From fdd0bfea08da7e3827424882e7a06e4d5189d4ab Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sun, 28 Sep 2025 10:15:14 -0700 Subject: [PATCH] rewrite quest metadata indexing - split ep3 download quests from quest index - fix Ep3 NTE download quests - automatically detect battle/challenge params and area remaps --- CMakeLists.txt | 1 + README.md | 2 +- src/ChatCommands.cc | 24 +- src/Client.cc | 8 +- src/Episode3/DataIndexes.cc | 51 +- src/Episode3/DataIndexes.hh | 25 +- src/Episode3/Server.cc | 8 +- src/Episode3/Tournament.cc | 2 +- src/HTTPServer.cc | 7 +- src/HTTPServer.hh | 2 +- src/IntegralExpression.hh | 1 - src/Lobby.cc | 10 +- src/Lobby.hh | 2 + src/Main.cc | 24 +- src/Map.cc | 12 +- src/Map.hh | 3 +- src/Menu.hh | 1 + src/Quest.cc | 557 ++--- src/Quest.hh | 49 +- src/QuestMetadata.cc | 164 ++ src/QuestMetadata.hh | 52 + src/QuestScript.cc | 1805 +++++++++++------ src/QuestScript.hh | 22 +- src/ReceiveCommands.cc | 185 +- src/ReceiveSubcommands.cc | 21 +- src/SendCommands.cc | 60 +- src/SendCommands.hh | 7 +- src/ServerState.cc | 24 +- src/ServerState.hh | 5 +- src/ShellCommands.cc | 2 +- system/config.example.json | 9 +- .../{download-ep3-trial => }/e765-gc3-e.mnm | Bin .../{download-ep3-trial => }/e765-gc3-j.mnm | Bin .../{download-ep3 => }/e901-gc3-e.mnm | Bin .../{download-ep3 => }/e901-gc3-j.mnm | Bin .../{download-ep3 => }/e903-gc3-e.mnm | Bin .../{download-ep3 => }/e903-gc3-j.mnm | Bin .../{download-ep3 => }/e904-gc3-e.mnm | Bin .../{download-ep3 => }/e904-gc3-j.mnm | Bin .../{download-ep3 => }/e905-gc3-e.mnm | Bin .../{download-ep3 => }/e905-gc3-j.mnm | Bin .../{download-ep3 => }/e906-gc3-e.mnm | Bin .../{download-ep3 => }/e906-gc3-j.mnm | Bin .../{download-ep3 => }/e907-gc3-e.mnm | Bin .../{download-ep3 => }/e907-gc3-j.mnm | Bin .../{download-ep3 => }/e908-gc3-e.mnm | Bin .../{download-ep3 => }/e908-gc3-j.mnm | Bin system/ep3/maps/e765-gc3-e.mnm | 2 +- system/ep3/maps/e765-gc3-j.mnm | 2 +- system/ep3/maps/e901-gc3-e.mnm | 2 +- system/ep3/maps/e901-gc3-j.mnm | 2 +- system/ep3/maps/e903-gc3-e.mnm | 2 +- system/ep3/maps/e903-gc3-j.mnm | 2 +- system/ep3/maps/e904-gc3-e.mnm | 2 +- system/ep3/maps/e904-gc3-j.mnm | 2 +- system/ep3/maps/e905-gc3-e.mnm | 2 +- system/ep3/maps/e905-gc3-j.mnm | 2 +- system/ep3/maps/e906-gc3-e.mnm | 2 +- system/ep3/maps/e906-gc3-j.mnm | 2 +- system/ep3/maps/e907-gc3-e.mnm | 2 +- system/ep3/maps/e907-gc3-j.mnm | 2 +- system/ep3/maps/e908-gc3-e.mnm | 2 +- system/ep3/maps/e908-gc3-j.mnm | 2 +- system/ep3/maps/map000001F4-e.bin | Bin system/ep3/maps/map000001F5-e.bin | Bin system/ep3/maps/map000001F6-e.bin | Bin system/ep3/maps/map000001F7-e.bin | Bin system/ep3/maps/map000001F8-e.bin | Bin system/ep3/maps/map00000230-e.bin | Bin system/ep3/maps/map00000231-e.bin | Bin system/ep3/maps/map00000244-e.bin | Bin system/ep3/maps/map00000245-e.bin | Bin system/ep3/maps/map00000246-e.bin | Bin system/ep3/maps/map00000258-e.bin | Bin system/ep3/maps/map00000259-e.bin | Bin system/ep3/maps/map0000026C-e.bin | Bin system/ep3/maps/map0000026D-e.bin | Bin system/ep3/maps/map0000026E-e.bin | Bin system/ep3/maps/map0000026F-e.bin | Bin system/ep3/maps/map00000280-e.bin | Bin system/ep3/maps/map00000281-e.bin | Bin system/ep3/maps/map00000282-e.bin | Bin system/ep3/maps/map00000294-e.bin | Bin system/ep3/maps/map00000295-e.bin | Bin system/ep3/maps/map00000296-e.bin | Bin system/ep3/maps/map000002A8-e.bin | Bin system/ep3/maps/map000002A9-e.bin | Bin system/ep3/maps/map000002BC-e.bin | Bin system/ep3/maps/map000002BD-e.bin | Bin system/ep3/maps/map000002E4-e.bin | Bin system/ep3/maps/map000002E5-e.bin | Bin system/ep3/maps/map000002F8-e.bin | Bin system/ep3/maps/map000002F9-e.bin | Bin system/ep3/maps/map000002FA-e.bin | Bin system/ep3/maps/map00000320-e.bin | Bin system/ep3/maps/map00000321-e.bin | Bin system/ep3/maps/map00000334-e.bin | Bin system/ep3/maps/map00000335-e.bin | Bin system/ep3/maps/map0000033E-e.bin | Bin system/ep3/maps/map0000033F-e.bin | Bin system/ep3/maps/map00000340-e.bin | Bin system/ep3/maps/map00000341-e.bin | Bin system/ep3/maps/map00000342-e.bin | Bin system/ep3/maps/map00000348-e.bin | Bin system/ep3/maps/map00000349-e.bin | Bin system/ep3/maps/map00000352-e.bin | Bin system/ep3/maps/map00000353-e.bin | Bin system/ep3/maps/map00000354-e.bin | Bin system/ep3/maps/map0000035C-e.bin | Bin system/ep3/maps/map0000035D-e.bin | Bin system/ep3/maps/map0000035E-e.bin | Bin system/ep3/maps/map0000035F-e.bin | Bin system/ep3/maps/map00000360-e.bin | Bin system/ep3/maps/map00000370-e.bin | Bin system/ep3/maps/map00000371-e.bin | Bin system/ep3/maps/map00000372-e.bin | Bin system/ep3/maps/map00000398-e.bin | Bin system/ep3/maps/map00000399-e.bin | Bin system/ep3/maps/map0000039A-e.bin | Bin system/ep3/maps/map0000039B-e.bin | Bin system/ep3/maps/map0000039C-e.bin | Bin system/ep3/maps/map000003B6-e.bin | Bin system/ep3/maps/map000003B7-e.bin | Bin system/ep3/maps/map000003B8-e.bin | Bin system/ep3/maps/map000003B9-e.bin | Bin system/ep3/maps/map000003BA-e.bin | Bin system/ep3/maps/map000003BB-e.bin | Bin system/ep3/maps/map000003BC-e.bin | Bin system/ep3/maps/map000003BD-e.bin | Bin system/ep3/maps/map000003BE-e.bin | Bin system/ep3/maps/map000003BF-e.bin | Bin system/ep3/maps/map000003C0-e.bin | Bin system/ep3/maps/map000003C1-e.bin | Bin system/ep3/maps/map000003C2-e.bin | Bin system/ep3/maps/map000003C3-e.bin | Bin system/ep3/maps/map000003C4-e.bin | Bin system/ep3/maps/map000003C5-e.bin | Bin system/ep3/maps/map000003C6-e.bin | Bin system/ep3/maps/map000003C7-e.bin | Bin system/ep3/maps/map000003C8-e.bin | Bin system/ep3/maps/map000003C9-e.bin | Bin system/ep3/maps/map000003CA-e.bin | Bin system/ep3/maps/map000003CB-e.bin | Bin system/quests/battle/b88002.json | 25 - system/quests/battle/b88003.json | 25 - system/quests/battle/b88004.json | 26 - system/quests/battle/b88005.json | 23 - system/quests/battle/b88006.json | 25 - system/quests/battle/b88007.json | 26 - system/quests/battle/b88008.json | 26 - system/quests/challenge-ep1/c88101.json | 1 - system/quests/challenge-ep1/c88102.json | 1 - system/quests/challenge-ep1/c88103.json | 1 - system/quests/challenge-ep1/c88104.json | 1 - system/quests/challenge-ep1/c88105.json | 1 - system/quests/challenge-ep1/c88106.json | 1 - system/quests/challenge-ep1/c88107.json | 1 - system/quests/challenge-ep1/c88108.json | 1 - system/quests/challenge-ep1/c88109.json | 1 - system/quests/challenge-ep2/d88201.json | 3 +- system/quests/challenge-ep2/d88202.json | 1 - system/quests/challenge-ep2/d88203.json | 1 - system/quests/challenge-ep2/d88204-gc-e.bin | Bin 1482 -> 1483 bytes system/quests/challenge-ep2/d88204.json | 1 - system/quests/challenge-ep2/d88205.json | 1 - system/quests/challenge-solo-ep1/c8811.json | 1 - system/quests/challenge-solo-ep1/c8812.json | 1 - system/quests/challenge-solo-ep1/c8813.json | 1 - system/quests/challenge-solo-ep1/c8814.json | 1 - system/quests/challenge-solo-ep1/c8815.json | 1 - system/quests/challenge-solo-ep1/c8816.json | 1 - system/quests/challenge-solo-ep1/c8817.json | 1 - system/quests/challenge-solo-ep1/c8818.json | 1 - system/quests/challenge-solo-ep1/c8819.json | 1 - system/quests/challenge-solo-ep2/d8821.json | 1 - system/quests/challenge-solo-ep2/d8822.json | 1 - system/quests/challenge-solo-ep2/d8823.json | 1 - system/quests/challenge-solo-ep2/d8824.json | 1 - system/quests/challenge-solo-ep2/d8825.json | 1 - .../government-console-ep1/q154-xb-e.bin | 1 - .../quests/government-console-ep1/q154-xb.dat | 1 - .../government-console-ep1/q155-xb-e.bin | 1 - .../quests/government-console-ep1/q155-xb.dat | 1 - .../government-console-ep1/q156-xb-e.bin | 1 - .../quests/government-console-ep1/q156-xb.dat | 1 - .../government-console-ep1/q158-xb-e.bin | 1 - .../quests/government-console-ep1/q158-xb.dat | 1 - .../government-console-ep1/q159-xb-e.bin | 1 - .../quests/government-console-ep1/q159-xb.dat | 1 - .../government-console-ep1/q161-xb-e.bin | 1 - .../quests/government-console-ep1/q161-xb.dat | 1 - .../government-console-ep1/q162-xb-e.bin | 1 - .../quests/government-console-ep1/q162-xb.dat | 1 - .../government-console-ep1/q163-xb-e.bin | 1 - .../quests/government-console-ep1/q163-xb.dat | 1 - .../government-console-ep1/q164-xb-e.bin | 1 - .../quests/government-console-ep1/q164-xb.dat | 1 - .../{q154-gc-e.bin => q180-gc-e.bin} | Bin .../{q154-gc.dat => q180-gc.dat} | Bin .../government-console-ep1/q180-xb-e.bin | 1 + .../quests/government-console-ep1/q180-xb.dat | 1 + .../quests/government-console-ep1/q180.json | 4 + .../{q155-gc-e.bin => q181-gc-e.bin} | Bin .../{q155-gc.dat => q181-gc.dat} | Bin .../government-console-ep1/q181-xb-e.bin | 1 + .../quests/government-console-ep1/q181-xb.dat | 1 + .../quests/government-console-ep1/q181.json | 4 + .../{q156-gc-e.bin => q182-gc-e.bin} | Bin .../{q156-gc.dat => q182-gc.dat} | Bin .../government-console-ep1/q182-xb-e.bin | 1 + .../quests/government-console-ep1/q182-xb.dat | 1 + .../quests/government-console-ep1/q182.json | 3 + .../{q158-gc-e.bin => q183-gc-e.bin} | Bin .../{q158-gc.dat => q183-gc.dat} | Bin .../government-console-ep1/q183-xb-e.bin | 1 + .../quests/government-console-ep1/q183-xb.dat | 1 + .../quests/government-console-ep1/q183.json | 4 + .../{q159-gc-e.bin => q184-gc-e.bin} | Bin .../{q159-gc.dat => q184-gc.dat} | Bin .../government-console-ep1/q184-xb-e.bin | 1 + .../quests/government-console-ep1/q184-xb.dat | 1 + .../quests/government-console-ep1/q184.json | 3 + .../{q161-gc-e.bin => q185-gc-e.bin} | Bin .../{q161-gc.dat => q185-gc.dat} | Bin .../government-console-ep1/q185-xb-e.bin | 1 + .../quests/government-console-ep1/q185-xb.dat | 1 + .../quests/government-console-ep1/q185.json | 3 + .../{q162-gc-e.bin => q186-gc-e.bin} | Bin .../{q162-gc.dat => q186-gc.dat} | Bin .../government-console-ep1/q186-xb-e.bin | 1 + .../quests/government-console-ep1/q186-xb.dat | 1 + .../quests/government-console-ep1/q186.json | 3 + .../{q163-gc-e.bin => q187-gc-e.bin} | Bin .../{q163-gc.dat => q187-gc.dat} | Bin .../government-console-ep1/q187-xb-e.bin | 1 + .../quests/government-console-ep1/q187-xb.dat | 1 + .../quests/government-console-ep1/q187.json | 4 + .../{q164-gc-e.bin => q188-gc-e.bin} | Bin .../{q164-gc.dat => q188-gc.dat} | Bin .../government-console-ep1/q188-xb-e.bin | 1 + .../quests/government-console-ep1/q188-xb.dat | 1 + .../quests/government-console-ep1/q188.json | 4 + ...8532-gc-e.bin.txt => q88532-gc3-e.bin.txt} | 0 .../hidden/{q88532-gc.dat => q88532-gc3.dat} | Bin ...8533-gc-e.bin.txt => q88533-gc3-e.bin.txt} | 0 .../hidden/{q88533-gc.dat => q88533-gc3.dat} | Bin .../b88001.json => retrieval/q058.json} | 34 +- tests/config.json | 2 - 248 files changed, 1944 insertions(+), 1543 deletions(-) create mode 100644 src/QuestMetadata.cc create mode 100644 src/QuestMetadata.hh rename system/ep3/maps-download/{download-ep3-trial => }/e765-gc3-e.mnm (100%) rename system/ep3/maps-download/{download-ep3-trial => }/e765-gc3-j.mnm (100%) rename system/ep3/maps-download/{download-ep3 => }/e901-gc3-e.mnm (100%) rename system/ep3/maps-download/{download-ep3 => }/e901-gc3-j.mnm (100%) rename system/ep3/maps-download/{download-ep3 => }/e903-gc3-e.mnm (100%) rename system/ep3/maps-download/{download-ep3 => }/e903-gc3-j.mnm (100%) rename system/ep3/maps-download/{download-ep3 => }/e904-gc3-e.mnm (100%) rename system/ep3/maps-download/{download-ep3 => }/e904-gc3-j.mnm (100%) rename system/ep3/maps-download/{download-ep3 => }/e905-gc3-e.mnm (100%) rename system/ep3/maps-download/{download-ep3 => }/e905-gc3-j.mnm (100%) rename system/ep3/maps-download/{download-ep3 => }/e906-gc3-e.mnm (100%) rename system/ep3/maps-download/{download-ep3 => }/e906-gc3-j.mnm (100%) rename system/ep3/maps-download/{download-ep3 => }/e907-gc3-e.mnm (100%) rename system/ep3/maps-download/{download-ep3 => }/e907-gc3-j.mnm (100%) rename system/ep3/maps-download/{download-ep3 => }/e908-gc3-e.mnm (100%) rename system/ep3/maps-download/{download-ep3 => }/e908-gc3-j.mnm (100%) mode change 100755 => 100644 system/ep3/maps/map000001F4-e.bin mode change 100755 => 100644 system/ep3/maps/map000001F5-e.bin mode change 100755 => 100644 system/ep3/maps/map000001F6-e.bin mode change 100755 => 100644 system/ep3/maps/map000001F7-e.bin mode change 100755 => 100644 system/ep3/maps/map000001F8-e.bin mode change 100755 => 100644 system/ep3/maps/map00000230-e.bin mode change 100755 => 100644 system/ep3/maps/map00000231-e.bin mode change 100755 => 100644 system/ep3/maps/map00000244-e.bin mode change 100755 => 100644 system/ep3/maps/map00000245-e.bin mode change 100755 => 100644 system/ep3/maps/map00000246-e.bin mode change 100755 => 100644 system/ep3/maps/map00000258-e.bin mode change 100755 => 100644 system/ep3/maps/map00000259-e.bin mode change 100755 => 100644 system/ep3/maps/map0000026C-e.bin mode change 100755 => 100644 system/ep3/maps/map0000026D-e.bin mode change 100755 => 100644 system/ep3/maps/map0000026E-e.bin mode change 100755 => 100644 system/ep3/maps/map0000026F-e.bin mode change 100755 => 100644 system/ep3/maps/map00000280-e.bin mode change 100755 => 100644 system/ep3/maps/map00000281-e.bin mode change 100755 => 100644 system/ep3/maps/map00000282-e.bin mode change 100755 => 100644 system/ep3/maps/map00000294-e.bin mode change 100755 => 100644 system/ep3/maps/map00000295-e.bin mode change 100755 => 100644 system/ep3/maps/map00000296-e.bin mode change 100755 => 100644 system/ep3/maps/map000002A8-e.bin mode change 100755 => 100644 system/ep3/maps/map000002A9-e.bin mode change 100755 => 100644 system/ep3/maps/map000002BC-e.bin mode change 100755 => 100644 system/ep3/maps/map000002BD-e.bin mode change 100755 => 100644 system/ep3/maps/map000002E4-e.bin mode change 100755 => 100644 system/ep3/maps/map000002E5-e.bin mode change 100755 => 100644 system/ep3/maps/map000002F8-e.bin mode change 100755 => 100644 system/ep3/maps/map000002F9-e.bin mode change 100755 => 100644 system/ep3/maps/map000002FA-e.bin mode change 100755 => 100644 system/ep3/maps/map00000320-e.bin mode change 100755 => 100644 system/ep3/maps/map00000321-e.bin mode change 100755 => 100644 system/ep3/maps/map00000334-e.bin mode change 100755 => 100644 system/ep3/maps/map00000335-e.bin mode change 100755 => 100644 system/ep3/maps/map0000033E-e.bin mode change 100755 => 100644 system/ep3/maps/map0000033F-e.bin mode change 100755 => 100644 system/ep3/maps/map00000340-e.bin mode change 100755 => 100644 system/ep3/maps/map00000341-e.bin mode change 100755 => 100644 system/ep3/maps/map00000342-e.bin mode change 100755 => 100644 system/ep3/maps/map00000348-e.bin mode change 100755 => 100644 system/ep3/maps/map00000349-e.bin mode change 100755 => 100644 system/ep3/maps/map00000352-e.bin mode change 100755 => 100644 system/ep3/maps/map00000353-e.bin mode change 100755 => 100644 system/ep3/maps/map00000354-e.bin mode change 100755 => 100644 system/ep3/maps/map0000035C-e.bin mode change 100755 => 100644 system/ep3/maps/map0000035D-e.bin mode change 100755 => 100644 system/ep3/maps/map0000035E-e.bin mode change 100755 => 100644 system/ep3/maps/map0000035F-e.bin mode change 100755 => 100644 system/ep3/maps/map00000360-e.bin mode change 100755 => 100644 system/ep3/maps/map00000370-e.bin mode change 100755 => 100644 system/ep3/maps/map00000371-e.bin mode change 100755 => 100644 system/ep3/maps/map00000372-e.bin mode change 100755 => 100644 system/ep3/maps/map00000398-e.bin mode change 100755 => 100644 system/ep3/maps/map00000399-e.bin mode change 100755 => 100644 system/ep3/maps/map0000039A-e.bin mode change 100755 => 100644 system/ep3/maps/map0000039B-e.bin mode change 100755 => 100644 system/ep3/maps/map0000039C-e.bin mode change 100755 => 100644 system/ep3/maps/map000003B6-e.bin mode change 100755 => 100644 system/ep3/maps/map000003B7-e.bin mode change 100755 => 100644 system/ep3/maps/map000003B8-e.bin mode change 100755 => 100644 system/ep3/maps/map000003B9-e.bin mode change 100755 => 100644 system/ep3/maps/map000003BA-e.bin mode change 100755 => 100644 system/ep3/maps/map000003BB-e.bin mode change 100755 => 100644 system/ep3/maps/map000003BC-e.bin mode change 100755 => 100644 system/ep3/maps/map000003BD-e.bin mode change 100755 => 100644 system/ep3/maps/map000003BE-e.bin mode change 100755 => 100644 system/ep3/maps/map000003BF-e.bin mode change 100755 => 100644 system/ep3/maps/map000003C0-e.bin mode change 100755 => 100644 system/ep3/maps/map000003C1-e.bin mode change 100755 => 100644 system/ep3/maps/map000003C2-e.bin mode change 100755 => 100644 system/ep3/maps/map000003C3-e.bin mode change 100755 => 100644 system/ep3/maps/map000003C4-e.bin mode change 100755 => 100644 system/ep3/maps/map000003C5-e.bin mode change 100755 => 100644 system/ep3/maps/map000003C6-e.bin mode change 100755 => 100644 system/ep3/maps/map000003C7-e.bin mode change 100755 => 100644 system/ep3/maps/map000003C8-e.bin mode change 100755 => 100644 system/ep3/maps/map000003C9-e.bin mode change 100755 => 100644 system/ep3/maps/map000003CA-e.bin mode change 100755 => 100644 system/ep3/maps/map000003CB-e.bin delete mode 100644 system/quests/battle/b88002.json delete mode 100644 system/quests/battle/b88003.json delete mode 100644 system/quests/battle/b88004.json delete mode 100644 system/quests/battle/b88005.json delete mode 100644 system/quests/battle/b88006.json delete mode 100644 system/quests/battle/b88007.json delete mode 100644 system/quests/battle/b88008.json delete mode 120000 system/quests/government-console-ep1/q154-xb-e.bin delete mode 120000 system/quests/government-console-ep1/q154-xb.dat delete mode 120000 system/quests/government-console-ep1/q155-xb-e.bin delete mode 120000 system/quests/government-console-ep1/q155-xb.dat delete mode 120000 system/quests/government-console-ep1/q156-xb-e.bin delete mode 120000 system/quests/government-console-ep1/q156-xb.dat delete mode 120000 system/quests/government-console-ep1/q158-xb-e.bin delete mode 120000 system/quests/government-console-ep1/q158-xb.dat delete mode 120000 system/quests/government-console-ep1/q159-xb-e.bin delete mode 120000 system/quests/government-console-ep1/q159-xb.dat delete mode 120000 system/quests/government-console-ep1/q161-xb-e.bin delete mode 120000 system/quests/government-console-ep1/q161-xb.dat delete mode 120000 system/quests/government-console-ep1/q162-xb-e.bin delete mode 120000 system/quests/government-console-ep1/q162-xb.dat delete mode 120000 system/quests/government-console-ep1/q163-xb-e.bin delete mode 120000 system/quests/government-console-ep1/q163-xb.dat delete mode 120000 system/quests/government-console-ep1/q164-xb-e.bin delete mode 120000 system/quests/government-console-ep1/q164-xb.dat rename system/quests/government-console-ep1/{q154-gc-e.bin => q180-gc-e.bin} (100%) rename system/quests/government-console-ep1/{q154-gc.dat => q180-gc.dat} (100%) create mode 120000 system/quests/government-console-ep1/q180-xb-e.bin create mode 120000 system/quests/government-console-ep1/q180-xb.dat create mode 100644 system/quests/government-console-ep1/q180.json rename system/quests/government-console-ep1/{q155-gc-e.bin => q181-gc-e.bin} (100%) rename system/quests/government-console-ep1/{q155-gc.dat => q181-gc.dat} (100%) create mode 120000 system/quests/government-console-ep1/q181-xb-e.bin create mode 120000 system/quests/government-console-ep1/q181-xb.dat create mode 100644 system/quests/government-console-ep1/q181.json rename system/quests/government-console-ep1/{q156-gc-e.bin => q182-gc-e.bin} (100%) rename system/quests/government-console-ep1/{q156-gc.dat => q182-gc.dat} (100%) create mode 120000 system/quests/government-console-ep1/q182-xb-e.bin create mode 120000 system/quests/government-console-ep1/q182-xb.dat create mode 100644 system/quests/government-console-ep1/q182.json rename system/quests/government-console-ep1/{q158-gc-e.bin => q183-gc-e.bin} (100%) rename system/quests/government-console-ep1/{q158-gc.dat => q183-gc.dat} (100%) create mode 120000 system/quests/government-console-ep1/q183-xb-e.bin create mode 120000 system/quests/government-console-ep1/q183-xb.dat create mode 100644 system/quests/government-console-ep1/q183.json rename system/quests/government-console-ep1/{q159-gc-e.bin => q184-gc-e.bin} (100%) rename system/quests/government-console-ep1/{q159-gc.dat => q184-gc.dat} (100%) create mode 120000 system/quests/government-console-ep1/q184-xb-e.bin create mode 120000 system/quests/government-console-ep1/q184-xb.dat create mode 100644 system/quests/government-console-ep1/q184.json rename system/quests/government-console-ep1/{q161-gc-e.bin => q185-gc-e.bin} (100%) rename system/quests/government-console-ep1/{q161-gc.dat => q185-gc.dat} (100%) create mode 120000 system/quests/government-console-ep1/q185-xb-e.bin create mode 120000 system/quests/government-console-ep1/q185-xb.dat create mode 100644 system/quests/government-console-ep1/q185.json rename system/quests/government-console-ep1/{q162-gc-e.bin => q186-gc-e.bin} (100%) rename system/quests/government-console-ep1/{q162-gc.dat => q186-gc.dat} (100%) create mode 120000 system/quests/government-console-ep1/q186-xb-e.bin create mode 120000 system/quests/government-console-ep1/q186-xb.dat create mode 100644 system/quests/government-console-ep1/q186.json rename system/quests/government-console-ep1/{q163-gc-e.bin => q187-gc-e.bin} (100%) rename system/quests/government-console-ep1/{q163-gc.dat => q187-gc.dat} (100%) create mode 120000 system/quests/government-console-ep1/q187-xb-e.bin create mode 120000 system/quests/government-console-ep1/q187-xb.dat create mode 100644 system/quests/government-console-ep1/q187.json rename system/quests/government-console-ep1/{q164-gc-e.bin => q188-gc-e.bin} (100%) rename system/quests/government-console-ep1/{q164-gc.dat => q188-gc.dat} (100%) create mode 120000 system/quests/government-console-ep1/q188-xb-e.bin create mode 120000 system/quests/government-console-ep1/q188-xb.dat create mode 100644 system/quests/government-console-ep1/q188.json rename system/quests/hidden/{q88532-gc-e.bin.txt => q88532-gc3-e.bin.txt} (100%) rename system/quests/hidden/{q88532-gc.dat => q88532-gc3.dat} (100%) rename system/quests/hidden/{q88533-gc-e.bin.txt => q88533-gc3-e.bin.txt} (100%) rename system/quests/hidden/{q88533-gc.dat => q88533-gc3.dat} (100%) rename system/quests/{battle/b88001.json => retrieval/q058.json} (78%) diff --git a/CMakeLists.txt b/CMakeLists.txt index e430b6bb..9035fc5e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -111,6 +111,7 @@ set(SOURCES src/PSOGCObjectGraph.cc src/PSOProtocol.cc src/Quest.cc + src/QuestMetadata.cc src/QuestScript.cc src/RareItemSet.cc src/ReceiveCommands.cc diff --git a/README.md b/README.md index e4aee9d0..722bfe8c 100644 --- a/README.md +++ b/README.md @@ -318,7 +318,7 @@ For .dat files, the `LANGUAGE` token may be omitted. If it's present, then that For example, the GameCube version of Lost HEAT SWORD is in two files named `q058-gc-e.bin` and `q058-gc.dat`. newserv knows these files are quests because they're in the system/quests/ directory, it knows they're for PSO GC because the filenames contain `-gc`, it knows this is the English version of the quest because the .bin filename ends with `-e` (even though the .dat filename does not), and it puts them in the Retrieval category because the files are within the retrieval/ directory within system/quests/. -Some quests have additional JSON metadata files that describe how the server should handle them. These metadata files are generally named similarly to their .bin and .dat counterparts, except the `VERSION` token may also be omitted if the metadata applies to all languages of the quest on all PSO versions. See the comments in system/quests/battle/b88001.json for all of the available options and how to use them. Some of the options are: +Some quests have additional JSON metadata files that describe how the server should handle them. These metadata files are generally named similarly to their .bin and .dat counterparts, except the `VERSION` token may also be omitted if the metadata applies to all languages of the quest on all PSO versions. See the comments in system/quests/retrieval/q058.json for all of the available options and how to use them. Some of the options are: - Disable or hide the quest if certain preceding quests aren't cleared or other conditions aren't met - Enable the quest to be joined while in progress - Override the common and/or rare item tables and set the allowed drop modes diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index 85755bc6..fc8f47d6 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -2074,8 +2074,7 @@ ChatCommandDefinition cc_quest( a.check_is_game(true); auto s = a.c->require_server_state(); - Version effective_version = is_ep3(a.c->version()) ? Version::GC_V3 : a.c->version(); - auto q = s->quest_index(effective_version)->get(stoul(a.text)); + auto q = s->quest_index->get(stoul(a.text)); if (!q) { throw precondition_failed("$C6Quest not found"); } @@ -2085,11 +2084,20 @@ ChatCommandDefinition cc_quest( if (l->count_clients() > 1) { throw precondition_failed("$C6This command can only\nbe used with no\nother players present"); } - if (!q->allow_start_from_chat_command) { + if (!q->meta.allow_start_from_chat_command) { throw precondition_failed("$C6This quest cannot\nbe started with the\n%squest command"); } } + for (size_t client_id = 0; client_id < l->max_clients; client_id++) { + auto lc = l->clients[client_id]; + if (lc) { + if (!q->version(lc->version(), lc->language())) { + throw precondition_failed("$C6Quest does not exist\nfor all players\' game\nversions"); + } + } + } + set_lobby_quest(a.c->require_lobby(), q, true); co_return; }); @@ -2365,8 +2373,9 @@ ChatCommandDefinition cc_sound( bool echo_to_all = (!a.text.empty() && a.text[0] == '!'); uint32_t sound_id = stoul(echo_to_all ? a.text.substr(1) : a.text, nullptr, 16); - // TODO: Using floor is technically incorrect here; it should be area - G_PlaySoundFromPlayer_6xB2 cmd = {{0xB2, 0x03, 0x0000}, static_cast(a.c->floor), 0x00, a.c->lobby_client_id, sound_id}; + auto l = a.c->require_lobby(); + uint8_t area = l->area_for_floor(a.c->version(), a.c->floor); + G_PlaySoundFromPlayer_6xB2 cmd = {{0xB2, 0x03, 0x0000}, area, 0x00, a.c->lobby_client_id, sound_id}; if (!echo_to_all) { send_command_t(a.c, 0x60, 0x00, cmd); } else if (a.c->proxy_session) { @@ -2812,13 +2821,10 @@ static void whatobj_whatene_fn(const Args& a, bool include_objs, bool include_en throw precondition_failed("$C4No map loaded"); } - // TODO: We should use the actual area if a loaded quest has reassigned - // them; it's likely that the variations will be wrong if we don't uint8_t area, layout_var; auto s = a.c->require_server_state(); if (l->episode != Episode::EP3) { - auto sdt = s->set_data_table(a.c->version(), l->episode, l->mode, l->difficulty); - area = sdt->default_area_for_floor(l->episode, a.c->floor); + area = l->area_for_floor(a.c->version(), a.c->floor); layout_var = (a.c->floor < 0x10) ? l->variations.entries[a.c->floor].layout.load() : 0x00; } else { area = a.c->floor; diff --git a/src/Client.cc b/src/Client.cc index fd4e7d3c..274c0b29 100644 --- a/src/Client.cc +++ b/src/Client.cc @@ -410,7 +410,8 @@ bool Client::can_see_quest( if (!q->has_version_any_language(this->version())) { return false; } - return this->evaluate_quest_availability_expression(q->available_expression, game, event, difficulty, num_players, v1_present); + return this->evaluate_quest_availability_expression( + q->meta.available_expression, game, event, difficulty, num_players, v1_present); } bool Client::can_play_quest( @@ -423,10 +424,11 @@ bool Client::can_play_quest( if (!q->has_version_any_language(this->version())) { return false; } - if (num_players > q->max_players) { + if (num_players > q->meta.max_players) { return false; } - return this->evaluate_quest_availability_expression(q->enabled_expression, game, event, difficulty, num_players, v1_present); + return this->evaluate_quest_availability_expression( + q->meta.enabled_expression, game, event, difficulty, num_players, v1_present); } bool Client::can_use_chat_commands() const { diff --git a/src/Episode3/DataIndexes.cc b/src/Episode3/DataIndexes.cc index 7427146b..9fb63249 100644 --- a/src/Episode3/DataIndexes.cc +++ b/src/Episode3/DataIndexes.cc @@ -2626,8 +2626,8 @@ MapIndex::VersionedMap::VersionedMap(shared_ptr map, uint8_ MapIndex::VersionedMap::VersionedMap(std::string&& compressed_data, uint8_t language) : language(language), - compressed_data(std::move(compressed_data)) { - string decompressed = prs_decompress(this->compressed_data); + compressed_data(make_shared(std::move(compressed_data))) { + string decompressed = prs_decompress(*this->compressed_data); if (decompressed.size() == sizeof(MapDefinitionTrial)) { this->map = make_shared(*reinterpret_cast(decompressed.data())); } else if (decompressed.size() == sizeof(MapDefinition)) { @@ -2646,21 +2646,30 @@ shared_ptr MapIndex::VersionedMap::trial() const { return this->trial_map; } -const std::string& MapIndex::VersionedMap::compressed(bool is_nte) const { - if (is_nte) { - if (this->compressed_trial_data.empty()) { +std::shared_ptr MapIndex::VersionedMap::compressed(bool trial) const { + if (trial) { + if (!this->compressed_data_trial) { auto md = this->trial(); - this->compressed_trial_data = prs_compress(md.get(), sizeof(*md)); + this->compressed_data_trial = make_shared(prs_compress(md.get(), sizeof(*md))); } - return this->compressed_trial_data; + return this->compressed_data_trial; } else { - if (this->compressed_data.empty()) { - this->compressed_data = prs_compress(this->map.get(), sizeof(*this->map)); + if (!this->compressed_data) { + this->compressed_data = make_shared(prs_compress(this->map.get(), sizeof(*this->map))); } return this->compressed_data; } } +std::shared_ptr MapIndex::VersionedMap::trial_download() const { + if (!this->download_data_trial) { + MapDefinitionTrial trial_map = *this->map; + trial_map.tag = 0x96; + this->download_data_trial = make_shared(prs_compress(&trial_map, sizeof(trial_map))); + } + return this->download_data_trial; +} + MapIndex::Map::Map(shared_ptr initial_version) : map_number(initial_version->map->map_number), initial_version(initial_version) { @@ -2704,6 +2713,7 @@ shared_ptr MapIndex::Map::version(uint8_t language } MapIndex::MapIndex(const string& directory) { + map> mutable_maps; for (const auto& item : std::filesystem::directory_iterator(directory)) { string filename = item.path().filename().string(); try { @@ -2756,9 +2766,10 @@ MapIndex::MapIndex(const string& directory) { } string name = vm->map->name.decode(vm->language); - auto map_it = this->maps.find(vm->map->map_number); - if (map_it == this->maps.end()) { - map_it = this->maps.emplace(vm->map->map_number, make_shared(vm)).first; + auto map_it = mutable_maps.find(vm->map->map_number); + if (map_it == mutable_maps.end()) { + map_it = mutable_maps.emplace(vm->map->map_number, make_shared(vm)).first; + this->maps.emplace(vm->map->map_number, map_it->second); static_game_data_log.debug_f("({}) Created Episode 3 map {:08X} {} ({}; {})", filename, vm->map->map_number, @@ -2866,22 +2877,6 @@ const string& MapIndex::get_compressed_list(size_t num_players, uint8_t language return compressed_map_list; } -shared_ptr MapIndex::for_number(uint32_t id) const { - return this->maps.at(id); -} - -shared_ptr MapIndex::for_name(const string& name) const { - return this->maps_by_name.at(name); -} - -set MapIndex::all_numbers() const { - set ret; - for (const auto& it : this->maps) { - ret.emplace(it.first); - } - return ret; -} - COMDeckIndex::COMDeckIndex(const string& filename) { try { auto json = phosg::JSON::parse(phosg::load_file(filename)); diff --git a/src/Episode3/DataIndexes.hh b/src/Episode3/DataIndexes.hh index 1a5feb6b..db6b8eec 100644 --- a/src/Episode3/DataIndexes.hh +++ b/src/Episode3/DataIndexes.hh @@ -1179,7 +1179,8 @@ struct OverlayState { struct MapDefinition { // .mnmd format; also the format of (decompressed) quests // If tag is not 0x00000100, the game considers the map to be corrupt in // offline mode and will delete it (if it's a download quest). The tag field - // doesn't seem to have any other use. + // doesn't seem to have any other use. In Trial Edition, download quests are + // expected to have 0x96 here instead. /* 0000 */ be_uint32_t tag; /* 0004 */ be_uint32_t map_number; // Must be unique across all maps @@ -1597,12 +1598,14 @@ public: VersionedMap(std::string&& compressed_data, uint8_t language); std::shared_ptr trial() const; - const std::string& compressed(bool is_nte) const; + std::shared_ptr compressed(bool trial) const; + std::shared_ptr trial_download() const; private: mutable std::shared_ptr trial_map; - mutable std::string compressed_data; - mutable std::string compressed_trial_data; + mutable std::shared_ptr compressed_data; + mutable std::shared_ptr compressed_data_trial; + mutable std::shared_ptr download_data_trial; }; class Map { @@ -1624,14 +1627,20 @@ public: }; const std::string& get_compressed_list(size_t num_players, uint8_t language) const; - std::shared_ptr for_number(uint32_t id) const; - std::shared_ptr for_name(const std::string& name) const; - std::set all_numbers() const; + inline std::shared_ptr get(uint32_t id) const { + return this->maps.at(id); + } + inline std::shared_ptr get(const std::string& name) const { + return this->maps_by_name.at(name); + } + inline const std::map>& all() const { + return this->maps; + } private: // The compressed map lists are generated on demand from the maps map below mutable std::vector> compressed_map_lists; - std::map> maps; + std::map> maps; std::unordered_map> maps_by_name; }; diff --git a/src/Episode3/Server.cc b/src/Episode3/Server.cc index 3b9069ea..d632cf8e 100644 --- a/src/Episode3/Server.cc +++ b/src/Episode3/Server.cc @@ -287,9 +287,9 @@ string Server::prepare_6xB6x41_map_definition(shared_ptr ma const auto& compressed = vm->compressed(is_nte); phosg::StringWriter w; - uint32_t subcommand_size = (compressed.size() + sizeof(G_MapData_Ep3_6xB6x41) + 3) & (~3); - w.put({{{{0xB6, 0, 0}, subcommand_size}, 0x41, {}}, vm->map->map_number.load(), compressed.size(), 0}); - w.write(compressed); + uint32_t subcommand_size = (compressed->size() + sizeof(G_MapData_Ep3_6xB6x41) + 3) & (~3); + w.put({{{{0xB6, 0, 0}, subcommand_size}, 0x41, {}}, vm->map->map_number.load(), compressed->size(), 0}); + w.write(*compressed); return std::move(w.str()); } @@ -2588,7 +2588,7 @@ void Server::handle_CAx41_map_request(shared_ptr, const string& data) { const auto& cmd = check_size_t(data); this->send_debug_command_received_message(cmd.header.subsubcommand, "MAP DATA"); if (!this->options.tournament || (this->options.tournament->get_map()->map_number == cmd.map_number)) { - this->last_chosen_map = this->options.map_index->for_number(cmd.map_number); + this->last_chosen_map = this->options.map_index->get(cmd.map_number); this->send_6xB6x41_to_all_clients(); } } diff --git a/src/Episode3/Tournament.cc b/src/Episode3/Tournament.cc index ed00e17c..cd619f83 100644 --- a/src/Episode3/Tournament.cc +++ b/src/Episode3/Tournament.cc @@ -357,7 +357,7 @@ void Tournament::init() { bool is_registration_complete; if (!this->source_json.is_null()) { this->name = this->source_json.get_string("name"); - this->map = this->map_index->for_number(this->source_json.get_int("map_number")); + this->map = this->map_index->get(this->source_json.get_int("map_number")); this->rules = Rules(this->source_json.at("rules")); this->flags = this->source_json.get_int("flags", 0x02); if (this->source_json.get_bool("is_2v2", false)) { diff --git a/src/HTTPServer.cc b/src/HTTPServer.cc index c9ab4d36..671e7158 100644 --- a/src/HTTPServer.cc +++ b/src/HTTPServer.cc @@ -717,10 +717,9 @@ asio::awaitable> HTTPServer::generate_rare_table_js } } -asio::awaitable> HTTPServer::generate_quest_list_json( - std::shared_ptr quest_index) { +asio::awaitable> HTTPServer::generate_quest_list_json() { co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> shared_ptr { - return make_shared(quest_index->json()); + return make_shared(this->state->quest_index->json()); }); } @@ -817,7 +816,7 @@ asio::awaitable> HTTPServer::handle_request(shared ret = co_await this->generate_rare_table_json(req.path.substr(20)); } else if (req.path == "/y/data/quests") { this->require_GET(req); - ret = co_await this->generate_quest_list_json(this->state->quest_index(Version::GC_V3)); + ret = co_await this->generate_quest_list_json(); } else if (req.path == "/y/data/config") { this->require_GET(req); ret = this->state->config_json; diff --git a/src/HTTPServer.hh b/src/HTTPServer.hh index 19bd32fe..e31a19e1 100644 --- a/src/HTTPServer.hh +++ b/src/HTTPServer.hh @@ -41,7 +41,7 @@ protected: std::shared_ptr generate_rare_table_list_json() const; asio::awaitable> generate_common_table_json(const std::string& table_name) const; asio::awaitable> generate_rare_table_json(const std::string& table_name) const; - asio::awaitable> generate_quest_list_json(std::shared_ptr q); + asio::awaitable> generate_quest_list_json(); void require_GET(const HTTPRequest& req); phosg::JSON require_POST(const HTTPRequest& req); diff --git a/src/IntegralExpression.hh b/src/IntegralExpression.hh index a4779a9d..553acc27 100644 --- a/src/IntegralExpression.hh +++ b/src/IntegralExpression.hh @@ -9,7 +9,6 @@ #include #include "PlayerSubordinates.hh" -#include "QuestScript.hh" #include "StaticGameData.hh" #include "TeamIndex.hh" diff --git a/src/Lobby.cc b/src/Lobby.cc index 96989b7b..43388ebb 100644 --- a/src/Lobby.cc +++ b/src/Lobby.cc @@ -186,6 +186,14 @@ void Lobby::reset_next_item_ids() { this->next_game_item_id = 0xCC000000; } +uint8_t Lobby::area_for_floor(Version version, uint8_t floor) const { + if (this->quest) { + return this->quest->meta.area_for_floor.at(floor); + } + auto sdt = this->require_server_state()->set_data_table(version, this->episode, this->mode, this->difficulty); + return sdt->default_area_for_floor(this->episode, floor); +} + shared_ptr Lobby::require_server_state() const { auto s = this->server_state.lock(); if (!s) { @@ -234,7 +242,7 @@ void Lobby::create_item_creator(Version logic_version) { this->difficulty, this->effective_section_id(), rand_crypt, - this->quest ? this->quest->battle_rules : nullptr); + this->quest ? this->quest->meta.battle_rules : nullptr); } uint8_t Lobby::effective_section_id() const { diff --git a/src/Lobby.hh b/src/Lobby.hh index b95a8a15..36bdcbe7 100644 --- a/src/Lobby.hh +++ b/src/Lobby.hh @@ -201,6 +201,8 @@ struct Lobby : public std::enable_shared_from_this { this->enabled_flags ^= static_cast(flag); } + uint8_t area_for_floor(Version version, uint8_t floor) const; + std::shared_ptr require_server_state() const; std::shared_ptr require_challenge_params() const; void create_item_creator(Version logic_version = Version::UNKNOWN); diff --git a/src/Main.cc b/src/Main.cc index 3f6f111a..e8d70b84 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -2801,10 +2801,9 @@ Action a_show_ep3_maps( s->load_ep3_cards(); s->load_ep3_maps(); - auto map_ids = s->ep3_map_index->all_numbers(); + const auto& map_ids = s->ep3_map_index->all(); phosg::log_info_f("{} maps", map_ids.size()); - for (uint32_t map_id : map_ids) { - auto map = s->ep3_map_index->for_number(map_id); + for (const auto& [map_number, map] : map_ids) { const auto& vms = map->all_versions(); for (size_t language = 0; language < vms.size(); language++) { if (!vms[language]) { @@ -2928,7 +2927,7 @@ Action a_check_supermaps( SuperMap::EfficiencyStats all_quests_eff; uint32_t random_seed = args.get("random-seed", 0, phosg::Arguments::IntFormat::HEX); - for (const auto& it : s->default_quest_index->quests_by_number) { + for (const auto& it : s->quest_index->quests_by_number) { auto supermap = it.second->get_supermap(random_seed); if (!supermap) { throw logic_error("quest does not have a supermap, even with a specified random seed"); @@ -2938,7 +2937,7 @@ Action a_check_supermaps( if (save_disassembly) { string filename = std::format("supermap_quest_{}_{:08X}.txt", it.first, random_seed); auto f = phosg::fopen_unique(filename, "wt"); - phosg::fwrite_fmt(f.get(), "QUEST {} ({})\n", it.first, it.second->name); + phosg::fwrite_fmt(f.get(), "QUEST {} ({})\n", it.first, it.second->meta.name); supermap->print(f.get()); filename_token = " => " + filename; } @@ -2949,7 +2948,7 @@ Action a_check_supermaps( } string filename = std::format("supermap_quest_{}_{:08X}_enemy_counts.txt", it.first, random_seed); auto f = phosg::fopen_unique(filename, "wt"); - phosg::fwrite_fmt(f.get(), "QUEST {} ({})\n", it.first, it.second->name); + phosg::fwrite_fmt(f.get(), "QUEST {} ({})\n", it.first, it.second->meta.name); phosg::fwrite_fmt(f.get(), "ENEMY--------------- DCNTE 11/2K DC-V1 DC-V2 PCNTE PC-V2 GCNTE GC-V3 XB-V3 BB-V4\n"); for (size_t type_ss = 0; type_ss < static_cast(EnemyType::MAX_ENEMY_TYPE); type_ss++) { EnemyType type = static_cast(type_ss); @@ -3005,6 +3004,19 @@ Action a_check_supermaps( phosg::fwrite_fmt(stderr, "ALL QUEST MAPS: {}\n", all_quests_eff_str); }); +Action a_check_quests( + "check-quests", nullptr, + +[](phosg::Arguments& args) { + auto s = make_shared(get_config_filename(args)); + s->is_debug = true; + s->load_config_early(); + s->clear_file_caches(); + s->load_patch_indexes(); + s->load_set_data_tables(); + s->load_maps(); + s->load_quest_index(); + }); + Action a_parse_object_graph( "parse-object-graph", nullptr, +[](phosg::Arguments& args) { uint32_t root_object_address = args.get("root", phosg::Arguments::IntFormat::HEX); diff --git a/src/Map.cc b/src/Map.cc index b4670beb..7785a918 100644 --- a/src/Map.cc +++ b/src/Map.cc @@ -67,7 +67,7 @@ vector SetDataTableBase::map_filenames_for_variations( return ret; } -uint8_t SetDataTableBase::default_area_for_floor(Episode episode, uint8_t floor) const { +uint8_t SetDataTableBase::default_area_for_floor(Version version, Episode episode, uint8_t floor) { // For some inscrutable reason, Pioneer 2's area number in Episode 4 is // discontiguous with all the rest. Why, Sega?? static const array areas_ep1 = { @@ -82,7 +82,7 @@ uint8_t SetDataTableBase::default_area_for_floor(Episode episode, uint8_t floor) case Episode::EP1: return areas_ep1.at(floor); case Episode::EP2: { - const auto& areas = ((this->version == Version::GC_NTE) ? areas_ep2_gc_nte : areas_ep2); + const auto& areas = ((version == Version::GC_NTE) ? areas_ep2_gc_nte : areas_ep2); return areas.at(floor); } case Episode::EP4: @@ -92,6 +92,10 @@ uint8_t SetDataTableBase::default_area_for_floor(Episode episode, uint8_t floor) } } +uint8_t SetDataTableBase::default_area_for_floor(Episode episode, uint8_t floor) const { + return this->default_area_for_floor(this->version, episode, floor); +} + SetDataTable::SetDataTable(Version version, const string& data) : SetDataTableBase(version) { if (is_big_endian(this->version)) { this->load_table_t(data); @@ -5833,7 +5837,7 @@ phosg::JSON MapState::RareEnemyRates::json() const { }); } -uint32_t MapState::RareEnemyRates::for_enemy_type(EnemyType type) const { +uint32_t MapState::RareEnemyRates::get(EnemyType type) const { switch (type) { case EnemyType::HILDEBEAR: return this->hildeblue; @@ -6071,7 +6075,7 @@ void MapState::index_super_map(const FloorConfig& fc, shared_ptrget_episode(), this->event, ene->floor); if ((type == EnemyType::MERICARAND) || (rare_type != type)) { unordered_map det_cache; - uint32_t bb_rare_rate = this->bb_rare_rates->for_enemy_type(type); + uint32_t bb_rare_rate = this->bb_rare_rates->get(type); for (Version v : ALL_NON_PATCH_VERSIONS) { // Skip this version if the enemy doesn't exist there uint16_t relative_enemy_index = ene->version(v).relative_enemy_index; diff --git a/src/Map.hh b/src/Map.hh index 6c9d9184..e592e40b 100644 --- a/src/Map.hh +++ b/src/Map.hh @@ -67,6 +67,7 @@ public: std::vector map_filenames_for_variations( Episode episode, GameMode mode, const Variations& variations, FilenameType type) const; + static uint8_t default_area_for_floor(Version version, Episode episode, uint8_t floor); uint8_t default_area_for_floor(Episode episode, uint8_t floor) const; protected: @@ -672,7 +673,7 @@ public: RareEnemyRates(uint32_t enemy_rate, uint32_t mericarand_rate, uint32_t boss_rate); explicit RareEnemyRates(const phosg::JSON& json); - uint32_t for_enemy_type(EnemyType type) const; + uint32_t get(EnemyType type) const; std::string str() const; phosg::JSON json() const; diff --git a/src/Menu.hh b/src/Menu.hh index ad13f24a..63416e6d 100644 --- a/src/Menu.hh +++ b/src/Menu.hh @@ -21,6 +21,7 @@ constexpr uint32_t LOBBY = 0x33000033; constexpr uint32_t GAME = 0x44000044; constexpr uint32_t QUEST_EP1 = 0x55010155; constexpr uint32_t QUEST_EP2 = 0x55020255; +constexpr uint32_t QUEST_EP3 = 0x55030355; // See the decsription of the A2 command in CommandFormats.hh for why these // menu IDs don't fit the rest of the pattern. constexpr uint32_t QUEST_CATEGORIES_EP1 = 0x01000001; diff --git a/src/Quest.cc b/src/Quest.cc index 874dffb6..09bf6a30 100644 --- a/src/Quest.cc +++ b/src/Quest.cc @@ -194,10 +194,10 @@ struct PSODownloadQuestHeader { } __packed_ws__(PSODownloadQuestHeader, 8); void VersionedQuest::assert_valid() const { - if (this->category_id == 0xFFFFFFFF) { + if (this->meta.category_id == 0xFFFFFFFF) { throw runtime_error("category ID is not set"); } - if (this->quest_number == 0xFFFFFFFF) { + if (this->meta.quest_number == 0xFFFFFFFF) { throw runtime_error("quest number is not set"); } if (this->version == Version::UNKNOWN) { @@ -206,96 +206,107 @@ void VersionedQuest::assert_valid() const { if (this->language == 0xFF) { throw runtime_error("language is not set"); } - if (this->episode == Episode::NONE) { - throw runtime_error("episode is not set"); + switch (this->meta.episode) { + case Episode::EP1: + for (size_t floor = 0; floor < this->meta.area_for_floor.size(); floor++) { + uint8_t area = this->meta.area_for_floor[floor]; + if (area >= 0x12) { + throw runtime_error("Episode 1 quest specifies invalid area"); + } + } + break; + case Episode::EP2: + if (is_v1_or_v2(this->version)) { + throw runtime_error("v1 or v2 quest specifies Episode 2"); + } + for (size_t floor = 0; floor < this->meta.area_for_floor.size(); floor++) { + uint8_t area = this->meta.area_for_floor[floor]; + if ((area < 0x12) || (area >= 0x24)) { + throw runtime_error("Episode 2 quest specifies invalid area"); + } + } + break; + case Episode::EP3: + if (!is_ep3(this->version)) { + throw runtime_error("non-Ep3 quest specifies Episode 3"); + } + for (size_t floor = 0; floor < this->meta.area_for_floor.size(); floor++) { + if (this->meta.area_for_floor[floor] != 0xFF) { + throw runtime_error("Episode 3 quest specifies floor overrides"); + } + } + break; + case Episode::EP4: + if (!is_v4(this->version)) { + throw runtime_error("non-v4 quest specifies Episode 4"); + } + for (size_t floor = 0; floor < this->meta.area_for_floor.size(); floor++) { + uint8_t area = this->meta.area_for_floor[floor]; + if (area != 0xFF && (area < 0x24 || area >= 0x2F)) { + throw runtime_error("Episode 4 quest specifies invalid floor"); + } + } + break; + case Episode::NONE: + throw runtime_error("episode is not set"); + default: + throw runtime_error("episode is not valid"); } - if (this->max_players == 0) { + if (this->meta.max_players == 0) { throw runtime_error("max players is not set"); } if (!this->bin_contents) { throw runtime_error("bin file is missing"); } - if (!is_ep3(this->version) && !this->dat_contents) { + if (!this->dat_contents) { throw runtime_error("dat file is missing"); } - if (!is_ep3(this->version) && !this->map_file) { + if (!this->map_file) { throw runtime_error("parsed map file is missing"); } - if (this->common_item_set_name.empty() != !this->common_item_set) { + if (this->meta.common_item_set_name.empty() != !this->meta.common_item_set) { throw runtime_error("common item set name/pointer mismatch"); } - if (this->rare_item_set_name.empty() != !this->rare_item_set) { + if (this->meta.rare_item_set_name.empty() != !this->meta.rare_item_set) { throw runtime_error("rare item set name/pointer mismatch"); } - if (this->allowed_drop_modes && !(this->allowed_drop_modes & (1 << static_cast(this->default_drop_mode)))) { + if (this->meta.allowed_drop_modes && + !(this->meta.allowed_drop_modes & (1 << static_cast(this->meta.default_drop_mode)))) { throw runtime_error("default drop mode is not allowed"); } } string VersionedQuest::bin_filename() const { - if (this->episode == Episode::EP3) { - return std::format("m{:06}p_e.bin", this->quest_number); - } else { - return std::format("quest{}.bin", this->quest_number); - } + return std::format("quest{}.bin", this->meta.quest_number); } string VersionedQuest::dat_filename() const { - if (this->episode == Episode::EP3) { - throw logic_error("Episode 3 quests do not have .dat files"); - } else { - return std::format("quest{}.dat", this->quest_number); - } + return std::format("quest{}.dat", this->meta.quest_number); } string VersionedQuest::pvr_filename() const { - if (this->episode == Episode::EP3) { - throw logic_error("Episode 3 quests do not have .pvr files"); - } else { - return std::format("quest{}.pvr", this->quest_number); - } + return std::format("quest{}.pvr", this->meta.quest_number); } string VersionedQuest::xb_filename() const { - if (this->episode == Episode::EP3) { - throw logic_error("Episode 3 quests do not have Xbox filenames"); - } else { - return std::format("quest{}_{}.dat", this->quest_number, static_cast(tolower(char_for_language_code(this->language)))); - } + return std::format("quest{}_{}.dat", + this->meta.quest_number, static_cast(tolower(char_for_language_code(this->language)))); } string VersionedQuest::encode_qst() const { unordered_map> files; - files.emplace(std::format("quest{}.bin", this->quest_number), this->bin_contents); - files.emplace(std::format("quest{}.dat", this->quest_number), this->dat_contents); + files.emplace(std::format("quest{}.bin", this->meta.quest_number), this->bin_contents); + files.emplace(std::format("quest{}.dat", this->meta.quest_number), this->dat_contents); if (this->pvr_contents) { - files.emplace(std::format("quest{}.pvr", this->quest_number), this->pvr_contents); + files.emplace(std::format("quest{}.pvr", this->meta.quest_number), this->pvr_contents); } - string xb_filename = std::format("quest{}_{}.dat", quest_number, static_cast(tolower(char_for_language_code(language)))); - return encode_qst_file(files, this->name, this->quest_number, xb_filename, this->version, this->is_dlq_encoded); + string xb_filename = std::format("quest{}_{}.dat", + this->meta.quest_number, static_cast(tolower(char_for_language_code(language)))); + return encode_qst_file(files, this->meta.name, this->meta.quest_number, xb_filename, this->version, this->is_dlq_encoded); } Quest::Quest(shared_ptr initial_version) - : quest_number(initial_version->quest_number), - category_id(initial_version->category_id), - episode(initial_version->episode), - allow_start_from_chat_command(initial_version->allow_start_from_chat_command), - joinable(initial_version->joinable), - max_players(initial_version->max_players), - lock_status_register(initial_version->lock_status_register), - name(initial_version->name), - supermap(nullptr), - battle_rules(initial_version->battle_rules), - challenge_template_index(initial_version->challenge_template_index), - description_flag(initial_version->description_flag), - available_expression(initial_version->available_expression), - enabled_expression(initial_version->enabled_expression), - common_item_set_name(initial_version->common_item_set_name), - rare_item_set_name(initial_version->rare_item_set_name), - common_item_set(initial_version->common_item_set), - rare_item_set(initial_version->rare_item_set), - allowed_drop_modes(initial_version->allowed_drop_modes), - default_drop_mode(initial_version->default_drop_mode) { + : meta(initial_version->meta), supermap(nullptr) { this->add_version(initial_version); } @@ -305,8 +316,9 @@ phosg::JSON Quest::json() const { versions_json.emplace_back(phosg::JSON::dict({ {"Version", phosg::name_for_enum(vq->version)}, {"Language", name_for_language_code(vq->language)}, - {"ShortDescription", vq->short_description}, - {"LongDescription", vq->long_description}, + {"Name", vq->meta.name}, + {"ShortDescription", vq->meta.short_description}, + {"LongDescription", vq->meta.long_description}, {"BINFileSize", vq->bin_contents ? vq->bin_contents->size() : phosg::JSON(nullptr)}, {"DATFileSize", vq->dat_contents ? vq->dat_contents->size() : phosg::JSON(nullptr)}, {"PVRFileSize", vq->pvr_contents ? vq->pvr_contents->size() : phosg::JSON(nullptr)}, @@ -314,23 +326,7 @@ phosg::JSON Quest::json() const { } return phosg::JSON::dict({ - {"Number", this->quest_number}, - {"CategoryID", this->category_id}, - {"Episode", name_for_episode(this->episode)}, - {"AllowStartFromChatCommand", this->allow_start_from_chat_command}, - {"Joinable", this->joinable}, - {"MaxPlayers", this->max_players}, - {"LockStatusRegister", (this->lock_status_register >= 0) ? this->lock_status_register : phosg::JSON(nullptr)}, - {"Name", this->name}, - {"BattleRules", this->battle_rules ? this->battle_rules->json() : phosg::JSON(nullptr)}, - {"ChallengeTemplateIndex", (this->challenge_template_index >= 0) ? this->challenge_template_index : phosg::JSON(nullptr)}, - {"DescriptionFlag", this->description_flag}, - {"AvailableExpression", this->available_expression ? this->available_expression->str() : phosg::JSON(nullptr)}, - {"EnabledExpression", this->available_expression ? this->available_expression->str() : phosg::JSON(nullptr)}, - {"CommonItemSetName", this->common_item_set_name.empty() ? phosg::JSON(nullptr) : this->common_item_set_name}, - {"RareItemSetName", this->rare_item_set_name.empty() ? phosg::JSON(nullptr) : this->rare_item_set_name}, - {"AllowedDropModes", this->allowed_drop_modes}, - {"DefaultDropMode", phosg::name_for_enum(this->default_drop_mode)}, + {"Metadata", this->meta.json()}, {"Versions", std::move(versions_json)}, }); } @@ -340,112 +336,7 @@ uint32_t Quest::versions_key(Version v, uint8_t language) { } void Quest::add_version(shared_ptr vq) { - if (this->quest_number != vq->quest_number) { - throw logic_error(std::format( - "incorrect versioned quest number (existing: {:08X}, new: {:08X})", - this->quest_number, vq->quest_number)); - } - if (this->category_id != vq->category_id) { - throw runtime_error(std::format( - "quest version is in a different category (existing: {:08X}, new: {:08X})", - this->category_id, vq->category_id)); - } - if (this->episode != vq->episode) { - throw runtime_error(std::format( - "quest version is in a different episode (existing: {}, new: {})", - name_for_episode(this->episode), name_for_episode(vq->episode))); - } - if (this->allow_start_from_chat_command != vq->allow_start_from_chat_command) { - throw runtime_error(std::format( - "quest version has a different allow_start_from_chat_command state (existing: {}, new: {})", - this->allow_start_from_chat_command ? "true" : "false", vq->allow_start_from_chat_command ? "true" : "false")); - } - if (this->joinable != vq->joinable) { - throw runtime_error(std::format( - "quest version has a different joinability state (existing: {}, new: {})", - this->joinable ? "true" : "false", vq->joinable ? "true" : "false")); - } - if (this->max_players != vq->max_players) { - throw runtime_error(std::format( - "quest version has a different maximum player count (existing: {}, new: {})", - this->max_players, vq->max_players)); - } - if (this->lock_status_register != vq->lock_status_register) { - throw runtime_error(std::format( - "quest version has a different lock status register (existing: {:04X}, new: {:04X})", - this->lock_status_register, vq->lock_status_register)); - } - if (!this->battle_rules != !vq->battle_rules) { - throw runtime_error(std::format( - "quest version has a different battle rules presence state (existing: {}, new: {})", - this->battle_rules ? "present" : "absent", vq->battle_rules ? "present" : "absent")); - } - if (this->battle_rules && (*this->battle_rules != *vq->battle_rules)) { - string existing_str = this->battle_rules->json().serialize(); - string new_str = vq->battle_rules->json().serialize(); - throw runtime_error(std::format( - "quest version has different battle rules (existing: {}, new: {})", - existing_str, new_str)); - } - if (this->challenge_template_index != vq->challenge_template_index) { - throw runtime_error(std::format( - "quest version has different challenge template index (existing: {}, new: {})", - this->challenge_template_index, vq->challenge_template_index)); - } - if (this->description_flag != vq->description_flag) { - throw runtime_error(std::format( - "quest version has different description flag (existing: {:02X}, new: {:02X})", - this->description_flag, vq->description_flag)); - } - if (!this->available_expression != !vq->available_expression) { - throw runtime_error(std::format( - "quest version has available expression but root quest does not, or vice versa (existing: {}, new: {})", - this->available_expression ? "present" : "absent", vq->available_expression ? "present" : "absent")); - } - if (this->available_expression && *this->available_expression != *vq->available_expression) { - string existing_str = this->available_expression->str(); - string new_str = vq->available_expression->str(); - throw runtime_error(std::format( - "quest version has a different available expression (existing: {}, new: {})", - existing_str, new_str)); - } - if (!this->enabled_expression != !vq->enabled_expression) { - throw runtime_error(std::format( - "quest version has enabled expression but root quest does not, or vice versa (existing: {}, new: {})", - this->enabled_expression ? "present" : "absent", vq->enabled_expression ? "present" : "absent")); - } - if (this->enabled_expression && *this->enabled_expression != *vq->enabled_expression) { - string existing_str = this->enabled_expression->str(); - string new_str = vq->enabled_expression->str(); - throw runtime_error(std::format( - "quest version has a different enabled expression (existing: {}, new: {})", - existing_str, new_str)); - } - if (this->common_item_set_name != vq->common_item_set_name) { - throw runtime_error(std::format( - "quest version has different common table name (existing: {}, new: {})", - this->common_item_set_name, vq->common_item_set_name)); - } - if (this->common_item_set != vq->common_item_set) { - throw runtime_error("quest version has different common table"); - } - if (this->rare_item_set_name != vq->rare_item_set_name) { - throw runtime_error(std::format( - "quest version has different rare table name (existing: {}, new: {})", - this->rare_item_set_name, vq->rare_item_set_name)); - } - if (this->rare_item_set != vq->rare_item_set) { - throw runtime_error("quest version has different rare table"); - } - if (this->allowed_drop_modes != vq->allowed_drop_modes) { - throw runtime_error(format("quest version has different allowed drop modes (existing: {:02X}, new: {:02X})", - this->allowed_drop_modes, vq->allowed_drop_modes)); - } - if (this->default_drop_mode != vq->default_drop_mode) { - throw runtime_error(format("quest version has different default drop mode (existing: {}, new: {})", - phosg::name_for_enum(this->default_drop_mode), phosg::name_for_enum(vq->default_drop_mode))); - } - + this->meta.assert_compatible(vq->meta); this->versions.emplace(this->versions_key(vq->version, vq->language), vq); } @@ -477,12 +368,12 @@ std::shared_ptr Quest::get_supermap(int64_t random_seed) const { return nullptr; } - auto supermap = make_shared(this->episode, map_files); + auto supermap = make_shared(this->meta.episode, map_files); if (save_to_cache) { this->supermap = supermap; } static_game_data_log.info_f("Constructed {} supermap for quest {} ({})", - save_to_cache ? "cacheable" : "temporary", this->quest_number, this->name); + save_to_cache ? "cacheable" : "temporary", this->meta.quest_number, this->meta.name); return supermap; } @@ -522,8 +413,7 @@ QuestIndex::QuestIndex( const string& directory, shared_ptr category_index, const unordered_map>& common_item_sets, - const unordered_map>& rare_item_sets, - bool is_ep3) + const unordered_map>& rare_item_sets) : directory(directory), category_index(category_index) { @@ -533,7 +423,7 @@ QuestIndex::QuestIndex( }; struct BINFileData { string filename; - unique_ptr metadata; + shared_ptr assembled; shared_ptr data; }; struct DATFileData { @@ -547,12 +437,6 @@ QuestIndex::QuestIndex( map json_files; map categories; for (const auto& cat : this->category_index->categories) { - // Don't index Ep3 download categories for non-Ep3 quest indexing, and vice - // versa - if (is_ep3 != cat->check_flag(QuestMenuType::EP3_DOWNLOAD)) { - continue; - } - auto add_file = [&](map& files, const string& basename, const string& filename, string&& value, bool check_chunk_size) { if (categories.emplace(basename, cat->category_id).first->second != cat->category_id) { throw runtime_error("file " + basename + " exists in multiple categories"); @@ -569,7 +453,7 @@ QuestIndex::QuestIndex( } }; - auto add_bin_file = [&](const string& basename, const string& filename, string&& data, const QuestMetadata* metadata) { + auto add_bin_file = [&](const string& basename, const string& filename, string&& data, shared_ptr assembled) { if (categories.emplace(basename, cat->category_id).first->second != cat->category_id) { throw runtime_error("bin file " + basename + " exists in multiple categories"); } @@ -581,9 +465,7 @@ QuestIndex::QuestIndex( auto& entry = emplace_ret.first->second; entry.filename = filename; entry.data = data_ptr; - if (metadata) { - entry.metadata = make_unique(*metadata); - } + entry.assembled = assembled; if (!(data_ptr->size() & 0x3FF)) { data_ptr->push_back(0x00); } @@ -614,7 +496,7 @@ QuestIndex::QuestIndex( } string file_path = cat_path + "/" + filename; - unique_ptr assembled; + shared_ptr assembled; try { string orig_filename = filename; string file_data; @@ -629,7 +511,7 @@ QuestIndex::QuestIndex( filename.resize(filename.size() - 4); } else if (filename.ends_with(".bin.txt")) { string include_dir = phosg::dirname(file_path); - assembled = make_unique(assemble_quest_script( + assembled = make_shared(assemble_quest_script( phosg::load_file(file_path), {include_dir, "system/quests/includes"}, {include_dir, "system/quests/includes", "system/client-functions/System"})); @@ -655,9 +537,9 @@ QuestIndex::QuestIndex( if (extension == "json") { add_file(json_files, file_basename, orig_filename, std::move(file_data), false); } else if (extension == "bin" || extension == "mnm") { - add_bin_file(file_basename, orig_filename, std::move(file_data), assembled ? &assembled->metadata : nullptr); + add_bin_file(file_basename, orig_filename, std::move(file_data), assembled); } else if (extension == "bind" || extension == "mnmd") { - add_bin_file(file_basename, orig_filename, prs_compress_optimal(file_data), assembled ? &assembled->metadata : nullptr); + add_bin_file(file_basename, orig_filename, prs_compress_optimal(file_data), assembled); } else if (extension == "dat") { add_dat_file(file_basename, orig_filename, std::move(file_data)); } else if (extension == "datd") { @@ -710,28 +592,18 @@ QuestIndex::QuestIndex( version_token = std::move(filename_tokens[1]); language_token = std::move(filename_tokens[2]); } - vq->category_id = categories.at(basename); - - // Find the quest's metadata. If the quest was assembled (that is, if it - // came from a .bin.txt file), use the metadata from the source file; - // otherwise, figure it out from the already-assembled code - if (entry.metadata) { - vq->quest_number = entry.metadata->quest_number; - vq->version = ::is_ep3(entry.metadata->version) ? Version::GC_V3 : entry.metadata->version; - vq->language = entry.metadata->language; - vq->episode = entry.metadata->episode; - vq->joinable = entry.metadata->joinable; - vq->max_players = entry.metadata->max_players; - vq->name = entry.metadata->name; - vq->short_description = entry.metadata->short_description; - vq->long_description = entry.metadata->long_description; + vq->meta.category_id = categories.at(basename); + if (entry.assembled) { + vq->meta.quest_number = entry.assembled->quest_number; + vq->version = entry.assembled->version; + vq->language = entry.assembled->language; } else { // Get the number from the first token if (quest_number_token.empty()) { throw runtime_error("quest number token is missing"); } - vq->quest_number = strtoull(quest_number_token.c_str() + 1, nullptr, 10); + vq->meta.quest_number = strtoull(quest_number_token.c_str() + 1, nullptr, 10); // Get the version from the second token static const unordered_map name_to_version({ @@ -755,147 +627,45 @@ QuestIndex::QuestIndex( throw runtime_error("language token is not a single character"); } vq->language = language_code_for_char(language_token[0]); - - auto bin_decompressed = prs_decompress(*entry.data); - switch (vq->version) { - case Version::DC_NTE: { - if (bin_decompressed.size() < sizeof(PSOQuestHeaderDCNTE)) { - throw invalid_argument("file is too small for header"); - } - auto* header = reinterpret_cast(bin_decompressed.data()); - vq->episode = Episode::EP1; - vq->max_players = 4; - vq->name = header->name.decode(vq->language); - if (vq->quest_number == 0xFFFFFFFF) { - vq->quest_number = phosg::fnv1a64(vq->name); - } - break; - } - - case Version::DC_11_2000: - case Version::DC_V1: - case Version::DC_V2: { - if (bin_decompressed.size() < sizeof(PSOQuestHeaderDC)) { - throw invalid_argument("file is too small for header"); - } - auto* header = reinterpret_cast(bin_decompressed.data()); - vq->episode = Episode::EP1; - vq->max_players = 4; - if (vq->quest_number == 0xFFFFFFFF) { - vq->quest_number = header->quest_number; - } - vq->name = header->name.decode(vq->language); - vq->short_description = header->short_description.decode(vq->language); - vq->long_description = header->long_description.decode(vq->language); - break; - } - - case Version::PC_NTE: - case Version::PC_V2: { - if (bin_decompressed.size() < sizeof(PSOQuestHeaderPC)) { - throw invalid_argument("file is too small for header"); - } - auto* header = reinterpret_cast(bin_decompressed.data()); - vq->episode = Episode::EP1; - vq->max_players = 4; - if (vq->quest_number == 0xFFFFFFFF) { - vq->quest_number = header->quest_number; - } - vq->name = header->name.decode(vq->language); - vq->short_description = header->short_description.decode(vq->language); - vq->long_description = header->long_description.decode(vq->language); - break; - } - - case Version::GC_EP3_NTE: - case Version::GC_EP3: { - // Note: This codepath handles Episode 3 download quests, which are not - // the same as Episode 3 quest scripts. The latter are only used offline - // in story mode, but can be disassembled with disassemble_quest_script. - // It's unfortunate that Version::GC_EP3 is used here for Episode 3 - // download quests (maps) and there for offline story mode scripts, but - // it's probably not worth refactoring this logic, at least right now. - if (bin_decompressed.size() != sizeof(Episode3::MapDefinition)) { - throw invalid_argument("file is incorrect size"); - } - auto* map = reinterpret_cast(bin_decompressed.data()); - vq->episode = Episode::EP3; - vq->max_players = 4; - if (vq->quest_number == 0xFFFFFFFF) { - vq->quest_number = map->map_number; - } - vq->name = map->name.decode(vq->language); - vq->short_description = map->quest_name.decode(vq->language); - vq->long_description = map->description.decode(vq->language); - break; - } - - case Version::XB_V3: - case Version::GC_NTE: - case Version::GC_V3: { - if (bin_decompressed.size() < sizeof(PSOQuestHeaderGC)) { - throw invalid_argument("file is too small for header"); - } - auto* header = reinterpret_cast(bin_decompressed.data()); - vq->episode = find_quest_episode_from_script( - bin_decompressed.data(), bin_decompressed.size(), vq->version); - vq->max_players = 4; - if (vq->quest_number == 0xFFFFFFFF) { - vq->quest_number = header->quest_number; - } - vq->name = header->name.decode(vq->language); - vq->short_description = header->short_description.decode(vq->language); - vq->long_description = header->long_description.decode(vq->language); - break; - } - - case Version::BB_V4: { - if (bin_decompressed.size() < sizeof(PSOQuestHeaderBB)) { - throw invalid_argument("file is too small for header"); - } - auto* header = reinterpret_cast(bin_decompressed.data()); - vq->episode = find_quest_episode_from_script( - bin_decompressed.data(), bin_decompressed.size(), vq->version); - vq->joinable |= header->joinable; - vq->max_players = 4; - if (vq->quest_number == 0xFFFFFFFF) { - vq->quest_number = header->quest_number; - } - vq->name = header->name.decode(vq->language); - vq->short_description = header->short_description.decode(vq->language); - vq->long_description = header->long_description.decode(vq->language); - break; - } - - default: - throw logic_error("invalid quest game version"); - } } - // Find the corresponding dat and pvr files + auto bin_decompressed = prs_decompress(*entry.data); + populate_quest_metadata_from_script(vq->meta, bin_decompressed.data(), bin_decompressed.size(), vq->version, vq->language); + + // If the quest was assembled (that is, if it came from a .bin.txt file), + // the metadata from the source file overrides any automatically-detected + // values from above + if (entry.assembled) { + vq->meta.quest_number = entry.assembled->quest_number; + vq->meta.episode = entry.assembled->episode; + vq->meta.joinable = entry.assembled->joinable; + vq->meta.max_players = entry.assembled->max_players; + vq->meta.name = entry.assembled->name; + vq->meta.short_description = entry.assembled->short_description; + vq->meta.long_description = entry.assembled->long_description; + } + + // Find the corresponding dat and pvr files with the same basename as the + // bin file; if not found, look for them without the language suffix const DATFileData* dat_filedata = nullptr; const FileData* pvr_filedata = nullptr; - if (!::is_ep3(vq->version)) { - // Look for dat and pvr files with the same basename as the bin file; if - // not found, look for them without the language suffix + try { + dat_filedata = &dat_files.at(basename); + } catch (const out_of_range&) { try { - dat_filedata = &dat_files.at(basename); + dat_filedata = &dat_files.at(quest_number_token + "-" + version_token); } catch (const out_of_range&) { - try { - dat_filedata = &dat_files.at(quest_number_token + "-" + version_token); - } catch (const out_of_range&) { - throw runtime_error("no dat file found for bin file " + basename); - } + throw runtime_error("no dat file found for bin file " + basename); } + } + try { + pvr_filedata = &pvr_files.at(basename); + } catch (const out_of_range&) { try { - pvr_filedata = &pvr_files.at(basename); + pvr_filedata = &pvr_files.at(quest_number_token + "-" + version_token); } catch (const out_of_range&) { - try { - pvr_filedata = &pvr_files.at(quest_number_token + "-" + version_token); - } catch (const out_of_range&) { - // pvr files aren't required (and most quests do not have them), so - // don't fail if it's missing - } + // pvr files aren't required (and most quests do not have them), so + // don't fail if it's missing } } vq->bin_contents = entry.data; @@ -924,64 +694,56 @@ QuestIndex::QuestIndex( if (json_filedata) { auto metadata_json = phosg::JSON::parse(*json_filedata->data); try { - vq->battle_rules = make_shared(metadata_json.at("BattleRules")); + vq->meta.description_flag = metadata_json.at("DescriptionFlag").as_int(); } catch (const out_of_range&) { } try { - vq->challenge_template_index = metadata_json.at("ChallengeTemplateIndex").as_int(); + vq->meta.available_expression = make_shared(metadata_json.get_string("AvailableIf")); } catch (const out_of_range&) { } try { - vq->description_flag = metadata_json.at("DescriptionFlag").as_int(); + vq->meta.enabled_expression = make_shared(metadata_json.get_string("EnabledIf")); } catch (const out_of_range&) { } try { - vq->available_expression = make_shared(metadata_json.get_string("AvailableIf")); + vq->meta.allow_start_from_chat_command = metadata_json.get_bool("AllowStartFromChatCommand"); } catch (const out_of_range&) { } try { - vq->enabled_expression = make_shared(metadata_json.get_string("EnabledIf")); + vq->meta.joinable = metadata_json.get_bool("Joinable"); } catch (const out_of_range&) { } try { - vq->allow_start_from_chat_command = metadata_json.get_bool("AllowStartFromChatCommand"); + vq->meta.lock_status_register = metadata_json.get_int("LockStatusRegister"); } catch (const out_of_range&) { } try { - vq->joinable = metadata_json.get_bool("Joinable"); + vq->meta.common_item_set_name = metadata_json.at("CommonItemSetName").as_string(); + } catch (const out_of_range&) { + } + if (!vq->meta.common_item_set_name.empty()) { + vq->meta.common_item_set = common_item_sets.at(vq->meta.common_item_set_name); + } + try { + vq->meta.rare_item_set_name = metadata_json.at("RareItemSetName").as_string(); + } catch (const out_of_range&) { + } + if (!vq->meta.rare_item_set_name.empty()) { + vq->meta.rare_item_set = rare_item_sets.at(vq->meta.rare_item_set_name); + } + try { + vq->meta.allowed_drop_modes = metadata_json.at("AllowedDropModes").as_int(); } catch (const out_of_range&) { } try { - vq->lock_status_register = metadata_json.get_int("LockStatusRegister"); - } catch (const out_of_range&) { - } - try { - vq->common_item_set_name = metadata_json.at("CommonItemSetName").as_string(); - } catch (const out_of_range&) { - } - if (!vq->common_item_set_name.empty()) { - vq->common_item_set = common_item_sets.at(vq->common_item_set_name); - } - try { - vq->rare_item_set_name = metadata_json.at("RareItemSetName").as_string(); - } catch (const out_of_range&) { - } - if (!vq->rare_item_set_name.empty()) { - vq->rare_item_set = rare_item_sets.at(vq->rare_item_set_name); - } - try { - vq->allowed_drop_modes = metadata_json.at("AllowedDropModes").as_int(); - } catch (const out_of_range&) { - } - try { - vq->default_drop_mode = phosg::enum_for_name(metadata_json.at("DefaultDropMode").as_string()); + vq->meta.default_drop_mode = phosg::enum_for_name(metadata_json.at("DefaultDropMode").as_string()); } catch (const out_of_range&) { } } vq->assert_valid(); - auto category_name = this->category_index->at(vq->category_id)->name; + auto category_name = this->category_index->at(vq->meta.category_id)->name; string filenames_str = entry.filename; if (dat_filedata) { filenames_str += std::format("/{}", dat_filedata->filename); @@ -992,30 +754,32 @@ QuestIndex::QuestIndex( if (json_filedata) { filenames_str += std::format("/{}", json_filedata->filename); } - auto q_it = this->quests_by_number.find(vq->quest_number); + auto q_it = this->quests_by_number.find(vq->meta.quest_number); if (q_it != this->quests_by_number.end()) { q_it->second->add_version(vq); - static_game_data_log.debug_f("({}) Added {} {} version of quest {} ({})", + static_game_data_log.debug_f("({}) Added {} {} version of quest {} ({}) with floors {}", filenames_str, phosg::name_for_enum(vq->version), char_for_language_code(vq->language), - vq->quest_number, - vq->name); + vq->meta.quest_number, + vq->meta.name, + phosg::format_data_string(vq->meta.area_for_floor.data(), 0x12)); } else { auto q = make_shared(vq); - this->quests_by_number.emplace(vq->quest_number, q); - this->quests_by_name.emplace(vq->name, q); - this->quests_by_category_id_and_number[q->category_id].emplace(vq->quest_number, q); - static_game_data_log.debug_f("({}) Created {} {} quest {} ({}) ({}, {} ({}), {})", + this->quests_by_number.emplace(vq->meta.quest_number, q); + this->quests_by_name.emplace(vq->meta.name, q); + this->quests_by_category_id_and_number[q->meta.category_id].emplace(vq->meta.quest_number, q); + static_game_data_log.debug_f("({}) Created {} {} quest {} ({}) ({}, {} ({}), {}) with floors {}", filenames_str, phosg::name_for_enum(vq->version), char_for_language_code(vq->language), - vq->quest_number, - vq->name, - name_for_episode(vq->episode), + vq->meta.quest_number, + vq->meta.name, + name_for_episode(vq->meta.episode), category_name, - vq->category_id, - vq->joinable ? "joinable" : "not joinable"); + vq->meta.category_id, + vq->meta.joinable ? "joinable" : "not joinable", + phosg::format_data_string(vq->meta.area_for_floor.data(), 0x12)); } } catch (const exception& e) { static_game_data_log.warning_f("({}) Failed to index quest file: {}", basename, e.what()); @@ -1096,7 +860,7 @@ vector>> QuestIndex::filt return ret; } for (auto it : category_it->second) { - if ((effective_episode != Episode::NONE) && (it.second->episode != effective_episode)) { + if ((effective_episode != Episode::NONE) && (it.second->meta.episode != effective_episode)) { continue; } bool all_required_versions_present = true; @@ -1145,8 +909,7 @@ string encode_download_quest_data(const string& compressed_data, size_t decompre data.resize((data.size() + 3) & (~3)); PSOV2Encryption encr(encryption_seed); - encr.encrypt(data.data() + sizeof(PSODownloadQuestHeader), - data.size() - sizeof(PSODownloadQuestHeader)); + encr.encrypt(data.data() + sizeof(PSODownloadQuestHeader), data.size() - sizeof(PSODownloadQuestHeader)); data.resize(original_size); return data; @@ -1158,12 +921,6 @@ shared_ptr VersionedQuest::create_download_quest(uint8_t overrid // this flag, we need to decompress the quest's .bin file, set the flag, then // recompress it again. - // This function should not be used for Episode 3 quests (they should be sent - // to the client as-is, without any encryption or other preprocessing) - if (this->episode == Episode::EP3 || is_ep3(this->version)) { - throw logic_error("Episode 3 quests cannot be converted to download quests"); - } - string decompressed_bin = prs_decompress(*this->bin_contents); void* data_ptr = decompressed_bin.data(); diff --git a/src/Quest.hh b/src/Quest.hh index 52a19c32..9f58e7fb 100644 --- a/src/Quest.hh +++ b/src/Quest.hh @@ -13,6 +13,7 @@ #include "ItemParameterTable.hh" #include "Map.hh" #include "PlayerSubordinates.hh" +#include "QuestMetadata.hh" #include "QuestScript.hh" #include "RareItemSet.hh" #include "StaticGameData.hh" @@ -34,7 +35,6 @@ enum class QuestMenuType { SOLO = 3, GOVERNMENT = 4, DOWNLOAD = 5, - EP3_DOWNLOAD = 6, // 7 can't be used as a menu type (it enables the per-episode filter) }; @@ -67,35 +67,16 @@ struct QuestCategoryIndex { }; struct VersionedQuest { + QuestMetadata meta; + // Most of these default values are intentionally invalid; we use these // values to check if each field was parsed during quest indexing. - uint32_t category_id = 0xFFFFFFFF; - uint32_t quest_number = 0xFFFFFFFF; Version version = Version::UNKNOWN; uint8_t language = 0xFF; - Episode episode = Episode::NONE; - bool joinable = false; - uint8_t max_players = 0x00; - std::string name; - std::string short_description; - std::string long_description; std::shared_ptr bin_contents; std::shared_ptr dat_contents; std::shared_ptr map_file; std::shared_ptr pvr_contents; - std::shared_ptr battle_rules; - ssize_t challenge_template_index = -1; - uint8_t description_flag = 0x00; - std::shared_ptr available_expression; - std::shared_ptr enabled_expression; - bool allow_start_from_chat_command = false; - int16_t lock_status_register = -1; - std::string common_item_set_name; - std::string rare_item_set_name; - std::shared_ptr common_item_set; - std::shared_ptr rare_item_set; - uint8_t allowed_drop_modes = 0x00; // 0 = use server default - ServerDropMode default_drop_mode = ServerDropMode::CLIENT; // Ignored if allowed_drop_modes == 0 bool is_dlq_encoded = false; void assert_valid() const; @@ -110,27 +91,8 @@ struct VersionedQuest { }; struct Quest { - uint32_t quest_number; - uint32_t category_id; - Episode episode; - bool allow_start_from_chat_command; - bool joinable; - uint8_t max_players; - int16_t lock_status_register; - std::string name; + QuestMetadata meta; mutable std::shared_ptr supermap; - std::shared_ptr battle_rules; - ssize_t challenge_template_index; - uint8_t description_flag; - std::shared_ptr available_expression; - std::shared_ptr enabled_expression; - std::string common_item_set_name; - std::string rare_item_set_name; - std::shared_ptr common_item_set; - std::shared_ptr rare_item_set; - uint8_t allowed_drop_modes = 0x00; // 0 = use server default - ServerDropMode default_drop_mode = ServerDropMode::CLIENT; // Ignored if allowed_drop_modes == 0 - std::map> versions; Quest() = delete; @@ -171,8 +133,7 @@ struct QuestIndex { const std::string& directory, std::shared_ptr category_index, const std::unordered_map>& common_item_sets, - const std::unordered_map>& rare_item_sets, - bool is_ep3); + const std::unordered_map>& rare_item_sets); phosg::JSON json() const; std::shared_ptr get(uint32_t quest_number) const; diff --git a/src/QuestMetadata.cc b/src/QuestMetadata.cc new file mode 100644 index 00000000..ed8ce76c --- /dev/null +++ b/src/QuestMetadata.cc @@ -0,0 +1,164 @@ +#include "QuestMetadata.hh" + +using namespace std; + +void QuestMetadata::assign_default_areas(Version version, Episode episode) { + for (size_t z = 0; z < 0x12; z++) { + this->area_for_floor[z] = SetDataTableBase::default_area_for_floor(version, episode, z); + } +} + +void QuestMetadata::assert_compatible(const QuestMetadata& other) const { + if (this->quest_number != other.quest_number) { + throw logic_error(std::format( + "incorrect versioned quest number (existing: {:08X}, new: {:08X})", + this->quest_number, other.quest_number)); + } + if (this->category_id != other.category_id) { + throw runtime_error(std::format( + "quest version is in a different category (existing: {:08X}, new: {:08X})", + this->category_id, other.category_id)); + } + if (this->episode != other.episode) { + throw runtime_error(std::format( + "quest version is in a different episode (existing: {}, new: {})", + name_for_episode(this->episode), name_for_episode(other.episode))); + } + if (this->allow_start_from_chat_command != other.allow_start_from_chat_command) { + throw runtime_error(std::format( + "quest version has a different allow_start_from_chat_command state (existing: {}, new: {})", + this->allow_start_from_chat_command ? "true" : "false", other.allow_start_from_chat_command ? "true" : "false")); + } + if (this->joinable != other.joinable) { + throw runtime_error(std::format( + "quest version has a different joinability state (existing: {}, new: {})", + this->joinable ? "true" : "false", other.joinable ? "true" : "false")); + } + if (this->max_players != other.max_players) { + throw runtime_error(std::format( + "quest version has a different maximum player count (existing: {}, new: {})", + this->max_players, other.max_players)); + } + if (this->lock_status_register != other.lock_status_register) { + throw runtime_error(std::format( + "quest version has a different lock status register (existing: {:04X}, new: {:04X})", + this->lock_status_register, other.lock_status_register)); + } + if (!this->battle_rules != !other.battle_rules) { + throw runtime_error(std::format( + "quest version has a different battle rules presence state (existing: {}, new: {})", + this->battle_rules ? "present" : "absent", other.battle_rules ? "present" : "absent")); + } + if (this->battle_rules && (*this->battle_rules != *other.battle_rules)) { + string existing_str = this->battle_rules->json().serialize(); + string new_str = other.battle_rules->json().serialize(); + throw runtime_error(std::format( + "quest version has different battle rules (existing: {}, new: {})", + existing_str, new_str)); + } + if (this->challenge_template_index != other.challenge_template_index) { + throw runtime_error(std::format( + "quest version has different challenge template index (existing: {}, new: {})", + this->challenge_template_index, other.challenge_template_index)); + } + if (this->challenge_exp_multiplier != other.challenge_exp_multiplier) { + throw runtime_error(std::format( + "quest version has different challenge EXP multiplier (existing: {}, new: {})", + this->challenge_exp_multiplier, other.challenge_exp_multiplier)); + } + if (this->challenge_difficulty != other.challenge_difficulty) { + throw runtime_error(std::format( + "quest version has different challenge difficulty (existing: {}, new: {})", + this->challenge_difficulty, other.challenge_difficulty)); + } + for (size_t z = 0; z < this->area_for_floor.size(); z++) { + const auto& this_fa = this->area_for_floor[z]; + const auto& other_fa = other.area_for_floor[z]; + if (this_fa != other_fa) { + throw runtime_error(std::format( + "quest version has different area on floor 0x{:02X} (existing: {}, new: {})", + z, phosg::format_data_string(this->area_for_floor.data(), 0x12), phosg::format_data_string(other.area_for_floor.data(), 0x12))); + } + } + if (this->description_flag != other.description_flag) { + throw runtime_error(std::format( + "quest version has different description flag (existing: {:02X}, new: {:02X})", + this->description_flag, other.description_flag)); + } + if (!this->available_expression != !other.available_expression) { + throw runtime_error(std::format( + "quest version has available expression but root quest does not, or vice versa (existing: {}, new: {})", + this->available_expression ? "present" : "absent", other.available_expression ? "present" : "absent")); + } + if (this->available_expression && *this->available_expression != *other.available_expression) { + string existing_str = this->available_expression->str(); + string new_str = other.available_expression->str(); + throw runtime_error(std::format( + "quest version has a different available expression (existing: {}, new: {})", + existing_str, new_str)); + } + if (!this->enabled_expression != !other.enabled_expression) { + throw runtime_error(std::format( + "quest version has enabled expression but root quest does not, or vice versa (existing: {}, new: {})", + this->enabled_expression ? "present" : "absent", other.enabled_expression ? "present" : "absent")); + } + if (this->enabled_expression && *this->enabled_expression != *other.enabled_expression) { + string existing_str = this->enabled_expression->str(); + string new_str = other.enabled_expression->str(); + throw runtime_error(std::format( + "quest version has a different enabled expression (existing: {}, new: {})", + existing_str, new_str)); + } + if (this->common_item_set_name != other.common_item_set_name) { + throw runtime_error(std::format( + "quest version has different common table name (existing: {}, new: {})", + this->common_item_set_name, other.common_item_set_name)); + } + if (this->common_item_set != other.common_item_set) { + throw runtime_error("quest version has different common table"); + } + if (this->rare_item_set_name != other.rare_item_set_name) { + throw runtime_error(std::format( + "quest version has different rare table name (existing: {}, new: {})", + this->rare_item_set_name, other.rare_item_set_name)); + } + if (this->rare_item_set != other.rare_item_set) { + throw runtime_error("quest version has different rare table"); + } + if (this->allowed_drop_modes != other.allowed_drop_modes) { + throw runtime_error(format("quest version has different allowed drop modes (existing: {:02X}, new: {:02X})", + this->allowed_drop_modes, other.allowed_drop_modes)); + } + if (this->default_drop_mode != other.default_drop_mode) { + throw runtime_error(format("quest version has different default drop mode (existing: {}, new: {})", + phosg::name_for_enum(this->default_drop_mode), phosg::name_for_enum(other.default_drop_mode))); + } +} + +phosg::JSON QuestMetadata::json() const { + auto floors_json = phosg::JSON::list(); + for (const auto& fa : this->area_for_floor) { + floors_json.emplace_back(fa); + } + return phosg::JSON::dict({ + {"CategoryID", this->category_id}, + {"Number", this->quest_number}, + {"Episode", name_for_episode(this->episode)}, + {"FloorAssignments", floors_json}, + {"Joinable", this->joinable}, + {"MaxPlayers", this->max_players}, + {"BattleRules", this->battle_rules ? this->battle_rules->json() : phosg::JSON(nullptr)}, + {"ChallengeTemplateIndex", (this->challenge_template_index >= 0) ? this->challenge_template_index : phosg::JSON(nullptr)}, + {"ChallengeEXPMultiplier", (this->challenge_exp_multiplier >= 0) ? this->challenge_exp_multiplier : phosg::JSON(nullptr)}, + {"ChallengeDifficulty", (this->challenge_difficulty >= 0) ? this->challenge_difficulty : phosg::JSON(nullptr)}, + {"DescriptionFlag", this->description_flag}, + {"AvailableExpression", this->available_expression ? this->available_expression->str() : phosg::JSON(nullptr)}, + {"EnabledExpression", this->available_expression ? this->available_expression->str() : phosg::JSON(nullptr)}, + {"CommonItemSetName", this->common_item_set_name.empty() ? phosg::JSON(nullptr) : this->common_item_set_name}, + {"RareItemSetName", this->rare_item_set_name.empty() ? phosg::JSON(nullptr) : this->rare_item_set_name}, + {"AllowedDropModes", this->allowed_drop_modes}, + {"DefaultDropMode", phosg::name_for_enum(this->default_drop_mode)}, + {"AllowStartFromChatCommand", this->allow_start_from_chat_command}, + {"LockStatusRegister", (this->lock_status_register >= 0) ? this->lock_status_register : phosg::JSON(nullptr)}, + }); +} diff --git a/src/QuestMetadata.hh b/src/QuestMetadata.hh new file mode 100644 index 00000000..0e6a2768 --- /dev/null +++ b/src/QuestMetadata.hh @@ -0,0 +1,52 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include "CommonItemSet.hh" +#include "IntegralExpression.hh" +#include "Map.hh" +#include "PlayerSubordinates.hh" +#include "RareItemSet.hh" + +struct QuestMetadata { + // This structure contains configuration that should be the same across all + // versions of the quest, except for the name and description strings. This + // is used in both the Quest and VersionedQuest structures; in Quest, the + // name and description are used only internally. + uint32_t category_id = 0xFFFFFFFF; + uint32_t quest_number = 0xFFFFFFFF; + Episode episode = Episode::NONE; + std::array area_for_floor; + bool joinable = false; + uint8_t max_players = 0x00; + std::shared_ptr battle_rules; + ssize_t challenge_template_index = -1; + float challenge_exp_multiplier = -1.0f; + int8_t challenge_difficulty = -1; + uint8_t description_flag = 0x00; + std::shared_ptr available_expression; + std::shared_ptr enabled_expression; + std::string common_item_set_name; // blank = use default + std::string rare_item_set_name; // blank = use default + std::shared_ptr common_item_set; + std::shared_ptr rare_item_set; + uint8_t allowed_drop_modes = 0x00; // 0 = use server default + ServerDropMode default_drop_mode = ServerDropMode::CLIENT; // Ignored if allowed_drop_modes == 0 + bool allow_start_from_chat_command = false; + int16_t lock_status_register = -1; + + std::string name; + std::string short_description; + std::string long_description; + + void assign_default_areas(Version version, Episode episode); + void assert_compatible(const QuestMetadata& other) const; + phosg::JSON json() const; + std::string areas_str() const; +}; diff --git a/src/QuestScript.cc b/src/QuestScript.cc index b37a6da3..be730c24 100644 --- a/src/QuestScript.cc +++ b/src/QuestScript.cc @@ -26,33 +26,37 @@ using namespace std; // This file documents PSO's quest script execution system. // The quest execution system has several relevant data structures: -// - The quest script. This is a stream of binary data containing opcodes (as -// defined below), each followed by their arguments. -// - The function table. This is a list of offsets into the quest script which -// can be used as targets for jumps and calls, as well as references to large -// data structures that don't fit in quest opcode arguments. -// - The registers. There are 256 registers, referred to as r0-r255. In later +// - The quest script is a stream of binary data containing opcodes (as defined +// below), each followed by their arguments. The offset of the code section +// of this stream is defined here. +// - The execution state specifies what the client should do on every frame. +// There are many possible states here, such as waiting for the player to +// dismiss a chat bubble, choose an item from a menu, etc. +// - The function table is a list of offsets into the quest script which can be +// used as targets for jumps and calls, as well as references to large data +// structures that don't fit in quest opcode arguments. +// - The quest registers are 32-bit integers referred to as r0-r255. In later // versions, registers may contain floating-point values, in which case // they're referred to as f0-f255 (but they still occupy the same memory as // r0-255). -// - The args list. This is a list of up to 8 values used for many quest +// - The args list is a list of up to 8 32-bit values used for many quest // opcodes in v3 and later. These opcodes are preceded by one or more // arg_push opcodes, which allow scripts the ability to pass values from // immediate data, registers, labels, or even pointers to registers. Opcodes // that use the args list are tagged with F_ARGS below. -// - The stack. This is an array of 32-bit integers (16 of them on v1/v2, 64 of -// them on v3/v4), which is used by the call and ret opcodes (which push and -// pop offsets into the quest script), but may also be used by the stack_push -// and stack_pop opcodes to work with arbitrary data. There is protection -// from stack underflows (the caller receives the value 0, or the thread +// - The stack is an array of 32-bit integers (16 of them on v1/v2, 64 of them +// on v3/v4), which is used by the call and ret opcodes (which push and pop +// offsets into the quest script), but may also be used by the stack_push and +// stack_pop opcodes to work with arbitrary data. There is protection from +// stack underflows (the caller receives the value 0, or the thread // terminates in case of the ret opcode), but there is no protection from // overflows. -// - Quest flags. These are a per-character array of 1024 single-bit flags -// saved with the character data. (On Episode 3, there are 8192 instead.) -// - Quest counters. These are a per-character array of 16 32-bit values saved +// - The quest flags are a per-character array of 1024 single-bit flags saved +// with the character data. (On Episode 3, there are 8192 instead.) +// - The quest counters are a per-character array of 16 32-bit values saved // with the character data. (On Episode 3, there are 48 instead.) -// - Event flags. These are an array of 0x100 bytes stored in the system file -// (not the character file). +// - The event flags are an array of 0x100 bytes stored in the system file (not +// the character file). using AttackData = BattleParamsIndex::AttackData; using ResistData = BattleParamsIndex::ResistData; @@ -160,11 +164,15 @@ struct QuestScriptOpcodeDefinition { LABEL16 = 0, LABEL16_SET, LABEL32, - REG, - REG_SET, - REG_SET_FIXED, // Sequence of N consecutive regs - REG32, - REG32_SET_FIXED, // Sequence of N consecutive regs + R_REG, + W_REG, + R_REG_SET, + R_REG_SET_FIXED, // Sequence of N consecutive regs + W_REG_SET_FIXED, // Sequence of N consecutive regs + R_REG32, + W_REG32, + R_REG32_SET_FIXED, // Sequence of N consecutive regs + W_REG32_SET_FIXED, // Sequence of N consecutive regs I8, I16, I32, @@ -235,8 +243,7 @@ using Arg = QuestScriptOpcodeDefinition::Argument; static_assert(NUM_VERSIONS == 14, "Don\'t forget to update the QuestScript flags and opcode definitions table"); -// F_PASS means the argument list isn't cleared after this opcode executes -static constexpr uint16_t F_PASS = 0x0001; // Version::PC_PATCH (unused for quests) +static constexpr uint16_t F_PUSH_ARG = 0x0001; // Version::PC_PATCH (unused for quests) // F_ARGS means this opcode takes its arguments via the argument list on v3 and // later. It has no effect on v2 and earlier. static constexpr uint16_t F_ARGS = 0x0002; // Version::BB_PATCH (unused for quests) @@ -253,11 +260,6 @@ static constexpr uint16_t F_GC_EP3TE = 0x0400; // Version::GC_EP3_NTE static constexpr uint16_t F_GC_EP3 = 0x0800; // Version::GC_EP3 static constexpr uint16_t F_XB_V3 = 0x1000; // Version::XB_V3 static constexpr uint16_t F_BB_V4 = 0x2000; // Version::BB_V4 -// This flag specifies that the opcode ends a function (returns). -static constexpr uint16_t F_RET = 0x4000; -// This flag specifies that the opcode sets the current episode. This is used -// to automatically detect a quest's episode from its script. -static constexpr uint16_t F_SET_EPISODE = 0x8000; static_assert(F_DC_NTE == v_flag(Version::DC_NTE)); static_assert(F_DC_112000 == v_flag(Version::DC_11_2000)); @@ -299,22 +301,31 @@ static constexpr auto LABEL16 = Arg::Type::LABEL16; static constexpr auto LABEL16_SET = Arg::Type::LABEL16_SET; // LABEL32 is a 32-bit index into the function table static constexpr auto LABEL32 = Arg::Type::LABEL32; -// REG is a single byte specifying a register number (rXX or fXX) -static constexpr auto REG = Arg::Type::REG; -// REG_SET is a single byte specifying how many registers follow, followed by +// R_REG is a single byte specifying a register number (rXX or fXX) which is +// read by the opcode and not modified +static constexpr auto R_REG = Arg::Type::R_REG; +// W_REG is a single byte specifying a register number (rXX or fXX) which is +// written by the opcode (and maybe also read beforehand) +static constexpr auto W_REG = Arg::Type::W_REG; +// R_REG_SET is a single byte specifying how many registers follow, followed by // that many bytes specifying individual register numbers. -static constexpr auto REG_SET = Arg::Type::REG_SET; -// REG_SET_FIXED is a single byte specifying a register number, but the opcode -// implicitly uses the following registers as well. For example, if an opcode -// takes a {REG_SET_FIXED, 4} and the value 100 was passed to that opcode, only -// the byte 0x64 would appear in the script data, but the opcode would use -// r100, r101, r102, and r103. -static constexpr auto REG_SET_FIXED = Arg::Type::REG_SET_FIXED; -// REG32 is a 32-bit register number. The high 24 bits are unused. -static constexpr auto REG32 = Arg::Type::REG32; -// REG32_SET_FIXED is like REG_SET_FIXED, but uses a 32-bit register number. -// The high 24 bits are unused. -static constexpr auto REG32_SET_FIXED = Arg::Type::REG32_SET_FIXED; +static constexpr auto R_REG_SET = Arg::Type::R_REG_SET; +// R_REG_SET_FIXED is a single byte specifying a register number, but the +// opcode implicitly reads the following registers as well. For example, if an +// opcode takes a {REG_SET_FIXED, 4} and the value 100 was passed to that +// opcode, only the byte 0x64 would appear in the script data, but the opcode +// would use r100, r101, r102, and r103. +static constexpr auto R_REG_SET_FIXED = Arg::Type::R_REG_SET_FIXED; +// W_REG_SET_FIXED is like R_REG_SET_FIXED, but is used for registers that are +// written (and maybe read beforehand) by the opcode. +static constexpr auto W_REG_SET_FIXED = Arg::Type::W_REG_SET_FIXED; +// [R/W]_REG32 is a 32-bit register number. The high 24 bits are unused. +static constexpr auto R_REG32 = Arg::Type::R_REG32; +static constexpr auto W_REG32 = Arg::Type::W_REG32; +// [RW]_REG32_SET_FIXED is like [RW]_REG_SET_FIXED, but uses a 32-bit register +// number. The high 24 bits are unused. +static constexpr auto R_REG32_SET_FIXED = Arg::Type::R_REG32_SET_FIXED; +static constexpr auto W_REG32_SET_FIXED = Arg::Type::W_REG32_SET_FIXED; // I8, I16, and I32 are unsigned integers of various sizes static constexpr auto I8 = Arg::Type::I8; static constexpr auto I16 = Arg::Type::I16; @@ -354,7 +365,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { {0x00, "nop", nullptr, {}, F_V0_V4}, // Pops new PC off stack - {0x01, "ret", nullptr, {}, F_V0_V4 | F_RET}, + {0x01, "ret", nullptr, {}, F_V0_V4}, // Stops execution for the current frame. Execution resumes immediately // after this opcode on the next frame. @@ -372,40 +383,40 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Pops r7-r1 from the stack {0x06, "va_end", nullptr, {}, F_V3_V4}, - // Replaces r1-r7 with the args stack, then calls labelA + // Replaces r1-r7 with the args list, then calls labelA {0x07, "va_call", nullptr, {SCRIPT16}, F_V3_V4}, // Copies a value from regB to regA - {0x08, "let", nullptr, {REG, REG}, F_V0_V4}, + {0x08, "let", nullptr, {W_REG, R_REG}, F_V0_V4}, // Sets regA to valueB - {0x09, "leti", nullptr, {REG, I32}, F_V0_V4}, + {0x09, "leti", nullptr, {W_REG, I32}, F_V0_V4}, // Sets regA to the memory address of regB. Note that this opcode was moved // to 0C in v3 and later. - {0x0A, "leta", nullptr, {REG, REG}, F_V0_V2}, + {0x0A, "leta", nullptr, {W_REG, R_REG}, F_V0_V2}, // Sets regA to valueB - {0x0A, "letb", nullptr, {REG, I8}, F_V3_V4}, + {0x0A, "letb", nullptr, {W_REG, I8}, F_V3_V4}, // Sets regA to valueB - {0x0B, "letw", nullptr, {REG, I16}, F_V3_V4}, + {0x0B, "letw", nullptr, {W_REG, I16}, F_V3_V4}, // Sets regA to the memory address of regB - {0x0C, "leta", nullptr, {REG, REG}, F_V3_V4}, + {0x0C, "leta", nullptr, {W_REG, R_REG}, F_V3_V4}, // Sets regA to the address of the offset of labelB in the function table // (to get the offset, use read4 after this) - {0x0D, "leto", nullptr, {REG, SCRIPT16}, F_V3_V4}, + {0x0D, "leto", nullptr, {W_REG, SCRIPT16}, F_V3_V4}, // Sets regA to 1 - {0x10, "set", nullptr, {REG}, F_V0_V4}, + {0x10, "set", nullptr, {W_REG}, F_V0_V4}, // Sets regA to 0 - {0x11, "clear", nullptr, {REG}, F_V0_V4}, + {0x11, "clear", nullptr, {W_REG}, F_V0_V4}, // Sets a regA to 0 if it's nonzero and vice versa - {0x12, "rev", nullptr, {REG}, F_V0_V4}, + {0x12, "rev", nullptr, {W_REG}, F_V0_V4}, // Sets flagA to 1. Sends 6x75. {0x13, "gset", nullptr, {I16}, F_V0_V4}, @@ -420,61 +431,61 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { {0x15, "grev", nullptr, {I16}, F_V0_V4}, // If regB is nonzero, sets flagA; otherwise, clears it - {0x16, "glet", nullptr, {I16, REG}, F_V0_V4}, + {0x16, "glet", nullptr, {I16, R_REG}, F_V0_V4}, // Sets regB to the value of flagA - {0x17, "gget", nullptr, {I16, REG}, F_V0_V4}, + {0x17, "gget", nullptr, {I16, R_REG}, F_V0_V4}, // regA += regB - {0x18, "add", nullptr, {REG, REG}, F_V0_V4}, + {0x18, "add", nullptr, {W_REG, R_REG}, F_V0_V4}, // regA += valueB - {0x19, "addi", nullptr, {REG, I32}, F_V0_V4}, + {0x19, "addi", nullptr, {W_REG, I32}, F_V0_V4}, // regA -= regB - {0x1A, "sub", nullptr, {REG, REG}, F_V0_V4}, + {0x1A, "sub", nullptr, {W_REG, R_REG}, F_V0_V4}, // regA -= valueB - {0x1B, "subi", nullptr, {REG, I32}, F_V0_V4}, + {0x1B, "subi", nullptr, {W_REG, I32}, F_V0_V4}, // regA *= regB - {0x1C, "mul", nullptr, {REG, REG}, F_V0_V4}, + {0x1C, "mul", nullptr, {W_REG, R_REG}, F_V0_V4}, // regA *= valueB - {0x1D, "muli", nullptr, {REG, I32}, F_V0_V4}, + {0x1D, "muli", nullptr, {W_REG, I32}, F_V0_V4}, // regA /= regB - {0x1E, "div", nullptr, {REG, REG}, F_V0_V4}, + {0x1E, "div", nullptr, {W_REG, R_REG}, F_V0_V4}, // regA /= valueB - {0x1F, "divi", nullptr, {REG, I32}, F_V0_V4}, + {0x1F, "divi", nullptr, {W_REG, I32}, F_V0_V4}, // regA &= regB - {0x20, "and", nullptr, {REG, REG}, F_V0_V4}, + {0x20, "and", nullptr, {W_REG, R_REG}, F_V0_V4}, // regA &= valueB - {0x21, "andi", nullptr, {REG, I32}, F_V0_V4}, + {0x21, "andi", nullptr, {W_REG, I32}, F_V0_V4}, // regA |= regB - {0x22, "or", nullptr, {REG, REG}, F_V0_V4}, + {0x22, "or", nullptr, {W_REG, R_REG}, F_V0_V4}, // regA |= valueB - {0x23, "ori", nullptr, {REG, I32}, F_V0_V4}, + {0x23, "ori", nullptr, {W_REG, I32}, F_V0_V4}, // regA ^= regB - {0x24, "xor", nullptr, {REG, REG}, F_V0_V4}, + {0x24, "xor", nullptr, {W_REG, R_REG}, F_V0_V4}, // regA ^= valueB - {0x25, "xori", nullptr, {REG, I32}, F_V0_V4}, + {0x25, "xori", nullptr, {W_REG, I32}, F_V0_V4}, // regA %= regB // Note: This does signed division, so if the value is negative, you might // get unexpected results. - {0x26, "mod", nullptr, {REG, REG}, F_V3_V4}, + {0x26, "mod", nullptr, {W_REG, R_REG}, F_V3_V4}, // regA %= valueB // Note: Unlike mod, this does unsigned division. - {0x27, "modi", nullptr, {REG, I32}, F_V3_V4}, + {0x27, "modi", nullptr, {W_REG, I32}, F_V3_V4}, // Jumps to labelA {0x28, "jmp", nullptr, {SCRIPT16}, F_V0_V4}, @@ -484,115 +495,115 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { {0x29, "call", nullptr, {SCRIPT16}, F_V0_V4}, // If all values in regsB are nonzero, jumps to labelA - {0x2A, "jmp_on", nullptr, {SCRIPT16, REG_SET}, F_V0_V4}, + {0x2A, "jmp_on", nullptr, {SCRIPT16, R_REG_SET}, F_V0_V4}, // If all values in regsB are zero, jumps to labelA - {0x2B, "jmp_off", nullptr, {SCRIPT16, REG_SET}, F_V0_V4}, + {0x2B, "jmp_off", nullptr, {SCRIPT16, R_REG_SET}, F_V0_V4}, // If regA == regB, jumps to labelC - {0x2C, "jmp_eq", "jmp_=", {REG, REG, SCRIPT16}, F_V0_V4}, + {0x2C, "jmp_eq", "jmp_=", {R_REG, R_REG, SCRIPT16}, F_V0_V4}, // If regA == valueB, jumps to labelC - {0x2D, "jmpi_eq", "jmpi_=", {REG, I32, SCRIPT16}, F_V0_V4}, + {0x2D, "jmpi_eq", "jmpi_=", {R_REG, I32, SCRIPT16}, F_V0_V4}, // If regA != regB, jumps to labelC - {0x2E, "jmp_ne", "jmp_!=", {REG, REG, SCRIPT16}, F_V0_V4}, + {0x2E, "jmp_ne", "jmp_!=", {R_REG, R_REG, SCRIPT16}, F_V0_V4}, // If regA != valueB, jumps to labelC - {0x2F, "jmpi_ne", "jmpi_!=", {REG, I32, SCRIPT16}, F_V0_V4}, + {0x2F, "jmpi_ne", "jmpi_!=", {R_REG, I32, SCRIPT16}, F_V0_V4}, // If regA > regB (unsigned), jumps to labelC - {0x30, "ujmp_gt", "ujmp_>", {REG, REG, SCRIPT16}, F_V0_V4}, + {0x30, "ujmp_gt", "ujmp_>", {R_REG, R_REG, SCRIPT16}, F_V0_V4}, // If regA > valueB (unsigned), jumps to labelC - {0x31, "ujmpi_gt", "ujmpi_>", {REG, I32, SCRIPT16}, F_V0_V4}, + {0x31, "ujmpi_gt", "ujmpi_>", {R_REG, I32, SCRIPT16}, F_V0_V4}, // If regA > regB (signed), jumps to labelC - {0x32, "jmp_gt", "jmp_>", {REG, REG, SCRIPT16}, F_V0_V4}, + {0x32, "jmp_gt", "jmp_>", {R_REG, R_REG, SCRIPT16}, F_V0_V4}, // If regA > valueB (signed), jumps to labelC - {0x33, "jmpi_gt", "jmpi_>", {REG, I32, SCRIPT16}, F_V0_V4}, + {0x33, "jmpi_gt", "jmpi_>", {R_REG, I32, SCRIPT16}, F_V0_V4}, // If regA < regB (unsigned), jumps to labelC - {0x34, "ujmp_lt", "ujmp_<", {REG, REG, SCRIPT16}, F_V0_V4}, + {0x34, "ujmp_lt", "ujmp_<", {R_REG, R_REG, SCRIPT16}, F_V0_V4}, // If regA < valueB (unsigned), jumps to labelC - {0x35, "ujmpi_lt", "ujmpi_<", {REG, I32, SCRIPT16}, F_V0_V4}, + {0x35, "ujmpi_lt", "ujmpi_<", {R_REG, I32, SCRIPT16}, F_V0_V4}, // If regA < regB (signed), jumps to labelC - {0x36, "jmp_lt", "jmp_<", {REG, REG, SCRIPT16}, F_V0_V4}, + {0x36, "jmp_lt", "jmp_<", {R_REG, R_REG, SCRIPT16}, F_V0_V4}, // If regA < valueB (signed), jumps to labelC - {0x37, "jmpi_lt", "jmpi_<", {REG, I32, SCRIPT16}, F_V0_V4}, + {0x37, "jmpi_lt", "jmpi_<", {R_REG, I32, SCRIPT16}, F_V0_V4}, // If regA >= regB (unsigned), jumps to labelC - {0x38, "ujmp_ge", "ujmp_>=", {REG, REG, SCRIPT16}, F_V0_V4}, + {0x38, "ujmp_ge", "ujmp_>=", {R_REG, R_REG, SCRIPT16}, F_V0_V4}, // If regA >= valueB (unsigned), jumps to labelC - {0x39, "ujmpi_ge", "ujmpi_>=", {REG, I32, SCRIPT16}, F_V0_V4}, + {0x39, "ujmpi_ge", "ujmpi_>=", {R_REG, I32, SCRIPT16}, F_V0_V4}, // If regA >= regB (signed), jumps to labelC - {0x3A, "jmp_ge", "jmp_>=", {REG, REG, SCRIPT16}, F_V0_V4}, + {0x3A, "jmp_ge", "jmp_>=", {R_REG, R_REG, SCRIPT16}, F_V0_V4}, // If regA >= valueB (signed), jumps to labelC - {0x3B, "jmpi_ge", "jmpi_>=", {REG, I32, SCRIPT16}, F_V0_V4}, + {0x3B, "jmpi_ge", "jmpi_>=", {R_REG, I32, SCRIPT16}, F_V0_V4}, // If regA <= regB (unsigned), jumps to labelC - {0x3C, "ujmp_le", "ujmp_<=", {REG, REG, SCRIPT16}, F_V0_V4}, + {0x3C, "ujmp_le", "ujmp_<=", {R_REG, R_REG, SCRIPT16}, F_V0_V4}, // If regA <= valueB (unsigned), jumps to labelC - {0x3D, "ujmpi_le", "ujmpi_<=", {REG, I32, SCRIPT16}, F_V0_V4}, + {0x3D, "ujmpi_le", "ujmpi_<=", {R_REG, I32, SCRIPT16}, F_V0_V4}, // If regA <= regB (signed), jumps to labelC - {0x3E, "jmp_le", "jmp_<=", {REG, REG, SCRIPT16}, F_V0_V4}, + {0x3E, "jmp_le", "jmp_<=", {R_REG, R_REG, SCRIPT16}, F_V0_V4}, // If regA <= valueB (signed), jumps to labelC - {0x3F, "jmpi_le", "jmpi_<=", {REG, I32, SCRIPT16}, F_V0_V4}, + {0x3F, "jmpi_le", "jmpi_<=", {R_REG, I32, SCRIPT16}, F_V0_V4}, // Jumps to labelsB[regA] - {0x40, "switch_jmp", nullptr, {REG, SCRIPT16_SET}, F_V0_V4}, + {0x40, "switch_jmp", nullptr, {R_REG, SCRIPT16_SET}, F_V0_V4}, // Calls labelsB[regA] - {0x41, "switch_call", nullptr, {REG, SCRIPT16_SET}, F_V0_V4}, + {0x41, "switch_call", nullptr, {R_REG, SCRIPT16_SET}, F_V0_V4}, // Does nothing {0x42, "nop_42", nullptr, {I32}, F_V0_V2}, // Pushes the value in regA to the stack - {0x42, "stack_push", nullptr, {REG}, F_V3_V4}, + {0x42, "stack_push", nullptr, {R_REG}, F_V3_V4}, // Pops a value from the stack and puts it into regA - {0x43, "stack_pop", nullptr, {REG}, F_V3_V4}, + {0x43, "stack_pop", nullptr, {W_REG}, F_V3_V4}, // Pushes (valueB) regs in increasing order starting at regA - {0x44, "stack_pushm", nullptr, {REG, I32}, F_V3_V4}, + {0x44, "stack_pushm", nullptr, {R_REG, I32}, F_V3_V4}, // Pops (valueB) regs in decreasing order ending at regA - {0x45, "stack_popm", nullptr, {REG, I32}, F_V3_V4}, + {0x45, "stack_popm", nullptr, {W_REG, I32}, F_V3_V4}, // Appends regA to the args list - {0x48, "arg_pushr", nullptr, {REG}, F_V3_V4 | F_PASS}, + {0x48, "arg_pushr", nullptr, {R_REG}, F_V3_V4 | F_PUSH_ARG}, // Appends valueA to the args list - {0x49, "arg_pushl", nullptr, {I32}, F_V3_V4 | F_PASS}, - {0x4A, "arg_pushb", nullptr, {I8}, F_V3_V4 | F_PASS}, - {0x4B, "arg_pushw", nullptr, {I16}, F_V3_V4 | F_PASS}, + {0x49, "arg_pushl", nullptr, {I32}, F_V3_V4 | F_PUSH_ARG}, + {0x4A, "arg_pushb", nullptr, {I8}, F_V3_V4 | F_PUSH_ARG}, + {0x4B, "arg_pushw", nullptr, {I16}, F_V3_V4 | F_PUSH_ARG}, // Appends the memory address of regA to the args list - {0x4C, "arg_pusha", nullptr, {REG}, F_V3_V4 | F_PASS}, + {0x4C, "arg_pusha", nullptr, {R_REG}, F_V3_V4 | F_PUSH_ARG}, // Appends the script offset of labelA to the args list - {0x4D, "arg_pusho", nullptr, {LABEL16}, F_V3_V4 | F_PASS}, + {0x4D, "arg_pusho", nullptr, {LABEL16}, F_V3_V4 | F_PUSH_ARG}, // Appends strA to the args list - {0x4E, "arg_pushs", nullptr, {CSTRING}, F_V3_V4 | F_PASS}, + {0x4E, "arg_pushs", nullptr, {CSTRING}, F_V3_V4 | F_PUSH_ARG}, // Creates dialogue with an object/NPC (valueA) starting with message strB {0x50, "message", nullptr, {I32, CSTRING}, F_V0_V4 | F_ARGS}, // Prompts the player with a list of choices (strB; items separated by // newlines) and returns the index of their choice in regA - {0x51, "list", nullptr, {REG, CSTRING}, F_V0_V4 | F_ARGS}, + {0x51, "list", nullptr, {W_REG, CSTRING}, F_V0_V4 | F_ARGS}, // Fades from black {0x52, "fadein", nullptr, {}, F_V0_V4}, @@ -643,7 +654,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { {0x5C, "message_end", "mesend", {}, F_V0_V4}, // Gets the current time, in seconds since 00:00:00 on 1 January 2000 - {0x5D, "gettime", nullptr, {REG}, F_V0_V4}, + {0x5D, "gettime", nullptr, {W_REG}, F_V0_V4}, // Closes a window_msg {0x5E, "window_msg_end", "winend", {}, F_V0_V4}, @@ -674,8 +685,8 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // according to qedit.info) // regsA[5] = template index (see 6x69 in CommandFormats.hh) // valueB is required in pre-v3 but is ignored - {0x66, "npc_crp", "npc_crp_V1", {{REG_SET_FIXED, 6}, I32}, F_V0_V2}, - {0x66, "npc_crp", "npc_crp_V3", {{REG_SET_FIXED, 6}}, F_V3_V4}, + {0x66, "npc_crp", "npc_crp_V1", {{R_REG_SET_FIXED, 6}, I32}, F_V0_V2}, + {0x66, "npc_crp", "npc_crp_V3", {{R_REG_SET_FIXED, 6}}, F_V3_V4}, // Creates a pipe. valueA is client ID {0x68, "create_pipe", nullptr, {I32}, F_V0_V4 | F_ARGS}, @@ -683,12 +694,12 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Checks player HP, but not in a straightforward manner. // Specifically, sets regA to 1 if (current_hp / max_hp) < (1 / valueB). // Sets regA to 0 otherwise. - {0x69, "p_hpstat", "p_hpstat_V1", {REG, CLIENT_ID}, F_V0_V2 | F_ARGS}, - {0x69, "p_hpstat", "p_hpstat_V3", {REG, CLIENT_ID}, F_V3_V4 | F_ARGS}, + {0x69, "p_hpstat", "p_hpstat_V1", {W_REG, CLIENT_ID}, F_V0_V2 | F_ARGS}, + {0x69, "p_hpstat", "p_hpstat_V3", {W_REG, CLIENT_ID}, F_V3_V4 | F_ARGS}, // Sets regA to 1 if player in slot (valueB) is dead, or 0 if alive. - {0x6A, "p_dead", "p_dead_V1", {REG, CLIENT_ID}, F_V0_V2 | F_ARGS}, - {0x6A, "p_dead", "p_dead_V3", {REG, CLIENT_ID}, F_V3_V4 | F_ARGS}, + {0x6A, "p_dead", "p_dead_V1", {W_REG, CLIENT_ID}, F_V0_V2 | F_ARGS}, + {0x6A, "p_dead", "p_dead_V3", {W_REG, CLIENT_ID}, F_V3_V4 | F_ARGS}, // Disables/enables telepipes/Ryuker {0x6B, "p_disablewarp", nullptr, {}, F_V0_V4}, @@ -699,8 +710,8 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regsA[3] = angle // regsA[4] = client ID // valueB is required in pre-v3 but is ignored - {0x6D, "p_move", "p_move_v1", {{REG_SET_FIXED, 5}, I32}, F_V0_V2}, - {0x6D, "p_move", "p_move_V3", {{REG_SET_FIXED, 5}}, F_V3_V4}, + {0x6D, "p_move", "p_move_v1", {{R_REG_SET_FIXED, 5}, I32}, F_V0_V2}, + {0x6D, "p_move", "p_move_V3", {{R_REG_SET_FIXED, 5}}, F_V3_V4}, // Causes the player with client ID valueA to look at an unspecified other // player. The specified player looks at the player with the lowest client @@ -727,7 +738,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // valueA = client ID // regsB[0-2] = position (x, y, z as integers) // regsB[3] = angle - {0x76, "set_player_start_position", "p_setpos", {CLIENT_ID, {REG_SET_FIXED, 4}}, F_V0_V4 | F_ARGS}, + {0x76, "set_player_start_position", "p_setpos", {CLIENT_ID, {R_REG_SET_FIXED, 4}}, F_V0_V4 | F_ARGS}, // Returns players to the Hunter's Guild counter. {0x77, "p_return_guild", nullptr, {}, F_V0_V4}, @@ -744,8 +755,8 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regsA[6] = initial state (0 = alive, 1 = dead, 2 = invisible text box, // according to qedit.info) // regsA[7] = client ID - {0x79, "npc_talk_pl", "npc_talk_pl_V1", {{REG32_SET_FIXED, 8}}, F_V0_V2}, - {0x79, "npc_talk_pl", "npc_talk_pl_V3", {{REG_SET_FIXED, 8}}, F_V3_V4}, + {0x79, "npc_talk_pl", "npc_talk_pl_V1", {{R_REG32_SET_FIXED, 8}}, F_V0_V2}, + {0x79, "npc_talk_pl", "npc_talk_pl_V3", {{R_REG_SET_FIXED, 8}}, F_V3_V4}, // Destroys an NPC created with npc_talk_pl. This opcode cannot be executed // multiple times on the same frame; if it is, only the last one will take @@ -757,8 +768,8 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { {0x7B, "npc_crtpk", "npc_crtpk_V3", {I32, I32}, F_V3_V4 | F_ARGS}, // Creates attacker NPC - {0x7C, "npc_crppk", "npc_crppk_V1", {{REG32_SET_FIXED, 7}, I32}, F_V0_V2}, - {0x7C, "npc_crppk", "npc_crppk_V3", {{REG_SET_FIXED, 7}}, F_V3_V4}, + {0x7C, "npc_crppk", "npc_crppk_V1", {{R_REG32_SET_FIXED, 7}, I32}, F_V0_V2}, + {0x7C, "npc_crppk", "npc_crppk_V3", {{R_REG_SET_FIXED, 7}}, F_V3_V4}, // Creates an NPC with client ID 1. It is not recommended to use this // opcode if a player can be in that slot - use npc_crptalk_id instead. @@ -767,8 +778,8 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regsA[4] = initial state (0 = alive, 1 = dead, 2 = invisible text box, // according to qedit.info) // regsA[5] = template index (see 6x69 in CommandFormats.hh) - {0x7D, "npc_crptalk", "npc_crptalk_v1", {{REG32_SET_FIXED, 6}, I32}, F_V0_V2}, - {0x7D, "npc_crptalk", "npc_crptalk_V3", {{REG_SET_FIXED, 6}}, F_V3_V4}, + {0x7D, "npc_crptalk", "npc_crptalk_v1", {{R_REG32_SET_FIXED, 6}, I32}, F_V0_V2}, + {0x7D, "npc_crptalk", "npc_crptalk_V3", {{R_REG_SET_FIXED, 6}}, F_V3_V4}, // Causes client ID valueA to look at client ID valueB. Sends 6x3E. {0x7E, "p_look_at", nullptr, {CLIENT_ID, CLIENT_ID}, F_V0_V4 | F_ARGS}, @@ -780,8 +791,8 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // according to qedit.info) // regsA[5] = client ID // regsA[6] = template index (see 6x69 in CommandFormats.hh) - {0x7F, "npc_crp_id", "npc_crp_id_V1", {{REG32_SET_FIXED, 7}, I32}, F_V0_V2}, - {0x7F, "npc_crp_id", "npc_crp_id_v3", {{REG_SET_FIXED, 7}}, F_V3_V4}, + {0x7F, "npc_crp_id", "npc_crp_id_V1", {{R_REG32_SET_FIXED, 7}, I32}, F_V0_V2}, + {0x7F, "npc_crp_id", "npc_crp_id_v3", {{R_REG_SET_FIXED, 7}}, F_V3_V4}, // Causes the camera to shake {0x80, "cam_quake", nullptr, {}, F_V0_V4}, @@ -797,8 +808,8 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regsA[0-2] = destination (x, y, z as integers) // regsA[3] = pan time (in frames; 30 frames/sec) // regsA[4] = end time (in frames; 30 frames/sec) - {0x84, "cam_pan", "cam_pan_V1", {{REG32_SET_FIXED, 5}, I32}, F_V0_V2}, - {0x84, "cam_pan", "cam_pan_V3", {{REG_SET_FIXED, 5}}, F_V3_V4}, + {0x84, "cam_pan", "cam_pan_V1", {{R_REG32_SET_FIXED, 5}, I32}, F_V0_V2}, + {0x84, "cam_pan", "cam_pan_V3", {{R_REG_SET_FIXED, 5}}, F_V3_V4}, // Temporarily sets the game's difficulty to Very Hard (even on v2). On v3 // and later, does nothing. @@ -813,25 +824,25 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Creates a telepipe. The telepipe disappears upon being used. // regsA[0-2] = location (x, y, z as integers) // regsA[3] = owner client ID (player or NPC must exist in the game) - {0x87, "pos_pipe", "pos_pipe_V1", {{REG32_SET_FIXED, 4}, I32}, F_V0_V2}, - {0x87, "pos_pipe", "pos_pipe_V3", {{REG_SET_FIXED, 4}}, F_V3_V4}, + {0x87, "pos_pipe", "pos_pipe_V1", {{R_REG32_SET_FIXED, 4}, I32}, F_V0_V2}, + {0x87, "pos_pipe", "pos_pipe_V3", {{R_REG_SET_FIXED, 4}}, F_V3_V4}, // Checks if all set events (enemies) have been destroyed in a given room. // regA = result (0 = not cleared, 1 = cleared) // regsB[0] = floor number // regsB[1] = room ID - {0x88, "if_zone_clear", nullptr, {REG, {REG_SET_FIXED, 2}}, F_V0_V4}, + {0x88, "if_zone_clear", nullptr, {W_REG, {R_REG_SET_FIXED, 2}}, F_V0_V4}, // Returns the number of enemies destroyed so far in this game (since the // quest began). - {0x89, "chk_ene_num", nullptr, {REG}, F_V0_V4}, + {0x89, "chk_ene_num", nullptr, {W_REG}, F_V0_V4}, // Constructs all objects or enemies that match the conditions: // regsA[0] = floor // regsA[1] = section // regsA[2] = group - {0x8A, "construct_delayed_object", "unhide_obj", {{REG_SET_FIXED, 3}}, F_V0_V4}, - {0x8B, "construct_delayed_enemy", "unhide_ene", {{REG_SET_FIXED, 3}}, F_V0_V4}, + {0x8A, "construct_delayed_object", "unhide_obj", {{R_REG_SET_FIXED, 3}}, F_V0_V4}, + {0x8B, "construct_delayed_enemy", "unhide_ene", {{R_REG_SET_FIXED, 3}}, F_V0_V4}, // Starts a new thread when the player is close enough to the given point. // The collision is created on the current floor; the thread is created @@ -839,15 +850,15 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regsA[0-2] = location (x, y, z as integers) // regsA[3] = radius // regsA[4] = label index where thread should start - {0x8C, "at_coords_call", nullptr, {{REG_SET_FIXED, 5}}, F_V0_V4}, + {0x8C, "at_coords_call", nullptr, {{R_REG_SET_FIXED, 5}}, F_V0_V4}, // Like at_coords_call, but the thread is not started automatically. // Instead, the player's primary action button becomes "talk" within the // radius, and the label is called when the player presses that button. - {0x8D, "at_coords_talk", nullptr, {{REG_SET_FIXED, 5}}, F_V0_V4}, + {0x8D, "at_coords_talk", nullptr, {{R_REG_SET_FIXED, 5}}, F_V0_V4}, // Like at_coords_call, but only triggers if an NPC enters the radius. - {0x8E, "npc_coords_call", "walk_to_coord_call", {{REG_SET_FIXED, 5}}, F_V0_V4}, + {0x8E, "npc_coords_call", "walk_to_coord_call", {{R_REG_SET_FIXED, 5}}, F_V0_V4}, // Like at_coords_call, but triggers when a player within the event radius // is also within the player radius of any other player. @@ -855,7 +866,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regsA[3] = event radius (centered at x, y, z defined above) // regsA[4] = player radius (centered at player) // regsA[5] = label index where thread should start - {0x8F, "party_coords_call", "col_npcinr", {{REG_SET_FIXED, 6}}, F_V0_V4}, + {0x8F, "party_coords_call", "col_npcinr", {{R_REG_SET_FIXED, 6}}, F_V0_V4}, // Enables/disables a switch flag (valueA). Does NOT send 6x05, so other // players will not know about this change! Use sw_send instead to keep @@ -883,7 +894,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regsA[4] = label number to call // regsA[5] = distance from ground to target // regB = returned object token (can be used with del_obj_param) - {0x94, "set_obj_param", nullptr, {{REG_SET_FIXED, 6}, REG}, F_V0_V4}, + {0x94, "set_obj_param", nullptr, {{R_REG_SET_FIXED, 6}, W_REG}, F_V0_V4}, // Causes the labelB to be called on a new thread when the player warps to // floorA. If the given floor already has a registered handler, it is @@ -902,7 +913,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regsA[5] = player radius (if NPC is closer, label is not called) // regsA[6-8] = warp location (x, y, z as integers) for use with // npc_chkwarp within the triggered function - {0x97, "check_npc_straggle", "col_plinaw", {{REG_SET_FIXED, 9}}, F_V1_V4}, + {0x97, "check_npc_straggle", "col_plinaw", {{R_REG_SET_FIXED, 9}}, F_V1_V4}, // Hides or shows the HUD. {0x98, "hud_hide", nullptr, {}, F_V0_V4}, @@ -950,8 +961,8 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Makes a player or NPC walk to a location. // regsA[0-2] = location (x, y, z as integers; y is ignored) // regsA[3] = client ID - {0xA8, "pl_walk", "pl_walk_V1", {{REG32_SET_FIXED, 4}, I32}, F_V0_V2}, - {0xA8, "pl_walk", "pl_walk_V3", {{REG_SET_FIXED, 4}}, F_V3_V4}, + {0xA8, "pl_walk", "pl_walk_V1", {{R_REG32_SET_FIXED, 4}, I32}, F_V0_V2}, + {0xA8, "pl_walk", "pl_walk_V3", {{R_REG_SET_FIXED, 4}}, F_V3_V4}, // Gives valueB Meseta to the player with client ID valueA. Negative values // do not appear to be handled properly; if this opcode attempts to take @@ -963,18 +974,18 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Deletes an interactable object previously created by set_obj_param. // valueA is the object's token (returned by set_obj_param in regB). - {0xB2, "del_obj_param", nullptr, {REG}, F_V0_V4}, + {0xB2, "del_obj_param", nullptr, {R_REG}, F_V0_V4}, // Creates an item in the player's inventory. If the item is successfully // created, this opcode sends 6x2B on all versions except BB. On BB, this // opcode sends 6xCA, and the server sends 6xBE to create the item. // regsA[0-2] = item.data1[0-2] // regB = returned item ID, or FFFFFFFF if item can't be created - {0xB3, "item_create", nullptr, {{REG_SET_FIXED, 3}, REG}, F_V0_V4}, + {0xB3, "item_create", nullptr, {{R_REG_SET_FIXED, 3}, W_REG}, F_V0_V4}, // Like item_create, but regsA specify all of item.data1 instead of only // the first 3 bytes. - {0xB4, "item_create2", nullptr, {{REG_SET_FIXED, 12}, REG}, F_V0_V4}, + {0xB4, "item_create2", nullptr, {{R_REG_SET_FIXED, 12}, W_REG}, F_V0_V4}, // Deletes an item from the player's inventory. Sends 6x29 if ths item is // found and deleted. If the item is stackable, only one of it is deleted; @@ -982,13 +993,14 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regA = item ID // regsB[0-11] = item.data1[0-11] for deleted item // regsB must not wrap around (that is, the first register in regsB cannot - // be in the range [r245, r255]). - {0xB5, "item_delete", nullptr, {REG, {REG_SET_FIXED, 12}}, F_V0_V4}, + // be in the range [r245, r255]). If the item is not found, regsB[0] is set + // to 0xFFFFFFFF and the rest of regsB are not affected. + {0xB5, "item_delete", nullptr, {R_REG, {W_REG_SET_FIXED, 12}}, F_V0_V4}, // Like item_delete, but searches by item.data1[0-2] instead of by item ID. // regsA[0-2] = item.data1[0-2] to search for // regsB[0-11] = item.data1[0-11] for deleted item - {0xB6, "item_delete_by_type", "item_delete2", {{REG_SET_FIXED, 3}, {REG_SET_FIXED, 12}}, F_V0_V4}, + {0xB6, "item_delete_by_type", "item_delete2", {{R_REG_SET_FIXED, 3}, {W_REG_SET_FIXED, 12}}, F_V0_V4}, // Searches the player's inventory for an item and returns its item ID. // The matching condition depends on the item's type: @@ -998,7 +1010,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Tool: data1[0-2] must match; if it's a tech disk, data1[4] must be 0 // regsA[0-2] = item.data1[0-2] to search for, as above // regB = found item ID, or FFFFFFFF if not found - {0xB7, "find_inventory_item", "item_check", {{REG_SET_FIXED, 3}, REG}, F_V0_V4}, + {0xB7, "find_inventory_item", "item_check", {{R_REG_SET_FIXED, 3}, W_REG}, F_V0_V4}, // Triggers set event valueA on the current floor. Sends 6x67. {0xB8, "setevt", nullptr, {I32}, F_V05_V4 | F_ARGS}, @@ -1007,7 +1019,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // executed, returns 2. // This opcode only returns 0-2, even in Ultimate (which results in 2 as // well). All non-v1 quests should use get_difficulty_level_v2 instead. - {0xB9, "get_difficulty_level_v1", "get_difflvl", {REG}, F_V05_V4}, + {0xB9, "get_difficulty_level_v1", "get_difflvl", {W_REG}, F_V05_V4}, // Sets a label to be called (in a new thread) when the quest exits. // This happens during the unload procedure when leaving the game, so most @@ -1026,8 +1038,8 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regsA[0-2] = location (x, y, z as integers) // regsA[3] = effect type // regsA[4] = duration (in frames; 30 frames/sec) - {0xC0, "particle", "particle_V1", {{REG32_SET_FIXED, 5}, I32}, F_V05_V2}, - {0xC0, "particle", "particle_V3", {{REG_SET_FIXED, 5}}, F_V3_V4}, + {0xC0, "particle", "particle_V1", {{R_REG32_SET_FIXED, 5}, I32}, F_V05_V2}, + {0xC0, "particle", "particle_V3", {{R_REG_SET_FIXED, 5}}, F_V3_V4}, // Specifies what NPCs should say in various situations. This opcode sets // strings for all NPCs; to set strings for only specific NPCs, use @@ -1076,7 +1088,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // template, 3: nothing) // regsA[2] = major variation (minor variation is set to zero) // regsA[3] = ignored - {0xC4, "map_designate", nullptr, {{REG_SET_FIXED, 4}}, F_V05_V4}, + {0xC4, "map_designate", nullptr, {{R_REG_SET_FIXED, 4}}, F_V05_V4}, // Locks (masterkey_on) or unlocks (masterkey_off) all doors {0xC5, "masterkey_on", nullptr, {}, F_V05_V4}, @@ -1088,13 +1100,13 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Sets the time displayed in the timer window. The value in regA should be // a number of seconds. - {0xC9, "winset_time", nullptr, {REG}, F_V05_V4}, + {0xC9, "winset_time", nullptr, {R_REG}, F_V05_V4}, // Returns the time from the system clock. // - On DC, reads from hardware registers // - On GC, reads from the TBRs // - On PCv2, XB, and BB, calls QueryPerformanceCounter - {0xCA, "getmtime", nullptr, {REG}, F_V05_V4}, + {0xCA, "getmtime", nullptr, {W_REG}, F_V05_V4}, // Creates an item in the quest board. // valueA = index of item (0-5) @@ -1115,8 +1127,8 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regsA[2] = entity (client ID, 0x1000 + enemy ID, or 0x4000 + object ID) // regsA[3] = y offset (as integer) // valueB is required in pre-v3 but is ignored - {0xCD, "particle_id", "particle_id_V1", {{REG32_SET_FIXED, 4}, I32}, F_V05_V2}, - {0xCD, "particle_id", "particle_id_V3", {{REG_SET_FIXED, 4}}, F_V3_V4}, + {0xCD, "particle_id", "particle_id_V1", {{R_REG32_SET_FIXED, 4}, I32}, F_V05_V2}, + {0xCD, "particle_id", "particle_id_V3", {{R_REG_SET_FIXED, 4}}, F_V3_V4}, // Creates an NPC. // regsA[0-2] = position (x, y, z as integers) @@ -1125,8 +1137,8 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // according to qedit.info) // regsA[5] = template index (see 6x69 in CommandFormats.hh) // regsA[6] = client ID - {0xCE, "npc_crptalk_id", "npc_crptalk_id_V1", {{REG32_SET_FIXED, 7}, I32}, F_V05_V2}, - {0xCE, "npc_crptalk_id", "npc_crptalk_id_V3", {{REG_SET_FIXED, 7}}, F_V3_V4}, + {0xCE, "npc_crptalk_id", "npc_crptalk_id_V1", {{R_REG32_SET_FIXED, 7}, I32}, F_V05_V2}, + {0xCE, "npc_crptalk_id", "npc_crptalk_id_V3", {{R_REG_SET_FIXED, 7}}, F_V3_V4}, // Deletes all strings registered with npc_text. {0xCF, "npc_text_clear_all", "npc_lang_clean", {}, F_V05_V4}, @@ -1138,7 +1150,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // data1[4]. The matching conditions are the same as in find_inventory_item // except that data1[4] must match regsA[3], instead of zero. Returns the // item ID in regB, or FFFFFFFF if not found. - {0xD1, "find_inventory_item_ex", "pl_chk_item2", {{REG_SET_FIXED, 4}, REG}, F_V1_V4}, + {0xD1, "find_inventory_item_ex", "pl_chk_item2", {{R_REG_SET_FIXED, 4}, W_REG}, F_V1_V4}, // Enables/disables the main menu and shortcut menu. {0xD2, "enable_mainmenu", nullptr, {}, F_V1_V4}, @@ -1158,14 +1170,14 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Closes the Quest Board message window {0xD7, "close_msg_qb", nullptr, {}, F_V1_V4}, - // Writes the valueB (a single byte) to the event flag (valueA) + // Writes the valueB (a single byte) to the event flag specified by valueA. {0xD8, "set_eventflag", "set_eventflag_v1", {I32, I32}, F_V1_V2 | F_ARGS}, {0xD8, "set_eventflag", "set_eventflag_v3", {I32, I32}, F_V3_V4 | F_ARGS}, // Sets regA to valueB, and sends 6x77 so other clients will also set their // local regA to valueB. - {0xD9, "sync_register", "sync_leti", {REG32, I32}, F_V1_V2 | F_ARGS}, - {0xD9, "sync_register", "sync_leti", {REG, I32}, F_V3_V4 | F_ARGS}, + {0xD9, "sync_register", "sync_leti", {W_REG32, I32}, F_V1_V2 | F_ARGS}, + {0xD9, "sync_register", "sync_leti", {W_REG, I32}, F_V3_V4 | F_ARGS}, // TODO: Document these {0xDA, "set_returnhunter", nullptr, {}, F_V1_V4}, @@ -1180,7 +1192,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Finds an item in the player's bank, and clears its entry in the bank. // regsA[0-5] = item.data1[0-5] (bank item must exactly match all bytes) // regB = 1 if item was found and cleared, 0 if not - {0xDE, "delete_bank_item", "unknownDE", {{REG_SET_FIXED, 6}, REG}, F_V1_V4}, + {0xDE, "delete_bank_item", "unknownDE", {{R_REG_SET_FIXED, 6}, W_REG}, F_V1_V4}, // Sets NPC AI behaviors. // regsA[0] = unknown (TODO) @@ -1207,8 +1219,8 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regsA[12] = attack technique probability (in range [0, 100]) // regsA[13] = unknown (TODO); appears to be a distance range // valueB = NPC template to modify (00-3F) - {0xDF, "npc_param", "npc_param_V1", {{REG32_SET_FIXED, 14}, I32}, F_V1_V2}, - {0xDF, "npc_param", "npc_param_V3", {{REG_SET_FIXED, 14}, I32}, F_V3_V4 | F_ARGS}, + {0xDF, "npc_param", "npc_param_V1", {{R_REG32_SET_FIXED, 14}, I32}, F_V1_V2}, + {0xDF, "npc_param", "npc_param_V3", {{R_REG_SET_FIXED, 14}, I32}, F_V3_V4 | F_ARGS}, // TODO(DX): Document this. It enables a flag that affects some logic in // TBoss1Dragon::update. The flag is disabled when the Dragon's boss arena @@ -1225,8 +1237,8 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Sets camera parameters for the current frame. // regsA[0-2] = relative location of focus point from player // regsA[3-5] = relative location of camera from player - {0xE2, "pcam_param", "pcam_param_V1", {{REG32_SET_FIXED, 6}}, F_V1_V2}, - {0xE2, "pcam_param", "pcam_param_V3", {{REG_SET_FIXED, 6}}, F_V3_V4}, + {0xE2, "pcam_param", "pcam_param_V1", {{R_REG32_SET_FIXED, 6}}, F_V1_V2}, + {0xE2, "pcam_param", "pcam_param_V3", {{R_REG_SET_FIXED, 6}}, F_V3_V4}, // Triggers set event (valueB) on floor (valueA). Sends 6x67. {0xE3, "start_setevt", "start_setevt_v1", {I32, I32}, F_V1_V2 | F_ARGS}, @@ -1237,22 +1249,22 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { {0xE5, "warp_off", nullptr, {}, F_V1_V4}, // Returns the client ID of the local client - {0xE6, "get_client_id", "get_slotnumber", {REG}, F_V1_V4}, + {0xE6, "get_client_id", "get_slotnumber", {W_REG}, F_V1_V4}, // Returns the client ID of the lobby/game leader - {0xE7, "get_leader_id", "get_servernumber", {REG}, F_V1_V4}, + {0xE7, "get_leader_id", "get_servernumber", {W_REG}, F_V1_V4}, // Sets an event flag from a register. In v3 and later, this is not needed, // since set_eventflag can be called with F_ARGS, but it still exists. - {0xE8, "set_eventflag2", nullptr, {I32, REG}, F_V1_V4 | F_ARGS}, + {0xE8, "set_eventflag2", nullptr, {I32, R_REG}, F_V1_V4 | F_ARGS}, // regA %= regB // This is exactly the same as the mod opcode (including its quirk). - {0xE9, "mod2", "res", {REG, REG}, F_V1_V4}, + {0xE9, "mod2", "res", {W_REG, R_REG}, F_V1_V4}, // regA %= valueB // This is exactly the same as the modi opcode (including its quirk). - {0xEA, "modi2", "unknownEA", {REG, I32}, F_V1_V4}, + {0xEA, "modi2", "unknownEA", {W_REG, I32}, F_V1_V4}, // Changes the background music. create_bgmctrl must be run before doing // this. The values for valueA are the same as for playbgm_epi. @@ -1263,7 +1275,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regsA[0] = switch flag number // regsA[1] = floor number // regsA[2] = flags (see 6x05 definition in CommandFormats.hh) - {0xEC, "update_switch_flag", "sw_send", {{REG_SET_FIXED, 3}}, F_V1_V4}, + {0xEC, "update_switch_flag", "sw_send", {{R_REG_SET_FIXED, 3}}, F_V1_V4}, // Creates a BGM controller object. Use this before set_bgm. {0xED, "create_bgmctrl", nullptr, {}, F_V1_V4}, @@ -1274,17 +1286,18 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Like sync_register, but takes the value from another register rather // than using an immediate value. On v3 and later, this is identical to // sync_register. - {0xEF, "sync_register2", "sync_let", {I32, REG32}, F_V1_V2}, - {0xEF, "sync_register2", nullptr, {REG, I32}, F_V3_V4 | F_ARGS}, + {0xEF, "sync_register2", "sync_let", {W_REG32, R_REG32}, F_V1_V2}, + {0xEF, "sync_register2", nullptr, {W_REG, I32}, F_V3_V4 | F_ARGS}, + // Same as sync_register2, but sends the value via UDP if UDP is enabled. // This opcode was removed after GC NTE and is missing from v3 and v4. - {0xF0, "sync_register2_udp", "send_regwork", {REG32, REG32}, F_V1_V2}, + {0xF0, "sync_register2_udp", "send_regwork", {W_REG32, W_REG32}, F_V1_V2}, // Sets the camera's location and angle. // regsA[0-2] = camera location (x, y, z as integers) // regsA[3-5] = camera focus location (x, y, z as integers) - {0xF1, "leti_fixed_camera", "leti_fixed_camera_V1", {{REG32_SET_FIXED, 6}}, F_V2}, - {0xF1, "leti_fixed_camera", "leti_fixed_camera_V3", {{REG_SET_FIXED, 6}}, F_V3_V4}, + {0xF1, "leti_fixed_camera", "leti_fixed_camera_V1", {{R_REG32_SET_FIXED, 6}}, F_V2}, + {0xF1, "leti_fixed_camera", "leti_fixed_camera_V3", {{R_REG_SET_FIXED, 6}}, F_V3_V4}, // Resets the camera to non-fixed (default behavior). {0xF2, "default_camera_pos1", nullptr, {}, F_V2_V4}, @@ -1315,19 +1328,19 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // doesn't correctly terminate it this way. However, the following field is // the TQuestThread pointer, which is non-null only if the sensor has // already triggered, so it doesn't misbehave. - {0xF801, "set_chat_callback", "set_chat_callback?", {{REG32_SET_FIXED, 5}, CSTRING}, F_V2_V4 | F_ARGS}, + {0xF801, "set_chat_callback", "set_chat_callback?", {{R_REG32_SET_FIXED, 5}, CSTRING}, F_V2_V4 | F_ARGS}, // Returns the difficulty level. Unlike get_difficulty_level_v1, this // correctly returns 3 in Ultimate. - {0xF808, "get_difficulty_level_v2", "get_difflvl2", {REG}, F_V2_V4}, + {0xF808, "get_difficulty_level_v2", "get_difflvl2", {W_REG}, F_V2_V4}, // Returns the number of players in the game. - {0xF809, "get_number_of_players", "get_number_of_player1", {REG}, F_V2_V4}, + {0xF809, "get_number_of_players", "get_number_of_player1", {W_REG}, F_V2_V4}, // Returns the location of the specified player. // regsA[0-2] = returned location (x, y, z as integers) // regB = client ID - {0xF80A, "get_coord_of_player", nullptr, {{REG_SET_FIXED, 3}, REG}, F_V2_V4}, + {0xF80A, "get_coord_of_player", nullptr, {{W_REG_SET_FIXED, 3}, R_REG}, F_V2_V4}, // Enables or disables the area map and minimap. {0xF80B, "enable_map", nullptr, {}, F_V2_V4}, @@ -1340,7 +1353,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // template, 3: nothing) // regsA[3] = major variation // regsA[4] = minor variation - {0xF80D, "map_designate_ex", nullptr, {{REG_SET_FIXED, 5}}, F_V2_V4}, + {0xF80D, "map_designate_ex", nullptr, {{R_REG_SET_FIXED, 5}}, F_V2_V4}, // Enables or disables weapon dropping upon death for a player. // Sends 6x81 (disable) or 6x82 (enable). @@ -1414,7 +1427,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { {0xF81C, "ba_start", "ba_disp_msg", {CSTRING}, F_V2_V4 | F_ARGS}, // Sets the number of levels to gain upon respawn in battle - {0xF81D, "death_lvl_up", nullptr, {I32}, F_V2_V4 | F_ARGS}, + {0xF81D, "ba_death_lvl_up", "death_lvl_up", {I32}, F_V2_V4 | F_ARGS}, // Sets the Meseta mode in battle. valueA (matches enum): // 0 => ALLOW @@ -1427,11 +1440,11 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regsA[3-8] specify first 6 bytes of an ItemData. This opcode consumes an // item ID, but does nothing else. - {0xF821, "nop_F821", nullptr, {{REG_SET_FIXED, 9}}, F_V2_V4}, + {0xF821, "nop_F821", nullptr, {{R_REG_SET_FIXED, 9}}, F_V2_V4}, // This opcode does nothing. It has two branches (one for online, one for // offline), but both branches do nothing. - {0xF822, "nop_F822", nullptr, {REG}, F_V2_V4}, + {0xF822, "nop_F822", nullptr, {R_REG}, F_V2_V4}, // Sets the challenge template index. See Client::create_challenge_overlay // for details on how the template is used. @@ -1443,26 +1456,26 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Sets the factor by which all EXP is multiplied in challenge mode. // The multiplier value is regsA[0] + (regsA[1] / regsA[2]). - {0xF825, "exp_multiplication", nullptr, {{REG_SET_FIXED, 3}}, F_V2_V4}, + {0xF825, "exp_multiplication", nullptr, {{R_REG_SET_FIXED, 3}}, F_V2_V4}, // Checks if any player is still alive in challenge mode. Returns 1 if all // players are dead, or 0 if not. - {0xF826, "cmode_check_all_players_dead", "if_player_alive_cm", {REG}, F_V2_V4}, + {0xF826, "cmode_check_all_players_dead", "if_player_alive_cm", {W_REG}, F_V2_V4}, // Checks if all players are still alive in challenge mode. Returns 1 if any // player is dead, or 0 if not. - {0xF827, "cmode_check_any_player_dead", "get_user_is_dead?", {REG}, F_V2_V4}, + {0xF827, "cmode_check_any_player_dead", "get_user_is_dead?", {W_REG}, F_V2_V4}, // Sends the player with client ID regA to floor regB. Does nothing if regA // doesn't refer to the local player. - {0xF828, "go_floor", nullptr, {REG, REG}, F_V2_V4}, + {0xF828, "go_floor", nullptr, {R_REG, R_REG}, F_V2_V4}, // Returns the number of enemies killed (in regB) by the player whose // client ID is regA. This value is capped to 999. - {0xF829, "get_num_kills", nullptr, {REG, REG}, F_V2_V4}, + {0xF829, "get_num_kills", nullptr, {R_REG, W_REG}, F_V2_V4}, // Resets the kill count for the player specified by regA. - {0xF82A, "reset_kills", nullptr, {REG}, F_V2_V4}, + {0xF82A, "reset_kills", nullptr, {R_REG}, F_V2_V4}, // Sets or clears a switch flag, and synchronizes the value to all players. // valueA = floor @@ -1473,35 +1486,35 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Checks a switch flag on the current floor // regsA[0] = switch flag index // regsA[1] = result (0 or 1) - {0xF82D, "read_switch_flag", "if_switch_not_pressed", {{REG_SET_FIXED, 2}}, F_V2_V4}, + {0xF82D, "read_switch_flag", "if_switch_not_pressed", {{W_REG_SET_FIXED, 2}}, F_V2_V4}, // Checks a switch flag on any floor // regsA[0] = floor // regsA[1] = switch flag index // regsA[2] = result (0 or 1) - {0xF82E, "read_switch_flag_on_floor", "if_switch_pressed", {{REG_SET_FIXED, 3}}, F_V2_V4}, + {0xF82E, "read_switch_flag_on_floor", "if_switch_pressed", {{W_REG_SET_FIXED, 3}}, F_V2_V4}, // Enables a player to control the Dragon. valueA specifies the client ID. - {0xF830, "control_dragon", nullptr, {REG}, F_V2_V4}, + {0xF830, "control_dragon", nullptr, {R_REG}, F_V2_V4}, // Disables player control of the Dragon. {0xF831, "release_dragon", nullptr, {}, F_V2_V4}, // Shrinks a player or returns them to normal size. regA specifies the // client ID. - {0xF838, "shrink", nullptr, {REG}, F_V2_V4}, - {0xF839, "unshrink", nullptr, {REG}, F_V2_V4}, + {0xF838, "shrink", nullptr, {R_REG}, F_V2_V4}, + {0xF839, "unshrink", nullptr, {R_REG}, F_V2_V4}, // These set some camera parameters for the specified player. These // parameters appear to be unused, so these opcodes essentially do nothing. // regsA[0] = client ID // regsA[1-3] = a Vector3F (x, y, z as integers) - {0xF83A, "set_shrink_cam1", nullptr, {{REG_SET_FIXED, 4}}, F_V2_V4}, - {0xF83B, "set_shrink_cam2", nullptr, {{REG_SET_FIXED, 4}}, F_V2_V4}, + {0xF83A, "set_shrink_cam1", nullptr, {{R_REG_SET_FIXED, 4}}, F_V2_V4}, + {0xF83B, "set_shrink_cam2", nullptr, {{R_REG_SET_FIXED, 4}}, F_V2_V4}, // Shows the timer window in challenge mode. regA is the time value to // display, in seconds. - {0xF83C, "disp_time_cmode", nullptr, {REG}, F_V2_V4}, + {0xF83C, "disp_time_cmode", nullptr, {R_REG}, F_V2_V4}, // Sets the total number of areas across all challenge quests for the // current episode @@ -1546,15 +1559,15 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // enemy. (Default 7) // ba_meseta_score: Sets the score earned per Meseta in the player's // inventory. (Default 0) - {0xF848, "ba_player_give_damage_score", "give_damage_score", {{REG_SET_FIXED, 3}}, F_V2_V4}, - {0xF849, "ba_player_take_damage_score", "take_damage_score", {{REG_SET_FIXED, 3}}, F_V2_V4}, - {0xF84A, "ba_enemy_give_damage_score", "enemy_give_score", {{REG_SET_FIXED, 3}}, F_V2_V4}, - {0xF84B, "ba_enemy_take_damage_score", "enemy_take_score", {{REG_SET_FIXED, 3}}, F_V2_V4}, - {0xF84C, "ba_player_kill_score", "kill_score", {{REG_SET_FIXED, 3}}, F_V2_V4}, - {0xF84D, "ba_player_death_score", "death_score", {{REG_SET_FIXED, 3}}, F_V2_V4}, - {0xF84E, "ba_enemy_kill_score", "enemy_kill_score", {{REG_SET_FIXED, 3}}, F_V2_V4}, - {0xF84F, "ba_enemy_death_score", "enemy_death_score", {{REG_SET_FIXED, 3}}, F_V2_V4}, - {0xF850, "ba_meseta_score", "meseta_score", {{REG_SET_FIXED, 3}}, F_V2_V4}, + {0xF848, "ba_player_give_damage_score", "give_damage_score", {{R_REG_SET_FIXED, 3}}, F_V2_V4}, + {0xF849, "ba_player_take_damage_score", "take_damage_score", {{R_REG_SET_FIXED, 3}}, F_V2_V4}, + {0xF84A, "ba_enemy_give_damage_score", "enemy_give_score", {{R_REG_SET_FIXED, 3}}, F_V2_V4}, + {0xF84B, "ba_enemy_take_damage_score", "enemy_take_score", {{R_REG_SET_FIXED, 3}}, F_V2_V4}, + {0xF84C, "ba_player_kill_score", "kill_score", {{R_REG_SET_FIXED, 3}}, F_V2_V4}, + {0xF84D, "ba_player_death_score", "death_score", {{R_REG_SET_FIXED, 3}}, F_V2_V4}, + {0xF84E, "ba_enemy_kill_score", "enemy_kill_score", {{R_REG_SET_FIXED, 3}}, F_V2_V4}, + {0xF84F, "ba_enemy_death_score", "enemy_death_score", {{R_REG_SET_FIXED, 3}}, F_V2_V4}, + {0xF850, "ba_meseta_score", "meseta_score", {{R_REG_SET_FIXED, 3}}, F_V2_V4}, // Sets the number of traps players can use in battle mode. regsA[1] is the // amount; regsA[0] is the trap type: @@ -1562,7 +1575,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // 1 = Slow trap (internal type 2) // 2 = Confuse trap (internal type 3) // 3 = Freeze trap (internal type 1) - {0xF851, "ba_set_trap_count", "ba_set_trap", {{REG_SET_FIXED, 2}}, F_V2_V4}, + {0xF851, "ba_set_trap_count", "ba_set_trap", {{R_REG_SET_FIXED, 2}}, F_V2_V4}, // Enables (0) or disables (1) the targeting reticle in battle {0xF852, "ba_hide_target_reticle", "ba_set_target", {I32}, F_V2_V4 | F_ARGS}, @@ -1591,8 +1604,8 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // should be done by the server on BB. // regsA[0] = client ID // regsA[1-3] = item.data1[0-2] - {0xF85A, "equip_item", "equip_item_v2", {{REG32_SET_FIXED, 4}}, F_V2}, - {0xF85A, "equip_item", "equip_item_v3", {{REG_SET_FIXED, 4}}, F_V3_V4}, + {0xF85A, "equip_item", "equip_item_v2", {{R_REG32_SET_FIXED, 4}}, F_V2}, + {0xF85A, "equip_item", "equip_item_v3", {{R_REG_SET_FIXED, 4}}, F_V3_V4}, // Unequips an item from a client. Sends 6x26 if an item is unequipped. // valueA = client ID @@ -1640,8 +1653,8 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regA/valueA = client ID (must match local client ID) // regB (must be a register, even on v3/v4) = item.data1[1] // strC = custom name - {0xF862, "give_s_rank_weapon", nullptr, {REG32, REG32, CSTRING}, F_V2}, - {0xF862, "give_s_rank_weapon", nullptr, {I32, REG, CSTRING}, F_V3_V4 | F_ARGS}, + {0xF862, "give_s_rank_weapon", nullptr, {R_REG32, R_REG32, CSTRING}, F_V2}, + {0xF862, "give_s_rank_weapon", nullptr, {I32, R_REG, CSTRING}, F_V3_V4 | F_ARGS}, // Returns the currently-equipped mag's levels. If no mag is equipped, // regsA are unaffected! Make sure to initialize them before using this. @@ -1649,8 +1662,8 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regsA[1] = returned POW level // regsA[2] = returned DEX level // regsA[3] = returned MIND level - {0xF863, "get_mag_levels", nullptr, {{REG32_SET_FIXED, 4}}, F_V2}, - {0xF863, "get_mag_levels", nullptr, {{REG_SET_FIXED, 4}}, F_V3_V4}, + {0xF863, "get_mag_levels", nullptr, {{W_REG32_SET_FIXED, 4}}, F_V2}, + {0xF863, "get_mag_levels", nullptr, {{W_REG_SET_FIXED, 4}}, F_V3_V4}, // Sets the color and rank text if the player manages to complete the // current challenge stage. @@ -1669,7 +1682,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // challenge rank text and color according to the rank they achieved. // Sends 07DF on BB; on other versions, sends nothing. // regA = return value (1 if item successfully created; 0 otherwise) - {0xF867, "award_item_give", "award_item_give_to?", {REG}, F_V2_V4}, + {0xF867, "award_item_give", "award_item_give_to?", {W_REG}, F_V2_V4}, // Specifies where the time threshold is for a challenge rank. // regsA[0] = rank (0 = B, 1 = A, 2 = S) @@ -1677,7 +1690,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // this rank or better) // regsA[2] = award flags mask (generally should be (1 << regsA[0])) // regB = result (0 = failed, 1 = success) - {0xF868, "set_cmode_rank_threshold", "set_cmode_rank", {{REG_SET_FIXED, 3}, REG}, F_V2_V4}, + {0xF868, "set_cmode_rank_threshold", "set_cmode_rank", {{R_REG_SET_FIXED, 3}, W_REG}, F_V2_V4}, // Registers a timing result of (regA) seconds for the current challenge // mode stage. Returns a result code in regB: @@ -1688,23 +1701,23 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // 4 = player did not achieve a new rank this time // 5 = player's inventory is full and can't receive the prize // 6 = internal errors (e.g. save file is missing, stage number not set) - {0xF869, "check_rank_time", nullptr, {REG, REG}, F_V2_V4}, + {0xF869, "check_rank_time", nullptr, {R_REG, W_REG}, F_V2_V4}, // Creates an item in the local player's bank, and saves the player's // challenge rank and title color. Sends 07DF on BB. This is used for the A // and B rank prizes. // regsA = item.data1[0-5] // regB = returned success code (1 = success, 0 = failed) - {0xF86A, "item_create_cmode", nullptr, {{REG_SET_FIXED, 6}, REG}, F_V2_V4}, + {0xF86A, "item_create_cmode", nullptr, {{R_REG_SET_FIXED, 6}, W_REG}, F_V2_V4}, // Sets the effective area for item drops in battle. valueA should be in // the range [1, 10]. - {0xF86B, "ba_set_box_drop_area", "ba_box_drops", {REG}, F_V2_V4}, + {0xF86B, "ba_set_box_drop_area", "ba_box_drops", {R_REG}, F_V2_V4}, // Shows a confirmation window asking if the player is satsified with their // choice of S rank prize and weapon name. // regA = result code (0 = OK, 1 = reconsider) - {0xF86C, "award_item_ok", "award_item_ok?", {REG}, F_V2_V4}, + {0xF86C, "award_item_ok", "award_item_ok?", {W_REG}, F_V2_V4}, // Enables or disables traps' ability to hurt the player who set them {0xF86D, "ba_set_trapself", nullptr, {}, F_V2_V4}, @@ -1724,7 +1737,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { {0xF872, "ba_set_time_limit", nullptr, {I32}, F_V2_V4 | F_ARGS}, // Sets regA to 1 if Dark Falz has been defeated, or 0 otherwise. - {0xF873, "dark_falz_is_dead", "falz_is_dead", {REG}, F_V2_V4}, + {0xF873, "dark_falz_is_dead", "falz_is_dead", {W_REG}, F_V2_V4}, // Sets an override for the challenge rank text. At the time the rank is // checked via check_rank_time, these overrides are applied in the order @@ -1737,19 +1750,19 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Enables or disables the transparency effect, similar to the Stealth // Suit. regA is the client ID. - {0xF875, "enable_stealth_suit_effect", nullptr, {REG}, F_V2_V4}, - {0xF876, "disable_stealth_suit_effect", nullptr, {REG}, F_V2_V4}, + {0xF875, "enable_stealth_suit_effect", nullptr, {R_REG}, F_V2_V4}, + {0xF876, "disable_stealth_suit_effect", nullptr, {R_REG}, F_V2_V4}, // Enables or disables the use of techniques for a player. regA is the // client ID. - {0xF877, "enable_techs", nullptr, {REG}, F_V2_V4}, - {0xF878, "disable_techs", nullptr, {REG}, F_V2_V4}, + {0xF877, "enable_techs", nullptr, {R_REG}, F_V2_V4}, + {0xF878, "disable_techs", nullptr, {R_REG}, F_V2_V4}, // Returns the gender of a character. // regA = client ID // regB = returned gender (0 = male, 1 = female, 2 = no player present or // invalid class flags) - {0xF879, "get_gender", nullptr, {REG, REG}, F_V2_V4}, + {0xF879, "get_gender", nullptr, {R_REG, W_REG}, F_V2_V4}, // Returns the race and class of a character. // regA = client ID @@ -1757,7 +1770,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // player present or invalid class flags) // regsB[1] = returned class (0 = hunter, 1 = ranger, 2 = force, 3 = no // player present or invalid class flags) - {0xF87A, "get_chara_class", nullptr, {REG, {REG_SET_FIXED, 2}}, F_V2_V4}, + {0xF87A, "get_chara_class", nullptr, {R_REG, {W_REG_SET_FIXED, 2}}, F_V2_V4}, // Removes Meseta from a player. Sends 6xC9 on BB. // regsA[0] = client ID @@ -1767,66 +1780,67 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regsB[1] = result code if player not present (0 is written here if there // is no player with the specified client ID) // Note that only one of regsB[0] and regsB[1] is written; the other is - // unchanged. Make sure to initialize these registers properly. - {0xF87B, "take_slot_meseta", nullptr, {{REG_SET_FIXED, 2}, REG}, F_V2_V4}, + // unchanged. Therefore, it's good practice to set regsB[1] to a nonzero + // value before using this opcode. + {0xF87B, "take_slot_meseta", nullptr, {{R_REG_SET_FIXED, 2}, {W_REG_SET_FIXED, 2}}, F_V2_V4}, // Returns the Guild Card file creation time in seconds since 00:00:00 on // 1 January 2000. - {0xF87C, "get_guild_card_file_creation_time", "get_encryption_key", {REG}, F_V2_V4}, + {0xF87C, "get_guild_card_file_creation_time", "get_encryption_key", {W_REG}, F_V2_V4}, // Kills the player whose client ID is regA. - {0xF87D, "kill_player", nullptr, {REG}, F_V2_V4}, + {0xF87D, "kill_player", nullptr, {R_REG}, F_V2_V4}, // Returns (in regA) the player's serial number. On BB, returns 0. - {0xF87E, "get_serial_number", nullptr, {REG}, F_V2_V3}, - {0xF87E, "return_0_F87E", nullptr, {REG}, F_V4}, + {0xF87E, "get_serial_number", nullptr, {W_REG}, F_V2_V3}, + {0xF87E, "return_0_F87E", nullptr, {W_REG}, F_V4}, // Reads an event flag from the system file. // regA = event flag index (0x00-0xFF) // regB = returned event flag value (1 byte) - {0xF87F, "get_eventflag", "read_guildcard_flag", {REG, REG}, F_V2_V4}, + {0xF87F, "get_eventflag", "read_guildcard_flag", {R_REG, W_REG}, F_V2_V4}, // Normally, trap damage is computed with the following formula: // (700.0 * area_factor[area] * 2.0 * (0.01 * level + 0.1)) // This opcode overrides that computation. The value is specified with the // integer and fractional parts split up: the actual value used by the game // will be regsA[0] + (regsA[1] / regsA[2]). - {0xF880, "set_trap_damage", "ba_set_dmgtrap", {{REG_SET_FIXED, 3}}, F_V2_V4}, + {0xF880, "set_trap_damage", "ba_set_dmgtrap", {{R_REG_SET_FIXED, 3}}, F_V2_V4}, // Loads the name of the player whose client ID is regA into a static // buffer, which can later be referred to with "" in message // strings. - {0xF881, "get_pl_name", "get_pl_name?", {REG}, F_V2_V4}, + {0xF881, "get_pl_name", "get_pl_name?", {R_REG}, F_V2_V4}, // Loads the job (Hunter, Ranger, or Force) of the player whose client ID // is regA into a static buffer, which can later be referred to with // "" in message strings. - {0xF882, "get_pl_job", nullptr, {REG}, F_V2_V4}, + {0xF882, "get_pl_job", nullptr, {R_REG}, F_V2_V4}, // Counts the number of players near the specified player. // regsA[0] = client ID // regsA[1] = radius (as integer) // regB = count - {0xF883, "get_player_proximity", "players_in_range", {{REG_SET_FIXED, 2}, REG}, F_V2_V4}, + {0xF883, "get_player_proximity", "players_in_range", {{R_REG_SET_FIXED, 2}, W_REG}, F_V2_V4}, // Writes 2 bytes to the event flags in the system file. // valueA = flag index (must be 254 or less) // regB/valueB = value - {0xF884, "set_eventflag16", "write_guild_flagw", {I32, REG}, F_V2}, + {0xF884, "set_eventflag16", "write_guild_flagw", {I32, R_REG}, F_V2}, {0xF884, "set_eventflag16", "write_guild_flagw", {I32, I32}, F_V3_V4 | F_ARGS}, // Writes 4 bytes to the event flags in the system file. // valueA = flag index (must be 252 or less) // regB/valueB = value - {0xF885, "set_eventflag32", "write_guild_flagl", {I32, REG}, F_V2}, + {0xF885, "set_eventflag32", "write_guild_flagl", {I32, R_REG}, F_V2}, {0xF885, "set_eventflag32", "write_guild_flagl", {I32, I32}, F_V3_V4 | F_ARGS}, // Returns (in regB) the battle result place (1, 2, 3, or 4) of the player // specified by regA. - {0xF886, "ba_get_place", nullptr, {REG, REG}, F_V2_V4}, + {0xF886, "ba_get_place", nullptr, {R_REG, W_REG}, F_V2_V4}, // Returns (in regB) the battle score of the player specified by regA. - {0xF887, "ba_get_score", nullptr, {REG, REG}, F_V2_V4}, + {0xF887, "ba_get_score", nullptr, {R_REG, W_REG}, F_V2_V4}, // TODO: Document these {0xF888, "enable_win_pfx", "ba_close_msg", {}, F_V2_V4}, @@ -1848,29 +1862,30 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // 11 = reviving // 12 = frozen // 13 = warping (BB only) - {0xF88A, "get_player_state", "get_player_status", {REG, REG}, F_V2_V4}, + {0xF88A, "get_player_state", "get_player_status", {R_REG, W_REG}, F_V2_V4}, // Sends a Simple Mail message to the player. // regA (v2) = sender's Guild Card number // valueA (v3+) = number of register that holds sender's Guild Card number // strB = sender name and message (separated by \n) - {0xF88B, "send_mail", nullptr, {REG, CSTRING}, F_V2_V4 | F_ARGS}, + {0xF88B, "send_mail", nullptr, {R_REG, CSTRING}, F_V2_V4 | F_ARGS}, - // Returns the game's major version (2 on DCv2/PC, 3 on GC, 4 on XB and BB) - {0xF88C, "get_game_version", nullptr, {REG}, F_V2_V4}, + // Returns the game's major version (2 on DCv2/PC/GCNTE, 3 on GC/Ep3, 4 on + // Xbox and BB) + {0xF88C, "get_game_version", nullptr, {W_REG}, F_V2_V4}, // Sets the local player's stage completion time in challenge mode. // regA = time in seconds // regB = value to be used in computation of token_v4 (BB only; see 6x95 in // CommandFormats.hh for details) - {0xF88D, "chl_set_timerecord", "chl_set_timerecord?", {REG}, F_V2 | F_V3}, - {0xF88D, "chl_set_timerecord", "chl_set_timerecord?", {REG, REG}, F_V4}, + {0xF88D, "chl_set_timerecord", "chl_set_timerecord?", {R_REG}, F_V2 | F_V3}, + {0xF88D, "chl_set_timerecord", "chl_set_timerecord?", {R_REG, R_REG}, F_V4}, // Gets the current player's completion time for the current challenge // stage in seconds. If the player's time is invalid or faster than the // time set by chl_set_min_time_online (or 5 minutes, if offline), returns // -2. If used in non-challenge mode, returns -1. - {0xF88E, "chl_get_timerecord", "chl_get_timerecord?", {REG}, F_V2_V4}, + {0xF88E, "chl_get_timerecord", "chl_get_timerecord?", {W_REG}, F_V2_V4}, // Sets the probabilities of getting recovery items from challenge mode // graves. There are 10 floating-point values, specified as fractions in an @@ -1890,7 +1905,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regsA[14] / regsA[15]: Chance of getting Monofluid x1 // regsA[16] / regsA[17]: Chance of getting Difluid x1 // regsA[18] / regsA[19]: Chance of getting Trifluid x1 - {0xF88F, "set_cmode_grave_rates", nullptr, {{REG_SET_FIXED, 20}}, F_V2_V4}, + {0xF88F, "set_cmode_grave_rates", nullptr, {{R_REG_SET_FIXED, 20}}, F_V2_V4}, // Clears all levels from the main warp. {0xF890, "clear_mainwarp_all", "clear_area_list", {}, F_V2_V4}, @@ -1911,14 +1926,14 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Reads 2 bytes or 4 bytes from the event flags in the system file. // regA = event flag index // regB = returned value - {0xF896, "get_eventflag16", "read_guildflag_16b", {REG, REG}, F_V2_V4}, - {0xF897, "get_eventflag32", "read_guildflag_32b", {REG, REG}, F_V2_V4}, + {0xF896, "get_eventflag16", "read_guildflag_16b", {R_REG, W_REG}, F_V2_V4}, + {0xF897, "get_eventflag32", "read_guildflag_32b", {R_REG, W_REG}, F_V2_V4}, // regA <<= regB - {0xF898, "shift_left", nullptr, {REG, REG}, F_V2_V4}, + {0xF898, "shift_left", nullptr, {W_REG, R_REG}, F_V2_V4}, // regA >>= regB - {0xF899, "shift_right", nullptr, {REG, REG}, F_V2_V4}, + {0xF899, "shift_right", nullptr, {W_REG, R_REG}, F_V2_V4}, // Generates a random number by calling rand(). Note that the returned // value is not uniform! The algorithm generates a uniform random number, @@ -1933,7 +1948,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regsA[0] = minimum value // regsA[1] = maximum value // regB = generated random value - {0xF89A, "get_random", nullptr, {{REG_SET_FIXED, 2}, REG}, F_V2_V4}, + {0xF89A, "get_random", nullptr, {{R_REG_SET_FIXED, 2}, W_REG}, F_V2_V4}, // Clears all game state, including all floor items, set states (enemy and // object), enemy and object states, wave event flags, and switch flags. @@ -1944,7 +1959,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // 0 = not chosen yet // 1 = no (releases players to interact on Pioneer 2) // 2 = yes (restarts the stage) - {0xF89C, "get_chl_retry_choice", "retry_menu", {REG}, F_V2_V4}, + {0xF89C, "get_chl_retry_choice", "retry_menu", {W_REG}, F_V2_V4}, // Creates the retry menu when a challenge stage is failed {0xF89D, "chl_create_retry_menu", "chl_enable_retry", {}, F_V2_V4}, @@ -1955,13 +1970,13 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Restores a player's HP and TP, clears status effects, and revives the // player if dead. // regA = client ID - {0xF89F, "player_recovery", "unknownF89F", {REG}, F_V2_V4}, + {0xF89F, "player_recovery", "unknownF89F", {R_REG}, F_V2_V4}, // These opcodes set, clear, and check (respectively) a flag that appears // to do nothing at all. {0xF8A0, "disable_bosswarp_option", "unknownF8A0", {}, F_V2_V4}, {0xF8A1, "enable_bosswarp_option", "unknownF8A1", {}, F_V2_V4}, - {0xF8A2, "is_bosswarp_opt_disabled", "get_bosswarp_option", {REG}, F_V2_V4}, + {0xF8A2, "is_bosswarp_opt_disabled", "get_bosswarp_option", {W_REG}, F_V2_V4}, // Loads the player's serial number into the "flag buffer", which is a // 4-byte buffer that can be written to event flags. (It's not obvious why @@ -1971,11 +1986,11 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Writes the flag buffer to event flags. regA specifies which event flag // (the first of 4 consecutive flags). - {0xF8A4, "write_flag_buf_to_event_flags", "encrypt_gc_entry_auto", {REG}, F_V2_V4}, + {0xF8A4, "write_flag_buf_to_event_flags", "encrypt_gc_entry_auto", {R_REG}, F_V2_V4}, // Like set_chat_callback, but without a filter string. The meanings of // regsA are the same as for set_chat_callback. - {0xF8A5, "set_chat_callback_no_filter", "chat_detect", {{REG_SET_FIXED, 5}}, F_V2_V4}, + {0xF8A5, "set_chat_callback_no_filter", "chat_detect", {{R_REG_SET_FIXED, 5}}, F_V2_V4}, // Creates a symbol chat collision object. See the description of // TOSymbolchatColli in Map.cc for details on how this object behaves and @@ -1990,22 +2005,22 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // that use this opcode should use the big-endian version of the struct. // (Practically, this means the first 32-bit field and the following 4 // 16-bit fields must be byteswapped.) - {0xF8A6, "set_symbol_chat_collision", "symbol_chat_create", {{REG_SET_FIXED, 10}}, F_V2_V4}, + {0xF8A6, "set_symbol_chat_collision", "symbol_chat_create", {{R_REG_SET_FIXED, 10}}, F_V2_V4}, // Sets the size that a player shrinks to when using the shrink opcode. // regA specified the client ID. The actual shrink size used is // regsB[0] + (regsB[1] / regsB[2]). If regsB[2] is 0, the fractional part // is considered to be zero and not used. - {0xF8A7, "set_shrink_size", nullptr, {REG, {REG_SET_FIXED, 3}}, F_V2_V4}, + {0xF8A7, "set_shrink_size", nullptr, {R_REG, {R_REG_SET_FIXED, 3}}, F_V2_V4}, // Sets the amount by which techniques level up upon respawn in battle. {0xF8A8, "ba_death_tech_level_up", "death_tech_lvl_up2", {I32}, F_V2_V4 | F_ARGS}, // Returns 1 if Vol Opt has been defeated in the current game/quest. - {0xF8A9, "vol_opt_is_dead", "volopt_is_dead", {REG}, F_V2_V4}, + {0xF8A9, "vol_opt_is_dead", "volopt_is_dead", {W_REG}, F_V2_V4}, // Returns 1 if the local player has a challenge mode grave message. - {0xF8AA, "is_there_grave_message", nullptr, {REG}, F_V2_V4}, + {0xF8AA, "is_there_grave_message", nullptr, {W_REG}, F_V2_V4}, // Returns the local player's battle mode records. The values returned are // the first 7 fields of the PlayerRecordsBattle structure (see @@ -2013,7 +2028,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regsA[0-3] = number of times placed 1st, 2nd, 3rd, and 4th, respectively // regsA[4] = number of disconnects // regsA[5-6] = unknown (TODO) - {0xF8AB, "get_ba_record", nullptr, {{REG_SET_FIXED, 7}}, F_V2_V4}, + {0xF8AB, "get_ba_record", nullptr, {{W_REG_SET_FIXED, 7}}, F_V2_V4}, // Returns the current player's challenge mode rank. Reads from the state // corresponding to the current game mode (that is, reads from online Ep1 @@ -2023,41 +2038,41 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // 1 = B rank // 2 = A rank // 3 = S rank - {0xF8AC, "get_cmode_prize_rank", nullptr, {REG}, F_V2_V4}, + {0xF8AC, "get_cmode_prize_rank", nullptr, {W_REG}, F_V2_V4}, // Returns the number of players (in regA). Unlike get_number_of_players, // this counts the number of objects that have entity IDs assigned in the // players' ID space, whereas get_number_of_players counds the number of // TObjPlayer objects. For all practical purposes, these should result in // the same number. - {0xF8AD, "get_number_of_players2", "get_number_of_player2", {REG}, F_V2_V4}, + {0xF8AD, "get_number_of_players2", "get_number_of_player2", {W_REG}, F_V2_V4}, // Returns 1 (in regA) if the current game has a nonempty name. The game // name is set by command 8A from the server. - {0xF8AE, "party_has_name", nullptr, {REG}, F_V2_V4}, + {0xF8AE, "party_has_name", nullptr, {W_REG}, F_V2_V4}, // Returns 1 (in regA) if there is a chat message available (that is, if // anyone has sent a chat message in the current game). - {0xF8AF, "someone_has_spoken", nullptr, {REG}, F_V2_V4}, + {0xF8AF, "someone_has_spoken", nullptr, {W_REG}, F_V2_V4}, // Reads a 1-byte, 2-byte, or 4-byte value from the address (regB/valueB) // and places it in regA. - {0xF8B0, "read1", nullptr, {REG, REG}, F_V2}, - {0xF8B0, "read1", nullptr, {REG, I32}, F_V3_V4 | F_ARGS}, - {0xF8B1, "read2", nullptr, {REG, REG}, F_V2}, - {0xF8B1, "read2", nullptr, {REG, I32}, F_V3_V4 | F_ARGS}, - {0xF8B2, "read4", nullptr, {REG, REG}, F_V2}, - {0xF8B2, "read4", nullptr, {REG, I32}, F_V3_V4 | F_ARGS}, + {0xF8B0, "read1", nullptr, {W_REG, R_REG}, F_V2}, + {0xF8B0, "read1", nullptr, {W_REG, I32}, F_V3_V4 | F_ARGS}, + {0xF8B1, "read2", nullptr, {W_REG, R_REG}, F_V2}, + {0xF8B1, "read2", nullptr, {W_REG, I32}, F_V3_V4 | F_ARGS}, + {0xF8B2, "read4", nullptr, {W_REG, R_REG}, F_V2}, + {0xF8B2, "read4", nullptr, {W_REG, I32}, F_V3_V4 | F_ARGS}, // Writes a 1-byte, 2-byte, or 4-byte value from regB/valueB to the address // (regA/valueA). On v2 and GC NTE, these opcodes have a bug which makes // them essentially useless: they ignore regB and instead write the value // in regA to the address in regA. - {0xF8B3, "write1", nullptr, {REG, REG}, F_V2}, + {0xF8B3, "write1", nullptr, {R_REG, R_REG}, F_V2}, {0xF8B3, "write1", nullptr, {I32, I32}, F_V3_V4 | F_ARGS}, - {0xF8B4, "write2", nullptr, {REG, REG}, F_V2}, + {0xF8B4, "write2", nullptr, {R_REG, R_REG}, F_V2}, {0xF8B4, "write2", nullptr, {I32, I32}, F_V3_V4 | F_ARGS}, - {0xF8B5, "write4", nullptr, {REG, REG}, F_V2}, + {0xF8B5, "write4", nullptr, {R_REG, R_REG}, F_V2}, {0xF8B5, "write4", nullptr, {I32, I32}, F_V3_V4 | F_ARGS}, // Returns a bitmask of 5 different types of detectable hacking. This @@ -2074,12 +2089,12 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // are_rare_drops_allowed in ItemCreator.cc) // 0x10 = any bits in validation_flags in the character file are set (see // PSOGCCharacterFile::Character in SaveFileFormats.hh) - {0xF8B6, "check_for_hacking", "is_mag_hacked", {REG}, F_V2_V4}, + {0xF8B6, "check_for_hacking", "is_mag_hacked", {W_REG}, F_V2_V4}, // Challenge mode cannot be completed unless this many seconds have passed // since the stage began. If not set or if offline, 5 minutes is used as // the threshold instead. - {0xF8B7, "chl_set_min_time_online", "unknownF8B7", {REG}, F_V2_V4}, + {0xF8B7, "chl_set_min_time_online", "unknownF8B7", {R_REG}, F_V2_V4}, // Disables the challenge mode retry menu {0xF8B8, "disable_retry_menu", "unknownF8B8", {}, F_V2_V4}, @@ -2093,18 +2108,19 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Behaves exactly the same as write_flag_buf_to_event_flags (F8A4). This // is the last opcode implemented before v3. - {0xF8BB, "write_flag_buf_to_event_flags2", "unknownF8BB", {REG}, F_V2_V4}, + {0xF8BB, "write_flag_buf_to_event_flags2", "unknownF8BB", {R_REG}, F_V2_V4}, // Returns (in regB) the Guild Card number of the player in the slot // specified by regA. If there is no player in that slot, returns FFFFFFFF. // This opcode is only implemented on certain later versions of PC v2, and // not on any v3 or later versions. - {0xF8BC, "get_player_guild_card_number", nullptr, {REG, REG}, F_PC_V2}, + {0xF8BC, "get_player_guild_card_number", nullptr, {R_REG, W_REG}, F_PC_V2}, // Sets the current episode. Must be used in the start label. valueA should // be 0 for Episode 1 (which is the default), 1 for Episode 2, or 2 for - // Episode 4 (BB only). - {0xF8BC, "set_episode", nullptr, {I32}, F_V3_V4 | F_SET_EPISODE}, + // Episode 4 (BB only). This opcode also resets the floor configuration, so + // it will undo any effects of the map_designate family of opcodes. + {0xF8BC, "set_episode", nullptr, {I32}, F_V3_V4}, // This opcode returns (in regsB) the full symbol chat data for the symbol // chat currently being said by the player specified in regA. The symbol @@ -2114,7 +2130,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // create_symbol_chat_monitor is run first. // This opcode is only implemented on certain later versions of PC v2, and // not on any v3 or later versions. - {0xF8BD, "get_current_symbol_chat", nullptr, {REG, {REG_SET_FIXED, 15}}, F_PC_V2}, // TODO: Document args + {0xF8BD, "get_current_symbol_chat", nullptr, {R_REG, {W_REG_SET_FIXED, 15}}, F_PC_V2}, // This opcode is enables the usage of get_current_symbol_chat. // This opcode is only implemented on certain later versions of PC v2, and @@ -2139,8 +2155,8 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // 0 = failed (server sent a D7 command) // 1 = pending // 2 = complete - {0xF8C1, "get_dl_status", nullptr, {REG}, F_V3}, - {0xF8C1, "nop_F8C1", nullptr, {REG}, F_V4}, + {0xF8C1, "get_dl_status", nullptr, {W_REG}, F_V3}, + {0xF8C1, "nop_F8C1", nullptr, {R_REG}, F_V4}, // Prepares to load a GBA ROM from a previous file_dl_req opcode. Does // nothing on Xbox and BB. @@ -2158,20 +2174,20 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // 4 = complete // This opcode always returns 0 on Xbox, and does nothing (doesn't even // affect regA) on BB. - {0xF8C3, "start_or_update_gba_joyboot", "get_gba_state?", {REG}, F_GC_V3 | F_GC_EP3TE | F_GC_EP3}, - {0xF8C3, "return_0_F8C3", nullptr, {REG}, F_XB_V3}, - {0xF8C3, "nop_F8C3", nullptr, {REG}, F_V4}, + {0xF8C3, "start_or_update_gba_joyboot", "get_gba_state?", {W_REG}, F_GC_V3 | F_GC_EP3TE | F_GC_EP3}, + {0xF8C3, "return_0_F8C3", nullptr, {W_REG}, F_XB_V3}, + {0xF8C3, "nop_F8C3", nullptr, {R_REG}, F_V4}, // Shows the challenge mode result window in split-screen mode. Does // nothing on BB. // regA = completion time in seconds, as returned by chl_get_timerecord - {0xF8C4, "congrats_msg_multi_cm", "unknownF8C4", {REG}, F_V3}, - {0xF8C4, "nop_F8C4", nullptr, {REG}, F_V4}, + {0xF8C4, "congrats_msg_multi_cm", "unknownF8C4", {R_REG}, F_V3}, + {0xF8C4, "nop_F8C4", nullptr, {R_REG}, F_V4}, // Checks if the stage is done in offline challenge mode. Returns 1 if the // stage is still in progress, or 0 if it's completed or failed. - {0xF8C5, "stage_in_progress_multi_cm", "stage_end_multi_cm", {REG}, F_V3}, - {0xF8C5, "nop_F8C5", nullptr, {REG}, F_V4}, + {0xF8C5, "stage_in_progress_multi_cm", "stage_end_multi_cm", {W_REG}, F_V3}, + {0xF8C5, "nop_F8C5", nullptr, {R_REG}, F_V4}, // Causes a fade to black, then exits the game. This is the same result as // receiving a 6x73 command. @@ -2180,50 +2196,50 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Causes a player to perform an animation. // regA = client ID // regB = animation number (TODO: document these) - {0xF8C7, "use_animation", nullptr, {REG, REG}, F_V3_V4}, + {0xF8C7, "use_animation", nullptr, {R_REG, R_REG}, F_V3_V4}, // Stops an animation started with use_animation. // regA = client ID - {0xF8C8, "stop_animation", nullptr, {REG}, F_V3_V4}, + {0xF8C8, "stop_animation", nullptr, {R_REG}, F_V3_V4}, // Causes a player to run to a location, as 6x42 does. Sends 6x42. // regsA[0-2] = location (x, y, z as integers; y is ignored) // regsA[3] = client ID - {0xF8C9, "run_to_coord", nullptr, {{REG_SET_FIXED, 4}, REG}, F_V3_V4}, + {0xF8C9, "run_to_coord", nullptr, {{R_REG_SET_FIXED, 4}, R_REG}, F_V3_V4}, // Makes a player invincible, or removes their invincibility. // regA = client ID // regB = enable invicibility (1 = enable, 0 = disable) - {0xF8CA, "set_slot_invincible", nullptr, {REG, REG}, F_V3_V4}, + {0xF8CA, "set_slot_invincible", nullptr, {R_REG, R_REG}, F_V3_V4}, // Removes a player's invicibility. clear_slot_invincible rXX is equivalent // to set_slot_invincible rXX, 0. // regA = client ID - {0xF8CB, "clear_slot_invincible", "set_slot_targetable?", {REG}, F_V3_V4}, + {0xF8CB, "clear_slot_invincible", "set_slot_targetable?", {R_REG}, F_V3_V4}, // These opcodes inflict various status conditions on a player. In the case // of Shifta/Deband/Jellen/Zalure, the effective technicuqe level is 21. // regA = client ID - {0xF8CC, "set_slot_poison", nullptr, {REG}, F_V3_V4}, - {0xF8CD, "set_slot_paralyze", nullptr, {REG}, F_V3_V4}, - {0xF8CE, "set_slot_shock", nullptr, {REG}, F_V3_V4}, - {0xF8CF, "set_slot_freeze", nullptr, {REG}, F_V3_V4}, - {0xF8D0, "set_slot_slow", nullptr, {REG}, F_V3_V4}, - {0xF8D1, "set_slot_confuse", nullptr, {REG}, F_V3_V4}, - {0xF8D2, "set_slot_shifta", nullptr, {REG}, F_V3_V4}, - {0xF8D3, "set_slot_deband", nullptr, {REG}, F_V3_V4}, - {0xF8D4, "set_slot_jellen", nullptr, {REG}, F_V3_V4}, - {0xF8D5, "set_slot_zalure", nullptr, {REG}, F_V3_V4}, + {0xF8CC, "set_slot_poison", nullptr, {R_REG}, F_V3_V4}, + {0xF8CD, "set_slot_paralyze", nullptr, {R_REG}, F_V3_V4}, + {0xF8CE, "set_slot_shock", nullptr, {R_REG}, F_V3_V4}, + {0xF8CF, "set_slot_freeze", nullptr, {R_REG}, F_V3_V4}, + {0xF8D0, "set_slot_slow", nullptr, {R_REG}, F_V3_V4}, + {0xF8D1, "set_slot_confuse", nullptr, {R_REG}, F_V3_V4}, + {0xF8D2, "set_slot_shifta", nullptr, {R_REG}, F_V3_V4}, + {0xF8D3, "set_slot_deband", nullptr, {R_REG}, F_V3_V4}, + {0xF8D4, "set_slot_jellen", nullptr, {R_REG}, F_V3_V4}, + {0xF8D5, "set_slot_zalure", nullptr, {R_REG}, F_V3_V4}, // Same as leti_fixed_camera, but takes floating-point arguments. // regsA[0-2] = camera location (x, y, z as floats) // regsA[3-5] = camera focus location (x, y, z as floats) - {0xF8D6, "fleti_fixed_camera", nullptr, {{REG_SET_FIXED, 6}}, F_V3_V4 | F_ARGS}, + {0xF8D6, "fleti_fixed_camera", nullptr, {{R_REG_SET_FIXED, 6}}, F_V3_V4 | F_ARGS}, // Sets the camera to follow the player at a fixed angle. // valueA = client ID // regsB[0-2] = camera angle (x, y, z as floats) - {0xF8D7, "fleti_locked_camera", nullptr, {I32, {REG_SET_FIXED, 3}}, F_V3_V4 | F_ARGS}, + {0xF8D7, "fleti_locked_camera", nullptr, {I32, {R_REG_SET_FIXED, 3}}, F_V3_V4 | F_ARGS}, // This opcode appears to be exactly the same as default_camera_pos. // TODO: Is there any difference? @@ -2243,24 +2259,24 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regsE[0-2] = result point (x, y, z as floats) // regsE[3] = the result code (0 = failed, 1 = success) // labelF = control point entries (array of valueA VectorXYZTF structures) - {0xF8DB, "get_vector_from_path", "unknownF8DB", {I32, FLOAT32, FLOAT32, I32, {REG_SET_FIXED, 4}, SCRIPT16}, F_V3_V4 | F_ARGS}, + {0xF8DB, "get_vector_from_path", "unknownF8DB", {I32, FLOAT32, FLOAT32, I32, {W_REG_SET_FIXED, 4}, SCRIPT16}, F_V3_V4 | F_ARGS}, // Same as npc_text, but only applies to a specific player slot. // valueA = client ID // valueB = situation number (same as for npc_text) // strC = string for NPC to say (up to 52 characters) - {0xF8DC, "npc_text_id", "NPC_action_string", {REG, REG, CSTRING_LABEL16}, F_V3_V4}, + {0xF8DC, "npc_text_id", "NPC_action_string", {R_REG, R_REG, CSTRING_LABEL16}, F_V3_V4}, // Returns a bitmask of the buttons which are currently pressed or held on // this frame. // regA = controller port number // regB = returned button flags - {0xF8DD, "get_held_buttons", "get_pad_cond", {REG, REG}, F_V3_V4}, + {0xF8DD, "get_held_buttons", "get_pad_cond", {R_REG, W_REG}, F_V3_V4}, // Returns a bitmask of the buttons which were newly pressed on this frame. // Buttons which were pressed on prevous frames and still held down are not // returned. Same arguments as get_held_buttons. - {0xF8DE, "get_pressed_buttons", "get_button_cond", {REG, REG}, F_V3_V4}, + {0xF8DE, "get_pressed_buttons", "get_button_cond", {R_REG, W_REG}, F_V3_V4}, // Freezes enemies and makes them untargetable, or unfreezes them and makes // them targetable again. Internally, this toggles a flag that disables @@ -2281,33 +2297,33 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Sets a player's HP or TP to their maximum HP or TP. // regA = client ID - {0xF8E3, "restore_hp", nullptr, {REG}, F_V3_V4}, - {0xF8E4, "restore_tp", nullptr, {REG}, F_V3_V4}, + {0xF8E3, "restore_hp", nullptr, {R_REG}, F_V3_V4}, + {0xF8E4, "restore_tp", nullptr, {R_REG}, F_V3_V4}, // Closes a chat bubble for a player, if one is open. // regA = client ID - {0xF8E5, "close_chat_bubble", nullptr, {REG}, F_V3_V4}, + {0xF8E5, "close_chat_bubble", nullptr, {R_REG}, F_V3_V4}, // Moves a dynamic collision object. // regA = object token (returned by set_obj_param, etc.) // regsB[0-2] = location (x, y, z as integers) - {0xF8E6, "move_coords_object", nullptr, {REG, {REG_SET_FIXED, 3}}, F_V3_V4}, + {0xF8E6, "move_coords_object", nullptr, {R_REG, {R_REG_SET_FIXED, 3}}, F_V3_V4}, // These are the same as their counterparts without _ex, but these return // an object token in regB which can be used with del_obj_param, // move_coords_object, etc. set_obj_param_ex is the same as set_obj_param, // since set_obj_param already returns an object token. - {0xF8E7, "at_coords_call_ex", nullptr, {{REG_SET_FIXED, 5}, REG}, F_V3_V4}, - {0xF8E8, "at_coords_talk_ex", nullptr, {{REG_SET_FIXED, 5}, REG}, F_V3_V4}, - {0xF8E9, "npc_coords_call_ex", "walk_to_coord_call_ex", {{REG_SET_FIXED, 5}, REG}, F_V3_V4}, - {0xF8EA, "party_coords_call_ex", "col_npcinr_ex", {{REG_SET_FIXED, 6}, REG}, F_V3_V4}, - {0xF8EB, "set_obj_param_ex", "unknownF8EB", {{REG_SET_FIXED, 6}, REG}, F_V3_V4}, - {0xF8EC, "npc_check_straggle_ex", "col_plinaw_ex", {{REG_SET_FIXED, 9}, REG}, F_V3_V4}, + {0xF8E7, "at_coords_call_ex", nullptr, {{R_REG_SET_FIXED, 5}, W_REG}, F_V3_V4}, + {0xF8E8, "at_coords_talk_ex", nullptr, {{R_REG_SET_FIXED, 5}, W_REG}, F_V3_V4}, + {0xF8E9, "npc_coords_call_ex", "walk_to_coord_call_ex", {{R_REG_SET_FIXED, 5}, W_REG}, F_V3_V4}, + {0xF8EA, "party_coords_call_ex", "col_npcinr_ex", {{R_REG_SET_FIXED, 6}, W_REG}, F_V3_V4}, + {0xF8EB, "set_obj_param_ex", "unknownF8EB", {{R_REG_SET_FIXED, 6}, W_REG}, F_V3_V4}, + {0xF8EC, "npc_check_straggle_ex", "col_plinaw_ex", {{R_REG_SET_FIXED, 9}, W_REG}, F_V3_V4}, // Returns 1 if the player is doing certain animations. (TODO: Which ones?) // regA = client ID // regB = returned value - {0xF8ED, "animation_check", nullptr, {REG, REG}, F_V3_V4}, + {0xF8ED, "animation_check", nullptr, {R_REG, W_REG}, F_V3_V4}, // Specifies which image to use for the image board in Pioneer 2 (if // placed). Only one image may be loaded at a time. Images must be square @@ -2339,55 +2355,56 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regsE[0-2] = result point (x, y, z as floats) // regsE[3] = the result code (0 = failed, 1 = success) // labelF = control point entries (array of valueA VectorXYZTF structures) - {0xF8F2, "compute_bezier_curve_point", "load_unk_data", {I32, FLOAT32, FLOAT32, I32, {REG_SET_FIXED, 4}, {LABEL16, Arg::DataType::BEZIER_CONTROL_POINT_DATA}}, F_V3_V4 | F_ARGS}, + {0xF8F2, "compute_bezier_curve_point", "load_unk_data", {I32, FLOAT32, FLOAT32, I32, {W_REG_SET_FIXED, 4}, {LABEL16, Arg::DataType::BEZIER_CONTROL_POINT_DATA}}, F_V3_V4 | F_ARGS}, // Creates a timed particle effect. Like the particle opcode, but the // location (and duration, for some reason) are floats. // regsA[0-2] = location (x, y, z as floats) // valueB = effect type // valueC = duration as float (in frames; 30 frames/sec) - {0xF8F3, "particle2", nullptr, {{REG_SET_FIXED, 3}, I32, FLOAT32}, F_V3_V4 | F_ARGS}, + {0xF8F3, "particle2", nullptr, {{R_REG_SET_FIXED, 3}, I32, FLOAT32}, F_V3_V4 | F_ARGS}, // Converts the integer in regB into a float in regA. - {0xF901, "dec2float", nullptr, {REG, REG}, F_V3_V4}, + {0xF901, "dec2float", nullptr, {W_REG, R_REG}, F_V3_V4}, // Converts the float in regB into an integer in regA. - {0xF902, "float2dec", nullptr, {REG, REG}, F_V3_V4}, + {0xF902, "float2dec", nullptr, {W_REG, R_REG}, F_V3_V4}, // These are the same as let and leti. Nominally regB/valueB should be a // float, but the implementation treats it as an int (which is still - // correct). - {0xF903, "flet", "floatlet", {REG, REG}, F_V3_V4}, - {0xF904, "fleti", "floati", {REG, FLOAT32}, F_V3_V4}, + // correct, since the float is already encoded before the handler function + // is called). + {0xF903, "flet", "floatlet", {W_REG, R_REG}, F_V3_V4}, + {0xF904, "fleti", "floati", {W_REG, FLOAT32}, F_V3_V4}, // regA += regB (or valueB), as floats - {0xF908, "fadd", nullptr, {REG, REG}, F_V3_V4}, - {0xF909, "faddi", nullptr, {REG, FLOAT32}, F_V3_V4}, + {0xF908, "fadd", nullptr, {W_REG, R_REG}, F_V3_V4}, + {0xF909, "faddi", nullptr, {W_REG, FLOAT32}, F_V3_V4}, // regA -= regB (or valueB), as floats - {0xF90A, "fsub", nullptr, {REG, REG}, F_V3_V4}, - {0xF90B, "fsubi", nullptr, {REG, FLOAT32}, F_V3_V4}, + {0xF90A, "fsub", nullptr, {W_REG, R_REG}, F_V3_V4}, + {0xF90B, "fsubi", nullptr, {W_REG, FLOAT32}, F_V3_V4}, // regA *= regB (or valueB), as floats - {0xF90C, "fmul", nullptr, {REG, REG}, F_V3_V4}, - {0xF90D, "fmuli", nullptr, {REG, FLOAT32}, F_V3_V4}, + {0xF90C, "fmul", nullptr, {W_REG, R_REG}, F_V3_V4}, + {0xF90D, "fmuli", nullptr, {W_REG, FLOAT32}, F_V3_V4}, // regA /= regB (or valueB), as floats - {0xF90E, "fdiv", nullptr, {REG, REG}, F_V3_V4}, - {0xF90F, "fdivi", nullptr, {REG, FLOAT32}, F_V3_V4}, + {0xF90E, "fdiv", nullptr, {W_REG, R_REG}, F_V3_V4}, + {0xF90F, "fdivi", nullptr, {W_REG, FLOAT32}, F_V3_V4}, // Returns the number of times a player has ever died - not just in the // current quest/game/session! // regA = client ID // regB = returned death count - {0xF910, "get_total_deaths", "get_unknown_count?", {CLIENT_ID, REG}, F_V3_V4 | F_ARGS}, + {0xF910, "get_total_deaths", "get_unknown_count?", {CLIENT_ID, W_REG}, F_V3_V4 | F_ARGS}, // Returns the stack size of the specified item in the player's inventory. // regsA[0] = client ID // regsA[1-3] = item.data1[0-2] // regB = returned amount of item present in player's inventory // If the item is not present, returns 0. - {0xF911, "get_stackable_item_count", "get_item_count", {{REG_SET_FIXED, 4}, REG}, F_V3_V4}, + {0xF911, "get_stackable_item_count", "get_item_count", {{R_REG_SET_FIXED, 4}, W_REG}, F_V3_V4}, // Freezes a character and hides their equips, or does the opposite. // Internally, this toggles the disable-update flag on TL_03. @@ -2419,7 +2436,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Checks if activate_palettex has been run for a player. // regA = client ID // regB = returned flag (0 = not overridden, 1 = overridden) - {0xF919, "get_palettex_activated", "get_paletteX_activated", {CLIENT_ID, REG}, F_V3_V4 | F_ARGS}, + {0xF919, "get_palettex_activated", "get_paletteX_activated", {CLIENT_ID, W_REG}, F_V3_V4 | F_ARGS}, // Checks if activate_palettex has been run for a player. // regA = client ID @@ -2429,7 +2446,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // uses this opcode preceded by only two arg_push opcodes, implying that it // was intended to take two arguments, but the client really does only use // quest_arg_stack[0] and quest_arg_stack[2]. - {0xF91A, "get_palettex_enabled", "get_unknown_paletteX_status?", {CLIENT_ID, I32, REG}, F_V3_V4 | F_ARGS}, + {0xF91A, "get_palettex_enabled", "get_unknown_paletteX_status?", {CLIENT_ID, I32, W_REG}, F_V3_V4 | F_ARGS}, // Disables/enables movement for a player. Unlike disable_movement1, this // does not send 6x2C. @@ -2438,26 +2455,26 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { {0xF91C, "enable_movement2", nullptr, {CLIENT_ID}, F_V3_V4 | F_ARGS}, // Returns the local player's play time in seconds. - {0xF91D, "get_time_played", nullptr, {REG}, F_V3_V4}, + {0xF91D, "get_time_played", nullptr, {W_REG}, F_V3_V4}, // Returns the number of Guild Cards saved to the Guild Card file. - {0xF91E, "get_guildcard_total", nullptr, {REG}, F_V3_V4}, + {0xF91E, "get_guildcard_total", nullptr, {W_REG}, F_V3_V4}, // Returns the amount of Meseta the player has in both their inventory and // bank. // regA = returned Meseta amount in inventory // regB = returned Meseta amount in bank - {0xF91F, "get_slot_meseta", nullptr, {{REG_SET_FIXED, 2}}, F_V3_V4}, + {0xF91F, "get_slot_meseta", nullptr, {{W_REG_SET_FIXED, 2}}, F_V3_V4}, // Returns a player's level. // valueA = client ID // regB = returned level - {0xF920, "get_player_level", nullptr, {CLIENT_ID, REG}, F_V3_V4 | F_ARGS}, + {0xF920, "get_player_level", nullptr, {CLIENT_ID, W_REG}, F_V3_V4 | F_ARGS}, // Returns a player's section ID. // valueA = client ID // regB = returned section ID (see name_to_section_id in StaticGameData.cc) - {0xF921, "get_section_id", "get_Section_ID", {CLIENT_ID, REG}, F_V3_V4 | F_ARGS}, + {0xF921, "get_section_id", "get_Section_ID", {CLIENT_ID, W_REG}, F_V3_V4 | F_ARGS}, // Returns a player's maximum and current HP and TP. // valueA = client ID @@ -2467,7 +2484,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regsB[3] = returned current TP // If there's no player in the given slot, the returned values are all // FFFFFFFF. - {0xF922, "get_player_hp_tp", "get_player_hp", {CLIENT_ID, {REG_SET_FIXED, 4}}, F_V3_V4 | F_ARGS}, + {0xF922, "get_player_hp_tp", "get_player_hp", {CLIENT_ID, {W_REG_SET_FIXED, 4}}, F_V3_V4 | F_ARGS}, // Returns the floor and room ID of the given player. // valueA = client ID @@ -2475,19 +2492,19 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regsB[1] = returned room number // If there's no player in the given slot, the returned values are both // FFFFFFFF. - {0xF923, "get_player_room", "get_floor_number", {CLIENT_ID, {REG_SET_FIXED, 2}}, F_V3_V4 | F_ARGS}, + {0xF923, "get_player_room", "get_floor_number", {CLIENT_ID, {W_REG_SET_FIXED, 2}}, F_V3_V4 | F_ARGS}, // Checks if each player (individually) is near the given location. // regsA[0-1] = location (x, z as integers; y not included) // regsA[2] = radius as integer // regsB[0-3] = returned results for each player slot (0 = player not // present or outside radius; 1 = player within radius) - {0xF924, "get_coord_player_detect", nullptr, {{REG_SET_FIXED, 3}, {REG_SET_FIXED, 4}}, F_V3_V4}, + {0xF924, "get_coord_player_detect", nullptr, {{R_REG_SET_FIXED, 3}, {W_REG_SET_FIXED, 4}}, F_V3_V4}, // Reads the value of a quest counter. // valueA = counter index (0-15) // regB = returned value - {0xF925, "read_counter", "read_global_flag", {I32, REG}, F_V3_V4 | F_ARGS}, + {0xF925, "read_counter", "read_global_flag", {I32, W_REG}, F_V3_V4 | F_ARGS}, // Writes a value to a quest counter. // valueA = counter index (0-15) @@ -2499,11 +2516,11 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regsA[0-2] = item.data1[0-2] // regsA[3] = item.data1[4] // regB = 1 if item was found, 0 if not - {0xF927, "find_bank_item", "item_check_bank", {{REG_SET_FIXED, 4}, REG}, F_V3_V4}, + {0xF927, "find_bank_item", "item_check_bank", {{R_REG_SET_FIXED, 4}, W_REG}, F_V3_V4}, // Returns whether each player is present. // regsA[0-3] = returned flags (for each player: 0 if absent, 1 if present) - {0xF928, "get_players_present", "floor_player_detect", {{REG_SET_FIXED, 4}}, F_V3_V4}, + {0xF928, "get_players_present", "floor_player_detect", {{W_REG_SET_FIXED, 4}}, F_V3_V4}, // Prepares to load a GBA ROM from a local GSL file. Does nothing on Xbox // and BB. @@ -2517,11 +2534,11 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // window opened by open_pack_select. Generally used for subsequent item // choices when trading multiple items. // regA = item ID - {0xF92B, "prevent_item_select", "item_select", {REG}, F_V3_V4}, + {0xF92B, "prevent_item_select", "item_select", {R_REG}, F_V3_V4}, // Returns the item chosen by the player in an open_pack_select window, or - // FFFFFFFF if they canceled it. - {0xF92C, "get_chosen_item_id", "get_item_id", {REG}, F_V3_V4}, + // 0xFFFFFFFF if they canceled it. + {0xF92C, "get_chosen_item_id", "get_item_id", {W_REG}, F_V3_V4}, // Adds a color overlay on the player's screen. The overlay fades in // linearly over the given number of frames. The overlay is not deleted @@ -2566,15 +2583,16 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // strB = message {0xF931, "chat_bubble", nullptr, {I32, CSTRING}, F_V3_V4 | F_ARGS}, - // Sets the episode to be loaded the next time an area is loaded. ValueA is - // the same as for set_episode. - {0xF932, "set_episode2", nullptr, {REG}, F_V3_V4}, + // Sets the episode to be loaded the next time an area is loaded. regA is + // the same as for set_episode. Unlike set_episode, this opcode does not + // reset the floor configuration. + {0xF932, "set_episode2", nullptr, {R_REG}, F_V3_V4}, // Sets the rank prizes in offline challenge mode. // regsA[0] = rank (unusual value order: 0 = S, 1 = B, 2 = A) // regsA[1-6] = item.data1[0-5] - {0xF933, "item_create_multi_cm", "unknownF933", {{REG_SET_FIXED, 7}}, F_V3}, - {0xF933, "nop_F933", nullptr, {{REG_SET_FIXED, 7}}, F_V4}, + {0xF933, "item_create_multi_cm", "unknownF933", {{R_REG_SET_FIXED, 7}}, F_V3}, + {0xF933, "nop_F933", nullptr, {{R_REG_SET_FIXED, 7}}, F_V4}, // Shows a scrolling text window. // valueA = X position on screen @@ -2585,7 +2603,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // valueF = scrolling speed // regG = set to 1 when message has entirely scrolled past // strH = message - {0xF934, "scroll_text", nullptr, {I32, I32, I32, I32, I32, FLOAT32, REG, CSTRING}, F_V3_V4 | F_ARGS}, + {0xF934, "scroll_text", nullptr, {I32, I32, I32, I32, I32, FLOAT32, W_REG, CSTRING}, F_V3_V4 | F_ARGS}, // Creates, destroys, or updates the GBA loading progress bar (same as the // quest download progress bar). These opcodes do nothing on Xbox and BB. @@ -2609,7 +2627,9 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Returns the item data for an item chosen with open_pack_select. // valueA = item ID // regsB[0-11] = returned item.data1[0-11] - {0xF93A, "get_item_info", nullptr, {ITEM_ID, {REG_SET_FIXED, 12}}, F_V3_V4 | F_ARGS}, + // If the item doesn't exist, regsB[0] is set to 0xFFFFFFFF and the rest of + // regsB are unaffected. + {0xF93A, "get_item_info", nullptr, {ITEM_ID, {W_REG_SET_FIXED, 12}}, F_V3_V4 | F_ARGS}, // Wraps an item in the player's inventory. The specified item is deleted // and a new one is created with the wrapped flag set. The new item has a @@ -2630,7 +2650,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Returns the local player's language setting. For values, see // name_for_language_code in StaticGameData.cc. - {0xF93D, "get_lang_setting", "get_lang_setting?", {REG}, F_V3_V4 | F_ARGS}, + {0xF93D, "get_lang_setting", "get_lang_setting?", {W_REG}, F_V3_V4 | F_ARGS}, // Sets some values to be sent to the server with send_statistic. // valueA = stat_id (used in send_statistic); this is set to the quest @@ -2651,12 +2671,12 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regA = result (0 = word not said, 1 = word said) // valueB = client ID // strC = string to match (same semantics as for set_chat_callback) - {0xF940, "check_for_keyword", "keyword", {REG, CLIENT_ID, CSTRING}, F_V3_V4 | F_ARGS}, + {0xF940, "check_for_keyword", "keyword", {W_REG, CLIENT_ID, CSTRING}, F_V3_V4 | F_ARGS}, // Returns a player's Guild Card number. // valueA = client ID // regB = returned Guild Card number - {0xF941, "get_guildcard_num", nullptr, {CLIENT_ID, REG}, F_V3_V4 | F_ARGS}, + {0xF941, "get_guildcard_num", nullptr, {CLIENT_ID, W_REG}, F_V3_V4 | F_ARGS}, // Returns the last symbol chat that a player said (at least, since the // capture buffer was created). Use create_symbol_chat_capture_buffer @@ -2666,7 +2686,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // (see SymbolChatT in PlayerSubordinates.hh for details). // valueA = client ID // regsB[0-14] = returned symbol chat data - {0xF942, "get_recent_symbol_chat", "symchat_unknown", {I32, {REG_SET_FIXED, 15}}, F_V3_V4 | F_ARGS}, + {0xF942, "get_recent_symbol_chat", "symchat_unknown", {I32, {W_REG_SET_FIXED, 15}}, F_V3_V4 | F_ARGS}, // Creates the capture buffer required by get_recent_symbol_chat. {0xF943, "create_symbol_chat_capture_buffer", "unknownF943", {}, F_V3_V4}, @@ -2674,7 +2694,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Checks whether an item is stackable. // valueA = item ID // regB = result (0 = not stackable, 1 = stackable, FFFFFFFF = not found) - {0xF944, "get_item_stackability", "get_wrap_status", {ITEM_ID, REG}, F_V3_V4 | F_ARGS}, + {0xF944, "get_item_stackability", "get_wrap_status", {ITEM_ID, W_REG}, F_V3_V4 | F_ARGS}, // Sets the floor where the players will start. This generally should be // used in the start label (where map_designate, etc. are used). This is @@ -2686,19 +2706,19 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // measured as numbers in the range [0, 65536]. // regA = result value // valueB = input angle - {0xF946, "sin", nullptr, {REG, I32}, F_V3_V4 | F_ARGS}, - {0xF947, "cos", nullptr, {REG, I32}, F_V3_V4 | F_ARGS}, - {0xF948, "tan", nullptr, {REG, I32}, F_V3_V4 | F_ARGS}, + {0xF946, "sin", nullptr, {W_REG, I32}, F_V3_V4 | F_ARGS}, + {0xF947, "cos", nullptr, {W_REG, I32}, F_V3_V4 | F_ARGS}, + {0xF948, "tan", nullptr, {W_REG, I32}, F_V3_V4 | F_ARGS}, // Computes the arctangent of the input ratio. Equivalent C: // regA = (int)((atan2(valueB, valueC) * 65536.0) / (2 * M_PI)) // regA = result (integer angle, 0-65535) // valueB = numerator as float // valueC = denominator as float - {0xF949, "atan2_int", "atan", {REG, FLOAT32, FLOAT32}, F_V3_V4 | F_ARGS}, + {0xF949, "atan2_int", "atan", {W_REG, FLOAT32, FLOAT32}, F_V3_V4 | F_ARGS}, // Sets regA to 1 if Olga Flow has been defeated, or 0 otherwise. - {0xF94A, "olga_flow_is_dead", "olga_is_dead", {REG}, F_V3_V4}, + {0xF94A, "olga_flow_is_dead", "olga_is_dead", {W_REG}, F_V3_V4}, // Creates a timed particle effect. Similar to the particle opcode, but the // created particles have no draw distance, so they are visible from very @@ -2706,7 +2726,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regsA[0-2] = location (x, y, z as integers) // regsA[3] = effect type // regsA[4] = duration (in frames; 30 frames/sec) - {0xF94B, "particle_effect_nc", "particle3", {{REG_SET_FIXED, 5}}, F_V3_V4}, + {0xF94B, "particle_effect_nc", "particle3", {{R_REG_SET_FIXED, 5}}, F_V3_V4}, // Creates a particle effect on a given entity. Similar to the particle_id // opcode, but the created particles have no draw distance, so they are @@ -2715,25 +2735,25 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regsA[1] = duration in frames // regsA[2] = entity (client ID, 0x1000 + enemy ID, or 0x4000 + object ID) // regsA[3] = y offset (as integer) - {0xF94C, "player_effect_nc", "particle3f_id", {{REG_SET_FIXED, 4}}, F_V3_V4}, + {0xF94C, "player_effect_nc", "particle3f_id", {{R_REG_SET_FIXED, 4}}, F_V3_V4}, // Returns 1 in regA if a file named PSO3_CHARACTER is present on either // memory card. This opcode is only available on PSO Plus on GC; that is, // it only exists on JP v1.4, JP v1.5, and US v1.2. - {0xF94D, "has_ep3_save_file", nullptr, {REG}, F_GC_V3 | F_ARGS}, + {0xF94D, "has_ep3_save_file", nullptr, {W_REG}, F_GC_V3 | F_ARGS}, // Gives the player one copy of a card. regA is the card ID. - {0xF94D, "give_card", "is_there_cardbattle?", {REG}, F_GC_EP3TE}, + {0xF94D, "give_card", "is_there_cardbattle?", {R_REG}, F_GC_EP3TE}, // Gives the player one copy of a card, or takes one copy away. // regsA[0] = card_id // regsA[1] = action (give card if >= 0, take card if < 0) - {0xF94D, "give_or_take_card", "is_there_cardbattle?", {{REG_SET_FIXED, 2}}, F_GC_EP3}, + {0xF94D, "give_or_take_card", "is_there_cardbattle?", {{R_REG_SET_FIXED, 2}}, F_GC_EP3}, // TODO(DX): Related to voice chat, but functionality is unknown. valueA is // a client ID; a value is read from that player's TVoiceChatClient object // and (!!value) is placed in regB. This value is set by the 6xB3 command. - {0xF94D, "unknown_F94D", nullptr, {I32, REG}, F_XB_V3 | F_ARGS}, + {0xF94D, "unknown_F94D", nullptr, {I32, W_REG}, F_XB_V3 | F_ARGS}, // These opcodes all do nothing on BB. F94D is presumably the voice chat // opcode from Xbox, which was removed, but it's not clear what the other @@ -2766,7 +2786,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { {0xF951, "bb_map_designate", "BB_Map_Designate", {I8, I8, I8, I8, I8}, F_V4}, // Returns the number of items in the player's inventory. - {0xF952, "bb_get_number_in_pack", "BB_get_number_in_pack", {REG}, F_V4}, + {0xF952, "bb_get_number_in_pack", "BB_get_number_in_pack", {W_REG}, F_V4}, // Requests an item exchange in the player's inventory. Sends 6xD5. // valueA/valueB/valueC = item.data1[0-2] to search for @@ -2779,7 +2799,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // valueA = item ID // regB = returned status (0 = can't be wrapped, 1 = can be wrapped, // 2 = item not found) - {0xF954, "bb_check_wrap", "BB_check_wrap", {I32, REG}, F_V4 | F_ARGS}, + {0xF954, "bb_check_wrap", "BB_check_wrap", {I32, W_REG}, F_V4 | F_ARGS}, // Requests an item exchange for Photon Drops. Sends 6xD7. // valueA/valueB/valueC = item.data1[0-2] for requested item @@ -2818,7 +2838,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Returns 1 if the Episode 4 boss death cutscene is playing, or 0 if not // (even if the boss has already been defeated). - {0xF95A, "bb_is_ep4_boss_dying", nullptr, {REG}, F_V4}, + {0xF95A, "bb_is_ep4_boss_dying", nullptr, {W_REG}, F_V4}, // Requests an item exchange. Sends 6xD9. // valueA = find_item.data1[0-2] (low 3 bytes; high byte unused) @@ -2869,7 +2889,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // 0 = 6xE3 hasn't been received // 1 = the received item is valid // 2 = the received item is invalid, or the item ID was already in use - {0xF961, "bb_get_6xE3_status", "unknownF961", {REG}, F_V4}, + {0xF961, "bb_get_6xE3_status", "unknownF961", {W_REG}, F_V4}, }; static const unordered_map& @@ -3177,7 +3197,7 @@ std::string disassemble_quest_script( case Type::LABEL16: case Type::LABEL32: { uint32_t label_id = (arg.type == Type::LABEL32) ? cmd_r.get_u32l() : cmd_r.get_u16l(); - if (def->flags & F_PASS) { + if (def->flags & F_PUSH_ARG) { arg_stack_values.emplace_back(ArgStackValue::Type::LABEL, label_id); } if (label_id >= function_table.size()) { @@ -3198,8 +3218,8 @@ std::string disassemble_quest_script( break; } case Type::LABEL16_SET: { - if (def->flags & F_PASS) { - throw logic_error("LABEL16_SET cannot be pushed to arg stack"); + if (def->flags & F_PUSH_ARG) { + throw logic_error("LABEL16_SET cannot be pushed to arg list"); } uint8_t num_functions = cmd_r.get_u8(); for (size_t z = 0; z < num_functions; z++) { @@ -3228,17 +3248,18 @@ std::string disassemble_quest_script( } break; } - case Type::REG: { + case Type::R_REG: + case Type::W_REG: { uint8_t reg = cmd_r.get_u8(); - if (def->flags & F_PASS) { + if (def->flags & F_PUSH_ARG) { arg_stack_values.emplace_back((def->opcode == 0x004C) ? ArgStackValue::Type::REG_PTR : ArgStackValue::Type::REG, reg); } dasm_arg = std::format("r{}", reg); break; } - case Type::REG_SET: { - if (def->flags & F_PASS) { - throw logic_error("REG_SET cannot be pushed to arg stack"); + case Type::R_REG_SET: { + if (def->flags & F_PUSH_ARG) { + throw logic_error("REG_SET cannot be pushed to arg list"); } uint8_t num_regs = cmd_r.get_u8(); for (size_t z = 0; z < num_regs; z++) { @@ -3251,17 +3272,19 @@ std::string disassemble_quest_script( } break; } - case Type::REG_SET_FIXED: { - if (def->flags & F_PASS) { - throw logic_error("REG_SET_FIXED cannot be pushed to arg stack"); + case Type::R_REG_SET_FIXED: + case Type::W_REG_SET_FIXED: { + if (def->flags & F_PUSH_ARG) { + throw logic_error("REG_SET_FIXED cannot be pushed to arg list"); } uint8_t first_reg = cmd_r.get_u8(); dasm_arg = std::format("r{}-r{}", first_reg, static_cast(first_reg + arg.count - 1)); break; } - case Type::REG32_SET_FIXED: { - if (def->flags & F_PASS) { - throw logic_error("REG32_SET_FIXED cannot be pushed to arg stack"); + case Type::R_REG32_SET_FIXED: + case Type::W_REG32_SET_FIXED: { + if (def->flags & F_PUSH_ARG) { + throw logic_error("REG32_SET_FIXED cannot be pushed to arg list"); } uint32_t first_reg = cmd_r.get_u32l(); dasm_arg = std::format("r{}-r{}", first_reg, static_cast(first_reg + arg.count - 1)); @@ -3269,7 +3292,7 @@ std::string disassemble_quest_script( } case Type::I8: { uint8_t v = cmd_r.get_u8(); - if (def->flags & F_PASS) { + if (def->flags & F_PUSH_ARG) { arg_stack_values.emplace_back(ArgStackValue::Type::INT, v); } dasm_arg = std::format("0x{:02X}", v); @@ -3277,7 +3300,7 @@ std::string disassemble_quest_script( } case Type::I16: { uint16_t v = cmd_r.get_u16l(); - if (def->flags & F_PASS) { + if (def->flags & F_PUSH_ARG) { arg_stack_values.emplace_back(ArgStackValue::Type::INT, v); } dasm_arg = std::format("0x{:04X}", v); @@ -3285,7 +3308,7 @@ std::string disassemble_quest_script( } case Type::I32: { uint32_t v = cmd_r.get_u32l(); - if (def->flags & F_PASS) { + if (def->flags & F_PUSH_ARG) { arg_stack_values.emplace_back(ArgStackValue::Type::INT, v); } dasm_arg = std::format("0x{:08X}", v); @@ -3293,7 +3316,7 @@ std::string disassemble_quest_script( } case Type::FLOAT32: { float v = cmd_r.get_f32l(); - if (def->flags & F_PASS) { + if (def->flags & F_PUSH_ARG) { arg_stack_values.emplace_back(ArgStackValue::Type::INT, as_type(v)); } dasm_arg = std::format("{:g}", v); @@ -3305,13 +3328,13 @@ std::string disassemble_quest_script( for (uint16_t ch = cmd_r.get_u16l(); ch; ch = cmd_r.get_u16l()) { w.put_u16l(ch); } - if (def->flags & F_PASS) { + if (def->flags & F_PUSH_ARG) { arg_stack_values.emplace_back(tt_utf16_to_utf8(w.str())); } dasm_arg = escape_string(w.str(), TextEncoding::UTF16); } else { string s = cmd_r.get_cstr(); - if (def->flags & F_PASS) { + if (def->flags & F_PUSH_ARG) { arg_stack_values.emplace_back(language ? tt_8859_to_utf8(s) : tt_sega_sjis_to_utf8(s)); } dasm_arg = escape_string(s, encoding_for_language(language)); @@ -3350,7 +3373,7 @@ std::string disassemble_quest_script( case Arg::Type::LABEL32: switch (arg_value.type) { case ArgStackValue::Type::REG: - dasm_arg = std::format("r{}/* warning: cannot determine label data type */", arg_value.as_int); + dasm_arg = std::format("r{} /* warning: cannot determine label data type */", arg_value.as_int); break; case ArgStackValue::Type::LABEL: case ArgStackValue::Type::INT: @@ -3366,8 +3389,10 @@ std::string disassemble_quest_script( dasm_arg = "/* invalid-type */"; } break; - case Arg::Type::REG: - case Arg::Type::REG32: + case Arg::Type::R_REG: + case Arg::Type::W_REG: + case Arg::Type::R_REG32: + case Arg::Type::W_REG32: switch (arg_value.type) { case ArgStackValue::Type::REG: dasm_arg = std::format("regs[r{}]", arg_value.as_int); @@ -3379,8 +3404,10 @@ std::string disassemble_quest_script( dasm_arg = "/* invalid-type */"; } break; - case Arg::Type::REG_SET_FIXED: - case Arg::Type::REG32_SET_FIXED: + case Arg::Type::R_REG_SET_FIXED: + case Arg::Type::W_REG_SET_FIXED: + case Arg::Type::R_REG32_SET_FIXED: + case Arg::Type::W_REG32_SET_FIXED: switch (arg_value.type) { case ArgStackValue::Type::REG: dasm_arg = std::format("regs[r{}]-regs[r{}+{}]", arg_value.as_int, arg_value.as_int, static_cast(arg_def.count - 1)); @@ -3429,9 +3456,9 @@ std::string disassemble_quest_script( } break; case Arg::Type::LABEL16_SET: - case Arg::Type::REG_SET: + case Arg::Type::R_REG_SET: default: - throw logic_error("set-type arg found on arg stack"); + throw logic_error("set-type arg found on arg list"); } if (!is_first_arg) { @@ -3445,7 +3472,7 @@ std::string disassemble_quest_script( } } - if (!(def->flags & F_PASS)) { + if (def->flags & F_ARGS) { arg_stack_values.clear(); } } @@ -3680,153 +3707,6 @@ std::string disassemble_quest_script( return phosg::join(lines, "\n"); } -Episode find_quest_episode_from_script(const void* data, size_t size, Version version) { - phosg::StringReader r(data, size); - - bool use_wstrs = false; - size_t code_offset = 0; - size_t function_table_offset = 0; - Episode header_episode = Episode::NONE; - switch (version) { - case Version::DC_NTE: - case Version::DC_11_2000: - case Version::DC_V1: - case Version::DC_V2: - case Version::PC_NTE: - case Version::PC_V2: - case Version::GC_NTE: - return Episode::EP1; - case Version::GC_V3: - case Version::GC_EP3_NTE: - case Version::GC_EP3: - case Version::XB_V3: { - const auto& header = r.get(); - code_offset = header.code_offset; - function_table_offset = header.function_table_offset; - break; - } - case Version::BB_V4: { - use_wstrs = true; - const auto& header = r.get(); - code_offset = header.code_offset; - function_table_offset = header.function_table_offset; - header_episode = episode_for_quest_episode_number(header.episode); - break; - } - default: - throw logic_error("invalid quest version"); - } - - unordered_set found_episodes; - try { - const auto& opcodes = opcodes_for_version(version); - // The set_episode opcode should always be in the first function (0) - phosg::StringReader cmd_r = r.sub(code_offset + r.pget_u32l(function_table_offset)); - - while (!cmd_r.eof()) { - uint16_t opcode = cmd_r.get_u8(); - if ((opcode & 0xFE) == 0xF8) { - opcode = (opcode << 8) | cmd_r.get_u8(); - } - - const QuestScriptOpcodeDefinition* def = nullptr; - try { - def = opcodes.at(opcode); - } catch (const out_of_range&) { - } - - if (def == nullptr) { - throw runtime_error(std::format("unknown quest opcode {:04X}", opcode)); - } - - if (def->flags & F_RET) { - break; - } - - if (!(def->flags & F_ARGS)) { - for (const auto& arg : def->args) { - using Type = QuestScriptOpcodeDefinition::Argument::Type; - string dasm_arg; - switch (arg.type) { - case Type::LABEL16: - cmd_r.skip(2); - break; - case Type::LABEL32: - cmd_r.skip(4); - break; - case Type::LABEL16_SET: - if (def->flags & F_PASS) { - throw logic_error("LABEL16_SET cannot be pushed to arg stack"); - } - cmd_r.skip(cmd_r.get_u8() * 2); - break; - case Type::REG: - cmd_r.skip(1); - break; - case Type::REG_SET: - if (def->flags & F_PASS) { - throw logic_error("REG_SET cannot be pushed to arg stack"); - } - cmd_r.skip(cmd_r.get_u8()); - break; - case Type::REG_SET_FIXED: - if (def->flags & F_PASS) { - throw logic_error("REG_SET_FIXED cannot be pushed to arg stack"); - } - cmd_r.skip(1); - break; - case Type::REG32_SET_FIXED: - if (def->flags & F_PASS) { - throw logic_error("REG32_SET_FIXED cannot be pushed to arg stack"); - } - cmd_r.skip(4); - break; - case Type::I8: - cmd_r.skip(1); - break; - case Type::I16: - cmd_r.skip(2); - break; - case Type::I32: - if (def->flags & F_SET_EPISODE) { - found_episodes.emplace(episode_for_quest_episode_number(cmd_r.get_u32l())); - } else { - cmd_r.skip(4); - } - break; - case Type::FLOAT32: - cmd_r.skip(4); - break; - case Type::CSTRING: - if (use_wstrs) { - for (uint16_t ch = cmd_r.get_u16l(); ch; ch = cmd_r.get_u16l()) { - } - } else { - for (uint8_t ch = cmd_r.get_u8(); ch; ch = cmd_r.get_u8()) { - } - } - break; - default: - throw logic_error("invalid argument type"); - } - } - } - } - } catch (const exception& e) { - phosg::log_warning_f("Cannot determine episode from quest script ({})", e.what()); - } - - if (found_episodes.size() > 1) { - throw runtime_error("multiple episodes found"); - } else if (found_episodes.size() == 1) { - return *found_episodes.begin(); - } else if (header_episode != Episode::NONE) { - return header_episode; - } else { - return Episode::EP1; - } -} - Episode episode_for_quest_episode_number(uint8_t episode_number) { switch (episode_number) { case 0x00: @@ -4467,23 +4347,32 @@ AssembledQuestScript assemble_quest_script( // If the corresponding argument is a REG or REG_SET_FIXED, push // the register number, not the register's value, since it's an // out-param - if ((arg_def.type == Type::REG) || (arg_def.type == Type::REG32)) { - code_w.put_u8(0x4A); // arg_pushb - auto reg = parse_reg(arg); - reg->offsets.emplace(code_w.size()); - code_w.put_u8(reg->number); - } else if ( - (arg_def.type == Type::REG_SET_FIXED) || - (arg_def.type == Type::REG32_SET_FIXED)) { - auto regs = parse_reg_set_fixed(arg, arg_def.count); - code_w.put_u8(0x4A); // arg_pushb - regs[0]->offsets.emplace(code_w.size()); - code_w.put_u8(regs[0]->number); - } else { - code_w.put_u8(0x48); // arg_pushr - auto reg = parse_reg(arg); - reg->offsets.emplace(code_w.size()); - code_w.put_u8(reg->number); + switch (arg_def.type) { + case Type::R_REG: + case Type::W_REG: + case Type::R_REG32: + case Type::W_REG32: { + code_w.put_u8(0x4A); // arg_pushb + auto reg = parse_reg(arg); + reg->offsets.emplace(code_w.size()); + code_w.put_u8(reg->number); + break; + } + case Type::R_REG_SET_FIXED: + case Type::W_REG_SET_FIXED: + case Type::R_REG32_SET_FIXED: + case Type::W_REG32_SET_FIXED: { + auto regs = parse_reg_set_fixed(arg, arg_def.count); + code_w.put_u8(0x4A); // arg_pushb + regs[0]->offsets.emplace(code_w.size()); + code_w.put_u8(regs[0]->number); + break; + } + default: + code_w.put_u8(0x48); // arg_pushr + auto reg = parse_reg(arg); + reg->offsets.emplace(code_w.size()); + code_w.put_u8(reg->number); } } else if ((arg[0] == '@') && ((arg[1] == 'r') || (arg[1] == 'f'))) { code_w.put_u8(0x4C); // arg_pusha @@ -4573,17 +4462,21 @@ AssembledQuestScript assemble_quest_script( } break; } - case Type::REG: - case Type::REG32: - add_reg(parse_reg(arg), arg_def.type == Type::REG32); + case Type::R_REG: + case Type::W_REG: + case Type::R_REG32: + case Type::W_REG32: + add_reg(parse_reg(arg), (arg_def.type == Type::R_REG32) || (arg_def.type == Type::W_REG32)); break; - case Type::REG_SET_FIXED: - case Type::REG32_SET_FIXED: { + case Type::R_REG_SET_FIXED: + case Type::W_REG_SET_FIXED: + case Type::R_REG32_SET_FIXED: + case Type::W_REG32_SET_FIXED: { auto regs = parse_reg_set_fixed(arg, arg_def.count); - add_reg(regs[0], arg_def.type == Type::REG32_SET_FIXED); + add_reg(regs[0], (arg_def.type == Type::R_REG32_SET_FIXED) || (arg_def.type == Type::W_REG32_SET_FIXED)); break; } - case Type::REG_SET: { + case Type::R_REG_SET: { auto regs = split_set(arg); code_w.put_u8(regs.size()); for (auto reg_arg : regs) { @@ -4756,16 +4649,726 @@ AssembledQuestScript assemble_quest_script( w.write(function_table.data(), function_table.size() * sizeof(function_table[0])); return AssembledQuestScript{ .data = std::move(w.str()), - .metadata = QuestMetadata{ - .quest_number = quest_num, - .version = quest_version, - .language = quest_language, - .episode = quest_episode, - .joinable = quest_joinable, - .max_players = quest_max_players, - .name = quest_name, - .short_description = quest_short_desc, - .long_description = quest_long_desc, - }, + .quest_number = quest_num, + .version = quest_version, + .language = quest_language, + .episode = quest_episode, + .joinable = quest_joinable, + .max_players = quest_max_players, + .name = quest_name, + .short_description = quest_short_desc, + .long_description = quest_long_desc, }; } + +void populate_quest_metadata_from_script( + QuestMetadata& meta, const void* data, size_t size, Version version, uint8_t language) { + phosg::StringReader r(data, size); + uint32_t code_offset = r.size(); + uint32_t function_table_offset = r.size(); + switch (version) { + case Version::DC_NTE: { + const auto& header = r.get(); + meta.episode = Episode::EP1; + meta.max_players = 4; + meta.name = header.name.decode(language); + if (meta.quest_number == 0xFFFFFFFF) { + meta.quest_number = phosg::fnv1a32(meta.name); + } + code_offset = header.code_offset; + function_table_offset = header.function_table_offset; + break; + } + case Version::DC_11_2000: { + const auto& header = r.get(); + meta.episode = Episode::EP1; + meta.max_players = 4; + meta.name = header.name.decode(language); + meta.short_description = header.short_description.decode(language); + meta.long_description = header.long_description.decode(language); + if (meta.quest_number == 0xFFFFFFFF) { + meta.quest_number = phosg::fnv1a32(meta.name); + } + code_offset = header.code_offset; + function_table_offset = header.function_table_offset; + break; + } + case Version::DC_V1: + case Version::DC_V2: { + const auto& header = r.get(); + meta.episode = Episode::EP1; + meta.max_players = 4; + meta.name = header.name.decode(language); + meta.short_description = header.short_description.decode(language); + meta.long_description = header.long_description.decode(language); + if (meta.quest_number == 0xFFFFFFFF) { + meta.quest_number = header.quest_number; + } + code_offset = header.code_offset; + function_table_offset = header.function_table_offset; + break; + } + case Version::PC_NTE: + case Version::PC_V2: { + const auto& header = r.get(); + meta.episode = Episode::EP1; + meta.max_players = 4; + if (meta.quest_number == 0xFFFFFFFF) { + meta.quest_number = header.quest_number; + } + meta.name = header.name.decode(language); + meta.short_description = header.short_description.decode(language); + meta.long_description = header.long_description.decode(language); + code_offset = header.code_offset; + function_table_offset = header.function_table_offset; + break; + } + case Version::GC_NTE: + case Version::GC_V3: + case Version::GC_EP3_NTE: + case Version::GC_EP3: + case Version::XB_V3: { + // Note: This codepath handles Episode 3 quest scripts, which are not the + // same as Episode 3 maps and download quests. Quest scripts (handled + // here) are only used offline in story mode, but can be disassembled + // with disassemble_quest_script, hence we need to be able to parse them. + const auto& header = r.get(); + meta.episode = Episode::EP1; + meta.max_players = 4; + if (meta.quest_number == 0xFFFFFFFF) { + meta.quest_number = header.quest_number; + } + meta.name = header.name.decode(language); + meta.short_description = header.short_description.decode(language); + meta.long_description = header.long_description.decode(language); + code_offset = header.code_offset; + function_table_offset = header.function_table_offset; + break; + } + case Version::BB_V4: { + const auto& header = r.get(); + meta.episode = episode_for_quest_episode_number(header.episode); + meta.joinable |= header.joinable; + meta.max_players = 4; + if (meta.quest_number == 0xFFFFFFFF) { + meta.quest_number = header.quest_number; + } + meta.name = header.name.decode(language); + meta.short_description = header.short_description.decode(language); + meta.long_description = header.long_description.decode(language); + code_offset = header.code_offset; + function_table_offset = header.function_table_offset; + break; + } + default: + throw logic_error("invalid quest version"); + } + + const auto& opcodes = opcodes_for_version(version); + bool version_has_args = F_HAS_ARGS & v_flag(version); + + struct RegisterFile { + // All registers are initially zero + size_t current_checkpoint = 0; + struct Register { + size_t checkpoint = 0; + uint32_t known_value = 0; + bool written = false; + }; + std::array regs; + + std::string str() const { + string ret = "["; + for (size_t z = 0; z < this->regs.size(); z++) { + if (this->is_valid(z) && this->regs[z].written) { + if (ret.size() > 1) { + ret += ","; + } + ret += std::format("r{}={}", z, this->get(z)); + } + } + ret += "]"; + return ret; + } + + uint32_t get(size_t which) const { + const auto& reg = this->regs[which & 0xFF]; + if (reg.checkpoint >= this->current_checkpoint) { + return reg.known_value; + } + throw runtime_error(std::format("value for r{} not known", which)); + } + inline void set(size_t which, uint32_t value) { + auto& reg = this->regs[which & 0xFF]; + reg.known_value = value; + reg.checkpoint = this->current_checkpoint; + reg.written = true; + } + inline void invalidate(size_t which) { + this->regs[which & 0xFF].checkpoint = this->current_checkpoint - 1; + } + inline void invalidate_sequence(size_t first, size_t count) { + if (count > 0x100) { + throw runtime_error("invalid count in invalidate_sequence"); + } + for (size_t z = 0; z < count; z++) { + this->invalidate(first); + } + } + inline void invalidate_all() { + this->current_checkpoint++; + } + inline bool is_valid(size_t which) const { + return this->regs[which & 0xFF].checkpoint >= this->current_checkpoint; + } + }; + + auto get_label_offset = [&](size_t label) -> uint32_t { + return code_offset + r.pget_u32l(function_table_offset + 4 * label); + }; + + // The set_episode opcode and floor remapping opcodes should always be in + // the first function (0), so we simulate that. But battle and challenge + // quests can also have setup opcodes in the floor handlers, so we have to + // simulate those too. + deque pending_fn_offsets{get_label_offset(0)}; + unordered_set done_fn_offsets; + shared_ptr battle_rules; + meta.assign_default_areas(version, meta.episode); + while (!pending_fn_offsets.empty()) { + uint32_t start_offset = pending_fn_offsets.front(); + pending_fn_offsets.pop_front(); + if (!done_fn_offsets.emplace(start_offset).second) { + continue; + } + // phosg::fwrite_fmt(stderr, "Trace: examining function starting at {:X}\n", start_offset - code_offset); + + vector args_list; + RegisterFile regs; + r.go(start_offset); + try { + while (!r.eof()) { + uint16_t opcode = r.get_u8(); + if ((opcode & 0xFE) == 0xF8) { + opcode = (opcode << 8) | r.get_u8(); + } + + const QuestScriptOpcodeDefinition* def = nullptr; + try { + def = opcodes.at(opcode); + } catch (const out_of_range&) { + } + if (def == nullptr) { + throw runtime_error(std::format("unknown quest opcode {:04X}", opcode)); + } + // phosg::fwrite_fmt(stderr, "... Trace: {:08X} -> {:04X} {} with {}\n", r.where() - (opcode > 0x100 ? 2 : 1) - code_offset, opcode, def->name, regs.str()); + + bool default_args = false; + bool use_args = version_has_args && (def->flags & F_ARGS); + + auto get_single_int32_arg = [&]() -> uint32_t { + if (use_args) { + if (args_list.size() != 1) { + throw runtime_error(std::format("incorrect argument count to {}", def->name)); + } + return args_list[0]; + } else { + return r.get_u32l(); + } + }; + + switch (opcode) { + case 0x0000: // nop + break; + + case 0x0001: // ret + case 0x0002: // sync + // We stop analyzing at sync because all of the opcodes we care about + // must happen on the first frame the thread runs (either in the + // start label or the floor handler). + r.go(r.size()); + break; + + case 0x0006: // va_end + regs.invalidate_sequence(1, 7); + break; + + case 0x0007: // va_call + r.skip(2); + regs.invalidate_all(); + break; + + case 0x0008: { // let + uint8_t a = r.get_u8(); + regs.set(a, regs.get(r.get_u8())); + break; + } + case 0x0009: { // leti + uint8_t a = r.get_u8(); + regs.set(a, r.get_u32l()); + break; + } + case 0x000A: { // leta (v1/v2); letb (v3/v4) + uint8_t a = r.get_u8(); + if (is_v1_or_v2(version)) { // leta + regs.invalidate(a); + r.skip(1); + } else { // letb + regs.set(a, r.get_u8()); + } + break; + } + case 0x000B: { // letw + uint8_t a = r.get_u8(); + regs.set(a, r.get_u16l()); + break; + } + case 0x000C: // leta (v3/v4) + regs.invalidate(r.get_u8()); + r.skip(1); + break; + case 0x000D: // leto + regs.invalidate(r.get_u8()); + r.skip(2); + break; + + case 0x0010: // set + regs.set(r.get_u8(), 1); + break; + case 0x0011: // clear + regs.set(r.get_u8(), 0); + break; + case 0x0012: { // rev + uint8_t a = r.get_u8(); + if (regs.is_valid(a)) { + regs.set(a, !regs.get(a)); + } + break; + } + + case 0x0028: { // jmp + uint32_t offset = get_label_offset(r.get_u16l()); + if (!done_fn_offsets.emplace(offset).second) { + r.go(r.size()); + } else { + r.go(offset); + } + break; + } + case 0x0029: // call + r.skip(2); + regs.invalidate_all(); + break; + + case 0x002C: // jmp_eq + case 0x002E: // jmp_ne + case 0x0030: // ujmp_gt + case 0x0032: // jmp_gt + case 0x0034: // ujmp_lt + case 0x0036: // jmp_lt + case 0x0038: // ujmp_ge + case 0x003A: // jmp_ge + case 0x003C: // ujmp_le + case 0x003E: // jmp_le + r.skip(4); // 2x R_REG, LABEL16 + break; + case 0x002D: // jmpi_eq + case 0x002F: // jmpi_ne + case 0x0031: // ujmpi_gt + case 0x0033: // jmpi_gt + case 0x0035: // ujmpi_lt + case 0x0037: // jmpi_lt + case 0x0039: // ujmpi_ge + case 0x003B: // jmpi_ge + case 0x003D: // ujmpi_le + case 0x003F: // jmpi_le + r.skip(7); // R_REG, I32, LABEL16 + break; + + case 0x0040: // switch_jmp + case 0x0041: { // switch_call + r.get_u8(); + r.skip(2 * r.get_u8()); + if (opcode == 0x0041) { + regs.invalidate_all(); + } + break; + } + + case 0x0045: { // stack_popm + uint8_t a = r.get_u8(); + regs.invalidate_sequence(a, r.get_u32l()); + break; + } + + case 0x0048: // arg_pushr + args_list.emplace_back(regs.get(r.get_u8())); + break; + case 0x0049: // arg_pushl + args_list.emplace_back(r.get_u32l()); + break; + case 0x004A: // arg_pushb + args_list.emplace_back(r.get_u8()); + break; + case 0x004B: // arg_pushw + args_list.emplace_back(r.get_u16l()); + break; + case 0x004C: // arg_pusha + r.skip(1); + break; + case 0x004D: // arg_pusho + args_list.emplace_back(get_label_offset(r.get_u16l())); + break; + case 0x004E: // arg_pushs + args_list.emplace_back(r.where()); + if (uses_utf16(version)) { + while (r.get_u16l()) { + } + } else { + while (r.get_u8()) { + } + } + break; + + case 0x0095: { // set_floor_handler + // We have to follow these because battle quests define their rules + // in the floor handler, not in the start label + if (use_args) { + if (args_list.size() != 2) { + throw runtime_error("incorrect argument count for set_floor_handler"); + } + pending_fn_offsets.emplace_back(get_label_offset(args_list[1])); + } else { + r.skip(4); // Floor number + pending_fn_offsets.emplace_back(get_label_offset(r.get_u16l())); + } + break; + } + + case 0xF80D: { // map_designate_ex + uint8_t base_reg = r.get_u8(); + meta.area_for_floor.at(regs.get(base_reg)) = regs.get(base_reg + 1); + // phosg::fwrite_fmt(stderr, ">>> Trace: map_designate_ex fa[{}]={}\n", regs.get(base_reg), regs.get(base_reg + 1)); + break; + } + + case 0xF811: // clear_ba_rules + battle_rules = make_shared(); + meta.battle_rules = battle_rules; + break; + + case 0xF812: // ba_set_tech_disk_mode + switch (get_single_int32_arg()) { + case 0: + battle_rules->tech_disk_mode = BattleRules::TechDiskMode::FORBID_ALL; + break; + case 1: + battle_rules->tech_disk_mode = BattleRules::TechDiskMode::ALLOW; + break; + case 2: + battle_rules->tech_disk_mode = BattleRules::TechDiskMode::LIMIT_LEVEL; + break; + default: + throw runtime_error("invalid battle tech disk mode"); + } + break; + + case 0xF813: // ba_set_weapon_and_armor_mode + switch (get_single_int32_arg()) { + case 0: + battle_rules->weapon_and_armor_mode = BattleRules::WeaponAndArmorMode::FORBID_ALL; + break; + case 1: + battle_rules->weapon_and_armor_mode = BattleRules::WeaponAndArmorMode::ALLOW; + break; + case 2: + battle_rules->weapon_and_armor_mode = BattleRules::WeaponAndArmorMode::CLEAR_AND_ALLOW; + break; + case 3: + battle_rules->weapon_and_armor_mode = BattleRules::WeaponAndArmorMode::FORBID_RARES; + break; + default: + throw runtime_error("invalid battle weapon and armor mode"); + } + break; + + case 0xF814: // ba_set_forbid_mags + switch (get_single_int32_arg()) { + case 0: + battle_rules->mag_mode = BattleRules::MagMode::FORBID_ALL; + break; + case 1: + battle_rules->mag_mode = BattleRules::MagMode::ALLOW; + break; + default: + throw runtime_error("invalid battle mag mode"); + } + break; + + case 0xF815: // ba_set_tool_mode + switch (get_single_int32_arg()) { + case 0: + battle_rules->tool_mode = BattleRules::ToolMode::FORBID_ALL; + break; + case 1: + battle_rules->tool_mode = BattleRules::ToolMode::ALLOW; + break; + case 2: + battle_rules->tool_mode = BattleRules::ToolMode::CLEAR_AND_ALLOW; + break; + default: + throw runtime_error("invalid battle tool mode"); + } + break; + + case 0xF816: // ba_set_trap_mode + switch (get_single_int32_arg()) { + case 0: + battle_rules->trap_mode = BattleRules::TrapMode::DEFAULT; + break; + case 1: + battle_rules->trap_mode = BattleRules::TrapMode::ALL_PLAYERS; + break; + default: + throw runtime_error("invalid battle trap mode"); + } + break; + + case 0xF817: // ba_set_unused_F817 + battle_rules->unused_F817 = get_single_int32_arg(); + break; + + case 0xF818: // ba_set_respawn + switch (get_single_int32_arg()) { + case 0: + battle_rules->respawn_mode = BattleRules::RespawnMode::FORBID; + break; + case 1: + battle_rules->respawn_mode = BattleRules::RespawnMode::ALLOW; + break; + case 2: + battle_rules->respawn_mode = BattleRules::RespawnMode::LIMIT_LIVES; + break; + default: + throw runtime_error("invalid battle tech disk mode"); + } + break; + + case 0xF819: // ba_set_replace_char + battle_rules->replace_char = get_single_int32_arg(); + break; + + case 0xF81A: // ba_dropwep + battle_rules->drop_weapon = get_single_int32_arg(); + break; + + case 0xF81B: // ba_teams + battle_rules->is_teams = get_single_int32_arg(); + break; + + case 0xF81D: // ba_death_lvl_up + battle_rules->death_level_up = get_single_int32_arg(); + break; + + case 0xF81E: // ba_set_meseta_drop_mode + switch (get_single_int32_arg()) { + case 0: + battle_rules->meseta_mode = BattleRules::MesetaMode::ALLOW; + break; + case 1: + battle_rules->meseta_mode = BattleRules::MesetaMode::FORBID_ALL; + break; + case 2: + battle_rules->meseta_mode = BattleRules::MesetaMode::CLEAR_AND_ALLOW; + break; + default: + throw runtime_error("invalid battle meseta mode"); + } + break; + + case 0xF823: // set_cmode_char_template + meta.challenge_template_index = get_single_int32_arg(); + // phosg::fwrite_fmt(stderr, ">>> Trace: meta.challenge_template_index = {}\n", meta.challenge_template_index); + break; + + case 0xF824: // set_cmode_difficulty + meta.challenge_difficulty = get_single_int32_arg(); + // phosg::fwrite_fmt(stderr, ">>> Trace: meta.challenge_difficulty = {}\n", meta.challenge_difficulty); + break; + + case 0xF825: { // exp_multiplication + uint8_t a = r.get_u8(); + meta.challenge_exp_multiplier = static_cast(regs.get(a)) + static_cast(regs.get(a + 1)) / static_cast(regs.get(a + 2)); + // phosg::fwrite_fmt(stderr, ">>> Trace: meta.challenge_exp_multiplier = {}\n", meta.challenge_exp_multiplier); + break; + } + + case 0xF851: { // ba_set_trap_count + uint8_t a = r.get_u8(); + // Why did they do this? Why not just make the indexes the same? + std::array trap_types = {0, 2, 3, 1}; + battle_rules->trap_counts.at(trap_types.at(regs.get(a))) = regs.get(a + 1); + break; + } + + case 0xF852: // ba_hide_target_reticle + battle_rules->hide_target_reticle = get_single_int32_arg(); + break; + + case 0xF85E: // ba_enable_sonar + battle_rules->enable_sonar = get_single_int32_arg(); + break; + + case 0xF85F: // ba_sonar_count + battle_rules->sonar_count = get_single_int32_arg(); + break; + + case 0xF86B: // ba_set_box_drop_area + battle_rules->box_drop_area = regs.get(r.get_u8()); + break; + + case 0xF86F: // ba_set_lives + battle_rules->lives = get_single_int32_arg(); + break; + + case 0xF870: // ba_set_max_tech_level + battle_rules->max_tech_level = get_single_int32_arg(); + break; + + case 0xF871: // ba_set_char_level + battle_rules->char_level = get_single_int32_arg(); + break; + + case 0xF872: // ba_set_time_limit + battle_rules->time_limit = get_single_int32_arg(); + break; + + case 0xF88C: // get_game_version + if (is_v1_or_v2(version)) { + regs.set(r.get_u8(), 2); + } else if (is_gc(version)) { + regs.set(r.get_u8(), 3); + } else { + regs.set(r.get_u8(), 4); + } + break; + + case 0xF89E: // ba_forbid_scape_dolls + battle_rules->forbid_scape_dolls = get_single_int32_arg(); + break; + + case 0xF8A8: // ba_death_tech_level_up + battle_rules->death_tech_level_up = get_single_int32_arg(); + break; + + case 0xF8BC: // set_episode + switch (r.get_u32l()) { + case 0: + meta.episode = Episode::EP1; + break; + case 1: + meta.episode = Episode::EP2; + break; + case 2: + if (!is_v4(version)) { + throw runtime_error("invalid argument to set_episode"); + } + meta.episode = Episode::EP4; + break; + default: + throw runtime_error("invalid argument to set_episode"); + } + meta.assign_default_areas(version, meta.episode); + // phosg::fwrite_fmt(stderr, ">>> Trace: meta.episode = {}\n", name_for_episode(meta.episode)); + break; + + case 0xF932: // set_episode2 + // This takes a register as an argument, so we can't handle it here + throw runtime_error("quest uses set_episode2"); + + case 0xF951: { // bb_map_designate + uint8_t floor = r.get_u8(); + meta.area_for_floor.at(floor) = r.get_u8(); + r.skip(3); // entities_list_type, vars.layout, vars.entities + // phosg::fwrite_fmt(stderr, ">>> Trace: bb_map_designate fa[{}]={}\n", floor, meta.area_for_floor.at(floor)); + break; + } + + default: + // phosg::fwrite_fmt(stderr, "Trace: unhandled opcode at {:X}: {:04X} ({})\n", r.where() - code_offset, opcode, def->name); + default_args = true; + } + + if (default_args) { + // Read and skip all immediate args, and invalidate any registers + for (size_t arg_index = 0; arg_index < def->args.size(); arg_index++) { + const auto& arg = def->args[arg_index]; + + using Type = QuestScriptOpcodeDefinition::Argument::Type; + switch (arg.type) { + case Type::W_REG: + regs.invalidate(use_args ? args_list.at(arg_index) : r.get_u8()); + break; + case Type::W_REG_SET_FIXED: + regs.invalidate_sequence(use_args ? args_list.at(arg_index) : r.get_u8(), arg.count); + break; + case Type::I8: + case Type::R_REG: + case Type::R_REG_SET_FIXED: + r.skip(use_args ? 0 : 1); + break; + case Type::I16: + case Type::LABEL16: + r.skip(use_args ? 0 : 2); + break; + case Type::W_REG32: + regs.invalidate(use_args ? args_list.at(arg_index) : r.get_u32l()); + break; + case Type::W_REG32_SET_FIXED: + regs.invalidate_sequence(use_args ? args_list.at(arg_index) : r.get_u32l(), arg.count); + break; + case Type::I32: + case Type::FLOAT32: + case Type::R_REG32: + case Type::R_REG32_SET_FIXED: + case Type::LABEL32: + r.skip(use_args ? 0 : 4); + break; + case Type::LABEL16_SET: + if (use_args) { + throw logic_error("LABEL16_SET cannot be encoded with F_ARGS"); + } + r.skip(r.get_u8() * 2); + break; + case Type::R_REG_SET: + if (use_args) { + throw logic_error("R_REG_SET cannot be encoded with F_ARGS"); + } + r.skip(r.get_u8()); + break; + case Type::CSTRING: + if (!use_args) { + if (uses_utf16(version)) { + while (r.get_u16l()) { + } + } else { + while (r.get_u8()) { + } + } + } + break; + default: + throw logic_error("invalid argument type"); + } + } + } + + if (!(def->flags & F_PUSH_ARG)) { + args_list.clear(); + } + } + } catch (const runtime_error& e) { + // phosg::fwrite_fmt(stderr, "!!! Trace: function skipped: {}\n", e.what()); + } + } +} diff --git a/src/QuestScript.hh b/src/QuestScript.hh index 7567e76a..d6caa6c1 100644 --- a/src/QuestScript.hh +++ b/src/QuestScript.hh @@ -5,6 +5,7 @@ #include #include +#include "QuestMetadata.hh" #include "StaticGameData.hh" #include "Text.hh" #include "Version.hh" @@ -19,6 +20,18 @@ struct PSOQuestHeaderDCNTE { /* 0020 */ } __packed_ws__(PSOQuestHeaderDCNTE, 0x20); +struct PSOQuestHeaderDC112000 { + /* 0000 */ le_uint32_t code_offset = 0; + /* 0004 */ le_uint32_t function_table_offset = 0; + /* 0008 */ le_uint32_t size = 0; + /* 000C */ le_uint16_t unknown_a1 = 0; + /* 000E */ le_uint16_t unknown_a2 = 0; + /* 0010 */ pstring name; + /* 0030 */ pstring short_description; + /* 00B0 */ pstring long_description; + /* 01D0 */ +} __packed_ws__(PSOQuestHeaderDC112000, 0x1D0); + struct PSOQuestHeaderDC { // Same format for DC v1 and v2 /* 0000 */ le_uint32_t code_offset = 0; /* 0004 */ le_uint32_t function_table_offset = 0; @@ -100,7 +113,8 @@ std::string disassemble_quest_script( bool reassembly_mode = false, bool use_qedit_names = false); -struct QuestMetadata { +struct AssembledQuestScript { + std::string data; int64_t quest_number = -1; Version version = Version::UNKNOWN; uint8_t language = 0xFF; @@ -111,13 +125,9 @@ struct QuestMetadata { std::string short_description; std::string long_description; }; -struct AssembledQuestScript { - std::string data; - QuestMetadata metadata; -}; AssembledQuestScript assemble_quest_script( const std::string& text, const std::vector& script_include_directories, const std::vector& native_include_directories); -Episode find_quest_episode_from_script(const void* data, size_t size, Version version); +void populate_quest_metadata_from_script(QuestMetadata& meta, const void* data, size_t size, Version version, uint8_t language); diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index d5f11399..3348efdf 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -341,14 +341,13 @@ static asio::awaitable on_login_complete(shared_ptr c) { !c->check_flag(Client::Flag::SEND_FUNCTION_CALL_ACTUALLY_RUNS_CODE))) { shared_ptr q; try { - int64_t quest_num = s->enable_send_function_call_quest_numbers.at(c->specific_version); - q = s->default_quest_index->get(quest_num); + q = s->quest_index->get(s->enable_send_function_call_quest_numbers.at(c->specific_version)); } catch (const out_of_range&) { } if (!q) { c->log.info_f("There is no quest to enable server function calls for specific version {:08X}", c->specific_version); } else if (q) { - auto vq = q->version(is_ep3(c->version()) ? Version::GC_V3 : c->version(), 1); + auto vq = q->version(c->version(), 1); if (vq) { c->set_flag(Client::Flag::HAS_SEND_FUNCTION_CALL); c->set_flag(Client::Flag::SEND_FUNCTION_CALL_ACTUALLY_RUNS_CODE); @@ -364,12 +363,14 @@ static asio::awaitable on_login_complete(shared_ptr c) { lobby_data.guild_card_number = c->login->account->account_id; send_command_t(c, 0x64, 0x01, cmd); } else { - c->log.info_f("Sending {} version of quest \"{}\"", char_for_language_code(vq->language), vq->name); + c->log.info_f("Sending {} version of quest \"{}\"", char_for_language_code(vq->language), vq->meta.name); string bin_filename = vq->bin_filename(); string dat_filename = vq->dat_filename(); string xb_filename = vq->xb_filename(); - send_open_quest_file(c, bin_filename, bin_filename, xb_filename, vq->quest_number, QuestFileType::ONLINE, vq->bin_contents); - send_open_quest_file(c, dat_filename, dat_filename, xb_filename, vq->quest_number, QuestFileType::ONLINE, vq->dat_contents); + send_open_quest_file( + c, bin_filename, bin_filename, xb_filename, vq->meta.quest_number, QuestFileType::ONLINE, vq->bin_contents); + send_open_quest_file( + c, dat_filename, dat_filename, xb_filename, vq->meta.quest_number, QuestFileType::ONLINE, vq->dat_contents); if (!is_v1_or_v2(c->version())) { send_command(c, 0xAC, 0x00); @@ -2153,11 +2154,10 @@ static asio::awaitable on_09(shared_ptr c, Channel::Message& msg) case MenuID::QUEST_EP1: case MenuID::QUEST_EP2: { bool is_download_quest = !c->lobby.lock(); - auto quest_index = s->quest_index(c->version()); - if (!quest_index) { + if (!s->quest_index) { send_quest_info(c, "$C7Quests are not available.", 0x00, is_download_quest); } else { - auto q = quest_index->get(cmd.item_id); + auto q = s->quest_index->get(cmd.item_id); if (!q) { send_quest_info(c, "$C4Quest does not\nexist.", 0x00, is_download_quest); } else { @@ -2165,12 +2165,22 @@ static asio::awaitable on_09(shared_ptr c, Channel::Message& msg) if (!vq) { send_quest_info(c, "$C4Quest does not\nexist for this game\nversion.", 0x00, is_download_quest); } else { - send_quest_info(c, vq->long_description, vq->description_flag, is_download_quest); + send_quest_info(c, vq->meta.long_description, vq->meta.description_flag, is_download_quest); } } } break; } + case MenuID::QUEST_EP3: { + auto map = s->ep3_download_map_index->get(cmd.item_id); + if (!map) { + send_quest_info(c, "$C4Map does not exist.", 0x00, true); + } else { + auto vm = map->version(c->language()); + send_quest_info(c, vm->map->description.decode(vm->language), 0x00, true); + } + break; + } case MenuID::GAME: { auto game = s->find_lobby(cmd.item_id); @@ -2245,7 +2255,7 @@ static asio::awaitable on_09(shared_ptr c, Channel::Message& msg) if (game->quest) { info += (game->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)) ? "$C6Quest: " : "$C4Quest: "; - info += remove_color(game->quest->name); + info += remove_color(game->quest->meta.name); info += "\n"; } else if (game->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)) { info += "$C6Quest in progress\n"; @@ -2396,22 +2406,22 @@ static void on_quest_loaded(shared_ptr l) { lc->delete_overlay(); - if ((l->quest->challenge_template_index >= 0) && !is_v4(leader_c->version())) { + if ((l->quest->meta.challenge_template_index >= 0) && !is_v4(leader_c->version())) { // If the leader is BB, they will send an 02DF command that will create // the overlays later; on other versions, we do it at quest start time // (now) instead, hence the version check above. if (is_v4(lc->version())) { lc->change_bank(lc->bb_character_index); } - lc->create_challenge_overlay(lc->version(), l->quest->challenge_template_index, s->level_table(lc->version())); + lc->create_challenge_overlay(lc->version(), l->quest->meta.challenge_template_index, s->level_table(lc->version())); lc->log.info_f("Created challenge overlay"); l->assign_inventory_and_bank_item_ids(lc, true); - } else if (l->quest->battle_rules) { + } else if (l->quest->meta.battle_rules) { if (is_v4(lc->version())) { lc->change_bank(lc->bb_character_index); } - lc->create_battle_overlay(l->quest->battle_rules, s->level_table(lc->version())); + lc->create_battle_overlay(l->quest->meta.battle_rules, s->level_table(lc->version())); lc->log.info_f("Created battle overlay"); } } @@ -2426,16 +2436,16 @@ void set_lobby_quest(shared_ptr l, shared_ptr q, bool substi } // Only allow loading battle/challenge quests if the game mode is correct - if ((q->challenge_template_index >= 0) != (l->mode == GameMode::CHALLENGE)) { + if ((q->meta.challenge_template_index >= 0) != (l->mode == GameMode::CHALLENGE)) { throw runtime_error("incorrect game mode"); } - if ((q->battle_rules != nullptr) != (l->mode == GameMode::BATTLE)) { + if ((q->meta.battle_rules != nullptr) != (l->mode == GameMode::BATTLE)) { throw runtime_error("incorrect game mode"); } auto s = l->require_server_state(); - if (q->joinable) { + if (q->meta.joinable) { l->set_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS); } else { l->set_flag(Lobby::Flag::QUEST_IN_PROGRESS); @@ -2445,11 +2455,14 @@ void set_lobby_quest(shared_ptr l, shared_ptr q, bool substi l->quest = q; if (l->episode != Episode::EP3) { - l->episode = q->episode; + l->episode = q->meta.episode; } - if (l->quest->allowed_drop_modes) { - l->allowed_drop_modes = l->quest->allowed_drop_modes; - l->drop_mode = l->quest->default_drop_mode; + if (l->quest->meta.allowed_drop_modes) { + l->allowed_drop_modes = l->quest->meta.allowed_drop_modes; + l->drop_mode = l->quest->meta.default_drop_mode; + } + if (l->quest->meta.challenge_difficulty >= 0) { + l->difficulty = l->quest->meta.challenge_difficulty; } l->create_item_creator(); @@ -2468,13 +2481,15 @@ void set_lobby_quest(shared_ptr l, shared_ptr q, bool substi lc->channel->disconnect(); break; } - lc->log.info_f("Sending {} version of quest \"{}\"", char_for_language_code(vq->language), vq->name); + lc->log.info_f("Sending {} version of quest \"{}\"", char_for_language_code(vq->language), vq->meta.name); string bin_filename = vq->bin_filename(); string dat_filename = vq->dat_filename(); string xb_filename = vq->xb_filename(); - send_open_quest_file(lc, bin_filename, bin_filename, xb_filename, vq->quest_number, QuestFileType::ONLINE, vq->bin_contents); - send_open_quest_file(lc, dat_filename, dat_filename, xb_filename, vq->quest_number, QuestFileType::ONLINE, vq->dat_contents); + send_open_quest_file( + lc, bin_filename, bin_filename, xb_filename, vq->meta.quest_number, QuestFileType::ONLINE, vq->bin_contents); + send_open_quest_file( + lc, dat_filename, dat_filename, xb_filename, vq->meta.quest_number, QuestFileType::ONLINE, vq->dat_contents); // There is no such thing as command AC (quest barrier) on PSO V1 and V2; // quests just start immediately when they're done downloading. (This is @@ -2532,24 +2547,11 @@ static asio::awaitable on_10_main_menu(shared_ptr c, uint32_t item break; case MainMenuItemID::DOWNLOAD_QUESTS: { - QuestMenuType menu_type = QuestMenuType::DOWNLOAD; if (is_ep3(c->version())) { - menu_type = QuestMenuType::EP3_DOWNLOAD; - // Episode 3 has only download quests, not online quests, so this is - // always the download quest menu. (Episode 3 does actually have - // online quests, but they're served via a server data request - // instead of the file download paradigm that other versions use.) - auto quest_index = s->quest_index(c->version()); - uint16_t version_flags = (1 << static_cast(c->version())); - const auto& categories = quest_index->categories(menu_type, Episode::EP3, version_flags); - if (categories.size() == 1) { - auto quests = quest_index->filter(Episode::EP3, version_flags, categories[0]->category_id); - send_quest_menu(c, quests, true); - break; - } + send_ep3_download_quest_menu(c); + } else { + send_quest_categories_menu(c, QuestMenuType::DOWNLOAD, Episode::NONE); } - - send_quest_categories_menu(c, s->quest_index(c->version()), menu_type, Episode::NONE); break; } @@ -2799,9 +2801,13 @@ static asio::awaitable on_10_game_menu(shared_ptr c, uint32_t item } static asio::awaitable on_10_quest_categories(shared_ptr c, uint32_t item_id) { + // Episode 3 doesn't have this menu + if (is_ep3(c->version())) { + throw runtime_error("Episode 3 client made selection on quest categories menu"); + } + auto s = c->require_server_state(); - auto quest_index = s->quest_index(c->version()); - if (!quest_index) { + if (!s->quest_index) { send_lobby_message_box(c, "$C7Quests are not available."); co_return; } @@ -2814,18 +2820,21 @@ static asio::awaitable on_10_quest_categories(shared_ptr c, uint32 include_condition = l->quest_include_condition(); } - const auto& quests = quest_index->filter(episode, version_flags, item_id, include_condition); + const auto& quests = s->quest_index->filter(episode, version_flags, item_id, include_condition); send_quest_menu(c, quests, !l); } static asio::awaitable on_10_quest_menu(shared_ptr c, uint32_t item_id) { + if (is_ep3(c->version())) { + throw runtime_error("Episode 1/2/4 quests cannot be downloaded by Ep3 clients"); + } + auto s = c->require_server_state(); - auto quest_index = s->quest_index(c->version()); - if (!quest_index) { + if (!s->quest_index) { send_lobby_message_box(c, "$C7Quests are not\navailable."); co_return; } - auto q = quest_index->get(item_id); + auto q = s->quest_index->get(item_id); if (!q) { send_lobby_message_box(c, "$C7Quest does not exist."); co_return; @@ -2840,7 +2849,7 @@ static asio::awaitable on_10_quest_menu(shared_ptr c, uint32_t ite } if (l) { - if (q->episode == Episode::EP3) { + if (q->meta.episode == Episode::EP3) { send_lobby_message_box(c, "$C7Episode 3 quests\ncannot be loaded\nvia this interface."); co_return; } @@ -2860,26 +2869,34 @@ static asio::awaitable on_10_quest_menu(shared_ptr c, uint32_t ite send_lobby_message_box(c, "$C7Quest does not exist\nfor this game version."); co_return; } - // Episode 3 uses the download quest commands (A6/A7) but does not - // expect the server to have already encrypted the quest files, unlike - // other versions. - // TODO: This is not true for Episode 3 Trial Edition. We also would - // have to convert the map to a MapDefinitionTrial, though. - if (is_ep3(vq->version)) { - send_open_quest_file(c, q->name, vq->bin_filename(), "", vq->quest_number, QuestFileType::EPISODE_3, vq->bin_contents); - } else { - vq = vq->create_download_quest(c->language()); - string xb_filename = vq->xb_filename(); - QuestFileType type = vq->pvr_contents ? QuestFileType::DOWNLOAD_WITH_PVR : QuestFileType::DOWNLOAD_WITHOUT_PVR; - send_open_quest_file(c, q->name, vq->bin_filename(), xb_filename, vq->quest_number, type, vq->bin_contents); - send_open_quest_file(c, q->name, vq->dat_filename(), xb_filename, vq->quest_number, type, vq->dat_contents); - if (vq->pvr_contents) { - send_open_quest_file(c, q->name, vq->pvr_filename(), xb_filename, vq->quest_number, type, vq->pvr_contents); - } + vq = vq->create_download_quest(c->language()); + string xb_filename = vq->xb_filename(); + QuestFileType type = vq->pvr_contents ? QuestFileType::DOWNLOAD_WITH_PVR : QuestFileType::DOWNLOAD_WITHOUT_PVR; + send_open_quest_file(c, q->meta.name, vq->bin_filename(), xb_filename, vq->meta.quest_number, type, vq->bin_contents); + send_open_quest_file(c, q->meta.name, vq->dat_filename(), xb_filename, vq->meta.quest_number, type, vq->dat_contents); + if (vq->pvr_contents) { + send_open_quest_file(c, q->meta.name, vq->pvr_filename(), xb_filename, vq->meta.quest_number, type, vq->pvr_contents); } } } +static asio::awaitable on_10_ep3_download_quest_menu(shared_ptr c, uint32_t item_id) { + auto s = c->require_server_state(); + if (!is_ep3(c->version())) { + throw runtime_error("Episode 3 quests can only be downloaded by Ep3 clients"); + } + if (c->lobby.lock()) { + throw runtime_error("Episode 3 quests can only be downloaded when client is not in a lobby"); + } + auto map = s->ep3_download_map_index->get(item_id); + auto vm = map->version(c->language()); + auto name = vm->map->name.decode(vm->language); + string filename = std::format("m{:06}p_{:c}.bin", map->map_number, tolower(char_for_language_code(vm->language))); + auto data = (c->version() == Version::GC_EP3_NTE) ? vm->trial_download() : vm->compressed(false); + send_open_quest_file(c, name, filename, "", map->map_number, QuestFileType::EPISODE_3, data); + co_return; +} + static asio::awaitable on_10_patch_switches(shared_ptr c, uint32_t item_id) { if (item_id == PatchesMenuItemID::GO_BACK) { send_main_menu(c); @@ -3036,6 +3053,9 @@ static asio::awaitable on_10(shared_ptr c, Channel::Message& msg) case MenuID::QUEST_EP2: co_await on_10_quest_menu(c, base_cmd.item_id); break; + case MenuID::QUEST_EP3: + co_await on_10_ep3_download_quest_menu(c, base_cmd.item_id); + break; case MenuID::PATCH_SWITCHES: co_await on_10_patch_switches(c, base_cmd.item_id); break; @@ -3207,7 +3227,7 @@ static asio::awaitable on_A2(shared_ptr c, Channel::Message& msg) } } - send_quest_categories_menu(c, s->quest_index(c->version()), menu_type, l->episode); + send_quest_categories_menu(c, menu_type, l->episode); l->set_flag(Lobby::Flag::QUEST_SELECTION_IN_PROGRESS); } @@ -4075,7 +4095,7 @@ static asio::awaitable on_DF_BB(shared_ptr c, Channel::Message& ms throw runtime_error("non-leader sent 02DF command"); } auto vq = l->quest->version(Version::BB_V4, c->language()); - if (vq->challenge_template_index != static_cast(cmd.template_index)) { + if (vq->meta.challenge_template_index != static_cast(cmd.template_index)) { throw runtime_error("challenge template index in quest metadata does not match index sent by client"); } @@ -4091,7 +4111,7 @@ static asio::awaitable on_DF_BB(shared_ptr c, Channel::Message& ms if (is_v4(lc->version())) { lc->change_bank(lc->bb_character_index); } - lc->create_challenge_overlay(lc->version(), l->quest->challenge_template_index, s->level_table(lc->version())); + lc->create_challenge_overlay(lc->version(), l->quest->meta.challenge_template_index, s->level_table(lc->version())); lc->log.info_f("Created challenge overlay"); l->assign_inventory_and_bank_item_ids(lc, true); } @@ -4103,6 +4123,12 @@ static asio::awaitable on_DF_BB(shared_ptr c, Channel::Message& ms case 0x03DF: { const auto& cmd = check_size_t(msg.data); + if (!l->quest) { + throw runtime_error("challenge mode difficulty config command sent in non-challenge game"); + } + if (static_cast(l->quest->meta.challenge_difficulty) != cmd.difficulty) { + throw runtime_error("incorrect difficulty level"); + } if (l->difficulty != cmd.difficulty) { l->difficulty = cmd.difficulty; l->create_item_creator(); @@ -4112,8 +4138,13 @@ static asio::awaitable on_DF_BB(shared_ptr c, Channel::Message& ms } case 0x04DF: { - const auto& cmd = check_size_t(msg.data); - l->challenge_exp_multiplier = cmd.exp_multiplier; + check_size_t(msg.data); + if (!l->quest) { + throw runtime_error("challenge mode difficulty config command sent in non-challenge game"); + } + l->challenge_exp_multiplier = (l->quest->meta.challenge_exp_multiplier < 0) + ? 1.0 + : l->quest->meta.challenge_exp_multiplier; l->log.info_f("(Challenge mode) EXP multiplier set to {:g}", l->challenge_exp_multiplier); break; } @@ -4907,7 +4938,7 @@ static asio::awaitable on_6F(shared_ptr c, Channel::Message& msg) shared_ptr q; try { int64_t quest_num = s->enable_send_function_call_quest_numbers.at(c->specific_version); - q = s->default_quest_index->get(quest_num); + q = s->quest_index->get(quest_num); } catch (const out_of_range&) { throw std::logic_error("cannot find patch enable quest after it was previously found during login"); } @@ -4915,12 +4946,12 @@ static asio::awaitable on_6F(shared_ptr c, Channel::Message& msg) if (!vq) { throw std::logic_error("cannot find patch enable quest version after it was previously found during login"); } - c->log.info_f("Sending {} version of quest \"{}\"", char_for_language_code(vq->language), vq->name); + c->log.info_f("Sending {} version of quest \"{}\"", char_for_language_code(vq->language), vq->meta.name); string bin_filename = vq->bin_filename(); string dat_filename = vq->dat_filename(); string xb_filename = vq->xb_filename(); - send_open_quest_file(c, bin_filename, bin_filename, xb_filename, vq->quest_number, QuestFileType::ONLINE, vq->bin_contents); - send_open_quest_file(c, dat_filename, dat_filename, xb_filename, vq->quest_number, QuestFileType::ONLINE, vq->dat_contents); + send_open_quest_file(c, bin_filename, bin_filename, xb_filename, vq->meta.quest_number, QuestFileType::ONLINE, vq->bin_contents); + send_open_quest_file(c, dat_filename, dat_filename, xb_filename, vq->meta.quest_number, QuestFileType::ONLINE, vq->dat_contents); co_return; } // Now l is not null @@ -4990,8 +5021,8 @@ static asio::awaitable on_6F(shared_ptr c, Channel::Message& msg) string bin_filename = vq->bin_filename(); string dat_filename = vq->dat_filename(); - send_open_quest_file(c, bin_filename, bin_filename, "", vq->quest_number, QuestFileType::ONLINE, vq->bin_contents); - send_open_quest_file(c, dat_filename, dat_filename, "", vq->quest_number, QuestFileType::ONLINE, vq->dat_contents); + send_open_quest_file(c, bin_filename, bin_filename, "", vq->meta.quest_number, QuestFileType::ONLINE, vq->bin_contents); + send_open_quest_file(c, dat_filename, dat_filename, "", vq->meta.quest_number, QuestFileType::ONLINE, vq->dat_contents); c->set_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST); c->log.info_f("LOADING_RUNNING_JOINABLE_QUEST flag set"); should_resume_game = false; @@ -5058,8 +5089,8 @@ static asio::awaitable on_99(shared_ptr c, Channel::Message& msg) string bin_filename = vq->bin_filename(); string dat_filename = vq->dat_filename(); - send_open_quest_file(c, bin_filename, bin_filename, "", vq->quest_number, QuestFileType::ONLINE, vq->bin_contents); - send_open_quest_file(c, dat_filename, dat_filename, "", vq->quest_number, QuestFileType::ONLINE, vq->dat_contents); + send_open_quest_file(c, bin_filename, bin_filename, "", vq->meta.quest_number, QuestFileType::ONLINE, vq->bin_contents); + send_open_quest_file(c, dat_filename, dat_filename, "", vq->meta.quest_number, QuestFileType::ONLINE, vq->dat_contents); c->set_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST); c->log.info_f("LOADING_RUNNING_JOINABLE_QUEST flag set"); diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index ed6cb118..e0495f07 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -3105,7 +3105,8 @@ static asio::awaitable on_set_quest_flag(shared_ptr c, SubcommandM if (l->drop_mode != ServerDropMode::DISABLED) { EnemyType boss_enemy_type = EnemyType::NONE; bool is_ep2 = (l->episode == Episode::EP2); - if ((l->episode == Episode::EP1) && (c->floor == 0x0E)) { + uint8_t area = l->area_for_floor(c->version(), c->floor); + if ((l->episode == Episode::EP1) && (area == 0x0E)) { // On Normal, Dark Falz does not have a third phase, so send the drop // request after the end of the second phase. On all other difficulty // levels, send it after the third phase. @@ -3114,7 +3115,7 @@ static asio::awaitable on_set_quest_flag(shared_ptr c, SubcommandM } else if ((difficulty != 0) && (flag_num == 0x0037)) { boss_enemy_type = EnemyType::DARK_FALZ_3; } - } else if (is_ep2 && (flag_num == 0x0057) && (c->floor == 0x0D)) { + } else if (is_ep2 && (flag_num == 0x0057) && (area == 0x0D)) { boss_enemy_type = EnemyType::OLGA_FLOW_2; } @@ -3187,9 +3188,9 @@ static asio::awaitable on_sync_quest_register(shared_ptr c, Subcom // If the lock status register is being written, change the game's flags to // allow or forbid joining if (l->quest && - l->quest->joinable && - (l->quest->lock_status_register >= 0) && - (cmd.register_number == l->quest->lock_status_register)) { + l->quest->meta.joinable && + (l->quest->meta.lock_status_register >= 0) && + (cmd.register_number == l->quest->meta.lock_status_register)) { // Lock if value is nonzero; unlock if value is zero if (cmd.value.as_int) { l->set_flag(Lobby::Flag::QUEST_IN_PROGRESS); @@ -3723,10 +3724,8 @@ static asio::awaitable on_set_entity_pos_and_angle_6x17(shared_ptr if (l->episode != Episode::EP1) { throw runtime_error("client sent 6x17 command in non-Ep1 game"); } - // TODO: If a quest is loaded, we should use the quest's floor assignments - // here instead of a constant - if (c->floor != 0x0D) { - throw runtime_error("client sent 6x17 command on floor other than Vol Opt"); + if (l->area_for_floor(c->version(), c->floor) != 0x0D) { + throw runtime_error("client sent 6x17 command in area other than Vol Opt"); } if (cmd.header.entity_id != c->lobby_client_id) { // If the target is on a different floor or does not exist, just drop the @@ -4606,7 +4605,7 @@ static asio::awaitable on_challenge_mode_retry_or_quit(shared_ptr throw runtime_error("6x97 sent by non-leader"); } - if (l->is_game() && (cmd.is_retry == 1) && l->quest && (l->quest->challenge_template_index >= 0)) { + if (l->is_game() && (cmd.is_retry == 1) && l->quest && (l->quest->meta.challenge_template_index >= 0)) { auto s = l->require_server_state(); for (auto& m : l->floor_item_managers) { @@ -4621,7 +4620,7 @@ static asio::awaitable on_challenge_mode_retry_or_quit(shared_ptr if (is_v4(lc->version())) { lc->change_bank(lc->bb_character_index); } - lc->create_challenge_overlay(lc->version(), l->quest->challenge_template_index, s->level_table(c->version())); + lc->create_challenge_overlay(lc->version(), l->quest->meta.challenge_template_index, s->level_table(c->version())); lc->log.info_f("Created challenge overlay"); l->assign_inventory_and_bank_item_ids(lc, true); } diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 1816f1fa..f955019f 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -1645,10 +1645,10 @@ void send_quest_menu_t( } auto& e = entries.emplace_back(); - e.menu_id = ((it.second->episode == Episode::EP1) || (it.second->episode == Episode::EP3)) ? MenuID::QUEST_EP1 : MenuID::QUEST_EP2; - e.item_id = it.second->quest_number; - e.name.encode(vq->name, c->language()); - e.short_description.encode(add_color(vq->short_description), c->language()); + e.menu_id = (it.second->meta.episode == Episode::EP2) ? MenuID::QUEST_EP2 : MenuID::QUEST_EP1; + e.item_id = it.second->meta.quest_number; + e.name.encode(vq->meta.name, c->language()); + e.short_description.encode(add_color(vq->meta.short_description), c->language()); } send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries); } @@ -1666,21 +1666,31 @@ void send_quest_menu_bb( } auto& e = entries.emplace_back(); - e.menu_id = (it.second->episode == Episode::EP1) ? MenuID::QUEST_EP1 : MenuID::QUEST_EP2; - e.item_id = it.second->quest_number; - e.name.encode(vq->name, c->language()); - e.short_description.encode(add_color(vq->short_description), c->language()); + e.menu_id = (it.second->meta.episode == Episode::EP2) ? MenuID::QUEST_EP2 : MenuID::QUEST_EP1; + e.item_id = it.second->meta.quest_number; + e.name.encode(vq->meta.name, c->language()); + e.short_description.encode(add_color(vq->meta.short_description), c->language()); e.disabled = (it.first == QuestIndex::IncludeState::DISABLED) ? 1 : 0; } send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries); } +void send_ep3_download_quest_menu(shared_ptr c) { + auto s = c->require_server_state(); + vector entries; + for (const auto& it : s->ep3_download_map_index->all()) { + auto vm = it.second->version(c->language()); + auto& e = entries.emplace_back(); + e.menu_id = MenuID::QUEST_EP3; + e.item_id = it.first; // map_number + e.name.encode(vm->map->name.decode(vm->language), c->language()); + e.short_description.encode(add_color(vm->map->location_name.decode(vm->language)), c->language()); + } + send_command_vt(c, 0xA4, entries.size(), entries); +} + template -void send_quest_categories_menu_t( - shared_ptr c, - shared_ptr quest_index, - QuestMenuType menu_type, - Episode episode) { +void send_quest_categories_menu_t(shared_ptr c, QuestMenuType menu_type, Episode episode) { QuestIndex::IncludeCondition include_condition = nullptr; if (!c->login->account->check_flag(Account::Flag::DISABLE_QUEST_REQUIREMENTS)) { auto l = c->lobby.lock(); @@ -1694,7 +1704,8 @@ void send_quest_categories_menu_t( } vector entries; - for (const auto& cat : quest_index->categories(menu_type, episode, version_flags, include_condition)) { + auto s = c->require_server_state(); + for (const auto& cat : s->quest_index->categories(menu_type, episode, version_flags, include_condition)) { auto& e = entries.emplace_back(); e.menu_id = cat->use_ep2_icon() ? MenuID::QUEST_CATEGORIES_EP2 : MenuID::QUEST_CATEGORIES_EP1; e.item_id = cat->category_id; @@ -1702,7 +1713,7 @@ void send_quest_categories_menu_t( e.short_description.encode(add_color(cat->description), c->language()); } - bool is_download_menu = (menu_type == QuestMenuType::DOWNLOAD) || (menu_type == QuestMenuType::EP3_DOWNLOAD); + bool is_download_menu = (menu_type == QuestMenuType::DOWNLOAD); send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries); } @@ -1736,15 +1747,11 @@ void send_quest_menu( } } -void send_quest_categories_menu( - shared_ptr c, - shared_ptr quest_index, - QuestMenuType menu_type, - Episode episode) { +void send_quest_categories_menu(shared_ptr c, QuestMenuType menu_type, Episode episode) { switch (c->version()) { case Version::PC_NTE: case Version::PC_V2: - send_quest_categories_menu_t(c, quest_index, menu_type, episode); + send_quest_categories_menu_t(c, menu_type, episode); break; case Version::DC_NTE: case Version::DC_11_2000: @@ -1754,13 +1761,13 @@ void send_quest_categories_menu( case Version::GC_V3: case Version::GC_EP3_NTE: case Version::GC_EP3: - send_quest_categories_menu_t(c, quest_index, menu_type, episode); + send_quest_categories_menu_t(c, menu_type, episode); break; case Version::XB_V3: - send_quest_categories_menu_t(c, quest_index, menu_type, episode); + send_quest_categories_menu_t(c, menu_type, episode); break; case Version::BB_V4: - send_quest_categories_menu_t(c, quest_index, menu_type, episode); + send_quest_categories_menu_t(c, menu_type, episode); break; default: throw logic_error("unimplemented versioned command"); @@ -4023,12 +4030,11 @@ void send_open_quest_file( if (chunk_bytes > 0x400) { chunk_bytes = 0x400; } - send_quest_file_chunk(c, filename, offset / 0x400, - contents->data() + offset, chunk_bytes, (type != QuestFileType::ONLINE)); + send_quest_file_chunk(c, filename, offset / 0x400, contents->data() + offset, chunk_bytes, (type != QuestFileType::ONLINE)); } // If there are still chunks to send, track the file so the chunk - // acknowledgement handler (13 or A7) cna know what to send next + // acknowledgement handler (13 or A7) can know what to send next if (chunks_to_send < total_chunks) { c->sending_files.emplace(filename, contents); c->log.info_f("Opened file {}", filename); diff --git a/src/SendCommands.hh b/src/SendCommands.hh index b53d0d61..08daadb5 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -312,11 +312,8 @@ void send_quest_menu( std::shared_ptr c, const std::vector>>& quests, bool is_download_menu); -void send_quest_categories_menu( - std::shared_ptr c, - std::shared_ptr quest_index, - QuestMenuType menu_type, - Episode episode); +void send_ep3_download_quest_menu(std::shared_ptr c); +void send_quest_categories_menu(std::shared_ptr c, QuestMenuType menu_type, Episode episode); void send_lobby_list(std::shared_ptr c); void send_player_records( diff --git a/src/ServerState.cc b/src/ServerState.cc index 6b2499f9..797ea6fd 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -394,10 +394,6 @@ shared_ptr> ServerState::information_contents_for_client(sh return is_v1_or_v2(c->version()) ? this->information_contents_v2 : this->information_contents_v3; } -shared_ptr ServerState::quest_index(Version version) const { - return is_ep3(version) ? this->ep3_download_quest_index : this->default_quest_index; -} - size_t ServerState::default_min_level_for_game(Version version, Episode episode, uint8_t difficulty) const { const auto& min_levels = is_v4(version) ? this->min_levels_v4 @@ -512,8 +508,8 @@ ItemData ServerState::parse_item_description(Version version, const string& desc } shared_ptr ServerState::common_item_set(Version logic_version, shared_ptr q) const { - if (q && q->common_item_set) { - return q->common_item_set; + if (q && q->meta.common_item_set) { + return q->meta.common_item_set; } else if (is_v1_or_v2(logic_version)) { // TODO: We should probably have a v1 common item set at some point too return this->common_item_sets.at("common-table-v1-v2"); @@ -525,8 +521,8 @@ shared_ptr ServerState::common_item_set(Version logic_versi } shared_ptr ServerState::rare_item_set(Version logic_version, shared_ptr q) const { - if (q && q->rare_item_set) { - return q->rare_item_set; + if (q && q->meta.rare_item_set) { + return q->meta.rare_item_set; } else if (is_v1(logic_version)) { return this->rare_item_sets.at("rare-table-v1"); } else if (is_v2(logic_version)) { @@ -2157,6 +2153,8 @@ void ServerState::load_ep3_cards() { void ServerState::load_ep3_maps() { config_log.info_f("Collecting Episode 3 maps"); this->ep3_map_index = make_shared("system/ep3/maps"); + config_log.info_f("Collecting Episode 3 download maps"); + this->ep3_download_map_index = make_shared("system/ep3/maps-download"); } void ServerState::load_ep3_tournament_state() { @@ -2169,14 +2167,8 @@ void ServerState::load_ep3_tournament_state() { void ServerState::load_quest_index() { config_log.info_f("Collecting quests"); - this->default_quest_index = make_shared("system/quests", this->quest_category_index, this->common_item_sets, this->rare_item_sets, false); - config_log.info_f("Collecting Episode 3 download quests"); - this->ep3_download_quest_index = make_shared( - "system/ep3/maps-download", - this->quest_category_index, - unordered_map>{}, - unordered_map>{}, - true); + this->quest_index = make_shared( + "system/quests", this->quest_category_index, this->common_item_sets, this->rare_item_sets); } void ServerState::compile_functions() { diff --git a/src/ServerState.hh b/src/ServerState.hh index ef8d2f3d..029db8f8 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -183,13 +183,13 @@ struct ServerState : public std::enable_shared_from_this { std::shared_ptr ep3_card_index; std::shared_ptr ep3_card_index_trial; std::shared_ptr ep3_map_index; + std::shared_ptr ep3_download_map_index; std::shared_ptr ep3_com_deck_index; std::shared_ptr ep3_default_ex_values; std::shared_ptr ep3_tournament_ex_values; std::shared_ptr ep3_tournament_final_round_ex_values; std::shared_ptr quest_category_index; - std::shared_ptr default_quest_index; - std::shared_ptr ep3_download_quest_index; + std::shared_ptr quest_index; std::shared_ptr level_table_v1_v2; std::shared_ptr level_table_v3; std::shared_ptr level_table_v4; @@ -375,7 +375,6 @@ struct ServerState : public std::enable_shared_from_this { } std::shared_ptr> information_contents_for_client(std::shared_ptr c) const; - std::shared_ptr quest_index(Version version) const; size_t default_min_level_for_game(Version version, Episode episode, uint8_t difficulty) const; diff --git a/src/ShellCommands.cc b/src/ShellCommands.cc index 56b8efc5..b9f8c835 100644 --- a/src/ShellCommands.cc +++ b/src/ShellCommands.cc @@ -703,7 +703,7 @@ ShellCommand c_create_tournament( +[](ShellCommand::Args& args) -> asio::awaitable> { string name = get_quoted_string(args.args); string map_name = get_quoted_string(args.args); - auto map = args.s->ep3_map_index->for_name(map_name); + auto map = args.s->ep3_map_index->get(map_name); uint32_t num_teams = stoul(get_quoted_string(args.args), nullptr, 0); Episode3::Rules rules; rules.set_defaults(); diff --git a/system/config.example.json b/system/config.example.json index e7f08d95..07dcc81e 100644 --- a/system/config.example.json +++ b/system/config.example.json @@ -639,7 +639,6 @@ // 0x008 - appears in solo mode (BB) // 0x010 - appears at government counter (BB) // 0x020 - appears in download quest menu - // 0x040 - appears in Episode 3 download quest menu // 0x080 - hide quests that don't match the game's episode // 0x100 - is Episode 2 Challenge category // directory_name: the directory inside system/quests that contains quests @@ -668,8 +667,6 @@ [0x010, "government-ep2", "The Military's Hero", "$E$CG-Heathcliff Flowen-\n$C6Quests that follow\nthe Episode 2\nstoryline"], [0x010, "government-ep4", "The Meteor Impact Incident", "$E$C6Quests that follow\nthe Episode 4\nstoryline"], [0x020, "download", "Download", "$E$C6Quests to download\nto your Memory Card"], - [0x040, "download-ep3-trial", "Trial Download", "$E$C6Quests to download\nto your Memory Card\nfrom Episode 3\nTrial Edition"], - [0x040, "download-ep3", "Download", "$E$C6Quests to download\nto your Memory Card"], ], // BB bank size. If you change either of these values, you must also add @@ -1231,9 +1228,9 @@ // true and false here, since the server doesn't have direct access to the // client's quest flags from their save file. // If you use an expression, the format is the same as the AvailableIf and - // EnabledIf fields in quest JSON files (see system/quests/battle/b88001.json - // for details). Note that the expression is only evaluated at the time the - // game is created, and the player-specific tokens like C_EpX_YY refer to the + // EnabledIf fields in quest JSONs (see system/quests/retrieval/q058.json for + // details). Note that the expression is only evaluated at the time the game + // is created, and the player-specific tokens like C_EpX_YY refer to the // player who created the game. // The UnlockAllAreas option is now gone; if you want the same behavior as if // it were enabled, uncomment all the "area unlocks" lines below. Note that diff --git a/system/ep3/maps-download/download-ep3-trial/e765-gc3-e.mnm b/system/ep3/maps-download/e765-gc3-e.mnm similarity index 100% rename from system/ep3/maps-download/download-ep3-trial/e765-gc3-e.mnm rename to system/ep3/maps-download/e765-gc3-e.mnm diff --git a/system/ep3/maps-download/download-ep3-trial/e765-gc3-j.mnm b/system/ep3/maps-download/e765-gc3-j.mnm similarity index 100% rename from system/ep3/maps-download/download-ep3-trial/e765-gc3-j.mnm rename to system/ep3/maps-download/e765-gc3-j.mnm diff --git a/system/ep3/maps-download/download-ep3/e901-gc3-e.mnm b/system/ep3/maps-download/e901-gc3-e.mnm similarity index 100% rename from system/ep3/maps-download/download-ep3/e901-gc3-e.mnm rename to system/ep3/maps-download/e901-gc3-e.mnm diff --git a/system/ep3/maps-download/download-ep3/e901-gc3-j.mnm b/system/ep3/maps-download/e901-gc3-j.mnm similarity index 100% rename from system/ep3/maps-download/download-ep3/e901-gc3-j.mnm rename to system/ep3/maps-download/e901-gc3-j.mnm diff --git a/system/ep3/maps-download/download-ep3/e903-gc3-e.mnm b/system/ep3/maps-download/e903-gc3-e.mnm similarity index 100% rename from system/ep3/maps-download/download-ep3/e903-gc3-e.mnm rename to system/ep3/maps-download/e903-gc3-e.mnm diff --git a/system/ep3/maps-download/download-ep3/e903-gc3-j.mnm b/system/ep3/maps-download/e903-gc3-j.mnm similarity index 100% rename from system/ep3/maps-download/download-ep3/e903-gc3-j.mnm rename to system/ep3/maps-download/e903-gc3-j.mnm diff --git a/system/ep3/maps-download/download-ep3/e904-gc3-e.mnm b/system/ep3/maps-download/e904-gc3-e.mnm similarity index 100% rename from system/ep3/maps-download/download-ep3/e904-gc3-e.mnm rename to system/ep3/maps-download/e904-gc3-e.mnm diff --git a/system/ep3/maps-download/download-ep3/e904-gc3-j.mnm b/system/ep3/maps-download/e904-gc3-j.mnm similarity index 100% rename from system/ep3/maps-download/download-ep3/e904-gc3-j.mnm rename to system/ep3/maps-download/e904-gc3-j.mnm diff --git a/system/ep3/maps-download/download-ep3/e905-gc3-e.mnm b/system/ep3/maps-download/e905-gc3-e.mnm similarity index 100% rename from system/ep3/maps-download/download-ep3/e905-gc3-e.mnm rename to system/ep3/maps-download/e905-gc3-e.mnm diff --git a/system/ep3/maps-download/download-ep3/e905-gc3-j.mnm b/system/ep3/maps-download/e905-gc3-j.mnm similarity index 100% rename from system/ep3/maps-download/download-ep3/e905-gc3-j.mnm rename to system/ep3/maps-download/e905-gc3-j.mnm diff --git a/system/ep3/maps-download/download-ep3/e906-gc3-e.mnm b/system/ep3/maps-download/e906-gc3-e.mnm similarity index 100% rename from system/ep3/maps-download/download-ep3/e906-gc3-e.mnm rename to system/ep3/maps-download/e906-gc3-e.mnm diff --git a/system/ep3/maps-download/download-ep3/e906-gc3-j.mnm b/system/ep3/maps-download/e906-gc3-j.mnm similarity index 100% rename from system/ep3/maps-download/download-ep3/e906-gc3-j.mnm rename to system/ep3/maps-download/e906-gc3-j.mnm diff --git a/system/ep3/maps-download/download-ep3/e907-gc3-e.mnm b/system/ep3/maps-download/e907-gc3-e.mnm similarity index 100% rename from system/ep3/maps-download/download-ep3/e907-gc3-e.mnm rename to system/ep3/maps-download/e907-gc3-e.mnm diff --git a/system/ep3/maps-download/download-ep3/e907-gc3-j.mnm b/system/ep3/maps-download/e907-gc3-j.mnm similarity index 100% rename from system/ep3/maps-download/download-ep3/e907-gc3-j.mnm rename to system/ep3/maps-download/e907-gc3-j.mnm diff --git a/system/ep3/maps-download/download-ep3/e908-gc3-e.mnm b/system/ep3/maps-download/e908-gc3-e.mnm similarity index 100% rename from system/ep3/maps-download/download-ep3/e908-gc3-e.mnm rename to system/ep3/maps-download/e908-gc3-e.mnm diff --git a/system/ep3/maps-download/download-ep3/e908-gc3-j.mnm b/system/ep3/maps-download/e908-gc3-j.mnm similarity index 100% rename from system/ep3/maps-download/download-ep3/e908-gc3-j.mnm rename to system/ep3/maps-download/e908-gc3-j.mnm diff --git a/system/ep3/maps/e765-gc3-e.mnm b/system/ep3/maps/e765-gc3-e.mnm index 3a2c130f..75bcef0d 120000 --- a/system/ep3/maps/e765-gc3-e.mnm +++ b/system/ep3/maps/e765-gc3-e.mnm @@ -1 +1 @@ -../maps-download/download-ep3-trial/e765-gc3-e.mnm \ No newline at end of file +../maps-download/e765-gc3-e.mnm \ No newline at end of file diff --git a/system/ep3/maps/e765-gc3-j.mnm b/system/ep3/maps/e765-gc3-j.mnm index 916d9de6..f8ca8bc2 120000 --- a/system/ep3/maps/e765-gc3-j.mnm +++ b/system/ep3/maps/e765-gc3-j.mnm @@ -1 +1 @@ -../maps-download/download-ep3-trial/e765-gc3-j.mnm \ No newline at end of file +../maps-download/e765-gc3-j.mnm \ No newline at end of file diff --git a/system/ep3/maps/e901-gc3-e.mnm b/system/ep3/maps/e901-gc3-e.mnm index 050a91b5..770ced57 120000 --- a/system/ep3/maps/e901-gc3-e.mnm +++ b/system/ep3/maps/e901-gc3-e.mnm @@ -1 +1 @@ -../maps-download/download-ep3/e901-gc3-e.mnm \ No newline at end of file +../maps-download/e901-gc3-e.mnm \ No newline at end of file diff --git a/system/ep3/maps/e901-gc3-j.mnm b/system/ep3/maps/e901-gc3-j.mnm index 95a7c056..0670658d 120000 --- a/system/ep3/maps/e901-gc3-j.mnm +++ b/system/ep3/maps/e901-gc3-j.mnm @@ -1 +1 @@ -../maps-download/download-ep3/e901-gc3-j.mnm \ No newline at end of file +../maps-download/e901-gc3-j.mnm \ No newline at end of file diff --git a/system/ep3/maps/e903-gc3-e.mnm b/system/ep3/maps/e903-gc3-e.mnm index 5c827d6d..2756dff9 120000 --- a/system/ep3/maps/e903-gc3-e.mnm +++ b/system/ep3/maps/e903-gc3-e.mnm @@ -1 +1 @@ -../maps-download/download-ep3/e903-gc3-e.mnm \ No newline at end of file +../maps-download/e903-gc3-e.mnm \ No newline at end of file diff --git a/system/ep3/maps/e903-gc3-j.mnm b/system/ep3/maps/e903-gc3-j.mnm index 0b68cdf7..192d8ba0 120000 --- a/system/ep3/maps/e903-gc3-j.mnm +++ b/system/ep3/maps/e903-gc3-j.mnm @@ -1 +1 @@ -../maps-download/download-ep3/e903-gc3-j.mnm \ No newline at end of file +../maps-download/e903-gc3-j.mnm \ No newline at end of file diff --git a/system/ep3/maps/e904-gc3-e.mnm b/system/ep3/maps/e904-gc3-e.mnm index fef930e9..a46ed2a7 120000 --- a/system/ep3/maps/e904-gc3-e.mnm +++ b/system/ep3/maps/e904-gc3-e.mnm @@ -1 +1 @@ -../maps-download/download-ep3/e904-gc3-e.mnm \ No newline at end of file +../maps-download/e904-gc3-e.mnm \ No newline at end of file diff --git a/system/ep3/maps/e904-gc3-j.mnm b/system/ep3/maps/e904-gc3-j.mnm index 03afd3e1..cfe13275 120000 --- a/system/ep3/maps/e904-gc3-j.mnm +++ b/system/ep3/maps/e904-gc3-j.mnm @@ -1 +1 @@ -../maps-download/download-ep3/e904-gc3-j.mnm \ No newline at end of file +../maps-download/e904-gc3-j.mnm \ No newline at end of file diff --git a/system/ep3/maps/e905-gc3-e.mnm b/system/ep3/maps/e905-gc3-e.mnm index 37a65ad5..8172e948 120000 --- a/system/ep3/maps/e905-gc3-e.mnm +++ b/system/ep3/maps/e905-gc3-e.mnm @@ -1 +1 @@ -../maps-download/download-ep3/e905-gc3-e.mnm \ No newline at end of file +../maps-download/e905-gc3-e.mnm \ No newline at end of file diff --git a/system/ep3/maps/e905-gc3-j.mnm b/system/ep3/maps/e905-gc3-j.mnm index ac07c838..df72d8bc 120000 --- a/system/ep3/maps/e905-gc3-j.mnm +++ b/system/ep3/maps/e905-gc3-j.mnm @@ -1 +1 @@ -../maps-download/download-ep3/e905-gc3-j.mnm \ No newline at end of file +../maps-download/e905-gc3-j.mnm \ No newline at end of file diff --git a/system/ep3/maps/e906-gc3-e.mnm b/system/ep3/maps/e906-gc3-e.mnm index 869cb8cc..821cac56 120000 --- a/system/ep3/maps/e906-gc3-e.mnm +++ b/system/ep3/maps/e906-gc3-e.mnm @@ -1 +1 @@ -../maps-download/download-ep3/e906-gc3-e.mnm \ No newline at end of file +../maps-download/e906-gc3-e.mnm \ No newline at end of file diff --git a/system/ep3/maps/e906-gc3-j.mnm b/system/ep3/maps/e906-gc3-j.mnm index c1e6364d..9fe18448 120000 --- a/system/ep3/maps/e906-gc3-j.mnm +++ b/system/ep3/maps/e906-gc3-j.mnm @@ -1 +1 @@ -../maps-download/download-ep3/e906-gc3-j.mnm \ No newline at end of file +../maps-download/e906-gc3-j.mnm \ No newline at end of file diff --git a/system/ep3/maps/e907-gc3-e.mnm b/system/ep3/maps/e907-gc3-e.mnm index 6e6f96b8..31f3f7e9 120000 --- a/system/ep3/maps/e907-gc3-e.mnm +++ b/system/ep3/maps/e907-gc3-e.mnm @@ -1 +1 @@ -../maps-download/download-ep3/e907-gc3-e.mnm \ No newline at end of file +../maps-download/e907-gc3-e.mnm \ No newline at end of file diff --git a/system/ep3/maps/e907-gc3-j.mnm b/system/ep3/maps/e907-gc3-j.mnm index e4efaa35..4b641c42 120000 --- a/system/ep3/maps/e907-gc3-j.mnm +++ b/system/ep3/maps/e907-gc3-j.mnm @@ -1 +1 @@ -../maps-download/download-ep3/e907-gc3-j.mnm \ No newline at end of file +../maps-download/e907-gc3-j.mnm \ No newline at end of file diff --git a/system/ep3/maps/e908-gc3-e.mnm b/system/ep3/maps/e908-gc3-e.mnm index 41de51d1..b3e31b3e 120000 --- a/system/ep3/maps/e908-gc3-e.mnm +++ b/system/ep3/maps/e908-gc3-e.mnm @@ -1 +1 @@ -../maps-download/download-ep3/e908-gc3-e.mnm \ No newline at end of file +../maps-download/e908-gc3-e.mnm \ No newline at end of file diff --git a/system/ep3/maps/e908-gc3-j.mnm b/system/ep3/maps/e908-gc3-j.mnm index 6fc4157c..12ae64ca 120000 --- a/system/ep3/maps/e908-gc3-j.mnm +++ b/system/ep3/maps/e908-gc3-j.mnm @@ -1 +1 @@ -../maps-download/download-ep3/e908-gc3-j.mnm \ No newline at end of file +../maps-download/e908-gc3-j.mnm \ No newline at end of file diff --git a/system/ep3/maps/map000001F4-e.bin b/system/ep3/maps/map000001F4-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000001F5-e.bin b/system/ep3/maps/map000001F5-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000001F6-e.bin b/system/ep3/maps/map000001F6-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000001F7-e.bin b/system/ep3/maps/map000001F7-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000001F8-e.bin b/system/ep3/maps/map000001F8-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000230-e.bin b/system/ep3/maps/map00000230-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000231-e.bin b/system/ep3/maps/map00000231-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000244-e.bin b/system/ep3/maps/map00000244-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000245-e.bin b/system/ep3/maps/map00000245-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000246-e.bin b/system/ep3/maps/map00000246-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000258-e.bin b/system/ep3/maps/map00000258-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000259-e.bin b/system/ep3/maps/map00000259-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map0000026C-e.bin b/system/ep3/maps/map0000026C-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map0000026D-e.bin b/system/ep3/maps/map0000026D-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map0000026E-e.bin b/system/ep3/maps/map0000026E-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map0000026F-e.bin b/system/ep3/maps/map0000026F-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000280-e.bin b/system/ep3/maps/map00000280-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000281-e.bin b/system/ep3/maps/map00000281-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000282-e.bin b/system/ep3/maps/map00000282-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000294-e.bin b/system/ep3/maps/map00000294-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000295-e.bin b/system/ep3/maps/map00000295-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000296-e.bin b/system/ep3/maps/map00000296-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000002A8-e.bin b/system/ep3/maps/map000002A8-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000002A9-e.bin b/system/ep3/maps/map000002A9-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000002BC-e.bin b/system/ep3/maps/map000002BC-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000002BD-e.bin b/system/ep3/maps/map000002BD-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000002E4-e.bin b/system/ep3/maps/map000002E4-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000002E5-e.bin b/system/ep3/maps/map000002E5-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000002F8-e.bin b/system/ep3/maps/map000002F8-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000002F9-e.bin b/system/ep3/maps/map000002F9-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000002FA-e.bin b/system/ep3/maps/map000002FA-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000320-e.bin b/system/ep3/maps/map00000320-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000321-e.bin b/system/ep3/maps/map00000321-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000334-e.bin b/system/ep3/maps/map00000334-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000335-e.bin b/system/ep3/maps/map00000335-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map0000033E-e.bin b/system/ep3/maps/map0000033E-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map0000033F-e.bin b/system/ep3/maps/map0000033F-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000340-e.bin b/system/ep3/maps/map00000340-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000341-e.bin b/system/ep3/maps/map00000341-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000342-e.bin b/system/ep3/maps/map00000342-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000348-e.bin b/system/ep3/maps/map00000348-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000349-e.bin b/system/ep3/maps/map00000349-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000352-e.bin b/system/ep3/maps/map00000352-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000353-e.bin b/system/ep3/maps/map00000353-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000354-e.bin b/system/ep3/maps/map00000354-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map0000035C-e.bin b/system/ep3/maps/map0000035C-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map0000035D-e.bin b/system/ep3/maps/map0000035D-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map0000035E-e.bin b/system/ep3/maps/map0000035E-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map0000035F-e.bin b/system/ep3/maps/map0000035F-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000360-e.bin b/system/ep3/maps/map00000360-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000370-e.bin b/system/ep3/maps/map00000370-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000371-e.bin b/system/ep3/maps/map00000371-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000372-e.bin b/system/ep3/maps/map00000372-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000398-e.bin b/system/ep3/maps/map00000398-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map00000399-e.bin b/system/ep3/maps/map00000399-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map0000039A-e.bin b/system/ep3/maps/map0000039A-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map0000039B-e.bin b/system/ep3/maps/map0000039B-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map0000039C-e.bin b/system/ep3/maps/map0000039C-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000003B6-e.bin b/system/ep3/maps/map000003B6-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000003B7-e.bin b/system/ep3/maps/map000003B7-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000003B8-e.bin b/system/ep3/maps/map000003B8-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000003B9-e.bin b/system/ep3/maps/map000003B9-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000003BA-e.bin b/system/ep3/maps/map000003BA-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000003BB-e.bin b/system/ep3/maps/map000003BB-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000003BC-e.bin b/system/ep3/maps/map000003BC-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000003BD-e.bin b/system/ep3/maps/map000003BD-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000003BE-e.bin b/system/ep3/maps/map000003BE-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000003BF-e.bin b/system/ep3/maps/map000003BF-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000003C0-e.bin b/system/ep3/maps/map000003C0-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000003C1-e.bin b/system/ep3/maps/map000003C1-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000003C2-e.bin b/system/ep3/maps/map000003C2-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000003C3-e.bin b/system/ep3/maps/map000003C3-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000003C4-e.bin b/system/ep3/maps/map000003C4-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000003C5-e.bin b/system/ep3/maps/map000003C5-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000003C6-e.bin b/system/ep3/maps/map000003C6-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000003C7-e.bin b/system/ep3/maps/map000003C7-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000003C8-e.bin b/system/ep3/maps/map000003C8-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000003C9-e.bin b/system/ep3/maps/map000003C9-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000003CA-e.bin b/system/ep3/maps/map000003CA-e.bin old mode 100755 new mode 100644 diff --git a/system/ep3/maps/map000003CB-e.bin b/system/ep3/maps/map000003CB-e.bin old mode 100755 new mode 100644 diff --git a/system/quests/battle/b88002.json b/system/quests/battle/b88002.json deleted file mode 100644 index 7f170aca..00000000 --- a/system/quests/battle/b88002.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "BattleRules": { - "TechDiskMode": "LIMIT_LEVEL", - "WeaponAndArmorMode": "CLEAR_AND_ALLOW", - "MagMode": "FORBID_ALL", - "ToolMode": "CLEAR_AND_ALLOW", - "TrapMode": "ALL_PLAYERS", - "RespawnMode": "ALLOW", - "ReplaceChar": 1, - "DropWeapon": 1, - "IsTeams": 0, - "HideTargetReticle": 1, - "DeathLevelUp": 5, - "MesetaMode": "CLEAR_AND_ALLOW", - "EnableSonar": 1, - "MaxTechLevel": 0, - "CharLevel": 0, - "TimeLimit": 10, - "ForbidScapeDolls": 1, - "DeathTechLevelUp": 1, - "TrapCounts": [0, 5, 5, 5], - "SonarCount": 5, - "BoxDropArea": 1 - } -} diff --git a/system/quests/battle/b88003.json b/system/quests/battle/b88003.json deleted file mode 100644 index bc08153d..00000000 --- a/system/quests/battle/b88003.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "BattleRules": { - "TechDiskMode": "LIMIT_LEVEL", - "WeaponAndArmorMode": "CLEAR_AND_ALLOW", - "MagMode": "FORBID_ALL", - "ToolMode": "CLEAR_AND_ALLOW", - "TrapMode": "ALL_PLAYERS", - "RespawnMode": "LIMIT_LIVES", - "ReplaceChar": 1, - "DropWeapon": 0, - "IsTeams": 0, - "HideTargetReticle": 1, - "DeathLevelUp": 3, - "MesetaMode": "FORBID_ALL", - "EnableSonar": 0, - "Lives": 10, - "MaxTechLevel": 0, - "CharLevel": 4, - "TimeLimit": 10, - "ForbidScapeDolls": 1, - "DeathTechLevelUp": 1, - "TrapCounts": [0, 10, 10, 10], - "BoxDropArea": 3 - } -} diff --git a/system/quests/battle/b88004.json b/system/quests/battle/b88004.json deleted file mode 100644 index 245e6fcc..00000000 --- a/system/quests/battle/b88004.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "BattleRules": { - "TechDiskMode": "LIMIT_LEVEL", - "WeaponAndArmorMode": "CLEAR_AND_ALLOW", - "MagMode": "FORBID_ALL", - "ToolMode": "CLEAR_AND_ALLOW", - "TrapMode": "ALL_PLAYERS", - "RespawnMode": "LIMIT_LIVES", - "ReplaceChar": 1, - "DropWeapon": 1, - "IsTeams": 0, - "HideTargetReticle": 1, - "DeathLevelUp": 5, - "MesetaMode": "CLEAR_AND_ALLOW", - "EnableSonar": 1, - "Lives": 10, - "MaxTechLevel": 1, - "CharLevel": 1, - "TimeLimit": 10, - "ForbidScapeDolls": 1, - "DeathTechLevelUp": 1, - "TrapCounts": [5, 5, 5, 5], - "SonarCount": 5, - "BoxDropArea": 1 - } -} diff --git a/system/quests/battle/b88005.json b/system/quests/battle/b88005.json deleted file mode 100644 index 051168ae..00000000 --- a/system/quests/battle/b88005.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "BattleRules": { - "TechDiskMode": "ALLOW", - "WeaponAndArmorMode": "ALLOW", - "MagMode": "FORBID_ALL", - "ToolMode": "ALLOW", - "TrapMode": "ALL_PLAYERS", - "RespawnMode": "FORBID", - "ReplaceChar": 0, - "DropWeapon": 1, - "IsTeams": 1, - "HideTargetReticle": 1, - "DeathLevelUp": 5, - "MesetaMode": "CLEAR_AND_ALLOW", - "EnableSonar": 1, - "TimeLimit": 10, - "ForbidScapeDolls": 1, - "DeathTechLevelUp": 1, - "TrapCounts": [5, 5, 5, 5], - "SonarCount": 5, - "BoxDropArea": 10 - } -} diff --git a/system/quests/battle/b88006.json b/system/quests/battle/b88006.json deleted file mode 100644 index 41d5b3e3..00000000 --- a/system/quests/battle/b88006.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "BattleRules": { - "TechDiskMode": "LIMIT_LEVEL", - "WeaponAndArmorMode": "CLEAR_AND_ALLOW", - "MagMode": "FORBID_ALL", - "ToolMode": "CLEAR_AND_ALLOW", - "TrapMode": "ALL_PLAYERS", - "RespawnMode": "LIMIT_LIVES", - "ReplaceChar": 1, - "DropWeapon": 1, - "IsTeams": 1, - "HideTargetReticle": 1, - "DeathLevelUp": 3, - "MesetaMode": "CLEAR_AND_ALLOW", - "EnableSonar": 0, - "Lives": 10, - "MaxTechLevel": 4, - "CharLevel": 19, - "TimeLimit": 10, - "ForbidScapeDolls": 1, - "DeathTechLevelUp": 1, - "TrapCounts": [5, 5, 0, 0], - "BoxDropArea": 6 - } -} diff --git a/system/quests/battle/b88007.json b/system/quests/battle/b88007.json deleted file mode 100644 index da53e917..00000000 --- a/system/quests/battle/b88007.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "BattleRules": { - "TechDiskMode": "LIMIT_LEVEL", - "WeaponAndArmorMode": "CLEAR_AND_ALLOW", - "MagMode": "FORBID_ALL", - "ToolMode": "CLEAR_AND_ALLOW", - "TrapMode": "ALL_PLAYERS", - "RespawnMode": "LIMIT_LIVES", - "ReplaceChar": 1, - "DropWeapon": 1, - "IsTeams": 0, - "HideTargetReticle": 1, - "DeathLevelUp": 1, - "MesetaMode": "CLEAR_AND_ALLOW", - "EnableSonar": 1, - "Lives": 15, - "MaxTechLevel": 0, - "CharLevel": 0, - "TimeLimit": 10, - "ForbidScapeDolls": 0, - "DeathTechLevelUp": 0, - "TrapCounts": [0, 0, 1, 0], - "SonarCount": 10, - "BoxDropArea": 2 - } -} diff --git a/system/quests/battle/b88008.json b/system/quests/battle/b88008.json deleted file mode 100644 index 1e37f1be..00000000 --- a/system/quests/battle/b88008.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "BattleRules": { - "TechDiskMode": "LIMIT_LEVEL", - "WeaponAndArmorMode": "CLEAR_AND_ALLOW", - "MagMode": "FORBID_ALL", - "ToolMode": "CLEAR_AND_ALLOW", - "TrapMode": "ALL_PLAYERS", - "RespawnMode": "LIMIT_LIVES", - "ReplaceChar": 1, - "DropWeapon": 0, - "IsTeams": 0, - "HideTargetReticle": 1, - "DeathLevelUp": 5, - "MesetaMode": "FORBID_ALL", - "EnableSonar": 1, - "Lives": 10, - "MaxTechLevel": 0, - "CharLevel": 19, - "TimeLimit": 10, - "ForbidScapeDolls": 1, - "DeathTechLevelUp": 0, - "TrapCounts": [0, 10, 10, 10], - "SonarCount": 10, - "BoxDropArea": 1 - } -} diff --git a/system/quests/challenge-ep1/c88101.json b/system/quests/challenge-ep1/c88101.json index 117527de..0f97f532 100644 --- a/system/quests/challenge-ep1/c88101.json +++ b/system/quests/challenge-ep1/c88101.json @@ -1,4 +1,3 @@ { - "ChallengeTemplateIndex": 0, "DescriptionFlag": 51 } diff --git a/system/quests/challenge-ep1/c88102.json b/system/quests/challenge-ep1/c88102.json index d1d90bb8..4a4e6cab 100644 --- a/system/quests/challenge-ep1/c88102.json +++ b/system/quests/challenge-ep1/c88102.json @@ -1,5 +1,4 @@ { - "ChallengeTemplateIndex": 1, "DescriptionFlag": 52, "AvailableIf": "CC_Ep1_1", "EnabledIf": "CC_Ep1_1" diff --git a/system/quests/challenge-ep1/c88103.json b/system/quests/challenge-ep1/c88103.json index 567bb603..8d8fee79 100644 --- a/system/quests/challenge-ep1/c88103.json +++ b/system/quests/challenge-ep1/c88103.json @@ -1,5 +1,4 @@ { - "ChallengeTemplateIndex": 2, "DescriptionFlag": 53, "AvailableIf": "CC_Ep1_2", "EnabledIf": "CC_Ep1_2" diff --git a/system/quests/challenge-ep1/c88104.json b/system/quests/challenge-ep1/c88104.json index 97a71e0e..51a889ea 100644 --- a/system/quests/challenge-ep1/c88104.json +++ b/system/quests/challenge-ep1/c88104.json @@ -1,5 +1,4 @@ { - "ChallengeTemplateIndex": 3, "DescriptionFlag": 54, "AvailableIf": "CC_Ep1_3", "EnabledIf": "CC_Ep1_3" diff --git a/system/quests/challenge-ep1/c88105.json b/system/quests/challenge-ep1/c88105.json index 4258bf23..83c22280 100644 --- a/system/quests/challenge-ep1/c88105.json +++ b/system/quests/challenge-ep1/c88105.json @@ -1,5 +1,4 @@ { - "ChallengeTemplateIndex": 4, "DescriptionFlag": 55, "AvailableIf": "CC_Ep1_4", "EnabledIf": "CC_Ep1_4" diff --git a/system/quests/challenge-ep1/c88106.json b/system/quests/challenge-ep1/c88106.json index 7b8a3772..0dc4e362 100644 --- a/system/quests/challenge-ep1/c88106.json +++ b/system/quests/challenge-ep1/c88106.json @@ -1,5 +1,4 @@ { - "ChallengeTemplateIndex": 5, "DescriptionFlag": 56, "AvailableIf": "CC_Ep1_5", "EnabledIf": "CC_Ep1_5" diff --git a/system/quests/challenge-ep1/c88107.json b/system/quests/challenge-ep1/c88107.json index f1141a8a..9489705a 100644 --- a/system/quests/challenge-ep1/c88107.json +++ b/system/quests/challenge-ep1/c88107.json @@ -1,5 +1,4 @@ { - "ChallengeTemplateIndex": 6, "DescriptionFlag": 57, "AvailableIf": "CC_Ep1_6", "EnabledIf": "CC_Ep1_6" diff --git a/system/quests/challenge-ep1/c88108.json b/system/quests/challenge-ep1/c88108.json index 72605eae..5f4b36d1 100644 --- a/system/quests/challenge-ep1/c88108.json +++ b/system/quests/challenge-ep1/c88108.json @@ -1,5 +1,4 @@ { - "ChallengeTemplateIndex": 7, "DescriptionFlag": 58, "AvailableIf": "CC_Ep1_7", "EnabledIf": "CC_Ep1_7" diff --git a/system/quests/challenge-ep1/c88109.json b/system/quests/challenge-ep1/c88109.json index 3a115194..ab8ad5d1 100644 --- a/system/quests/challenge-ep1/c88109.json +++ b/system/quests/challenge-ep1/c88109.json @@ -1,5 +1,4 @@ { - "ChallengeTemplateIndex": 8, "DescriptionFlag": 59, "AvailableIf": "CC_Ep1_8", "EnabledIf": "CC_Ep1_8" diff --git a/system/quests/challenge-ep2/d88201.json b/system/quests/challenge-ep2/d88201.json index b99a4f21..ad43c1b9 100644 --- a/system/quests/challenge-ep2/d88201.json +++ b/system/quests/challenge-ep2/d88201.json @@ -1,4 +1,3 @@ { - "ChallengeTemplateIndex": 1, - "DescriptionFlag": 61, + "DescriptionFlag": 61 } diff --git a/system/quests/challenge-ep2/d88202.json b/system/quests/challenge-ep2/d88202.json index 30bb1a9e..51df0ad9 100644 --- a/system/quests/challenge-ep2/d88202.json +++ b/system/quests/challenge-ep2/d88202.json @@ -1,5 +1,4 @@ { - "ChallengeTemplateIndex": 4, "DescriptionFlag": 62, "AvailableIf": "CC_Ep2_1", "EnabledIf": "CC_Ep2_1" diff --git a/system/quests/challenge-ep2/d88203.json b/system/quests/challenge-ep2/d88203.json index 85ace15a..aab1c802 100644 --- a/system/quests/challenge-ep2/d88203.json +++ b/system/quests/challenge-ep2/d88203.json @@ -1,5 +1,4 @@ { - "ChallengeTemplateIndex": 5, "DescriptionFlag": 63, "AvailableIf": "CC_Ep2_2", "EnabledIf": "CC_Ep2_2" diff --git a/system/quests/challenge-ep2/d88204-gc-e.bin b/system/quests/challenge-ep2/d88204-gc-e.bin index bb5e1c17ac9723001021cf6f2f9bbdd04d6ee5bd..0894c704a076c418f4f971302b7a28fdbbcfe8f5 100644 GIT binary patch delta 1047 zcmV+y1nB$93(E_z0RaIGlL7(Af7$`;_x=y?{tA|y-jFih0^T>?8PNO+zL(ywF6RH< zPFZhtAZTIlc4Z)ATHIx1{~$bLZ)|UJAlorM)YdMA|1jr109u7rr(QqFus8n!_-V;y zKxGI2W&mdZXaH#dziI$$0Bito{SqNVpll5FZfBhTO>bmnEDBO#KW=Lve?l%G4cB*R z<-;Ii)MshocHSUiAU|+&X?kTY9F<-vka9w2NNDc*X*<)F1Mo%$00;nI_-bTo0mk7T z`eqny-)R318EF3z8nEr?`Y1u@`XBv~==v1`00jUC=IHtj0)u9#aA^M`==v%{Q!eOl z`0R3FbaiYH{Aq6Wb0G>yfAt$qKNaybK0$PJP_xb<`~UC?FYr)PSwqiAPe)bJ+~h1L z|5txWPDD*fOiaNq|8*z-OTSK4LrY9WFaIAPbZBpL4o@>6X_Fsqa{yaTK0I?Eb8l@N zXKZP0cV>dGyyf{tHENayS_wAq~1I1^pog{Rf2o2>l5V z{R+bU3wD5Kn9%ict^b+O^>VHMn$Y!gt^b?Q^>nTOoY3`kt^c9$ zkVau?Z1S-03x_TL`2pVo`AVnoE%4JX2LUKBh4{}9|M<8&hsmFe?$iGO_>IX3%gF-w z_>RdG;x+;J-#p0yNeCjwBmc1p0A0$l(g4G;F3N97-4iuNf`+L3pEJ;4!3hj8VLUjHVFR?=X6OM z;rJJz_*72+_7qM~+396va{N~Kp#d%Z@P+qX$r{PZFaV&8;qnqK{}cz(uJaMe2~ziB z_tOc>_b4|0e~I?}_?`Ht9`2S+~|Nm47!1M`iosq3Tf%D^R{3cPdeHX!qz z_n`SEs>Tep{J>%udgxewUi-p?_0qrFi{EuVso&26-z}gBq=xA+# R^J4h_3IBdU=&2O^005xQ0*U|t delta 1046 zcmV+x1nK+B3(5~dj;baia}X>j#(Aqx9QfAt$q74bAaL3Dw1v)axf|L_Vg@KAqKSwl!qM^)z0*mO09#J*JaZs(Z{clc zY-w%4>`p@)DU z_xNXIV)3j^75UI>GM_c|eY z_H%@7b%ZU)Eed(SV;^B|Zf|q|xdNi3dDl4p3&%xrav33MA-X9A2>k~AAqV{kg#8Kq z3K0DZMtv=BQ-@L6e-M}TaE7h_nDudnt^b+za)zz{n)P#rt^b?#bcU_}ob`2vt^c8r zMwek}Y_RVO&xbAl0pC9Y`AYCD@IBKn0VpuQh0hTGKlr#i$)Ak;?$ZGHjmZeV%gF-x zj>#0_|26^mJjnq`2>v3*Be4kpUCJM^(g3k8?#gdT7Gu~>e?dNJ_8>Ac3XJqu0pB>` z5O8kaNf!wJ3p2eA{}ujmNf-$K3%xW5{|<6V83_Liy)_8`4s%Hw2>%N`HVFR?bV(b( z;rO8VR8Ie}6i!gt>E~r-a#r~7p#d%Mh4)>_hZ@Ps0H6ts@~09l{|C}=^AX7jol^H= z_X*4P_b4`rfA;6xCUN-AqqcAW}&{+wWrpy4PN+WAVHP{Md~2z4$R2{Lsey zE&No~0->t-|6=(6|5R-V!1M{7k*z>uf%D`1CSy^ue<%D--28_Ip!20^4FYke2T(C_ z{E)`{ul&xq{Otz-`Vl(>2w?e1O9Nt*gu{&d<$Q4m@F@cu0R1_Xfb=FEw>&=xAoHE~ zp!p^-stmQp{J{Ln82r{_{NeoU+aTZh1(6r0qrIHj~MWs8~mQY+6p;q=xBiR QV)+062!5kM=&Aew0N*(Pi~s-t diff --git a/system/quests/challenge-ep2/d88204.json b/system/quests/challenge-ep2/d88204.json index 8c21cf70..290defa5 100644 --- a/system/quests/challenge-ep2/d88204.json +++ b/system/quests/challenge-ep2/d88204.json @@ -1,5 +1,4 @@ { - "ChallengeTemplateIndex": 8, "DescriptionFlag": 64, "AvailableIf": "CC_Ep2_3", "EnabledIf": "CC_Ep2_3" diff --git a/system/quests/challenge-ep2/d88205.json b/system/quests/challenge-ep2/d88205.json index 5d2d3dd5..ac6d36e6 100644 --- a/system/quests/challenge-ep2/d88205.json +++ b/system/quests/challenge-ep2/d88205.json @@ -1,5 +1,4 @@ { - "ChallengeTemplateIndex": 8, "DescriptionFlag": 65, "AvailableIf": "CC_Ep2_4", "EnabledIf": "CC_Ep2_4" diff --git a/system/quests/challenge-solo-ep1/c8811.json b/system/quests/challenge-solo-ep1/c8811.json index b8f868cf..9719a860 100644 --- a/system/quests/challenge-solo-ep1/c8811.json +++ b/system/quests/challenge-solo-ep1/c8811.json @@ -1,5 +1,4 @@ { - "ChallengeTemplateIndex": 0, "DescriptionFlag": 51, "AvailableIf": "V_NumPlayers == 2", "AllowStartFromChatCommand": true diff --git a/system/quests/challenge-solo-ep1/c8812.json b/system/quests/challenge-solo-ep1/c8812.json index a5705bde..a87a27ff 100644 --- a/system/quests/challenge-solo-ep1/c8812.json +++ b/system/quests/challenge-solo-ep1/c8812.json @@ -1,5 +1,4 @@ { - "ChallengeTemplateIndex": 1, "DescriptionFlag": 52, "AvailableIf": "V_NumPlayers == 2", "AllowStartFromChatCommand": true diff --git a/system/quests/challenge-solo-ep1/c8813.json b/system/quests/challenge-solo-ep1/c8813.json index ffd7d285..39f4a060 100644 --- a/system/quests/challenge-solo-ep1/c8813.json +++ b/system/quests/challenge-solo-ep1/c8813.json @@ -1,5 +1,4 @@ { - "ChallengeTemplateIndex": 2, "DescriptionFlag": 53, "AvailableIf": "V_NumPlayers == 2", "AllowStartFromChatCommand": true diff --git a/system/quests/challenge-solo-ep1/c8814.json b/system/quests/challenge-solo-ep1/c8814.json index b3a9169a..5cc8cb2b 100644 --- a/system/quests/challenge-solo-ep1/c8814.json +++ b/system/quests/challenge-solo-ep1/c8814.json @@ -1,5 +1,4 @@ { - "ChallengeTemplateIndex": 3, "DescriptionFlag": 54, "AvailableIf": "V_NumPlayers == 2", "AllowStartFromChatCommand": true diff --git a/system/quests/challenge-solo-ep1/c8815.json b/system/quests/challenge-solo-ep1/c8815.json index cb2bb760..bfd7cac7 100644 --- a/system/quests/challenge-solo-ep1/c8815.json +++ b/system/quests/challenge-solo-ep1/c8815.json @@ -1,5 +1,4 @@ { - "ChallengeTemplateIndex": 4, "DescriptionFlag": 55, "AvailableIf": "V_NumPlayers == 2", "AllowStartFromChatCommand": true diff --git a/system/quests/challenge-solo-ep1/c8816.json b/system/quests/challenge-solo-ep1/c8816.json index 931509a5..03b95529 100644 --- a/system/quests/challenge-solo-ep1/c8816.json +++ b/system/quests/challenge-solo-ep1/c8816.json @@ -1,5 +1,4 @@ { - "ChallengeTemplateIndex": 5, "DescriptionFlag": 56, "AvailableIf": "V_NumPlayers == 2", "AllowStartFromChatCommand": true diff --git a/system/quests/challenge-solo-ep1/c8817.json b/system/quests/challenge-solo-ep1/c8817.json index 4552f344..0abe6ca2 100644 --- a/system/quests/challenge-solo-ep1/c8817.json +++ b/system/quests/challenge-solo-ep1/c8817.json @@ -1,5 +1,4 @@ { - "ChallengeTemplateIndex": 6, "DescriptionFlag": 57, "AvailableIf": "V_NumPlayers == 2", "AllowStartFromChatCommand": true diff --git a/system/quests/challenge-solo-ep1/c8818.json b/system/quests/challenge-solo-ep1/c8818.json index 376bef94..28c96227 100644 --- a/system/quests/challenge-solo-ep1/c8818.json +++ b/system/quests/challenge-solo-ep1/c8818.json @@ -1,5 +1,4 @@ { - "ChallengeTemplateIndex": 7, "DescriptionFlag": 58, "AvailableIf": "V_NumPlayers == 2", "AllowStartFromChatCommand": true diff --git a/system/quests/challenge-solo-ep1/c8819.json b/system/quests/challenge-solo-ep1/c8819.json index 73e5e189..4980e473 100644 --- a/system/quests/challenge-solo-ep1/c8819.json +++ b/system/quests/challenge-solo-ep1/c8819.json @@ -1,5 +1,4 @@ { - "ChallengeTemplateIndex": 8, "DescriptionFlag": 59, "AvailableIf": "V_NumPlayers == 2", "AllowStartFromChatCommand": true diff --git a/system/quests/challenge-solo-ep2/d8821.json b/system/quests/challenge-solo-ep2/d8821.json index 89f0d3bc..e1f8e9d7 100644 --- a/system/quests/challenge-solo-ep2/d8821.json +++ b/system/quests/challenge-solo-ep2/d8821.json @@ -1,5 +1,4 @@ { - "ChallengeTemplateIndex": 1, "DescriptionFlag": 61, "AvailableIf": "V_NumPlayers == 2", "AllowStartFromChatCommand": true diff --git a/system/quests/challenge-solo-ep2/d8822.json b/system/quests/challenge-solo-ep2/d8822.json index 43c85810..d7b1b621 100644 --- a/system/quests/challenge-solo-ep2/d8822.json +++ b/system/quests/challenge-solo-ep2/d8822.json @@ -1,5 +1,4 @@ { - "ChallengeTemplateIndex": 4, "DescriptionFlag": 62, "AvailableIf": "V_NumPlayers == 2", "AllowStartFromChatCommand": true diff --git a/system/quests/challenge-solo-ep2/d8823.json b/system/quests/challenge-solo-ep2/d8823.json index 0386267c..bec38f01 100644 --- a/system/quests/challenge-solo-ep2/d8823.json +++ b/system/quests/challenge-solo-ep2/d8823.json @@ -1,5 +1,4 @@ { - "ChallengeTemplateIndex": 5, "DescriptionFlag": 63, "AvailableIf": "V_NumPlayers == 2", "AllowStartFromChatCommand": true diff --git a/system/quests/challenge-solo-ep2/d8824.json b/system/quests/challenge-solo-ep2/d8824.json index e04a6242..169390d5 100644 --- a/system/quests/challenge-solo-ep2/d8824.json +++ b/system/quests/challenge-solo-ep2/d8824.json @@ -1,5 +1,4 @@ { - "ChallengeTemplateIndex": 8, "DescriptionFlag": 64, "AvailableIf": "V_NumPlayers == 2", "AllowStartFromChatCommand": true diff --git a/system/quests/challenge-solo-ep2/d8825.json b/system/quests/challenge-solo-ep2/d8825.json index 8412f477..cf728fdc 100644 --- a/system/quests/challenge-solo-ep2/d8825.json +++ b/system/quests/challenge-solo-ep2/d8825.json @@ -1,5 +1,4 @@ { - "ChallengeTemplateIndex": 8, "DescriptionFlag": 65, "AvailableIf": "V_NumPlayers == 2", "AllowStartFromChatCommand": true diff --git a/system/quests/government-console-ep1/q154-xb-e.bin b/system/quests/government-console-ep1/q154-xb-e.bin deleted file mode 120000 index a6a89ad8..00000000 --- a/system/quests/government-console-ep1/q154-xb-e.bin +++ /dev/null @@ -1 +0,0 @@ -q154-gc-e.bin \ No newline at end of file diff --git a/system/quests/government-console-ep1/q154-xb.dat b/system/quests/government-console-ep1/q154-xb.dat deleted file mode 120000 index 3c699c41..00000000 --- a/system/quests/government-console-ep1/q154-xb.dat +++ /dev/null @@ -1 +0,0 @@ -q154-gc.dat \ No newline at end of file diff --git a/system/quests/government-console-ep1/q155-xb-e.bin b/system/quests/government-console-ep1/q155-xb-e.bin deleted file mode 120000 index 94a2e315..00000000 --- a/system/quests/government-console-ep1/q155-xb-e.bin +++ /dev/null @@ -1 +0,0 @@ -q155-gc-e.bin \ No newline at end of file diff --git a/system/quests/government-console-ep1/q155-xb.dat b/system/quests/government-console-ep1/q155-xb.dat deleted file mode 120000 index a3355875..00000000 --- a/system/quests/government-console-ep1/q155-xb.dat +++ /dev/null @@ -1 +0,0 @@ -q155-gc.dat \ No newline at end of file diff --git a/system/quests/government-console-ep1/q156-xb-e.bin b/system/quests/government-console-ep1/q156-xb-e.bin deleted file mode 120000 index 2553f1c6..00000000 --- a/system/quests/government-console-ep1/q156-xb-e.bin +++ /dev/null @@ -1 +0,0 @@ -q156-gc-e.bin \ No newline at end of file diff --git a/system/quests/government-console-ep1/q156-xb.dat b/system/quests/government-console-ep1/q156-xb.dat deleted file mode 120000 index 93a50830..00000000 --- a/system/quests/government-console-ep1/q156-xb.dat +++ /dev/null @@ -1 +0,0 @@ -q156-gc.dat \ No newline at end of file diff --git a/system/quests/government-console-ep1/q158-xb-e.bin b/system/quests/government-console-ep1/q158-xb-e.bin deleted file mode 120000 index 430a2cd4..00000000 --- a/system/quests/government-console-ep1/q158-xb-e.bin +++ /dev/null @@ -1 +0,0 @@ -q158-gc-e.bin \ No newline at end of file diff --git a/system/quests/government-console-ep1/q158-xb.dat b/system/quests/government-console-ep1/q158-xb.dat deleted file mode 120000 index fe2d5538..00000000 --- a/system/quests/government-console-ep1/q158-xb.dat +++ /dev/null @@ -1 +0,0 @@ -q158-gc.dat \ No newline at end of file diff --git a/system/quests/government-console-ep1/q159-xb-e.bin b/system/quests/government-console-ep1/q159-xb-e.bin deleted file mode 120000 index ec645198..00000000 --- a/system/quests/government-console-ep1/q159-xb-e.bin +++ /dev/null @@ -1 +0,0 @@ -q159-gc-e.bin \ No newline at end of file diff --git a/system/quests/government-console-ep1/q159-xb.dat b/system/quests/government-console-ep1/q159-xb.dat deleted file mode 120000 index 9585fb2a..00000000 --- a/system/quests/government-console-ep1/q159-xb.dat +++ /dev/null @@ -1 +0,0 @@ -q159-gc.dat \ No newline at end of file diff --git a/system/quests/government-console-ep1/q161-xb-e.bin b/system/quests/government-console-ep1/q161-xb-e.bin deleted file mode 120000 index b1b103a1..00000000 --- a/system/quests/government-console-ep1/q161-xb-e.bin +++ /dev/null @@ -1 +0,0 @@ -q161-gc-e.bin \ No newline at end of file diff --git a/system/quests/government-console-ep1/q161-xb.dat b/system/quests/government-console-ep1/q161-xb.dat deleted file mode 120000 index 379fa86f..00000000 --- a/system/quests/government-console-ep1/q161-xb.dat +++ /dev/null @@ -1 +0,0 @@ -q161-gc.dat \ No newline at end of file diff --git a/system/quests/government-console-ep1/q162-xb-e.bin b/system/quests/government-console-ep1/q162-xb-e.bin deleted file mode 120000 index a16364f1..00000000 --- a/system/quests/government-console-ep1/q162-xb-e.bin +++ /dev/null @@ -1 +0,0 @@ -q162-gc-e.bin \ No newline at end of file diff --git a/system/quests/government-console-ep1/q162-xb.dat b/system/quests/government-console-ep1/q162-xb.dat deleted file mode 120000 index f7f1fa7a..00000000 --- a/system/quests/government-console-ep1/q162-xb.dat +++ /dev/null @@ -1 +0,0 @@ -q162-gc.dat \ No newline at end of file diff --git a/system/quests/government-console-ep1/q163-xb-e.bin b/system/quests/government-console-ep1/q163-xb-e.bin deleted file mode 120000 index f8b5de6e..00000000 --- a/system/quests/government-console-ep1/q163-xb-e.bin +++ /dev/null @@ -1 +0,0 @@ -q163-gc-e.bin \ No newline at end of file diff --git a/system/quests/government-console-ep1/q163-xb.dat b/system/quests/government-console-ep1/q163-xb.dat deleted file mode 120000 index eb480972..00000000 --- a/system/quests/government-console-ep1/q163-xb.dat +++ /dev/null @@ -1 +0,0 @@ -q163-gc.dat \ No newline at end of file diff --git a/system/quests/government-console-ep1/q164-xb-e.bin b/system/quests/government-console-ep1/q164-xb-e.bin deleted file mode 120000 index aa30f497..00000000 --- a/system/quests/government-console-ep1/q164-xb-e.bin +++ /dev/null @@ -1 +0,0 @@ -q164-gc-e.bin \ No newline at end of file diff --git a/system/quests/government-console-ep1/q164-xb.dat b/system/quests/government-console-ep1/q164-xb.dat deleted file mode 120000 index f62618de..00000000 --- a/system/quests/government-console-ep1/q164-xb.dat +++ /dev/null @@ -1 +0,0 @@ -q164-gc.dat \ No newline at end of file diff --git a/system/quests/government-console-ep1/q154-gc-e.bin b/system/quests/government-console-ep1/q180-gc-e.bin similarity index 100% rename from system/quests/government-console-ep1/q154-gc-e.bin rename to system/quests/government-console-ep1/q180-gc-e.bin diff --git a/system/quests/government-console-ep1/q154-gc.dat b/system/quests/government-console-ep1/q180-gc.dat similarity index 100% rename from system/quests/government-console-ep1/q154-gc.dat rename to system/quests/government-console-ep1/q180-gc.dat diff --git a/system/quests/government-console-ep1/q180-xb-e.bin b/system/quests/government-console-ep1/q180-xb-e.bin new file mode 120000 index 00000000..c8cb352d --- /dev/null +++ b/system/quests/government-console-ep1/q180-xb-e.bin @@ -0,0 +1 @@ +q180-gc-e.bin \ No newline at end of file diff --git a/system/quests/government-console-ep1/q180-xb.dat b/system/quests/government-console-ep1/q180-xb.dat new file mode 120000 index 00000000..658dbc20 --- /dev/null +++ b/system/quests/government-console-ep1/q180-xb.dat @@ -0,0 +1 @@ +q180-gc.dat \ No newline at end of file diff --git a/system/quests/government-console-ep1/q180.json b/system/quests/government-console-ep1/q180.json new file mode 100644 index 00000000..24c7194d --- /dev/null +++ b/system/quests/government-console-ep1/q180.json @@ -0,0 +1,4 @@ +{ + "AvailableIf": "!V_V1Present", + "Joinable": true, +} diff --git a/system/quests/government-console-ep1/q155-gc-e.bin b/system/quests/government-console-ep1/q181-gc-e.bin similarity index 100% rename from system/quests/government-console-ep1/q155-gc-e.bin rename to system/quests/government-console-ep1/q181-gc-e.bin diff --git a/system/quests/government-console-ep1/q155-gc.dat b/system/quests/government-console-ep1/q181-gc.dat similarity index 100% rename from system/quests/government-console-ep1/q155-gc.dat rename to system/quests/government-console-ep1/q181-gc.dat diff --git a/system/quests/government-console-ep1/q181-xb-e.bin b/system/quests/government-console-ep1/q181-xb-e.bin new file mode 120000 index 00000000..e4ec31eb --- /dev/null +++ b/system/quests/government-console-ep1/q181-xb-e.bin @@ -0,0 +1 @@ +q181-gc-e.bin \ No newline at end of file diff --git a/system/quests/government-console-ep1/q181-xb.dat b/system/quests/government-console-ep1/q181-xb.dat new file mode 120000 index 00000000..ba60660e --- /dev/null +++ b/system/quests/government-console-ep1/q181-xb.dat @@ -0,0 +1 @@ +q181-gc.dat \ No newline at end of file diff --git a/system/quests/government-console-ep1/q181.json b/system/quests/government-console-ep1/q181.json new file mode 100644 index 00000000..24c7194d --- /dev/null +++ b/system/quests/government-console-ep1/q181.json @@ -0,0 +1,4 @@ +{ + "AvailableIf": "!V_V1Present", + "Joinable": true, +} diff --git a/system/quests/government-console-ep1/q156-gc-e.bin b/system/quests/government-console-ep1/q182-gc-e.bin similarity index 100% rename from system/quests/government-console-ep1/q156-gc-e.bin rename to system/quests/government-console-ep1/q182-gc-e.bin diff --git a/system/quests/government-console-ep1/q156-gc.dat b/system/quests/government-console-ep1/q182-gc.dat similarity index 100% rename from system/quests/government-console-ep1/q156-gc.dat rename to system/quests/government-console-ep1/q182-gc.dat diff --git a/system/quests/government-console-ep1/q182-xb-e.bin b/system/quests/government-console-ep1/q182-xb-e.bin new file mode 120000 index 00000000..9f1758c4 --- /dev/null +++ b/system/quests/government-console-ep1/q182-xb-e.bin @@ -0,0 +1 @@ +q182-gc-e.bin \ No newline at end of file diff --git a/system/quests/government-console-ep1/q182-xb.dat b/system/quests/government-console-ep1/q182-xb.dat new file mode 120000 index 00000000..1862fbc7 --- /dev/null +++ b/system/quests/government-console-ep1/q182-xb.dat @@ -0,0 +1 @@ +q182-gc.dat \ No newline at end of file diff --git a/system/quests/government-console-ep1/q182.json b/system/quests/government-console-ep1/q182.json new file mode 100644 index 00000000..0e87304d --- /dev/null +++ b/system/quests/government-console-ep1/q182.json @@ -0,0 +1,3 @@ +{ + "AvailableIf": "!V_V1Present", +} diff --git a/system/quests/government-console-ep1/q158-gc-e.bin b/system/quests/government-console-ep1/q183-gc-e.bin similarity index 100% rename from system/quests/government-console-ep1/q158-gc-e.bin rename to system/quests/government-console-ep1/q183-gc-e.bin diff --git a/system/quests/government-console-ep1/q158-gc.dat b/system/quests/government-console-ep1/q183-gc.dat similarity index 100% rename from system/quests/government-console-ep1/q158-gc.dat rename to system/quests/government-console-ep1/q183-gc.dat diff --git a/system/quests/government-console-ep1/q183-xb-e.bin b/system/quests/government-console-ep1/q183-xb-e.bin new file mode 120000 index 00000000..2d140a19 --- /dev/null +++ b/system/quests/government-console-ep1/q183-xb-e.bin @@ -0,0 +1 @@ +q183-gc-e.bin \ No newline at end of file diff --git a/system/quests/government-console-ep1/q183-xb.dat b/system/quests/government-console-ep1/q183-xb.dat new file mode 120000 index 00000000..2636f549 --- /dev/null +++ b/system/quests/government-console-ep1/q183-xb.dat @@ -0,0 +1 @@ +q183-gc.dat \ No newline at end of file diff --git a/system/quests/government-console-ep1/q183.json b/system/quests/government-console-ep1/q183.json new file mode 100644 index 00000000..24c7194d --- /dev/null +++ b/system/quests/government-console-ep1/q183.json @@ -0,0 +1,4 @@ +{ + "AvailableIf": "!V_V1Present", + "Joinable": true, +} diff --git a/system/quests/government-console-ep1/q159-gc-e.bin b/system/quests/government-console-ep1/q184-gc-e.bin similarity index 100% rename from system/quests/government-console-ep1/q159-gc-e.bin rename to system/quests/government-console-ep1/q184-gc-e.bin diff --git a/system/quests/government-console-ep1/q159-gc.dat b/system/quests/government-console-ep1/q184-gc.dat similarity index 100% rename from system/quests/government-console-ep1/q159-gc.dat rename to system/quests/government-console-ep1/q184-gc.dat diff --git a/system/quests/government-console-ep1/q184-xb-e.bin b/system/quests/government-console-ep1/q184-xb-e.bin new file mode 120000 index 00000000..f44b688a --- /dev/null +++ b/system/quests/government-console-ep1/q184-xb-e.bin @@ -0,0 +1 @@ +q184-gc-e.bin \ No newline at end of file diff --git a/system/quests/government-console-ep1/q184-xb.dat b/system/quests/government-console-ep1/q184-xb.dat new file mode 120000 index 00000000..6cdb2c47 --- /dev/null +++ b/system/quests/government-console-ep1/q184-xb.dat @@ -0,0 +1 @@ +q184-gc.dat \ No newline at end of file diff --git a/system/quests/government-console-ep1/q184.json b/system/quests/government-console-ep1/q184.json new file mode 100644 index 00000000..0e87304d --- /dev/null +++ b/system/quests/government-console-ep1/q184.json @@ -0,0 +1,3 @@ +{ + "AvailableIf": "!V_V1Present", +} diff --git a/system/quests/government-console-ep1/q161-gc-e.bin b/system/quests/government-console-ep1/q185-gc-e.bin similarity index 100% rename from system/quests/government-console-ep1/q161-gc-e.bin rename to system/quests/government-console-ep1/q185-gc-e.bin diff --git a/system/quests/government-console-ep1/q161-gc.dat b/system/quests/government-console-ep1/q185-gc.dat similarity index 100% rename from system/quests/government-console-ep1/q161-gc.dat rename to system/quests/government-console-ep1/q185-gc.dat diff --git a/system/quests/government-console-ep1/q185-xb-e.bin b/system/quests/government-console-ep1/q185-xb-e.bin new file mode 120000 index 00000000..daf89da8 --- /dev/null +++ b/system/quests/government-console-ep1/q185-xb-e.bin @@ -0,0 +1 @@ +q185-gc-e.bin \ No newline at end of file diff --git a/system/quests/government-console-ep1/q185-xb.dat b/system/quests/government-console-ep1/q185-xb.dat new file mode 120000 index 00000000..260f4f92 --- /dev/null +++ b/system/quests/government-console-ep1/q185-xb.dat @@ -0,0 +1 @@ +q185-gc.dat \ No newline at end of file diff --git a/system/quests/government-console-ep1/q185.json b/system/quests/government-console-ep1/q185.json new file mode 100644 index 00000000..0e87304d --- /dev/null +++ b/system/quests/government-console-ep1/q185.json @@ -0,0 +1,3 @@ +{ + "AvailableIf": "!V_V1Present", +} diff --git a/system/quests/government-console-ep1/q162-gc-e.bin b/system/quests/government-console-ep1/q186-gc-e.bin similarity index 100% rename from system/quests/government-console-ep1/q162-gc-e.bin rename to system/quests/government-console-ep1/q186-gc-e.bin diff --git a/system/quests/government-console-ep1/q162-gc.dat b/system/quests/government-console-ep1/q186-gc.dat similarity index 100% rename from system/quests/government-console-ep1/q162-gc.dat rename to system/quests/government-console-ep1/q186-gc.dat diff --git a/system/quests/government-console-ep1/q186-xb-e.bin b/system/quests/government-console-ep1/q186-xb-e.bin new file mode 120000 index 00000000..d35b2455 --- /dev/null +++ b/system/quests/government-console-ep1/q186-xb-e.bin @@ -0,0 +1 @@ +q186-gc-e.bin \ No newline at end of file diff --git a/system/quests/government-console-ep1/q186-xb.dat b/system/quests/government-console-ep1/q186-xb.dat new file mode 120000 index 00000000..1e03148e --- /dev/null +++ b/system/quests/government-console-ep1/q186-xb.dat @@ -0,0 +1 @@ +q186-gc.dat \ No newline at end of file diff --git a/system/quests/government-console-ep1/q186.json b/system/quests/government-console-ep1/q186.json new file mode 100644 index 00000000..0e87304d --- /dev/null +++ b/system/quests/government-console-ep1/q186.json @@ -0,0 +1,3 @@ +{ + "AvailableIf": "!V_V1Present", +} diff --git a/system/quests/government-console-ep1/q163-gc-e.bin b/system/quests/government-console-ep1/q187-gc-e.bin similarity index 100% rename from system/quests/government-console-ep1/q163-gc-e.bin rename to system/quests/government-console-ep1/q187-gc-e.bin diff --git a/system/quests/government-console-ep1/q163-gc.dat b/system/quests/government-console-ep1/q187-gc.dat similarity index 100% rename from system/quests/government-console-ep1/q163-gc.dat rename to system/quests/government-console-ep1/q187-gc.dat diff --git a/system/quests/government-console-ep1/q187-xb-e.bin b/system/quests/government-console-ep1/q187-xb-e.bin new file mode 120000 index 00000000..5c995538 --- /dev/null +++ b/system/quests/government-console-ep1/q187-xb-e.bin @@ -0,0 +1 @@ +q187-gc-e.bin \ No newline at end of file diff --git a/system/quests/government-console-ep1/q187-xb.dat b/system/quests/government-console-ep1/q187-xb.dat new file mode 120000 index 00000000..b24cd753 --- /dev/null +++ b/system/quests/government-console-ep1/q187-xb.dat @@ -0,0 +1 @@ +q187-gc.dat \ No newline at end of file diff --git a/system/quests/government-console-ep1/q187.json b/system/quests/government-console-ep1/q187.json new file mode 100644 index 00000000..24c7194d --- /dev/null +++ b/system/quests/government-console-ep1/q187.json @@ -0,0 +1,4 @@ +{ + "AvailableIf": "!V_V1Present", + "Joinable": true, +} diff --git a/system/quests/government-console-ep1/q164-gc-e.bin b/system/quests/government-console-ep1/q188-gc-e.bin similarity index 100% rename from system/quests/government-console-ep1/q164-gc-e.bin rename to system/quests/government-console-ep1/q188-gc-e.bin diff --git a/system/quests/government-console-ep1/q164-gc.dat b/system/quests/government-console-ep1/q188-gc.dat similarity index 100% rename from system/quests/government-console-ep1/q164-gc.dat rename to system/quests/government-console-ep1/q188-gc.dat diff --git a/system/quests/government-console-ep1/q188-xb-e.bin b/system/quests/government-console-ep1/q188-xb-e.bin new file mode 120000 index 00000000..b483053a --- /dev/null +++ b/system/quests/government-console-ep1/q188-xb-e.bin @@ -0,0 +1 @@ +q188-gc-e.bin \ No newline at end of file diff --git a/system/quests/government-console-ep1/q188-xb.dat b/system/quests/government-console-ep1/q188-xb.dat new file mode 120000 index 00000000..a009ba5d --- /dev/null +++ b/system/quests/government-console-ep1/q188-xb.dat @@ -0,0 +1 @@ +q188-gc.dat \ No newline at end of file diff --git a/system/quests/government-console-ep1/q188.json b/system/quests/government-console-ep1/q188.json new file mode 100644 index 00000000..24c7194d --- /dev/null +++ b/system/quests/government-console-ep1/q188.json @@ -0,0 +1,4 @@ +{ + "AvailableIf": "!V_V1Present", + "Joinable": true, +} diff --git a/system/quests/hidden/q88532-gc-e.bin.txt b/system/quests/hidden/q88532-gc3-e.bin.txt similarity index 100% rename from system/quests/hidden/q88532-gc-e.bin.txt rename to system/quests/hidden/q88532-gc3-e.bin.txt diff --git a/system/quests/hidden/q88532-gc.dat b/system/quests/hidden/q88532-gc3.dat similarity index 100% rename from system/quests/hidden/q88532-gc.dat rename to system/quests/hidden/q88532-gc3.dat diff --git a/system/quests/hidden/q88533-gc-e.bin.txt b/system/quests/hidden/q88533-gc3-e.bin.txt similarity index 100% rename from system/quests/hidden/q88533-gc-e.bin.txt rename to system/quests/hidden/q88533-gc3-e.bin.txt diff --git a/system/quests/hidden/q88533-gc.dat b/system/quests/hidden/q88533-gc3.dat similarity index 100% rename from system/quests/hidden/q88533-gc.dat rename to system/quests/hidden/q88533-gc3.dat diff --git a/system/quests/battle/b88001.json b/system/quests/retrieval/q058.json similarity index 78% rename from system/quests/battle/b88001.json rename to system/quests/retrieval/q058.json index c6988120..a2c97a34 100644 --- a/system/quests/battle/b88001.json +++ b/system/quests/retrieval/q058.json @@ -2,38 +2,6 @@ // Each quest may have an optional JSON file (like this one) that defines // server-side behaviors for the quest. - // For battle quests, the BattleRules field should be defined to match the - // rules that the quest defines internally. These are the rules for Battle 1. - "BattleRules": { - "TechDiskMode": "ALLOW", - "WeaponAndArmorMode": "ALLOW", - "MagMode": "ALLOW", - "ToolMode": "ALLOW", - "TrapMode": "ALL_PLAYERS", - "RespawnMode": "ALLOW", - "ReplaceChar": 0, - "DropWeapon": 1, - "IsTeams": 1, - "HideTargetReticle": 1, - "DeathLevelUp": 3, - "MesetaMode": "ALLOW", - "EnableSonar": 1, - "TimeLimit": 10, - "ForbidScapeDolls": 1, - "DeathTechLevelUp": 1, - "TrapCounts": [5, 5, 5, 5], - "SonarCount": 5, - "BoxDropArea": 10, - // These rules are used by other battles, but not by Battle 1: - // "Lives": 10, - // "MaxTechLevel": 15, - // "CharLevel": 1, - }, - - // Challenge quests should specify the ChallengeTemplateIndex field, which - // should match the template that the quest uses to replace player characters. - // "ChallengeTemplateIndex": 0, - // Quests may be set to be unavailable until a preceding quest has been // cleared or a team reward has been purchased. To enable this behavior, set // a value for AvailableIf in the quest's JSON file. (This is ignored if the @@ -47,7 +15,7 @@ // V_V1Present: Whether there are any V1 players in the current game // You can also use constants, parentheses, and many common integer and // boolean operators. An example expression with random values is shown here. - // "AvailableIf": "(F_016D || T_EpicCustomQuest || (V_NumPlayers <= 2)) && !F_0173", + // "AvailableIf": "(F_016D || T_MyCustomQuest || (V_NumPlayers <= 2)) && !F_0173", // On BB, quests may be disabled but still visible to the player. This // expression controls when that should be the case. If AvailableIf evaluates diff --git a/tests/config.json b/tests/config.json index 96bec856..c5174f4b 100644 --- a/tests/config.json +++ b/tests/config.json @@ -263,8 +263,6 @@ [0x010, "government-ep2", "The Military's Hero", "$E$CG-Heathcliff Flowen-\n$C6Quests that follow\nthe Episode 2\nstoryline"], [0x010, "government-ep4", "The Meteor Impact Incident", "$E$C6Quests that follow\nthe Episode 4\nstoryline"], [0x020, "download", "Download", "$E$C6Quests to download\nto your Memory Card"], - [0x040, "download-ep3-trial", "Trial Download", "$E$C6Quests to download\nto your Memory Card\nfrom Episode 3\nTrial Edition"], - [0x040, "download-ep3", "Download", "$E$C6Quests to download\nto your Memory Card"], ], "ItemStackLimits": [