implement episode 3 battles
This commit is contained in:
+10
-1
@@ -48,7 +48,16 @@ add_executable(newserv
|
||||
src/Client.cc
|
||||
src/Compression.cc
|
||||
src/DNSServer.cc
|
||||
src/Episode3.cc
|
||||
src/Episode3/AssistServer.cc
|
||||
src/Episode3/Card.cc
|
||||
src/Episode3/CardSpecial.cc
|
||||
src/Episode3/DataIndex.cc
|
||||
src/Episode3/DeckState.cc
|
||||
src/Episode3/MapState.cc
|
||||
src/Episode3/PlayerState.cc
|
||||
src/Episode3/PlayerStateSubordinates.cc
|
||||
src/Episode3/RulerServer.cc
|
||||
src/Episode3/Server.cc
|
||||
src/FileContentsCache.cc
|
||||
src/FunctionCompiler.cc
|
||||
src/GSLArchive.cc
|
||||
|
||||
@@ -30,8 +30,8 @@ With that said, I offer no guarantees on how or when this project will advance.
|
||||
|
||||
Current known issues / missing features / things to do:
|
||||
- Support disconnect hooks to clean up state, like if a client disconnects during quest loading or during a trade window execution.
|
||||
- Episode 3 battles and tournaments aren't implemented. (Some reverse-engineering has already been done here though - see Episode3.hh/cc)
|
||||
- Card auctions could be supported without too much effort, though; only the EF command needs to be implemented.
|
||||
- Episode 3 battles are implemented but not well-tested.
|
||||
- Card auctions could be supported without too much effort, though; only the EF command needs to be implemented.
|
||||
- PSOBB is not well-tested and likely will disconnect or misbehave when clients try to use unimplemented features.
|
||||
- Enemy indexes also desync slightly in most games, often in later areas, leading to incorrect EXP values being given for killed enemies.
|
||||
- Fix some edge cases on the BB proxy server (e.g. make sure Change Ship does the right thing, which is not the same as what it should do on V2/V3).
|
||||
@@ -42,6 +42,7 @@ Current known issues / missing features / things to do:
|
||||
- Enforce client-side size limits (e.g. for 60/62 commands) on the server side as well. (For 60/62 specifically, perhaps transform them to 6C/6D if needed.)
|
||||
- Encapsulate BB server-side random state and make replays deterministic.
|
||||
- The internal menu abstraction is ugly and hard to work with. Rewrite it.
|
||||
- Add default values for all commands (like we use for Episode 3 battle commands).
|
||||
|
||||
## Compatibility
|
||||
|
||||
@@ -63,7 +64,7 @@ newserv supports several versions of PSO. Specifically:
|
||||
*Notes:*
|
||||
1. *DC support has only been tested with the US versions of PSO DC. Other versions probably don't work, but will be easy to add. Please submit a GitHub issue if you have a non-US DC version, and can provide a log from a connection attempt.*
|
||||
2. *This version only supports the modem adapter, which Dolphin does not currently emulate, so it's difficult to test.*
|
||||
3. *Episode 3 players can download quests, join lobbies, create and join games, and trade cards, but CARD battles are not implemented yet. Tournaments are also not supported.*
|
||||
3. *Episode 3 players can download quests, join lobbies, create and join games, and trade cards. CARD battles are implemented but not well-tested. Tournaments and View Battle are not implemented.*
|
||||
4. *newserv's implementation of PSOX is based on disassembly of the client executable; it has never been tested with a real client and most likely doesn't work.*
|
||||
5. *Some basic features are not implemented in Blue Burst games, so the games are not very playable. A lot of work has to be done to get BB games to a playable state.*
|
||||
6. *Support for PSO Dreamcast Trial Edition is very incomplete and probably never will be complete. This is really just exploring a curiosity that sheds some light on early network engineering done by Sega, not an actual attempt at supporting this version of the game.*
|
||||
|
||||
+1281
-1398
File diff suppressed because it is too large
Load Diff
-936
@@ -1,936 +0,0 @@
|
||||
#include "Episode3.hh"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <array>
|
||||
#include <phosg/Filesystem.hh>
|
||||
|
||||
#include "Loggers.hh"
|
||||
#include "Compression.hh"
|
||||
#include "Text.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
|
||||
static const vector<const char*> name_for_card_type({
|
||||
"HunterSC",
|
||||
"ArkzSC",
|
||||
"Item",
|
||||
"Creature",
|
||||
"Action",
|
||||
"Assist",
|
||||
});
|
||||
|
||||
static const unordered_map<uint8_t, const char*> description_for_when({
|
||||
{0x01, "Set"}, // TODO: Is 01 this, or "Permanent"?
|
||||
{0x02, "Attack"},
|
||||
{0x03, "??? (TODO)"},
|
||||
{0x04, "Before turn"},
|
||||
{0x05, "Destroyed"},
|
||||
{0x0A, "Permanent"}, // only used on Tollaw; could be same as 01
|
||||
{0x0B, "Battle"},
|
||||
{0x0C, "Opponent destroyed"}, // TODO: but this is also used for some support things like Shifta, and for Snatch, which also applies when opponents are not destroyed
|
||||
{0x0D, "Attack lands"},
|
||||
{0x0E, "Before attack phase"},
|
||||
{0x16, "Battle end"},
|
||||
{0x17, "Each defense"},
|
||||
{0x20, "Each attack"},
|
||||
{0x22, "Act phase"},
|
||||
{0x27, "Move phase"},
|
||||
{0x29, "Set and act phases"},
|
||||
{0x33, "Defense phase"},
|
||||
{0x3D, "Battle"}, // TODO: how is this different from 3D and 0B?
|
||||
{0x3E, "Battle"}, // TODO: how is this different from 3D and 0B?
|
||||
{0x3F, "Each defense"}, // TODO: how is this different from 17?
|
||||
{0x46, "On specific turn"},
|
||||
});
|
||||
|
||||
static const unordered_map<string, const char*> description_for_expr_token({
|
||||
{"f", "Number of FCs controlled by current SC"},
|
||||
{"d", "Die roll"},
|
||||
{"ap", "Attacker AP"}, // Unused
|
||||
{"tp", "Attacker TP"},
|
||||
{"hp", "Attacker HP"}, // TODO: How is this different from ehp?
|
||||
{"mhp", "Attacker maximum HP"},
|
||||
{"dm", "Unknown: dm"}, // Unused
|
||||
{"tdm", "Unknown: tdm"}, // Unused
|
||||
{"tf", "Number of SC\'s destroyed FCs"},
|
||||
{"ac", "Remaining ATK points"},
|
||||
{"php", "Unknown: php"}, // Unused
|
||||
{"dc", "Unknown: dc"}, // Unused
|
||||
{"cs", "Unknown: cs"}, // Unused
|
||||
{"a", "Unknown: a"}, // Unused
|
||||
{"kap", "Action cards AP"},
|
||||
{"ktp", "Action cards TP"},
|
||||
{"dn", "Unknown: dn"}, // Unused
|
||||
{"hf", "Unknown: hf"}, // Unused
|
||||
{"df", "Number of destroyed ally FCs (including SC\'s own)"},
|
||||
{"ff", "Number of ally FCs (including SC\'s own)"},
|
||||
{"ef", "Number of enemy FCs"},
|
||||
{"bi", "Number of Native FCs on either team"},
|
||||
{"ab", "Number of A.Beast FCs on either team"},
|
||||
{"mc", "Number of Machine FCs on either team"},
|
||||
{"dk", "Number of Dark FCs on either team"},
|
||||
{"sa", "Number of Sword-type items on either team"},
|
||||
{"gn", "Number of Gun-type items on either team"},
|
||||
{"wd", "Number of Cane-type items on either team"},
|
||||
{"tt", "Unknown: tt"}, // Unused
|
||||
{"lv", "Dice bonus"},
|
||||
{"adm", "Attack damage"},
|
||||
{"ddm", "Defending damage"},
|
||||
{"sat", "Number of Sword-type items on SC\'s team"},
|
||||
{"edm", "Defending damage"}, // TODO: How is this different from ddm?
|
||||
{"ldm", "Unknown: ldm"}, // Unused
|
||||
{"rdm", "Defending damage"}, // TODO: How is this different from ddm/edm?
|
||||
{"fdm", "Final damage (after defense)"},
|
||||
{"ndm", "Unknown: ndm"}, // Unused
|
||||
{"ehp", "Attacker HP"},
|
||||
});
|
||||
|
||||
// Arguments are encoded as 3-character null-terminated strings (why?!), and are
|
||||
// used for adding conditions to effects (e.g. making them only trigger in
|
||||
// certain situations) or otherwise customizing their results.
|
||||
// Argument meanings:
|
||||
// a01 = ???
|
||||
// cXY/CXY = linked items (require item with cYX/CYX to be equipped as well)
|
||||
// dXY = roll one die; require result between X and Y inclusive
|
||||
// e00 = effect lasts while equipped? (in contrast to tXX)
|
||||
// hXX = require HP >= XX
|
||||
// iXX = require HP <= XX
|
||||
// nXX = require condition XX (see description_for_n_condition)
|
||||
// oXX = seems to be "require previous random-condition effect to have happened"
|
||||
// TODO: this is used as both o01 (recovery) and o11 (reflection)
|
||||
// conditions - why the difference?
|
||||
// pXX = who to target (see description_for_p_target)
|
||||
// rXX = randomly pass with XX% chance (if XX == 00, 100% chance?)
|
||||
// sXY = require card cost between X and Y ATK points (inclusive)
|
||||
// tXX = lasts XX turns, or activate after XX turns
|
||||
|
||||
static const vector<const char*> description_for_n_condition({
|
||||
/* n00 */ "Always true",
|
||||
/* n01 */ "??? (TODO)",
|
||||
/* n02 */ "Destroyed with a single attack?",
|
||||
/* n03 */ "Unknown", // Unused
|
||||
/* n04 */ "Attack has Pierce",
|
||||
/* n05 */ "Attack has Rampage",
|
||||
/* n06 */ "Native attribute",
|
||||
/* n07 */ "A.Beast attribute",
|
||||
/* n08 */ "Machine attribute",
|
||||
/* n09 */ "Dark attribute",
|
||||
/* n10 */ "Sword-type item",
|
||||
/* n11 */ "Gun-type item",
|
||||
/* n12 */ "Cane-type item",
|
||||
/* n13 */ "Guard item",
|
||||
/* n14 */ "Story Character",
|
||||
/* n15 */ "Attacker does not use action cards",
|
||||
/* n16 */ "Aerial attribute",
|
||||
/* n17 */ "Same AP as opponent",
|
||||
/* n18 */ "Opponent is SC",
|
||||
/* n19 */ "Has Paralyzed condition",
|
||||
/* n20 */ "Has Frozen condition",
|
||||
});
|
||||
|
||||
static const vector<const char*> description_for_p_target({
|
||||
/* p00 */ "Unknown: p00", // Unused; probably invalid
|
||||
/* p01 */ "SC / FC who set the card",
|
||||
/* p02 */ "Attacking SC / FC",
|
||||
/* p03 */ "Unknown: p03", // Unused
|
||||
/* p04 */ "Unknown: p04", // Unused
|
||||
/* p05 */ "Unknown: p05", // Unused
|
||||
/* p06 */ "??? (TODO)",
|
||||
/* p07 */ "??? (TODO; Weakness)",
|
||||
/* p08 */ "FC's owner SC",
|
||||
/* p09 */ "Unknown: p09", // Unused
|
||||
/* p10 */ "All ally FCs",
|
||||
/* p11 */ "All ally FCs", // TODO: how is this different from p10?
|
||||
/* p12 */ "All non-aerial FCs on both teams",
|
||||
/* p13 */ "All FCs on both teams that are Frozen",
|
||||
/* p14 */ "All FCs on both teams that have <= 3 HP",
|
||||
/* p15 */ "All FCs except SCs", // TODO: used during attacks only?
|
||||
/* p16 */ "All FCs except SCs", // TODO: used during attacks only? how is this different from p15?
|
||||
/* p17 */ "This card",
|
||||
/* p18 */ "SC who equipped this card",
|
||||
/* p19 */ "Unknown: p19", // Unused
|
||||
/* p20 */ "Unknown: p20", // Unused
|
||||
/* p21 */ "Unknown: p21", // Unused
|
||||
/* p22 */ "All characters (SCs & FCs) including this card", // TODO: But why does Shifta apply only to allies then?
|
||||
/* p23 */ "All characters (SCs & FCs) except this card",
|
||||
/* p24 */ "All FCs on both teams that have Paralysis",
|
||||
/* p25 */ "Unknown: p25", // Unused
|
||||
/* p26 */ "Unknown: p26", // Unused
|
||||
/* p27 */ "Unknown: p27", // Unused
|
||||
/* p28 */ "Unknown: p28", // Unused
|
||||
/* p29 */ "Unknown: p29", // Unused
|
||||
/* p30 */ "Unknown: p30", // Unused
|
||||
/* p31 */ "Unknown: p31", // Unused
|
||||
/* p32 */ "Unknown: p32", // Unused
|
||||
/* p33 */ "Unknown: p33", // Unused
|
||||
/* p34 */ "Unknown: p34", // Unused
|
||||
/* p35 */ "All characters (SCs & FCs) within range", // Used for Explosion effect
|
||||
/* p36 */ "All ally SCs within range, but not the caster", // Resta
|
||||
/* p37 */ "All FCs or all opponent FCs (TODO)", // TODO: when to use which selector? is a3 involved here somehow?
|
||||
/* p38 */ "All allies except items within range (and not this card)",
|
||||
/* p39 */ "All FCs that cost 4 or more points",
|
||||
/* p40 */ "All FCs that cost 3 or fewer points",
|
||||
/* p41 */ "Unknown: p41", // Unused
|
||||
/* p42 */ "Attacker during defense phase", // Only used by TP Defense
|
||||
/* p43 */ "Owner SC of defending FC during attack",
|
||||
/* p44 */ "SC\'s own creature FCs within range",
|
||||
/* p45 */ "Both attacker and defender", // Used for Snatch, which moves EXP from one to the other
|
||||
/* p46 */ "All SCs & FCs one space left or right of this card",
|
||||
/* p47 */ "FC\'s owner Boss SC", // Only used for Gibbles+ which explicitly mentions Boss SC, so it looks like this is p08 but for bosses
|
||||
/* p48 */ "Everything within range, including this card\'s user", // Madness
|
||||
/* p49 */ "All ally FCs within range except this card",
|
||||
});
|
||||
|
||||
struct Ep3AbilityDescription {
|
||||
uint8_t command;
|
||||
bool has_expr;
|
||||
const char* name;
|
||||
const char* description;
|
||||
};
|
||||
|
||||
static const std::vector<Ep3AbilityDescription> name_for_effect_command({
|
||||
{0x00, false, nullptr, nullptr},
|
||||
{0x01, true, "AP Boost", "Temporarily increase AP by N"},
|
||||
{0x02, false, "Rampage", "Rampage"},
|
||||
{0x03, true, "Multi Strike", "Duplicate attack N times"},
|
||||
{0x04, true, "Damage Modifier 1", "Set attack damage / AP to N after action cards applied (step 1)"},
|
||||
{0x05, false, "Immobile", "Give Immobile condition"},
|
||||
{0x06, false, "Hold", "Give Hold condition"},
|
||||
{0x07, false, nullptr, nullptr},
|
||||
{0x08, true, "TP Boost", "Add N TP temporarily during attack"},
|
||||
{0x09, true, "Give Damage", "Cause direct N HP loss"},
|
||||
{0x0A, false, "Guom", "Give Guom condition"},
|
||||
{0x0B, false, "Paralyze", "Give Paralysis condition"},
|
||||
{0x0C, false, nullptr, nullptr},
|
||||
{0x0D, false, "A/H Swap", "Swap AP and HP temporarily"},
|
||||
{0x0E, false, "Pierce", "Attack SC directly even if they have items equipped"},
|
||||
{0x0F, false, nullptr, nullptr},
|
||||
{0x10, true, "Heal", "Increase HP by N"},
|
||||
{0x11, false, "Return to Hand", "Return card to hand"},
|
||||
{0x12, false, nullptr, nullptr},
|
||||
{0x13, false, nullptr, nullptr},
|
||||
{0x14, false, "Acid", "Give Acid condition"},
|
||||
{0x15, false, nullptr, nullptr},
|
||||
{0x16, true, "Mighty Knuckle", "Temporarily increase AP by N, and set ATK dice to zero"},
|
||||
{0x17, true, "Unit Blow", "Temporarily increase AP by N * number of this card set within phase"},
|
||||
{0x18, false, "Curse", "Give Curse condition"},
|
||||
{0x19, false, "Combo (AP)", "Temporarily increase AP by number of this card set within phase"},
|
||||
{0x1A, false, "Pierce/Rampage Block", "Block attack if Pierce/Rampage (?)"},
|
||||
{0x1B, false, "Ability Trap", "Temporarily disable opponent abilities"},
|
||||
{0x1C, false, "Freeze", "Give Freeze condition"},
|
||||
{0x1D, false, "Anti-Abnormality", "Cure all conditions"},
|
||||
{0x1E, false, nullptr, nullptr},
|
||||
{0x1F, false, "Explosion", "Damage all SCs and FCs by number of this same card set * 2"},
|
||||
{0x20, false, nullptr, nullptr},
|
||||
{0x21, false, nullptr, nullptr},
|
||||
{0x22, false, nullptr, nullptr},
|
||||
{0x23, false, "Return to Deck", "Cancel discard and move to bottom of deck instead"},
|
||||
{0x24, false, "Aerial", "Give Aerial status"},
|
||||
{0x25, true, "AP Loss", "Make attacker temporarily lose N AP during defense"},
|
||||
{0x26, true, "Bonus From Leader", "Gain AP equal to the number of cards of type N on the field"},
|
||||
{0x27, false, "Free Maneuver", "Enable movement over occupied tiles"},
|
||||
{0x28, false, "Haste", "Make move actions free"},
|
||||
{0x29, true, "Clone", "Make setting this card free if at least one card of type N is already on the field"},
|
||||
{0x2A, true, "DEF Disable by Cost", "Disable use of any defense cards costing between (N / 10) and (N % 10) points, inclusive"},
|
||||
{0x2B, true, "Filial", "Increase controlling SC\'s HP by N when this card is destroyed"},
|
||||
{0x2C, true, "Snatch", "Steal N EXP during attack"},
|
||||
{0x2D, true, "Hand Disrupter", "DIscard N cards from hand immediately"},
|
||||
{0x2E, false, "Drop", "Give Drop condition"},
|
||||
{0x2F, false, "Action Disrupter", "Destroy all action cards used by attacker"},
|
||||
{0x30, true, "Set HP", "Set HP to N (?) (TODO)"},
|
||||
{0x31, false, "Native Shield", "Block attacks from Native creatures"},
|
||||
{0x32, false, "A.Beast Shield", "Block attacks from A.Beast creatures"},
|
||||
{0x33, false, "Machine Shield", "Block attacks from Machine creatures"},
|
||||
{0x34, false, "Dark Shield", "Block attacks from Dark creatures"},
|
||||
{0x35, false, "Sword Shield", "Block attacks from Sword items"},
|
||||
{0x36, false, "Gun Shield", "Block attacks from Gun items"},
|
||||
{0x37, false, "Cane Shield", "Block attacks from Cane items"},
|
||||
{0x38, false, nullptr, nullptr},
|
||||
{0x39, false, nullptr, nullptr},
|
||||
{0x3A, false, "Defender", "Make attacks go to setter of this card instead of original target"},
|
||||
{0x3B, false, "Survival Decoys", "Redirect damage for multi-sided attack"},
|
||||
{0x3C, true, "Give/Take EXP", "Give N EXP, or take if N is negative"},
|
||||
{0x3D, false, nullptr, nullptr},
|
||||
{0x3E, false, "Death Companion", "If this card has 1 or 2 HP, set its HP to N"},
|
||||
{0x3F, true, "EXP Decoy", "If defender has EXP, lose EXP instead of getting damage when attacked"},
|
||||
{0x40, true, "Set MV", "Set MV to N"},
|
||||
{0x41, true, "Group", "Temporarily increase AP by N * number of this card on field, excluding itself"},
|
||||
{0x42, false, "Berserk", "User of this card receives the same damage as target, and isn't helped by target's defense cards"},
|
||||
{0x43, false, "Guard Creature", "Attacks on controlling SC damage this card instead"},
|
||||
{0x44, false, "Tech", "Technique cards cost 1 fewer ATK point"},
|
||||
{0x45, false, "Big Swing", "Increase all attacking ATK costs by 1"},
|
||||
{0x46, false, nullptr, nullptr},
|
||||
{0x47, false, "Shield Weapon", "Limit attacker\'s choice of target to guard items"},
|
||||
{0x48, false, "ATK Dice Boost", "Increase ATK dice roll by 1"},
|
||||
{0x49, false, nullptr, nullptr},
|
||||
{0x4A, false, "Major Pierce", "If SC has over half of max HP, attacks target SC instead of equipped items"},
|
||||
{0x4B, false, "Heavy Pierce", "If SC has 3 or more items equipped, attacks target SC instead of equipped items"},
|
||||
{0x4C, false, "Major Rampage", "If SC has over half of max HP, attacks target SC and all equipped items"},
|
||||
{0x4D, false, "Heavy Rampage", "If SC has 3 or more items equipped, attacks target SC and all equipped items"},
|
||||
{0x4E, true, "AP Growth", "Permanently increase AP by N"},
|
||||
{0x4F, true, "TP Growth", "Permanently increase TP by N"},
|
||||
{0x50, true, "Reborn", "If any card of type N is on the field, this card goes to the hand when destroyed instead of being discarded"},
|
||||
{0x51, true, "Copy", "Temporarily set AP/TP to N percent (or 100% if N is 0) of opponent\'s values"},
|
||||
{0x52, false, nullptr, nullptr},
|
||||
{0x53, true, "Misc. Guards", "Add N to card's defense value"},
|
||||
{0x54, true, "AP Override", "Set AP to N temporarily"},
|
||||
{0x55, true, "TP Override", "Set TP to N temporarily"},
|
||||
{0x56, false, "Return", "Return card to hand on destruction instead of discarding"},
|
||||
{0x57, false, "A/T Swap Perm", "Permanently swap AP and TP"},
|
||||
{0x58, false, "A/H Swap Perm", "Permanently swap AP and HP"},
|
||||
{0x59, true, "Slayers/Assassins", "Temporarily increase AP during attack"},
|
||||
{0x5A, false, "Anti-Abnormality", "Remove all conditions"},
|
||||
{0x5B, false, "Fixed Range", "Use SC\'s range instead of weapon or attack card ranges"},
|
||||
{0x5C, false, "Elude", "SC does not lose HP when equipped items are destroyed"},
|
||||
{0x5D, false, "Parry", "Forward attack to a random FC within one tile of original target, excluding attacker and original target"},
|
||||
{0x5E, false, "Block Attack", "Completely block attack"},
|
||||
{0x5F, false, nullptr, nullptr},
|
||||
{0x60, false, nullptr, nullptr},
|
||||
{0x61, true, "Combo (TP)", "Gain TP equal to the number of cards of type N on the field"},
|
||||
{0x62, true, "Misc. AP Bonuses", "Temporarily increase AP by N"},
|
||||
{0x63, true, "Misc. TP Bonuses", "Temporarily increase TP by N"},
|
||||
{0x64, false, nullptr, nullptr},
|
||||
{0x65, true, "Misc. Defense Bonuses", "Decrease damage by N"},
|
||||
{0x66, true, "Mostly Halfguards", "Reduce damage from incoming attack by N"},
|
||||
{0x67, false, "Periodic Field", "Swap immunity to tech or physical attacks"},
|
||||
{0x68, false, "Unlimited Summoning", "Allow unlimited summoning"},
|
||||
{0x69, false, nullptr, nullptr},
|
||||
{0x6A, true, "MV Bonus", "Increase MV by N"},
|
||||
{0x6B, true, "Forward Damage", "Give N damage back to attacker during defense (?) (TODO)"},
|
||||
{0x6C, true, "Weak Spot / Influence", "Temporarily decrease AP by N"},
|
||||
{0x6D, true, "Damage Modifier 2", "Set attack damage / AP after action cards applied (step 2)"},
|
||||
{0x6E, true, "Weak Hit Block", "Block all attacks of N damage or less"},
|
||||
{0x6F, true, "AP Silence", "Temporarily decrease AP of opponent by N"},
|
||||
{0x70, true, "TP Silence", "Temporarily decrease TP of opponent by N"},
|
||||
{0x71, false, "A/T Swap", "Temporarily swap AP and TP"},
|
||||
{0x72, true, "Halfguard", "Halve damage from attacks that would inflict N or more damage"},
|
||||
{0x73, false, nullptr, nullptr},
|
||||
{0x74, true, "Rampage AP Loss", "Temporarily reduce AP by N"},
|
||||
{0x75, false, nullptr, nullptr},
|
||||
{0x76, false, "Reflect", "Generate reverse attack"},
|
||||
});
|
||||
|
||||
void Ep3CardDefinition::Stat::decode_code() {
|
||||
this->type = static_cast<Type>(this->code / 1000);
|
||||
int16_t value = this->code - (this->type * 1000);
|
||||
if (value != 999) {
|
||||
switch (this->type) {
|
||||
case Type::BLANK:
|
||||
this->stat = 0;
|
||||
break;
|
||||
case Type::STAT:
|
||||
case Type::PLUS_STAT:
|
||||
case Type::EQUALS_STAT:
|
||||
this->stat = value;
|
||||
break;
|
||||
case Type::MINUS_STAT:
|
||||
this->stat = -value;
|
||||
break;
|
||||
default:
|
||||
throw runtime_error("invalid card stat type");
|
||||
}
|
||||
} else {
|
||||
this->stat = 0;
|
||||
this->type = static_cast<Type>(this->type + 4);
|
||||
}
|
||||
}
|
||||
|
||||
string Ep3CardDefinition::Stat::str() const {
|
||||
switch (this->type) {
|
||||
case Type::BLANK:
|
||||
return "";
|
||||
case Type::STAT:
|
||||
return string_printf("%hhd", this->stat);
|
||||
case Type::PLUS_STAT:
|
||||
return string_printf("+%hhd", this->stat);
|
||||
case Type::MINUS_STAT:
|
||||
return string_printf("-%d", -this->stat);
|
||||
case Type::EQUALS_STAT:
|
||||
return string_printf("=%hhd", this->stat);
|
||||
case Type::UNKNOWN:
|
||||
return "?";
|
||||
case Type::PLUS_UNKNOWN:
|
||||
return "+?";
|
||||
case Type::MINUS_UNKNOWN:
|
||||
return "-?";
|
||||
case Type::EQUALS_UNKNOWN:
|
||||
return "=?";
|
||||
default:
|
||||
return string_printf("[%02hhX %02hhX]", this->type, this->stat);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
bool Ep3CardDefinition::Effect::is_empty() const {
|
||||
return (this->command == 0 &&
|
||||
this->expr.is_filled_with(0) &&
|
||||
this->when == 0 &&
|
||||
this->arg1.is_filled_with(0) &&
|
||||
this->arg2.is_filled_with(0) &&
|
||||
this->arg3.is_filled_with(0) &&
|
||||
this->unknown_a3.is_filled_with(0));
|
||||
}
|
||||
|
||||
string Ep3CardDefinition::Effect::str_for_arg(const std::string& arg) {
|
||||
if (arg.empty()) {
|
||||
return arg;
|
||||
}
|
||||
if (arg.size() != 3) {
|
||||
return arg + "/(invalid)";
|
||||
}
|
||||
size_t value;
|
||||
try {
|
||||
value = stoul(arg.c_str() + 1, nullptr, 10);
|
||||
} catch (const invalid_argument&) {
|
||||
return arg + "/(invalid)";
|
||||
}
|
||||
|
||||
switch (arg[0]) {
|
||||
case 'a':
|
||||
return arg + "/(unknown)";
|
||||
case 'C':
|
||||
case 'c':
|
||||
return string_printf("%s/Req. linked item (%zu=>%zu)", arg.c_str(), value / 10, value % 10);
|
||||
case 'd':
|
||||
return string_printf("%s/Req. die roll in [%zu, %zu]", arg.c_str(), value / 10, value % 10);
|
||||
case 'e':
|
||||
return arg + "/While equipped";
|
||||
case 'h':
|
||||
return string_printf("%s/Req. HP >= %zu", arg.c_str(), value);
|
||||
case 'i':
|
||||
return string_printf("%s/Req. HP <= %zu", arg.c_str(), value);
|
||||
case 'n':
|
||||
try {
|
||||
return string_printf("%s/Req. condition: %s", arg.c_str(), description_for_n_condition.at(value));
|
||||
} catch (const out_of_range&) {
|
||||
return arg + "/(unknown)";
|
||||
}
|
||||
case 'o':
|
||||
return arg + "/Req. prev effect conditions passed";
|
||||
case 'p':
|
||||
try {
|
||||
return string_printf("%s/Target: %s", arg.c_str(), description_for_p_target.at(value));
|
||||
} catch (const out_of_range&) {
|
||||
return arg + "/(unknown)";
|
||||
}
|
||||
case 'r':
|
||||
return string_printf("%s/Req. random with %zu%% chance", arg.c_str(), value == 0 ? 100 : value);
|
||||
case 's':
|
||||
return string_printf("%s/Req. cost in [%zu, %zu]", arg.c_str(), value / 10, value % 10);
|
||||
case 't':
|
||||
return string_printf("%s/Turns: %zu", arg.c_str(), value);
|
||||
default:
|
||||
return arg + "/(unknown)";
|
||||
}
|
||||
}
|
||||
|
||||
string Ep3CardDefinition::Effect::str() const {
|
||||
string cmd_str = string_printf("(%hhu) %02hhX", this->effect_num, this->command);
|
||||
try {
|
||||
const char* name = name_for_effect_command.at(this->command).name;
|
||||
if (name) {
|
||||
cmd_str += ':';
|
||||
cmd_str += name;
|
||||
}
|
||||
} catch (const out_of_range&) { }
|
||||
|
||||
string when_str = string_printf("%02hhX", this->when);
|
||||
try {
|
||||
const char* name = description_for_when.at(this->when);
|
||||
if (name) {
|
||||
when_str += ':';
|
||||
when_str += name;
|
||||
}
|
||||
} catch (const out_of_range&) { }
|
||||
|
||||
string expr_str = this->expr;
|
||||
if (!expr_str.empty()) {
|
||||
expr_str = ", expr=" + expr_str;
|
||||
}
|
||||
|
||||
string arg1str = this->str_for_arg(this->arg1);
|
||||
string arg2str = this->str_for_arg(this->arg2);
|
||||
string arg3str = this->str_for_arg(this->arg3);
|
||||
string a3str = format_data_string(this->unknown_a3.data(), sizeof(this->unknown_a3));
|
||||
return string_printf("(cmd=%s%s, when=%s, arg1=%s, arg2=%s, arg3=%s, a3=%s)",
|
||||
cmd_str.c_str(), expr_str.c_str(), when_str.c_str(), arg1str.data(),
|
||||
arg2str.data(), arg3str.data(), a3str.c_str());
|
||||
}
|
||||
|
||||
|
||||
|
||||
void Ep3CardDefinition::decode_range() {
|
||||
// If the cell representing the FC is nonzero, the card has a range from a
|
||||
// list of constants. Otherwise, its range is already defined in the range
|
||||
// array and should be left alone.
|
||||
uint8_t index = (this->range[4] >> 8) & 0xF;
|
||||
if (index != 0) {
|
||||
this->range.clear(0);
|
||||
switch (index) {
|
||||
case 1: // Single cell in front of FC
|
||||
this->range[3] = 0x00000100;
|
||||
break;
|
||||
case 2: // Cell in front of FC and the front-left and front-right (Slash)
|
||||
this->range[3] = 0x00001110;
|
||||
break;
|
||||
case 3: // 3 cells in a line in front of FC
|
||||
this->range[1] = 0x00000100;
|
||||
this->range[2] = 0x00000100;
|
||||
this->range[3] = 0x00000100;
|
||||
break;
|
||||
case 4: // All 8 cells around FC
|
||||
this->range[3] = 0x00001110;
|
||||
this->range[4] = 0x00001010;
|
||||
this->range[5] = 0x00001110;
|
||||
break;
|
||||
case 5: // 2 cells in a line in front of FC
|
||||
this->range[2] = 0x00000100;
|
||||
this->range[3] = 0x00000100;
|
||||
break;
|
||||
case 6: // Entire field (renders as "A")
|
||||
for (size_t x = 0; x < 6; x++) {
|
||||
this->range[x] = 0x000FFFFF;
|
||||
}
|
||||
break;
|
||||
case 7: // Superposition of 4 and 5 (unused)
|
||||
this->range[2] = 0x00000100;
|
||||
this->range[3] = 0x00001110;
|
||||
this->range[4] = 0x00001010;
|
||||
this->range[5] = 0x00001110;
|
||||
break;
|
||||
case 8: // All 8 cells around FC and FC's cell
|
||||
this->range[3] = 0x00001110;
|
||||
this->range[4] = 0x00001110;
|
||||
this->range[5] = 0x00001110;
|
||||
break;
|
||||
case 9: // No cells
|
||||
break;
|
||||
// The table in the DOL file only appears to contain 9 entries; there are
|
||||
// some pointers immediately after. So probably if a card specified A-F,
|
||||
// its range would be filled in with garbage in the original game.
|
||||
default:
|
||||
throw runtime_error("invalid fixed range index");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
string name_for_rarity(uint8_t rarity) {
|
||||
static const vector<const char*> names({
|
||||
"N1",
|
||||
"R1",
|
||||
"S",
|
||||
"E",
|
||||
"N2",
|
||||
"N3",
|
||||
"N4",
|
||||
"R2",
|
||||
"R3",
|
||||
"R4",
|
||||
"SS",
|
||||
"D1",
|
||||
"D2",
|
||||
"INVIS",
|
||||
});
|
||||
try {
|
||||
return names.at(rarity - 1);
|
||||
} catch (const out_of_range&) {
|
||||
return string_printf("(%02hhX)", rarity);
|
||||
}
|
||||
}
|
||||
|
||||
string name_for_target_mode(uint8_t target_mode) {
|
||||
static const vector<const char*> names({
|
||||
"NONE",
|
||||
"SINGLE",
|
||||
"MULTI",
|
||||
"SELF",
|
||||
"TEAM",
|
||||
"ALL",
|
||||
"MULTI-ALLY",
|
||||
"ALL-ALLY",
|
||||
"ALL-ATTACK",
|
||||
"OWN-FCS",
|
||||
});
|
||||
try {
|
||||
return names.at(target_mode);
|
||||
} catch (const out_of_range&) {
|
||||
return string_printf("(%02hhX)", target_mode);
|
||||
}
|
||||
}
|
||||
|
||||
string string_for_colors(const parray<uint8_t, 8>& colors) {
|
||||
string ret;
|
||||
for (size_t x = 0; x < 8; x++) {
|
||||
if (colors[x]) {
|
||||
ret += '0' + colors[x];
|
||||
}
|
||||
}
|
||||
if (ret.empty()) {
|
||||
return "none";
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
string string_for_assist_turns(uint8_t turns) {
|
||||
if (turns == 90) {
|
||||
return "ONCE";
|
||||
} else if (turns == 99) {
|
||||
return "FOREVER";
|
||||
} else {
|
||||
return string_printf("%hhu", turns);
|
||||
}
|
||||
}
|
||||
|
||||
string string_for_range(const parray<be_uint32_t, 6>& range) {
|
||||
string ret;
|
||||
for (size_t x = 0; x < 6; x++) {
|
||||
ret += string_printf("%05" PRIX32 "/", range[x].load());
|
||||
}
|
||||
while (starts_with(ret, "00000/")) {
|
||||
ret = ret.substr(6);
|
||||
}
|
||||
if (!ret.empty()) {
|
||||
ret.resize(ret.size() - 1);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
string Ep3CardDefinition::str() const {
|
||||
string type_str;
|
||||
try {
|
||||
type_str = name_for_card_type.at(this->type);
|
||||
} catch (const out_of_range&) {
|
||||
type_str = string_printf("%02hhX", this->type);
|
||||
}
|
||||
string rarity_str = name_for_rarity(this->rarity);
|
||||
string target_mode_str = name_for_target_mode(this->target_mode);
|
||||
string range_str = string_for_range(this->range);
|
||||
string assist_turns_str = string_for_assist_turns(this->assist_turns);
|
||||
string hp_str = this->hp.str();
|
||||
string ap_str = this->ap.str();
|
||||
string tp_str = this->tp.str();
|
||||
string mv_str = this->mv.str();
|
||||
string left_str = string_for_colors(this->left_colors);
|
||||
string right_str = string_for_colors(this->right_colors);
|
||||
string top_str = string_for_colors(this->top_colors);
|
||||
string effects_str;
|
||||
for (size_t x = 0; x < 3; x++) {
|
||||
if (this->effects[x].is_empty()) {
|
||||
continue;
|
||||
}
|
||||
if (!effects_str.empty()) {
|
||||
effects_str += ", ";
|
||||
}
|
||||
effects_str += this->effects[x].str();
|
||||
}
|
||||
return string_printf(
|
||||
"[Card: %04" PRIX32 " name=%s type=%s-%02hhX rare=%s cost=%hhX+%hhX "
|
||||
"target=%s range=%s assist_turns=%s cannot_move=%s cannot_attack=%s "
|
||||
"hidden=%s hp=%s ap=%s tp=%s mv=%s left=%s right=%s top=%s a2=%04hX "
|
||||
"a3=%04hX assist_effect=[%hu, %hu] drop_rates=[%hu, %hu] effects=[%s]]",
|
||||
this->card_id.load(),
|
||||
this->name.data(),
|
||||
type_str.c_str(),
|
||||
this->subtype,
|
||||
rarity_str.c_str(),
|
||||
this->self_cost,
|
||||
this->ally_cost,
|
||||
target_mode_str.c_str(),
|
||||
range_str.c_str(),
|
||||
assist_turns_str.c_str(),
|
||||
this->cannot_move ? "true" : "false",
|
||||
this->cannot_attack ? "true" : "false",
|
||||
this->hide_in_deck_edit ? "true" : "false",
|
||||
hp_str.c_str(),
|
||||
ap_str.c_str(),
|
||||
tp_str.c_str(),
|
||||
mv_str.c_str(),
|
||||
left_str.c_str(),
|
||||
right_str.c_str(),
|
||||
top_str.c_str(),
|
||||
this->unknown_a2.load(),
|
||||
this->unknown_a3.load(),
|
||||
this->assist_effect[0].load(),
|
||||
this->assist_effect[1].load(),
|
||||
this->drop_rates[0].load(),
|
||||
this->drop_rates[1].load(),
|
||||
effects_str.c_str());
|
||||
}
|
||||
|
||||
|
||||
|
||||
Ep3DataIndex::Ep3DataIndex(const string& directory, bool debug)
|
||||
: debug(debug) {
|
||||
|
||||
unordered_map<uint32_t, vector<string>> card_tags;
|
||||
unordered_map<uint32_t, string> card_text;
|
||||
if (this->debug) {
|
||||
try {
|
||||
string data = prs_decompress(load_file(directory + "/cardtext.mnr"));
|
||||
StringReader r(data);
|
||||
|
||||
while (!r.eof()) {
|
||||
uint32_t card_id = stoul(r.get_cstr());
|
||||
|
||||
// Read all pages for this card
|
||||
string text;
|
||||
string first_page;
|
||||
for (;;) {
|
||||
string line = r.get_cstr();
|
||||
if (line.empty()) {
|
||||
break;
|
||||
}
|
||||
if (first_page.empty()) {
|
||||
first_page = line;
|
||||
}
|
||||
text += '\n';
|
||||
text += line;
|
||||
}
|
||||
|
||||
// In orig_text, turn all \t into $ (following newserv conventions)
|
||||
string orig_text = text;
|
||||
for (char& ch : orig_text) {
|
||||
if (ch == '\t') {
|
||||
ch = '$';
|
||||
}
|
||||
}
|
||||
|
||||
// Preprocess first page: first, delete all color markers
|
||||
size_t offset = first_page.find("\tC");
|
||||
while (offset != string::npos) {
|
||||
first_page = first_page.substr(0, offset) + first_page.substr(offset + 3);
|
||||
offset = first_page.find("\tC");
|
||||
}
|
||||
// Preprocess first page: delete all lines that don't start with \t
|
||||
offset = first_page.find('\t');
|
||||
if (offset == string::npos) {
|
||||
first_page.clear();
|
||||
} else {
|
||||
first_page = first_page.substr(offset);
|
||||
}
|
||||
// Preprocess first page: merge lines that don't begin with \t
|
||||
for (offset = 0; offset < first_page.size(); offset++) {
|
||||
if (first_page[offset] == '\n' && first_page[offset + 1] != '\t') {
|
||||
first_page = first_page.substr(0, offset) + first_page.substr(offset + 1);
|
||||
offset--;
|
||||
}
|
||||
}
|
||||
|
||||
// Split first page into tags, and collapse whitespace in the tag names
|
||||
vector<string> tags;
|
||||
auto lines = split(first_page, '\n');
|
||||
for (const auto& line : lines) {
|
||||
string tag;
|
||||
if (line[0] == '\t' && line[1] == 'D') {
|
||||
tag = "D: " + line.substr(2);
|
||||
} else if (line[0] == '\t' && line[1] == 'S') {
|
||||
tag = "S: " + line.substr(2);
|
||||
}
|
||||
if (!tag.empty()) {
|
||||
for (size_t offset = tag.find(" "); offset != string::npos; offset = tag.find(" ")) {
|
||||
tag = tag.substr(0, offset) + tag.substr(offset + 1);
|
||||
}
|
||||
tags.emplace_back(move(tag));
|
||||
}
|
||||
}
|
||||
|
||||
if (!card_text.emplace(card_id, move(orig_text)).second) {
|
||||
throw runtime_error("duplicate card text id");
|
||||
}
|
||||
if (!card_tags.emplace(card_id, move(tags)).second) {
|
||||
throw logic_error("duplicate card tags id");
|
||||
}
|
||||
|
||||
r.go((r.where() + 0x3FF) & (~0x3FF));
|
||||
}
|
||||
|
||||
} catch (const exception& e) {
|
||||
static_game_data_log.warning("Failed to load card text: %s", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
this->compressed_card_definitions = load_file(directory + "/cardupdate.mnr");
|
||||
string data = prs_decompress(this->compressed_card_definitions);
|
||||
// There's a footer after the card definitions, but we ignore it
|
||||
if (data.size() % sizeof(Ep3CardDefinition) != sizeof(Ep3CardDefinitionsFooter)) {
|
||||
throw runtime_error(string_printf(
|
||||
"decompressed card update file size %zX is not aligned with card definition size %zX (%zX extra bytes)",
|
||||
data.size(), sizeof(Ep3CardDefinition), data.size() % sizeof(Ep3CardDefinition)));
|
||||
}
|
||||
const auto* def = reinterpret_cast<const Ep3CardDefinition*>(data.data());
|
||||
size_t max_cards = data.size() / sizeof(Ep3CardDefinition);
|
||||
for (size_t x = 0; x < max_cards; x++) {
|
||||
// The last card entry has the build date and some other metadata (and
|
||||
// isn't a real card, obviously), so skip it. Seems like the card ID is
|
||||
// always a large number that won't fit in a uint16_t, so we use that to
|
||||
// determine if the entry is a real card or not.
|
||||
if (def[x].card_id & 0xFFFF0000) {
|
||||
continue;
|
||||
}
|
||||
shared_ptr<CardEntry> entry(new CardEntry({def[x], {}, {}}));
|
||||
if (!this->card_definitions.emplace(entry->def.card_id, entry).second) {
|
||||
throw runtime_error(string_printf(
|
||||
"duplicate card id: %08" PRIX32, entry->def.card_id.load()));
|
||||
}
|
||||
|
||||
entry->def.hp.decode_code();
|
||||
entry->def.ap.decode_code();
|
||||
entry->def.tp.decode_code();
|
||||
entry->def.mv.decode_code();
|
||||
entry->def.decode_range();
|
||||
|
||||
if (this->debug) {
|
||||
try {
|
||||
entry->text = move(card_text.at(def[x].card_id));
|
||||
} catch (const out_of_range&) { }
|
||||
try {
|
||||
entry->debug_tags = move(card_tags.at(def[x].card_id));
|
||||
} catch (const out_of_range&) { }
|
||||
}
|
||||
}
|
||||
|
||||
static_game_data_log.info("Indexed %zu Episode 3 card definitions", this->card_definitions.size());
|
||||
} catch (const exception& e) {
|
||||
static_game_data_log.warning("Failed to load Episode 3 card update: %s", e.what());
|
||||
}
|
||||
|
||||
for (const auto& filename : list_directory(directory)) {
|
||||
try {
|
||||
shared_ptr<MapEntry> entry;
|
||||
|
||||
if (ends_with(filename, ".mnmd")) {
|
||||
entry.reset(new MapEntry(load_object_file<Ep3Map>(directory + "/" + filename)));
|
||||
} else if (ends_with(filename, ".mnm")) {
|
||||
entry.reset(new MapEntry(load_file(directory + "/" + filename)));
|
||||
}
|
||||
|
||||
if (entry.get()) {
|
||||
if (!this->maps.emplace(entry->map.map_number, entry).second) {
|
||||
throw runtime_error("duplicate map number");
|
||||
}
|
||||
string name = entry->map.name;
|
||||
static_game_data_log.info("Indexed Episode 3 map %s (%08" PRIX32 "; %s)",
|
||||
filename.c_str(), entry->map.map_number.load(), name.c_str());
|
||||
}
|
||||
|
||||
} catch (const exception& e) {
|
||||
static_game_data_log.warning("Failed to index Episode 3 map %s: %s",
|
||||
filename.c_str(), e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ep3DataIndex::MapEntry::MapEntry(const Ep3Map& map) : map(map) { }
|
||||
|
||||
Ep3DataIndex::MapEntry::MapEntry(const string& compressed)
|
||||
: compressed_data(compressed) {
|
||||
string decompressed = prs_decompress(this->compressed_data);
|
||||
if (decompressed.size() != sizeof(Ep3Map)) {
|
||||
throw runtime_error(string_printf(
|
||||
"decompressed data size is incorrect (expected %zu bytes, read %zu bytes)",
|
||||
sizeof(Ep3Map), decompressed.size()));
|
||||
}
|
||||
this->map = *reinterpret_cast<const Ep3Map*>(decompressed.data());
|
||||
}
|
||||
|
||||
string Ep3DataIndex::MapEntry::compressed() const {
|
||||
if (this->compressed_data.empty()) {
|
||||
this->compressed_data = prs_compress(&this->map, sizeof(this->map));
|
||||
}
|
||||
return this->compressed_data;
|
||||
}
|
||||
|
||||
const string& Ep3DataIndex::get_compressed_card_definitions() const {
|
||||
if (this->compressed_card_definitions.empty()) {
|
||||
throw runtime_error("card definitions are not available");
|
||||
}
|
||||
return this->compressed_card_definitions;
|
||||
}
|
||||
|
||||
shared_ptr<const Ep3DataIndex::CardEntry> Ep3DataIndex::get_card_definition(
|
||||
uint32_t id) const {
|
||||
return this->card_definitions.at(id);
|
||||
}
|
||||
|
||||
std::set<uint32_t> Ep3DataIndex::all_card_ids() const {
|
||||
std::set<uint32_t> ret;
|
||||
for (const auto& it : this->card_definitions) {
|
||||
ret.emplace(it.first);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
const string& Ep3DataIndex::get_compressed_map_list() const {
|
||||
if (this->compressed_map_list.empty()) {
|
||||
// TODO: Write a version of prs_compress that takes iovecs (or something
|
||||
// similar) so we can eliminate all this string copying here.
|
||||
StringWriter entries_w;
|
||||
StringWriter strings_w;
|
||||
|
||||
for (const auto& map_it : this->maps) {
|
||||
Ep3MapList::Entry e;
|
||||
const auto& map = map_it.second->map;
|
||||
e.map_x = map.map_x;
|
||||
e.map_y = map.map_y;
|
||||
e.scene_data2 = map.scene_data2;
|
||||
e.map_number = map.map_number.load();
|
||||
e.width = map.width;
|
||||
e.height = map.height;
|
||||
e.map_tiles = map.map_tiles;
|
||||
e.modification_tiles = map.modification_tiles;
|
||||
|
||||
e.name_offset = strings_w.size();
|
||||
strings_w.write(map.name.data(), map.name.len());
|
||||
strings_w.put_u8(0);
|
||||
e.location_name_offset = strings_w.size();
|
||||
strings_w.write(map.location_name.data(), map.location_name.len());
|
||||
strings_w.put_u8(0);
|
||||
e.quest_name_offset = strings_w.size();
|
||||
strings_w.write(map.quest_name.data(), map.quest_name.len());
|
||||
strings_w.put_u8(0);
|
||||
e.description_offset = strings_w.size();
|
||||
strings_w.write(map.description.data(), map.description.len());
|
||||
strings_w.put_u8(0);
|
||||
|
||||
e.unknown_a2 = 0xFF000000;
|
||||
|
||||
entries_w.put(e);
|
||||
}
|
||||
|
||||
Ep3MapList header;
|
||||
header.num_maps = this->maps.size();
|
||||
header.unknown_a1 = 0;
|
||||
header.strings_offset = entries_w.size();
|
||||
header.total_size = sizeof(Ep3MapList) + entries_w.size() + strings_w.size();
|
||||
|
||||
PRSCompressor prs;
|
||||
prs.add(&header, sizeof(header));
|
||||
prs.add(entries_w.str());
|
||||
prs.add(strings_w.str());
|
||||
|
||||
StringWriter compressed_w;
|
||||
compressed_w.put_u32b(prs.input_size());
|
||||
compressed_w.write(prs.close());
|
||||
this->compressed_map_list = move(compressed_w.str());
|
||||
static_game_data_log.info("Generated Episode 3 compressed map list (%zu -> %zu bytes)",
|
||||
this->compressed_map_list.size(), this->compressed_map_list.size());
|
||||
}
|
||||
return this->compressed_map_list;
|
||||
}
|
||||
|
||||
shared_ptr<const Ep3DataIndex::MapEntry> Ep3DataIndex::get_map(uint32_t id) const {
|
||||
return this->maps.at(id);
|
||||
}
|
||||
|
||||
std::set<uint32_t> Ep3DataIndex::all_map_ids() const {
|
||||
std::set<uint32_t> ret;
|
||||
for (const auto& it : this->maps) {
|
||||
ret.emplace(it.first);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
-374
@@ -1,374 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <phosg/Encoding.hh>
|
||||
|
||||
#include "Text.hh"
|
||||
|
||||
|
||||
|
||||
// Note: Much of the structures and enums here are based on the card list file,
|
||||
// and comparing the card text with the data in the file. Some inferences may be
|
||||
// incorrect here, since Episode 3's card text is wrong in various places.
|
||||
|
||||
struct Ep3CardDefinition {
|
||||
enum Rarity : uint8_t {
|
||||
N1 = 0x01,
|
||||
R1 = 0x02,
|
||||
S = 0x03,
|
||||
E = 0x04,
|
||||
N2 = 0x05,
|
||||
N3 = 0x06,
|
||||
N4 = 0x07,
|
||||
R2 = 0x08,
|
||||
R3 = 0x09,
|
||||
R4 = 0x0A,
|
||||
SS = 0x0B,
|
||||
D1 = 0x0C,
|
||||
D2 = 0x0D,
|
||||
INVIS = 0x0E,
|
||||
};
|
||||
|
||||
enum Type : uint8_t {
|
||||
SC_HUNTERS = 0x00, // No subtypes
|
||||
SC_ARKZ = 0x01, // No subtypes
|
||||
ITEM = 0x02, // Subtype 01 = sword, 02 = gun, 03 = cane. TODO: there are many more subtypes than those 3
|
||||
CREATURE = 0x03, // No subtypes (TODO: Where are attributes stored then?)
|
||||
ACTION = 0x04, // TODO: What do the subtypes mean? Are they actually flags instead?
|
||||
ASSIST = 0x05, // No subtypes
|
||||
};
|
||||
|
||||
struct Stat {
|
||||
enum Type : uint8_t {
|
||||
BLANK = 0,
|
||||
STAT = 1,
|
||||
PLUS_STAT = 2,
|
||||
MINUS_STAT = 3,
|
||||
EQUALS_STAT = 4,
|
||||
UNKNOWN = 5,
|
||||
PLUS_UNKNOWN = 6,
|
||||
MINUS_UNKNOWN = 7,
|
||||
EQUALS_UNKNOWN = 8,
|
||||
};
|
||||
be_uint16_t code;
|
||||
Type type;
|
||||
int8_t stat;
|
||||
|
||||
void decode_code();
|
||||
std::string str() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct Effect {
|
||||
uint8_t effect_num;
|
||||
uint8_t command; // See name_for_effect_command in Episode3.cc for details
|
||||
ptext<char, 0x0F> expr; // May be blank if the command doesn't use it
|
||||
uint8_t when; // See description_for_when in Episode3.cc for details
|
||||
ptext<char, 4> arg1;
|
||||
ptext<char, 4> arg2;
|
||||
ptext<char, 4> arg3;
|
||||
parray<uint8_t, 2> unknown_a3;
|
||||
|
||||
bool is_empty() const;
|
||||
static std::string str_for_arg(const std::string& arg);
|
||||
std::string str() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
be_uint32_t card_id;
|
||||
parray<uint8_t, 0x40> jp_name;
|
||||
int8_t type; // Type enum. If <0, then this is the end of the card list
|
||||
uint8_t self_cost; // ATK dice points required
|
||||
uint8_t ally_cost; // ATK points from allies required; PBs use this
|
||||
uint8_t unused_a0; // Always 0
|
||||
Stat hp;
|
||||
Stat ap;
|
||||
Stat tp;
|
||||
Stat mv;
|
||||
parray<uint8_t, 8> left_colors;
|
||||
parray<uint8_t, 8> right_colors;
|
||||
parray<uint8_t, 8> top_colors;
|
||||
parray<be_uint32_t, 6> range;
|
||||
be_uint32_t unused_a1; // Always 0
|
||||
// Target modes:
|
||||
// 00 = no targeting (used for defense cards, mags, shields, etc.)
|
||||
// 01 = single enemy
|
||||
// 02 = multiple enemies (with range)
|
||||
// 03 = self (assist)
|
||||
// 04 = team (assist)
|
||||
// 05 = everyone (assist)
|
||||
// 06 = multiple allies (with range); only used by Shifta
|
||||
// 07 = all allies including yourself; see Anti, Resta, Leilla
|
||||
// 08 = all (attack); see e.g. Last Judgment, Earthquake
|
||||
// 09 = your own FCs but not SCs; see Traitor
|
||||
uint8_t target_mode;
|
||||
uint8_t assist_turns; // 90 (dec) = once, 99 (dec) = forever
|
||||
uint8_t cannot_move; // 0 for SC and creature cards; 1 for everything else
|
||||
uint8_t cannot_attack; // 1 for shields, mags, defense actions, and assist cards
|
||||
uint8_t unused_a2; // Always 0
|
||||
uint8_t hide_in_deck_edit; // 0 = player can use this card (appears in deck edit)
|
||||
uint8_t subtype; // e.g. gun, sword, etc. (used for checking if SCs can use it)
|
||||
uint8_t rarity; // Rarity enum
|
||||
be_uint16_t unknown_a2;
|
||||
be_uint16_t unknown_a3;
|
||||
// These two fields seem to always contain the same value, and are always 0
|
||||
// for non-assist cards and nonzero for assists. Each assist card has a unique
|
||||
// value here and no effects, which makes it look like this is how assist
|
||||
// effects are implemented. There seems to be some 1k-modulation going on here
|
||||
// too; most cards are in the range 101-174 but a few have e.g. 1150, 2141. A
|
||||
// few pairs of cards have the same effect, which makes it look like some
|
||||
// other fields are also involved in determining their effects (see e.g. Skip
|
||||
// Draw / Skip Move, Dice Fever / Dice Fever +, Reverse Card / Rich +).
|
||||
parray<be_uint16_t, 2> assist_effect;
|
||||
// Drop rates are decimal-encoded with the following fields:
|
||||
// - rate % 10 (that is, the lowest decimal place) specifies the required game
|
||||
// mode. 0 means any mode, 1 means offline only, 2 means 1P free-battle, 3
|
||||
// means 2P+ free battle, 4 means story mode.
|
||||
// - (rate / 10) % 100 (that is, the tens and hundreds decimal places) specify
|
||||
// something else, but it's not clear what exactly.
|
||||
// - rate / 1000 (the thousands decimal place) specifies the level class
|
||||
// required to get this drop.
|
||||
// - rate / 10000 (the ten-thousands decimal place) must be either 0, 1, or 2,
|
||||
// but it's not clear yet what each value means.
|
||||
// The drop rates are completely ignored if any of the following are true
|
||||
// (which means the card can never be found in a normal post-battle draw):
|
||||
// - type is SC_HUNTERS or SC_ARKZ
|
||||
// - unknown_a3 is 0x23 or 0x24
|
||||
// - rarity is E, D1, D2, or INVIS
|
||||
// - hide_in_deck_edit is 1 (specifically 1; other nonzero values here don't
|
||||
// prevent the card from appearing in post-battle draws)
|
||||
parray<be_uint16_t, 2> drop_rates;
|
||||
ptext<char, 0x14> name;
|
||||
ptext<char, 0x0B> jp_short_name;
|
||||
ptext<char, 0x08> short_name;
|
||||
Effect effects[3];
|
||||
uint8_t unused_a3;
|
||||
|
||||
void decode_range();
|
||||
std::string str() const;
|
||||
} __attribute__((packed)); // 0x128 bytes in total
|
||||
|
||||
struct Ep3CardDefinitionsFooter {
|
||||
be_uint32_t num_cards1;
|
||||
be_uint32_t unknown_a1;
|
||||
be_uint32_t num_cards2;
|
||||
be_uint32_t unknown_a2[11];
|
||||
be_uint32_t unknown_offset_a3;
|
||||
be_uint32_t unknown_a4[3];
|
||||
be_uint32_t footer_offset;
|
||||
be_uint32_t unknown_a5[3];
|
||||
} __attribute__((packed));
|
||||
|
||||
struct Ep3Deck {
|
||||
ptext<char, 0x10> name;
|
||||
be_uint32_t client_id; // 0-3
|
||||
// List of card IDs. The card count is the number of nonzero entries here
|
||||
// before a zero entry (or 50 if no entries are nonzero). The first card ID is
|
||||
// the SC card, which the game implicitly subtracts from the limit - so a
|
||||
// valid deck should actually have 31 cards in it.
|
||||
parray<le_uint16_t, 50> card_ids;
|
||||
be_uint32_t unknown_a1;
|
||||
// Last modification time
|
||||
le_uint16_t year;
|
||||
uint8_t month;
|
||||
uint8_t day;
|
||||
uint8_t hour;
|
||||
uint8_t minute;
|
||||
uint8_t second;
|
||||
uint8_t unknown_a2;
|
||||
} __attribute__((packed)); // 0x84 bytes in total
|
||||
|
||||
struct Ep3Config {
|
||||
// Offsets in comments in this struct are relative to start of 61/98 command
|
||||
/* 0728 */ parray<uint8_t, 0x1434> unknown_a1;
|
||||
/* 1B5C */ parray<Ep3Deck, 25> decks;
|
||||
/* 2840 */ uint64_t unknown_a2;
|
||||
/* 2848 */ be_uint32_t offline_clv_exp; // CLvOff = this / 100
|
||||
/* 284C */ be_uint32_t online_clv_exp; // CLvOn = this / 100
|
||||
/* 2850 */ parray<uint8_t, 0x14C> unknown_a3;
|
||||
/* 299C */ ptext<char, 0x10> name;
|
||||
// Other records are probably somewhere in here - e.g. win/loss, play time, etc.
|
||||
/* 29AC */ parray<uint8_t, 0xCC> unknown_a4;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct Ep3BattleRules {
|
||||
// When this structure is used in a map/quest definition, FF in any of these
|
||||
// fields means the user is allowed to override it. Any non-FF fields are
|
||||
// fixed for the map/quest and cannot be overridden.
|
||||
uint8_t overall_time_limit; // In increments of 5 minutes; 0 = unlimited
|
||||
uint8_t phase_time_limit; // In seconds; 0 = unlimited
|
||||
uint8_t allowed_cards; // 0 = any, 1 = N-rank only, 2 = N and R, 3 = N, R, and S
|
||||
uint8_t min_dice; // 0 = default (1)
|
||||
// 4
|
||||
uint8_t max_dice; // 0 = default (6)
|
||||
uint8_t disable_deck_shuffle; // 0 = shuffle on, 1 = off
|
||||
uint8_t disable_deck_loop; // 0 = loop on, 1 = off
|
||||
uint8_t char_hp;
|
||||
// 8
|
||||
uint8_t hp_type; // 0 = defeat player, 1 = defeat team, 2 = common hp
|
||||
uint8_t no_assist_cards; // 1 = assist cards disallowed
|
||||
uint8_t disable_dialogue; // 0 = dialogue on, 1 = dialogue off
|
||||
uint8_t dice_exchange_mode; // 0 = high attack, 1 = high defense, 2 = none
|
||||
// C
|
||||
uint8_t disable_dice_boost; // 0 = dice boost on, 1 = off
|
||||
parray<uint8_t, 3> unused;
|
||||
} __attribute__((packed));
|
||||
|
||||
|
||||
|
||||
struct Ep3MapList {
|
||||
be_uint32_t num_maps;
|
||||
be_uint32_t unknown_a1; // Always 0?
|
||||
be_uint32_t strings_offset; // From after total_size field (add 0x10 to this value)
|
||||
be_uint32_t total_size; // Including header, entries, and strings
|
||||
|
||||
struct Entry { // Should be 0x220 bytes in total
|
||||
be_uint16_t map_x;
|
||||
be_uint16_t map_y;
|
||||
be_uint16_t scene_data2;
|
||||
be_uint16_t map_number;
|
||||
// Text offsets are from the beginning of the strings block after all map
|
||||
// entries (that is, add strings_offset to them to get the string offset)
|
||||
be_uint32_t name_offset;
|
||||
be_uint32_t location_name_offset;
|
||||
be_uint32_t quest_name_offset;
|
||||
be_uint32_t description_offset;
|
||||
be_uint16_t width;
|
||||
be_uint16_t height;
|
||||
parray<uint8_t, 0x100> map_tiles;
|
||||
parray<uint8_t, 0x100> modification_tiles;
|
||||
be_uint32_t unknown_a2; // Seems to always be 0xFF000000
|
||||
} __attribute__((packed));
|
||||
|
||||
// Variable-length fields:
|
||||
// Entry entries[num_maps];
|
||||
// char strings[...EOF]; // Null-terminated strings, pointed to by offsets in Entry structs
|
||||
} __attribute__((packed));
|
||||
|
||||
struct Ep3CompressedMapHeader { // .mnm file format
|
||||
le_uint32_t map_number;
|
||||
le_uint32_t compressed_data_size;
|
||||
// Compressed data immediately follows (which decompresses to an Ep3Map)
|
||||
} __attribute__((packed));
|
||||
|
||||
struct Ep3Map { // .mnmd format; also the format of (decompressed) Ep3 quests
|
||||
/* 0000 */ be_uint32_t unknown_a1;
|
||||
/* 0004 */ be_uint32_t map_number;
|
||||
/* 0008 */ uint8_t width;
|
||||
/* 0009 */ uint8_t height;
|
||||
/* 000A */ uint8_t scene_data2; // TODO: What is this?
|
||||
// All alt_maps fields (including the floats) past num_alt_maps are filled in
|
||||
// with FF. For example, if num_alt_maps == 8, the last two fields in each
|
||||
// alt_maps array are filled with FF.
|
||||
/* 000B */ uint8_t num_alt_maps; // TODO: What are the alt maps for?
|
||||
// In the map_tiles array, the values are:
|
||||
// 00 = not a valid tile
|
||||
// 01 = valid tile unless punched out (later)
|
||||
// 02 = team A start (1v1)
|
||||
// 03, 04 = team A start (2v2)
|
||||
// 05 = ???
|
||||
// 06, 07 = team B start (2v2)
|
||||
// 08 = team B start (1v1)
|
||||
// Note that the game displays the map reversed vertically in the preview
|
||||
// window. For example, player 1 is on team A, which usually starts at the top
|
||||
// of the map as defined in this struct, or at the bottom as shown in the
|
||||
// preview window.
|
||||
/* 000C */ parray<uint8_t, 0x100> map_tiles;
|
||||
/* 010C */ parray<uint8_t, 0x0C> unknown_a2;
|
||||
/* 0118 */ parray<uint8_t, 0x100> alt_maps1[0x0A];
|
||||
/* 0B18 */ parray<uint8_t, 0x100> alt_maps2[0x0A];
|
||||
/* 1518 */ parray<be_float, 0x12> alt_maps_unknown_a3[0x0A];
|
||||
/* 17E8 */ parray<be_float, 0x12> alt_maps_unknown_a4[0x0A];
|
||||
/* 1AB8 */ parray<be_float, 0x6C> unknown_a5;
|
||||
// In the modification_tiles array, the values are:
|
||||
// 10 = blocked (as if the corresponding map_tiles value was 00)
|
||||
// 20 = blocked (maybe one of 10 or 20 are passable by Aerial characters though)
|
||||
// 30, 31 = teleporters (green, red)
|
||||
// 40-44 = ???? (used in 244, 2E4, 2F9)
|
||||
// 50 = appears as improperly-z-buffered teal cube in preview
|
||||
// TODO: There may be more values that are valid here.
|
||||
/* 1C68 */ parray<uint8_t, 0x100> modification_tiles;
|
||||
/* 1D68 */ parray<uint8_t, 0x74> unknown_a6;
|
||||
/* 1DDC */ Ep3BattleRules default_rules;
|
||||
/* 1DEC */ parray<uint8_t, 4> unknown_a7;
|
||||
/* 1DF0 */ ptext<char, 0x14> name;
|
||||
/* 1E04 */ ptext<char, 0x14> location_name;
|
||||
/* 1E18 */ ptext<char, 0x3C> quest_name; // == location_name if not a quest
|
||||
/* 1E54 */ ptext<char, 0x190> description;
|
||||
/* 1FE4 */ be_uint16_t map_x;
|
||||
/* 1FE6 */ be_uint16_t map_y;
|
||||
struct NPCDeck {
|
||||
ptext<char, 0x18> name;
|
||||
parray<be_uint16_t, 0x20> card_ids; // Last one appears to always be FFFF
|
||||
} __attribute__((packed));
|
||||
/* 1FE8 */ NPCDeck npc_decks[3]; // Unused if name[0] == 0
|
||||
struct NPCCharacter {
|
||||
parray<be_uint16_t, 2> unknown_a1;
|
||||
parray<uint8_t, 4> unknown_a2;
|
||||
ptext<char, 0x10> name;
|
||||
parray<be_uint16_t, 0x7E> unknown_a3;
|
||||
} __attribute__((packed));
|
||||
/* 20F0 */ NPCCharacter npc_chars[3]; // Unused if name[0] == 0
|
||||
/* 242C */ parray<uint8_t, 0x14> unknown_a8; // Always FF?
|
||||
/* 2440 */ ptext<char, 0x190> before_message;
|
||||
/* 25D0 */ ptext<char, 0x190> after_message;
|
||||
/* 2760 */ ptext<char, 0x190> dispatch_message; // Usually "You can only dispatch <character>" or blank
|
||||
struct DialogueSet {
|
||||
be_uint16_t unknown_a1;
|
||||
be_uint16_t unknown_a2; // Always 0x0064 if valid, 0xFFFF if unused?
|
||||
ptext<char, 0x40> strings[4];
|
||||
} __attribute__((packed)); // Total size: 0x104 bytes
|
||||
/* 28F0 */ DialogueSet dialogue_sets[3][0x10]; // Up to 0x10 per valid NPC
|
||||
/* 59B0 */ be_uint16_t reward_card_id; // TODO: This could be an array. The only examples I've seen have only one here
|
||||
/* 59B2 */ parray<be_uint16_t, 0x33> unknown_a9;
|
||||
/* 5A18 */
|
||||
} __attribute__((packed));
|
||||
|
||||
class Ep3DataIndex {
|
||||
public:
|
||||
explicit Ep3DataIndex(const std::string& directory, bool debug = false);
|
||||
|
||||
struct CardEntry {
|
||||
Ep3CardDefinition def;
|
||||
std::string text;
|
||||
std::vector<std::string> debug_tags; // Empty unless debug == true
|
||||
};
|
||||
|
||||
class MapEntry {
|
||||
public:
|
||||
Ep3Map map;
|
||||
|
||||
MapEntry(const Ep3Map& map);
|
||||
MapEntry(const std::string& compressed_data);
|
||||
|
||||
std::string compressed() const;
|
||||
|
||||
private:
|
||||
mutable std::string compressed_data;
|
||||
};
|
||||
|
||||
const std::string& get_compressed_card_definitions() const;
|
||||
std::shared_ptr<const CardEntry> get_card_definition(uint32_t id) const;
|
||||
std::set<uint32_t> all_card_ids() const;
|
||||
|
||||
const std::string& get_compressed_map_list() const;
|
||||
std::shared_ptr<const MapEntry> get_map(uint32_t id) const;
|
||||
std::set<uint32_t> all_map_ids() const;
|
||||
|
||||
private:
|
||||
bool debug;
|
||||
|
||||
std::string compressed_card_definitions;
|
||||
std::unordered_map<uint32_t, std::shared_ptr<CardEntry>> card_definitions;
|
||||
|
||||
// The compressed map list is generated on demand from the maps map below.
|
||||
// It's marked mutable because the logical consistency of the Ep3DataIndex
|
||||
// object is not violated from the caller's perspective even if we don't
|
||||
// generate the compressed map list at load time.
|
||||
mutable std::string compressed_map_list;
|
||||
std::map<uint32_t, std::shared_ptr<MapEntry>> maps;
|
||||
};
|
||||
@@ -0,0 +1,286 @@
|
||||
#include "AssistServer.hh"
|
||||
|
||||
#include "Server.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace Episode3 {
|
||||
|
||||
|
||||
|
||||
// Note: This order matches the order that the cards are defined in the original
|
||||
// code. This is relevant for consistency of results when choosing a random card
|
||||
// (for God Whim).
|
||||
const vector<uint16_t> ALL_ASSIST_CARD_IDS = {
|
||||
0x0018, 0x0019, 0x001A, 0x00F5, 0x00F6, 0x00F7, 0x00F8, 0x00F9, 0x00FA,
|
||||
0x00FB, 0x00FC, 0x00FD, 0x00FE, 0x00FF, 0x0100, 0x0101, 0x0102, 0x0103,
|
||||
0x0104, 0x0105, 0x0106, 0x0107, 0x0108, 0x0109, 0x010A, 0x010B, 0x010C,
|
||||
0x010D, 0x010E, 0x010F, 0x0121, 0x0125, 0x0126, 0x0127, 0x0128, 0x0129,
|
||||
0x012A, 0x012B, 0x012C, 0x012D, 0x012E, 0x012F, 0x0130, 0x0131, 0x0132,
|
||||
0x0133, 0x0134, 0x0135, 0x0136, 0x0137, 0x0138, 0x0139, 0x013A, 0x013B,
|
||||
0x013C, 0x013D, 0x013E, 0x013F, 0x0140, 0x0141, 0x0142, 0x0143, 0x0144,
|
||||
0x0145, 0x0146, 0x0148, 0x014A, 0x014B, 0x014C, 0x014D, 0x014E, 0x023F,
|
||||
0x0240, 0x0241, 0x0242};
|
||||
|
||||
AssistEffect assist_effect_number_for_card_id(uint16_t card_id) {
|
||||
static const unordered_map<uint16_t, AssistEffect> card_id_to_effect({
|
||||
{0x00F5, /* 0x0001 */ AssistEffect::DICE_HALF},
|
||||
{0x00F6, /* 0x0002 */ AssistEffect::DICE_PLUS_1},
|
||||
{0x00F7, /* 0x0003 */ AssistEffect::DICE_FEVER},
|
||||
{0x00F8, /* 0x0004 */ AssistEffect::CARD_RETURN},
|
||||
{0x00F9, /* 0x0005 */ AssistEffect::LAND_PRICE},
|
||||
{0x00FA, /* 0x0006 */ AssistEffect::POWERLESS_RAIN},
|
||||
{0x00FB, /* 0x0007 */ AssistEffect::BRAVE_WIND},
|
||||
{0x00FC, /* 0x0008 */ AssistEffect::SILENT_COLOSSEUM},
|
||||
{0x00FD, /* 0x0009 */ AssistEffect::RESISTANCE},
|
||||
{0x00FE, /* 0x000A */ AssistEffect::INDEPENDENT},
|
||||
{0x00FF, /* 0x000B */ AssistEffect::ASSISTLESS},
|
||||
{0x0100, /* 0x000C */ AssistEffect::ATK_DICE_2},
|
||||
{0x0101, /* 0x000D */ AssistEffect::DEFLATION},
|
||||
{0x0102, /* 0x000E */ AssistEffect::INFLATION},
|
||||
{0x0103, /* 0x000F */ AssistEffect::EXCHANGE},
|
||||
{0x0104, /* 0x0010 */ AssistEffect::INFLUENCE},
|
||||
{0x0105, /* 0x0011 */ AssistEffect::SKIP_SET},
|
||||
{0x0106, /* 0x0012 */ AssistEffect::SKIP_MOVE},
|
||||
{0x0121, /* 0x0013 */ AssistEffect::SKIP_ACT},
|
||||
{0x0137, /* 0x0014 */ AssistEffect::SKIP_DRAW},
|
||||
{0x0107, /* 0x0015 */ AssistEffect::FLY},
|
||||
{0x0108, /* 0x0016 */ AssistEffect::NECROMANCER},
|
||||
{0x0109, /* 0x0017 */ AssistEffect::PERMISSION},
|
||||
{0x010A, /* 0x0018 */ AssistEffect::SHUFFLE_ALL},
|
||||
{0x010B, /* 0x0019 */ AssistEffect::LEGACY},
|
||||
{0x010C, /* 0x001A */ AssistEffect::ASSIST_REVERSE},
|
||||
{0x010D, /* 0x001B */ AssistEffect::STAMINA},
|
||||
{0x010E, /* 0x001C */ AssistEffect::AP_ABSORPTION},
|
||||
{0x010F, /* 0x001D */ AssistEffect::HEAVY_FOG},
|
||||
{0x0125, /* 0x001E */ AssistEffect::TRASH_1},
|
||||
{0x0126, /* 0x001F */ AssistEffect::EMPTY_HAND},
|
||||
{0x0127, /* 0x0020 */ AssistEffect::HITMAN},
|
||||
{0x0128, /* 0x0021 */ AssistEffect::ASSIST_TRASH},
|
||||
{0x0129, /* 0x0022 */ AssistEffect::SHUFFLE_GROUP},
|
||||
{0x012A, /* 0x0023 */ AssistEffect::ASSIST_VANISH},
|
||||
{0x012B, /* 0x0024 */ AssistEffect::CHARITY},
|
||||
{0x012C, /* 0x0025 */ AssistEffect::INHERITANCE},
|
||||
{0x012D, /* 0x0026 */ AssistEffect::FIX},
|
||||
{0x012E, /* 0x0027 */ AssistEffect::MUSCULAR},
|
||||
{0x012F, /* 0x0028 */ AssistEffect::CHANGE_BODY},
|
||||
{0x0130, /* 0x0029 */ AssistEffect::GOD_WHIM},
|
||||
{0x0131, /* 0x002A */ AssistEffect::GOLD_RUSH},
|
||||
{0x0132, /* 0x002B */ AssistEffect::ASSIST_RETURN},
|
||||
{0x0133, /* 0x002C */ AssistEffect::REQUIEM},
|
||||
{0x0134, /* 0x002D */ AssistEffect::RANSOM},
|
||||
{0x0135, /* 0x002E */ AssistEffect::SIMPLE},
|
||||
{0x0136, /* 0x002F */ AssistEffect::SLOW_TIME},
|
||||
{0x023F, /* 0x0030 */ AssistEffect::QUICK_TIME},
|
||||
{0x0138, /* 0x0031 */ AssistEffect::TERRITORY},
|
||||
{0x0139, /* 0x0032 */ AssistEffect::OLD_TYPE},
|
||||
{0x013A, /* 0x0033 */ AssistEffect::FLATLAND},
|
||||
{0x013B, /* 0x0034 */ AssistEffect::IMMORTALITY},
|
||||
{0x013C, /* 0x0035 */ AssistEffect::SNAIL_PACE},
|
||||
{0x013D, /* 0x0036 */ AssistEffect::TECH_FIELD},
|
||||
{0x013E, /* 0x0037 */ AssistEffect::FOREST_RAIN},
|
||||
{0x013F, /* 0x0038 */ AssistEffect::CAVE_WIND},
|
||||
{0x0140, /* 0x0039 */ AssistEffect::MINE_BRIGHTNESS},
|
||||
{0x0141, /* 0x003A */ AssistEffect::RUIN_DARKNESS},
|
||||
{0x0142, /* 0x003B */ AssistEffect::SABER_DANCE},
|
||||
{0x0143, /* 0x003C */ AssistEffect::BULLET_STORM},
|
||||
{0x0144, /* 0x003D */ AssistEffect::CANE_PALACE},
|
||||
{0x0145, /* 0x003E */ AssistEffect::GIANT_GARDEN},
|
||||
{0x0146, /* 0x003F */ AssistEffect::MARCH_OF_THE_MEEK},
|
||||
{0x0148, /* 0x0040 */ AssistEffect::SUPPORT},
|
||||
{0x014A, /* 0x0041 */ AssistEffect::RICH},
|
||||
{0x014B, /* 0x0042 */ AssistEffect::REVERSE_CARD},
|
||||
{0x014C, /* 0x0043 */ AssistEffect::VENGEANCE},
|
||||
{0x014D, /* 0x0044 */ AssistEffect::SQUEEZE},
|
||||
{0x014E, /* 0x0045 */ AssistEffect::HOMESICK},
|
||||
{0x0240, /* 0x0046 */ AssistEffect::BOMB},
|
||||
{0x0241, /* 0x0047 */ AssistEffect::SKIP_TURN},
|
||||
{0x0242, /* 0x0048 */ AssistEffect::BATTLE_ROYALE},
|
||||
{0x0018, /* 0x0049 */ AssistEffect::DICE_FEVER_PLUS},
|
||||
{0x0019, /* 0x004A */ AssistEffect::RICH_PLUS},
|
||||
{0x001A, /* 0x004B */ AssistEffect::CHARITY_PLUS},
|
||||
});
|
||||
try {
|
||||
return card_id_to_effect.at(card_id);
|
||||
} catch (const out_of_range&) {
|
||||
return AssistEffect::NONE;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
AssistServer::AssistServer(shared_ptr<Server> server)
|
||||
: w_server(server),
|
||||
assist_effects(AssistEffect::NONE),
|
||||
num_assist_cards_set(0),
|
||||
client_ids_with_assists(0xFF),
|
||||
active_assist_effects(AssistEffect::NONE),
|
||||
num_active_assists(0) { }
|
||||
|
||||
shared_ptr<Server> AssistServer::server() {
|
||||
auto s = this->w_server.lock();
|
||||
if (!s) {
|
||||
throw runtime_error("server is deleted");
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
shared_ptr<const Server> AssistServer::server() const {
|
||||
auto s = this->w_server.lock();
|
||||
if (!s) {
|
||||
throw runtime_error("server is deleted");
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
uint16_t AssistServer::card_id_for_card_ref(uint16_t card_ref) const {
|
||||
return this->server()->card_id_for_card_ref(card_ref);
|
||||
}
|
||||
|
||||
shared_ptr<const DataIndex::CardEntry> AssistServer::definition_for_card_id(
|
||||
uint16_t card_id) const {
|
||||
return this->server()->definition_for_card_id(card_id);
|
||||
}
|
||||
|
||||
uint32_t AssistServer::compute_num_assist_effects_for_client(uint16_t client_id) {
|
||||
this->populate_effects();
|
||||
this->num_assist_cards_set = 0;
|
||||
if (this->should_block_assist_effects_for_client(client_id)) {
|
||||
this->num_active_assists = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
auto ce = this->assist_card_defs[z];
|
||||
auto hes = this->hand_and_equip_states[z];
|
||||
if (ce && (!hes || (hes->assist_delay_turns < 1))) {
|
||||
bool affected = false;
|
||||
if (ce->def.target_mode == TargetMode::TEAM) {
|
||||
auto this_deck_entry = this->deck_entries[client_id];
|
||||
auto other_deck_entry = this->deck_entries[z];
|
||||
if (this_deck_entry && other_deck_entry &&
|
||||
(this_deck_entry->team_id == other_deck_entry->team_id)) {
|
||||
affected = true;
|
||||
}
|
||||
} else if ((ce->def.target_mode == TargetMode::SELF) && (z == client_id)) {
|
||||
affected = true;
|
||||
} else if (ce->def.target_mode == TargetMode::EVERYONE) {
|
||||
affected = true;
|
||||
}
|
||||
if (affected) {
|
||||
this->client_ids_with_assists[this->num_assist_cards_set++] = z;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this->recompute_effects();
|
||||
return this->num_assist_cards_set;
|
||||
}
|
||||
|
||||
uint32_t AssistServer::compute_num_assist_effects_for_team(uint32_t team_id) {
|
||||
this->num_assist_cards_set = 0;
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
auto ce = this->assist_card_defs[z];
|
||||
auto hes = this->hand_and_equip_states[z];
|
||||
if (ce && (!hes || (hes->assist_delay_turns < 1))) {
|
||||
bool affected = false;
|
||||
if (ce->def.target_mode == TargetMode::TEAM) {
|
||||
if (this->deck_entries[z] && (this->deck_entries[z]->team_id == team_id)) {
|
||||
affected = true;
|
||||
}
|
||||
} else if (ce->def.target_mode == TargetMode::EVERYONE) {
|
||||
affected = true;
|
||||
}
|
||||
if (affected) {
|
||||
this->client_ids_with_assists[this->num_assist_cards_set++] = z;
|
||||
}
|
||||
}
|
||||
}
|
||||
this->recompute_effects();
|
||||
return this->num_assist_cards_set;
|
||||
}
|
||||
|
||||
bool AssistServer::should_block_assist_effects_for_client(uint16_t client_id) const {
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
auto eff = this->assist_effects[z];
|
||||
auto ce = this->assist_card_defs[z];
|
||||
if (((eff == AssistEffect::RESISTANCE) || (eff == AssistEffect::INDEPENDENT)) && ce) {
|
||||
if (ce->def.target_mode == TargetMode::TEAM) {
|
||||
if (this->deck_entries[client_id] && this->deck_entries[z] &&
|
||||
(this->deck_entries[client_id]->team_id == this->deck_entries[z]->team_id)) {
|
||||
return true;
|
||||
}
|
||||
} else if ((ce->def.target_mode == TargetMode::SELF) && (client_id == z)) {
|
||||
return true;
|
||||
} else if (ce->def.target_mode == TargetMode::EVERYONE) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
AssistEffect AssistServer::get_active_assist_by_index(size_t index) const {
|
||||
if (index < this->num_active_assists) {
|
||||
return this->active_assist_effects[index];
|
||||
}
|
||||
return AssistEffect::NONE;
|
||||
}
|
||||
|
||||
void AssistServer::populate_effects() {
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
this->assist_card_defs[z] = nullptr;
|
||||
this->assist_effects[z] = AssistEffect::NONE;
|
||||
const auto& hes = this->hand_and_equip_states[z];
|
||||
if (hes) {
|
||||
uint16_t card_id = hes->assist_card_id == 0xFFFF
|
||||
? this->card_id_for_card_ref(hes->assist_card_id)
|
||||
: hes->assist_card_id.load();
|
||||
this->assist_effects[z] = assist_effect_number_for_card_id(card_id);
|
||||
if (this->assist_effects[z] != AssistEffect::NONE) {
|
||||
this->assist_card_defs[z] = this->definition_for_card_id(card_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AssistServer::recompute_effects() {
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
this->active_assist_effects[z] = AssistEffect::NONE;
|
||||
this->active_assist_card_defs[z] = nullptr;
|
||||
}
|
||||
this->num_active_assists = 0;
|
||||
|
||||
if (this->num_assist_cards_set != 0) {
|
||||
for (size_t z = 0; z < this->num_assist_cards_set; z++) {
|
||||
auto eff = this->assist_effects[this->client_ids_with_assists[z]];
|
||||
if (eff == AssistEffect::RESISTANCE || eff == AssistEffect::INDEPENDENT) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Note: this->num_assist_cards_set is > 0 when we get here
|
||||
for (size_t z = 0; z < this->num_assist_cards_set - 1; z++) {
|
||||
for (size_t w = z + 1; w < this->num_assist_cards_set; w++) {
|
||||
uint8_t z_client_id = this->client_ids_with_assists[z];
|
||||
uint8_t w_client_id = this->client_ids_with_assists[w];
|
||||
if (this->hand_and_equip_states[w_client_id]->assist_card_set_number <
|
||||
this->hand_and_equip_states[z_client_id]->assist_card_set_number) {
|
||||
this->client_ids_with_assists[z] = w_client_id;
|
||||
this->client_ids_with_assists[w] = z_client_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this->num_active_assists = this->num_assist_cards_set;
|
||||
for (size_t z = 0; z < this->num_assist_cards_set; z++) {
|
||||
this->active_assist_effects[z] = this->assist_effects[this->client_ids_with_assists[z]];
|
||||
this->active_assist_card_defs[z] = this->assist_card_defs[this->client_ids_with_assists[z]];
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
} // namespace Episode3
|
||||
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "DataIndex.hh"
|
||||
#include "PlayerState.hh"
|
||||
#include "DeckState.hh"
|
||||
|
||||
namespace Episode3 {
|
||||
|
||||
|
||||
|
||||
class Server;
|
||||
|
||||
extern const std::vector<uint16_t> ALL_ASSIST_CARD_IDS;
|
||||
|
||||
AssistEffect assist_effect_number_for_card_id(uint16_t card_id);
|
||||
|
||||
class AssistServer {
|
||||
public:
|
||||
explicit AssistServer(std::shared_ptr<Server> server);
|
||||
std::shared_ptr<Server> server();
|
||||
std::shared_ptr<const Server> server() const;
|
||||
|
||||
uint16_t card_id_for_card_ref(uint16_t card_ref) const;
|
||||
std::shared_ptr<const DataIndex::CardEntry> definition_for_card_id(uint16_t card_id) const;
|
||||
|
||||
uint32_t compute_num_assist_effects_for_client(uint16_t client_id);
|
||||
uint32_t compute_num_assist_effects_for_team(uint32_t team_id);
|
||||
|
||||
bool should_block_assist_effects_for_client(uint16_t client_id) const;
|
||||
AssistEffect get_active_assist_by_index(size_t index) const;
|
||||
|
||||
void populate_effects();
|
||||
void recompute_effects();
|
||||
|
||||
private:
|
||||
std::weak_ptr<Server> w_server;
|
||||
|
||||
public:
|
||||
parray<AssistEffect, 4> assist_effects;
|
||||
std::shared_ptr<const DataIndex::CardEntry> assist_card_defs[4];
|
||||
uint32_t num_assist_cards_set;
|
||||
parray<uint8_t, 4> client_ids_with_assists;
|
||||
parray<AssistEffect, 4> active_assist_effects;
|
||||
std::shared_ptr<const DataIndex::CardEntry> active_assist_card_defs[4];
|
||||
uint32_t num_active_assists;
|
||||
std::shared_ptr<HandAndEquipState> hand_and_equip_states[4];
|
||||
std::shared_ptr<parray<CardShortStatus, 0x10>> card_short_statuses[4];
|
||||
std::shared_ptr<DeckEntry> deck_entries[4];
|
||||
std::shared_ptr<parray<ActionChainWithConds, 9>> set_card_action_chains[4];
|
||||
std::shared_ptr<parray<ActionMetadata, 9>> set_card_action_metadatas[4];
|
||||
};
|
||||
|
||||
|
||||
|
||||
} // namespace Episode3
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,131 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "../Text.hh"
|
||||
#include "../CommandFormats.hh"
|
||||
#include "DataIndex.hh"
|
||||
|
||||
namespace Episode3 {
|
||||
|
||||
|
||||
|
||||
class ServerBase;
|
||||
class Server;
|
||||
class PlayerState;
|
||||
|
||||
class Card : public std::enable_shared_from_this<Card> {
|
||||
public:
|
||||
Card(
|
||||
uint16_t card_id,
|
||||
uint16_t card_ref,
|
||||
uint16_t client_id,
|
||||
std::shared_ptr<Server> server);
|
||||
void init();
|
||||
std::shared_ptr<Server> server();
|
||||
std::shared_ptr<const Server> server() const;
|
||||
std::shared_ptr<PlayerState> player_state();
|
||||
std::shared_ptr<const PlayerState> player_state() const;
|
||||
|
||||
ssize_t apply_abnormal_condition(
|
||||
const CardDefinition::Effect& eff,
|
||||
uint8_t def_effect_index,
|
||||
uint16_t target_card_ref,
|
||||
uint16_t sc_card_ref,
|
||||
int16_t value,
|
||||
int8_t dice_roll_value,
|
||||
int8_t random_percent);
|
||||
void apply_ap_adjust_assists_to_attack(
|
||||
std::shared_ptr<const Card> attacker_card,
|
||||
int16_t* inout_attacker_ap,
|
||||
int16_t* in_defense_power) const;
|
||||
bool card_type_is_sc_or_creature() const;
|
||||
bool check_card_flag(uint32_t flags) const;
|
||||
void commit_attack(
|
||||
int16_t damage,
|
||||
std::shared_ptr<Card> attacker_card,
|
||||
G_ApplyConditionEffect_GC_Ep3_6xB4x06* cmd,
|
||||
size_t strike_number,
|
||||
int16_t* out_effective_damage);
|
||||
int16_t compute_defense_power_for_attacker_card(
|
||||
std::shared_ptr<const Card> attacker_card);
|
||||
void destroy_set_card(std::shared_ptr<Card> attacker_card);
|
||||
int32_t error_code_for_move_to_location(const Location& loc) const;
|
||||
void execute_attack(std::shared_ptr<Card> attacker_card);
|
||||
bool get_attack_condition_value(
|
||||
ConditionType cond_type,
|
||||
uint16_t card_ref,
|
||||
uint8_t def_effect_index,
|
||||
uint16_t value,
|
||||
uint16_t* out_value) const;
|
||||
std::shared_ptr<const DataIndex::CardEntry> get_definition() const;
|
||||
uint16_t get_card_ref() const;
|
||||
uint8_t get_client_id() const;
|
||||
uint8_t get_current_hp() const;
|
||||
uint8_t get_max_hp() const;
|
||||
CardShortStatus get_short_status();
|
||||
uint8_t get_team_id() const;
|
||||
int32_t move_to_location(const Location& loc);
|
||||
void propagate_shared_hp_if_needed();
|
||||
void send_6xB4x4E_4C_4D_if_needed(bool always_send = false);
|
||||
void send_6xB4x4E_if_needed(bool always_send = false);
|
||||
void set_current_and_max_hp(int16_t hp);
|
||||
void set_current_hp(
|
||||
uint32_t new_hp, bool propagate_shared_hp = true, bool enforce_max_hp = true);
|
||||
void update_stats_on_destruction();
|
||||
void clear_action_chain_and_metadata_and_most_flags();
|
||||
void compute_action_chain_results(
|
||||
bool apply_action_conditions, bool ignore_this_card_ap_tp);
|
||||
void unknown_802380C0();
|
||||
void unknown_80237F98(bool require_condition_20_or_21);
|
||||
void unknown_80237F88();
|
||||
void unknown_80235AA0();
|
||||
void unknown_80235AD4();
|
||||
void unknown_80235B10();
|
||||
void unknown_80236374(std::shared_ptr<Card> other_card, const ActionState* as);
|
||||
void unknown_802379BC(uint16_t card_ref);
|
||||
void unknown_802379DC(const ActionState& pa);
|
||||
void unknown_80237A90(const ActionState& pa, uint16_t action_card_ref);
|
||||
void unknown_8023813C();
|
||||
bool is_guard_item() const;
|
||||
bool unknown_80236554(std::shared_ptr<Card> other_card, const ActionState* as);
|
||||
void unknown_802362D8(std::shared_ptr<Card> other_card);
|
||||
void unknown_80237734();
|
||||
|
||||
private:
|
||||
std::weak_ptr<Server> w_server;
|
||||
std::weak_ptr<PlayerState> w_player_state;
|
||||
|
||||
public:
|
||||
int16_t max_hp;
|
||||
int16_t current_hp;
|
||||
std::shared_ptr<const DataIndex::CardEntry> def_entry;
|
||||
uint8_t client_id;
|
||||
uint16_t card_id;
|
||||
uint16_t card_ref;
|
||||
uint16_t sc_card_ref;
|
||||
std::shared_ptr<const DataIndex::CardEntry> sc_def_entry;
|
||||
CardType sc_card_type;
|
||||
uint8_t team_id;
|
||||
uint32_t card_flags;
|
||||
Location loc;
|
||||
Direction facing_direction;
|
||||
ActionChainWithConds action_chain;
|
||||
ActionMetadata action_metadata;
|
||||
int16_t ap;
|
||||
int16_t tp;
|
||||
uint32_t num_ally_fcs_destroyed_at_set_time;
|
||||
uint32_t num_cards_destroyed_by_team_at_set_time;
|
||||
uint32_t unknown_a9;
|
||||
int16_t last_attack_preliminary_damage;
|
||||
int16_t last_attack_final_damage;
|
||||
uint32_t num_destroyed_ally_fcs;
|
||||
std::weak_ptr<Card> w_destroyer_sc_card;
|
||||
int16_t current_defense_power;
|
||||
};
|
||||
|
||||
|
||||
|
||||
} // namespace Episode3
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,347 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "../Text.hh"
|
||||
#include "DataIndex.hh"
|
||||
|
||||
namespace Episode3 {
|
||||
|
||||
|
||||
|
||||
struct UnknownMatrixEntry {
|
||||
uint16_t card_id;
|
||||
uint8_t unknown_v1;
|
||||
uint8_t unknown_v2;
|
||||
};
|
||||
|
||||
const UnknownMatrixEntry* unknown_8024DAFC(
|
||||
uint16_t row_card_id,
|
||||
uint16_t column_card_id,
|
||||
bool use_entry_v1,
|
||||
size_t* out_entry_index = nullptr);
|
||||
|
||||
|
||||
|
||||
class CardSpecial {
|
||||
public:
|
||||
enum class ExpressionTokenType {
|
||||
SPACE = 0, // Also used for end of string (get_next_expr_token returns null)
|
||||
REFERENCE = 1, // Reference to a value from the env stats (e.g. hp)
|
||||
NUMBER = 2, // Constant value (e.g. 2)
|
||||
SUBTRACT = 3, // "-" in input string
|
||||
ADD = 4, // "+" in input string
|
||||
ROUND_DIVIDE = 5, // "/" in input string
|
||||
FLOOR_DIVIDE = 6, // "//" in input string
|
||||
MULTIPLY = 7, // "*" in input string
|
||||
};
|
||||
|
||||
struct DiceRoll {
|
||||
uint8_t client_id;
|
||||
uint8_t unknown_a2;
|
||||
uint8_t value;
|
||||
bool value_used_in_expr;
|
||||
uint16_t unknown_a5;
|
||||
|
||||
DiceRoll();
|
||||
void clear();
|
||||
};
|
||||
|
||||
struct AttackEnvStats {
|
||||
uint32_t num_set_cards; // "f" in expr
|
||||
uint32_t dice_roll_value1; // "d" in expr
|
||||
uint32_t effective_ap; // "ap" in expr
|
||||
uint32_t effective_tp; // "tp" in expr
|
||||
uint32_t current_hp; // "hp" in expr
|
||||
uint32_t max_hp; // "mhp" in expr
|
||||
uint32_t effective_ap_if_not_tech; // "dm" in expr
|
||||
uint32_t effective_ap_if_not_physical; // "tdm" in expr
|
||||
uint32_t player_num_destroyed_fcs; // "tf" in expr
|
||||
uint32_t player_num_atk_points; // "ac" in expr
|
||||
uint32_t defined_max_hp; // "php" in expr
|
||||
uint32_t dice_roll_value2; // "dc" in expr
|
||||
uint32_t card_cost; // "cs" in expr
|
||||
uint32_t total_num_set_cards; // "a" in expr
|
||||
uint32_t action_cards_ap; // "kap" in expr
|
||||
uint32_t action_cards_tp; // "ktp" in expr
|
||||
uint32_t unknown_a1; // "dn" in expr
|
||||
uint32_t num_item_or_creature_cards_in_hand; // "hf" in expr
|
||||
uint32_t num_destroyed_ally_fcs; // "df" in expr
|
||||
uint32_t target_team_num_set_cards; // "ff" in expr
|
||||
uint32_t condition_giver_team_num_set_cards; // "ef" in expr
|
||||
uint32_t num_native_creatures; // "bi" in expr
|
||||
uint32_t num_a_beast_creatures; // "ab" in expr
|
||||
uint32_t num_machine_creatures; // "mc" in expr
|
||||
uint32_t num_dark_creatures; // "dk" in expr
|
||||
uint32_t num_sword_type_items; // "sa" in expr
|
||||
uint32_t num_gun_type_items; // "gn" in expr
|
||||
uint32_t num_cane_type_items; // "wd" in expr
|
||||
uint32_t effective_ap_if_not_tech2; // "tt" in expr
|
||||
uint32_t team_dice_boost; // "lv" in expr
|
||||
uint32_t sc_effective_ap; // "adm" in expr
|
||||
uint32_t attack_bonus; // "ddm" in expr
|
||||
uint32_t num_sword_type_items_on_team; // "sat" in expr
|
||||
uint32_t target_attack_bonus; // "edm" in expr
|
||||
uint32_t last_attack_preliminary_damage; // "ldm" in expr
|
||||
uint32_t last_attack_damage; // "rdm" in expr
|
||||
uint32_t total_last_attack_damage; // "fdm" in expr
|
||||
uint32_t last_attack_damage_count; // "ndm" in expr
|
||||
uint32_t target_current_hp; // "ehp" in expr
|
||||
|
||||
AttackEnvStats();
|
||||
void clear();
|
||||
|
||||
uint32_t at(size_t offset) const;
|
||||
} __attribute__((packed));
|
||||
|
||||
CardSpecial(std::shared_ptr<Server> server);
|
||||
std::shared_ptr<Server> server();
|
||||
std::shared_ptr<const Server> server() const;
|
||||
|
||||
void adjust_attack_damage_due_to_conditions(
|
||||
std::shared_ptr<const Card> target_card, int16_t* inout_damage, uint16_t attacker_card_ref);
|
||||
void adjust_dice_boost_if_team_has_condition_52(
|
||||
uint8_t team_id, uint8_t* inout_dice_boost, std::shared_ptr<const Card> card);
|
||||
void apply_action_conditions(
|
||||
uint8_t when,
|
||||
std::shared_ptr<const Card> attacker_card,
|
||||
std::shared_ptr<Card> defender_card,
|
||||
uint32_t flags,
|
||||
const ActionState* as);
|
||||
bool apply_attribute_guard_if_possible(
|
||||
uint32_t flags,
|
||||
CardClass card_class,
|
||||
std::shared_ptr<Card> card,
|
||||
uint16_t condition_giver_card_ref,
|
||||
uint16_t attacker_card_ref);
|
||||
bool apply_defense_condition(
|
||||
uint8_t when,
|
||||
Condition* defender_cond,
|
||||
uint8_t cond_index,
|
||||
const ActionState& defense_state,
|
||||
std::shared_ptr<Card> defender_card,
|
||||
uint32_t flags,
|
||||
bool unknown_p8);
|
||||
bool apply_defense_conditions(
|
||||
const ActionState& as,
|
||||
uint8_t when,
|
||||
std::shared_ptr<Card> defender_card,
|
||||
uint32_t flags);
|
||||
bool apply_stat_deltas_to_all_cards_from_all_conditions_with_card_ref(
|
||||
uint16_t card_ref);
|
||||
bool apply_stat_deltas_to_card_from_condition_and_clear_cond(
|
||||
Condition& cond, std::shared_ptr<Card> card);
|
||||
bool apply_stats_deltas_to_card_from_all_conditions_with_card_ref(
|
||||
uint16_t card_ref, std::shared_ptr<Card> card);
|
||||
bool card_has_condition_with_ref(
|
||||
std::shared_ptr<const Card> card,
|
||||
ConditionType cond_type,
|
||||
uint16_t card_ref,
|
||||
uint16_t match_card_ref) const;
|
||||
bool card_is_destroyed(std::shared_ptr<const Card> card) const;
|
||||
void compute_attack_ap(
|
||||
std::shared_ptr<const Card> target_card,
|
||||
int16_t* out_value,
|
||||
uint16_t attacker_card_ref);
|
||||
AttackEnvStats compute_attack_env_stats(
|
||||
const ActionState& pa,
|
||||
std::shared_ptr<const Card> card,
|
||||
const DiceRoll& dice_roll,
|
||||
uint16_t target_card_ref,
|
||||
uint16_t condition_giver_card_ref);
|
||||
std::shared_ptr<Card> compute_replaced_target_based_on_conditions(
|
||||
uint16_t target_card_ref,
|
||||
int unknown_p3,
|
||||
int unknown_p4,
|
||||
uint16_t attacker_card_ref,
|
||||
uint16_t set_card_ref,
|
||||
int unknown_p7,
|
||||
uint32_t* unknown_p9,
|
||||
uint8_t def_effect_index,
|
||||
uint32_t* unknown_p11,
|
||||
uint16_t sc_card_ref);
|
||||
StatSwapType compute_stat_swap_type(std::shared_ptr<const Card> card) const;
|
||||
void compute_team_dice_boost(uint8_t team_id);
|
||||
bool condition_has_when_20_or_21(const Condition& cond) const;
|
||||
size_t count_action_cards_with_condition_for_all_current_attacks(
|
||||
ConditionType cond_type, uint16_t card_ref) const;
|
||||
size_t count_action_cards_with_condition_for_current_attack(
|
||||
std::shared_ptr<const Card> card, ConditionType cond_type, uint16_t card_ref) const;
|
||||
size_t count_cards_with_card_id_set_by_player_except_card_ref(
|
||||
uint16_t card_id, uint16_t card_ref) const;
|
||||
std::vector<std::shared_ptr<const Card>> get_all_set_cards_by_team_and_class(
|
||||
CardClass card_class, uint8_t team_id, bool exclude_destroyed_cards) const;
|
||||
ActionState create_attack_state_from_card_action_chain(
|
||||
std::shared_ptr<const Card> attacker_card) const;
|
||||
ActionState create_defense_state_for_card_pair_action_chains(
|
||||
std::shared_ptr<const Card> attacker_card,
|
||||
std::shared_ptr<const Card> defender_card) const;
|
||||
void destroy_card_if_hp_zero(
|
||||
std::shared_ptr<Card> card, uint16_t attacker_card_ref);
|
||||
bool evaluate_effect_arg2_condition(
|
||||
const ActionState& as,
|
||||
std::shared_ptr<const Card> card,
|
||||
const char* arg2_text,
|
||||
DiceRoll& dice_roll,
|
||||
uint16_t set_card_ref,
|
||||
uint16_t sc_card_ref,
|
||||
uint8_t random_percent,
|
||||
uint8_t when) const;
|
||||
int32_t evaluate_effect_expr(
|
||||
const AttackEnvStats& ast,
|
||||
const char* expr,
|
||||
DiceRoll& dice_roll) const;
|
||||
bool execute_effect(
|
||||
Condition& cond,
|
||||
std::shared_ptr<Card> card,
|
||||
int16_t expr_value,
|
||||
int16_t unknown_p5,
|
||||
ConditionType cond_type,
|
||||
uint unknown_p7,
|
||||
uint16_t attacker_card_ref);
|
||||
const Condition* find_condition_with_parameters(
|
||||
std::shared_ptr<const Card> card,
|
||||
ConditionType cond_type,
|
||||
uint16_t set_card_ref,
|
||||
uint8_t def_effect_index) const;
|
||||
Condition* find_condition_with_parameters(
|
||||
std::shared_ptr<Card> card,
|
||||
ConditionType cond_type,
|
||||
uint16_t set_card_ref,
|
||||
uint8_t def_effect_index) const;
|
||||
static void get_card1_loc_with_card2_opposite_direction(
|
||||
Location* out_loc,
|
||||
std::shared_ptr<const Card> card1,
|
||||
std::shared_ptr<const Card> card2);
|
||||
uint16_t get_card_id_with_effective_range(
|
||||
std::shared_ptr<const Card> card1, uint16_t default_card_id, std::shared_ptr<const Card> card2) const;
|
||||
static void get_effective_ap_tp(
|
||||
StatSwapType type,
|
||||
int16_t* effective_ap,
|
||||
int16_t* effective_tp,
|
||||
int16_t hp,
|
||||
int16_t ap,
|
||||
int16_t tp);
|
||||
const char* get_next_expr_token(
|
||||
const char *expr, ExpressionTokenType* out_type, int32_t* out_value) const;
|
||||
std::vector<std::shared_ptr<const Card>> get_targeted_cards_for_condition(
|
||||
uint16_t card_ref,
|
||||
uint8_t def_effect_index,
|
||||
uint16_t setter_card_ref,
|
||||
const ActionState& as,
|
||||
int16_t p_target_type,
|
||||
bool apply_usability_filters) const;
|
||||
std::vector<std::shared_ptr<Card>> get_targeted_cards_for_condition(
|
||||
uint16_t card_ref,
|
||||
uint8_t def_effect_index,
|
||||
uint16_t setter_card_ref,
|
||||
const ActionState& as,
|
||||
int16_t p_target_type,
|
||||
bool apply_usability_filters);
|
||||
bool is_card_targeted_by_condition(
|
||||
const Condition& cond, const ActionState& as, std::shared_ptr<const Card> card) const;
|
||||
void on_card_set(std::shared_ptr<PlayerState> ps, uint16_t card_ref);
|
||||
const CardDefinition::Effect* original_definition_for_condition(
|
||||
const Condition& cond) const;
|
||||
bool card_ref_has_ability_trap(const Condition& eff) const;
|
||||
void send_6xB4x06_for_exp_change(
|
||||
std::shared_ptr<const Card> card,
|
||||
uint16_t attacker_card_ref,
|
||||
uint8_t dice_roll_value,
|
||||
bool unknown_p5) const;
|
||||
void send_6xB4x06_for_card_destroyed(
|
||||
std::shared_ptr<const Card> destroyed_card, uint16_t attacker_card_ref) const;
|
||||
uint16_t send_6xB4x06_if_card_ref_invalid(
|
||||
uint16_t card_ref, int16_t value) const;
|
||||
void send_6xB4x06_for_stat_delta(
|
||||
std::shared_ptr<const Card> card,
|
||||
uint16_t attacker_card_ref,
|
||||
uint32_t flags,
|
||||
int16_t hp_delta,
|
||||
bool unknown_p6,
|
||||
bool unknown_p7) const;
|
||||
bool should_cancel_condition_due_to_anti_abnormality(
|
||||
const CardDefinition::Effect& eff,
|
||||
std::shared_ptr<const Card> card,
|
||||
uint16_t target_card_ref,
|
||||
uint16_t sc_card_ref) const;
|
||||
bool should_return_card_ref_to_hand_on_destruction(
|
||||
uint16_t card_ref) const;
|
||||
size_t sum_last_attack_damage(
|
||||
std::vector<std::shared_ptr<const Card>>* out_cards,
|
||||
int32_t* out_damage_sum,
|
||||
size_t* out_damage_count) const;
|
||||
void update_condition_orders(std::shared_ptr<Card> card);
|
||||
int16_t max_all_attack_bonuses(size_t* out_count) const;
|
||||
void unknown_80244AA8(std::shared_ptr<Card> card);
|
||||
void unknown_80244E20(
|
||||
std::shared_ptr<const Card> attacker_card,
|
||||
std::shared_ptr<Card> target_card,
|
||||
int16_t* inout_unknown_p4);
|
||||
void unknown_8024C2B0(
|
||||
uint32_t when,
|
||||
uint16_t set_card_ref,
|
||||
const ActionState& as,
|
||||
uint16_t sc_card_ref,
|
||||
bool apply_defense_condition_to_all_cards = true,
|
||||
uint16_t apply_defense_condition_to_card_ref = 0xFFFF);
|
||||
std::vector<std::shared_ptr<const Card>> get_all_set_cards() const;
|
||||
std::vector<std::shared_ptr<const Card>> find_cards_by_condition_inc_exc(
|
||||
ConditionType include_cond,
|
||||
ConditionType exclude_cond = ConditionType::NONE,
|
||||
AssistEffect include_eff = AssistEffect::NONE,
|
||||
AssistEffect exclude_eff = AssistEffect::NONE) const;
|
||||
void clear_invalid_conditions_on_card(
|
||||
std::shared_ptr<Card> card, const ActionState& as);
|
||||
void on_card_destroyed(
|
||||
std::shared_ptr<Card> attacker_card, std::shared_ptr<Card> destroyed_card);
|
||||
std::vector<std::shared_ptr<const Card>> find_cards_in_hp_range(
|
||||
int16_t min, int16_t max) const;
|
||||
std::vector<std::shared_ptr<const Card>> find_all_cards_by_aerial_attribute(bool is_aerial) const;
|
||||
std::vector<std::shared_ptr<const Card>> find_cards_damaged_by_at_least(int16_t damage) const;
|
||||
std::vector<std::shared_ptr<const Card>> find_all_set_cards_on_client_team(uint8_t client_id) const;
|
||||
std::vector<std::shared_ptr<const Card>> find_all_cards_on_same_or_other_team(uint8_t client_id, bool same_team) const;
|
||||
std::shared_ptr<const Card> sc_card_for_client_id(uint8_t client_id) const;
|
||||
std::shared_ptr<const Card> get_attacker_card(const ActionState& as) const;
|
||||
std::vector<std::shared_ptr<const Card>> get_attacker_card_and_sc_if_item(const ActionState& as) const;
|
||||
std::vector<std::shared_ptr<const Card>> find_all_set_cards_with_cost_in_range(uint8_t min_cost, uint8_t max_cost) const;
|
||||
std::vector<std::shared_ptr<const Card>> filter_cards_by_range(
|
||||
const std::vector<std::shared_ptr<const Card>>& cards,
|
||||
std::shared_ptr<const Card> card1,
|
||||
const Location& card1_loc,
|
||||
std::shared_ptr<const Card> card2) const;
|
||||
void unknown_8024AAB8(const ActionState& as);
|
||||
void unknown_80244BE4(std::shared_ptr<Card> unknown_p2);
|
||||
void unknown_80244CA8(std::shared_ptr<Card> card);
|
||||
template <uint8_t When1, uint8_t When2>
|
||||
void unknown1_t(
|
||||
std::shared_ptr<Card> unknown_p2, const ActionState* existing_as = nullptr);
|
||||
void unknown_80249060(std::shared_ptr<Card> unknown_p2);
|
||||
void unknown_80249254(std::shared_ptr<Card> unknown_p2);
|
||||
void unknown_8024945C(std::shared_ptr<Card> unknown_p2, const ActionState& existing_as);
|
||||
void unknown_8024966C(std::shared_ptr<Card> unknown_p2, const ActionState* existing_as);
|
||||
static std::shared_ptr<Card> sc_card_for_card(std::shared_ptr<Card> unknown_p2);
|
||||
void unknown_8024A9D8(const ActionState& pa, uint16_t action_card_ref);
|
||||
void unknown_8024504C(std::shared_ptr<Card> unknown_p2);
|
||||
template <uint8_t When1, uint8_t When2, uint8_t When3, uint8_t When4>
|
||||
void unknown_t2(std::shared_ptr<Card> unknown_p2);
|
||||
void unknown_8024997C(std::shared_ptr<Card> card);
|
||||
void unknown_8024A394(std::shared_ptr<Card> card);
|
||||
bool client_has_atk_dice_boost_condition(uint8_t client_id);
|
||||
void unknown_8024A6DC(
|
||||
std::shared_ptr<Card> unknown_p2, std::shared_ptr<Card> unknown_p3);
|
||||
std::vector<std::shared_ptr<const Card>> find_all_sc_cards_of_class(
|
||||
CardClass card_class) const;
|
||||
|
||||
private:
|
||||
std::weak_ptr<Server> w_server;
|
||||
ActionState unknown_action_state_a1;
|
||||
ActionState action_state;
|
||||
uint16_t unknown_a2;
|
||||
};
|
||||
|
||||
|
||||
|
||||
} // namespace Episode3
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,815 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <phosg/Encoding.hh>
|
||||
|
||||
#include "../Text.hh"
|
||||
|
||||
namespace Episode3 {
|
||||
|
||||
|
||||
|
||||
// The comment in Server.hh does not apply to this file (and DataIndex.cc).
|
||||
// Except for the Location structure, these structures and functions are not
|
||||
// based on Sega's original implementation.
|
||||
|
||||
class DataIndex;
|
||||
|
||||
|
||||
|
||||
enum class StatSwapType : uint8_t {
|
||||
NONE = 0,
|
||||
A_T_SWAP = 1,
|
||||
A_H_SWAP = 2,
|
||||
};
|
||||
|
||||
enum class ActionType : uint8_t {
|
||||
INVALID_00 = 0,
|
||||
DEFENSE = 1,
|
||||
ATTACK = 2,
|
||||
};
|
||||
|
||||
enum class AttackMedium : uint8_t {
|
||||
UNKNOWN = 0,
|
||||
PHYSICAL = 1,
|
||||
TECH = 2,
|
||||
UNKNOWN_03 = 3, // Probably Resta
|
||||
INVALID_FF = 0xFF,
|
||||
};
|
||||
|
||||
enum class CriterionCode : uint8_t {
|
||||
NONE = 0x00,
|
||||
HU_CLASS_SC = 0x01,
|
||||
RA_CLASS_SC = 0x02,
|
||||
FO_CLASS_SC = 0x03,
|
||||
SAME_TEAM = 0x04,
|
||||
SAME_PLAYER = 0x05,
|
||||
SAME_TEAM_NOT_SAME_PLAYER = 0x06, // Allies only
|
||||
UNKNOWN_07 = 0x07,
|
||||
NOT_SC = 0x08,
|
||||
SC = 0x09,
|
||||
HU_OR_RA_CLASS_SC = 0x0A,
|
||||
HUNTER_HUMAN_SC = 0x0B,
|
||||
HUNTER_HU_CLASS_MALE_SC = 0x0C,
|
||||
HUNTER_FEMALE_SC = 0x0D,
|
||||
HUNTER_HU_OR_FO_CLASS_HUMAN_SC = 0x0E,
|
||||
HUNTER_HU_CLASS_ANDROID_SC = 0x0F,
|
||||
UNKNOWN_10 = 0x10,
|
||||
UNKNOWN_11 = 0x11,
|
||||
HUNTER_HUNEWEARL_CLASS_SC = 0x12,
|
||||
HUNTER_RA_CLASS_MALE_SC = 0x13,
|
||||
HUNTER_RA_CLASS_FEMALE_SC = 0x14,
|
||||
HUNTER_RA_OR_FO_CLASS_FEMALE_SC = 0x15,
|
||||
HUNTER_HU_OR_RA_CLASS_HUMAN_SC = 0x16,
|
||||
HUNTER_RA_CLASS_ANDROID_SC = 0x17,
|
||||
HUNTER_FO_CLASS_FEMALE_SC = 0x18,
|
||||
HUNTER_FEMALE_HUMAN_SC = 0x19,
|
||||
HUNTER_ANDROID_SC = 0x1A,
|
||||
HU_OR_FO_CLASS_SC = 0x1B,
|
||||
RA_OR_FO_CLASS_SC = 0x1C,
|
||||
PHYSICAL_OR_UNKNOWN_ATTACK_MEDIUM = 0x1D,
|
||||
TECH_OR_UNKNOWN_ATTACK_MEDIUM = 0x1E,
|
||||
PHYSICAL_OR_TECH_OR_UNKNOWN_ATTACK_MEDIUM = 0x1F,
|
||||
UNKNOWN_20 = 0x20,
|
||||
UNKNOWN_21 = 0x21,
|
||||
UNKNOWN_22 = 0x22,
|
||||
};
|
||||
|
||||
enum class CardRarity : uint8_t {
|
||||
N1 = 0x01,
|
||||
R1 = 0x02,
|
||||
S = 0x03,
|
||||
E = 0x04,
|
||||
N2 = 0x05,
|
||||
N3 = 0x06,
|
||||
N4 = 0x07,
|
||||
R2 = 0x08,
|
||||
R3 = 0x09,
|
||||
R4 = 0x0A,
|
||||
SS = 0x0B,
|
||||
D1 = 0x0C,
|
||||
D2 = 0x0D,
|
||||
INVIS = 0x0E,
|
||||
};
|
||||
|
||||
enum class CardType : uint8_t {
|
||||
HUNTERS_SC = 0x00,
|
||||
ARKZ_SC = 0x01,
|
||||
ITEM = 0x02,
|
||||
CREATURE = 0x03,
|
||||
ACTION = 0x04,
|
||||
ASSIST = 0x05,
|
||||
INVALID_FF = 0xFF,
|
||||
END_CARD_LIST = 0xFF,
|
||||
};
|
||||
|
||||
enum class CardClass : uint16_t {
|
||||
HU_SC = 0x0000,
|
||||
RA_SC = 0x0001,
|
||||
FO_SC = 0x0002,
|
||||
NATIVE_CREATURE = 0x000A,
|
||||
A_BEAST_CREATURE = 0x000B,
|
||||
MACHINE_CREATURE = 0x000C,
|
||||
DARK_CREATURE = 0x000D,
|
||||
GUARD_ITEM = 0x0015,
|
||||
MAG_ITEM = 0x0017,
|
||||
SWORD_ITEM = 0x0018,
|
||||
GUN_ITEM = 0x0019,
|
||||
CANE_ITEM = 0x001A,
|
||||
ATTACK_ACTION = 0x001E,
|
||||
DEFENSE_ACTION = 0x001F,
|
||||
TECH = 0x0020,
|
||||
PHOTON_BLAST = 0x0021,
|
||||
CONNECT_ONLY_ATTACK_ACTION = 0x0022,
|
||||
BOSS_ATTACK_ACTION = 0x0023,
|
||||
BOSS_TECH = 0x0024,
|
||||
ASSIST = 0x0028,
|
||||
};
|
||||
|
||||
bool card_class_is_tech_like(CardClass cc);
|
||||
|
||||
enum class TargetMode : uint8_t {
|
||||
NONE = 0x00, // Used for defense cards, mags, shields, etc.
|
||||
SINGLE_RANGE = 0x01,
|
||||
MULTI_RANGE = 0x02,
|
||||
SELF = 0x03,
|
||||
TEAM = 0x04,
|
||||
EVERYONE = 0x05,
|
||||
MULTI_RANGE_ALLIES = 0x06, // e.g. Shifta
|
||||
ALL_ALLIES = 0x07, // e.g. Anti, Resta, Leilla
|
||||
ALL = 0x08, // e.g. Last Judgment, Earthquake
|
||||
OWN_FCS = 0x09, // e.g. Traitor
|
||||
};
|
||||
|
||||
enum class ConditionType : uint8_t {
|
||||
NONE = 0x00,
|
||||
AP_BOOST = 0x01, // Temporarily increase AP by N
|
||||
RAMPAGE = 0x02,
|
||||
MULTI_STRIKE = 0x03, // Duplicate attack N times
|
||||
DAMAGE_MOD_1 = 0x04, // Set attack damage / AP to N after action cards applied (step 1)
|
||||
IMMOBILE = 0x05, // Give Immobile condition
|
||||
HOLD = 0x06, // Give Hold condition
|
||||
UNKNOWN_07 = 0x07,
|
||||
TP_BOOST = 0x08, // Add N TP temporarily during attack
|
||||
GIVE_DAMAGE = 0x09, // Cause direct N HP loss
|
||||
GUOM = 0x0A, // Give Guom condition
|
||||
PARALYZE = 0x0B, // Give Paralysis condition
|
||||
UNKNOWN_0C = 0x0C, // Swap AP and TP temporarily (presumably)
|
||||
A_H_SWAP = 0x0D, // Swap AP and HP temporarily
|
||||
PIERCE = 0x0E, // Attack SC directly even if they have items equipped
|
||||
UNKNOWN_0F = 0x0F,
|
||||
HEAL = 0x10, // Increase HP by N
|
||||
RETURN_TO_HAND = 0x11, // Return card to hand
|
||||
UNKNOWN_12 = 0x12,
|
||||
UNKNOWN_13 = 0x13,
|
||||
ACID = 0x14, // Give Acid condition
|
||||
UNKNOWN_15 = 0x15,
|
||||
MIGHTY_KNUCKLE = 0x16, // Temporarily increase AP by N, and set ATK dice to zero
|
||||
UNIT_BLOW = 0x17, // Temporarily increase AP by N * number of this card set within phase
|
||||
CURSE = 0x18, // Give Curse condition
|
||||
COMBO_AP = 0x19, // Temporarily increase AP by number of this card set within phase
|
||||
PIERCE_RAMPAGE_BLOCK = 0x1A, // Block attack if Pierce/Rampage
|
||||
ABILITY_TRAP = 0x1B, // Temporarily disable opponent abilities
|
||||
FREEZE = 0x1C, // Give Freeze condition
|
||||
ANTI_ABNORMALITY_1 = 0x1D, // Cure all abnormal conditions
|
||||
UNKNOWN_1E = 0x1E,
|
||||
EXPLOSION = 0x1F, // Damage all SCs and FCs by number of this same card set * 2
|
||||
UNKNOWN_20 = 0x20,
|
||||
UNKNOWN_21 = 0x21,
|
||||
UNKNOWN_22 = 0x22,
|
||||
RETURN_TO_DECK = 0x23, // Cancel discard and move to bottom of deck instead
|
||||
AERIAL = 0x24, // Give Aerial status
|
||||
AP_LOSS = 0x25, // Make attacker temporarily lose N AP during defense
|
||||
BONUS_FROM_LEADER = 0x26, // Gain AP equal to the number of cards of type N on the field
|
||||
FREE_MANEUVER = 0x27, // Enable movement over occupied tiles
|
||||
HASTE = 0x28, // Multiply all move action costs by expr (which may be zero)
|
||||
CLONE = 0x29, // Make setting this card free if at least one card of type N is already on the field
|
||||
DEF_DISABLE_BY_COST = 0x2A, // Disable use of any defense cards costing between (N / 10) and (N % 10) points, inclusive
|
||||
FILIAL = 0x2B, // Increase controlling SC's HP by N when this card is destroyed
|
||||
SNATCH = 0x2C, // Steal N EXP during attack
|
||||
HAND_DISRUPTER = 0x2D, // Discard N cards from hand immediately
|
||||
DROP = 0x2E, // Give Drop condition
|
||||
ACTION_DISRUPTER = 0x2F, // Destroy all action cards used by attacker
|
||||
SET_HP = 0x30, // Set HP to N
|
||||
NATIVE_SHIELD = 0x31, // Block attacks from Native creatures
|
||||
A_BEAST_SHIELD = 0x32, // Block attacks from A.Beast creatures
|
||||
MACHINE_SHIELD = 0x33, // Block attacks from Machine creatures
|
||||
DARK_SHIELD = 0x34, // Block attacks from Dark creatures
|
||||
SWORD_SHIELD = 0x35, // Block attacks from Sword items
|
||||
GUN_SHIELD = 0x36, // Block attacks from Gun items
|
||||
CANE_SHIELD = 0x37, // Block attacks from Cane items
|
||||
UNKNOWN_38 = 0x38,
|
||||
UNKNOWN_39 = 0x39,
|
||||
DEFENDER = 0x3A, // Make attacks go to setter of this card instead of original target
|
||||
SURVIVAL_DECOYS = 0x3B, // Redirect damage for multi-sided attack
|
||||
GIVE_OR_TAKE_EXP = 0x3C, // Give N EXP, or take if N is negative
|
||||
UNKNOWN_3D = 0x3D,
|
||||
DEATH_COMPANION = 0x3E, // If this card has 1 or 2 HP, set its HP to N
|
||||
EXP_DECOY = 0x3F, // If defender has EXP, lose EXP instead of getting damage when attacked
|
||||
SET_MV = 0x40, // Set MV to N
|
||||
GROUP = 0x41, // Temporarily increase AP by N * number of this card on field, excluding itself
|
||||
BERSERK = 0x42, // User of this card receives the same damage as target, and isn't helped by target's defense cards
|
||||
GUARD_CREATURE = 0x43, // Attacks on controlling SC damage this card instead
|
||||
TECH = 0x44, // Technique cards cost 1 fewer ATK point
|
||||
BIG_SWING = 0x45, // Increase all attacking ATK costs by 1
|
||||
UNKNOWN_46 = 0x46,
|
||||
SHIELD_WEAPON = 0x47, // Limit attacker's choice of target to guard items
|
||||
ATK_DICE_BOOST = 0x48, // Increase ATK dice roll by 1
|
||||
UNKNOWN_49 = 0x49,
|
||||
MAJOR_PIERCE = 0x4A, // If SC has over half of max HP, attacks target SC instead of equipped items
|
||||
HEAVY_PIERCE = 0x4B, // If SC has 3 or more items equipped, attacks target SC instead of equipped items
|
||||
MAJOR_RAMPAGE = 0x4C, // If SC has over half of max HP, attacks target SC and all equipped items
|
||||
HEAVY_RAMPAGE = 0x4D, // If SC has 3 or more items equipped, attacks target SC and all equipped items
|
||||
AP_GROWTH = 0x4E, // Permanently increase AP by N
|
||||
TP_GROWTH = 0x4F, // Permanently increase TP by N
|
||||
REBORN = 0x50, // If any card of type N is on the field, this card goes to the hand when destroyed instead of being discarded
|
||||
COPY = 0x51, // Temporarily set AP/TP to N percent (or 100% if N is 0) of opponent's values
|
||||
UNKNOWN_52 = 0x52,
|
||||
MISC_GUARDS = 0x53, // Add N to card's defense value
|
||||
AP_OVERRIDE = 0x54, // Set AP to N temporarily
|
||||
TP_OVERRIDE = 0x55, // Set TP to N temporarily
|
||||
RETURN = 0x56, // Return card to hand on destruction instead of discarding
|
||||
A_T_SWAP_PERM = 0x57, // Permanently swap AP and TP
|
||||
A_H_SWAP_PERM = 0x58, // Permanently swap AP and HP
|
||||
SLAYERS_ASSASSINS = 0x59, // Temporarily increase AP during attack
|
||||
ANTI_ABNORMALITY_2 = 0x5A, // Remove all conditions
|
||||
FIXED_RANGE = 0x5B, // Use SC's range instead of weapon or attack card ranges
|
||||
ELUDE = 0x5C, // SC does not lose HP when equipped items are destroyed
|
||||
PARRY = 0x5D, // Forward attack to a random FC within one tile of original target, excluding attacker and original target
|
||||
BLOCK_ATTACK = 0x5E, // Completely block attack
|
||||
UNKNOWN_5F = 0x5F,
|
||||
UNKNOWN_60 = 0x60,
|
||||
COMBO_TP = 0x61, // Gain TP equal to the number of cards of type N on the field
|
||||
MISC_AP_BONUSES = 0x62, // Temporarily increase AP by N
|
||||
MISC_TP_BONUSES = 0x63, // Temporarily increase TP by N
|
||||
UNKNOWN_64 = 0x64,
|
||||
MISC_DEFENSE_BONUSES = 0x65, // Decrease damage by N
|
||||
MOSTLY_HALFGUARDS = 0x66, // Reduce damage from incoming attack by N
|
||||
PERIODIC_FIELD = 0x67, // Swap immunity to tech or physical attacks
|
||||
FC_LIMIT_BY_COUNT = 0x68, // Change FC limit from 8 ATK points total to 4 FCs total
|
||||
UNKNOWN_69 = 0x69,
|
||||
MV_BONUS = 0x6A, // Increase MV by N
|
||||
FORWARD_DAMAGE = 0x6B,
|
||||
WEAK_SPOT_INFLUENCE = 0x6C, // Temporarily decrease AP by N
|
||||
DAMAGE_MODIFIER_2 = 0x6D, // Set attack damage / AP after action cards applied (step 2)
|
||||
WEAK_HIT_BLOCK = 0x6E, // Block all attacks of N damage or less
|
||||
AP_SILENCE = 0x6F, // Temporarily decrease AP of opponent by N
|
||||
TP_SILENCE = 0x70, // Temporarily decrease TP of opponent by N
|
||||
A_T_SWAP = 0x71, // Temporarily swap AP and TP
|
||||
HALFGUARD = 0x72, // Halve damage from attacks that would inflict N or more damage
|
||||
UNKNOWN_73 = 0x73,
|
||||
RAMPAGE_AP_LOSS = 0x74, // Temporarily reduce AP by N
|
||||
UNKNOWN_75 = 0x75,
|
||||
REFLECT = 0x76, // Generate reverse attack
|
||||
UNKNOWN_77 = 0x77,
|
||||
ANY = 0x78, // Not a real condition; used as a wildcard in search functions
|
||||
UNKNOWN_79 = 0x79,
|
||||
UNKNOWN_7A = 0x7A,
|
||||
UNKNOWN_7B = 0x7B,
|
||||
UNKNOWN_7C = 0x7C,
|
||||
UNKNOWN_7D = 0x7D,
|
||||
INVALID_FF = 0xFF,
|
||||
ANY_FF = 0xFF, // Used as a wildcard in some search functions
|
||||
};
|
||||
|
||||
enum class AssistEffect : uint16_t {
|
||||
NONE = 0x0000,
|
||||
DICE_HALF = 0x0001,
|
||||
DICE_PLUS_1 = 0x0002,
|
||||
DICE_FEVER = 0x0003,
|
||||
CARD_RETURN = 0x0004,
|
||||
LAND_PRICE = 0x0005,
|
||||
POWERLESS_RAIN = 0x0006,
|
||||
BRAVE_WIND = 0x0007,
|
||||
SILENT_COLOSSEUM = 0x0008,
|
||||
RESISTANCE = 0x0009,
|
||||
INDEPENDENT = 0x000A,
|
||||
ASSISTLESS = 0x000B,
|
||||
ATK_DICE_2 = 0x000C,
|
||||
DEFLATION = 0x000D,
|
||||
INFLATION = 0x000E,
|
||||
EXCHANGE = 0x000F,
|
||||
INFLUENCE = 0x0010,
|
||||
SKIP_SET = 0x0011,
|
||||
SKIP_MOVE = 0x0012,
|
||||
SKIP_ACT = 0x0013,
|
||||
SKIP_DRAW = 0x0014,
|
||||
FLY = 0x0015,
|
||||
NECROMANCER = 0x0016,
|
||||
PERMISSION = 0x0017,
|
||||
SHUFFLE_ALL = 0x0018,
|
||||
LEGACY = 0x0019,
|
||||
ASSIST_REVERSE = 0x001A,
|
||||
STAMINA = 0x001B,
|
||||
AP_ABSORPTION = 0x001C,
|
||||
HEAVY_FOG = 0x001D,
|
||||
TRASH_1 = 0x001E,
|
||||
EMPTY_HAND = 0x001F,
|
||||
HITMAN = 0x0020,
|
||||
ASSIST_TRASH = 0x0021,
|
||||
SHUFFLE_GROUP = 0x0022,
|
||||
ASSIST_VANISH = 0x0023,
|
||||
CHARITY = 0x0024,
|
||||
INHERITANCE = 0x0025,
|
||||
FIX = 0x0026,
|
||||
MUSCULAR = 0x0027,
|
||||
CHANGE_BODY = 0x0028,
|
||||
GOD_WHIM = 0x0029,
|
||||
GOLD_RUSH = 0x002A,
|
||||
ASSIST_RETURN = 0x002B,
|
||||
REQUIEM = 0x002C,
|
||||
RANSOM = 0x002D,
|
||||
SIMPLE = 0x002E,
|
||||
SLOW_TIME = 0x002F,
|
||||
QUICK_TIME = 0x0030,
|
||||
TERRITORY = 0x0031,
|
||||
OLD_TYPE = 0x0032,
|
||||
FLATLAND = 0x0033,
|
||||
IMMORTALITY = 0x0034,
|
||||
SNAIL_PACE = 0x0035,
|
||||
TECH_FIELD = 0x0036,
|
||||
FOREST_RAIN = 0x0037,
|
||||
CAVE_WIND = 0x0038,
|
||||
MINE_BRIGHTNESS = 0x0039,
|
||||
RUIN_DARKNESS = 0x003A,
|
||||
SABER_DANCE = 0x003B,
|
||||
BULLET_STORM = 0x003C,
|
||||
CANE_PALACE = 0x003D,
|
||||
GIANT_GARDEN = 0x003E,
|
||||
MARCH_OF_THE_MEEK = 0x003F,
|
||||
SUPPORT = 0x0040,
|
||||
RICH = 0x0041,
|
||||
REVERSE_CARD = 0x0042,
|
||||
VENGEANCE = 0x0043,
|
||||
SQUEEZE = 0x0044,
|
||||
HOMESICK = 0x0045,
|
||||
BOMB = 0x0046,
|
||||
SKIP_TURN = 0x0047,
|
||||
BATTLE_ROYALE = 0x0048,
|
||||
DICE_FEVER_PLUS = 0x0049,
|
||||
RICH_PLUS = 0x004A,
|
||||
CHARITY_PLUS = 0x004B,
|
||||
ANY = 0x004C, // Unused on cards; used in some search functions
|
||||
};
|
||||
|
||||
enum class BattlePhase : uint8_t {
|
||||
INVALID_00 = 0,
|
||||
DICE = 1,
|
||||
SET = 2,
|
||||
MOVE = 3,
|
||||
ACTION = 4,
|
||||
DRAW = 5,
|
||||
INVALID_FF = 0xFF,
|
||||
};
|
||||
|
||||
enum class ActionSubphase : uint8_t {
|
||||
ATTACK = 0,
|
||||
DEFENSE = 2,
|
||||
INVALID_FF = 0xFF,
|
||||
};
|
||||
|
||||
enum class SetupPhase : uint8_t {
|
||||
REGISTRATION = 0,
|
||||
STARTER_ROLLS = 1,
|
||||
HAND_REDRAW_OPTION = 2,
|
||||
MAIN_BATTLE = 3,
|
||||
BATTLE_ENDED = 4,
|
||||
INVALID_FF = 0xFF,
|
||||
};
|
||||
|
||||
enum class RegistrationPhase : uint8_t {
|
||||
AWAITING_NUM_PLAYERS = 0, // num_players not set yet
|
||||
AWAITING_PLAYERS = 1, // num_players set, but some players not registered
|
||||
AWAITING_DECKS = 2, // all players registered, but some decks missing
|
||||
REGISTERED = 3, // All players/decks present, but battle not started yet
|
||||
BATTLE_STARTED = 4,
|
||||
INVALID_FF = 0xFF,
|
||||
};
|
||||
|
||||
|
||||
|
||||
enum class Direction : uint8_t {
|
||||
RIGHT = 0,
|
||||
UP = 1,
|
||||
LEFT = 2,
|
||||
DOWN = 3,
|
||||
INVALID_FF = 0xFF,
|
||||
};
|
||||
|
||||
Direction turn_left(Direction d);
|
||||
Direction turn_right(Direction d);
|
||||
Direction turn_around(Direction d);
|
||||
const char* name_for_direction(Direction d);
|
||||
|
||||
struct Location {
|
||||
uint8_t x;
|
||||
uint8_t y;
|
||||
Direction direction;
|
||||
uint8_t unused;
|
||||
|
||||
Location();
|
||||
Location(uint8_t x, uint8_t y);
|
||||
Location(uint8_t x, uint8_t y, Direction direction);
|
||||
bool operator==(const Location& other) const = default;
|
||||
bool operator!=(const Location& other) const = default;
|
||||
|
||||
void clear();
|
||||
void clear_FF();
|
||||
} __attribute__((packed));
|
||||
|
||||
struct CardDefinition {
|
||||
struct Stat {
|
||||
enum Type : uint8_t {
|
||||
BLANK = 0,
|
||||
STAT = 1,
|
||||
PLUS_STAT = 2,
|
||||
MINUS_STAT = 3,
|
||||
EQUALS_STAT = 4,
|
||||
UNKNOWN = 5,
|
||||
PLUS_UNKNOWN = 6,
|
||||
MINUS_UNKNOWN = 7,
|
||||
EQUALS_UNKNOWN = 8,
|
||||
};
|
||||
be_uint16_t code;
|
||||
Type type;
|
||||
int8_t stat;
|
||||
|
||||
void decode_code();
|
||||
std::string str() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct Effect {
|
||||
uint8_t effect_num;
|
||||
ConditionType type;
|
||||
ptext<char, 0x0F> expr; // May be blank if the condition type doesn't use it
|
||||
uint8_t when;
|
||||
ptext<char, 4> arg1;
|
||||
ptext<char, 4> arg2;
|
||||
ptext<char, 4> arg3;
|
||||
CriterionCode apply_criterion;
|
||||
uint8_t unknown_a2;
|
||||
|
||||
bool is_empty() const;
|
||||
static std::string str_for_arg(const std::string& arg);
|
||||
std::string str() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
be_uint32_t card_id;
|
||||
parray<uint8_t, 0x40> jp_name;
|
||||
CardType type; // Type enum. If <0, then this is the end of the card list
|
||||
uint8_t self_cost; // ATK dice points required
|
||||
uint8_t ally_cost; // ATK points from allies required; PBs use this
|
||||
uint8_t unused1;
|
||||
Stat hp;
|
||||
Stat ap;
|
||||
Stat tp;
|
||||
Stat mv;
|
||||
parray<uint8_t, 8> left_colors;
|
||||
parray<uint8_t, 8> right_colors;
|
||||
parray<uint8_t, 8> top_colors;
|
||||
parray<be_uint32_t, 6> range;
|
||||
be_uint32_t unused2;
|
||||
TargetMode target_mode;
|
||||
uint8_t assist_turns; // 90 (dec) = once, 99 (dec) = forever
|
||||
uint8_t cannot_move; // 0 for SC and creature cards; 1 for everything else
|
||||
uint8_t cannot_attack; // 1 for shields, mags, defense actions, and assist cards
|
||||
uint8_t unused3;
|
||||
uint8_t hide_in_deck_edit; // 0 = player can use this card (appears in deck edit)
|
||||
CriterionCode usable_criterion;
|
||||
CardRarity rarity;
|
||||
be_uint16_t unknown_a2;
|
||||
be_uint16_t be_card_class; // Used for checking attributes (e.g. item types)
|
||||
// These two fields seem to always contain the same value, and are always 0
|
||||
// for non-assist cards and nonzero for assists. Each assist card has a unique
|
||||
// value here and no effects, which makes it look like this is how assist
|
||||
// effects are implemented. There seems to be some 1k-modulation going on here
|
||||
// too; most cards are in the range 101-174 but a few have e.g. 1150, 2141. A
|
||||
// few pairs of cards have the same effect, which makes it look like some
|
||||
// other fields are also involved in determining their effects (see e.g. Skip
|
||||
// Draw / Skip Move, Dice Fever / Dice Fever +, Reverse Card / Rich +).
|
||||
parray<be_uint16_t, 2> assist_effect;
|
||||
// Drop rates are decimal-encoded with the following fields:
|
||||
// - rate % 10 (that is, the lowest decimal place) specifies the required game
|
||||
// mode. 0 means any mode, 1 means offline only, 2 means 1P free-battle, 3
|
||||
// means 2P+ free battle, 4 means story mode.
|
||||
// - (rate / 10) % 100 (that is, the tens and hundreds decimal places) specify
|
||||
// something else, but it's not clear what exactly.
|
||||
// - rate / 1000 (the thousands decimal place) specifies the level class
|
||||
// required to get this drop.
|
||||
// - rate / 10000 (the ten-thousands decimal place) must be either 0, 1, or 2,
|
||||
// but it's not clear yet what each value means.
|
||||
// The drop rates are completely ignored if any of the following are true
|
||||
// (which means the card can never be found in a normal post-battle draw):
|
||||
// - type is SC_HUNTERS or SC_ARKZ
|
||||
// - unknown_a3 is 0x23 or 0x24
|
||||
// - rarity is E, D1, D2, or INVIS
|
||||
// - hide_in_deck_edit is 1 (specifically 1; other nonzero values here don't
|
||||
// prevent the card from appearing in post-battle draws)
|
||||
parray<be_uint16_t, 2> drop_rates;
|
||||
ptext<char, 0x14> en_name;
|
||||
ptext<char, 0x0B> jp_short_name;
|
||||
ptext<char, 0x08> en_short_name;
|
||||
Effect effects[3];
|
||||
uint8_t unused4;
|
||||
|
||||
bool is_sc() const;
|
||||
bool is_fc() const;
|
||||
bool is_named_android_sc() const;
|
||||
bool any_top_color_matches(const CardDefinition& other) const;
|
||||
CardClass card_class() const;
|
||||
|
||||
void decode_range();
|
||||
std::string str() const;
|
||||
} __attribute__((packed)); // 0x128 bytes in total
|
||||
|
||||
struct CardDefinitionsFooter {
|
||||
be_uint32_t num_cards1;
|
||||
be_uint32_t unknown_a1;
|
||||
be_uint32_t num_cards2;
|
||||
be_uint32_t unknown_a2[11];
|
||||
be_uint32_t unknown_offset_a3;
|
||||
be_uint32_t unknown_a4[3];
|
||||
be_uint32_t footer_offset;
|
||||
be_uint32_t unknown_a5[3];
|
||||
} __attribute__((packed));
|
||||
|
||||
struct DeckDefinition {
|
||||
ptext<char, 0x10> name;
|
||||
be_uint32_t client_id; // 0-3
|
||||
// List of card IDs. The card count is the number of nonzero entries here
|
||||
// before a zero entry (or 50 if no entries are nonzero). The first card ID is
|
||||
// the SC card, which the game implicitly subtracts from the limit - so a
|
||||
// valid deck should actually have 31 cards in it.
|
||||
parray<le_uint16_t, 50> card_ids;
|
||||
be_uint32_t unknown_a1;
|
||||
// Last modification time
|
||||
le_uint16_t year;
|
||||
uint8_t month;
|
||||
uint8_t day;
|
||||
uint8_t hour;
|
||||
uint8_t minute;
|
||||
uint8_t second;
|
||||
uint8_t unknown_a2;
|
||||
} __attribute__((packed)); // 0x84 bytes in total
|
||||
|
||||
struct PlayerConfig {
|
||||
// Offsets in comments in this struct are relative to start of 61/98 command
|
||||
/* 0728 */ parray<uint8_t, 0x1434> unknown_a1;
|
||||
/* 1B5C */ parray<DeckDefinition, 25> decks;
|
||||
/* 2840 */ uint64_t unknown_a2;
|
||||
/* 2848 */ be_uint32_t offline_clv_exp; // CLvOff = this / 100
|
||||
/* 284C */ be_uint32_t online_clv_exp; // CLvOn = this / 100
|
||||
/* 2850 */ parray<uint8_t, 0x14C> unknown_a3;
|
||||
/* 299C */ ptext<char, 0x10> name;
|
||||
// Other records are probably somewhere in here - e.g. win/loss, play time, etc.
|
||||
/* 29AC */ parray<uint8_t, 0xCC> unknown_a4;
|
||||
} __attribute__((packed));
|
||||
|
||||
enum class HPType : uint8_t {
|
||||
DEFEAT_PLAYER = 0,
|
||||
DEFEAT_TEAM = 1,
|
||||
COMMON_HP = 2,
|
||||
};
|
||||
|
||||
enum class DiceExchangeMode : uint8_t {
|
||||
HIGH_ATK = 0,
|
||||
HIGH_DEF = 1,
|
||||
NONE = 2,
|
||||
};
|
||||
|
||||
enum class AllowedCards : uint8_t {
|
||||
ALL = 0,
|
||||
N_ONLY = 1,
|
||||
N_R_ONLY = 2,
|
||||
N_R_S_ONLY = 3,
|
||||
};
|
||||
|
||||
struct Rules {
|
||||
// When this structure is used in a map/quest definition, FF in any of these
|
||||
// fields means the user is allowed to override it. Any non-FF fields are
|
||||
// fixed for the map/quest and cannot be overridden.
|
||||
uint8_t overall_time_limit; // In increments of 5 minutes; 0 = unlimited
|
||||
uint8_t phase_time_limit; // In seconds; 0 = unlimited
|
||||
AllowedCards allowed_cards;
|
||||
uint8_t min_dice; // 0 = default (1)
|
||||
// 4
|
||||
uint8_t max_dice; // 0 = default (6)
|
||||
uint8_t disable_deck_shuffle; // 0 = shuffle on, 1 = off
|
||||
uint8_t disable_deck_loop; // 0 = loop on, 1 = off
|
||||
uint8_t char_hp;
|
||||
// 8
|
||||
HPType hp_type;
|
||||
uint8_t no_assist_cards; // 1 = assist cards disallowed
|
||||
uint8_t disable_dialogue; // 0 = dialogue on, 1 = dialogue off
|
||||
DiceExchangeMode dice_exchange_mode;
|
||||
// C
|
||||
uint8_t disable_dice_boost; // 0 = dice boost on, 1 = off
|
||||
parray<uint8_t, 3> unused;
|
||||
|
||||
Rules();
|
||||
bool operator==(const Rules& other) const = default;
|
||||
bool operator!=(const Rules& other) const = default;
|
||||
void clear();
|
||||
|
||||
bool check_invalid_fields() const;
|
||||
bool check_and_reset_invalid_fields();
|
||||
|
||||
std::string str() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct StateFlags {
|
||||
le_uint16_t turn_num;
|
||||
BattlePhase battle_phase;
|
||||
uint8_t current_team_turn1;
|
||||
uint8_t current_team_turn2;
|
||||
ActionSubphase action_subphase;
|
||||
SetupPhase setup_phase;
|
||||
RegistrationPhase registration_phase;
|
||||
parray<le_uint32_t, 2> team_exp;
|
||||
parray<uint8_t, 2> team_dice_boost;
|
||||
uint8_t first_team_turn;
|
||||
uint8_t tournament_flag;
|
||||
parray<CardType, 4> client_sc_card_types;
|
||||
|
||||
StateFlags();
|
||||
bool operator==(const StateFlags& other) const = default;
|
||||
bool operator!=(const StateFlags& other) const = default;
|
||||
void clear();
|
||||
void clear_FF();
|
||||
} __attribute__((packed));
|
||||
|
||||
|
||||
|
||||
struct MapList {
|
||||
be_uint32_t num_maps;
|
||||
be_uint32_t unknown_a1; // Always 0?
|
||||
be_uint32_t strings_offset; // From after total_size field (add 0x10 to this value)
|
||||
be_uint32_t total_size; // Including header, entries, and strings
|
||||
|
||||
struct Entry { // Should be 0x220 bytes in total
|
||||
be_uint16_t map_x;
|
||||
be_uint16_t map_y;
|
||||
be_uint16_t scene_data2;
|
||||
be_uint16_t map_number;
|
||||
// Text offsets are from the beginning of the strings block after all map
|
||||
// entries (that is, add strings_offset to them to get the string offset)
|
||||
be_uint32_t name_offset;
|
||||
be_uint32_t location_name_offset;
|
||||
be_uint32_t quest_name_offset;
|
||||
be_uint32_t description_offset;
|
||||
be_uint16_t width;
|
||||
be_uint16_t height;
|
||||
parray<parray<uint8_t, 0x10>, 0x10> map_tiles;
|
||||
parray<parray<uint8_t, 0x10>, 0x10> modification_tiles;
|
||||
be_uint32_t unknown_a2; // Seems to always be 0xFF000000
|
||||
} __attribute__((packed));
|
||||
|
||||
// Variable-length fields:
|
||||
// Entry entries[num_maps];
|
||||
// char strings[...EOF]; // Null-terminated strings, pointed to by offsets in Entry structs
|
||||
} __attribute__((packed));
|
||||
|
||||
struct CompressedMapHeader { // .mnm file format
|
||||
le_uint32_t map_number;
|
||||
le_uint32_t compressed_data_size;
|
||||
// Compressed data immediately follows (which decompresses to a MapDefinition)
|
||||
} __attribute__((packed));
|
||||
|
||||
struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
/* 0000 */ be_uint32_t unknown_a1;
|
||||
/* 0004 */ be_uint32_t map_number;
|
||||
/* 0008 */ uint8_t width;
|
||||
/* 0009 */ uint8_t height;
|
||||
/* 000A */ uint8_t scene_data2; // TODO: What is this?
|
||||
// All alt_maps fields (including the floats) past num_alt_maps are filled in
|
||||
// with FF. For example, if num_alt_maps == 8, the last two fields in each
|
||||
// alt_maps array are filled with FF.
|
||||
/* 000B */ uint8_t num_alt_maps; // TODO: What are the alt maps for?
|
||||
// In the map_tiles array, the values are:
|
||||
// 00 = not a valid tile
|
||||
// 01 = valid tile unless punched out (later)
|
||||
// 02 = team A start (1v1)
|
||||
// 03, 04 = team A start (2v2)
|
||||
// 05 = ???
|
||||
// 06, 07 = team B start (2v2)
|
||||
// 08 = team B start (1v1)
|
||||
// Note that the game displays the map reversed vertically in the preview
|
||||
// window. For example, player 1 is on team A, which usually starts at the top
|
||||
// of the map as defined in this struct, or at the bottom as shown in the
|
||||
// preview window.
|
||||
/* 000C */ parray<parray<uint8_t, 0x10>, 0x10> map_tiles;
|
||||
// The start_tile_definitions field is a list of 6 bytes for each team. The
|
||||
// low 6 bits of each byte match the starting location for the relevant player
|
||||
// in map_tiles; the high 2 bits are the player's initial facing direction.
|
||||
// - If the team has 1 player, only byte [0] is used.
|
||||
// - If the team has 2 players, bytes [1] and [2] are used.
|
||||
// - If the team has 3 players, bytes [3] through [5] are used.
|
||||
/* 010C */ parray<parray<uint8_t, 6>, 2> start_tile_definitions;
|
||||
/* 0118 */ parray<parray<uint8_t, 0x10>, 0x10> alt_maps1[2][0x0A];
|
||||
/* 1518 */ parray<be_float, 0x12> alt_maps_unknown_a3[2][0x0A];
|
||||
/* 1AB8 */ parray<be_float, 0x24> unknown_a5[3];
|
||||
// In the modification_tiles array, the values are:
|
||||
// 10 = blocked (as if the corresponding map_tiles value was 00)
|
||||
// 20 = blocked (maybe one of 10 or 20 are passable by Aerial characters)
|
||||
// 30-34 = teleporters (2 of each value may be present)
|
||||
// 40-44 = traps (one of each type is chosen at random to be a real trap at
|
||||
// battle start time)
|
||||
// 50 = appears as improperly-z-buffered teal cube in preview, behaves as a
|
||||
// blocked tile (like 10 and 20)
|
||||
/* 1C68 */ parray<parray<uint8_t, 0x10>, 0x10> modification_tiles;
|
||||
/* 1D68 */ parray<uint8_t, 0x74> unknown_a6;
|
||||
/* 1DDC */ Rules default_rules;
|
||||
/* 1DEC */ parray<uint8_t, 4> unknown_a7;
|
||||
/* 1DF0 */ ptext<char, 0x14> name;
|
||||
/* 1E04 */ ptext<char, 0x14> location_name;
|
||||
/* 1E18 */ ptext<char, 0x3C> quest_name; // == location_name if not a quest
|
||||
/* 1E54 */ ptext<char, 0x190> description;
|
||||
/* 1FE4 */ be_uint16_t map_x;
|
||||
/* 1FE6 */ be_uint16_t map_y;
|
||||
struct NPCDeck {
|
||||
ptext<char, 0x18> name;
|
||||
parray<be_uint16_t, 0x20> card_ids; // Last one appears to always be FFFF
|
||||
} __attribute__((packed));
|
||||
/* 1FE8 */ NPCDeck npc_decks[3]; // Unused if name[0] == 0
|
||||
struct NPCCharacter {
|
||||
parray<be_uint16_t, 2> unknown_a1;
|
||||
parray<uint8_t, 4> unknown_a2;
|
||||
ptext<char, 0x10> name;
|
||||
parray<be_uint16_t, 0x7E> unknown_a3;
|
||||
} __attribute__((packed));
|
||||
/* 20F0 */ NPCCharacter npc_chars[3]; // Unused if name[0] == 0
|
||||
/* 242C */ parray<uint8_t, 0x14> unknown_a8; // Always FF?
|
||||
/* 2440 */ ptext<char, 0x190> before_message;
|
||||
/* 25D0 */ ptext<char, 0x190> after_message;
|
||||
/* 2760 */ ptext<char, 0x190> dispatch_message; // Usually "You can only dispatch <character>" or blank
|
||||
struct DialogueSet {
|
||||
be_uint16_t unknown_a1;
|
||||
be_uint16_t unknown_a2; // Always 0x0064 if valid, 0xFFFF if unused?
|
||||
ptext<char, 0x40> strings[4];
|
||||
} __attribute__((packed)); // Total size: 0x104 bytes
|
||||
/* 28F0 */ DialogueSet dialogue_sets[3][0x10]; // Up to 0x10 per valid NPC
|
||||
/* 59B0 */ parray<be_uint16_t, 0x10> reward_card_ids;
|
||||
/* 59D0 */ parray<uint8_t, 0x0C> unknown_a9;
|
||||
/* 59DC */ uint8_t unknown_a10;
|
||||
/* 59DD */ parray<uint8_t, 0x3B> unknown_a11;
|
||||
/* 5A18 */
|
||||
|
||||
std::string str(const DataIndex* data_index = nullptr) const;
|
||||
} __attribute__((packed));
|
||||
|
||||
|
||||
|
||||
class DataIndex {
|
||||
public:
|
||||
explicit DataIndex(const std::string& directory, bool debug = false);
|
||||
|
||||
struct CardEntry {
|
||||
CardDefinition def;
|
||||
std::string text;
|
||||
std::vector<std::string> debug_tags; // Empty unless debug == true
|
||||
};
|
||||
|
||||
class MapEntry {
|
||||
public:
|
||||
MapDefinition map;
|
||||
|
||||
MapEntry(const MapDefinition& map);
|
||||
MapEntry(const std::string& compressed_data);
|
||||
|
||||
std::string compressed() const;
|
||||
|
||||
private:
|
||||
mutable std::string compressed_data;
|
||||
};
|
||||
|
||||
const std::string& get_compressed_card_definitions() const;
|
||||
std::shared_ptr<const CardEntry> definition_for_card_id(uint32_t id) const;
|
||||
std::set<uint32_t> all_card_ids() const;
|
||||
|
||||
const std::string& get_compressed_map_list() const;
|
||||
std::shared_ptr<const MapEntry> definition_for_map_number(uint32_t id) const;
|
||||
std::set<uint32_t> all_map_ids() const;
|
||||
|
||||
private:
|
||||
bool debug;
|
||||
|
||||
std::string compressed_card_definitions;
|
||||
std::unordered_map<uint32_t, std::shared_ptr<CardEntry>> card_definitions;
|
||||
|
||||
// The compressed map list is generated on demand from the maps map below.
|
||||
// It's marked mutable because the logical consistency of the DataIndex object
|
||||
// is not violated from the caller's perspective even if we don't generate the
|
||||
// compressed map list at load time.
|
||||
mutable std::string compressed_map_list;
|
||||
std::map<uint32_t, std::shared_ptr<MapEntry>> maps;
|
||||
};
|
||||
|
||||
|
||||
|
||||
} // namespace Episode3
|
||||
@@ -0,0 +1,286 @@
|
||||
#include "DeckState.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace Episode3 {
|
||||
|
||||
|
||||
|
||||
NameEntry::NameEntry() {
|
||||
this->clear();
|
||||
}
|
||||
|
||||
void NameEntry::clear() {
|
||||
this->client_id = 0xFF;
|
||||
this->present = 0;
|
||||
this->unused_by_server = 0;
|
||||
this->unused = 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
DeckEntry::DeckEntry() {
|
||||
this->clear();
|
||||
}
|
||||
|
||||
void DeckEntry::clear() {
|
||||
this->team_id = 0xFFFFFFFF;
|
||||
this->god_whim_flag = 3;
|
||||
this->unused1 = 0;
|
||||
this->unused2 = 0;
|
||||
this->unused3 = 0;
|
||||
this->card_ids.clear(0xFFFF);
|
||||
}
|
||||
|
||||
|
||||
|
||||
uint8_t index_for_card_ref(uint16_t card_ref) {
|
||||
return card_ref & 0xFF;
|
||||
}
|
||||
|
||||
uint8_t client_id_for_card_ref(uint16_t card_ref) {
|
||||
return (card_ref >> 8) & 0xFF;
|
||||
}
|
||||
|
||||
|
||||
|
||||
uint8_t DeckState::num_drawable_cards() const {
|
||||
return this->card_refs.size() - this->draw_index;
|
||||
}
|
||||
|
||||
bool DeckState::set_card_ref_in_play(uint16_t card_ref) {
|
||||
if (!this->contains_card_ref(card_ref)) {
|
||||
return false;
|
||||
}
|
||||
uint8_t index = index_for_card_ref(card_ref);
|
||||
if (this->entries[index].state == CardState::IN_HAND) {
|
||||
this->entries[index].state = CardState::IN_PLAY;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool DeckState::contains_card_ref(uint16_t card_ref) const {
|
||||
return index_for_card_ref(card_ref) < this->entries.size();
|
||||
}
|
||||
|
||||
void DeckState::disable_loop() {
|
||||
this->loop_enabled = false;
|
||||
}
|
||||
|
||||
void DeckState::disable_shuffle() {
|
||||
this->shuffle_enabled = false;
|
||||
}
|
||||
|
||||
uint16_t DeckState::draw_card() {
|
||||
if (this->num_drawable_cards() == 0) {
|
||||
this->restart();
|
||||
}
|
||||
if (this->num_drawable_cards() == 0) {
|
||||
return 0xFFFF;
|
||||
}
|
||||
|
||||
uint16_t ref = this->card_refs[this->draw_index++];
|
||||
this->entries[index_for_card_ref(ref)].state = CardState::IN_HAND;
|
||||
return ref;
|
||||
}
|
||||
|
||||
bool DeckState::draw_card_by_ref(uint16_t card_ref) {
|
||||
if (card_ref == 0xFFFF) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t index = index_for_card_ref(card_ref);
|
||||
if (index > this->entries.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the card is discarded, then it should be before the draw index, and we
|
||||
// can just change its state.
|
||||
if (this->entries[index].state == CardState::DISCARDED) {
|
||||
this->entries[index].state = CardState::IN_HAND;
|
||||
return true;
|
||||
|
||||
// If the card is still drawable, we need to move it so it's just in front of
|
||||
// the draw index, then immediately draw it
|
||||
} else if (this->entries[index].state == CardState::DRAWABLE) {
|
||||
ssize_t ref_index;
|
||||
for (ref_index = this->card_refs.size(); ref_index >= 0; ref_index--) {
|
||||
if (this->card_refs[ref_index] == card_ref) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ref_index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t ref_uindex = ref_index;
|
||||
for (; ref_uindex > this->draw_index; ref_uindex--) {
|
||||
// Note: draw_index is also unsigned, so ref_uindex cannot be zero here
|
||||
this->card_refs[ref_uindex] = this->card_refs[ref_uindex - 1];
|
||||
}
|
||||
this->card_refs[this->draw_index] = card_ref;
|
||||
this->entries[index].state = CardState::IN_HAND;
|
||||
this->draw_index++;
|
||||
return true;
|
||||
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t DeckState::card_id_for_card_ref(uint16_t card_ref) const {
|
||||
if (card_ref == 0xFFFF) {
|
||||
return 0xFFFF;
|
||||
}
|
||||
|
||||
uint8_t index = index_for_card_ref(card_ref);
|
||||
if (index < this->entries.size()) {
|
||||
return this->entries[index].card_id;
|
||||
} else {
|
||||
return 0xFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t DeckState::sc_card_id() const {
|
||||
return this->entries[0].card_id;
|
||||
}
|
||||
|
||||
uint16_t DeckState::sc_card_ref() const {
|
||||
return this->card_refs[0];
|
||||
}
|
||||
|
||||
uint16_t DeckState::card_ref_for_index(uint8_t index) const {
|
||||
return this->card_ref_base | index;
|
||||
}
|
||||
|
||||
DeckState::CardState DeckState::state_for_card_ref(uint16_t card_ref) const {
|
||||
uint8_t index = index_for_card_ref(card_ref);
|
||||
return (index < this->entries.size()) ? this->entries[index].state : CardState::INVALID;
|
||||
}
|
||||
|
||||
void DeckState::restart() {
|
||||
// First, if deck loop is on, return all discarded cards to the drawable state
|
||||
if (this->loop_enabled) {
|
||||
for (size_t z = 0; z < this->entries.size(); z++) {
|
||||
if (this->entries[z].state == CardState::DISCARDED) {
|
||||
this->entries[z].state = CardState::DRAWABLE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For any cards that are still in hand or still in play, move their refs to
|
||||
// the already-drawn part of the deck
|
||||
this->draw_index = 0;
|
||||
for (size_t z = 0; z < this->entries.size(); z++) {
|
||||
if (this->entries[z].state != CardState::DRAWABLE) {
|
||||
this->card_refs[this->draw_index++] = this->card_ref_for_index(z);
|
||||
}
|
||||
}
|
||||
|
||||
// For now-drawable cards, put their refs after the draw index
|
||||
size_t index = this->draw_index;
|
||||
for (size_t z = 0; z < this->entries.size(); z++) {
|
||||
if (this->entries[z].state == CardState::DRAWABLE) {
|
||||
this->card_refs[index++] = this->card_ref_for_index(z);
|
||||
}
|
||||
}
|
||||
|
||||
this->shuffle();
|
||||
}
|
||||
|
||||
void DeckState::do_mulligan() {
|
||||
for (size_t z = 0; z < this->entries.size(); z++) {
|
||||
if (this->entries[z].state == CardState::DISCARDED) {
|
||||
this->entries[z].state = CardState::DRAWABLE;
|
||||
}
|
||||
}
|
||||
this->draw_index = 1;
|
||||
|
||||
if (this->shuffle_enabled) {
|
||||
// Get the next 5 cards from the deck, and put the previous 5 cards after
|
||||
// them (so they will be shuffled back in).
|
||||
for (uint8_t z = 0; z < 5; z++) {
|
||||
uint8_t index = z + this->draw_index;
|
||||
uint16_t temp_ref = this->card_refs[index];
|
||||
this->card_refs[index] = this->card_refs[index + 5];
|
||||
this->card_refs[index + 5] = temp_ref;
|
||||
}
|
||||
|
||||
// Shuffle the deck, except the first 5 cards (which are about to be drawn).
|
||||
size_t max = this->num_drawable_cards() - 5;
|
||||
uint8_t base_index = this->draw_index + 5;
|
||||
for (size_t z = 0; z < this->card_refs.size(); z++) {
|
||||
uint8_t index1 = this->random_crypt->next() % max;
|
||||
uint8_t index2 = this->random_crypt->next() % max;
|
||||
uint16_t temp_ref = this->card_refs[base_index + index1];
|
||||
this->card_refs[base_index + index1] = this->card_refs[base_index + index2];
|
||||
this->card_refs[base_index + index2] = temp_ref;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool DeckState::set_card_ref_drawable_next(uint16_t card_ref) {
|
||||
if (card_ref == 0xFFFF) {
|
||||
return false;
|
||||
}
|
||||
if (client_id_for_card_ref(card_ref) != this->client_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t index = index_for_card_ref(card_ref);
|
||||
if (this->entries[index].state == CardState::DRAWABLE) {
|
||||
return false;
|
||||
} else if (this->draw_index < 1) {
|
||||
return false;
|
||||
} else {
|
||||
this->entries[index].state = CardState::DRAWABLE;
|
||||
this->card_refs[--this->draw_index] = card_ref;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool DeckState::set_card_ref_drawable_at_end(uint16_t card_ref) {
|
||||
if (this->set_card_ref_drawable_next(card_ref)) {
|
||||
uint16_t head_card_ref = this->card_refs[this->draw_index];
|
||||
if (this->draw_index < this->card_refs.size() - 1) {
|
||||
for (size_t z = this->draw_index; z < this->card_refs.size() - 1; z++) {
|
||||
this->card_refs[z] = this->card_refs[z + 1];
|
||||
}
|
||||
}
|
||||
this->card_refs[this->card_refs.size() - 1] = head_card_ref;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void DeckState::set_card_discarded(uint16_t card_ref) {
|
||||
uint8_t index = index_for_card_ref(card_ref);
|
||||
if (index < this->entries.size()) {
|
||||
this->entries[index].state = CardState::DISCARDED;
|
||||
}
|
||||
}
|
||||
|
||||
void DeckState::shuffle() {
|
||||
if (this->shuffle_enabled) {
|
||||
size_t max = this->num_drawable_cards();
|
||||
for (size_t z = 0; z < this->card_refs.size(); z++) {
|
||||
// Note: This is the way Sega originally implemented shuffling - they just
|
||||
// do N swaps on the entire array. A more uniform way to do it would be to
|
||||
// instead swap each item with another random item (possibly itself) that
|
||||
// doesn't appear earlier than it in the array, but this is not what Sega
|
||||
// did.
|
||||
uint8_t index1 = this->draw_index + this->random_crypt->next() % max;
|
||||
uint8_t index2 = this->draw_index + this->random_crypt->next() % max;
|
||||
uint16_t temp_ref = this->card_refs[index1];
|
||||
this->card_refs[index1] = this->card_refs[index2];
|
||||
this->card_refs[index2] = temp_ref;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
} // namespace Episode3
|
||||
@@ -0,0 +1,117 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "../Text.hh"
|
||||
#include "../PSOEncryption.hh"
|
||||
|
||||
namespace Episode3 {
|
||||
|
||||
|
||||
|
||||
struct NameEntry {
|
||||
parray<char, 0x10> name;
|
||||
uint8_t client_id;
|
||||
uint8_t present;
|
||||
uint8_t unused_by_server;
|
||||
uint8_t unused;
|
||||
|
||||
NameEntry();
|
||||
void clear();
|
||||
} __attribute__((packed));
|
||||
|
||||
struct DeckEntry {
|
||||
ptext<char, 0x10> name;
|
||||
le_uint32_t team_id;
|
||||
parray<le_uint16_t, 0x1F> card_ids;
|
||||
// If the following flag is not set to 3, then the God Whim assist effect can
|
||||
// use cards that are hidden from the player during deck building. The client
|
||||
// always sets this to 3, and it's not clear why this even exists.
|
||||
uint8_t god_whim_flag;
|
||||
uint8_t unused1;
|
||||
le_uint16_t unused2;
|
||||
be_uint16_t unused3;
|
||||
|
||||
DeckEntry();
|
||||
void clear();
|
||||
} __attribute__((packed));
|
||||
|
||||
uint8_t index_for_card_ref(uint16_t card_ref);
|
||||
uint8_t client_id_for_card_ref(uint16_t card_ref);
|
||||
|
||||
class DeckState {
|
||||
public:
|
||||
enum class CardState {
|
||||
DRAWABLE = 0,
|
||||
STORY_CHARACTER = 1,
|
||||
IN_HAND = 2,
|
||||
IN_PLAY = 3,
|
||||
DISCARDED = 4,
|
||||
INVALID = 5,
|
||||
};
|
||||
|
||||
template <typename CardIDT>
|
||||
DeckState(
|
||||
uint8_t client_id,
|
||||
const parray<CardIDT, 0x1F>& card_ids,
|
||||
std::shared_ptr<PSOV2Encryption> random_crypt)
|
||||
: client_id(client_id),
|
||||
draw_index(1),
|
||||
card_ref_base(this->client_id << 8),
|
||||
shuffle_enabled(true),
|
||||
loop_enabled(true),
|
||||
random_crypt(random_crypt) {
|
||||
for (size_t z = 0; z < card_ids.size(); z++) {
|
||||
auto& e = this->entries[z];
|
||||
e.card_id = card_ids[z];
|
||||
e.deck_index = z;
|
||||
e.state = CardState::DRAWABLE;
|
||||
this->card_refs[z] = this->card_ref_for_index(z);
|
||||
}
|
||||
this->entries[0].state = CardState::STORY_CHARACTER;
|
||||
}
|
||||
|
||||
void disable_loop();
|
||||
void disable_shuffle();
|
||||
|
||||
uint8_t num_drawable_cards() const;
|
||||
bool contains_card_ref(uint16_t card_ref) const;
|
||||
uint16_t card_id_for_card_ref(uint16_t card_ref) const;
|
||||
uint16_t sc_card_id() const;
|
||||
uint16_t sc_card_ref() const;
|
||||
uint16_t card_ref_for_index(uint8_t index) const;
|
||||
CardState state_for_card_ref(uint16_t card_ref) const;
|
||||
|
||||
uint16_t draw_card();
|
||||
bool draw_card_by_ref(uint16_t card_ref);
|
||||
bool set_card_ref_in_play(uint16_t card_ref);
|
||||
bool set_card_ref_drawable_next(uint16_t card_ref);
|
||||
bool set_card_ref_drawable_at_end(uint16_t card_ref);
|
||||
void set_card_discarded(uint16_t card_ref);
|
||||
|
||||
void restart();
|
||||
void shuffle();
|
||||
void do_mulligan();
|
||||
|
||||
private:
|
||||
struct CardEntry {
|
||||
uint16_t card_id;
|
||||
uint8_t deck_index;
|
||||
CardState state;
|
||||
};
|
||||
uint8_t client_id;
|
||||
uint8_t draw_index;
|
||||
uint16_t card_ref_base;
|
||||
bool shuffle_enabled;
|
||||
bool loop_enabled;
|
||||
parray<CardEntry, 31> entries;
|
||||
parray<uint16_t, 31> card_refs;
|
||||
|
||||
std::shared_ptr<PSOV2Encryption> random_crypt;
|
||||
};
|
||||
|
||||
|
||||
|
||||
} // namespace Episode3
|
||||
@@ -0,0 +1,95 @@
|
||||
#include "MapState.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace Episode3 {
|
||||
|
||||
|
||||
|
||||
MapState::MapState() {
|
||||
this->clear();
|
||||
}
|
||||
|
||||
void MapState::clear() {
|
||||
this->width = 0;
|
||||
this->height = 0;
|
||||
for (size_t y = 0; y < this->tiles.size(); y++) {
|
||||
this->tiles[y].clear(0);
|
||||
}
|
||||
for (size_t z = 0; z < 2; z++) {
|
||||
this->start_tile_definitions[z].clear(0);
|
||||
}
|
||||
}
|
||||
|
||||
void MapState::print(FILE* stream) const {
|
||||
fprintf(stream, "[Map: w=%hu h=%hu]\n", this->width.load(), this->height.load());
|
||||
for (size_t y = 0; y < this->height; y++) {
|
||||
fputc(' ', stream);
|
||||
for (size_t x = 0; x < this->width; x++) {
|
||||
fprintf(stream, " %02hhX", this->tiles[y][x]);
|
||||
}
|
||||
fputc('\n', stream);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
MapAndRulesState::MapAndRulesState() {
|
||||
this->clear();
|
||||
}
|
||||
|
||||
void MapAndRulesState::clear() {
|
||||
this->map.clear();
|
||||
this->num_players = 0;
|
||||
this->unused1 = 0;
|
||||
this->unused_by_server = 0;
|
||||
this->num_players_per_team = 0;
|
||||
this->num_team0_players = 0;
|
||||
this->unused2 = 0;
|
||||
this->start_facing_directions = 0;
|
||||
this->unused3 = 0;
|
||||
this->map_number = 0;
|
||||
this->unused4 = 0;
|
||||
this->rules.clear();
|
||||
this->unused5 = 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
bool MapAndRulesState::loc_is_within_bounds(uint8_t x, uint8_t y) const {
|
||||
return (x < this->map.width) && (y < this->map.height);
|
||||
}
|
||||
|
||||
bool MapAndRulesState::tile_is_vacant(uint8_t x, uint8_t y) {
|
||||
if (!this->loc_is_within_bounds(x, y)) {
|
||||
return false;
|
||||
}
|
||||
return (this->map.tiles[y][x] == 1);
|
||||
}
|
||||
|
||||
void MapAndRulesState::set_occupied_bit_for_tile(uint8_t x, uint8_t y) {
|
||||
this->map.tiles[y][x] |= 0x10;
|
||||
}
|
||||
|
||||
void MapAndRulesState::clear_occupied_bit_for_tile(uint8_t x, uint8_t y) {
|
||||
this->map.tiles[y][x] &= 0xEF;
|
||||
}
|
||||
|
||||
|
||||
|
||||
OverlayState::OverlayState() {
|
||||
this->clear();
|
||||
}
|
||||
|
||||
void OverlayState::clear() {
|
||||
for (size_t y = 0; y < this->tiles.size(); y++) {
|
||||
this->tiles[y].clear(0);
|
||||
}
|
||||
this->unused1.clear(0);
|
||||
this->unused2.clear(0);
|
||||
this->unused3.clear(0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
} // namespace Episode3
|
||||
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "../Text.hh"
|
||||
#include "DataIndex.hh"
|
||||
|
||||
namespace Episode3 {
|
||||
|
||||
|
||||
|
||||
struct MapState {
|
||||
le_uint16_t width;
|
||||
le_uint16_t height;
|
||||
parray<parray<uint8_t, 0x10>, 0x10> tiles;
|
||||
parray<parray<uint8_t, 6>, 2> start_tile_definitions;
|
||||
|
||||
MapState();
|
||||
void clear();
|
||||
|
||||
void print(FILE* stream) const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct MapAndRulesState {
|
||||
MapState map;
|
||||
uint8_t num_players;
|
||||
uint8_t unused1;
|
||||
uint8_t unused_by_server;
|
||||
uint8_t num_players_per_team;
|
||||
uint8_t num_team0_players;
|
||||
uint8_t unused2;
|
||||
le_uint16_t start_facing_directions;
|
||||
uint32_t unused3;
|
||||
le_uint32_t map_number;
|
||||
uint32_t unused4;
|
||||
Rules rules;
|
||||
uint32_t unused5;
|
||||
|
||||
MapAndRulesState();
|
||||
void clear();
|
||||
|
||||
bool loc_is_within_bounds(uint8_t x, uint8_t y) const;
|
||||
bool tile_is_vacant(uint8_t x, uint8_t y);
|
||||
|
||||
void set_occupied_bit_for_tile(uint8_t x, uint8_t y);
|
||||
void clear_occupied_bit_for_tile(uint8_t x, uint8_t y);
|
||||
} __attribute__((packed));
|
||||
|
||||
struct OverlayState {
|
||||
parray<parray<uint8_t, 0x10>, 0x10> tiles;
|
||||
parray<le_uint32_t, 5> unused1;
|
||||
parray<le_uint32_t, 0x10> unused2;
|
||||
parray<le_uint16_t, 0x10> unused3;
|
||||
|
||||
OverlayState();
|
||||
void clear();
|
||||
} __attribute__((packed));
|
||||
|
||||
|
||||
|
||||
} // namespace Episode3
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,193 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "../Text.hh"
|
||||
#include "DataIndex.hh"
|
||||
#include "Card.hh"
|
||||
#include "DeckState.hh"
|
||||
#include "PlayerStateSubordinates.hh"
|
||||
|
||||
namespace Episode3 {
|
||||
|
||||
|
||||
|
||||
class ServerBase;
|
||||
class Server;
|
||||
|
||||
class PlayerState : public std::enable_shared_from_this<PlayerState> {
|
||||
public:
|
||||
PlayerState(uint8_t client_id, std::shared_ptr<Server> server);
|
||||
void init();
|
||||
std::shared_ptr<Server> server();
|
||||
std::shared_ptr<const Server> server() const;
|
||||
|
||||
bool draw_cards_allowed() const;
|
||||
void apply_assist_card_effect_on_set(
|
||||
std::shared_ptr<PlayerState> setter_ps);
|
||||
void apply_dice_effects();
|
||||
uint16_t card_ref_for_hand_index(size_t hand_index) const;
|
||||
int16_t compute_attack_or_defense_atk_costs(const ActionState& pa) const;
|
||||
void compute_total_set_cards_cost();
|
||||
size_t count_set_cards() const;
|
||||
size_t count_set_refs() const;
|
||||
void discard_all_assist_cards_from_hand();
|
||||
void discard_all_attack_action_cards_from_hand();
|
||||
void discard_all_item_and_creature_cards_from_hand();
|
||||
void discard_and_redraw_hand();
|
||||
bool discard_card_or_add_to_draw_pile(
|
||||
uint16_t card_ref, bool add_to_draw_pile);
|
||||
void discard_random_hand_card();
|
||||
bool discard_ref_from_hand(uint16_t card_ref);
|
||||
void discard_set_assist_card();
|
||||
bool do_mulligan();
|
||||
void draw_hand(ssize_t override_count = 0);
|
||||
void draw_initial_hand();
|
||||
int32_t error_code_for_client_setting_card(
|
||||
uint16_t card_ref,
|
||||
uint8_t card_index,
|
||||
const Location* loc,
|
||||
uint8_t assist_target_client_id) const;
|
||||
std::vector<uint16_t> get_all_cards_within_range(
|
||||
const parray<uint8_t, 9 * 9>& range,
|
||||
const Location& loc,
|
||||
uint8_t target_team_id) const;
|
||||
uint8_t get_atk_points() const;
|
||||
void get_short_status_for_card_index_in_hand(
|
||||
size_t hand_index, CardShortStatus* stat) const;
|
||||
std::shared_ptr<DeckState> get_deck();
|
||||
uint8_t get_def_points() const;
|
||||
uint8_t get_dice_result(size_t which) const;
|
||||
size_t get_hand_size() const;
|
||||
uint16_t get_sc_card_id() const;
|
||||
std::shared_ptr<Card> get_sc_card();
|
||||
std::shared_ptr<const Card> get_sc_card() const;
|
||||
uint16_t get_sc_card_ref() const;
|
||||
CardType get_sc_card_type() const;
|
||||
std::shared_ptr<Card> get_set_card(size_t set_index);
|
||||
std::shared_ptr<const Card> get_set_card(size_t set_index) const;
|
||||
uint16_t get_set_ref(size_t set_index) const;
|
||||
uint8_t get_team_id() const;
|
||||
ssize_t hand_index_for_card_ref(uint16_t card_ref) const;
|
||||
size_t set_index_for_card_ref(uint16_t card_ref) const;
|
||||
bool is_mulligan_allowed() const;
|
||||
bool is_team_turn() const;
|
||||
void log_discard(uint16_t card_ref, uint16_t reason);
|
||||
bool move_card_to_location_by_card_index(
|
||||
size_t card_index, const Location& new_loc);
|
||||
void move_null_hand_refs_to_end();
|
||||
void on_cards_destroyed();
|
||||
void replace_all_set_assists_with_random_assists();
|
||||
bool replace_assist_card_by_id(uint16_t card_id);
|
||||
bool return_set_card_to_hand2(uint16_t card_ref);
|
||||
bool return_set_card_to_hand1(uint16_t card_ref);
|
||||
uint8_t roll_dice(size_t num_dice);
|
||||
uint8_t roll_dice_with_effects(size_t num_dice);
|
||||
void send_set_card_updates(bool always_send = false);
|
||||
void set_assist_flags_from_assist_effects();
|
||||
bool set_card_from_hand(
|
||||
uint16_t card_ref,
|
||||
uint8_t card_index,
|
||||
const Location* loc,
|
||||
uint8_t assist_target_client_id,
|
||||
bool skip_error_checks_and_atk_sub);
|
||||
void set_initial_location();
|
||||
void set_map_occupied_bit_for_card_on_warp_tile(
|
||||
std::shared_ptr<const Card> card);
|
||||
void set_map_occupied_bits_for_sc_and_creatures();
|
||||
void subtract_def_points(uint8_t cost);
|
||||
bool subtract_or_check_atk_or_def_points_for_action(
|
||||
const ActionState& pa, bool deduct_points);
|
||||
void subtract_atk_points(uint8_t cost);
|
||||
void update_hand_and_equip_state_and_send_6xB4x02_if_needed(
|
||||
bool always_send = false);
|
||||
void set_random_assist_card_from_hand_for_free();
|
||||
void send_6xB4x04_if_needed(bool always_send = false);
|
||||
std::vector<uint16_t> get_card_refs_within_range_from_all_players(
|
||||
const parray<uint8_t, 9 * 9>& range,
|
||||
const Location& loc,
|
||||
CardType type) const;
|
||||
void unknown_80239460();
|
||||
void unknown_802394C4();
|
||||
void unknown_80239528();
|
||||
void handle_before_turn_assist_effects();
|
||||
int16_t get_assist_turns_remaining();
|
||||
bool set_action_cards_for_action_state(const ActionState& pa);
|
||||
void unknown_8023C174();
|
||||
void handle_homesick_assist_effect(std::shared_ptr<Card> card);
|
||||
void apply_main_die_assist_effects(uint8_t* die_value) const;
|
||||
void roll_main_dice();
|
||||
void unknown_8023C110();
|
||||
void compute_team_dice_boost_after_draw_phase();
|
||||
|
||||
private:
|
||||
std::weak_ptr<Server> w_server;
|
||||
|
||||
public:
|
||||
std::shared_ptr<Card> sc_card;
|
||||
std::shared_ptr<Card> set_cards[8];
|
||||
uint8_t client_id;
|
||||
uint16_t num_mulligans_allowed;
|
||||
CardType sc_card_type;
|
||||
uint8_t team_id;
|
||||
uint8_t atk_points;
|
||||
uint8_t def_points;
|
||||
uint8_t atk_points2;
|
||||
uint8_t atk_points2_max;
|
||||
uint8_t atk_bonuses;
|
||||
uint8_t def_bonuses;
|
||||
parray<uint8_t, 2> dice_results;
|
||||
uint8_t unknown_a4;
|
||||
uint8_t dice_max;
|
||||
uint8_t total_set_cards_cost;
|
||||
uint16_t sc_card_id;
|
||||
uint16_t sc_card_ref;
|
||||
|
||||
// This array is unfortunately heterogeneous; specifically:
|
||||
// [0] through [5] are hand refs
|
||||
// [6] is the current assist card ref (which may belong to another player)
|
||||
// [7] is the previous assist card ref
|
||||
// [8] through [15] are set refs
|
||||
parray<uint16_t, 0x10> card_refs;
|
||||
|
||||
std::shared_ptr<DeckState> deck_state;
|
||||
parray<uint16_t, 0x10> discard_log_card_refs;
|
||||
parray<uint16_t, 0x10> discard_log_reasons;
|
||||
uint8_t assist_remaining_turns;
|
||||
uint16_t assist_card_set_number;
|
||||
uint16_t set_assist_card_id;
|
||||
bool god_whim_can_use_hidden_cards;
|
||||
ActionChainWithConds unknown_a12;
|
||||
ActionMetadata unknown_a13;
|
||||
uint32_t unknown_a14;
|
||||
uint32_t assist_flags;
|
||||
uint8_t assist_delay_turns;
|
||||
Direction start_facing_direction;
|
||||
std::shared_ptr<HandAndEquipState> hand_and_equip;
|
||||
|
||||
// Like card_refs above, these arrays are also heterogeneous, but the indices
|
||||
// are not the same as for card_refs! THe indices here are:
|
||||
// [0] is the SC card status
|
||||
// [1] through [6] are hand cards
|
||||
// [7] through [14] are set cards
|
||||
// [15] is the assist card
|
||||
std::shared_ptr<parray<CardShortStatus, 0x10>> card_short_statuses;
|
||||
parray<CardShortStatus, 0x10> prev_card_short_statuses;
|
||||
|
||||
// In these arrays, [0] is the SC card and the rest are the set cards.
|
||||
std::shared_ptr<parray<ActionChainWithConds, 9>> set_card_action_chains;
|
||||
std::shared_ptr<parray<ActionMetadata, 9>> set_card_action_metadatas;
|
||||
parray<ActionChainWithConds, 9> prev_set_card_action_chains;
|
||||
parray<ActionMetadata, 9> prev_set_card_action_metadatas;
|
||||
|
||||
uint32_t num_destroyed_fcs;
|
||||
uint8_t unknown_a16;
|
||||
uint8_t unknown_a17;
|
||||
PlayerStats stats;
|
||||
};
|
||||
|
||||
|
||||
|
||||
} // namespace Episode3
|
||||
@@ -0,0 +1,547 @@
|
||||
#include "PlayerState.hh"
|
||||
|
||||
#include "Server.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace Episode3 {
|
||||
|
||||
|
||||
|
||||
Condition::Condition() {
|
||||
this->clear();
|
||||
}
|
||||
|
||||
void Condition::clear() {
|
||||
this->type = ConditionType::NONE;
|
||||
this->remaining_turns = 0;
|
||||
this->a_arg_value = 0;
|
||||
this->dice_roll_value = 0;
|
||||
this->flags = 0;
|
||||
this->card_definition_effect_index = 0;
|
||||
this->card_ref = 0xFFFF;
|
||||
this->value = 0;
|
||||
this->condition_giver_card_ref = 0xFFFF;
|
||||
this->random_percent = 0;
|
||||
this->value8 = 0;
|
||||
this->order = 0;
|
||||
this->unknown_a8 = 0;
|
||||
}
|
||||
|
||||
void Condition::clear_FF() {
|
||||
this->type = ConditionType::INVALID_FF;
|
||||
this->remaining_turns = 0xFF;
|
||||
this->a_arg_value = -1;
|
||||
this->dice_roll_value = 0xFF;
|
||||
this->flags = 0xFF;
|
||||
this->card_definition_effect_index = 0xFF;
|
||||
this->card_ref = 0xFFFF;
|
||||
this->value = -1;
|
||||
this->condition_giver_card_ref = 0xFFFF;
|
||||
this->random_percent = 0xFF;
|
||||
this->value8 = -1;
|
||||
this->order = 0xFF;
|
||||
this->unknown_a8 = 0xFF;
|
||||
}
|
||||
|
||||
|
||||
|
||||
EffectResult::EffectResult() {
|
||||
this->clear();
|
||||
}
|
||||
|
||||
void EffectResult::clear() {
|
||||
this->attacker_card_ref = 0xFFFF;
|
||||
this->target_card_ref = 0xFFFF;
|
||||
this->value = 0;
|
||||
this->current_hp = 0;
|
||||
this->ap = 0;
|
||||
this->tp = 0;
|
||||
this->flags = 0;
|
||||
this->operation = 0;
|
||||
this->condition_index = 0;
|
||||
this->dice_roll_value = 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
CardShortStatus::CardShortStatus() {
|
||||
this->clear();
|
||||
}
|
||||
|
||||
void CardShortStatus::clear() {
|
||||
this->card_ref = 0xFFFF;
|
||||
this->current_hp = 0;
|
||||
this->card_flags = 0;
|
||||
this->loc.clear();
|
||||
this->unused1 = 0xFFFF;
|
||||
this->max_hp = 0;
|
||||
this->unused2 = 0;
|
||||
}
|
||||
|
||||
void CardShortStatus::clear_FF() {
|
||||
this->card_ref = 0xFFFF;
|
||||
this->current_hp = 0xFFFF;
|
||||
this->card_flags = 0xFFFFFFFF;
|
||||
this->loc.clear_FF();
|
||||
this->unused1 = 0xFFFF;
|
||||
this->max_hp = -1;
|
||||
this->unused2 = 0xFF;
|
||||
}
|
||||
|
||||
|
||||
|
||||
ActionState::ActionState() {
|
||||
this->clear();
|
||||
}
|
||||
|
||||
void ActionState::clear() {
|
||||
this->client_id = 0xFFFF;
|
||||
this->unused = 0;
|
||||
this->facing_direction = Direction::RIGHT;
|
||||
this->attacker_card_ref = 0xFFFF;
|
||||
this->defense_card_ref = 0xFFFF;
|
||||
this->original_attacker_card_ref = 0xFFFF;
|
||||
this->target_card_refs.clear(0xFFFF);
|
||||
this->action_card_refs.clear(0xFFFF);
|
||||
}
|
||||
|
||||
|
||||
|
||||
ActionChain::ActionChain() {
|
||||
this->clear();
|
||||
}
|
||||
|
||||
void ActionChain::clear() {
|
||||
this->effective_ap = 0;
|
||||
this->effective_tp = 0;
|
||||
this->ap_effect_bonus = 0;
|
||||
this->damage = 0;
|
||||
this->acting_card_ref = 0xFFFF;
|
||||
this->unknown_card_ref_a3 = 0xFFFF;
|
||||
this->attack_action_card_ref_count = 0;
|
||||
this->attack_medium = AttackMedium::UNKNOWN;
|
||||
this->target_card_ref_count = 0;
|
||||
this->action_subphase = ActionSubphase::INVALID_FF;
|
||||
this->strike_count = 1;
|
||||
this->damage_multiplier = 1;
|
||||
this->attack_number = 0xFF;
|
||||
this->tp_effect_bonus = 0;
|
||||
this->unused1 = 0;
|
||||
this->unused2 = 0;
|
||||
this->card_ap = 0;
|
||||
this->card_tp = 0;
|
||||
this->flags = 0;
|
||||
this->attack_action_card_refs.clear(0xFFFF);
|
||||
this->target_card_refs.clear(0xFFFF);
|
||||
}
|
||||
|
||||
void ActionChain::clear_FF() {
|
||||
this->effective_ap = -1;
|
||||
this->effective_tp = -1;
|
||||
this->ap_effect_bonus = -1;
|
||||
this->damage = -1;
|
||||
this->acting_card_ref = 0xFFFF;
|
||||
this->unknown_card_ref_a3 = 0xFFFF;
|
||||
this->attack_action_card_refs.clear(0xFFFF);
|
||||
this->attack_action_card_ref_count = 0xFF;
|
||||
this->attack_medium = AttackMedium::INVALID_FF;
|
||||
this->target_card_ref_count = 0xFF;
|
||||
this->action_subphase = ActionSubphase::INVALID_FF;
|
||||
this->strike_count = 0xFF;
|
||||
this->damage_multiplier = -1;
|
||||
this->attack_number = 0xFF;
|
||||
this->tp_effect_bonus = -1;
|
||||
this->unused1 = 0xFF;
|
||||
this->unused2 = 0xFF;
|
||||
this->card_ap = -1;
|
||||
this->card_tp = -1;
|
||||
this->flags = 0xFFFFFFFF;
|
||||
this->target_card_refs.clear(0xFFFF);
|
||||
}
|
||||
|
||||
|
||||
|
||||
ActionChainWithConds::ActionChainWithConds() {
|
||||
this->clear();
|
||||
}
|
||||
|
||||
void ActionChainWithConds::clear() {
|
||||
this->chain.effective_ap = 0;
|
||||
this->chain.effective_tp = 0;
|
||||
this->chain.ap_effect_bonus = 0;
|
||||
this->chain.damage = 0;
|
||||
this->clear_inner();
|
||||
}
|
||||
|
||||
void ActionChainWithConds::clear_FF() {
|
||||
this->chain.clear_FF();
|
||||
for (size_t z = 0; z < 9; z++) {
|
||||
this->conditions[z].clear_FF();
|
||||
}
|
||||
}
|
||||
|
||||
void ActionChainWithConds::clear_inner() {
|
||||
this->chain.unknown_card_ref_a3 = 0xFFFF;
|
||||
this->chain.acting_card_ref = 0xFFFF;
|
||||
this->chain.attack_medium = AttackMedium::INVALID_FF;
|
||||
this->chain.flags = 0;
|
||||
this->chain.action_subphase = ActionSubphase::INVALID_FF;
|
||||
this->chain.attack_number = 0xFF;
|
||||
this->reset();
|
||||
this->clear_target_card_refs();
|
||||
this->chain.attack_action_card_ref_count = 0;
|
||||
this->chain.attack_action_card_refs.clear(0xFFFF);
|
||||
}
|
||||
|
||||
void ActionChainWithConds::clear_target_card_refs() {
|
||||
this->chain.target_card_ref_count = 0;
|
||||
this->chain.target_card_refs.clear(0xFFFF);
|
||||
}
|
||||
|
||||
void ActionChainWithConds::reset() {
|
||||
this->chain.effective_ap = 0;
|
||||
this->chain.effective_tp = 0;
|
||||
this->chain.ap_effect_bonus = 0;
|
||||
this->chain.tp_effect_bonus = 0;
|
||||
this->chain.unused1 = 0;
|
||||
this->chain.unused2 = 0;
|
||||
this->chain.damage = 0;
|
||||
this->chain.strike_count = 1;
|
||||
this->chain.damage_multiplier = 1;
|
||||
}
|
||||
|
||||
bool ActionChainWithConds::check_flag(uint32_t flags) const {
|
||||
return (this->chain.flags & flags) != 0;
|
||||
}
|
||||
|
||||
void ActionChainWithConds::clear_flags(uint32_t flags) {
|
||||
this->chain.flags &= ~flags;
|
||||
}
|
||||
|
||||
void ActionChainWithConds::set_flags(uint32_t flags) {
|
||||
this->chain.flags |= flags;
|
||||
}
|
||||
|
||||
void ActionChainWithConds::add_attack_action_card_ref(
|
||||
uint16_t card_ref, shared_ptr<Server> server) {
|
||||
if (card_ref != 0xFFFF) {
|
||||
this->chain.attack_action_card_refs[this->chain.attack_action_card_ref_count++] = card_ref;
|
||||
}
|
||||
this->set_flags(8);
|
||||
this->chain.action_subphase = server->get_current_action_subphase();
|
||||
}
|
||||
|
||||
void ActionChainWithConds::add_target_card_ref(uint16_t card_ref) {
|
||||
if (card_ref != 0xFFFF &&
|
||||
this->chain.target_card_ref_count < this->chain.target_card_refs.size()) {
|
||||
this->chain.target_card_refs[this->chain.target_card_ref_count++] = card_ref;
|
||||
}
|
||||
}
|
||||
|
||||
void ActionChainWithConds::compute_attack_medium(shared_ptr<Server> server) {
|
||||
this->chain.attack_medium = AttackMedium::PHYSICAL;
|
||||
for (size_t z = 0; z < this->chain.attack_action_card_ref_count; z++) {
|
||||
uint16_t card_ref = this->chain.attack_action_card_refs[z];
|
||||
if (card_ref == 0xFFFF) {
|
||||
break;
|
||||
}
|
||||
auto ce = server->definition_for_card_ref(card_ref);
|
||||
if (!ce) {
|
||||
continue;
|
||||
}
|
||||
if (card_class_is_tech_like(ce->def.card_class())) {
|
||||
this->chain.attack_medium = AttackMedium::TECH;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool ActionChainWithConds::get_condition_value(
|
||||
ConditionType cond_type,
|
||||
uint16_t card_ref,
|
||||
uint8_t def_effect_index,
|
||||
uint16_t value,
|
||||
uint16_t* out_value) const {
|
||||
bool any_found = false;
|
||||
uint8_t max_order = 10;
|
||||
for (size_t z = 0; z < 9; z++) {
|
||||
auto& cond = this->conditions[z];
|
||||
if (((cond_type == ConditionType::ANY) || (cond.type == cond_type)) &&
|
||||
((def_effect_index == 0xFF) || (cond.card_definition_effect_index == def_effect_index)) &&
|
||||
((card_ref == 0xFFFF) || (cond.card_ref == card_ref)) &&
|
||||
((value == 0xFFFF) || (cond.value == value))) {
|
||||
if (!any_found || (max_order < cond.order)) {
|
||||
if (!out_value) {
|
||||
return true;
|
||||
}
|
||||
*out_value = cond.value;
|
||||
max_order = cond.order;
|
||||
}
|
||||
any_found = true;
|
||||
}
|
||||
}
|
||||
return any_found;
|
||||
}
|
||||
|
||||
void ActionChainWithConds::set_action_subphase_from_card(
|
||||
shared_ptr<const Card> card) {
|
||||
this->chain.action_subphase = card->server()->get_current_action_subphase();
|
||||
}
|
||||
|
||||
bool ActionChainWithConds::unknown_8024DEC4() const {
|
||||
return this->check_flag(4) ? false : (this->chain.target_card_ref_count != 0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
ActionMetadata::ActionMetadata() {
|
||||
this->clear();
|
||||
}
|
||||
|
||||
void ActionMetadata::clear() {
|
||||
this->card_ref = 0xFFFF;
|
||||
this->target_card_ref_count = 0;
|
||||
this->defense_card_ref_count = 0;
|
||||
this->action_subphase = ActionSubphase::INVALID_FF;
|
||||
this->defense_power = 0;
|
||||
this->defense_bonus = 0;
|
||||
this->attack_bonus = 0;
|
||||
this->flags = 0;
|
||||
this->target_card_refs.clear(0xFFFF);
|
||||
this->defense_card_refs.clear(0xFFFF);
|
||||
this->original_attacker_card_refs.clear(0xFFFF);
|
||||
}
|
||||
|
||||
void ActionMetadata::clear_FF() {
|
||||
this->card_ref = 0xFFFF;
|
||||
this->target_card_ref_count = 0xFF;
|
||||
this->defense_card_ref_count = 0xFF;
|
||||
this->action_subphase = ActionSubphase::INVALID_FF;
|
||||
this->defense_power = -1;
|
||||
this->defense_bonus = -1;
|
||||
this->attack_bonus = -1;
|
||||
this->flags = 0xFFFFFFFF;
|
||||
this->target_card_refs.clear(0xFFFF);
|
||||
this->defense_card_refs.clear(0xFFFF);
|
||||
this->original_attacker_card_refs.clear(0xFFFF);
|
||||
}
|
||||
|
||||
bool ActionMetadata::check_flag(uint32_t mask) const {
|
||||
return (this->flags & mask) != 0;
|
||||
}
|
||||
|
||||
void ActionMetadata::set_flags(uint32_t flags) {
|
||||
this->flags |= flags;
|
||||
}
|
||||
|
||||
void ActionMetadata::clear_flags(uint32_t flags) {
|
||||
this->flags &= ~flags;
|
||||
}
|
||||
|
||||
void ActionMetadata::clear_defense_and_attacker_card_refs() {
|
||||
this->defense_card_ref_count = 0;
|
||||
this->defense_card_refs.clear(0xFFFF);
|
||||
this->original_attacker_card_refs.clear(0xFFFF);
|
||||
}
|
||||
|
||||
void ActionMetadata::clear_target_card_refs() {
|
||||
this->target_card_ref_count = 0;
|
||||
this->target_card_refs.clear(0xFFFF);
|
||||
}
|
||||
|
||||
void ActionMetadata::add_target_card_ref(uint16_t card_ref) {
|
||||
if (card_ref != 0xFFFF &&
|
||||
this->target_card_ref_count < this->target_card_refs.size()) {
|
||||
this->target_card_refs[this->target_card_ref_count++] = card_ref;
|
||||
}
|
||||
}
|
||||
|
||||
void ActionMetadata::add_defense_card_ref(
|
||||
uint16_t defense_card_ref,
|
||||
shared_ptr<Card> card,
|
||||
uint16_t original_attacker_card_ref) {
|
||||
if ((defense_card_ref != 0xFFFF) && (this->defense_card_ref_count < 8)) {
|
||||
this->defense_card_refs[this->defense_card_ref_count] = defense_card_ref;
|
||||
this->original_attacker_card_refs[this->defense_card_ref_count] = original_attacker_card_ref;
|
||||
this->defense_card_ref_count++;
|
||||
this->action_subphase = card->server()->get_current_action_subphase();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
HandAndEquipState::HandAndEquipState() {
|
||||
this->clear();
|
||||
}
|
||||
|
||||
void HandAndEquipState::clear() {
|
||||
this->dice_results.clear(0);
|
||||
this->atk_points = 0;
|
||||
this->def_points = 0;
|
||||
this->atk_points2 = 0;
|
||||
this->unknown_a1 = 0;
|
||||
this->total_set_cards_cost = 0;
|
||||
this->is_cpu_player = 0;
|
||||
this->assist_flags = 0;
|
||||
this->hand_card_refs.clear(0xFFFF);
|
||||
this->assist_card_ref = 0xFFFF;
|
||||
this->set_card_refs.clear(0xFFFF);
|
||||
this->sc_card_ref = 0xFFFF;
|
||||
this->hand_card_refs2.clear(0xFFFF);
|
||||
this->set_card_refs2.clear(0xFFFF);
|
||||
this->assist_card_ref2 = 0xFFFF;
|
||||
this->assist_card_set_number = 0;
|
||||
this->assist_card_id = 0xFFFF;
|
||||
this->assist_remaining_turns = 0;
|
||||
this->assist_delay_turns = 0;
|
||||
this->atk_bonuses = 0;
|
||||
this->def_bonuses = 0;
|
||||
this->unused2.clear(0);
|
||||
}
|
||||
|
||||
void HandAndEquipState::clear_FF() {
|
||||
this->dice_results.clear(0xFF);
|
||||
this->atk_points = 0xFF;
|
||||
this->def_points = 0xFF;
|
||||
this->atk_points2 = 0xFF;
|
||||
this->unknown_a1 = 0xFF;
|
||||
this->total_set_cards_cost = 0xFF;
|
||||
this->is_cpu_player = 0xFF;
|
||||
this->assist_flags = 0xFFFFFFFF;
|
||||
this->hand_card_refs.clear(0xFFFF);
|
||||
this->assist_card_ref = 0xFFFF;
|
||||
this->set_card_refs.clear(0xFFFF);
|
||||
this->sc_card_ref = 0xFFFF;
|
||||
this->hand_card_refs2.clear(0xFFFF);
|
||||
this->set_card_refs2.clear(0xFFFF);
|
||||
this->assist_card_ref2 = 0xFFFF;
|
||||
this->assist_card_set_number = 0xFFFF;
|
||||
this->assist_card_id = 0xFFFF;
|
||||
this->assist_remaining_turns = 0xFF;
|
||||
this->assist_delay_turns = 0xFF;
|
||||
this->atk_bonuses = 0xFF;
|
||||
this->def_bonuses = 0xFF;
|
||||
this->unused2.clear(0xFF);
|
||||
}
|
||||
|
||||
|
||||
|
||||
PlayerStats::PlayerStats() {
|
||||
this->clear();
|
||||
}
|
||||
|
||||
void PlayerStats::clear() {
|
||||
this->damage_given = 0;
|
||||
this->damage_taken = 0;
|
||||
this->num_opponent_cards_destroyed = 0;
|
||||
this->num_owned_cards_destroyed = 0;
|
||||
this->total_move_distance = 0;
|
||||
this->num_cards_set = 0;
|
||||
this->num_item_or_creature_cards_set = 0;
|
||||
this->num_attack_actions_set = 0;
|
||||
this->num_tech_cards_set = 0;
|
||||
this->num_assist_cards_set = 0;
|
||||
this->defense_actions_set_on_self = 0;
|
||||
this->defense_actions_set_on_ally = 0;
|
||||
this->num_cards_drawn = 0;
|
||||
this->max_attack_damage = 0;
|
||||
this->max_attack_combo_size = 0;
|
||||
this->num_attacks_given = 0;
|
||||
this->num_attacks_taken = 0;
|
||||
this->sc_damage_taken = 0;
|
||||
this->action_card_negated_damage = 0;
|
||||
this->unused = 0;
|
||||
}
|
||||
|
||||
float PlayerStats::score(size_t num_rounds) const {
|
||||
// Note: This formula doesn't match the formula on PSO-World, which is:
|
||||
// 35
|
||||
// + (Attack Damage - Damage Taken)
|
||||
// + (Max Card Combo x 3)
|
||||
// - (Story Character Damage x 1.8)
|
||||
// - (Turns x 2.7)
|
||||
// + (Action Card Negated Damage x 0.8)
|
||||
// I don't know where that formula came from, but this one came from the USA
|
||||
// Ep3 PsoV3.dol, so it's presumably correct. Is the PSO-World formula simply
|
||||
// incorrect, or is it from e.g. the Japanese version, which may have a
|
||||
// different rank calculation function?
|
||||
return 38.0f
|
||||
+ 0.8f * this->action_card_negated_damage
|
||||
- 2.3f * num_rounds
|
||||
- 1.8f * this->sc_damage_taken
|
||||
+ 3.0f * this->max_attack_combo_size
|
||||
+ (this->damage_given - this->damage_taken);
|
||||
}
|
||||
|
||||
uint8_t PlayerStats::rank(size_t num_rounds) const {
|
||||
return this->rank_for_score(this->score(num_rounds));
|
||||
}
|
||||
|
||||
const char* PlayerStats::rank_name(size_t num_rounds) const {
|
||||
return this->name_for_rank(this->rank_for_score(this->score(num_rounds)));
|
||||
}
|
||||
|
||||
constexpr size_t RANK_THRESHOLD_COUNT = 9;
|
||||
static const float RANK_THRESHOLDS[RANK_THRESHOLD_COUNT] = {
|
||||
15.0f, 25.0f, 30.0f, 40.0f, 50.0f, 60.0f, 65.0f, 75.0f, 85.0f};
|
||||
static const char* RANK_NAMES[RANK_THRESHOLD_COUNT + 1] = {
|
||||
"E", "D", "D+", "C", "C+", "B", "B+", "A", "A+", "S"};
|
||||
|
||||
uint8_t PlayerStats::rank_for_score(float score) {
|
||||
size_t rank = 0;
|
||||
while (rank < RANK_THRESHOLD_COUNT && RANK_THRESHOLDS[rank] <= score) {
|
||||
rank++;
|
||||
}
|
||||
return rank;
|
||||
}
|
||||
|
||||
const char* PlayerStats::name_for_rank(uint8_t rank) {
|
||||
if (rank >= RANK_THRESHOLD_COUNT + 1) {
|
||||
throw invalid_argument("invalid rank");
|
||||
}
|
||||
return RANK_NAMES[rank];
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
bool is_card_within_range(
|
||||
const parray<uint8_t, 9 * 9>& range,
|
||||
const Location& anchor_loc,
|
||||
const CardShortStatus& ss) {
|
||||
if (ss.card_ref == 0xFFFF) {
|
||||
return false;
|
||||
}
|
||||
if (range[0] == 2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((ss.loc.x < anchor_loc.x - 4) || (ss.loc.x > anchor_loc.x + 4)) {
|
||||
return false;
|
||||
}
|
||||
if ((ss.loc.y < anchor_loc.y - 4) || (ss.loc.y > anchor_loc.y + 4)) {
|
||||
return false;
|
||||
}
|
||||
return (range[(ss.loc.x - anchor_loc.x) + ((ss.loc.y - anchor_loc.y) + 4) * 9 + 4] != 0);
|
||||
}
|
||||
|
||||
vector<uint16_t> get_card_refs_within_range(
|
||||
const parray<uint8_t, 9 * 9>& range,
|
||||
const Location& loc,
|
||||
const parray<CardShortStatus, 0x10>& short_statuses) {
|
||||
vector<uint16_t> ret;
|
||||
if (is_card_within_range(range, loc, short_statuses[0])) {
|
||||
ret.emplace_back(short_statuses[0].card_ref);
|
||||
}
|
||||
for (size_t card_index = 7; card_index < 15; card_index++) {
|
||||
const auto& ss = short_statuses[card_index];
|
||||
if (is_card_within_range(range, loc, ss)) {
|
||||
ret.emplace_back(ss.card_ref);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
|
||||
} // namespace Episode3
|
||||
@@ -0,0 +1,265 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "../Text.hh"
|
||||
#include "DataIndex.hh"
|
||||
|
||||
namespace Episode3 {
|
||||
|
||||
|
||||
|
||||
class ServerBase;
|
||||
class Server;
|
||||
class Card;
|
||||
|
||||
struct Condition {
|
||||
ConditionType type;
|
||||
uint8_t remaining_turns;
|
||||
int8_t a_arg_value;
|
||||
uint8_t dice_roll_value;
|
||||
uint8_t flags;
|
||||
uint8_t card_definition_effect_index;
|
||||
le_uint16_t card_ref;
|
||||
le_int16_t value;
|
||||
le_uint16_t condition_giver_card_ref;
|
||||
uint8_t random_percent;
|
||||
int8_t value8;
|
||||
uint8_t order;
|
||||
uint8_t unknown_a8;
|
||||
|
||||
Condition();
|
||||
bool operator==(const Condition& other) const = default;
|
||||
bool operator!=(const Condition& other) const = default;
|
||||
|
||||
void clear();
|
||||
void clear_FF();
|
||||
} __attribute__((packed));
|
||||
|
||||
struct EffectResult {
|
||||
le_uint16_t attacker_card_ref;
|
||||
le_uint16_t target_card_ref;
|
||||
int8_t value;
|
||||
int8_t current_hp;
|
||||
int8_t ap;
|
||||
int8_t tp;
|
||||
uint8_t flags;
|
||||
int8_t operation; // May be a negative condition number
|
||||
uint8_t condition_index;
|
||||
uint8_t dice_roll_value;
|
||||
|
||||
EffectResult();
|
||||
bool operator==(const EffectResult& other) const = default;
|
||||
bool operator!=(const EffectResult& other) const = default;
|
||||
|
||||
void clear();
|
||||
} __attribute__((packed));
|
||||
|
||||
struct CardShortStatus {
|
||||
le_uint16_t card_ref;
|
||||
le_uint16_t current_hp;
|
||||
le_uint32_t card_flags;
|
||||
Location loc;
|
||||
le_uint16_t unused1;
|
||||
int8_t max_hp;
|
||||
uint8_t unused2;
|
||||
|
||||
CardShortStatus();
|
||||
bool operator==(const CardShortStatus& other) const = default;
|
||||
bool operator!=(const CardShortStatus& other) const = default;
|
||||
|
||||
void clear();
|
||||
void clear_FF();
|
||||
} __attribute__((packed));
|
||||
|
||||
struct ActionState {
|
||||
le_uint16_t client_id;
|
||||
uint8_t unused;
|
||||
Direction facing_direction;
|
||||
le_uint16_t attacker_card_ref;
|
||||
le_uint16_t defense_card_ref;
|
||||
parray<le_uint16_t, 4 * 9> target_card_refs;
|
||||
parray<le_uint16_t, 9> action_card_refs;
|
||||
le_uint16_t original_attacker_card_ref;
|
||||
|
||||
ActionState();
|
||||
bool operator==(const ActionState& other) const = default;
|
||||
bool operator!=(const ActionState& other) const = default;
|
||||
|
||||
void clear();
|
||||
} __attribute__((packed));
|
||||
|
||||
struct ActionChain {
|
||||
int8_t effective_ap;
|
||||
int8_t effective_tp;
|
||||
int8_t ap_effect_bonus;
|
||||
int8_t damage;
|
||||
le_uint16_t acting_card_ref;
|
||||
le_uint16_t unknown_card_ref_a3;
|
||||
parray<le_uint16_t, 8> attack_action_card_refs;
|
||||
uint8_t attack_action_card_ref_count;
|
||||
AttackMedium attack_medium;
|
||||
uint8_t target_card_ref_count;
|
||||
ActionSubphase action_subphase;
|
||||
uint8_t strike_count;
|
||||
int8_t damage_multiplier;
|
||||
uint8_t attack_number;
|
||||
int8_t tp_effect_bonus;
|
||||
uint8_t unused1;
|
||||
uint8_t unused2;
|
||||
int8_t card_ap;
|
||||
int8_t card_tp;
|
||||
le_uint32_t flags;
|
||||
parray<le_uint16_t, 4 * 9> target_card_refs;
|
||||
|
||||
ActionChain();
|
||||
bool operator==(const ActionChain& other) const = default;
|
||||
bool operator!=(const ActionChain& other) const = default;
|
||||
|
||||
void clear();
|
||||
void clear_FF();
|
||||
} __attribute__((packed));
|
||||
|
||||
struct ActionChainWithConds {
|
||||
ActionChain chain;
|
||||
parray<Condition, 9> conditions;
|
||||
|
||||
ActionChainWithConds();
|
||||
bool operator==(const ActionChainWithConds& other) const = default;
|
||||
bool operator!=(const ActionChainWithConds& other) const = default;
|
||||
|
||||
void clear();
|
||||
void clear_FF();
|
||||
void clear_inner();
|
||||
void clear_target_card_refs();
|
||||
void reset();
|
||||
|
||||
bool check_flag(uint32_t flags) const;
|
||||
void clear_flags(uint32_t flags);
|
||||
void set_flags(uint32_t flags);
|
||||
|
||||
void add_attack_action_card_ref(uint16_t card_ref, std::shared_ptr<Server> server);
|
||||
void add_target_card_ref(uint16_t card_ref);
|
||||
|
||||
void compute_attack_medium(std::shared_ptr<Server> server);
|
||||
bool get_condition_value(
|
||||
ConditionType cond_type,
|
||||
uint16_t card_ref,
|
||||
uint8_t def_effect_index,
|
||||
uint16_t value,
|
||||
uint16_t* out_value) const;
|
||||
|
||||
void set_action_subphase_from_card(std::shared_ptr<const Card> card);
|
||||
bool unknown_8024DEC4() const;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct ActionMetadata {
|
||||
le_uint16_t card_ref;
|
||||
uint8_t target_card_ref_count;
|
||||
uint8_t defense_card_ref_count;
|
||||
ActionSubphase action_subphase;
|
||||
int8_t defense_power;
|
||||
int8_t defense_bonus;
|
||||
int8_t attack_bonus;
|
||||
le_uint32_t flags;
|
||||
parray<le_uint16_t, 4 * 9> target_card_refs;
|
||||
parray<le_uint16_t, 8> defense_card_refs;
|
||||
parray<le_uint16_t, 8> original_attacker_card_refs;
|
||||
|
||||
ActionMetadata();
|
||||
bool operator==(const ActionMetadata& other) const = default;
|
||||
bool operator!=(const ActionMetadata& other) const = default;
|
||||
|
||||
void clear();
|
||||
void clear_FF();
|
||||
|
||||
bool check_flag(uint32_t mask) const;
|
||||
void set_flags(uint32_t flags);
|
||||
void clear_flags(uint32_t flags);
|
||||
|
||||
void clear_defense_and_attacker_card_refs();
|
||||
void clear_target_card_refs();
|
||||
void add_target_card_ref(uint16_t card_ref);
|
||||
void add_defense_card_ref(
|
||||
uint16_t defense_card_ref,
|
||||
std::shared_ptr<Card> card,
|
||||
uint16_t original_attacker_card_ref);
|
||||
} __attribute__((packed));
|
||||
|
||||
struct HandAndEquipState {
|
||||
parray<uint8_t, 2> dice_results;
|
||||
uint8_t atk_points;
|
||||
uint8_t def_points;
|
||||
uint8_t atk_points2; // TODO: rename this to something more appropriate
|
||||
uint8_t unknown_a1;
|
||||
uint8_t total_set_cards_cost;
|
||||
uint8_t is_cpu_player;
|
||||
le_uint32_t assist_flags;
|
||||
parray<le_uint16_t, 6> hand_card_refs;
|
||||
le_uint16_t assist_card_ref;
|
||||
parray<le_uint16_t, 8> set_card_refs;
|
||||
le_uint16_t sc_card_ref;
|
||||
parray<le_uint16_t, 6> hand_card_refs2;
|
||||
parray<le_uint16_t, 8> set_card_refs2;
|
||||
le_uint16_t assist_card_ref2;
|
||||
le_uint16_t assist_card_set_number;
|
||||
le_uint16_t assist_card_id;
|
||||
uint8_t assist_remaining_turns;
|
||||
uint8_t assist_delay_turns;
|
||||
uint8_t atk_bonuses;
|
||||
uint8_t def_bonuses;
|
||||
parray<uint8_t, 2> unused2;
|
||||
|
||||
HandAndEquipState();
|
||||
bool operator==(const HandAndEquipState& other) const = default;
|
||||
bool operator!=(const HandAndEquipState& other) const = default;
|
||||
|
||||
void clear();
|
||||
void clear_FF();
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PlayerStats {
|
||||
le_uint16_t damage_given;
|
||||
le_uint16_t damage_taken;
|
||||
le_uint16_t num_opponent_cards_destroyed;
|
||||
le_uint16_t num_owned_cards_destroyed;
|
||||
le_uint16_t total_move_distance;
|
||||
le_uint16_t num_cards_set;
|
||||
le_uint16_t num_item_or_creature_cards_set;
|
||||
le_uint16_t num_attack_actions_set;
|
||||
le_uint16_t num_tech_cards_set;
|
||||
le_uint16_t num_assist_cards_set;
|
||||
le_uint16_t defense_actions_set_on_self;
|
||||
le_uint16_t defense_actions_set_on_ally;
|
||||
le_uint16_t num_cards_drawn;
|
||||
le_uint16_t max_attack_damage;
|
||||
le_uint16_t max_attack_combo_size;
|
||||
le_uint16_t num_attacks_given;
|
||||
le_uint16_t num_attacks_taken;
|
||||
le_uint16_t sc_damage_taken;
|
||||
le_uint16_t action_card_negated_damage;
|
||||
le_uint16_t unused;
|
||||
|
||||
PlayerStats();
|
||||
void clear();
|
||||
|
||||
float score(size_t num_rounds) const;
|
||||
uint8_t rank(size_t num_rounds) const;
|
||||
const char* rank_name(size_t num_rounds) const;
|
||||
|
||||
static uint8_t rank_for_score(float score);
|
||||
static const char* name_for_rank(uint8_t rank);
|
||||
} __attribute__((packed));
|
||||
|
||||
|
||||
|
||||
std::vector<uint16_t> get_card_refs_within_range(
|
||||
const parray<uint8_t, 9 * 9>& range,
|
||||
const Location& loc,
|
||||
const parray<CardShortStatus, 0x10>& short_statuses);
|
||||
|
||||
|
||||
|
||||
} // namespace Episode3
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,232 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "DataIndex.hh"
|
||||
#include "PlayerState.hh"
|
||||
#include "DeckState.hh"
|
||||
#include "AssistServer.hh"
|
||||
|
||||
namespace Episode3 {
|
||||
|
||||
|
||||
|
||||
class Server;
|
||||
|
||||
void compute_effective_range(
|
||||
parray<uint8_t, 9 * 9>& ret,
|
||||
std::shared_ptr<const DataIndex> data_index,
|
||||
uint16_t card_id,
|
||||
const Location& loc,
|
||||
std::shared_ptr<const MapAndRulesState> map_and_rules);
|
||||
|
||||
bool card_linkage_is_valid(
|
||||
std::shared_ptr<const DataIndex::CardEntry> right_def,
|
||||
std::shared_ptr<const DataIndex::CardEntry> left_def,
|
||||
std::shared_ptr<const DataIndex::CardEntry> sc_def,
|
||||
bool has_permission_effect);
|
||||
|
||||
class RulerServer {
|
||||
public:
|
||||
struct MovePath {
|
||||
int32_t length;
|
||||
uint32_t remaining_distance;
|
||||
Location end_loc;
|
||||
parray<Location, 11> step_locs;
|
||||
uint32_t num_occupied_tiles;
|
||||
uint32_t cost;
|
||||
|
||||
MovePath();
|
||||
void add_step(const Location& loc);
|
||||
uint32_t get_cost() const;
|
||||
uint32_t get_length_plus1() const;
|
||||
void reset_totals();
|
||||
bool is_valid() const;
|
||||
};
|
||||
|
||||
explicit RulerServer(std::shared_ptr<Server> server);
|
||||
std::shared_ptr<Server> server();
|
||||
std::shared_ptr<const Server> server() const;
|
||||
|
||||
ActionChainWithConds* action_chain_with_conds_for_card_ref(
|
||||
uint16_t card_ref);
|
||||
const ActionChainWithConds* action_chain_with_conds_for_card_ref(
|
||||
uint16_t card_ref) const;
|
||||
bool any_attack_action_card_is_support_tech_or_support_pb(
|
||||
const ActionState& pa) const;
|
||||
bool card_has_pierce_or_rampage(
|
||||
uint8_t client_id,
|
||||
ConditionType cond_type,
|
||||
bool* out_has_rampage,
|
||||
uint16_t attacker_card_ref,
|
||||
uint16_t action_card_ref,
|
||||
uint8_t def_effect_index,
|
||||
AttackMedium attack_medium) const;
|
||||
bool attack_action_has_rampage_and_not_pierce(
|
||||
const ActionState& pa, uint16_t card_ref) const;
|
||||
bool attack_action_has_pierce_and_not_rampage(
|
||||
const ActionState& pa, uint8_t client_id);
|
||||
bool card_exists_by_status(const CardShortStatus& stat) const;
|
||||
bool card_has_mighty_knuckle(uint32_t card_ref) const;
|
||||
uint16_t card_id_for_card_ref(uint16_t card_ref) const;
|
||||
static bool card_id_is_boss_sc(uint16_t card_id);
|
||||
static bool card_id_is_support_tech_or_support_pb(uint16_t card_id);
|
||||
bool card_ref_can_attack(uint16_t card_ref);
|
||||
bool card_ref_can_move(
|
||||
uint8_t client_id, uint16_t card_ref, bool ignore_atk_points) const;
|
||||
bool card_ref_has_class_usability_condition(
|
||||
uint16_t card_ref) const;
|
||||
bool card_ref_has_free_maneuver(uint16_t card_ref) const;
|
||||
bool card_ref_is_aerial(uint16_t card_ref) const;
|
||||
bool card_ref_is_aerial_or_has_free_maneuver(
|
||||
uint16_t card_ref) const;
|
||||
bool card_ref_is_boss_sc(uint32_t card_ref) const;
|
||||
bool card_ref_or_any_set_card_has_condition_46(
|
||||
uint16_t card_ref) const;
|
||||
bool card_ref_or_sc_has_fixed_range(uint16_t card_ref) const;
|
||||
bool check_move_path_and_get_cost(
|
||||
uint8_t client_id,
|
||||
uint16_t card_ref,
|
||||
parray<uint8_t, 0x100>* visited_map,
|
||||
MovePath* out_path,
|
||||
uint32_t* out_cost) const;
|
||||
bool check_pierce_and_rampage(
|
||||
uint16_t card_ref,
|
||||
ConditionType cond_type,
|
||||
bool* out_has_pierce,
|
||||
uint16_t attacker_card_ref,
|
||||
uint16_t action_card_ref,
|
||||
uint8_t def_effect_index,
|
||||
AttackMedium attack_medium) const;
|
||||
bool check_usability_or_apply_condition_for_card_refs(
|
||||
uint16_t card_ref1,
|
||||
uint16_t card_ref2,
|
||||
uint16_t card_ref3,
|
||||
uint8_t def_effect_index,
|
||||
AttackMedium attack_medium) const;
|
||||
bool check_usability_or_condition_apply(
|
||||
uint8_t client_id1,
|
||||
uint16_t card_id1,
|
||||
uint8_t client_id2,
|
||||
uint16_t card_id2,
|
||||
uint16_t card_id3,
|
||||
uint8_t def_effect_index,
|
||||
bool is_condition_check,
|
||||
AttackMedium attack_medium) const;
|
||||
uint16_t compute_attack_or_defense_costs(
|
||||
const ActionState& pa,
|
||||
bool allow_mighty_knuckle,
|
||||
uint8_t* out_ally_cost) const;
|
||||
bool compute_effective_range_and_target_mode_for_attack(
|
||||
const ActionState& pa,
|
||||
uint16_t* out_effective_card_id,
|
||||
TargetMode* out_effective_target_mode,
|
||||
uint16_t* out_orig_card_ref) const;
|
||||
size_t count_rampage_targets_for_attack(
|
||||
const ActionState& pa, uint8_t client_id) const;
|
||||
bool defense_card_can_apply_to_attack(
|
||||
uint16_t defense_card_ref,
|
||||
uint16_t attacker_card_ref,
|
||||
uint16_t attacker_sc_card_ref) const;
|
||||
bool defense_card_matches_any_attack_card_top_color(
|
||||
const ActionState& pa) const;
|
||||
std::shared_ptr<const DataIndex::CardEntry> definition_for_card_ref(uint16_t card_ref) const;
|
||||
int32_t error_code_for_client_setting_card(
|
||||
uint8_t client_id,
|
||||
uint16_t card_ref,
|
||||
const Location* loc,
|
||||
uint8_t assist_target_client_id) const;
|
||||
bool find_condition_on_card_ref(
|
||||
uint16_t card_ref,
|
||||
ConditionType cond_type,
|
||||
Condition* out_se = nullptr,
|
||||
size_t* out_value_sum = nullptr,
|
||||
bool find_first_instead_of_max = false) const;
|
||||
bool flood_fill_move_path(
|
||||
const ActionChainWithConds& chain,
|
||||
int8_t x,
|
||||
int8_t y,
|
||||
Direction direction,
|
||||
uint8_t max_atk_points,
|
||||
int16_t max_distance,
|
||||
bool is_free_maneuver_or_aerial,
|
||||
bool is_aerial,
|
||||
parray<uint8_t, 0x100>* visited_map,
|
||||
MovePath* path,
|
||||
size_t num_occupied_tiles,
|
||||
size_t num_vacant_tiles) const;
|
||||
uint16_t get_ally_sc_card_ref(uint16_t card_ref) const;
|
||||
std::shared_ptr<const DataIndex::CardEntry> definition_for_card_id(
|
||||
uint32_t card_id) const;
|
||||
uint32_t get_card_id_with_effective_range(
|
||||
uint16_t card_ref, uint16_t card_id_override, TargetMode* out_target_mode) const;
|
||||
uint8_t get_card_ref_max_hp(uint16_t card_ref) const;
|
||||
bool get_creature_summon_area(
|
||||
uint8_t client_id, Location* out_loc, uint8_t* out_region_size) const;
|
||||
std::shared_ptr<HandAndEquipState> get_hand_and_equip_state_for_client_id(
|
||||
uint8_t client_id);
|
||||
std::shared_ptr<const HandAndEquipState> get_hand_and_equip_state_for_client_id(
|
||||
uint8_t client_id) const;
|
||||
bool get_move_path_length_and_cost(
|
||||
uint32_t client_id,
|
||||
uint32_t card_ref,
|
||||
const Location& loc,
|
||||
uint32_t* out_length,
|
||||
uint32_t* out_cost) const;
|
||||
ssize_t get_path_cost(
|
||||
const ActionChainWithConds& chain,
|
||||
ssize_t path_length,
|
||||
ssize_t cost_penalty) const;
|
||||
ActionType get_pending_action_type(const ActionState& pa) const;
|
||||
bool is_attack_valid(const ActionState& pa);
|
||||
bool is_attack_or_defense_valid(const ActionState& pa);
|
||||
bool is_card_ref_in_hand(uint16_t card_ref) const;
|
||||
bool is_defense_valid(const ActionState& pa);
|
||||
void link_objects(
|
||||
std::shared_ptr<MapAndRulesState> map_and_rules,
|
||||
std::shared_ptr<StateFlags> state_flags,
|
||||
std::shared_ptr<AssistServer> assist_server);
|
||||
size_t max_move_distance_for_card_ref(uint32_t card_ref) const;
|
||||
static void offsets_for_direction(
|
||||
const Location& loc, int32_t* out_x_offset, int32_t* out_y_offset);
|
||||
void register_player(
|
||||
uint8_t client_id,
|
||||
std::shared_ptr<HandAndEquipState> hes,
|
||||
std::shared_ptr<parray<CardShortStatus, 0x10>> short_statuses,
|
||||
std::shared_ptr<DeckEntry> deck_entry,
|
||||
std::shared_ptr<parray<ActionChainWithConds, 9>> set_card_action_chains,
|
||||
std::shared_ptr<parray<ActionMetadata, 9>> set_card_action_metadatas);
|
||||
void replace_D1_D2_rarity_cards_with_Attack(
|
||||
parray<le_uint16_t, 0x1F>& card_ids) const;
|
||||
AttackMedium get_attack_medium(const ActionState& pa) const;
|
||||
void set_client_team_id(uint8_t client_id, uint8_t team_id);
|
||||
int32_t set_cost_for_card(uint8_t client_id, uint16_t card_ref) const;
|
||||
const CardShortStatus* short_status_for_card_ref(uint16_t card_ref) const;
|
||||
bool should_allow_attacks_on_current_turn() const;
|
||||
int32_t verify_deck(
|
||||
const parray<le_uint16_t, 0x1F>& card_ids,
|
||||
const parray<uint8_t, 0x2F0>* owned_card_counts = nullptr) const;
|
||||
|
||||
private:
|
||||
std::weak_ptr<Server> w_server;
|
||||
|
||||
public:
|
||||
std::shared_ptr<HandAndEquipState> hand_and_equip_states[4];
|
||||
std::shared_ptr<parray<CardShortStatus, 0x10>> short_statuses[4];
|
||||
std::shared_ptr<DeckEntry> deck_entries[4];
|
||||
std::shared_ptr<parray<ActionChainWithConds, 9>> set_card_action_chains[4];
|
||||
std::shared_ptr<parray<ActionMetadata, 9>> set_card_action_metadatas[4];
|
||||
std::shared_ptr<MapAndRulesState> map_and_rules;
|
||||
std::shared_ptr<StateFlags> state_flags;
|
||||
std::shared_ptr<AssistServer> assist_server;
|
||||
parray<uint8_t, 4> team_id_for_client_id;
|
||||
int32_t error_code1;
|
||||
int32_t error_code2;
|
||||
int32_t error_code3;
|
||||
};
|
||||
|
||||
|
||||
|
||||
} // namespace Episode3
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,280 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "../Text.hh"
|
||||
#include "../CommandFormats.hh"
|
||||
#include "AssistServer.hh"
|
||||
#include "CardSpecial.hh"
|
||||
#include "MapState.hh"
|
||||
#include "PlayerState.hh"
|
||||
#include "RulerServer.hh"
|
||||
|
||||
struct Lobby;
|
||||
|
||||
namespace Episode3 {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* This implementation of Episode 3 battles (contained in all files in the
|
||||
* src/Episode3 directory, except for DataIndex.hh/cc) is derived from Sega's
|
||||
* original server implementation, reverse-engineered from the Episode 3 client
|
||||
* executable. The control flow, function breakdown, and structure definitions
|
||||
* in these files map very closely to how their server implementation was
|
||||
* written; notable differences (due to necessary environment differences or bug
|
||||
* fixes) are described in the comments therein.
|
||||
*
|
||||
* There are likely undiscovered bugs in this code, some originally written by
|
||||
* Sega, but more written by me as I manually transcribed and updated this code.
|
||||
*/
|
||||
|
||||
// Class ownership levels (classes may only contain weak_ptrs, not shared_ptrs,
|
||||
// to classes at the same or higher level):
|
||||
// - ServerBase
|
||||
// - - Server
|
||||
// - - - RulerServer
|
||||
// - - - - AssistServer
|
||||
// - - - - CardSpecial
|
||||
// - - - - - StateFlags
|
||||
// - - - - - DeckEntry
|
||||
// - - - - - PlayerState
|
||||
// - - - - - - Card
|
||||
// - - - - - - - CardShortStatus
|
||||
// - - - - - - - DeckState
|
||||
// - - - - - - - HandAndEquipState
|
||||
// - - - - - - - MapAndRulesState / OverlayState
|
||||
// - - - - - - - - Everything within DataIndex
|
||||
|
||||
class Server;
|
||||
|
||||
|
||||
|
||||
enum BehaviorFlag {
|
||||
SKIP_DECK_VERIFY = 0x00000001,
|
||||
IGNORE_CARD_COUNTS = 0x00000002,
|
||||
SKIP_D1_D2_REPLACE = 0x00000004,
|
||||
DISABLE_TIME_LIMITS = 0x00000008,
|
||||
};
|
||||
|
||||
class ServerBase : public std::enable_shared_from_this<ServerBase> {
|
||||
public:
|
||||
ServerBase(
|
||||
std::shared_ptr<Lobby> lobby,
|
||||
std::shared_ptr<const DataIndex> data_index,
|
||||
uint32_t behavior_flags,
|
||||
uint32_t random_seed);
|
||||
void init();
|
||||
void reset();
|
||||
void recreate_server();
|
||||
|
||||
struct PresenceEntry {
|
||||
uint8_t player_present;
|
||||
uint8_t deck_valid;
|
||||
uint8_t is_cpu_player;
|
||||
PresenceEntry();
|
||||
void clear();
|
||||
} __attribute__((packed));
|
||||
|
||||
std::weak_ptr<Lobby> lobby;
|
||||
std::shared_ptr<const DataIndex> data_index;
|
||||
uint32_t behavior_flags;
|
||||
uint32_t random_seed;
|
||||
|
||||
std::shared_ptr<MapAndRulesState> map_and_rules1;
|
||||
std::shared_ptr<MapAndRulesState> map_and_rules2;
|
||||
std::shared_ptr<DeckEntry> deck_entries[4];
|
||||
std::shared_ptr<Server> server;
|
||||
parray<PresenceEntry, 4> presence_entries;
|
||||
uint8_t num_clients_present;
|
||||
parray<NameEntry, 4> name_entries;
|
||||
parray<uint8_t, 4> name_entries_valid;
|
||||
OverlayState overlay_state;
|
||||
parray<parray<uint8_t, 0x2F0>, 4> client_card_counts;
|
||||
};
|
||||
|
||||
class Server : public std::enable_shared_from_this<Server> {
|
||||
public:
|
||||
explicit Server(std::shared_ptr<ServerBase> base);
|
||||
void init();
|
||||
std::shared_ptr<ServerBase> base();
|
||||
std::shared_ptr<const ServerBase> base() const;
|
||||
|
||||
template <typename T>
|
||||
void send(const T& cmd) const {
|
||||
if (cmd.header.size != sizeof(cmd) / 4) {
|
||||
throw std::logic_error("outbound command size field is incorrect");
|
||||
}
|
||||
if (cmd.header.subsubcommand == 0x06) {
|
||||
this->num_6xB4x06_commands_sent++;
|
||||
this->prev_num_6xB4x06_commands_sent = this->num_6xB4x06_commands_sent;
|
||||
if (this->num_6xB4x06_commands_sent > 0x100) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this->send(&cmd, cmd.header.size * 4);
|
||||
}
|
||||
void send(const void* data, size_t size) const;
|
||||
|
||||
void add_team_exp(uint8_t team_id, int32_t exp);
|
||||
bool advance_battle_phase();
|
||||
void action_phase_after();
|
||||
void draw_phase_before();
|
||||
std::shared_ptr<const DataIndex::CardEntry> definition_for_card_ref(uint16_t card_ref) const;
|
||||
std::shared_ptr<Card> card_for_set_card_ref(uint16_t card_ref);
|
||||
std::shared_ptr<const Card> card_for_set_card_ref(uint16_t card_ref) const;
|
||||
uint16_t card_id_for_card_ref(uint16_t card_ref) const;
|
||||
bool card_ref_is_empty_or_has_valid_card_id(uint16_t card_ref) const;
|
||||
bool check_for_battle_end();
|
||||
void check_for_destroyed_cards_and_send_6xB4x05_6xB4x02();
|
||||
bool check_presence_entry(uint8_t client_id);
|
||||
void clear_player_flags_after_dice_phase();
|
||||
void compute_all_map_occupied_bits();
|
||||
void compute_team_dice_boost(uint8_t team_id);
|
||||
void copy_player_states_to_prev_states();
|
||||
std::shared_ptr<const DataIndex::CardEntry> definition_for_card_id(uint16_t card_id) const;
|
||||
void destroy_cards_with_zero_hp();
|
||||
void determine_first_team_turn();
|
||||
void dice_phase_after();
|
||||
void set_phase_before();
|
||||
void draw_phase_after();
|
||||
void dice_phase_before();
|
||||
void end_attack_list_for_client(uint8_t client_id);
|
||||
void end_action_phase();
|
||||
bool enqueue_attack_or_defense(uint8_t client_id, ActionState* pa);
|
||||
BattlePhase get_battle_phase() const;
|
||||
ActionSubphase get_current_action_subphase() const;
|
||||
uint8_t get_current_team_turn() const;
|
||||
std::shared_ptr<PlayerState> get_player_state(uint8_t client_id);
|
||||
std::shared_ptr<const PlayerState> get_player_state(uint8_t client_id) const;
|
||||
uint32_t get_random(uint32_t max);
|
||||
float get_random_float_0_1();
|
||||
uint32_t get_round_num() const;
|
||||
SetupPhase get_setup_phase() const;
|
||||
uint32_t get_should_copy_prev_states_to_current_states() const;
|
||||
bool is_registration_complete() const;
|
||||
void move_phase_after();
|
||||
void action_phase_before();
|
||||
void send_6xB4x1C_names_update();
|
||||
int8_t send_6xB4x33_remove_ally_atk_if_needed(const ActionState& pa);
|
||||
void send_all_state_updates();
|
||||
void send_set_card_updates_and_6xB4x04_if_needed();
|
||||
void set_battle_ended();
|
||||
void set_battle_started();
|
||||
void set_client_id_ready_to_advance_phase(uint8_t client_id);
|
||||
void set_phase_after();
|
||||
void move_phase_before();
|
||||
void set_player_deck_valid(uint8_t client_id);
|
||||
void setup_and_start_battle();
|
||||
void update_battle_state_flags_and_send_6xB4x03_if_needed(
|
||||
bool always_send = false);
|
||||
bool update_registration_phase();
|
||||
void on_server_data_input(const std::string& data);
|
||||
void handle_6xB3x0B_mulligan_hand(const std::string& data);
|
||||
void handle_6xB3x0C_end_mulligan_phase(const std::string& data);
|
||||
void handle_6xB3x0D_end_non_action_phase(const std::string& data);
|
||||
void handle_6xB3x0E_discard_card_from_hand(const std::string& data);
|
||||
void handle_6xB3x0F_set_card_from_hand(const std::string& data);
|
||||
void handle_6xB3x10_move_fc_to_location(const std::string& data);
|
||||
void handle_6xB3x11_enqueue_attack_or_defense(const std::string& data);
|
||||
void handle_6xB3x12_end_attack_list(const std::string& data);
|
||||
void handle_6xB3x13_update_map_during_setup(const std::string& data);
|
||||
void handle_6xB3x14_update_deck_during_setup(const std::string& data);
|
||||
void handle_6xB3x15_unused_hard_reset_server_state(const std::string& data);
|
||||
void handle_6xB3x1B_update_player_name(const std::string& data);
|
||||
void handle_6xB3x1D_start_battle(const std::string& data);
|
||||
void handle_6xB3x21_end_battle(const std::string& data);
|
||||
void handle_6xB3x28_end_defense_list(const std::string& data);
|
||||
void handle_6xB3x2B_ignored(const std::string&);
|
||||
void handle_6xB3x34_subtract_ally_atk_points(const std::string& data);
|
||||
void handle_6xB3x37_client_ready_to_advance_from_starter_roll_phase(const std::string& data);
|
||||
void handle_6xB3x3A_ignored(const std::string& data);
|
||||
void handle_6xB3x40_map_list_request(const std::string& data);
|
||||
void handle_6xB3x41_map_request(const std::string& data);
|
||||
void handle_6xB3x48_end_turn(const std::string& data);
|
||||
void handle_6xB3x49_card_counts(const std::string& data);
|
||||
void unknown_8023D4E0(uint32_t flags);
|
||||
uint32_t get_team_exp(uint8_t team_id) const;
|
||||
uint32_t send_6xB4x06_if_card_ref_invalid(
|
||||
uint16_t card_ref, int16_t negative_value);
|
||||
void unknown_8023EEF4();
|
||||
void execute_bomb_assist_effect();
|
||||
void replace_targets_due_to_destruction_or_conditions(
|
||||
ActionState* as);
|
||||
bool any_target_exists_for_attack(const ActionState& as);
|
||||
uint8_t get_current_team_turn2() const;
|
||||
void unknown_8023EE48();
|
||||
void unknown_8023EE80();
|
||||
void unknown_802402F4();
|
||||
void send_6xB4x39() const;
|
||||
void send_6xB4x05(); // Recomputes the map occupied bits, so can't be const
|
||||
void send_6xB4x02_for_all_players_if_needed(bool always_send = false);
|
||||
void send_6xB4x50() const;
|
||||
|
||||
std::vector<std::shared_ptr<Card>> const_cast_set_cards_v(
|
||||
const std::vector<std::shared_ptr<const Card>>& cards);
|
||||
private:
|
||||
typedef void (Server::*handler_t)(const std::string&);
|
||||
static const std::unordered_map<uint8_t, handler_t> subcommand_handlers;
|
||||
|
||||
std::weak_ptr<ServerBase> w_base;
|
||||
|
||||
public:
|
||||
uint32_t battle_finished;
|
||||
uint32_t battle_in_progress;
|
||||
uint32_t round_num;
|
||||
BattlePhase battle_phase;
|
||||
uint8_t first_team_turn;
|
||||
uint8_t current_team_turn1;
|
||||
SetupPhase setup_phase;
|
||||
RegistrationPhase registration_phase;
|
||||
ActionSubphase action_subphase;
|
||||
uint8_t current_team_turn2;
|
||||
ActionState pending_attacks[0x20];
|
||||
uint32_t num_pending_attacks;
|
||||
parray<uint8_t, 4> client_done_enqueuing_attacks;
|
||||
parray<uint8_t, 4> player_ready_to_end_phase;
|
||||
std::shared_ptr<PSOV2Encryption> random_crypt;
|
||||
uint32_t unknown_a10;
|
||||
uint32_t overall_time_expired;
|
||||
// Note: In the original implementation, this is a uint32_t and is measured in
|
||||
// seconds. In our environment, the simplest implementation uses now(), which
|
||||
// returns microseconds, so we use a uint64_t instead.
|
||||
uint64_t battle_start_usecs;
|
||||
uint32_t should_copy_prev_states_to_current_states;
|
||||
std::shared_ptr<CardSpecial> card_special;
|
||||
std::shared_ptr<StateFlags> state_flags;
|
||||
std::shared_ptr<PlayerState> player_states[4];
|
||||
parray<uint32_t, 4> clients_done_in_mulligan_phase;
|
||||
uint32_t num_pending_attacks_with_cards;
|
||||
std::shared_ptr<Card> attack_cards[0x20];
|
||||
ActionState pending_attacks_with_cards[0x20];
|
||||
uint32_t unknown_a14;
|
||||
uint32_t unknown_a15;
|
||||
parray<uint32_t, 4> defense_list_ended_for_client;
|
||||
std::shared_ptr<AssistServer> assist_server;
|
||||
uint16_t next_assist_card_set_number;
|
||||
std::shared_ptr<RulerServer> ruler_server;
|
||||
parray<parray<parray<uint8_t, 2>, 2>, 5> warp_positions; // Array indexes are (type, end, x/y)
|
||||
parray<int16_t, 2> team_exp;
|
||||
parray<int16_t, 2> team_dice_boost;
|
||||
parray<uint32_t, 2> team_client_count;
|
||||
parray<uint32_t, 2> team_num_ally_fcs_destroyed;
|
||||
parray<uint32_t, 2> team_num_cards_destroyed;
|
||||
uint32_t hard_reset_flag;
|
||||
uint8_t tournament_flag;
|
||||
parray<uint8_t, 5> num_trap_tiles_of_type;
|
||||
parray<uint8_t, 5> chosen_trap_tile_index_of_type;
|
||||
parray<parray<parray<uint8_t, 2>, 8>, 5> trap_tile_locs;
|
||||
ActionState pb_action_states[4];
|
||||
parray<uint8_t, 4> has_done_pb;
|
||||
parray<parray<uint8_t, 4>, 4> has_done_pb_with_client;
|
||||
mutable uint32_t num_6xB4x06_commands_sent;
|
||||
mutable uint32_t prev_num_6xB4x06_commands_sent;
|
||||
};
|
||||
|
||||
|
||||
|
||||
} // namespace Episode3
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "Text.hh"
|
||||
#include "Quest.hh"
|
||||
#include "Items.hh"
|
||||
#include "Episode3/Server.hh"
|
||||
|
||||
struct Lobby {
|
||||
enum Flag {
|
||||
@@ -71,6 +72,7 @@ struct Lobby {
|
||||
uint32_t random_seed;
|
||||
std::shared_ptr<std::mt19937> random;
|
||||
std::shared_ptr<const CommonItemCreator> common_item_creator;
|
||||
std::shared_ptr<Episode3::ServerBase> ep3_server_base;
|
||||
|
||||
// lobby stuff
|
||||
uint8_t event;
|
||||
|
||||
+60
-28
@@ -148,6 +148,18 @@ void populate_state_from_config(shared_ptr<ServerState> s,
|
||||
s->episode_3_send_function_call_enabled = false;
|
||||
}
|
||||
|
||||
try {
|
||||
s->catch_handler_exceptions = d.at("CatchHandlerExceptions")->as_bool();
|
||||
} catch (const out_of_range&) {
|
||||
s->catch_handler_exceptions = true;
|
||||
}
|
||||
|
||||
try {
|
||||
s->ep3_behavior_flags = d.at("Episode3BehaviorFlags")->as_int();
|
||||
} catch (const out_of_range&) {
|
||||
s->ep3_behavior_flags = 0;
|
||||
}
|
||||
|
||||
shared_ptr<JSONObject> log_levels_json;
|
||||
try {
|
||||
log_levels_json = d.at("LogLevels");
|
||||
@@ -292,6 +304,8 @@ The options are:\n\
|
||||
--show-ep3-data\n\
|
||||
Print the Episode 3 data files (maps and card definitions) from the\n\
|
||||
system/ep3 directory in a human-readable format.\n\
|
||||
--show-ep3-card=ID\n\
|
||||
Describe the Episode 3 card with the given ID.\n\
|
||||
--replay-log\n\
|
||||
Replay a terminal log as if it were a client session. input-filename may\n\
|
||||
be specified for this option. This is used for regression testing, to\n\
|
||||
@@ -322,6 +336,7 @@ enum class Behavior {
|
||||
DECOMPRESS_BC0,
|
||||
ENCRYPT_DATA,
|
||||
DECRYPT_DATA,
|
||||
DECRYPT_TRIVIAL_DATA,
|
||||
FIND_DECRYPTION_SEED,
|
||||
DECODE_QUEST_FILE,
|
||||
DECODE_SJIS,
|
||||
@@ -339,6 +354,7 @@ static bool behavior_takes_input_filename(Behavior b) {
|
||||
(b == Behavior::DECOMPRESS_BC0) ||
|
||||
(b == Behavior::ENCRYPT_DATA) ||
|
||||
(b == Behavior::DECRYPT_DATA) ||
|
||||
(b == Behavior::DECRYPT_TRIVIAL_DATA) ||
|
||||
(b == Behavior::DECODE_QUEST_FILE) ||
|
||||
(b == Behavior::DECODE_SJIS) ||
|
||||
(b == Behavior::EXTRACT_GSL) ||
|
||||
@@ -353,6 +369,7 @@ static bool behavior_takes_output_filename(Behavior b) {
|
||||
(b == Behavior::DECOMPRESS_BC0) ||
|
||||
(b == Behavior::ENCRYPT_DATA) ||
|
||||
(b == Behavior::DECRYPT_DATA) ||
|
||||
(b == Behavior::DECRYPT_TRIVIAL_DATA) ||
|
||||
(b == Behavior::DECODE_SJIS);
|
||||
}
|
||||
|
||||
@@ -381,6 +398,7 @@ int main(int argc, char** argv) {
|
||||
const char* replay_required_access_key = "";
|
||||
const char* replay_required_password = "";
|
||||
uint32_t root_object_address = 0;
|
||||
uint16_t ep3_card_id = 0xFFFF;
|
||||
struct sockaddr_storage cat_client_remote;
|
||||
for (int x = 1; x < argc; x++) {
|
||||
if (!strcmp(argv[x], "--help")) {
|
||||
@@ -398,6 +416,8 @@ int main(int argc, char** argv) {
|
||||
behavior = Behavior::ENCRYPT_DATA;
|
||||
} else if (!strcmp(argv[x], "--decrypt-data")) {
|
||||
behavior = Behavior::DECRYPT_DATA;
|
||||
} else if (!strcmp(argv[x], "--decrypt-trivial-data")) {
|
||||
behavior = Behavior::DECRYPT_TRIVIAL_DATA;
|
||||
} else if (!strcmp(argv[x], "--find-decryption-seed")) {
|
||||
behavior = Behavior::FIND_DECRYPTION_SEED;
|
||||
} else if (!strcmp(argv[x], "--decode-sjis")) {
|
||||
@@ -446,6 +466,9 @@ int main(int argc, char** argv) {
|
||||
skip_big_endian = true;
|
||||
} else if (!strcmp(argv[x], "--show-ep3-data")) {
|
||||
behavior = Behavior::SHOW_EP3_DATA;
|
||||
} else if (!strncmp(argv[x], "--show-ep3-card=", 16)) {
|
||||
behavior = Behavior::SHOW_EP3_DATA;
|
||||
ep3_card_id = strtoul(&argv[x][16], nullptr, 16);
|
||||
} else if (!strcmp(argv[x], "--parse-object-graph")) {
|
||||
behavior = Behavior::PARSE_OBJECT_GRAPH;
|
||||
} else if (!strcmp(argv[x], "--replay-log")) {
|
||||
@@ -476,10 +499,14 @@ int main(int argc, char** argv) {
|
||||
auto read_input_data = [&]() -> string {
|
||||
string data;
|
||||
if (input_filename && strcmp(input_filename, "-")) {
|
||||
return load_file(input_filename);
|
||||
data = load_file(input_filename);
|
||||
} else {
|
||||
return read_all(stdin);
|
||||
data = read_all(stdin);
|
||||
}
|
||||
if (parse_data) {
|
||||
data = parse_data_string(data);
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
auto write_output_data = [&](const void* data, size_t size) {
|
||||
@@ -500,10 +527,6 @@ int main(int argc, char** argv) {
|
||||
case Behavior::COMPRESS_BC0:
|
||||
case Behavior::DECOMPRESS_BC0: {
|
||||
string data = read_input_data();
|
||||
if (parse_data) {
|
||||
data = parse_data_string(data);
|
||||
}
|
||||
|
||||
size_t input_bytes = data.size();
|
||||
if (behavior == Behavior::COMPRESS_PRS) {
|
||||
data = prs_compress(data);
|
||||
@@ -548,9 +571,6 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
|
||||
string data = read_input_data();
|
||||
if (parse_data) {
|
||||
data = parse_data_string(data);
|
||||
}
|
||||
|
||||
size_t original_size = data.size();
|
||||
data.resize((data.size() + 7) & (~7), '\0');
|
||||
@@ -584,6 +604,14 @@ int main(int argc, char** argv) {
|
||||
break;
|
||||
}
|
||||
|
||||
case Behavior::DECRYPT_TRIVIAL_DATA: {
|
||||
uint8_t basis = stoul(seed, nullptr, 16);
|
||||
string data = read_input_data();
|
||||
decrypt_trivial_gci_data(data.data(), data.size(), basis);
|
||||
write_output_data(data.data(), data.size());
|
||||
break;
|
||||
}
|
||||
|
||||
case Behavior::FIND_DECRYPTION_SEED: {
|
||||
if (find_decryption_seed_plaintexts.empty() || !find_decryption_seed_ciphertext) {
|
||||
throw runtime_error("both --encrypted and --decrypted must be specified");
|
||||
@@ -681,9 +709,6 @@ int main(int argc, char** argv) {
|
||||
|
||||
case Behavior::DECODE_SJIS: {
|
||||
string data = read_input_data();
|
||||
if (parse_data) {
|
||||
data = parse_data_string(data);
|
||||
}
|
||||
auto decoded = decode_sjis(data);
|
||||
write_output_data(decoded.data(), decoded.size() * sizeof(decoded[0]));
|
||||
break;
|
||||
@@ -725,26 +750,33 @@ int main(int argc, char** argv) {
|
||||
|
||||
case Behavior::SHOW_EP3_DATA: {
|
||||
config_log.info("Collecting Episode 3 data");
|
||||
Ep3DataIndex index("system/ep3", true);
|
||||
Episode3::DataIndex index("system/ep3", true);
|
||||
|
||||
auto map_ids = index.all_map_ids();
|
||||
log_info("%zu maps", map_ids.size());
|
||||
for (uint32_t map_id : map_ids) {
|
||||
auto map = index.get_map(map_id);
|
||||
string name = map->map.name;
|
||||
string location = map->map.location_name;
|
||||
log_info("(Map %08" PRIX32 ") %s @ %s", map_id, name.c_str(), location.c_str());
|
||||
// TODO: Print more information about the map here
|
||||
}
|
||||
if (ep3_card_id == 0xFFFF) {
|
||||
auto map_ids = index.all_map_ids();
|
||||
log_info("%zu maps", map_ids.size());
|
||||
for (uint32_t map_id : map_ids) {
|
||||
auto map = index.definition_for_map_number(map_id);
|
||||
string s = map->map.str(&index);
|
||||
fprintf(stdout, "%s\n", s.c_str());
|
||||
}
|
||||
|
||||
auto card_ids = index.all_card_ids();
|
||||
log_info("%zu card definitions", card_ids.size());
|
||||
for (uint32_t card_id : card_ids) {
|
||||
auto entry = index.get_card_definition(card_id);
|
||||
auto card_ids = index.all_card_ids();
|
||||
log_info("%zu card definitions", card_ids.size());
|
||||
for (uint32_t card_id : card_ids) {
|
||||
auto entry = index.definition_for_card_id(card_id);
|
||||
string s = entry->def.str();
|
||||
string tags = entry->debug_tags.empty() ? "(none)" : join(entry->debug_tags, ", ");
|
||||
string text = entry->text.empty() ? "(No text available)" : entry->text;
|
||||
fprintf(stdout, "%s\nTags: %s\n%s\n\n", s.c_str(), tags.c_str(), text.c_str());
|
||||
}
|
||||
|
||||
} else {
|
||||
auto entry = index.definition_for_card_id(ep3_card_id);
|
||||
string s = entry->def.str();
|
||||
string tags = entry->debug_tags.empty() ? "(none)" : join(entry->debug_tags, ", ");
|
||||
string text = entry->text.empty() ? "(No text available)" : entry->text;
|
||||
log_info("%s\nTags: %s\n%s\n", s.c_str(), tags.c_str(), text.c_str());
|
||||
fprintf(stdout, "%s\nTags: %s\n%s\n", s.c_str(), tags.c_str(), text.c_str());
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -824,7 +856,7 @@ int main(int argc, char** argv) {
|
||||
state->load_bb_file("ItemRT.rel")));
|
||||
|
||||
config_log.info("Collecting Episode 3 data");
|
||||
state->ep3_data_index.reset(new Ep3DataIndex("system/ep3"));
|
||||
state->ep3_data_index.reset(new Episode3::DataIndex("system/ep3"));
|
||||
|
||||
config_log.info("Collecting quest metadata");
|
||||
state->quest_index.reset(new QuestIndex("system/quests"));
|
||||
|
||||
+12
-1
@@ -833,4 +833,15 @@ shared_ptr<PSOBBEncryption> PSOBBMultiKeyImitatorEncryption::ensure_crypt() {
|
||||
}
|
||||
}
|
||||
return this->active_crypt;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
void decrypt_trivial_gci_data(void* data, size_t size, uint8_t basis) {
|
||||
uint8_t* bytes = reinterpret_cast<uint8_t*>(data);
|
||||
uint8_t key = basis + 0x80;
|
||||
for (size_t z = 0; z < size; z++) {
|
||||
key = (key * 5) + 1;
|
||||
bytes[z] ^= key;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,3 +226,7 @@ protected:
|
||||
std::string seed;
|
||||
bool jsd1_use_detector_seed;
|
||||
};
|
||||
|
||||
|
||||
|
||||
void decrypt_trivial_gci_data(void* data, size_t size, uint8_t basis);
|
||||
|
||||
+3
-3
@@ -11,7 +11,7 @@
|
||||
#include "LevelTable.hh"
|
||||
#include "Version.hh"
|
||||
#include "Text.hh"
|
||||
#include "Episode3.hh"
|
||||
#include "Episode3/DataIndex.hh"
|
||||
|
||||
|
||||
|
||||
@@ -407,7 +407,7 @@ struct PSOPlayerDataGCEp3 { // For command 61
|
||||
parray<le_uint32_t, 0x1E> blocked_senders;
|
||||
le_uint32_t auto_reply_enabled;
|
||||
char auto_reply[0xAC];
|
||||
Ep3Config ep3_config;
|
||||
Episode3::PlayerConfig ep3_config;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PSOPlayerDataBB { // For command 61
|
||||
@@ -506,7 +506,7 @@ public:
|
||||
std::unique_ptr<PendingCardTrade> pending_card_trade;
|
||||
|
||||
// Null unless the client is Episode 3 and has sent its config already
|
||||
std::shared_ptr<Ep3Config> ep3_config;
|
||||
std::shared_ptr<Episode3::PlayerConfig> ep3_config;
|
||||
|
||||
// These are only used if the client is BB
|
||||
std::string bb_username;
|
||||
|
||||
@@ -878,9 +878,9 @@ static HandlerResult S_6x(shared_ptr<ServerState>,
|
||||
string map_data = prs_decompress(
|
||||
data.data() + sizeof(cmd), data.size() - sizeof(cmd));
|
||||
save_file(filename, map_data);
|
||||
if (map_data.size() != sizeof(Ep3Map)) {
|
||||
if (map_data.size() != sizeof(Episode3::MapDefinition)) {
|
||||
session.log.warning("Wrote %zu bytes to %s (expected %zu bytes; the file may be invalid)",
|
||||
map_data.size(), filename.c_str(), sizeof(Ep3Map));
|
||||
map_data.size(), filename.c_str(), sizeof(Episode3::MapDefinition));
|
||||
} else {
|
||||
session.log.info("Wrote %zu bytes to %s", map_data.size(), filename.c_str());
|
||||
}
|
||||
@@ -1016,7 +1016,7 @@ static HandlerResult S_13_A7(shared_ptr<ServerState>,
|
||||
if (sf->f.get()) {
|
||||
session.log.info("Writing %" PRIu32 " bytes to %s",
|
||||
cmd.data_size.load(), sf->output_filename.c_str());
|
||||
fwritex(sf->f.get(), cmd.data, cmd.data_size);
|
||||
fwritex(sf->f.get(), cmd.data.data(), cmd.data_size);
|
||||
}
|
||||
sf->remaining_bytes -= cmd.data_size;
|
||||
|
||||
|
||||
+6
-13
@@ -459,10 +459,10 @@ Quest::Quest(const string& bin_filename)
|
||||
case GameVersion::XB:
|
||||
case GameVersion::GC: {
|
||||
if (this->category == QuestCategory::EPISODE_3) {
|
||||
if (bin_decompressed.size() != sizeof(Ep3Map)) {
|
||||
if (bin_decompressed.size() != sizeof(Episode3::MapDefinition)) {
|
||||
throw invalid_argument("file is incorrect size");
|
||||
}
|
||||
auto* header = reinterpret_cast<const Ep3Map*>(bin_decompressed.data());
|
||||
auto* header = reinterpret_cast<const Episode3::MapDefinition*>(bin_decompressed.data());
|
||||
this->joinable = false;
|
||||
this->episode = 0xFF;
|
||||
this->name = decode_sjis(header->name);
|
||||
@@ -677,21 +677,14 @@ string Quest::decode_gci(
|
||||
// wrote a fairly trivial XOR loop over the first 0x100 bytes, leaving the
|
||||
// remaining bytes completely unencrypted (but still compressed).
|
||||
size_t unscramble_size = min<size_t>(0x100, data.size());
|
||||
{
|
||||
uint8_t key = 0x80; // Technically basis + 0x80, but basis is zero
|
||||
for (size_t z = 0; z < unscramble_size; z++) {
|
||||
key = (key * 5) + 1;
|
||||
data[z] ^= key;
|
||||
}
|
||||
}
|
||||
decrypt_trivial_gci_data(data.data(), unscramble_size, 0);
|
||||
|
||||
size_t decompressed_size = prs_decompress_size(data);
|
||||
if (decompressed_size != sizeof(Ep3Map)) {
|
||||
if (decompressed_size != sizeof(Episode3::MapDefinition)) {
|
||||
throw runtime_error(string_printf(
|
||||
"decompressed quest is 0x%zX bytes; expected 0x%zX bytes",
|
||||
decompressed_size, sizeof(Ep3Map)));
|
||||
decompressed_size, sizeof(Episode3::MapDefinition)));
|
||||
}
|
||||
|
||||
return data;
|
||||
|
||||
} else {
|
||||
@@ -800,7 +793,7 @@ static pair<string, string> decode_qst_t(FILE* f) {
|
||||
if (header.flag != dest->size() / 0x400) {
|
||||
throw runtime_error("qst contains chunks out of order");
|
||||
}
|
||||
dest->append(reinterpret_cast<const char*>(cmd.data), cmd.data_size);
|
||||
dest->append(reinterpret_cast<const char*>(cmd.data.data()), cmd.data_size);
|
||||
|
||||
} else {
|
||||
throw runtime_error("invalid command in qst file");
|
||||
|
||||
+13
-87
@@ -907,98 +907,24 @@ static void on_ep3_server_data_request(shared_ptr<ServerState> s, shared_ptr<Cli
|
||||
throw runtime_error("Episode 3 server data request sent in lobby or non-Episode 3 game");
|
||||
}
|
||||
|
||||
const auto& header = check_size_t<G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5>(
|
||||
data, sizeof(G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5), 0xFFFF);
|
||||
const auto& header = check_size_t<G_CardBattleCommandHeader>(
|
||||
data, sizeof(G_CardBattleCommandHeader), 0xFFFF);
|
||||
|
||||
// TODO: We can support this since set_mask_for_ep3_game_command already
|
||||
// exists; I just don't want to make a copy of the data string
|
||||
if (header.mask_key != 0) {
|
||||
throw runtime_error("Episode 3 server data request has nonzero mask key");
|
||||
}
|
||||
|
||||
if (header.basic_header.subcommand != 0xB3) {
|
||||
if (header.subcommand != 0xB3) {
|
||||
throw runtime_error("unknown Episode 3 server data request");
|
||||
}
|
||||
|
||||
switch (header.subsubcommand) {
|
||||
// Phase 1: map select
|
||||
case 0x40:
|
||||
check_size_t<G_MapListRequest_GC_Ep3_6xB3x40_CAx40>(data);
|
||||
send_ep3_map_list(s, l);
|
||||
break;
|
||||
case 0x41: {
|
||||
const auto& cmd = check_size_t<G_MapDataRequest_GC_Ep3_6xB3x41_CAx41>(data);
|
||||
send_ep3_map_data(s, l, cmd.map_number);
|
||||
break;
|
||||
if (!l->ep3_server_base || l->ep3_server_base->server->battle_finished) {
|
||||
if (!l->ep3_server_base) {
|
||||
l->log.info("Creating Episode 3 server state");
|
||||
} else {
|
||||
l->log.info("Recreating Episode 3 server state");
|
||||
}
|
||||
|
||||
/* What follows is some raw code that has survived since the days of khyller
|
||||
* (approx. 2004). Much more research and engineering is needed to get
|
||||
* Episode III battles to work, but this could be used as a starting point.
|
||||
|
||||
// phase 2: deck/name entry
|
||||
case 0x13:
|
||||
ti = FindTeam(s, c->teamID);
|
||||
memcpy(&ti->ep3game, ((DWORD)c->bufferin + 0x14), 0x2AC);
|
||||
CommandEp3InitChangeState(s, c, 1);
|
||||
break;
|
||||
case 0x1B:
|
||||
ti = FindTeam(s, c->teamID);
|
||||
memcpy(&ti->ep3names[*(BYTE*)((DWORD)c->bufferin + 0x24)], ((DWORD)c->bufferin + 0x14), 0x14); // NOTICE: may be 0x26 instead of 0x24
|
||||
CommandEp3InitSendNames(s, c);
|
||||
break;
|
||||
case 0x14:
|
||||
ti = FindTeam(s, c->teamID);
|
||||
memcpy(&ti->ep3decks[*(BYTE*)((DWORD)c->bufferin + 0x14)], ((DWORD)c->bufferin + 0x18), 0x58); // NOTICE: may be 0x16 instead of 0x14
|
||||
Ep3FillHand(&ti->ep3game, &ti->ep3decks[*(BYTE*)((DWORD)c->bufferin + 0x14)], &ti->ep3pcs[*(BYTE*)((DWORD)c->bufferin + 0x14)]);
|
||||
//Ep3RollDice(&ti->ep3game, &ti->ep3pcs[*(BYTE*)((DWORD)c->bufferin + 0x14)]);
|
||||
CommandEp3InitSendDecks(s, c);
|
||||
CommandEp3InitSendMapLayout(s, c);
|
||||
for (x = 0, param = 0; x < 4; x++) if ((ti->ep3decks[x].clientID != 0xFFFFFFFF) && (ti->ep3names[x].clientID != 0xFF)) param++;
|
||||
if (param >= ti->ep3game.numPlayers) CommandEp3InitChangeState(s, c, 3);
|
||||
break;
|
||||
// phase 3: hands & game states
|
||||
case 0x1D:
|
||||
ti = FindTeam(s, c->teamID);
|
||||
Ep3ReprocessMap(&ti->ep3game);
|
||||
CommandEp3SendMapData(s, c, ti->ep3game.mapID);
|
||||
for (y = 0, x = 0; x < 4; x++)
|
||||
{
|
||||
if ((ti->ep3decks[x].clientID == 0xFFFFFFFF) || (ti->ep3names[x].clientID == 0xFF)) continue;
|
||||
Ep3EquipCard(&ti->ep3game, &ti->ep3decks[x], &ti->ep3pcs[x], 0); // equip SC card
|
||||
CommandEp3InitHandUpdate(s, c, x);
|
||||
CommandEp3InitStatUpdate(s, c, x);
|
||||
y++;
|
||||
}
|
||||
CommandEp3Init_B4_06(s, c, (y == 4) ? true : false);
|
||||
CommandEp3InitSendMapLayout(s, c);
|
||||
for (x = 0; x < 4; x++)
|
||||
{
|
||||
if ((ti->ep3decks[x].clientID == 0xFFFFFFFF) || (ti->ep3names[x].clientID == 0xFF)) continue;
|
||||
CommandEp3Init_B4_4E(s, c, x);
|
||||
CommandEp3Init_B4_4C(s, c, x);
|
||||
CommandEp3Init_B4_4D(s, c, x);
|
||||
CommandEp3Init_B4_4F(s, c, x);
|
||||
}
|
||||
CommandEp3InitSendDecks(s, c);
|
||||
CommandEp3InitSendMapLayout(s, c);
|
||||
for (x = 0; x < 4; x++)
|
||||
{
|
||||
if ((ti->ep3decks[x].clientID == 0xFFFFFFFF) || (ti->ep3names[x].clientID == 0xFF)) continue;
|
||||
CommandEp3InitHandUpdate(s, c, x);
|
||||
}
|
||||
CommandEp3InitSendNames(s, c);
|
||||
CommandEp3InitChangeState(s, c, 4);
|
||||
CommandEp3Init_B4_50(s, c);
|
||||
CommandEp3InitSendMapLayout(s, c);
|
||||
CommandEp3Init_B4_39(s, c); // MISSING: 60 00 AC 00 B4 2A 00 00 39 56 00 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
|
||||
CommandEp3InitBegin(s, c);
|
||||
break; */
|
||||
|
||||
default:
|
||||
c->log.warning("Unknown Episode III server data request: %02X",
|
||||
header.subsubcommand);
|
||||
l->ep3_server_base = make_shared<Episode3::ServerBase>(
|
||||
l, s->ep3_data_index, s->ep3_behavior_flags, l->random_seed);
|
||||
l->ep3_server_base->init();
|
||||
}
|
||||
l->ep3_server_base->server->on_server_data_input(data);
|
||||
}
|
||||
|
||||
static void on_ep3_tournament_control(shared_ptr<ServerState>, shared_ptr<Client> c,
|
||||
@@ -1860,7 +1786,7 @@ static void on_player_data(shared_ptr<ServerState> s, shared_ptr<Client> c,
|
||||
throw runtime_error("non-Episode 3 client sent Episode 3 player data");
|
||||
}
|
||||
const auto* pd3 = &check_size_t<PSOPlayerDataGCEp3>(data);
|
||||
c->game_data.ep3_config.reset(new Ep3Config(pd3->ep3_config));
|
||||
c->game_data.ep3_config.reset(new Episode3::PlayerConfig(pd3->ep3_config));
|
||||
pd = reinterpret_cast<const PSOPlayerDataV3*>(pd3);
|
||||
} else {
|
||||
pd = &check_size_t<PSOPlayerDataV3>(data, sizeof(PSOPlayerDataV3),
|
||||
|
||||
@@ -209,8 +209,8 @@ static void on_subcommand_forward_check_size_ep3_game(shared_ptr<ServerState>,
|
||||
static void on_subcommand_ep3_battle_subs(shared_ptr<ServerState>,
|
||||
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
|
||||
const string& orig_data) {
|
||||
check_size_sc<G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5>(
|
||||
orig_data, sizeof(G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5), 0xFFFF);
|
||||
check_size_sc<G_CardBattleCommandHeader>(
|
||||
orig_data, sizeof(G_CardBattleCommandHeader), 0xFFFF);
|
||||
if (!l->is_game() || !(l->flags & Lobby::Flag::EPISODE_3_ONLY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
+4
-26
@@ -303,7 +303,7 @@ void send_quest_buffer_overflow(
|
||||
|
||||
S_WriteFile_13_A7 cmd;
|
||||
cmd.filename = filename;
|
||||
memcpy(cmd.data, fn->code.data(), fn->code.size());
|
||||
memcpy(cmd.data.data(), fn->code.data(), fn->code.size());
|
||||
if (fn->code.size() < 0x400) {
|
||||
memset(&cmd.data[fn->code.size()], 0, 0x400 - fn->code.size());
|
||||
}
|
||||
@@ -1719,28 +1719,6 @@ void send_ep3_rank_update(shared_ptr<Client> c) {
|
||||
send_command_t(c, 0xB7, 0x00, cmd);
|
||||
}
|
||||
|
||||
void send_ep3_map_list(shared_ptr<ServerState> s, shared_ptr<Lobby> l) {
|
||||
const auto& data = s->ep3_data_index->get_compressed_map_list();
|
||||
|
||||
StringWriter w;
|
||||
uint32_t subcommand_size = (data.size() + sizeof(G_MapList_GC_Ep3_6xB6x40) + 3) & (~3);
|
||||
w.put<G_MapList_GC_Ep3_6xB6x40>(
|
||||
G_MapList_GC_Ep3_6xB6x40{{{{0xB6, 0, 0}, subcommand_size}, 0x40, {}}, data.size(), 0});
|
||||
w.write(data);
|
||||
send_command(l, 0x6C, 0x00, w.str());
|
||||
}
|
||||
|
||||
void send_ep3_map_data(shared_ptr<ServerState> s, shared_ptr<Lobby> l, uint32_t map_id) {
|
||||
auto entry = s->ep3_data_index->get_map(map_id);
|
||||
const auto& compressed = entry->compressed();
|
||||
|
||||
StringWriter w;
|
||||
uint32_t subcommand_size = (compressed.size() + sizeof(G_MapData_GC_Ep3_6xB6x41) + 3) & (~3);
|
||||
w.put<G_MapData_GC_Ep3_6xB6x41>({{{{0xB6, 0, 0}, subcommand_size}, 0x41, {}}, entry->map.map_number.load(), compressed.size(), 0});
|
||||
w.write(compressed);
|
||||
send_command(l, 0x6C, 0x00, w.str());
|
||||
}
|
||||
|
||||
void send_ep3_card_battle_table_state(shared_ptr<Lobby> l, uint16_t table_number) {
|
||||
S_CardBattleTableState_GC_Ep3_E4 cmd;
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
@@ -1778,8 +1756,8 @@ void set_mask_for_ep3_game_command(void* vdata, size_t size, uint8_t mask_key) {
|
||||
throw logic_error("Episode 3 game command is too short for masking");
|
||||
}
|
||||
|
||||
auto* header = reinterpret_cast<G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5*>(vdata);
|
||||
size_t command_bytes = header->basic_header.size * 4;
|
||||
auto* header = reinterpret_cast<G_CardBattleCommandHeader*>(vdata);
|
||||
size_t command_bytes = header->size * 4;
|
||||
if (command_bytes != size) {
|
||||
throw runtime_error("command size field does not match actual size");
|
||||
}
|
||||
@@ -1826,7 +1804,7 @@ void send_quest_file_chunk(
|
||||
|
||||
S_WriteFile_13_A7 cmd;
|
||||
cmd.filename = filename;
|
||||
memcpy(cmd.data, data, size);
|
||||
memcpy(cmd.data.data(), data, size);
|
||||
if (size < 0x400) {
|
||||
memset(&cmd.data[size], 0, 0x400 - size);
|
||||
}
|
||||
|
||||
@@ -278,10 +278,6 @@ void send_ep3_media_update(
|
||||
uint32_t which,
|
||||
const std::string& compressed_data);
|
||||
void send_ep3_rank_update(std::shared_ptr<Client> c);
|
||||
void send_ep3_map_list(
|
||||
std::shared_ptr<ServerState> s, std::shared_ptr<Lobby> l);
|
||||
void send_ep3_map_data(
|
||||
std::shared_ptr<ServerState> s, std::shared_ptr<Lobby> l, uint32_t map_id);
|
||||
void send_ep3_card_battle_table_state(std::shared_ptr<Lobby> l, uint16_t table_number);
|
||||
|
||||
// Pass mask_key = 0 to unmask the command
|
||||
|
||||
+8
-4
@@ -144,11 +144,15 @@ void Server::on_client_input(Channel& ch, uint16_t command, uint32_t flag, std::
|
||||
if (c->should_disconnect) {
|
||||
server->disconnect_client(c);
|
||||
} else {
|
||||
try {
|
||||
if (server->state->catch_handler_exceptions) {
|
||||
try {
|
||||
on_command(server->state, c, command, flag, data);
|
||||
} catch (const exception& e) {
|
||||
server_log.warning("Error processing client command: %s", e.what());
|
||||
c->should_disconnect = true;
|
||||
}
|
||||
} else {
|
||||
on_command(server->state, c, command, flag, data);
|
||||
} catch (const exception& e) {
|
||||
server_log.warning("Error processing client command: %s", e.what());
|
||||
c->should_disconnect = true;
|
||||
}
|
||||
if (c->should_disconnect) {
|
||||
server->disconnect_client(c);
|
||||
|
||||
@@ -23,6 +23,8 @@ ServerState::ServerState()
|
||||
allow_saving(true),
|
||||
item_tracking_enabled(true),
|
||||
episode_3_send_function_call_enabled(false),
|
||||
catch_handler_exceptions(true),
|
||||
ep3_behavior_flags(0),
|
||||
run_shell_behavior(RunShellBehavior::DEFAULT), next_lobby_id(1),
|
||||
pre_lobby_event(0),
|
||||
ep3_menu_song(-1) {
|
||||
|
||||
+3
-1
@@ -50,13 +50,15 @@ struct ServerState {
|
||||
bool allow_saving;
|
||||
bool item_tracking_enabled;
|
||||
bool episode_3_send_function_call_enabled;
|
||||
bool catch_handler_exceptions;
|
||||
uint32_t ep3_behavior_flags;
|
||||
RunShellBehavior run_shell_behavior;
|
||||
std::vector<std::shared_ptr<const PSOBBEncryption::KeyFile>> bb_private_keys;
|
||||
std::shared_ptr<const FunctionCodeIndex> function_code_index;
|
||||
std::shared_ptr<const PatchFileIndex> pc_patch_file_index;
|
||||
std::shared_ptr<const PatchFileIndex> bb_patch_file_index;
|
||||
std::shared_ptr<const DOLFileIndex> dol_file_index;
|
||||
std::shared_ptr<const Ep3DataIndex> ep3_data_index;
|
||||
std::shared_ptr<const Episode3::DataIndex> ep3_data_index;
|
||||
std::shared_ptr<const QuestIndex> quest_index;
|
||||
std::shared_ptr<const LevelTable> level_table;
|
||||
std::shared_ptr<const BattleParamsIndex> battle_params;
|
||||
|
||||
+60
-7
@@ -186,11 +186,18 @@ template <typename ItemT, size_t Count>
|
||||
struct parray {
|
||||
ItemT items[Count];
|
||||
|
||||
parray(ItemT v) {
|
||||
this->clear(v);
|
||||
}
|
||||
template <typename ArgT = ItemT, std::enable_if_t<std::is_arithmetic<ArgT>::value, bool> = true>
|
||||
parray() {
|
||||
this->clear(0);
|
||||
}
|
||||
template <typename ArgT = ItemT, std::enable_if_t<!std::is_arithmetic<ArgT>::value, bool> = true>
|
||||
template <typename ArgT = ItemT, std::enable_if_t<std::is_pointer<ArgT>::value, bool> = true>
|
||||
parray() {
|
||||
this->clear(nullptr);
|
||||
}
|
||||
template <typename ArgT = ItemT, std::enable_if_t<!std::is_arithmetic<ArgT>::value && !std::is_pointer<ArgT>::value, bool> = true>
|
||||
parray() { }
|
||||
|
||||
parray(const parray& other) {
|
||||
@@ -203,10 +210,10 @@ struct parray {
|
||||
this->operator=(s);
|
||||
}
|
||||
|
||||
constexpr size_t size() {
|
||||
constexpr static size_t size() {
|
||||
return Count;
|
||||
}
|
||||
constexpr size_t bytes() {
|
||||
constexpr static size_t bytes() {
|
||||
return Count * sizeof(ItemT);
|
||||
}
|
||||
ItemT* data() {
|
||||
@@ -220,17 +227,58 @@ struct parray {
|
||||
if (index >= Count) {
|
||||
throw std::out_of_range("array index out of bounds");
|
||||
}
|
||||
return this->items[index];
|
||||
// Note: This looks really dumb, but apparently works around an issue in GCC
|
||||
// that causes a "returning address of temporary" error here.
|
||||
return *&this->items[index];
|
||||
}
|
||||
const ItemT& operator[](size_t index) const {
|
||||
if (index >= Count) {
|
||||
throw std::out_of_range("array index out of bounds");
|
||||
}
|
||||
return this->items[index];
|
||||
return *&this->items[index];
|
||||
}
|
||||
|
||||
ItemT& at(size_t index) {
|
||||
return this->operator[](index);
|
||||
}
|
||||
const ItemT& at(size_t index) const {
|
||||
return this->operator[](index);
|
||||
}
|
||||
|
||||
ItemT* sub_ptr(size_t offset = 0, size_t count = Count) {
|
||||
if (offset + count > Count) {
|
||||
throw std::out_of_range("sub-array out of range");
|
||||
}
|
||||
return &this->items[offset];
|
||||
}
|
||||
const ItemT* sub_ptr(size_t offset = 0, size_t count = Count) const {
|
||||
if (offset + count > Count) {
|
||||
throw std::out_of_range("sub-array out of range");
|
||||
}
|
||||
return &this->items[offset];
|
||||
}
|
||||
|
||||
template <size_t SubCount>
|
||||
parray<ItemT, SubCount>& sub(size_t offset = 0) {
|
||||
if (offset + SubCount > Count) {
|
||||
throw std::out_of_range("sub-array out of range");
|
||||
}
|
||||
return *reinterpret_cast<parray<ItemT, SubCount>*>(&this->items[offset]);
|
||||
}
|
||||
template <size_t SubCount>
|
||||
const parray<ItemT, SubCount>& sub(size_t offset = 0) const {
|
||||
if (offset + SubCount > Count) {
|
||||
throw std::out_of_range("sub-array out of range");
|
||||
}
|
||||
return *reinterpret_cast<parray<ItemT, SubCount>*>(&this->items[offset]);
|
||||
}
|
||||
|
||||
void assign_range(const ItemT* new_items, size_t count = Count, size_t start_offset = 0) {
|
||||
for (size_t x = start_offset; (x < Count) && (x < start_offset + count); x++) {
|
||||
this->items[x] = new_items[x];
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: These can be made faster by only clearing the unused space after the
|
||||
// strncpy_t (if any) instead of clearing all the space every time
|
||||
parray& operator=(const parray& s) {
|
||||
for (size_t x = 0; x < Count; x++) {
|
||||
this->items[x] = s.items[x];
|
||||
@@ -284,6 +332,11 @@ struct parray {
|
||||
this->items[x] = v;
|
||||
}
|
||||
}
|
||||
void clear() {
|
||||
for (size_t x = 0; x < Count; x++) {
|
||||
this->items[x] = ItemT();
|
||||
}
|
||||
}
|
||||
void clear_after(size_t position, ItemT v = 0) {
|
||||
for (size_t x = position; x < Count; x++) {
|
||||
this->items[x] = v;
|
||||
|
||||
@@ -248,12 +248,22 @@
|
||||
// they are at the newserv main menu. If set, this value must be an integer.
|
||||
// "Episode3MenuSong": 0,
|
||||
|
||||
// Episode 3 battle behavior flags. When set to zero, battles behave as they
|
||||
// did on the original Sega servers.
|
||||
// TODO: Document what nonzero values do here.
|
||||
"Episode3BehaviorFlags": 0,
|
||||
|
||||
// Whether to enable patches on Episode 3 USA. This functionality depends on
|
||||
// exploiting a bug in Episode 3, and while it seems to work reliably on
|
||||
// Dolphin, it hasn't been tested on a real GameCube. So, newserv doesn't
|
||||
// enable Episode 3 patches by default; it only does so if this option is on.
|
||||
// "EnableEpisode3SendFunctionCall": true,
|
||||
|
||||
// Whether to enable certain exception handling. Disabling this is generally
|
||||
// only useful for debugging newserv itself, and it should usually be left on
|
||||
// (which is the default behavior).
|
||||
"CatchHandlerExceptions": true,
|
||||
|
||||
// By default, the server keeps track of items in all games, even for versions
|
||||
// other than Blue Burst. This enables use of the $what command, as well as
|
||||
// protection against item duplication cheats (the cheater is disconnected
|
||||
|
||||
Reference in New Issue
Block a user