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>
This commit was merged in pull request #4.
This commit is contained in:
314
src/lib/sim/CommandSerializer.cpp
Normal file
314
src/lib/sim/CommandSerializer.cpp
Normal file
@@ -0,0 +1,314 @@
|
||||
#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;
|
||||
}
|
||||
Reference in New Issue
Block a user