mirror of
https://github.com/rr-/pq-cli.git
synced 2026-11-12 22:40:36 -07:00
pqw: import and export web saves
A .pqw file is what Progress Quest web's roster page downloads: base64 of one character's JSON. pqcli.pqw converts between that and Player, keeping the generator state, so a character carries on exactly as it would have where it came from. Saves from before 2022, which were not UTF-8, are read as Latin-1, and upstream's two spell renames are merged the way its loader does. Online realm fields are dropped with a warning. The upstream tests check both directions against the real JS: a save made by upstream, at a task boundary or mid-task, is imported and played on for 3000 tasks next to upstream; then the same the other way.
This commit is contained in:
5 files changed
+586
-4
No files matched your search
@@ -146,6 +146,10 @@ def play(
|
||||
data: GameData, spec: dict[str, Any], ticks: int, tasks: int
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Play pqcli the way the harness plays upstream: 100 ms ticks."""
|
||||
return run(new_simulation(data, spec), ticks, tasks)
|
||||
|
||||
|
||||
def new_simulation(data: GameData, spec: dict[str, Any]) -> Simulation:
|
||||
race = next(race for race in data.races if race.name == spec["race"])
|
||||
class_ = next(c for c in data.classes if c.name == spec["class"])
|
||||
seed = cast(RngState, tuple(spec["seed"]))
|
||||
@@ -159,6 +163,14 @@ def play(
|
||||
)
|
||||
simulation = Simulation(player, data)
|
||||
simulation.tick(0) # sets up the Prologue, as upstream's sold() does
|
||||
return simulation
|
||||
|
||||
|
||||
def run(
|
||||
simulation: Simulation, ticks: int, tasks: int
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Keep playing in 100 ms ticks, snapshotting after each task."""
|
||||
player = simulation.player
|
||||
snapshots = []
|
||||
for _ in range(ticks):
|
||||
completing = player.task_bar.done
|
||||
|
||||
@@ -114,8 +114,26 @@ function snapshot() {
|
||||
|
||||
function play(request) {
|
||||
newGame(request.spec);
|
||||
return run(request.ticks, request.tasks);
|
||||
}
|
||||
|
||||
// Play a game to some point, then return its save, as .pqw files hold it.
|
||||
function exportGame(request) {
|
||||
newGame(request.spec);
|
||||
run(request.ticks, request.tasks);
|
||||
SaveGame();
|
||||
return JSON.parse(store.roster)[request.spec.name];
|
||||
}
|
||||
|
||||
// Load a save and keep playing it.
|
||||
function resume(request) {
|
||||
LoadGame(JSON.parse(JSON.stringify(request.game)));
|
||||
return run(request.ticks, request.tasks);
|
||||
}
|
||||
|
||||
function run(ticks, tasks) {
|
||||
const snapshots = [];
|
||||
for (let i = 0; i < request.ticks; i++) {
|
||||
for (let i = 0; i < ticks; i++) {
|
||||
const completing = TaskBar.done();
|
||||
now += 100;
|
||||
clock.lasttick = now - 100;
|
||||
@@ -123,7 +141,7 @@ function play(request) {
|
||||
if (completing) {
|
||||
// copy, since game's lists keep changing after this tick
|
||||
snapshots.push(JSON.parse(JSON.stringify(snapshot())));
|
||||
if (snapshots.length >= request.tasks) break;
|
||||
if (snapshots.length >= tasks) break;
|
||||
}
|
||||
}
|
||||
return snapshots;
|
||||
@@ -148,7 +166,24 @@ function lists() {
|
||||
return result;
|
||||
}
|
||||
|
||||
const handlers = { play: play, call: call, lists: lists };
|
||||
// The roster page's .pqw encoding, from roster.js.
|
||||
function encode(request) {
|
||||
return btoa(unescape(encodeURIComponent(JSON.stringify(request.game))));
|
||||
}
|
||||
|
||||
function decode(request) {
|
||||
return JSON.parse(decodeURIComponent(escape(atob(request.text))));
|
||||
}
|
||||
|
||||
const handlers = {
|
||||
encode: encode,
|
||||
decode: decode,
|
||||
play: play,
|
||||
export: exportGame,
|
||||
resume: resume,
|
||||
call: call,
|
||||
lists: lists,
|
||||
};
|
||||
const requests = JSON.parse(fs.readFileSync(0, "utf8"));
|
||||
const responses = requests.map((request) => handlers[request.cmd](request));
|
||||
process.stdout.write(JSON.stringify(responses));
|
||||
Reference in new issue
Block a user