Add deterministic record/playback for a run. Recording captures `(seed, config hash, ordered tick-tagged commands)` and re-simulates on playback — no state snapshots. `DotaFactory.exe --replay <file>` re-plays a recorded run view-only with manual speed/pause. Reviewed-on: #4 Co-authored-by: Malte Langkabel <malte.langkabel@gmail.com> Co-committed-by: Malte Langkabel <malte.langkabel@gmail.com>
315 lines
9.4 KiB
C++
315 lines
9.4 KiB
C++
#include "CommandSerializer.h"
|
|
|
|
#include <sstream>
|
|
#include <vector>
|
|
|
|
#include "BuildingType.h"
|
|
#include "Command.h"
|
|
#include "Rotation.h"
|
|
#include "ShipLayout.h"
|
|
|
|
namespace
|
|
{
|
|
char rotationToChar(Rotation rotation)
|
|
{
|
|
switch (rotation)
|
|
{
|
|
case Rotation::North: return 'N';
|
|
case Rotation::East: return 'E';
|
|
case Rotation::South: return 'S';
|
|
case Rotation::West: return 'W';
|
|
}
|
|
return 'E';
|
|
}
|
|
|
|
Rotation rotationFromString(const std::string& token)
|
|
{
|
|
if (token == "N") { return Rotation::North; }
|
|
if (token == "S") { return Rotation::South; }
|
|
if (token == "W") { return Rotation::West; }
|
|
return Rotation::East;
|
|
}
|
|
|
|
// Reads "<count> (<moduleId> <x> <y> <rot>)*" from the stream. Sets ok=false on
|
|
// a stream failure.
|
|
ShipLayoutConfig parseLayout(std::istringstream& in, bool& ok)
|
|
{
|
|
ShipLayoutConfig layout;
|
|
int count = 0;
|
|
if (!(in >> count) || count < 0) { ok = false; return layout; }
|
|
for (int i = 0; i < count; ++i)
|
|
{
|
|
PlacedModule placed;
|
|
std::string rotToken;
|
|
int x = 0;
|
|
int y = 0;
|
|
if (!(in >> placed.moduleId >> x >> y >> rotToken)) { ok = false; return layout; }
|
|
placed.position = QPoint(x, y);
|
|
placed.rotation = rotationFromString(rotToken);
|
|
layout.placedModules.push_back(placed);
|
|
}
|
|
return layout;
|
|
}
|
|
|
|
// Reads "<countA> (<itemId>)* <countB> (<itemId>)*" from the stream.
|
|
void parseFilters(std::istringstream& in,
|
|
std::vector<ItemType>& filterA,
|
|
std::vector<ItemType>& filterB,
|
|
bool& ok)
|
|
{
|
|
int countA = 0;
|
|
if (!(in >> countA) || countA < 0) { ok = false; return; }
|
|
for (int i = 0; i < countA; ++i)
|
|
{
|
|
std::string id;
|
|
if (!(in >> id)) { ok = false; return; }
|
|
filterA.push_back(ItemType{id});
|
|
}
|
|
int countB = 0;
|
|
if (!(in >> countB) || countB < 0) { ok = false; return; }
|
|
for (int i = 0; i < countB; ++i)
|
|
{
|
|
std::string id;
|
|
if (!(in >> id)) { ok = false; return; }
|
|
filterB.push_back(ItemType{id});
|
|
}
|
|
}
|
|
|
|
// "<count> (<moduleId> <x> <y> <rot>)*"
|
|
void appendLayout(std::ostringstream& out, const ShipLayoutConfig& layout)
|
|
{
|
|
out << layout.placedModules.size();
|
|
for (const PlacedModule& placed : layout.placedModules)
|
|
{
|
|
out << ' ' << placed.moduleId
|
|
<< ' ' << placed.position.x()
|
|
<< ' ' << placed.position.y()
|
|
<< ' ' << rotationToChar(placed.rotation);
|
|
}
|
|
}
|
|
|
|
// "<countA> (<itemId>)* <countB> (<itemId>)*"
|
|
void appendFilters(std::ostringstream& out,
|
|
const std::vector<ItemType>& filterA,
|
|
const std::vector<ItemType>& filterB)
|
|
{
|
|
out << filterA.size();
|
|
for (const ItemType& type : filterA) { out << ' ' << type.id; }
|
|
out << ' ' << filterB.size();
|
|
for (const ItemType& type : filterB) { out << ' ' << type.id; }
|
|
}
|
|
} // namespace
|
|
|
|
std::string serializeCommand(const Command& command)
|
|
{
|
|
std::ostringstream out;
|
|
|
|
switch (command.kind)
|
|
{
|
|
case CommandKind::PlaceBuilding:
|
|
{
|
|
const PlaceBuildingCommand& c = static_cast<const PlaceBuildingCommand&>(command);
|
|
out << "place " << buildingTypeId(c.type)
|
|
<< ' ' << c.anchor.x() << ' ' << c.anchor.y()
|
|
<< ' ' << rotationToChar(c.rotation);
|
|
if (c.recipeId.has_value())
|
|
{
|
|
out << " recipe " << *c.recipeId;
|
|
}
|
|
if (c.shipLayout.has_value())
|
|
{
|
|
out << " layout ";
|
|
appendLayout(out, *c.shipLayout);
|
|
}
|
|
if (c.hasSplitterFilters)
|
|
{
|
|
out << " filters ";
|
|
appendFilters(out, c.splitterFilterA, c.splitterFilterB);
|
|
}
|
|
break;
|
|
}
|
|
case CommandKind::Demolish:
|
|
out << "demolish " << static_cast<const DemolishCommand&>(command).id;
|
|
break;
|
|
case CommandKind::RotateInPlace:
|
|
{
|
|
const RotateInPlaceCommand& c = static_cast<const RotateInPlaceCommand&>(command);
|
|
out << "rotate " << c.id << ' ' << rotationToChar(c.newRotation);
|
|
break;
|
|
}
|
|
case CommandKind::SetRecipe:
|
|
{
|
|
const SetRecipeCommand& c = static_cast<const SetRecipeCommand&>(command);
|
|
out << "setrecipe " << c.id << ' ' << c.recipeId;
|
|
break;
|
|
}
|
|
case CommandKind::SetShipLayout:
|
|
{
|
|
const SetShipLayoutCommand& c = static_cast<const SetShipLayoutCommand&>(command);
|
|
out << "setlayout " << c.id << ' ';
|
|
appendLayout(out, c.layout);
|
|
break;
|
|
}
|
|
case CommandKind::SetSiteSplitterFilters:
|
|
{
|
|
const SetSiteSplitterFiltersCommand& c =
|
|
static_cast<const SetSiteSplitterFiltersCommand&>(command);
|
|
out << "sitefilters " << c.id << ' ';
|
|
appendFilters(out, c.filterA, c.filterB);
|
|
break;
|
|
}
|
|
case CommandKind::SetSplitterFilters:
|
|
{
|
|
const SetSplitterFiltersCommand& c =
|
|
static_cast<const SetSplitterFiltersCommand&>(command);
|
|
out << "splitterfilters " << c.tile.x() << ' ' << c.tile.y() << ' ';
|
|
appendFilters(out, c.filterA, c.filterB);
|
|
break;
|
|
}
|
|
case CommandKind::ClearBeltTiles:
|
|
{
|
|
const ClearBeltTilesCommand& c = static_cast<const ClearBeltTilesCommand&>(command);
|
|
out << "clearbelt " << c.tiles.size();
|
|
for (const QPoint& tile : c.tiles)
|
|
{
|
|
out << ' ' << tile.x() << ' ' << tile.y();
|
|
}
|
|
break;
|
|
}
|
|
case CommandKind::ApplySchematicChoice:
|
|
out << "schematic " << static_cast<const ApplySchematicChoiceCommand&>(command).choiceIndex;
|
|
break;
|
|
case CommandKind::Reset:
|
|
// A reset rolls the replay file; it is never written as a stream entry.
|
|
break;
|
|
}
|
|
|
|
return out.str();
|
|
}
|
|
|
|
std::shared_ptr<Command> parseCommand(const std::string& tokens)
|
|
{
|
|
std::istringstream in(tokens);
|
|
std::string verb;
|
|
if (!(in >> verb))
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
bool ok = true;
|
|
|
|
if (verb == "place")
|
|
{
|
|
std::shared_ptr<PlaceBuildingCommand> c = std::make_shared<PlaceBuildingCommand>();
|
|
std::string typeToken;
|
|
std::string rotToken;
|
|
int x = 0;
|
|
int y = 0;
|
|
if (!(in >> typeToken >> x >> y >> rotToken)) { return nullptr; }
|
|
const std::optional<BuildingType> type = parseBuildingType(typeToken);
|
|
if (!type.has_value()) { return nullptr; }
|
|
c->type = *type;
|
|
c->anchor = QPoint(x, y);
|
|
c->rotation = rotationFromString(rotToken);
|
|
|
|
std::string segment;
|
|
while (in >> segment)
|
|
{
|
|
if (segment == "recipe")
|
|
{
|
|
std::string id;
|
|
if (!(in >> id)) { return nullptr; }
|
|
c->recipeId = id;
|
|
}
|
|
else if (segment == "layout")
|
|
{
|
|
c->shipLayout = parseLayout(in, ok);
|
|
if (!ok) { return nullptr; }
|
|
}
|
|
else if (segment == "filters")
|
|
{
|
|
parseFilters(in, c->splitterFilterA, c->splitterFilterB, ok);
|
|
if (!ok) { return nullptr; }
|
|
c->hasSplitterFilters = true;
|
|
}
|
|
else
|
|
{
|
|
return nullptr;
|
|
}
|
|
}
|
|
return c;
|
|
}
|
|
if (verb == "demolish")
|
|
{
|
|
std::shared_ptr<DemolishCommand> c = std::make_shared<DemolishCommand>();
|
|
if (!(in >> c->id)) { return nullptr; }
|
|
return c;
|
|
}
|
|
if (verb == "rotate")
|
|
{
|
|
std::shared_ptr<RotateInPlaceCommand> c = std::make_shared<RotateInPlaceCommand>();
|
|
std::string rotToken;
|
|
if (!(in >> c->id >> rotToken)) { return nullptr; }
|
|
c->newRotation = rotationFromString(rotToken);
|
|
return c;
|
|
}
|
|
if (verb == "setrecipe")
|
|
{
|
|
std::shared_ptr<SetRecipeCommand> c = std::make_shared<SetRecipeCommand>();
|
|
if (!(in >> c->id >> c->recipeId)) { return nullptr; }
|
|
return c;
|
|
}
|
|
if (verb == "setlayout")
|
|
{
|
|
std::shared_ptr<SetShipLayoutCommand> c = std::make_shared<SetShipLayoutCommand>();
|
|
if (!(in >> c->id)) { return nullptr; }
|
|
c->layout = parseLayout(in, ok);
|
|
if (!ok) { return nullptr; }
|
|
return c;
|
|
}
|
|
if (verb == "sitefilters")
|
|
{
|
|
std::shared_ptr<SetSiteSplitterFiltersCommand> c =
|
|
std::make_shared<SetSiteSplitterFiltersCommand>();
|
|
if (!(in >> c->id)) { return nullptr; }
|
|
parseFilters(in, c->filterA, c->filterB, ok);
|
|
if (!ok) { return nullptr; }
|
|
return c;
|
|
}
|
|
if (verb == "splitterfilters")
|
|
{
|
|
std::shared_ptr<SetSplitterFiltersCommand> c =
|
|
std::make_shared<SetSplitterFiltersCommand>();
|
|
int x = 0;
|
|
int y = 0;
|
|
if (!(in >> x >> y)) { return nullptr; }
|
|
c->tile = QPoint(x, y);
|
|
parseFilters(in, c->filterA, c->filterB, ok);
|
|
if (!ok) { return nullptr; }
|
|
return c;
|
|
}
|
|
if (verb == "clearbelt")
|
|
{
|
|
std::shared_ptr<ClearBeltTilesCommand> c = std::make_shared<ClearBeltTilesCommand>();
|
|
int count = 0;
|
|
if (!(in >> count) || count < 0) { return nullptr; }
|
|
for (int i = 0; i < count; ++i)
|
|
{
|
|
int x = 0;
|
|
int y = 0;
|
|
if (!(in >> x >> y)) { return nullptr; }
|
|
c->tiles.push_back(QPoint(x, y));
|
|
}
|
|
return c;
|
|
}
|
|
if (verb == "schematic")
|
|
{
|
|
std::shared_ptr<ApplySchematicChoiceCommand> c =
|
|
std::make_shared<ApplySchematicChoiceCommand>();
|
|
if (!(in >> c->choiceIndex)) { return nullptr; }
|
|
return c;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|