Files
dota_factory/src/lib/sim/CommandSerializer.cpp
Malte Langkabel d74ba5bfad Replay: deterministic record & playback (#4)
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>
2026-07-01 19:20:08 +00:00

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;
}