diff --git a/notes/psobb/localized-assets/npc-coren/tsuchan.txt b/notes/psobb/localized-assets/npc-coren/tsuchan.txt index d910e20f..2a2ebcb3 100644 --- a/notes/psobb/localized-assets/npc-coren/tsuchan.txt +++ b/notes/psobb/localized-assets/npc-coren/tsuchan.txt @@ -1,96 +1,94 @@ -########################################################### - -NPC: Coren Tsu - The Wanderer -AREAS: Pioneer 2 - -Translations by: apexseals (discord: apexseals) -Proofing & Debugging by: nolrinale (github.com/nolrinale) - -########################################################### - -presentation: - -I am Coren Tsu, a wandering merchant, -you could say. - -Please take some time to look at -the rare and wonderous goods -I have been collecting. - -If you spend a little meseta, -you could win a wonderful prize. - -Well? Wanna try? - - -You may win, -you may lose. - -But if you don't win, -don't take it out on me. - -That's just the way -gambling is, yes? - -Well then, how much -meseta do you want to pay? - -As long as you pay me, -I'll give you a great service. - - -Huh? - -That's too bad... - -Well, these kind of things usually -have a chance to lose money. - -Let's keep this discreet. -If you feel up to it, talk to me again. - - -It seems you have -too many items. - -First, go and -organize your items, - -Then speak to me again. - -What? - -You said you'd try, -then you said no. - -People like that -fail at everything. - - -What the...? - -You don't have the -meseta to pay me? - -I won't work with such -cold hearted people. - - -Alright, let's do it. - -You better pray -for something good... - - -Look here! -Take it! - -Even if you had bad luck, -something good will come out of it. - -You'll win someday! - -In case you want to try again, -come back to me once more. - - +########################################################### + +NPC: Coren Tsu - The Wanderer +AREAS: Pioneer 2 + +Translations by: apexseals (discord: apexseals) +Proofing & Debugging by: nolrinale (github.com/nolrinale) + +########################################################### + +presentation: + +I am Coren Tsu, a wandering merchant, +you could say. + +Please take some time to look at +the rare and wonderous goods +I have been collecting. + +If you spend a little meseta, +you could win a wonderful prize. + +Well? Wanna try? + + +You may win, +you may lose. + +But if you don't win, +don't take it out on me. + +That's just the way +gambling is, yes? + +Well then, how much +meseta do you want to pay? + +As long as you pay me, +I'll give you a great service. + + +Huh? + +That's too bad... + +Well, these kind of things usually +have a chance to lose money. + +Let's keep this discreet. +If you feel up to it, talk to me again. + + +It seems you have +too many items. + +First, go and +organize your items, + +Then speak to me again. + +What? + +You said you'd try, +then you said no. + +People like that +fail at everything. + + +What the...? + +You don't have the +meseta to pay me? + +I won't work with such +cold hearted people. + + +Alright, let's do it. + +You better pray +for something good... + + +Look here! +Take it! + +Even if you had bad luck, +something good will come out of it. + +You'll win someday! + +In case you want to try again, +come back to me once more. diff --git a/notes/psobb/vanilla-tables/items.txt b/notes/psobb/vanilla-tables/items.txt deleted file mode 100755 index 78f50796..00000000 --- a/notes/psobb/vanilla-tables/items.txt +++ /dev/null @@ -1,1538 +0,0 @@ -000000,Unused -000100,Saber -000101,Brand -000102,Buster -000103,Pallasch -000104,Gladius -000105,DB's SABER -000106,KALADGOLG -000107,DURANDAL -000108,GALATINE -000200,Sword -000201,Gigush -000202,Breaker -000203,Claymore -000204,Calibur -000205,FLOWEN'S SWORD -000206,LAST SURVIVOR -000207,DRAGON SLAYER -000300,Dagger -000301,Knife -000302,Blade -000303,Edge -000304,Ripper -000305,BLADE DANCE -000306,BLOODY ART -000307,CROSS SCAR -000308,ZERO DIVIDE -000309,TWIN KAMUI -000400,Partisan -000401,Halbert -000402,Glaive -000403,Berdys -000404,Gungnir -000405,BRIONAC -000406,VJAYA -000407,GAE BOLG -000408,ASTERON BELT -000500,Slicer -000501,Spinner -000502,Cutter -000503,Sawcer -000504,Diska -000505,SLICER OF ASSASSIN -000506,DISKA OF LIBERATOR -000507,DISKA OF BRAVEMEN -000508,IZMAELA -000600,Handgun -000601,Autogun -000602,Lockgun -000603,Railgun -000604,Raygun -000605,VARISTA -000606,CUSTOM RAY Ver.00 -000607,BRAVACE -000608,TENSION BLASTER -000700,Rifle -000701,Sniper -000702,Blaster -000703,Beam -000704,Laser -000705,VISK'235W -000706,WALS'MK2 -000707,JUSTY'23ST -000708,RIANOV 303SNR -000709,RIANOV 303SNR-1 -00070A,RIANOV 303SNR-2 -00070B,RIANOV 303SNR-3 -00070C,RIANOV 303SNR-4 -00070D,RIANOV 303SNR-5 -000800,Mechgun -000801,Assault -000802,Repeater -000803,Gatling -000804,Vulcan -000805,M&A60 VISE -000806,H&S25 JUSTICE -000807,L&K14 COMBAT -000900,Shot -000901,Spread -000902,Cannon -000903,Launcher -000904,Arms -000905,CRUSH BULLET -000906,METEOR SMASH -000907,FINAL IMPACT -000A00,Cane -000A01,Stick -000A02,Mace -000A03,Club -000A04,CLUB OF LACONIUM -000A05,MACE OF ADAMAN -000A06,CLUB OF ZUMIURAN -000A07,LOLIPOP -000B00,Rod -000B01,Pole -000B02,Pillar -000B03,Striker -000B04,BATTLE VERGE -000B05,BRAVE HAMMER -000B06,ALIVE AQHU -000B07,VALKYRIE -000C00,Wand -000C01,Staff -000C02,Baton -000C03,Scepter -000C04,FIRE SCEPTER:AGNI -000C05,ICE STAFF:DAGON -000C06,STORM VAND:INDRA -000C07,EARTH WAND BROWNIE -000D00,PHOTON CLAW -000D01,SILENCE CLAW -000D02,NEI'S CLAW -000D03,PHOENIX CLAW -000E00,DOUBLE SABER -000E01,STAG CUTLERY -000E02,TWIN BRAND -000F00,BRAVE KNUCKLE -000F01,ANGRY FIST -000F02,GOD HAND -000F03,SONIC KNUCKLE -001000,OROTIAGITO -001001,AGITO (1975) -001002,AGITO (1983) -001003,AGITO (2001) -001004,AGITO (1991) -001005,AGITO (1977) -001006,AGITO (1980) -001007,RAIKIRI -001100,SOUL EATER -001101,SOUL BANISH -001200,SPREAD NEEDLE -001300,HOLY RAY -001400,INFERNO BAZOOKA -001401,RAMBLING MAY -001402,L&K38 COMBAT -001500,FLAME VISIT -001501,BURNING VISIT -001600,AKIKO'S FRYING PAN -001700,SORCERER'S CANE -001800,S-BEAT'S BLADE -001900,P-ARMS'S BLADE -001A00,DELSABER'S BUSTER -001B00,BRINGER'S RIFLE -001C00,EGG BLASTER -001D00,PSYCHO WAND -001E00,HEAVEN PUNISHER -001F00,LAVIS CANNON -002000,VICTOR AXE -002001,LACONIUM AXE -002100,CHAIN SAWD -002200,CADUCEUS -002201,MERCURIUS ROD -002300,STING TIP -002400,MAGICAL PIECE -002500,TECHNICAL CROZIER -002600,SUPPRESSED GUN -002700,ANCIENT SABER -002800,HARISEN BATTLE FAN -002900,YAMIGARASU -002A00,AKIKO'S WOK -002B00,TOY HAMMER -002C00,ELYSION -002D00,RED SABER -002E00,METEOR CUDGEL -002F00,MONKEY KING BAR -002F01,BLACK KING BAR -003000,DOUBLE CANNON -003001,GIRASOLE -003100,HUGE BATTLE FAN -003200,TSUMIKIRI J-SWORD -003300,SEALED J-SWORD -003400,RED SWORD -003500,CRAZY TUNE -003600,TWIN CHAKRAM -003700,WOK OF AKIKO'S SHOP -003800,LAVIS BLADE -003900,RED DAGGER -003A00,MADAM'S PARASOL -003B00,MADAM'S UMBRELLA -003C00,IMPERIAL PICK -003D00,BERDYSH -003E00,RED PARTISAN -003F00,FLIGHT CUTTER -004000,FLIGHT FAN -004100,RED SLICER -004200,HANDGUN:GULD -004201,MASTER RAVEN -004300,HANDGUN:MILLA -004301,LAST SWAN -004400,RED HANDGUN -004500,FROZEN SHOOTER -004501,SNOW QUEEN -004600,ANTI ANDROID RIFLE -004700,ROCKET PUNCH -004800,SAMBA MARACAS -004900,TWIN PSYCHOGUN -004A00,DRILL LAUNCHER -004B00,GULD MILLA -004B01,DUAL BIRD -004C00,RED MECHGUN -004D00,BELRA CANNON -004E00,PANZER FAUST -004E01,IRON FAUST -004F00,SUMMIT MOON -005000,WINDMILL -005100,EVIL CURST -005200,FLOWER CANE -005300,HILDEBEAR'S CANE -005400,HILDEBLUE'S CANE -005500,RABBIT WAND -005600,PLANTAIN LEAF -005601,FATSIA -005700,DEMONIC FORK -005800,STRIKER OF CHAO -005900,BROOM -005A00,PROPHETS OF MOTAV -005B00,THE SIGH OF A GOD -005C00,TWINKLE STAR -005D00,PLANTAIN FAN -005E00,TWIN BLAZE -005F00,MARINA'S BAG -006000,DRAGON'S CLAW -006100,PANTHER'S CLAW -006200,S-RED'S BLADE -006300,PLANTAIN HUGE FAN -006400,CHAMELEON SCYTHE -006500,YASMINKOV 3000R -006600,ANO RIFLE -006700,BARANZ LAUNCHER -006800,BRANCH OF PAKUPAKU -006900,HEART OF POUMN -006A00,YASMINKOV 2000H -006B00,YASMINKOV 7000V -006C00,YASMINKOV 9000M -006D00,MASER BEAM -006D01,POWER MASER -006E00,Game Magazine -006E01,LOGiN -006F00,FLOWER BOUQUET -007000, SABER -007001, SABER -007002, SABER -007003, SABER -007004, SABER -007005, SABER -007006, SABER -007007, SABER -007008, SABER -007009, SABER -00700A, SABER -00700B, SABER -00700C, SABER -00700D, SABER -00700E, SABER -00700F, SABER -007010, SABER -007100, SWORD -007101, SWORD -007102, SWORD -007103, SWORD -007104, SWORD -007105, SWORD -007106, SWORD -007107, SWORD -007108, SWORD -007109, SWORD -00710A, SWORD -00710B, SWORD -00710C, SWORD -00710D, SWORD -00710E, SWORD -00710F, SWORD -007110, SWORD -007200, BLADE -007201, BLADE -007202, BLADE -007203, BLADE -007204, BLADE -007205, BLADE -007206, BLADE -007207, BLADE -007208, BLADE -007209, BLADE -00720A, BLADE -00720B, BLADE -00720C, BLADE -00720D, BLADE -00720E, BLADE -00720F, BLADE -007210, BLADE -007300, PARTISAN -007301, PARTISAN -007302, PARTISAN -007303, PARTISAN -007304, PARTISAN -007305, PARTISAN -007306, PARTISAN -007307, PARTISAN -007308, PARTISAN -007309, PARTISAN -00730A, PARTISAN -00730B, PARTISAN -00730C, PARTISAN -00730D, PARTISAN -00730E, PARTISAN -00730F, PARTISAN -007310, PARTISAN -007400, SLICER -007401, SLICER -007402, SLICER -007403, SLICER -007404, SLICER -007405, SLICER -007406, SLICER -007407, SLICER -007408, SLICER -007409, SLICER -00740A, SLICER -00740B, SLICER -00740C, SLICER -00740D, SLICER -00740E, SLICER -00740F, SLICER -007410, SLICER -007500, GUN -007501, GUN -007502, GUN -007503, GUN -007504, GUN -007505, GUN -007506, GUN -007507, GUN -007508, GUN -007509, GUN -00750A, GUN -00750B, GUN -00750C, GUN -00750D, GUN -00750E, GUN -00750F, GUN -007510, GUN -007600, RIFLE -007601, RIFLE -007602, RIFLE -007603, RIFLE -007604, RIFLE -007605, RIFLE -007606, RIFLE -007607, RIFLE -007608, RIFLE -007609, RIFLE -00760A, RIFLE -00760B, RIFLE -00760C, RIFLE -00760D, RIFLE -00760E, RIFLE -00760F, RIFLE -007610, RIFLE -007700, MECHGUN -007701, MECHGUN -007702, MECHGUN -007703, MECHGUN -007704, MECHGUN -007705, MECHGUN -007706, MECHGUN -007707, MECHGUN -007708, MECHGUN -007709, MECHGUN -00770A, MECHGUN -00770B, MECHGUN -00770C, MECHGUN -00770D, MECHGUN -00770E, MECHGUN -00770F, MECHGUN -007710, MECHGUN -007800, SHOT -007801, SHOT -007802, SHOT -007803, SHOT -007804, SHOT -007805, SHOT -007806, SHOT -007807, SHOT -007808, SHOT -007809, SHOT -00780A, SHOT -00780B, SHOT -00780C, SHOT -00780D, SHOT -00780E, SHOT -00780F, SHOT -007810, SHOT -007900, CANE -007901, CANE -007902, CANE -007903, CANE -007904, CANE -007905, CANE -007906, CANE -007907, CANE -007908, CANE -007909, CANE -00790A, CANE -00790B, CANE -00790C, CANE -00790D, CANE -00790E, CANE -00790F, CANE -007910, CANE -007A00, ROD -007A01, ROD -007A02, ROD -007A03, ROD -007A04, ROD -007A05, ROD -007A06, ROD -007A07, ROD -007A08, ROD -007A09, ROD -007A0A, ROD -007A0B, ROD -007A0C, ROD -007A0D, ROD -007A0E, ROD -007A0F, ROD -007A10, ROD -007B00, WAND -007B01, WAND -007B02, WAND -007B04, WAND -007B05, WAND -007B06, WAND -007B07, WAND -007B08, WAND -007B09, WAND -007B0A, WAND -007B0B, WAND -007B0C, WAND -007B0D, WAND -007B0E, WAND -007B0F, WAND -007B10, WAND -007C00, TWIN -007C01, TWIN -007C02, TWIN -007C03, TWIN -007C04, TWIN -007C05, TWIN -007C06, TWIN -007C07, TWIN -007C08, TWIN -007C09, TWIN -007C0A, TWIN -007C0B, TWIN -007C0C, TWIN -007C0D, TWIN -007C0E, TWIN -007C0F, TWIN -007C10, TWIN -007D00, CLAW -007D01, CLAW -007D02, CLAW -007D03, CLAW -007D04, CLAW -007D05, CLAW -007D06, CLAW -007D07, CLAW -007D08, CLAW -007D09, CLAW -007D0A, CLAW -007D0B, CLAW -007D0C, CLAW -007D0D, CLAW -007D0E, CLAW -007D0F, CLAW -007D10, CLAW -007E00, BAZOOKA -007E01, BAZOOKA -007E02, BAZOOKA -007E03, BAZOOKA -007E04, BAZOOKA -007E05, BAZOOKA -007E06, BAZOOKA -007E07, BAZOOKA -007E08, BAZOOKA -007E09, BAZOOKA -007E0A, BAZOOKA -007E0B, BAZOOKA -007E0C, BAZOOKA -007E0D, BAZOOKA -007E0E, BAZOOKA -007E0F, BAZOOKA -007E10, BAZOOKA -007F00, NEEDLE -007F01, NEEDLE -007F02, NEEDLE -007F03, NEEDLE -007F04, NEEDLE -007F05, NEEDLE -007F06, NEEDLE -007F07, NEEDLE -007F08, NEEDLE -007F09, NEEDLE -007F0A, NEEDLE -007F0B, NEEDLE -007F0C, NEEDLE -007F0D, NEEDLE -007F0E, NEEDLE -007F0F, NEEDLE -007F10, NEEDLE -008000, SCYTHE -008001, SCYTHE -008002, SCYTHE -008003, SCYTHE -008004, SCYTHE -008005, SCYTHE -008006, SCYTHE -008007, SCYTHE -008008, SCYTHE -008009, SCYTHE -00800A, SCYTHE -00800B, SCYTHE -00800C, SCYTHE -00800D, SCYTHE -00800E, SCYTHE -00800F, SCYTHE -008010, SCYTHE -008100, HAMMER -008101, HAMMER -008102, HAMMER -008103, HAMMER -008104, HAMMER -008105, HAMMER -008106, HAMMER -008107, HAMMER -008108, HAMMER -008109, HAMMER -00810A, HAMMER -00810B, HAMMER -00810C, HAMMER -00810D, HAMMER -00810E, HAMMER -00810F, HAMMER -008110, HAMMER -008200, MOON -008201, MOON -008202, MOON -008203, MOON -008204, MOON -008205, MOON -008206, MOON -008207, MOON -008208, MOON -008209, MOON -00820A, MOON -00820B, MOON -00820C, MOON -00820D, MOON -00820E, MOON -00820F, MOON -008210, MOON -008300, PSYCHOGUN -008301, PSYCHOGUN -008302, PSYCHOGUN -008303, PSYCHOGUN -008304, PSYCHOGUN -008305, PSYCHOGUN -008306, PSYCHOGUN -008307, PSYCHOGUN -008308, PSYCHOGUN -008309, PSYCHOGUN -00830A, PSYCHOGUN -00830B, PSYCHOGUN -00830C, PSYCHOGUN -00830D, PSYCHOGUN -00830E, PSYCHOGUN -00830F, PSYCHOGUN -008310, PSYCHOGUN -008400, PUNCH -008401, PUNCH -008402, PUNCH -008403, PUNCH -008404, PUNCH -008405, PUNCH -008406, PUNCH -008407, PUNCH -008408, PUNCH -008409, PUNCH -00840A, PUNCH -00840B, PUNCH -00840C, PUNCH -00840D, PUNCH -00840E, PUNCH -00840F, PUNCH -008410, PUNCH -008500, WINDMILL -008501, WINDMILL -008502, WINDMILL -008503, WINDMILL -008504, WINDMILL -008505, WINDMILL -008506, WINDMILL -008507, WINDMILL -008508, WINDMILL -008509, WINDMILL -00850A, WINDMILL -00850B, WINDMILL -00850C, WINDMILL -00850D, WINDMILL -00850E, WINDMILL -00850F, WINDMILL -008510, WINDMILL -008600, HARISEN -008601, HARISEN -008602, HARISEN -008603, HARISEN -008604, HARISEN -008605, HARISEN -008606, HARISEN -008607, HARISEN -008608, HARISEN -008609, HARISEN -00860A, HARISEN -00860B, HARISEN -00860C, HARISEN -00860D, HARISEN -00860E, HARISEN -00860F, HARISEN -008610, HARISEN -008700, KATANA -008701, KATANA -008702, KATANA -008703, KATANA -008704, KATANA -008705, KATANA -008706, KATANA -008707, KATANA -008708, KATANA -008709, KATANA -00870A, KATANA -00870B, KATANA -00870C, KATANA -00870D, KATANA -00870E, KATANA -00870F, KATANA -008710, KATANA -008800, J-CUTTER -008801, J-CUTTER -008802, J-CUTTER -008803, J-CUTTER -008804, J-CUTTER -008805, J-CUTTER -008806, J-CUTTER -008807, J-CUTTER -008808, J-CUTTER -008809, J-CUTTER -00880A, J-CUTTER -00880B, J-CUTTER -00880C, J-CUTTER -00880D, J-CUTTER -00880E, J-CUTTER -00880F, J-CUTTER -008810, J-CUTTER -008900,MUSASHI -008901,YAMATO -008902,ASUKA -008903,SANGE & YASHA -008A00,SANGE -008A01,YASHA -008A02,KAMUI -008B00,PHOTON LAUNCHER -008B01,GUILTY LIGHT -008B02,RED SCORPIO -008B03,PHONON MASER -008C00,TALIS -008C01,MAHU -008C02,HITOGATA -008C03,DANCING HITOGATA -008C04,KUNAI -008D00,NUG-2000 BAZOOKA -008E00,S-BERILL'S HAND #0 -008E01,S-BERILL'S HAND #1 -008F00,FLOWEN'S SWORD AUW 3060 -008F01,FLOWEN'S SWORD AUW 3064 -008F02,FLOWEN'S SWORD AUW 3067 -008F03,FLOWEN'S SWORD AUW 3073 -008F04,FLOWEN'S SWORD AUW 3077 -008F05,FLOWEN'S SWORD AUW 3082 -008F06,FLOWEN'S SWORD AUW 3083 -008F07,FLOWEN'S SWORD AUW 3084 -008F08,FLOWEN'S SWORD AUW 3079 -009000,DB'S SWORD AUW 3062 -009001,DB'S SWORD AUW 3067 -009002,DB'S SWORD AUW 3069 -009003,DB'S SWORD AUW 3064 -009004,DB'S SWORD AUW 3069 -009005,DB'S SWORD AUW 3073 -009006,DB'S SWORD AUW 3070 -009007,DB'S SWORD AUW 3075 -009008,DB'S SWORD AUW 3077 -009100,GIGUE BAZOOKA -009200,GUARDIANNA -009300,VIRIDIA CARD -009301,GREENILL CARD -009302,SKYLY CARD -009303,BLUEFULL CARD -009304,PURPLENUM CARD -009305,PINKAL CARD -009306,REDRIA CARD -009307,ORAN CARD -009308,YELLOWBOZE CARD -009309,WHITILL CARD -009400,MORNING GLORY -009500,PARTISAN OF LIGHTING -009501,Partisan of Ice -009502,Partisan of Fire -009600,GAL WIND -009700,ZANBA -009800,RIKA'S CLAW -009900,ANGEL HARP -009A00,DEMOLITION COMET -009B00,NEI'S CLAW -009C00,RAINBOW BATON -009D00,DARK FLOW -009E00,DARK METEOR -009F00,DARK BRIDGE -00A000,G-ASSASIN'S SABERS -00A100,RAPPY'S FAN -00A200,BOOMA'S CLAW -00A201,GOBOOMA'S CLAW -00A202,GIGOBOOMA'S CLAW -00A300,RUBY BULLET -00A400,AMORE ROSE -00A500, SWORDS -00A501, SWORDS -00A502, SWORDS -00A503, SWORDS -00A504, SWORDS -00A505, SWORDS -00A506, SWORDS -00A507, SWORDS -00A508, SWORDS -00A509, SWORDS -00A50A, SWORDS -00A50B, SWORDS -00A50C, SWORDS -00A50D, SWORDS -00A50E, SWORDS -00A50F, SWORDS -00A510, SWORDS -00A600, LAUNCHER -00A601, LAUNCHER -00A602, LAUNCHER -00A603, LAUNCHER -00A604, LAUNCHER -00A605, LAUNCHER -00A606, LAUNCHER -00A607, LAUNCHER -00A608, LAUNCHER -00A609, LAUNCHER -00A60A, LAUNCHER -00A60B, LAUNCHER -00A60C, LAUNCHER -00A60D, LAUNCHER -00A60E, LAUNCHER -00A60F, LAUNCHER -00A610, LAUNCHER -00A700, CARD -00A701, CARD -00A702, CARD -00A703, CARD -00A704, CARD -00A705, CARD -00A706, CARD -00A707, CARD -00A708, CARD -00A709, CARD -00A70A, CARD -00A70B, CARD -00A70C, CARD -00A70D, CARD -00A70E, CARD -00A70F, CARD -00A710, CARD -00A800, KNUCKLE -00A801, KNUCKLE -00A802, KNUCKLE -00A803, KNUCKLE -00A804, KNUCKLE -00A805, KNUCKLE -00A806, KNUCKLE -00A807, KNUCKLE -00A808, KNUCKLE -00A809, KNUCKLE -00A80A, KNUCKLE -00A80B, KNUCKLE -00A80C, KNUCKLE -00A80D, KNUCKLE -00A80E, KNUCKLE -00A80F, KNUCKLE -00A810, KNUCKLE -00A900, AXE -00A901, AXE -00A902, AXE -00A903, AXE -00A904, AXE -00A905, AXE -00A906, AXE -00A907, AXE -00A908, AXE -00A909, AXE -00A90A, AXE -00A90B, AXE -00A90C, AXE -00A90D, AXE -00A90E, AXE -00A90F, AXE -00A910, AXE -00AA00,SLICER OF FANATIC -00AB00,LAME D'ARGENT -00AC00,EXCALIBUR -00AD00,Saber -00AD01,Saber -00AD02,Saber -00AD03,RAGE DE FEU -00AE00,DAISY CHAIN -00AF00,OPHELIE SEIZE -00B000,MILLE MARTEAUX -00B100,LE COGNEUR -00B200,COMMANDER BLADE -00B300,VIVIENNE -00B400,KUSANAGI -00B500,SACRED DUSTER -00B600,GUREN -00B700,SHOUREN -00B800,JIZAI -00B900,FLAMBERGE -00BA00,YUNCHANG -00BB00,SNAKE SPIRE -00BC00,FLAPJACK FLAPPER -00BD00,GETSUGASAN -00BE00,MAGUWA -00BF00,HEAVEN STRIKER -00C000,CANNON ROUGE -00C100,METEOR ROUGE -00C200,SOLFERINO -00C300,CLIO -00C400,SIREN GLASS HAMMER -00C500,GLIDE DIVINE -00C600,SHICHISHITO -00C700,MURASAME -00C800,DAYLIGHT SCAR -00C900,DECALOG -00CA00,5TH ANNIV. BLADE -00CB00,PRINCIPAL'S GIFT PARASOL -00CC00,AKIKO'S CLEAVER -00CD00,TANEGASHIMA -00CE00,TREE CLIPPERS -00CF00,NICE SHOT -00D000,RAINBOW -00D100,EMPRESS -00D200,ANO BAZOOKA -00D300,SYNTHESIZER -00D400,BAMBOO SPEAR -00D500,KAN'EI TSUHO -00D600,JITTE -00D700,BUTTERFLY NET -00D800,SYRINGE -00D900,BATTLEDORE -00DA00,RACKET -00DB00,HAMMER -00DC00,GREAT BOUQUET -00DD00,TypeSA/Saber -00DE00,TypeSL/Saber -00DE01,TypeSL/Slicer -00DE02,TypeSL/Claw -00DE03,TypeSL/Katana -00DF00,TypeJS/Saber -00DF01,TypeJS/Slicer -00DF02,TypeJS/J-Sword -00E000,TypeSW/Sword -00E001,TypeSW/Slicer -00E002,TypeSW/J-Sword -00E100,TypeRO/Sword -00E101,TypeRO/Halbert -00E102,TypeRO/Rod -00E200,TypeBL/BLADE -00E300,TypeKN/Blade -00E301,TypeKN/Claw -00E400,TypeHA/HALBERT -00E401,TypeHA/Rod -00E500,TypeDS/D.SABER -00E501,TypeDS/Rod -00E502,TypeDS/WAND -00E600,TypeCL/CLAW -00E700,TypeSS/SWORDS -00E800,TypeGU/HANDGUN -00E801,TypeGU/MECHGUN -00E900,TypeRI/RIFLE -00EA00,TypeME/MECHGUN -00EB00,TypeSH/SHOT -00EC00,TypeWA/WAND -010100,Frame -010101,Armor -010102,Psy Armor -010103,Giga Frame -010104,Soul Frame -010105,Cross Armor -010106,Solid Frame -010107,Brave Armor -010108,Hyper Frame -010109,Grand Armor -01010A,Shock Frame -01010B,King's Frame -01010C,Dragon Frame -01010D,Absorb Armor -01010E,Protect Frame -01010F,General Armor -010110,Perfect Frame -010111,Valiant Frame -010112,Imperial Armor -010113,Holiness Armor -010114,Guardian Armor -010115,Divinity Armor -010116,Ultimate Frame -010117,Celestial Armor -010118,HUNTER FIELD -010119,RANGER FIELD -01011A,FORCE FIELD -01011B,REVIVAL GARMENT -01011C,SPIRIT GARMENT -01011D,STINK FRAME -01011E,D-PARTS Ver1.01 -01011F,D-PARTS Ver2.10 -010120,PARASITE WEAR:De Rol -010121,PARASITE WEAR:Nelgal -010122,PARASITE WEAR:Vajulla -010123,SENSE PLATE -010124,GRAVITON PLATE -010125,ATTRIBUTE PLATE -010126,FLOWEN'S FRAME -010127,CUSTOM FRAME Ver.00 -010128,DB's ARMOR -010129,GUARD WAVE -01012A,DF FIELD -01012B,LUMINOUS FIELD -01012C,CHU CHU FEVER -01012D,LOVE HEART -01012E,FLAME GARMENT -01012F,VIRUS ARMOR:Lafuteria -010130,BRIGHTNESS CIRCLE -010131,AURA FIELD -010132,ELECTRO FRAME -010133,SACRED CLOTH -010134,SMOKING PLATE -010135,STAR CUIRASS -010136,BLACK HOUND CUIRASS -010137,MORNING PRAYER -010138,BLACK ODOSHI DOMARU -010139,RED ODOSHI DOMARU -01013A,BLACK ODOSHI RED NIMAIDOU -01013B,BLUE ODOSHI VIOLET NIMAIDOU -01013C,DIRTY LIFEJACKET -01013D,KROE'S SWEATER -01013E,WEDDING DRESS -01013F,SONIC TEAM ARMOR -010140,RED COAT -010141,THIRTEEN -010142,MOTHER GARB -010143,MOTHER GARB+ -010144,DRESS PLATE -010145,SWEETHEART -010146,IGNITION CLOAK -010147,CONGEAL CLOAK -010148,TEMPEST CLOAK -010149,CURSED CLOAK -01014A,SELECT CLOAK -01014B,SPIRIT CUIRASS -01014C,REVIVAL CUIRASS -01014D,ALLIANCE UNIFORM -01014E,OFFICER UNIFORM -01014F,COMMANDER UNIFORM -010150,CRIMSON COAT -010151,INFANTRY GEAR -010152,LIEUTENANT GEAR -010153,INFANTRY MANTLE -010154,LIEUTENANT MANTLE -010155,UNION FIELD -010156,SAMURAI ARMOR -010157,STEALTH SUIT -010159,Torn Dragon Vest -010200,Barrier -010201,Shield -010202,Core Shield -010203,Giga Shield -010204,Soul Barrier -010205,Hard Shield -010206,Brave Barrier -010207,Solid Shield -010208,Flame Barrier -010209,Plasma Barrier -01020A,Freeze Barrier -01020B,Psychic Barrier -01020C,General Shield -01020D,Protect Barrier -01020E,Glorious Shield -01020F,Imperial Barrier -010210,Guardian Shield -010211,Divinity Barrier -010212,Ultimate Shield -010213,Spiritual Shield -010214,Celestial Shield -010215,INVISIBLE GUARD -010216,SACRED GUARD -010217,S-PARTS Ver1.16 -010218,S-PARTS Ver2.01 -010219,LIGHT RELIEF -01021A,SHIELD OF DELSABER -01021B,FORCE WALL -01021C,RANGER WALL -01021D,HUNTER WALL -01021E,ATTRIBUTE WALL -01021F,SECRET GEAR -010220,COMBAT GEAR -010221,PROTO REGENE GEAR -010222,REGENERATE GEAR -010223,REGENE GEAR ADV. -010224,FLOWEN'S SHIELD -010225,CUSTOM BARRIER Ver.00 -010226,DB'S SHIELD -010227,RED RING -010228,TRIPOLIC SHIELD -010229,STANDSTILL SHIELD -01022A,SAFETY HEART -01022B,KASAMI BRACER -01022C,GODS SHIELD SUZAKU -01022D,GODS SHIELD GENBU -01022E,GODS SHIELD BYAKKO -01022F,GODS SHIELD SEIRYU -010230,HUNTER'S SHELL -010231,RICO'S GLASSES -010232,RICO'S EARRING -010233,Barrier -010234,Barrier -010235,SECURE FEET -010236, -010237, -010238, -010239, -01023A,RESTA MERGE -01023B,ANTI MERGE -01023C,SHIFTA MERGE -01023D,DEBAND MERGE -01023E,FOIE MERGE -01023F,GIFOIE MERGE -010240,RAFOIE MERGE -010241,RED MERGE -010242,BARTA MERGE -010243,GIBARTA MERGE -010244,RABARTA MERGE -010245,BLUE MERGE -010246,ZONDE MERGE -010247,GIZONDE MERGE -010248,RAZONDE MERGE -010249,YELLOW MERGE -01024A,RECOVERY BARRIER -01024B,ASSIST BARRIER -01024C,RED BARRIER -01024D,BLUE BARRIER -01024E,YELLOW BARRIER -01024F,WEAPONS GOLD SHIELD -010250,BLACK GEAR -010251,WORKS GUARD -010252,RAGOL RING -010253,BLUE RING (7 Colors) -010254,BLUE RING -010255,BLUE RING -010256,BLUE RING -010257,BLUE RING -010258,BLUE RING -010259,BLUE RING (Real) -01025A,BLUE RING -01025B,GREEN RING -01025C,GREEN RING -01025D,GREEN RING -01025E,GREEN RING -01025F,GREEN RING (Real). -010260,GREEN RING -010261,GREEN RING -010262,GREEN RING -010263,YELLOW RING -010264,YELLOW RING -010265,YELLOW RING -010266,YELLOW RING (Real) -010267,YELLOW RING -010268,YELLOW RING -010269,YELLOW RING -01026A,YELLOW RING -01026B,PURPLE RING -01026C,PURPLE RING (Real). -01026D,PURPLE RING -01026E,PURPLE RING -01026F,PURPLE RING -010270,PURPLE RING -010271,PURPLE RING -010272,PURPLE RING -010273,ANTI-DARK RING -010274,WHITE RING -010275,WHITE RING (Real). -010276,WHITE RING -010277,WHITE RING -010278,WHITE RING -010279,WHITE RING -01027A,WHITE RING -01027B,ANTI-LIGHT RING -01027C,BLACK RING -01027D,BLACK RING -01027E,BLACK RING -01027F,BLACK RING -010280,BLACK RING (Real) -010281,BLACK RING -010282,BLACK RING -010283,WEAPONS SILVER SHIELD -010284,WEAPONS COPPER SHIELD -010285,GRATIA -010286,TRIPOLIC REFLECTOR -010287,STRIKER PLUS -010288,REGENERATE GEAR B.P. -010289,RUPIKA -01028A,YATA MIRROR -01028B,BUNNY EARS -01028C,CAT EARS -01028D,THREE SEALS -01028E,GOD'S SHIELD "KOURYU" -01028F,DF SHIELD -010290,FROM THE DEPTHS -010291,DE ROL LE SHIELD -010292,HONEYCOMB REFLECTOR -010293,EPSIGUARD -010294,ANGEL RING -010295,RED UNION GUARD -010296,GREEN UNION GUARD -010297,YELLOW UNION GUARD -010298,BLUE UNION GUARD -010299,STINK SHIELD -01029A,Black Gear -01029B,GENPEI, Real -01029C,GENPEI, Greenil -01029D,GENPEI, Skyly -01029E,GENPEI, Blueful -01029F,GENPEI, Purplenum -0102A0,GENPEI, Pinkal -0102A1,GENPEI, Redria -0102A2,GENPEI, Oran -0102A3,GENPEI, Yellowboze -0102A4,GENPEI, Whitill -010300,Knight/Power -010301,General/Power -010302,Ogre/Power -010303,God/Power -010304,Priest/Mind -010305,General/Mind -010306,Angel/Mind -010307,God/Mind -010308,Marksman/Arm -010309,General/Arm -01030A,Elf/Arm -01030B,God/Arm -01030C,Thief/Legs -01030D,General/Legs -01030E,Elf/Legs -01030F,God/Legs -010310,Digger/HP -010311,General/HP -010312,Dragon/HP -010313,God/HP -010314,Magician/TP -010315,General/TP -010316,Angle/TP -010317,God/TP -010318,Warrior/Body -010319,General/Body -01031A,Metal/Body -01031B,God/Body -01031C,Angel/Luck -01031D,God/Luck -01031E,Master/Ability -01031F,Hero/Ability -010320,God/Ability -010321,Resist/Fire -010322,Resist/Flame -010323,Resist/Burning -010324,Resist/Cold -010325,Resist/Freeze -010326,Resist/Blizzard -010327,Resist/Shock -010328,Resist/Thunder -010329,Resist/Storm -01032A,Resist/Light -01032B,Resist/Saint -01032C,Resist/Holy -01032D,Resist/Dark -01032E,Resist/Evil -01032F,Resist/Devil -010330,All/Resist -010331,Super/Resist -010332,Perfect/Resist -010333,HP/Restorate -010334,HP/Generate -010335,HP/Revival -010336,TP/Restorate -010337,TP/Generate -010338,TP/Revival -010339,PB/Amplifier -01033A,PB/Generate -01033B,PB/Create -01033C,Wizard/Technique -01033D,Devil/Technique -01033E,God/Technique -01033F,General/Battle -010340,Devil/Battle -010341,God/Battle -010342,CURE POISON -010343,CURE PARALYSIS -010344,CURE SLOW -010345,CURE CONFUSION -010346,CURE FREEZE -010347,CURE SHOCK -010348,YASAKANI MAGATAMA -010349,V101 -01034A,V501 -01034B,V502 -01034C,V801 -01034D,LIMITER -01034E,ADEPT -01034F,SWORDSMAN LORE -010350,PROOF OF SWORD-SAINT -010351,SMARTLINK -010352,DIVINE PROTECTION -010353,HEAVENLY/BATTLE -010354,HEAVENLY/POWER -010355,HEAVENLY/MIND -010356,HEAVENLY/ARMS -010357,HEAVENLY/LEGS -010358,HEAVENLY/BODY -010359,HEAVENLY/LUCK -01035A,HEAVENLY/ABILITY -01035B,CENTURION/ABILITY -01035B,Centurion/Ability -01035C,Friend Ring -01035D,Heavenly/HP -01035E,Heavenly/TP -01035F,Heavenly/Resist -010360,Heavenly/Technique -010361,HP/Resurrection -010362,TP/Resurrection -010363,PB/Increase -010366,Trap/Search -010367,HP/Rebirth -010368,TP/Rebirth -010369,PB/Boost -01036A,Centurion/Battle -030000,Monomate -030001,Dimate -030002,Trimate -030100,Monofluid -030101,Difluid -030102,Trifluid -030200,Disk: Lv.01 -030201,Disk: Lv.02 -030202,Disk: Lv.03 -030203,Disk: Lv.04 -030204,Disk: Lv.05 -030205,Disk: Lv.06 -030206,Disk: Lv.07 -030207,Disk: Lv.08 -030208,Disk: Lv.09 -030209,Disk: Lv.10 -03020A,Disk: Lv.11 -03020B,Disk: Lv.12 -03020C,Disk: Lv.13 -03020D,Disk: Lv.14 -03020E,Disk: Lv.15 -03020F,Disk: Lv.16 -030210,Disk: Lv.17 -030211,Disk: Lv.18 -030212,Disk: Lv.19 -030213,Disk: Lv.20 -030214,Disk: Lv.21 -030215,Disk: Lv.22 -030216,Disk: Lv.23 -030217,Disk: Lv.24 -030218,Disk: Lv.25 -030219,Disk: Lv.26 -03021A,Disk: Lv.27 -03021B,Disk: Lv.28 -03021C,Disk: Lv.29 -03021D,Disk: Lv.30 -030300,Sol Atomizer -030400,Moon Atomizer -030500,Star Atomizer -030600,Antidote -030601,Antiparalysis -030700,Telepipe -030800,Trap Vision -030900,Scape Doll -030A00,Monogrinder -030A01,Digrinder -030A02,Trigrinder -030B00,Power Material -030B01,Mind Material -030B02,Evade Material -030B03,HP Material -030B04,TP Material -030B05,Def Material -030B06,Luck Material -030C00,Cell Of MAG 502 -030C01,Cell Of MAG 213 -030C02,Parts Of RoboChao -030C03,Heart Of Opa Opa -030C04,Heart Of Pian -030C05,Heart Of Chao -030D00,Sorcerer's Right Arm -030D01,S-beat's Arms -030D02,P-arm's Arms -030D03,Delsabre's Right Arm -030D04,C-bringer's Right Arm -030D05,Delsabre's Left Arm -030D06,S-red's Arms -030D07,Dragon's Claw -030D08,Hildebear's Head -030D09,Hildeblue's Head -030D0A,Parts of Baranz -030D0B,Belra's Right Arms -030D0C,GIGUE'S ARMS -030D0D,S-BERILL'S ARMS -030D0E,G-Assasin's Arms -030D0F,Booma's Right Arm -030D10,Gobooma's Right Arm -030D11,GIGOBOOMA'S RIGHT ARMS -030D12,GAL WIND -030D13,Rappy's Wing -030D14,Cladding of Epsilon -030D15,De Rol Le Shell -030E00,BERILL PHOTON -030E01,PARASITIC GENE FLOW -030E02,MAGIC STONE IRITISTA -030E03,BLUE BLACK STONE -030E04,SYNCESTA -030E05,MAGIC WATER -030E06,PARASITIC CELL TYPE D -030E07,MAGIC ROCK HEART KEY -030E08,MAGIC ROCK MOOLA -030E09,STAR AMPLIFIER -030E0A,BOOK OF HITOGATA -030E0B,HEART OF CHU CHU -030E0C,PART OF EGG BLASTER -030E0D,HEART OF ANGEL -030E0E,HEART OF DEVIL -030E0F,KIT OF HAMBERGER -030E10,PANTHER'S SPIRIT -030E11,KIT OF MARK3 -030E12,KIT OF MASTER SYSTEM -030E13,KIT OF GENESIS -030E14,KIT OF SEGA SATURN -030E15,KIT OF DREAMCAST -030E16,Amplifier of Resta -030E17,Amplifier of Anti -030E18,Amplifier of Shifta -030E19,Amplifier of Deband -030E1A,Amplifier of Foie -030E1B,Amplifier of Gifoie -030E1C,Amplifier of Rafoie -030E1D,Amplifier of Barta -030E1E,Amplifier of Gibarta -030E1F,Amplifier of Rabarta -030E20,Amplifier of Zonde -030E21,Amplifier of Gizonde -030E22,Amplifier of Razonde -030E23,Amplifier of Red -030E24,Amplifier of Blue -030E25,Amplifier of Yellow -030E26,HEART OF KAPUKAPU -030E27,PHOTON BOOSTER -030F00,Add Slot -031000,PHOTON DROP -031001,PHOTON SPHERE -031002,PHOTON CRYSTAL -031003,Secret Ticket -031004,Photon Ticket -031100,BOOK OF KATANA 1 -031101,BOOK OF KATANA 2 -031102,BOOK OF KATANA 3 -031200,WEAPONS BRONZE BADGE -031201,WEAPONS SILVER BADGE -031202,WEAPONS GOLD BADGE -031203,WEAPONS CRYSTAL BADGE -031204,WEAPONS STEEL BADGE -031205,WEAPONS ALUMINUM BADGE -031206,WEAPONS LEATHER BADGE -031207,WEAPONS BONE BADGE -031209,Item Exchange Ticket -03120A,VALENTINE'S CHOCOLATE -031210,Flower Bouquet -031211,Cake -031212,Accessories -031213,Mr.Naka's Business Card -031300,PRESENT -031400,CHOCOLATE -031401,CANDY -031402,CAKE -031403,SILVER BADGE -031404,GOLD BADGE -031405,CRYSTAL BADGE -031406,IRON BADGE -031407,ALUMINUM BADGE -031408,LEATHER BADGE -031409,BONE BADGE -03140A,BONQUET -03140B,DECOCTION -031500,CHRISTMAS PRESENT -031501,EASTER EGG -031502,JACK-O'S-LANTERN -031600,DISK VOL.1 -031601,DISK VOL.2 -031602,DISK VOL.3 -031603,DISK VOL.4 -031604,DISK VOL.5 -031605,DISK VOL.6 -031606,DISK VOL.7 -031607,DISK VOL.8 -031608,DISK VOL.9 -031609,DISK VOL.10 -03160A,DISK VOL.11 -03160B,DISK VOL.12 -031700,HUNTERS REPORT -031701,HUNTERS REPORT RANK A -031702,HUNTERS REPORT RANK B -031703,HUNTERS REPORT RANK C -031704,HUNTERS REPORT RANK F -031800,Tablet -031802,Dragon Scale -031803,Heaven Striker Coat -031804,Pioneer Parts -031805,Amitie's Memo -031806,Heart of Morolian -031807,Rappy's Beak -031809,D-Photon Core -03180A,Liberta Kit -03180B,Catnip -03180C,Puyo DNA -03180D,Cell of MAG 505 -03180F,Cell of MAG 507 -031900,Team Points 500 -031901,Team Points 1000 -031902,Team Points 5000 -031903,Team Points 10000 -010365,Stat/maintenance -031503,Bronze Easter Egg -031504,Silver Easter Egg -031505,Gold Easter Egg -0102a5,Battlemage's Bracer -0102a6,The Ultimate Shield -010364,Bad/Luck -010366,Trap/Search -010367,Hp/Rebirth -010368,TP/Rebirth -010369,PB/Boost -002101,Buzz Sawd -00030a,Double Booma Claws -00030b,Double Gobooma Claws -00030c,Double Gigobooma Claws -00040a,Hildeblue's Hammer -000409,Hildebear's Hammer -000d04,Inferno Claw -002202,Sigurd's Staff -002601,Suppressed Autogun -002602,Suppressed Lockgun -002603,Suppressed Railgun -002604,Suppressed Raygun -00930a,The Ace Of Spades -009501,Partisan Of Ice -009502,Partisan Of Fire -009801,Rika's Claw Mk.2 -006a01,Yasminkov 2001H -006501,Yasminkov 3001R -006b01,Yasminkov 7001V -006c01,Yasminkov 9001M -006c02,Doom Binger -008904,Texas Massacres -010158,Battlemage's Cloak -010159,Torn Dragon Vest -0102a5,Battlemage's Bracer -0102a6,The Ultimate Shield -010364,Bad/Luck -010366,Trap/Search -010367,Hp/Rebirth -010368,TP/Rebirth -010369,PB/Boost -01036a,Centurion/Battle -030e28,Quantum Booster -030e29,Enhanced Scope -010365,Stat/maintenance -031503,Bronze Easter Egg -031504,Silver Easter Egg -031505,Gold Easter Egg -031808,Yahoo's Engine -00D001,Rainblow -001302,Love Ray -0102A7,Rainbow Ring -001201,Dragon Needle -001301,Death Ray -020000,Mag -030280,Lvl30 Foie -030281,Lvl30 Gifoie -030282,Lvl30 Rafoie -030283,Lvl30 Barta -030284,Lvl30 Gibarta -030285,Lvl30 Rabarta -030286,Lvl30 Zonde -030287,Lvl30 Gizonde -030288,Lvl30 Razonde -030289,Lvl30 Grants -03028A,Lvl30 Deband -03028B,Lvl30 Jellen -03028C,Lvl30 Zalure -03028D,Lvl30 Shifta -03028E,Lvl30 Ryuker -03028F,Lvl30 Resta -030290,Lvl30 Anti -030291,Lvl30 Reverser -030292,Lvl30 Megid -000208,Emperor -000E05,Borealis [000E05] -000809,M&D09 YASMINKOV [000809] -006B02,12G Remmington, "BOOMSTICK" [006B02] -000909,Bowcaster of Ares [000909] -009B01,Attitude of Aphrodite [009B01] \ No newline at end of file diff --git a/src/Account.cc b/src/Account.cc index 4f567713..94378cd4 100644 --- a/src/Account.cc +++ b/src/Account.cc @@ -1,1003 +1,1003 @@ -#include -#include -#include - -#include -#include -#include -#include - -#include "Account.hh" - -using namespace std; - -shared_ptr DCNTELicense::from_json(const JSON& json) { - auto ret = make_shared(); - ret->serial_number = json.get_string("SerialNumber"); - ret->access_key = json.get_string("AccessKey"); - if (ret->serial_number.size() > 16) { - throw runtime_error("serial number is too long"); - } - if (ret->serial_number.empty()) { - throw runtime_error("serial number is too short"); - } - if (ret->access_key.size() > 16) { - throw runtime_error("access key is too long"); - } - if (ret->access_key.empty()) { - throw runtime_error("access key is too short"); - } - return ret; -} - -JSON DCNTELicense::json() const { - return JSON::dict({ - {"SerialNumber", this->serial_number}, - {"AccessKey", this->access_key}, - }); -} - -shared_ptr V1V2License::from_json(const JSON& json) { - auto ret = make_shared(); - ret->serial_number = json.get_int("SerialNumber"); - ret->access_key = json.get_string("AccessKey"); - if (ret->serial_number == 0) { - throw runtime_error("serial number is zero"); - } - if (ret->access_key.size() != 8) { - throw runtime_error("access key length is incorrect"); - } - return ret; -} - -JSON V1V2License::json() const { - return JSON::dict({ - {"SerialNumber", this->serial_number}, - {"AccessKey", this->access_key}, - }); -} - -shared_ptr GCLicense::from_json(const JSON& json) { - auto ret = make_shared(); - ret->serial_number = json.get_int("SerialNumber"); - ret->access_key = json.get_string("AccessKey"); - ret->password = json.get_string("Password"); - if (ret->serial_number == 0) { - throw runtime_error("serial number is zero"); - } - if (ret->access_key.size() != 12) { - throw runtime_error("access key length is incorrect"); - } - if (ret->password.empty()) { - throw runtime_error("password is too short"); - } - return ret; -} - -JSON GCLicense::json() const { - return JSON::dict({ - {"SerialNumber", this->serial_number}, - {"AccessKey", this->access_key}, - {"Password", this->password}, - }); -} - -shared_ptr XBLicense::from_json(const JSON& json) { - auto ret = make_shared(); - ret->gamertag = json.get_string("GamerTag"); - ret->user_id = json.get_int("UserID"); - ret->account_id = json.get_int("AccountID"); - if (ret->gamertag.empty()) { - throw runtime_error("gamertag is too short"); - } - if (ret->user_id == 0) { - throw runtime_error("user ID is zero"); - } - if (ret->account_id == 0) { - throw runtime_error("account ID is zero"); - } - return ret; -} - -JSON XBLicense::json() const { - return JSON::dict({ - {"GamerTag", this->gamertag}, - {"UserID", this->user_id}, - {"AccountID", this->account_id}, - }); -} - -shared_ptr BBLicense::from_json(const JSON& json) { - auto ret = make_shared(); - ret->username = json.get_string("UserName"); - ret->password = json.get_string("Password"); - if (ret->username.size() > 16) { - throw runtime_error("username is too long"); - } - if (ret->username.empty()) { - throw runtime_error("username is too short"); - } - if (ret->password.size() > 16) { - throw runtime_error("password is too long"); - } - if (ret->password.empty()) { - throw runtime_error("password is too short"); - } - return ret; -} - -JSON BBLicense::json() const { - return JSON::dict({ - {"UserName", this->username}, - {"Password", this->password}, - }); -} - -Account::Account(const JSON& json) - : account_id(0), - flags(0), - ban_end_time(0), - ep3_current_meseta(0), - ep3_total_meseta_earned(0), - bb_team_id(0) { - uint64_t format_version = 0; - try { - format_version = json.get_int("FormatVersion"); - } catch (const out_of_range&) { - } - - if (format_version == 0) { - // Original format - no account ID - this->account_id = json.get_int("SerialNumber"); - string access_key = json.get_string("AccessKey", ""); - string dc_nte_serial_number = json.get_string("DCNTESerialNumber", ""); - string dc_nte_access_key = json.get_string("DCNTEAccessKey", ""); - string gc_password = json.get_string("GCPassword", ""); - string xb_gamertag = json.get_string("XBGamerTag", ""); - uint64_t xb_user_id = json.get_int("XBUserID", 0); - uint64_t xb_account_id = json.get_int("XBAccountID", 0); - string bb_username = json.get_string("BBUsername", ""); - string bb_password = json.get_string("BBPassword", ""); - if (access_key.size() == 12) { - if (!gc_password.empty()) { - auto lic = make_shared(); - lic->serial_number = this->account_id; - lic->access_key = access_key; - lic->password = gc_password; - this->gc_licenses.emplace(lic->serial_number, lic); - } - } else if (access_key.size() >= 8) { - auto lic = make_shared(); - lic->serial_number = this->account_id; - lic->access_key = access_key.substr(0, 8); - this->dc_licenses.emplace(lic->serial_number, lic); - this->pc_licenses.emplace(lic->serial_number, lic); - } - if (!dc_nte_serial_number.empty() && !dc_nte_access_key.empty()) { - auto lic = make_shared(); - lic->serial_number = dc_nte_serial_number; - lic->access_key = dc_nte_access_key; - this->dc_nte_licenses.emplace(lic->serial_number, lic); - } - if (!xb_gamertag.empty() && xb_user_id && xb_account_id) { - auto lic = make_shared(); - lic->gamertag = xb_gamertag; - lic->user_id = xb_user_id; - lic->account_id = xb_account_id; - this->xb_licenses.emplace(lic->gamertag, lic); - } - if (!bb_username.empty() && !bb_password.empty()) { - auto lic = make_shared(); - lic->username = bb_username; - lic->password = bb_password; - this->bb_licenses.emplace(lic->username, lic); - } - } else { - // Second-gen format - with account ID; multiple credentials per version - this->account_id = json.get_int("AccountID"); - for (const auto& it : json.get_list("DCNTELicenses")) { - auto lic = DCNTELicense::from_json(*it); - this->dc_nte_licenses.emplace(lic->serial_number, lic); - } - for (const auto& it : json.get_list("DCLicenses")) { - auto lic = V1V2License::from_json(*it); - this->dc_licenses.emplace(lic->serial_number, lic); - } - for (const auto& it : json.get_list("PCLicenses")) { - auto lic = V1V2License::from_json(*it); - this->pc_licenses.emplace(lic->serial_number, lic); - } - for (const auto& it : json.get_list("GCLicenses")) { - auto lic = GCLicense::from_json(*it); - this->gc_licenses.emplace(lic->serial_number, lic); - } - for (const auto& it : json.get_list("XBLicenses")) { - auto lic = XBLicense::from_json(*it); - this->xb_licenses.emplace(lic->gamertag, lic); - } - for (const auto& it : json.get_list("BBLicenses")) { - auto lic = BBLicense::from_json(*it); - this->bb_licenses.emplace(lic->username, lic); - } - } - - this->flags = json.get_int("Flags", 0); - this->ban_end_time = json.get_int("BanEndTime", 0); - this->last_player_name = json.get_string("LastPlayerName", ""); - this->auto_reply_message = json.get_string("AutoReplyMessage", ""); - this->ep3_current_meseta = json.get_int("Ep3CurrentMeseta", 0); - this->ep3_total_meseta_earned = json.get_int("Ep3TotalMesetaEarned", 0); - this->bb_team_id = json.get_int("BBTeamID", 0); - - try { - for (const auto& it : json.get_list("AutoPatchesEnabled")) { - this->auto_patches_enabled.emplace(it->as_string()); - } - } catch (const out_of_range&) { - } -} - -JSON Account::json() const { - JSON dc_nte_json = JSON::list(); - for (const auto& it : this->dc_nte_licenses) { - dc_nte_json.emplace_back(it.second->json()); - } - JSON dc_json = JSON::list(); - for (const auto& it : this->dc_licenses) { - dc_json.emplace_back(it.second->json()); - } - JSON pc_json = JSON::list(); - for (const auto& it : this->pc_licenses) { - pc_json.emplace_back(it.second->json()); - } - JSON gc_json = JSON::list(); - for (const auto& it : this->gc_licenses) { - gc_json.emplace_back(it.second->json()); - } - JSON xb_json = JSON::list(); - for (const auto& it : this->xb_licenses) { - xb_json.emplace_back(it.second->json()); - } - JSON bb_json = JSON::list(); - for (const auto& it : this->bb_licenses) { - bb_json.emplace_back(it.second->json()); - } - - JSON auto_patches_json = JSON::list(); - for (const auto& it : this->auto_patches_enabled) { - auto_patches_json.emplace_back(it); - } - - return JSON::dict({ - {"FormatVersion", 1}, - {"AccountID", this->account_id}, - {"DCNTELicenses", std::move(dc_nte_json)}, - {"DCLicenses", std::move(dc_json)}, - {"PCLicenses", std::move(pc_json)}, - {"GCLicenses", std::move(gc_json)}, - {"XBLicenses", std::move(xb_json)}, - {"BBLicenses", std::move(bb_json)}, - {"Flags", this->flags}, - {"BanEndTime", this->ban_end_time}, - {"LastPlayerName", this->last_player_name}, - {"AutoReplyMessage", this->auto_reply_message}, - {"Ep3CurrentMeseta", this->ep3_current_meseta}, - {"Ep3TotalMesetaEarned", this->ep3_total_meseta_earned}, - {"BBTeamID", this->bb_team_id}, - {"AutoPatchesEnabled", std::move(auto_patches_json)}, - }); -} - -void Account::print(FILE* stream) const { - fprintf(stream, "Account: %010" PRIu32 "/%08" PRIX32 "\n", this->account_id, this->account_id); - - if (this->flags) { - string flags_str = ""; - if (this->flags == static_cast(Flag::ROOT)) { - flags_str = "ROOT"; - } else if (this->flags == static_cast(Flag::ADMINISTRATOR)) { - flags_str = "ADMINISTRATOR"; - } else if (this->flags == static_cast(Flag::MODERATOR)) { - flags_str = "MODERATOR"; - } else { - if (this->flags & static_cast(Flag::KICK_USER)) { - flags_str += "KICK_USER,"; - } - if (this->flags & static_cast(Flag::BAN_USER)) { - flags_str += "BAN_USER,"; - } - if (this->flags & static_cast(Flag::SILENCE_USER)) { - flags_str += "SILENCE_USER,"; - } - if (this->flags & static_cast(Flag::CHANGE_EVENT)) { - flags_str += "CHANGE_EVENT,"; - } - if (this->flags & static_cast(Flag::ANNOUNCE)) { - flags_str += "ANNOUNCE,"; - } - if (this->flags & static_cast(Flag::FREE_JOIN_GAMES)) { - flags_str += "FREE_JOIN_GAMES,"; - } - if (this->flags & static_cast(Flag::DEBUG)) { - flags_str += "DEBUG,"; - } - if (this->flags & static_cast(Flag::CHEAT_ANYWHERE)) { - flags_str += "CHEAT_ANYWHERE,"; - } - if (this->flags & static_cast(Flag::DISABLE_QUEST_REQUIREMENTS)) { - flags_str += "ALWAYS_ENABLE_CHAT_COMMANDS,"; - } - if (this->flags & static_cast(Flag::IS_SHARED_ACCOUNT)) { - flags_str += "IS_SHARED_ACCOUNT,"; - } - } - if (flags_str.empty()) { - flags_str = "none"; - } else if (ends_with(flags_str, ",")) { - flags_str.pop_back(); - } - fprintf(stream, " Flags: %08" PRIX32 " (%s)\n", this->flags, flags_str.c_str()); - } - - if (this->ban_end_time) { - string time_str = format_time(this->ban_end_time); - fprintf(stream, " Banned until: %" PRIu64 " (%s)\n", this->ban_end_time, time_str.c_str()); - } - if (this->ep3_current_meseta || this->ep3_total_meseta_earned) { - fprintf(stream, " Episode 3 meseta: %" PRIu32 " (total earned: %" PRIu32 ")\n", this->ep3_current_meseta, this->ep3_total_meseta_earned); - } - if (!this->last_player_name.empty()) { - fprintf(stream, " Last player name: \"%s\"\n", this->last_player_name.c_str()); - } - if (!this->auto_reply_message.empty()) { - fprintf(stream, " Auto reply message: \"%s\"\n", this->auto_reply_message.c_str()); - } - if (this->bb_team_id) { - fprintf(stream, " BB team ID: %08" PRIX32 "\n", this->bb_team_id); - } - if (this->is_temporary) { - fprintf(stream, " Is temporary license: true\n"); - } - - for (const auto& it : this->dc_nte_licenses) { - fprintf(stream, " DC NTE license: serial_number=%s access_key=%s\n", - it.second->serial_number.c_str(), it.second->access_key.c_str()); - } - for (const auto& it : this->dc_licenses) { - fprintf(stream, " DC license: serial_number=%" PRIX32 " access_key=%s\n", - it.second->serial_number, it.second->access_key.c_str()); - } - for (const auto& it : this->pc_licenses) { - fprintf(stream, " PC license: serial_number=%" PRIX32 " access_key=%s\n", - it.second->serial_number, it.second->access_key.c_str()); - } - for (const auto& it : this->gc_licenses) { - fprintf(stream, " GC license: serial_number=%010" PRIu32 " access_key=%s password=%s\n", - it.second->serial_number, it.second->access_key.c_str(), it.second->password.c_str()); - } - for (const auto& it : this->xb_licenses) { - fprintf(stream, " XB license: gamertag=%s user_id=%016" PRIX64 " account_id=%016" PRIX64 "\n", - it.second->gamertag.c_str(), it.second->user_id, it.second->account_id); - } - for (const auto& it : this->bb_licenses) { - fprintf(stream, " BB license: username=%s password=%s\n", - it.second->username.c_str(), it.second->password.c_str()); - } -} - -void Account::save() const { - if (!this->is_temporary) { - auto json = this->json(); - string json_data = json.serialize(JSON::SerializeOption::FORMAT | JSON::SerializeOption::HEX_INTEGERS); - string filename = string_printf("system/licenses/%010" PRIu32 ".json", this->account_id); - save_file(filename, json_data); - } -} - -void Account::delete_file() const { - string filename = string_printf("system/licenses/%010" PRIu32 ".json", this->account_id); - remove(filename.c_str()); -} - -size_t AccountIndex::count() const { - shared_lock g(this->lock); - return this->by_account_id.size(); -} - -shared_ptr AccountIndex::from_account_id(uint32_t account_id) const { - try { - shared_lock g(this->lock); - return this->by_account_id.at(account_id); - } catch (const out_of_range&) { - throw missing_account(); - } -} - -shared_ptr AccountIndex::from_dc_nte_credentials_locked(const string& serial_number, const string& access_key) { - auto login = make_shared(); - login->account = this->by_dc_nte_serial_number.at(serial_number); - login->dc_nte_license = login->account->dc_nte_licenses.at(serial_number); - if (login->dc_nte_license->access_key != access_key) { - throw incorrect_access_key(); - } - if (login->account->ban_end_time && (login->account->ban_end_time >= now())) { - throw invalid_argument("user is banned"); - } - return login; -} - -shared_ptr AccountIndex::from_dc_nte_credentials( - const string& serial_number, const string& access_key, bool allow_create) { - if (serial_number.empty()) { - throw no_username(); - } - - try { - shared_lock g(this->lock); - return this->from_dc_nte_credentials_locked(serial_number, access_key); - } catch (const out_of_range&) { - } - - unique_lock g(this->lock); - try { - return this->from_dc_nte_credentials_locked(serial_number, access_key); - } catch (const out_of_range&) { - } - - if (allow_create) { - auto login = make_shared(); - login->account_was_created = true; - login->account = make_shared(); - login->account->account_id = fnv1a32(serial_number) & 0x7FFFFFFF; - auto lic = make_shared(); - lic->serial_number = serial_number; - lic->access_key = access_key; - login->account->dc_nte_licenses.emplace(lic->serial_number, lic); - login->dc_nte_license = lic; - this->add_locked(login->account); - return login; - } else { - throw missing_account(); - } -} - -shared_ptr AccountIndex::from_dc_credentials_locked( - uint32_t serial_number, const string& access_key, const string& character_name) { - auto login = make_shared(); - login->account = this->by_dc_serial_number.at(serial_number); - login->dc_license = login->account->dc_licenses.at(serial_number); - bool is_shared = login->account->check_flag(Account::Flag::IS_SHARED_ACCOUNT); - if (!is_shared && (login->dc_license->access_key != access_key)) { - throw incorrect_access_key(); - } - if (login->account->ban_end_time && (login->account->ban_end_time >= now())) { - throw invalid_argument("user is banned"); - } - if (is_shared) { - login->account = this->create_temporary_account_for_shared_account(login->account, access_key + ":" + character_name); - } - return login; -} - -shared_ptr AccountIndex::from_dc_credentials( - uint32_t serial_number, const string& access_key, const string& character_name, bool allow_create) { - if (serial_number == 0) { - throw no_username(); - } - - try { - shared_lock g(this->lock); - return this->from_dc_credentials_locked(serial_number, access_key, character_name); - } catch (const out_of_range&) { - } - - unique_lock g(this->lock); - try { - return this->from_dc_credentials_locked(serial_number, access_key, character_name); - } catch (const out_of_range&) { - } - - if (allow_create) { - auto login = make_shared(); - login->account_was_created = true; - login->account = make_shared(); - login->account->account_id = serial_number; - auto lic = make_shared(); - lic->serial_number = serial_number; - lic->access_key = access_key; - login->account->dc_licenses.emplace(lic->serial_number, lic); - login->dc_license = lic; - this->add_locked(login->account); - return login; - } else { - throw missing_account(); - } -} - -shared_ptr AccountIndex::from_pc_nte_credentials(uint32_t guild_card_number, bool allow_create) { - if (!allow_create) { - throw missing_account(); - } - if (guild_card_number == 0xFFFFFFFF) { - guild_card_number = random_object() & 0x7FFFFFFF; - } - auto login = make_shared(); - login->account_was_created = true; - login->account = make_shared(); - login->account->account_id = guild_card_number; - login->account->is_temporary = true; - auto lic = make_shared(); - lic->serial_number = guild_card_number; - login->account->pc_licenses.emplace(lic->serial_number, lic); - login->pc_license = lic; - this->add(login->account); - return login; -} - -shared_ptr AccountIndex::from_pc_credentials_locked( - uint32_t serial_number, const string& access_key, const string& character_name) { - auto login = make_shared(); - login->account = this->by_pc_serial_number.at(serial_number); - login->pc_license = login->account->pc_licenses.at(serial_number); - bool is_shared = login->account->check_flag(Account::Flag::IS_SHARED_ACCOUNT); - if (!is_shared && (login->pc_license->access_key != access_key)) { - throw incorrect_access_key(); - } - if (login->account->ban_end_time && (login->account->ban_end_time >= now())) { - throw invalid_argument("user is banned"); - } - if (is_shared) { - login->account = this->create_temporary_account_for_shared_account(login->account, access_key + ":" + character_name); - } - return login; -} - -shared_ptr AccountIndex::from_pc_credentials( - uint32_t serial_number, const string& access_key, const string& character_name, bool allow_create) { - if (serial_number == 0) { - throw no_username(); - } - - try { - shared_lock g(this->lock); - return this->from_pc_credentials_locked(serial_number, access_key, character_name); - } catch (const out_of_range&) { - } - - unique_lock g(this->lock); - try { - return this->from_pc_credentials_locked(serial_number, access_key, character_name); - } catch (const out_of_range&) { - } - - if (allow_create) { - auto login = make_shared(); - login->account_was_created = true; - login->account = make_shared(); - login->account->account_id = serial_number; - auto lic = make_shared(); - lic->serial_number = serial_number; - lic->access_key = access_key; - login->account->pc_licenses.emplace(lic->serial_number, lic); - login->pc_license = lic; - this->add_locked(login->account); - return login; - } else { - throw missing_account(); - } -} - -shared_ptr AccountIndex::from_gc_credentials_locked( - uint32_t serial_number, const string& access_key, const string* password, const string& character_name) { - auto login = make_shared(); - login->account = this->by_gc_serial_number.at(serial_number); - login->gc_license = login->account->gc_licenses.at(serial_number); - bool is_shared = login->account->check_flag(Account::Flag::IS_SHARED_ACCOUNT); - if (!is_shared && (login->gc_license->access_key != access_key)) { - throw incorrect_access_key(); - } - if (password && (login->gc_license->password != *password)) { - throw incorrect_password(); - } - if (login->account->ban_end_time && (login->account->ban_end_time >= now())) { - throw invalid_argument("user is banned"); - } - if (is_shared) { - login->account = this->create_temporary_account_for_shared_account(login->account, access_key + ":" + character_name); - } - return login; -} - -shared_ptr AccountIndex::from_gc_credentials( - uint32_t serial_number, const string& access_key, const string* password, const string& character_name, bool allow_create) { - if (serial_number == 0) { - throw no_username(); - } - - try { - shared_lock g(this->lock); - return this->from_gc_credentials_locked(serial_number, access_key, password, character_name); - } catch (const out_of_range&) { - } - - unique_lock g(this->lock); - try { - return this->from_gc_credentials_locked(serial_number, access_key, password, character_name); - } catch (const out_of_range&) { - } - - if (allow_create && password) { - auto login = make_shared(); - login->account_was_created = true; - login->account = make_shared(); - login->account->account_id = serial_number; - auto lic = make_shared(); - lic->serial_number = serial_number; - lic->access_key = access_key; - lic->password = *password; - login->account->gc_licenses.emplace(lic->serial_number, lic); - login->gc_license = lic; - this->add_locked(login->account); - return login; - } else { - throw missing_account(); - } -} - -shared_ptr AccountIndex::from_xb_credentials_locked(const string& gamertag, uint64_t user_id, uint64_t account_id) { - auto login = make_shared(); - login->account = this->by_xb_gamertag.at(gamertag); - login->xb_license = login->account->xb_licenses.at(gamertag); - if ((login->xb_license->user_id && (login->xb_license->user_id != user_id)) || - (login->xb_license->account_id && (login->xb_license->account_id != account_id))) { - throw incorrect_access_key(); - } - if (login->account->ban_end_time && (login->account->ban_end_time >= now())) { - throw invalid_argument("user is banned"); - } - return login; -} - -shared_ptr AccountIndex::from_xb_credentials( - const string& gamertag, uint64_t user_id, uint64_t account_id, bool allow_create) { - if (user_id == 0 || account_id == 0) { - throw incorrect_access_key(); - } - - try { - shared_lock g(this->lock); - return this->from_xb_credentials_locked(gamertag, user_id, account_id); - } catch (const out_of_range&) { - } - - unique_lock g(this->lock); - try { - return this->from_xb_credentials_locked(gamertag, user_id, account_id); - } catch (const out_of_range&) { - } - - if (allow_create) { - auto login = make_shared(); - login->account_was_created = true; - login->account = make_shared(); - login->account->account_id = fnv1a32(gamertag) & 0x7FFFFFFF; - auto lic = make_shared(); - lic->gamertag = gamertag; - lic->user_id = user_id; - lic->account_id = account_id; - login->account->xb_licenses.emplace(lic->gamertag, lic); - login->xb_license = lic; - this->add_locked(login->account); - return login; - } else { - throw missing_account(); - } -} - -shared_ptr AccountIndex::from_bb_credentials_locked(const string& username, const string* password) { - auto login = make_shared(); - login->account = this->by_bb_username.at(username); - login->bb_license = login->account->bb_licenses.at(username); - if (password && (login->bb_license->password != *password)) { - throw incorrect_password(); - } - if (login->account->ban_end_time && (login->account->ban_end_time >= now())) { - throw invalid_argument("user is banned"); - } - return login; -} - -shared_ptr AccountIndex::from_bb_credentials(const string& username, const string* password, bool allow_create) { - if (username.empty() || (password && password->empty())) { - throw no_username(); - } - - try { - shared_lock g(this->lock); - return this->from_bb_credentials_locked(username, password); - } catch (const out_of_range&) { - } - - unique_lock g(this->lock); - try { - return this->from_bb_credentials_locked(username, password); - } catch (const out_of_range&) { - } - - if (allow_create && password) { - auto login = make_shared(); - login->account_was_created = true; - login->account = make_shared(); - login->account->account_id = fnv1a32(username) & 0x7FFFFFFF; - auto lic = make_shared(); - lic->username = username; - lic->password = *password; - login->account->bb_licenses.emplace(lic->username, lic); - login->bb_license = lic; - this->add_locked(login->account); - return login; - } else { - throw missing_account(); - } -} - -vector> AccountIndex::all() const { - shared_lock g(this->lock); - vector> ret; - ret.reserve(this->by_account_id.size()); - for (const auto& it : this->by_account_id) { - ret.emplace_back(it.second); - } - return ret; -} - -void AccountIndex::add(shared_ptr a) { - unique_lock g(this->lock); - this->add_locked(a); -} - -void AccountIndex::add_locked(shared_ptr a) { - if (this->force_all_temporary) { - a->is_temporary = true; - } - - for (const auto& it : a->dc_nte_licenses) { - if (this->by_dc_nte_serial_number.count(it.second->serial_number)) { - throw runtime_error("account already exists with this DC NTE serial number"); - } - } - for (const auto& it : a->dc_licenses) { - if (this->by_dc_serial_number.count(it.second->serial_number)) { - throw runtime_error("account already exists with this DC serial number"); - } - } - for (const auto& it : a->pc_licenses) { - if (this->by_pc_serial_number.count(it.second->serial_number)) { - throw runtime_error("account already exists with this PC NTE serial number"); - } - } - for (const auto& it : a->gc_licenses) { - if (this->by_gc_serial_number.count(it.second->serial_number)) { - throw runtime_error("account already exists with this GC serial number"); - } - } - for (const auto& it : a->xb_licenses) { - if (this->by_xb_gamertag.count(it.second->gamertag)) { - throw runtime_error("account already exists with this XB gamertag"); - } - } - for (const auto& it : a->bb_licenses) { - if (this->by_bb_username.count(it.second->username)) { - throw runtime_error("account already exists with this BB username"); - } - } - - while (this->by_account_id.count(a->account_id) || !a->account_id || (a->account_id == 0xFFFFFFFF)) { - a->account_id = (a->account_id + 1) & 0x7FFFFFFF; - } - - this->by_account_id[a->account_id] = a; - for (const auto& it : a->dc_nte_licenses) { - this->by_dc_nte_serial_number[it.second->serial_number] = a; - } - for (const auto& it : a->dc_licenses) { - this->by_dc_serial_number[it.second->serial_number] = a; - } - for (const auto& it : a->pc_licenses) { - this->by_pc_serial_number[it.second->serial_number] = a; - } - for (const auto& it : a->gc_licenses) { - this->by_gc_serial_number[it.second->serial_number] = a; - } - for (const auto& it : a->xb_licenses) { - this->by_xb_gamertag[it.second->gamertag] = a; - } - for (const auto& it : a->bb_licenses) { - this->by_bb_username[it.second->username] = a; - } -} - -void AccountIndex::remove(uint32_t account_id) { - unique_lock g(this->lock); - auto acc_it = this->by_account_id.find(account_id); - if (acc_it == this->by_account_id.end()) { - throw out_of_range("account does not exist"); - } - auto a = std::move(acc_it->second); - this->by_account_id.erase(acc_it); - - for (const auto& it : a->dc_nte_licenses) { - this->by_dc_nte_serial_number.erase(it.second->serial_number); - } - for (const auto& it : a->dc_licenses) { - this->by_dc_serial_number.erase(it.second->serial_number); - } - for (const auto& it : a->pc_licenses) { - this->by_pc_serial_number.erase(it.second->serial_number); - } - for (const auto& it : a->gc_licenses) { - this->by_gc_serial_number.erase(it.second->serial_number); - } - for (const auto& it : a->xb_licenses) { - this->by_xb_gamertag.erase(it.second->gamertag); - } - for (const auto& it : a->bb_licenses) { - this->by_bb_username.erase(it.second->username); - } -} - -void AccountIndex::add_dc_nte_license(shared_ptr account, shared_ptr license) { - if (!this->by_dc_nte_serial_number.emplace(license->serial_number, account).second) { - throw runtime_error("serial number already registered"); - } - if (!account->dc_nte_licenses.emplace(license->serial_number, license).second) { - this->by_dc_nte_serial_number.erase(license->serial_number); - throw logic_error("serial number registered in account but not in account index"); - } -} - -void AccountIndex::add_dc_license(shared_ptr account, shared_ptr license) { - if (!this->by_dc_serial_number.emplace(license->serial_number, account).second) { - throw runtime_error("serial number already registered"); - } - if (!account->dc_licenses.emplace(license->serial_number, license).second) { - this->by_dc_serial_number.erase(license->serial_number); - throw logic_error("serial number registered in account but not in account index"); - } -} - -void AccountIndex::add_pc_license(shared_ptr account, shared_ptr license) { - if (!this->by_pc_serial_number.emplace(license->serial_number, account).second) { - throw runtime_error("serial number already registered"); - } - if (!account->pc_licenses.emplace(license->serial_number, license).second) { - this->by_pc_serial_number.erase(license->serial_number); - throw logic_error("serial number registered in account but not in account index"); - } -} - -void AccountIndex::add_gc_license(shared_ptr account, shared_ptr license) { - if (!this->by_gc_serial_number.emplace(license->serial_number, account).second) { - throw runtime_error("serial number already registered"); - } - if (!account->gc_licenses.emplace(license->serial_number, license).second) { - this->by_gc_serial_number.erase(license->serial_number); - throw logic_error("serial number registered in account but not in account index"); - } -} - -void AccountIndex::add_xb_license(shared_ptr account, shared_ptr license) { - if (!this->by_xb_gamertag.emplace(license->gamertag, account).second) { - throw runtime_error("gamertag already registered"); - } - if (!account->xb_licenses.emplace(license->gamertag, license).second) { - this->by_xb_gamertag.erase(license->gamertag); - throw logic_error("gamertag registered in account but not in account index"); - } -} - -void AccountIndex::add_bb_license(shared_ptr account, shared_ptr license) { - if (!this->by_bb_username.emplace(license->username, account).second) { - throw runtime_error("username already registered"); - } - if (!account->bb_licenses.emplace(license->username, license).second) { - this->by_bb_username.erase(license->username); - throw logic_error("username registered in account but not in account index"); - } -} - -void AccountIndex::remove_dc_nte_license(shared_ptr account, const string& serial_number) { - auto it = account->dc_nte_licenses.find(serial_number); - if (it == account->dc_nte_licenses.end()) { - throw runtime_error("license not registered to account"); - } - if (!this->by_dc_nte_serial_number.erase(it->second->serial_number)) { - throw runtime_error("license registered in account but not in account index"); - } - account->dc_nte_licenses.erase(it); -} - -void AccountIndex::remove_dc_license(shared_ptr account, uint32_t serial_number) { - auto it = account->dc_licenses.find(serial_number); - if (it == account->dc_licenses.end()) { - throw runtime_error("license not registered to account"); - } - if (!this->by_dc_serial_number.erase(it->second->serial_number)) { - throw runtime_error("license registered in account but not in account index"); - } - account->dc_licenses.erase(it); -} - -void AccountIndex::remove_pc_license(shared_ptr account, uint32_t serial_number) { - auto it = account->pc_licenses.find(serial_number); - if (it == account->pc_licenses.end()) { - throw runtime_error("license not registered to account"); - } - if (!this->by_pc_serial_number.erase(it->second->serial_number)) { - throw runtime_error("license registered in account but not in account index"); - } - account->pc_licenses.erase(it); -} - -void AccountIndex::remove_gc_license(shared_ptr account, uint32_t serial_number) { - auto it = account->gc_licenses.find(serial_number); - if (it == account->gc_licenses.end()) { - throw runtime_error("license not registered to account"); - } - if (!this->by_gc_serial_number.erase(it->second->serial_number)) { - throw runtime_error("license registered in account but not in account index"); - } - account->gc_licenses.erase(it); -} - -void AccountIndex::remove_xb_license(shared_ptr account, const string& gamertag) { - auto it = account->xb_licenses.find(gamertag); - if (it == account->xb_licenses.end()) { - throw runtime_error("license not registered to account"); - } - if (!this->by_xb_gamertag.erase(it->second->gamertag)) { - throw runtime_error("license registered in account but not in account index"); - } - account->xb_licenses.erase(it); -} - -void AccountIndex::remove_bb_license(shared_ptr account, const string& username) { - auto it = account->bb_licenses.find(username); - if (it == account->bb_licenses.end()) { - throw runtime_error("license not registered to account"); - } - if (!this->by_bb_username.erase(it->second->username)) { - throw runtime_error("license registered in account but not in account index"); - } - account->bb_licenses.erase(it); -} - -shared_ptr AccountIndex::create_temporary_account_for_shared_account( - shared_ptr src_a, const string& variation_data) const { - auto ret = make_shared(*src_a); - ret->is_temporary = true; - ret->account_id = fnv1a32(&src_a->account_id, sizeof(src_a->account_id)); - ret->account_id = fnv1a32(variation_data, ret->account_id); - return ret; -} - -AccountIndex::AccountIndex(bool force_all_temporary) - : force_all_temporary(force_all_temporary) { - if (!this->force_all_temporary) { - if (!isdir("system/licenses")) { - mkdir("system/licenses", 0755); - } else { - for (const auto& item : list_directory("system/licenses")) { - if (ends_with(item, ".json")) { - try { - JSON json = JSON::parse(load_file("system/licenses/" + item)); - this->add(make_shared(json)); - } catch (const exception& e) { - log_error("Failed to index account %s", item.c_str()); - throw; - } - } - } - } - } -} +#include +#include +#include + +#include +#include +#include +#include + +#include "Account.hh" + +using namespace std; + +shared_ptr DCNTELicense::from_json(const JSON& json) { + auto ret = make_shared(); + ret->serial_number = json.get_string("SerialNumber"); + ret->access_key = json.get_string("AccessKey"); + if (ret->serial_number.size() > 16) { + throw runtime_error("serial number is too long"); + } + if (ret->serial_number.empty()) { + throw runtime_error("serial number is too short"); + } + if (ret->access_key.size() > 16) { + throw runtime_error("access key is too long"); + } + if (ret->access_key.empty()) { + throw runtime_error("access key is too short"); + } + return ret; +} + +JSON DCNTELicense::json() const { + return JSON::dict({ + {"SerialNumber", this->serial_number}, + {"AccessKey", this->access_key}, + }); +} + +shared_ptr V1V2License::from_json(const JSON& json) { + auto ret = make_shared(); + ret->serial_number = json.get_int("SerialNumber"); + ret->access_key = json.get_string("AccessKey"); + if (ret->serial_number == 0) { + throw runtime_error("serial number is zero"); + } + if (ret->access_key.size() != 8) { + throw runtime_error("access key length is incorrect"); + } + return ret; +} + +JSON V1V2License::json() const { + return JSON::dict({ + {"SerialNumber", this->serial_number}, + {"AccessKey", this->access_key}, + }); +} + +shared_ptr GCLicense::from_json(const JSON& json) { + auto ret = make_shared(); + ret->serial_number = json.get_int("SerialNumber"); + ret->access_key = json.get_string("AccessKey"); + ret->password = json.get_string("Password"); + if (ret->serial_number == 0) { + throw runtime_error("serial number is zero"); + } + if (ret->access_key.size() != 12) { + throw runtime_error("access key length is incorrect"); + } + if (ret->password.empty()) { + throw runtime_error("password is too short"); + } + return ret; +} + +JSON GCLicense::json() const { + return JSON::dict({ + {"SerialNumber", this->serial_number}, + {"AccessKey", this->access_key}, + {"Password", this->password}, + }); +} + +shared_ptr XBLicense::from_json(const JSON& json) { + auto ret = make_shared(); + ret->gamertag = json.get_string("GamerTag"); + ret->user_id = json.get_int("UserID"); + ret->account_id = json.get_int("AccountID"); + if (ret->gamertag.empty()) { + throw runtime_error("gamertag is too short"); + } + if (ret->user_id == 0) { + throw runtime_error("user ID is zero"); + } + if (ret->account_id == 0) { + throw runtime_error("account ID is zero"); + } + return ret; +} + +JSON XBLicense::json() const { + return JSON::dict({ + {"GamerTag", this->gamertag}, + {"UserID", this->user_id}, + {"AccountID", this->account_id}, + }); +} + +shared_ptr BBLicense::from_json(const JSON& json) { + auto ret = make_shared(); + ret->username = json.get_string("UserName"); + ret->password = json.get_string("Password"); + if (ret->username.size() > 16) { + throw runtime_error("username is too long"); + } + if (ret->username.empty()) { + throw runtime_error("username is too short"); + } + if (ret->password.size() > 16) { + throw runtime_error("password is too long"); + } + if (ret->password.empty()) { + throw runtime_error("password is too short"); + } + return ret; +} + +JSON BBLicense::json() const { + return JSON::dict({ + {"UserName", this->username}, + {"Password", this->password}, + }); +} + +Account::Account(const JSON& json) + : account_id(0), + flags(0), + ban_end_time(0), + ep3_current_meseta(0), + ep3_total_meseta_earned(0), + bb_team_id(0) { + uint64_t format_version = 0; + try { + format_version = json.get_int("FormatVersion"); + } catch (const out_of_range&) { + } + + if (format_version == 0) { + // Original format - no account ID + this->account_id = json.get_int("SerialNumber"); + string access_key = json.get_string("AccessKey", ""); + string dc_nte_serial_number = json.get_string("DCNTESerialNumber", ""); + string dc_nte_access_key = json.get_string("DCNTEAccessKey", ""); + string gc_password = json.get_string("GCPassword", ""); + string xb_gamertag = json.get_string("XBGamerTag", ""); + uint64_t xb_user_id = json.get_int("XBUserID", 0); + uint64_t xb_account_id = json.get_int("XBAccountID", 0); + string bb_username = json.get_string("BBUsername", ""); + string bb_password = json.get_string("BBPassword", ""); + if (access_key.size() == 12) { + if (!gc_password.empty()) { + auto lic = make_shared(); + lic->serial_number = this->account_id; + lic->access_key = access_key; + lic->password = gc_password; + this->gc_licenses.emplace(lic->serial_number, lic); + } + } else if (access_key.size() >= 8) { + auto lic = make_shared(); + lic->serial_number = this->account_id; + lic->access_key = access_key.substr(0, 8); + this->dc_licenses.emplace(lic->serial_number, lic); + this->pc_licenses.emplace(lic->serial_number, lic); + } + if (!dc_nte_serial_number.empty() && !dc_nte_access_key.empty()) { + auto lic = make_shared(); + lic->serial_number = dc_nte_serial_number; + lic->access_key = dc_nte_access_key; + this->dc_nte_licenses.emplace(lic->serial_number, lic); + } + if (!xb_gamertag.empty() && xb_user_id && xb_account_id) { + auto lic = make_shared(); + lic->gamertag = xb_gamertag; + lic->user_id = xb_user_id; + lic->account_id = xb_account_id; + this->xb_licenses.emplace(lic->gamertag, lic); + } + if (!bb_username.empty() && !bb_password.empty()) { + auto lic = make_shared(); + lic->username = bb_username; + lic->password = bb_password; + this->bb_licenses.emplace(lic->username, lic); + } + } else { + // Second-gen format - with account ID; multiple credentials per version + this->account_id = json.get_int("AccountID"); + for (const auto& it : json.get_list("DCNTELicenses")) { + auto lic = DCNTELicense::from_json(*it); + this->dc_nte_licenses.emplace(lic->serial_number, lic); + } + for (const auto& it : json.get_list("DCLicenses")) { + auto lic = V1V2License::from_json(*it); + this->dc_licenses.emplace(lic->serial_number, lic); + } + for (const auto& it : json.get_list("PCLicenses")) { + auto lic = V1V2License::from_json(*it); + this->pc_licenses.emplace(lic->serial_number, lic); + } + for (const auto& it : json.get_list("GCLicenses")) { + auto lic = GCLicense::from_json(*it); + this->gc_licenses.emplace(lic->serial_number, lic); + } + for (const auto& it : json.get_list("XBLicenses")) { + auto lic = XBLicense::from_json(*it); + this->xb_licenses.emplace(lic->gamertag, lic); + } + for (const auto& it : json.get_list("BBLicenses")) { + auto lic = BBLicense::from_json(*it); + this->bb_licenses.emplace(lic->username, lic); + } + } + + this->flags = json.get_int("Flags", 0); + this->ban_end_time = json.get_int("BanEndTime", 0); + this->last_player_name = json.get_string("LastPlayerName", ""); + this->auto_reply_message = json.get_string("AutoReplyMessage", ""); + this->ep3_current_meseta = json.get_int("Ep3CurrentMeseta", 0); + this->ep3_total_meseta_earned = json.get_int("Ep3TotalMesetaEarned", 0); + this->bb_team_id = json.get_int("BBTeamID", 0); + + try { + for (const auto& it : json.get_list("AutoPatchesEnabled")) { + this->auto_patches_enabled.emplace(it->as_string()); + } + } catch (const out_of_range&) { + } +} + +JSON Account::json() const { + JSON dc_nte_json = JSON::list(); + for (const auto& it : this->dc_nte_licenses) { + dc_nte_json.emplace_back(it.second->json()); + } + JSON dc_json = JSON::list(); + for (const auto& it : this->dc_licenses) { + dc_json.emplace_back(it.second->json()); + } + JSON pc_json = JSON::list(); + for (const auto& it : this->pc_licenses) { + pc_json.emplace_back(it.second->json()); + } + JSON gc_json = JSON::list(); + for (const auto& it : this->gc_licenses) { + gc_json.emplace_back(it.second->json()); + } + JSON xb_json = JSON::list(); + for (const auto& it : this->xb_licenses) { + xb_json.emplace_back(it.second->json()); + } + JSON bb_json = JSON::list(); + for (const auto& it : this->bb_licenses) { + bb_json.emplace_back(it.second->json()); + } + + JSON auto_patches_json = JSON::list(); + for (const auto& it : this->auto_patches_enabled) { + auto_patches_json.emplace_back(it); + } + + return JSON::dict({ + {"FormatVersion", 1}, + {"AccountID", this->account_id}, + {"DCNTELicenses", std::move(dc_nte_json)}, + {"DCLicenses", std::move(dc_json)}, + {"PCLicenses", std::move(pc_json)}, + {"GCLicenses", std::move(gc_json)}, + {"XBLicenses", std::move(xb_json)}, + {"BBLicenses", std::move(bb_json)}, + {"Flags", this->flags}, + {"BanEndTime", this->ban_end_time}, + {"LastPlayerName", this->last_player_name}, + {"AutoReplyMessage", this->auto_reply_message}, + {"Ep3CurrentMeseta", this->ep3_current_meseta}, + {"Ep3TotalMesetaEarned", this->ep3_total_meseta_earned}, + {"BBTeamID", this->bb_team_id}, + {"AutoPatchesEnabled", std::move(auto_patches_json)}, + }); +} + +void Account::print(FILE* stream) const { + fprintf(stream, "Account: %010" PRIu32 "/%08" PRIX32 "\n", this->account_id, this->account_id); + + if (this->flags) { + string flags_str = ""; + if (this->flags == static_cast(Flag::ROOT)) { + flags_str = "ROOT"; + } else if (this->flags == static_cast(Flag::ADMINISTRATOR)) { + flags_str = "ADMINISTRATOR"; + } else if (this->flags == static_cast(Flag::MODERATOR)) { + flags_str = "MODERATOR"; + } else { + if (this->flags & static_cast(Flag::KICK_USER)) { + flags_str += "KICK_USER,"; + } + if (this->flags & static_cast(Flag::BAN_USER)) { + flags_str += "BAN_USER,"; + } + if (this->flags & static_cast(Flag::SILENCE_USER)) { + flags_str += "SILENCE_USER,"; + } + if (this->flags & static_cast(Flag::CHANGE_EVENT)) { + flags_str += "CHANGE_EVENT,"; + } + if (this->flags & static_cast(Flag::ANNOUNCE)) { + flags_str += "ANNOUNCE,"; + } + if (this->flags & static_cast(Flag::FREE_JOIN_GAMES)) { + flags_str += "FREE_JOIN_GAMES,"; + } + if (this->flags & static_cast(Flag::DEBUG)) { + flags_str += "DEBUG,"; + } + if (this->flags & static_cast(Flag::CHEAT_ANYWHERE)) { + flags_str += "CHEAT_ANYWHERE,"; + } + if (this->flags & static_cast(Flag::DISABLE_QUEST_REQUIREMENTS)) { + flags_str += "ALWAYS_ENABLE_CHAT_COMMANDS,"; + } + if (this->flags & static_cast(Flag::IS_SHARED_ACCOUNT)) { + flags_str += "IS_SHARED_ACCOUNT,"; + } + } + if (flags_str.empty()) { + flags_str = "none"; + } else if (ends_with(flags_str, ",")) { + flags_str.pop_back(); + } + fprintf(stream, " Flags: %08" PRIX32 " (%s)\n", this->flags, flags_str.c_str()); + } + + if (this->ban_end_time) { + string time_str = format_time(this->ban_end_time); + fprintf(stream, " Banned until: %" PRIu64 " (%s)\n", this->ban_end_time, time_str.c_str()); + } + if (this->ep3_current_meseta || this->ep3_total_meseta_earned) { + fprintf(stream, " Episode 3 meseta: %" PRIu32 " (total earned: %" PRIu32 ")\n", this->ep3_current_meseta, this->ep3_total_meseta_earned); + } + if (!this->last_player_name.empty()) { + fprintf(stream, " Last player name: \"%s\"\n", this->last_player_name.c_str()); + } + if (!this->auto_reply_message.empty()) { + fprintf(stream, " Auto reply message: \"%s\"\n", this->auto_reply_message.c_str()); + } + if (this->bb_team_id) { + fprintf(stream, " BB team ID: %08" PRIX32 "\n", this->bb_team_id); + } + if (this->is_temporary) { + fprintf(stream, " Is temporary license: true\n"); + } + + for (const auto& it : this->dc_nte_licenses) { + fprintf(stream, " DC NTE license: serial_number=%s access_key=%s\n", + it.second->serial_number.c_str(), it.second->access_key.c_str()); + } + for (const auto& it : this->dc_licenses) { + fprintf(stream, " DC license: serial_number=%" PRIX32 " access_key=%s\n", + it.second->serial_number, it.second->access_key.c_str()); + } + for (const auto& it : this->pc_licenses) { + fprintf(stream, " PC license: serial_number=%" PRIX32 " access_key=%s\n", + it.second->serial_number, it.second->access_key.c_str()); + } + for (const auto& it : this->gc_licenses) { + fprintf(stream, " GC license: serial_number=%010" PRIu32 " access_key=%s password=%s\n", + it.second->serial_number, it.second->access_key.c_str(), it.second->password.c_str()); + } + for (const auto& it : this->xb_licenses) { + fprintf(stream, " XB license: gamertag=%s user_id=%016" PRIX64 " account_id=%016" PRIX64 "\n", + it.second->gamertag.c_str(), it.second->user_id, it.second->account_id); + } + for (const auto& it : this->bb_licenses) { + fprintf(stream, " BB license: username=%s password=%s\n", + it.second->username.c_str(), it.second->password.c_str()); + } +} + +void Account::save() const { + if (!this->is_temporary) { + auto json = this->json(); + string json_data = json.serialize(JSON::SerializeOption::FORMAT | JSON::SerializeOption::HEX_INTEGERS); + string filename = string_printf("system/licenses/%010" PRIu32 ".json", this->account_id); + save_file(filename, json_data); + } +} + +void Account::delete_file() const { + string filename = string_printf("system/licenses/%010" PRIu32 ".json", this->account_id); + remove(filename.c_str()); +} + +size_t AccountIndex::count() const { + shared_lock g(this->lock); + return this->by_account_id.size(); +} + +shared_ptr AccountIndex::from_account_id(uint32_t account_id) const { + try { + shared_lock g(this->lock); + return this->by_account_id.at(account_id); + } catch (const out_of_range&) { + throw missing_account(); + } +} + +shared_ptr AccountIndex::from_dc_nte_credentials_locked(const string& serial_number, const string& access_key) { + auto login = make_shared(); + login->account = this->by_dc_nte_serial_number.at(serial_number); + login->dc_nte_license = login->account->dc_nte_licenses.at(serial_number); + if (login->dc_nte_license->access_key != access_key) { + throw incorrect_access_key(); + } + if (login->account->ban_end_time && (login->account->ban_end_time >= now())) { + throw invalid_argument("user is banned"); + } + return login; +} + +shared_ptr AccountIndex::from_dc_nte_credentials( + const string& serial_number, const string& access_key, bool allow_create) { + if (serial_number.empty()) { + throw no_username(); + } + + try { + shared_lock g(this->lock); + return this->from_dc_nte_credentials_locked(serial_number, access_key); + } catch (const out_of_range&) { + } + + unique_lock g(this->lock); + try { + return this->from_dc_nte_credentials_locked(serial_number, access_key); + } catch (const out_of_range&) { + } + + if (allow_create) { + auto login = make_shared(); + login->account_was_created = true; + login->account = make_shared(); + login->account->account_id = fnv1a32(serial_number) & 0x7FFFFFFF; + auto lic = make_shared(); + lic->serial_number = serial_number; + lic->access_key = access_key; + login->account->dc_nte_licenses.emplace(lic->serial_number, lic); + login->dc_nte_license = lic; + this->add_locked(login->account); + return login; + } else { + throw missing_account(); + } +} + +shared_ptr AccountIndex::from_dc_credentials_locked( + uint32_t serial_number, const string& access_key, const string& character_name) { + auto login = make_shared(); + login->account = this->by_dc_serial_number.at(serial_number); + login->dc_license = login->account->dc_licenses.at(serial_number); + bool is_shared = login->account->check_flag(Account::Flag::IS_SHARED_ACCOUNT); + if (!is_shared && (login->dc_license->access_key != access_key)) { + throw incorrect_access_key(); + } + if (login->account->ban_end_time && (login->account->ban_end_time >= now())) { + throw invalid_argument("user is banned"); + } + if (is_shared) { + login->account = this->create_temporary_account_for_shared_account(login->account, access_key + ":" + character_name); + } + return login; +} + +shared_ptr AccountIndex::from_dc_credentials( + uint32_t serial_number, const string& access_key, const string& character_name, bool allow_create) { + if (serial_number == 0) { + throw no_username(); + } + + try { + shared_lock g(this->lock); + return this->from_dc_credentials_locked(serial_number, access_key, character_name); + } catch (const out_of_range&) { + } + + unique_lock g(this->lock); + try { + return this->from_dc_credentials_locked(serial_number, access_key, character_name); + } catch (const out_of_range&) { + } + + if (allow_create) { + auto login = make_shared(); + login->account_was_created = true; + login->account = make_shared(); + login->account->account_id = serial_number; + auto lic = make_shared(); + lic->serial_number = serial_number; + lic->access_key = access_key; + login->account->dc_licenses.emplace(lic->serial_number, lic); + login->dc_license = lic; + this->add_locked(login->account); + return login; + } else { + throw missing_account(); + } +} + +shared_ptr AccountIndex::from_pc_nte_credentials(uint32_t guild_card_number, bool allow_create) { + if (!allow_create) { + throw missing_account(); + } + if (guild_card_number == 0xFFFFFFFF) { + guild_card_number = random_object() & 0x7FFFFFFF; + } + auto login = make_shared(); + login->account_was_created = true; + login->account = make_shared(); + login->account->account_id = guild_card_number; + login->account->is_temporary = true; + auto lic = make_shared(); + lic->serial_number = guild_card_number; + login->account->pc_licenses.emplace(lic->serial_number, lic); + login->pc_license = lic; + this->add(login->account); + return login; +} + +shared_ptr AccountIndex::from_pc_credentials_locked( + uint32_t serial_number, const string& access_key, const string& character_name) { + auto login = make_shared(); + login->account = this->by_pc_serial_number.at(serial_number); + login->pc_license = login->account->pc_licenses.at(serial_number); + bool is_shared = login->account->check_flag(Account::Flag::IS_SHARED_ACCOUNT); + if (!is_shared && (login->pc_license->access_key != access_key)) { + throw incorrect_access_key(); + } + if (login->account->ban_end_time && (login->account->ban_end_time >= now())) { + throw invalid_argument("user is banned"); + } + if (is_shared) { + login->account = this->create_temporary_account_for_shared_account(login->account, access_key + ":" + character_name); + } + return login; +} + +shared_ptr AccountIndex::from_pc_credentials( + uint32_t serial_number, const string& access_key, const string& character_name, bool allow_create) { + if (serial_number == 0) { + throw no_username(); + } + + try { + shared_lock g(this->lock); + return this->from_pc_credentials_locked(serial_number, access_key, character_name); + } catch (const out_of_range&) { + } + + unique_lock g(this->lock); + try { + return this->from_pc_credentials_locked(serial_number, access_key, character_name); + } catch (const out_of_range&) { + } + + if (allow_create) { + auto login = make_shared(); + login->account_was_created = true; + login->account = make_shared(); + login->account->account_id = serial_number; + auto lic = make_shared(); + lic->serial_number = serial_number; + lic->access_key = access_key; + login->account->pc_licenses.emplace(lic->serial_number, lic); + login->pc_license = lic; + this->add_locked(login->account); + return login; + } else { + throw missing_account(); + } +} + +shared_ptr AccountIndex::from_gc_credentials_locked( + uint32_t serial_number, const string& access_key, const string* password, const string& character_name) { + auto login = make_shared(); + login->account = this->by_gc_serial_number.at(serial_number); + login->gc_license = login->account->gc_licenses.at(serial_number); + bool is_shared = login->account->check_flag(Account::Flag::IS_SHARED_ACCOUNT); + if (!is_shared && (login->gc_license->access_key != access_key)) { + throw incorrect_access_key(); + } + if (password && (login->gc_license->password != *password)) { + throw incorrect_password(); + } + if (login->account->ban_end_time && (login->account->ban_end_time >= now())) { + throw invalid_argument("user is banned"); + } + if (is_shared) { + login->account = this->create_temporary_account_for_shared_account(login->account, access_key + ":" + character_name); + } + return login; +} + +shared_ptr AccountIndex::from_gc_credentials( + uint32_t serial_number, const string& access_key, const string* password, const string& character_name, bool allow_create) { + if (serial_number == 0) { + throw no_username(); + } + + try { + shared_lock g(this->lock); + return this->from_gc_credentials_locked(serial_number, access_key, password, character_name); + } catch (const out_of_range&) { + } + + unique_lock g(this->lock); + try { + return this->from_gc_credentials_locked(serial_number, access_key, password, character_name); + } catch (const out_of_range&) { + } + + if (allow_create && password) { + auto login = make_shared(); + login->account_was_created = true; + login->account = make_shared(); + login->account->account_id = serial_number; + auto lic = make_shared(); + lic->serial_number = serial_number; + lic->access_key = access_key; + lic->password = *password; + login->account->gc_licenses.emplace(lic->serial_number, lic); + login->gc_license = lic; + this->add_locked(login->account); + return login; + } else { + throw missing_account(); + } +} + +shared_ptr AccountIndex::from_xb_credentials_locked(const string& gamertag, uint64_t user_id, uint64_t account_id) { + auto login = make_shared(); + login->account = this->by_xb_gamertag.at(gamertag); + login->xb_license = login->account->xb_licenses.at(gamertag); + if ((login->xb_license->user_id && (login->xb_license->user_id != user_id)) || + (login->xb_license->account_id && (login->xb_license->account_id != account_id))) { + throw incorrect_access_key(); + } + if (login->account->ban_end_time && (login->account->ban_end_time >= now())) { + throw invalid_argument("user is banned"); + } + return login; +} + +shared_ptr AccountIndex::from_xb_credentials( + const string& gamertag, uint64_t user_id, uint64_t account_id, bool allow_create) { + if (user_id == 0 || account_id == 0) { + throw incorrect_access_key(); + } + + try { + shared_lock g(this->lock); + return this->from_xb_credentials_locked(gamertag, user_id, account_id); + } catch (const out_of_range&) { + } + + unique_lock g(this->lock); + try { + return this->from_xb_credentials_locked(gamertag, user_id, account_id); + } catch (const out_of_range&) { + } + + if (allow_create) { + auto login = make_shared(); + login->account_was_created = true; + login->account = make_shared(); + login->account->account_id = fnv1a32(gamertag) & 0x7FFFFFFF; + auto lic = make_shared(); + lic->gamertag = gamertag; + lic->user_id = user_id; + lic->account_id = account_id; + login->account->xb_licenses.emplace(lic->gamertag, lic); + login->xb_license = lic; + this->add_locked(login->account); + return login; + } else { + throw missing_account(); + } +} + +shared_ptr AccountIndex::from_bb_credentials_locked(const string& username, const string* password) { + auto login = make_shared(); + login->account = this->by_bb_username.at(username); + login->bb_license = login->account->bb_licenses.at(username); + if (password && (login->bb_license->password != *password)) { + throw incorrect_password(); + } + if (login->account->ban_end_time && (login->account->ban_end_time >= now())) { + throw invalid_argument("user is banned"); + } + return login; +} + +shared_ptr AccountIndex::from_bb_credentials(const string& username, const string* password, bool allow_create) { + if (username.empty() || (password && password->empty())) { + throw no_username(); + } + + try { + shared_lock g(this->lock); + return this->from_bb_credentials_locked(username, password); + } catch (const out_of_range&) { + } + + unique_lock g(this->lock); + try { + return this->from_bb_credentials_locked(username, password); + } catch (const out_of_range&) { + } + + if (allow_create && password) { + auto login = make_shared(); + login->account_was_created = true; + login->account = make_shared(); + login->account->account_id = fnv1a32(username) & 0x7FFFFFFF; + auto lic = make_shared(); + lic->username = username; + lic->password = *password; + login->account->bb_licenses.emplace(lic->username, lic); + login->bb_license = lic; + this->add_locked(login->account); + return login; + } else { + throw missing_account(); + } +} + +vector> AccountIndex::all() const { + shared_lock g(this->lock); + vector> ret; + ret.reserve(this->by_account_id.size()); + for (const auto& it : this->by_account_id) { + ret.emplace_back(it.second); + } + return ret; +} + +void AccountIndex::add(shared_ptr a) { + unique_lock g(this->lock); + this->add_locked(a); +} + +void AccountIndex::add_locked(shared_ptr a) { + if (this->force_all_temporary) { + a->is_temporary = true; + } + + for (const auto& it : a->dc_nte_licenses) { + if (this->by_dc_nte_serial_number.count(it.second->serial_number)) { + throw runtime_error("account already exists with this DC NTE serial number"); + } + } + for (const auto& it : a->dc_licenses) { + if (this->by_dc_serial_number.count(it.second->serial_number)) { + throw runtime_error("account already exists with this DC serial number"); + } + } + for (const auto& it : a->pc_licenses) { + if (this->by_pc_serial_number.count(it.second->serial_number)) { + throw runtime_error("account already exists with this PC NTE serial number"); + } + } + for (const auto& it : a->gc_licenses) { + if (this->by_gc_serial_number.count(it.second->serial_number)) { + throw runtime_error("account already exists with this GC serial number"); + } + } + for (const auto& it : a->xb_licenses) { + if (this->by_xb_gamertag.count(it.second->gamertag)) { + throw runtime_error("account already exists with this XB gamertag"); + } + } + for (const auto& it : a->bb_licenses) { + if (this->by_bb_username.count(it.second->username)) { + throw runtime_error("account already exists with this BB username"); + } + } + + while (this->by_account_id.count(a->account_id) || !a->account_id || (a->account_id == 0xFFFFFFFF)) { + a->account_id = (a->account_id + 1) & 0x7FFFFFFF; + } + + this->by_account_id[a->account_id] = a; + for (const auto& it : a->dc_nte_licenses) { + this->by_dc_nte_serial_number[it.second->serial_number] = a; + } + for (const auto& it : a->dc_licenses) { + this->by_dc_serial_number[it.second->serial_number] = a; + } + for (const auto& it : a->pc_licenses) { + this->by_pc_serial_number[it.second->serial_number] = a; + } + for (const auto& it : a->gc_licenses) { + this->by_gc_serial_number[it.second->serial_number] = a; + } + for (const auto& it : a->xb_licenses) { + this->by_xb_gamertag[it.second->gamertag] = a; + } + for (const auto& it : a->bb_licenses) { + this->by_bb_username[it.second->username] = a; + } +} + +void AccountIndex::remove(uint32_t account_id) { + unique_lock g(this->lock); + auto acc_it = this->by_account_id.find(account_id); + if (acc_it == this->by_account_id.end()) { + throw out_of_range("account does not exist"); + } + auto a = std::move(acc_it->second); + this->by_account_id.erase(acc_it); + + for (const auto& it : a->dc_nte_licenses) { + this->by_dc_nte_serial_number.erase(it.second->serial_number); + } + for (const auto& it : a->dc_licenses) { + this->by_dc_serial_number.erase(it.second->serial_number); + } + for (const auto& it : a->pc_licenses) { + this->by_pc_serial_number.erase(it.second->serial_number); + } + for (const auto& it : a->gc_licenses) { + this->by_gc_serial_number.erase(it.second->serial_number); + } + for (const auto& it : a->xb_licenses) { + this->by_xb_gamertag.erase(it.second->gamertag); + } + for (const auto& it : a->bb_licenses) { + this->by_bb_username.erase(it.second->username); + } +} + +void AccountIndex::add_dc_nte_license(shared_ptr account, shared_ptr license) { + if (!this->by_dc_nte_serial_number.emplace(license->serial_number, account).second) { + throw runtime_error("serial number already registered"); + } + if (!account->dc_nte_licenses.emplace(license->serial_number, license).second) { + this->by_dc_nte_serial_number.erase(license->serial_number); + throw logic_error("serial number registered in account but not in account index"); + } +} + +void AccountIndex::add_dc_license(shared_ptr account, shared_ptr license) { + if (!this->by_dc_serial_number.emplace(license->serial_number, account).second) { + throw runtime_error("serial number already registered"); + } + if (!account->dc_licenses.emplace(license->serial_number, license).second) { + this->by_dc_serial_number.erase(license->serial_number); + throw logic_error("serial number registered in account but not in account index"); + } +} + +void AccountIndex::add_pc_license(shared_ptr account, shared_ptr license) { + if (!this->by_pc_serial_number.emplace(license->serial_number, account).second) { + throw runtime_error("serial number already registered"); + } + if (!account->pc_licenses.emplace(license->serial_number, license).second) { + this->by_pc_serial_number.erase(license->serial_number); + throw logic_error("serial number registered in account but not in account index"); + } +} + +void AccountIndex::add_gc_license(shared_ptr account, shared_ptr license) { + if (!this->by_gc_serial_number.emplace(license->serial_number, account).second) { + throw runtime_error("serial number already registered"); + } + if (!account->gc_licenses.emplace(license->serial_number, license).second) { + this->by_gc_serial_number.erase(license->serial_number); + throw logic_error("serial number registered in account but not in account index"); + } +} + +void AccountIndex::add_xb_license(shared_ptr account, shared_ptr license) { + if (!this->by_xb_gamertag.emplace(license->gamertag, account).second) { + throw runtime_error("gamertag already registered"); + } + if (!account->xb_licenses.emplace(license->gamertag, license).second) { + this->by_xb_gamertag.erase(license->gamertag); + throw logic_error("gamertag registered in account but not in account index"); + } +} + +void AccountIndex::add_bb_license(shared_ptr account, shared_ptr license) { + if (!this->by_bb_username.emplace(license->username, account).second) { + throw runtime_error("username already registered"); + } + if (!account->bb_licenses.emplace(license->username, license).second) { + this->by_bb_username.erase(license->username); + throw logic_error("username registered in account but not in account index"); + } +} + +void AccountIndex::remove_dc_nte_license(shared_ptr account, const string& serial_number) { + auto it = account->dc_nte_licenses.find(serial_number); + if (it == account->dc_nte_licenses.end()) { + throw runtime_error("license not registered to account"); + } + if (!this->by_dc_nte_serial_number.erase(it->second->serial_number)) { + throw runtime_error("license registered in account but not in account index"); + } + account->dc_nte_licenses.erase(it); +} + +void AccountIndex::remove_dc_license(shared_ptr account, uint32_t serial_number) { + auto it = account->dc_licenses.find(serial_number); + if (it == account->dc_licenses.end()) { + throw runtime_error("license not registered to account"); + } + if (!this->by_dc_serial_number.erase(it->second->serial_number)) { + throw runtime_error("license registered in account but not in account index"); + } + account->dc_licenses.erase(it); +} + +void AccountIndex::remove_pc_license(shared_ptr account, uint32_t serial_number) { + auto it = account->pc_licenses.find(serial_number); + if (it == account->pc_licenses.end()) { + throw runtime_error("license not registered to account"); + } + if (!this->by_pc_serial_number.erase(it->second->serial_number)) { + throw runtime_error("license registered in account but not in account index"); + } + account->pc_licenses.erase(it); +} + +void AccountIndex::remove_gc_license(shared_ptr account, uint32_t serial_number) { + auto it = account->gc_licenses.find(serial_number); + if (it == account->gc_licenses.end()) { + throw runtime_error("license not registered to account"); + } + if (!this->by_gc_serial_number.erase(it->second->serial_number)) { + throw runtime_error("license registered in account but not in account index"); + } + account->gc_licenses.erase(it); +} + +void AccountIndex::remove_xb_license(shared_ptr account, const string& gamertag) { + auto it = account->xb_licenses.find(gamertag); + if (it == account->xb_licenses.end()) { + throw runtime_error("license not registered to account"); + } + if (!this->by_xb_gamertag.erase(it->second->gamertag)) { + throw runtime_error("license registered in account but not in account index"); + } + account->xb_licenses.erase(it); +} + +void AccountIndex::remove_bb_license(shared_ptr account, const string& username) { + auto it = account->bb_licenses.find(username); + if (it == account->bb_licenses.end()) { + throw runtime_error("license not registered to account"); + } + if (!this->by_bb_username.erase(it->second->username)) { + throw runtime_error("license registered in account but not in account index"); + } + account->bb_licenses.erase(it); +} + +shared_ptr AccountIndex::create_temporary_account_for_shared_account( + shared_ptr src_a, const string& variation_data) const { + auto ret = make_shared(*src_a); + ret->is_temporary = true; + ret->account_id = fnv1a32(&src_a->account_id, sizeof(src_a->account_id)); + ret->account_id = fnv1a32(variation_data, ret->account_id); + return ret; +} + +AccountIndex::AccountIndex(bool force_all_temporary) + : force_all_temporary(force_all_temporary) { + if (!this->force_all_temporary) { + if (!isdir("system/licenses")) { + mkdir("system/licenses", 0755); + } else { + for (const auto& item : list_directory("system/licenses")) { + if (ends_with(item, ".json")) { + try { + JSON json = JSON::parse(load_file("system/licenses/" + item)); + this->add(make_shared(json)); + } catch (const exception& e) { + log_error("Failed to index account %s", item.c_str()); + throw; + } + } + } + } + } +} diff --git a/src/Account.hh b/src/Account.hh index 3e2139fb..8665d5e2 100644 --- a/src/Account.hh +++ b/src/Account.hh @@ -1,263 +1,263 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include "Text.hh" - -class LicenseIndex; - -struct DCNTELicense { - std::string serial_number; - std::string access_key; - - static std::shared_ptr from_json(const JSON& json); - JSON json() const; -}; - -struct V1V2License { - uint32_t serial_number = 0; - std::string access_key; - - static std::shared_ptr from_json(const JSON& json); - JSON json() const; -}; - -struct GCLicense { - uint32_t serial_number = 0; - std::string access_key; - std::string password; - - static std::shared_ptr from_json(const JSON& json); - JSON json() const; -}; - -struct XBLicense { - std::string gamertag; - uint64_t user_id = 0; - uint64_t account_id = 0; - - static std::shared_ptr from_json(const JSON& json); - JSON json() const; -}; - -struct BBLicense { - std::string username; - std::string password; - - static std::shared_ptr from_json(const JSON& json); - JSON json() const; -}; - -struct Account { - enum class Flag : uint32_t { - // clang-format off - KICK_USER = 0x00000001, - BAN_USER = 0x00000002, - SILENCE_USER = 0x00000004, - CHANGE_EVENT = 0x00000010, - ANNOUNCE = 0x00000020, - FREE_JOIN_GAMES = 0x00000040, - DEBUG = 0x01000000, - CHEAT_ANYWHERE = 0x02000000, - DISABLE_QUEST_REQUIREMENTS = 0x04000000, - ALWAYS_ENABLE_CHAT_COMMANDS = 0x08000000, - MODERATOR = 0x00000007, - ADMINISTRATOR = 0x000000FF, - ROOT = 0x7FFFFFFF, - IS_SHARED_ACCOUNT = 0x80000000, - // NOTE: When adding or changing license flags, don't forget to change the - // documentation in the shell's help text. - UNUSED_BITS = 0x70FFFF00, - // clang-format on - }; - - // account_id is also the account's guild card number - uint32_t account_id = 0; - - uint32_t flags = 0; - uint64_t ban_end_time = 0; // 0 = not banned - std::string last_player_name; - std::string auto_reply_message; - - uint32_t ep3_current_meseta = 0; - uint32_t ep3_total_meseta_earned = 0; - - uint32_t bb_team_id = 0; - bool is_temporary = false; // If true, isn't saved to disk - - std::unordered_set auto_patches_enabled; - - std::unordered_map> dc_nte_licenses; - std::unordered_map> dc_licenses; - std::unordered_map> pc_licenses; - std::unordered_map> gc_licenses; - std::unordered_map> xb_licenses; - std::unordered_map> bb_licenses; - - Account() = default; - explicit Account(const JSON& json); - virtual ~Account() = default; - - JSON json() const; - virtual void save() const; - virtual void delete_file() const; - - [[nodiscard]] inline bool check_flag(Flag flag) const { - return !!(this->flags & static_cast(flag)); - } - inline void set_flag(Flag flag) { - this->flags |= static_cast(flag); - } - inline void clear_flag(Flag flag) { - this->flags &= (~static_cast(flag)); - } - inline void toggle_flag(Flag flag) { - this->flags ^= static_cast(flag); - } - inline void replace_all_flags(Flag mask) { - this->flags = static_cast(mask); - } - - void print(FILE* stream) const; -}; - -struct Login { - bool account_was_created = false; - // This field will never be null - std::shared_ptr account; - // Exactly one of the following will be non-null, representing the license - // that the client logged in with - std::shared_ptr dc_nte_license; - std::shared_ptr dc_license; - std::shared_ptr pc_license; - std::shared_ptr gc_license; - std::shared_ptr xb_license; - std::shared_ptr bb_license; -}; - -class AccountIndex { -public: - class no_username : public std::invalid_argument { - public: - no_username() : invalid_argument("serial number is zero or username is missing") {} - }; - class incorrect_password : public std::invalid_argument { - public: - incorrect_password() : invalid_argument("incorrect password") {} - }; - class incorrect_access_key : public std::invalid_argument { - public: - incorrect_access_key() : invalid_argument("incorrect access key") {} - }; - class missing_account : public std::invalid_argument { - public: - missing_account() : invalid_argument("missing account") {} - }; - - explicit AccountIndex(bool force_all_temporary); - virtual ~AccountIndex() = default; - - std::shared_ptr create_account(bool is_temporary) const; - - size_t count() const; - std::vector> all() const; - - void add(std::shared_ptr a); - void remove(uint32_t serial_number); - - void add_dc_nte_license(std::shared_ptr account, std::shared_ptr license); - void add_dc_license(std::shared_ptr account, std::shared_ptr license); - void add_pc_license(std::shared_ptr account, std::shared_ptr license); - void add_gc_license(std::shared_ptr account, std::shared_ptr license); - void add_xb_license(std::shared_ptr account, std::shared_ptr license); - void add_bb_license(std::shared_ptr account, std::shared_ptr license); - void remove_dc_nte_license(std::shared_ptr account, const std::string& serial_number); - void remove_dc_license(std::shared_ptr account, uint32_t serial_number); - void remove_pc_license(std::shared_ptr account, uint32_t serial_number); - void remove_gc_license(std::shared_ptr account, uint32_t serial_number); - void remove_xb_license(std::shared_ptr account, const std::string& gamertag); - void remove_bb_license(std::shared_ptr account, const std::string& username); - - std::shared_ptr from_account_id(uint32_t account_id) const; - std::shared_ptr from_dc_nte_credentials( - const std::string& serial_number, - const std::string& access_key, - bool allow_create); - std::shared_ptr from_dc_credentials( - uint32_t serial_number, - const std::string& access_key, - const std::string& character_name, - bool allow_create); - std::shared_ptr from_pc_nte_credentials( - uint32_t guild_card_number, - bool allow_create); - std::shared_ptr from_pc_credentials( - uint32_t serial_number, - const std::string& access_key, - const std::string& character_name, - bool allow_create); - std::shared_ptr from_gc_credentials( - uint32_t serial_number, - const std::string& access_key, - const std::string* password, - const std::string& character_name, - bool allow_create); - std::shared_ptr from_xb_credentials( - const std::string& gamertag, - uint64_t user_id, - uint64_t account_id, - bool allow_create); - std::shared_ptr from_bb_credentials( - const std::string& username, - const std::string* password, - bool allow_create); - - std::shared_ptr create_temporary_account_for_shared_account( - std::shared_ptr src_a, const std::string& variation_data) const; - -protected: - bool force_all_temporary; - - // This class must be thread-safe because it's used by both the patch server - // and game server threads - mutable std::shared_mutex lock; - std::unordered_map> by_account_id; - std::unordered_map> by_dc_nte_serial_number; - std::unordered_map> by_dc_serial_number; - std::unordered_map> by_pc_serial_number; - std::unordered_map> by_gc_serial_number; - std::unordered_map> by_xb_gamertag; - std::unordered_map> by_bb_username; - - void add_locked(std::shared_ptr a); - - std::shared_ptr from_dc_nte_credentials_locked( - const std::string& serial_number, - const std::string& access_key); - std::shared_ptr from_dc_credentials_locked( - uint32_t serial_number, - const std::string& access_key, - const std::string& character_name); - std::shared_ptr from_pc_credentials_locked( - uint32_t serial_number, - const std::string& access_key, - const std::string& character_name); - std::shared_ptr from_gc_credentials_locked( - uint32_t serial_number, - const std::string& access_key, - const std::string* password, - const std::string& character_name); - std::shared_ptr from_xb_credentials_locked( - const std::string& gamertag, - uint64_t user_id, - uint64_t account_id); - std::shared_ptr from_bb_credentials_locked( - const std::string& username, - const std::string* password); -}; +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "Text.hh" + +class LicenseIndex; + +struct DCNTELicense { + std::string serial_number; + std::string access_key; + + static std::shared_ptr from_json(const JSON& json); + JSON json() const; +}; + +struct V1V2License { + uint32_t serial_number = 0; + std::string access_key; + + static std::shared_ptr from_json(const JSON& json); + JSON json() const; +}; + +struct GCLicense { + uint32_t serial_number = 0; + std::string access_key; + std::string password; + + static std::shared_ptr from_json(const JSON& json); + JSON json() const; +}; + +struct XBLicense { + std::string gamertag; + uint64_t user_id = 0; + uint64_t account_id = 0; + + static std::shared_ptr from_json(const JSON& json); + JSON json() const; +}; + +struct BBLicense { + std::string username; + std::string password; + + static std::shared_ptr from_json(const JSON& json); + JSON json() const; +}; + +struct Account { + enum class Flag : uint32_t { + // clang-format off + KICK_USER = 0x00000001, + BAN_USER = 0x00000002, + SILENCE_USER = 0x00000004, + CHANGE_EVENT = 0x00000010, + ANNOUNCE = 0x00000020, + FREE_JOIN_GAMES = 0x00000040, + DEBUG = 0x01000000, + CHEAT_ANYWHERE = 0x02000000, + DISABLE_QUEST_REQUIREMENTS = 0x04000000, + ALWAYS_ENABLE_CHAT_COMMANDS = 0x08000000, + MODERATOR = 0x00000007, + ADMINISTRATOR = 0x000000FF, + ROOT = 0x7FFFFFFF, + IS_SHARED_ACCOUNT = 0x80000000, + // NOTE: When adding or changing license flags, don't forget to change the + // documentation in the shell's help text. + UNUSED_BITS = 0x70FFFF00, + // clang-format on + }; + + // account_id is also the account's guild card number + uint32_t account_id = 0; + + uint32_t flags = 0; + uint64_t ban_end_time = 0; // 0 = not banned + std::string last_player_name; + std::string auto_reply_message; + + uint32_t ep3_current_meseta = 0; + uint32_t ep3_total_meseta_earned = 0; + + uint32_t bb_team_id = 0; + bool is_temporary = false; // If true, isn't saved to disk + + std::unordered_set auto_patches_enabled; + + std::unordered_map> dc_nte_licenses; + std::unordered_map> dc_licenses; + std::unordered_map> pc_licenses; + std::unordered_map> gc_licenses; + std::unordered_map> xb_licenses; + std::unordered_map> bb_licenses; + + Account() = default; + explicit Account(const JSON& json); + virtual ~Account() = default; + + JSON json() const; + virtual void save() const; + virtual void delete_file() const; + + [[nodiscard]] inline bool check_flag(Flag flag) const { + return !!(this->flags & static_cast(flag)); + } + inline void set_flag(Flag flag) { + this->flags |= static_cast(flag); + } + inline void clear_flag(Flag flag) { + this->flags &= (~static_cast(flag)); + } + inline void toggle_flag(Flag flag) { + this->flags ^= static_cast(flag); + } + inline void replace_all_flags(Flag mask) { + this->flags = static_cast(mask); + } + + void print(FILE* stream) const; +}; + +struct Login { + bool account_was_created = false; + // This field will never be null + std::shared_ptr account; + // Exactly one of the following will be non-null, representing the license + // that the client logged in with + std::shared_ptr dc_nte_license; + std::shared_ptr dc_license; + std::shared_ptr pc_license; + std::shared_ptr gc_license; + std::shared_ptr xb_license; + std::shared_ptr bb_license; +}; + +class AccountIndex { +public: + class no_username : public std::invalid_argument { + public: + no_username() : invalid_argument("serial number is zero or username is missing") {} + }; + class incorrect_password : public std::invalid_argument { + public: + incorrect_password() : invalid_argument("incorrect password") {} + }; + class incorrect_access_key : public std::invalid_argument { + public: + incorrect_access_key() : invalid_argument("incorrect access key") {} + }; + class missing_account : public std::invalid_argument { + public: + missing_account() : invalid_argument("missing account") {} + }; + + explicit AccountIndex(bool force_all_temporary); + virtual ~AccountIndex() = default; + + std::shared_ptr create_account(bool is_temporary) const; + + size_t count() const; + std::vector> all() const; + + void add(std::shared_ptr a); + void remove(uint32_t serial_number); + + void add_dc_nte_license(std::shared_ptr account, std::shared_ptr license); + void add_dc_license(std::shared_ptr account, std::shared_ptr license); + void add_pc_license(std::shared_ptr account, std::shared_ptr license); + void add_gc_license(std::shared_ptr account, std::shared_ptr license); + void add_xb_license(std::shared_ptr account, std::shared_ptr license); + void add_bb_license(std::shared_ptr account, std::shared_ptr license); + void remove_dc_nte_license(std::shared_ptr account, const std::string& serial_number); + void remove_dc_license(std::shared_ptr account, uint32_t serial_number); + void remove_pc_license(std::shared_ptr account, uint32_t serial_number); + void remove_gc_license(std::shared_ptr account, uint32_t serial_number); + void remove_xb_license(std::shared_ptr account, const std::string& gamertag); + void remove_bb_license(std::shared_ptr account, const std::string& username); + + std::shared_ptr from_account_id(uint32_t account_id) const; + std::shared_ptr from_dc_nte_credentials( + const std::string& serial_number, + const std::string& access_key, + bool allow_create); + std::shared_ptr from_dc_credentials( + uint32_t serial_number, + const std::string& access_key, + const std::string& character_name, + bool allow_create); + std::shared_ptr from_pc_nte_credentials( + uint32_t guild_card_number, + bool allow_create); + std::shared_ptr from_pc_credentials( + uint32_t serial_number, + const std::string& access_key, + const std::string& character_name, + bool allow_create); + std::shared_ptr from_gc_credentials( + uint32_t serial_number, + const std::string& access_key, + const std::string* password, + const std::string& character_name, + bool allow_create); + std::shared_ptr from_xb_credentials( + const std::string& gamertag, + uint64_t user_id, + uint64_t account_id, + bool allow_create); + std::shared_ptr from_bb_credentials( + const std::string& username, + const std::string* password, + bool allow_create); + + std::shared_ptr create_temporary_account_for_shared_account( + std::shared_ptr src_a, const std::string& variation_data) const; + +protected: + bool force_all_temporary; + + // This class must be thread-safe because it's used by both the patch server + // and game server threads + mutable std::shared_mutex lock; + std::unordered_map> by_account_id; + std::unordered_map> by_dc_nte_serial_number; + std::unordered_map> by_dc_serial_number; + std::unordered_map> by_pc_serial_number; + std::unordered_map> by_gc_serial_number; + std::unordered_map> by_xb_gamertag; + std::unordered_map> by_bb_username; + + void add_locked(std::shared_ptr a); + + std::shared_ptr from_dc_nte_credentials_locked( + const std::string& serial_number, + const std::string& access_key); + std::shared_ptr from_dc_credentials_locked( + uint32_t serial_number, + const std::string& access_key, + const std::string& character_name); + std::shared_ptr from_pc_credentials_locked( + uint32_t serial_number, + const std::string& access_key, + const std::string& character_name); + std::shared_ptr from_gc_credentials_locked( + uint32_t serial_number, + const std::string& access_key, + const std::string* password, + const std::string& character_name); + std::shared_ptr from_xb_credentials_locked( + const std::string& gamertag, + uint64_t user_id, + uint64_t account_id); + std::shared_ptr from_bb_credentials_locked( + const std::string& username, + const std::string* password); +}; diff --git a/src/BattleParamsIndex.cc b/src/BattleParamsIndex.cc index 02261b62..8cf63439 100644 --- a/src/BattleParamsIndex.cc +++ b/src/BattleParamsIndex.cc @@ -1,83 +1,83 @@ -#include "BattleParamsIndex.hh" - -#include -#include - -#include "Loggers.hh" -#include "PSOEncryption.hh" -#include "StaticGameData.hh" - -using namespace std; - -void BattleParamsIndex::Table::print(FILE* stream) const { - auto print_entry = +[](FILE* stream, const PlayerStats& e) { - fprintf(stream, - "%5hu %5hu %5hu %5hu %5hu %5hu %5hu %5hu %5" PRIu32 " %5" PRIu32, - e.char_stats.atp.load(), - e.char_stats.mst.load(), - e.char_stats.evp.load(), - e.char_stats.hp.load(), - e.char_stats.dfp.load(), - e.char_stats.ata.load(), - e.char_stats.lck.load(), - e.esp.load(), - e.experience.load(), - e.meseta.load()); - }; - - for (size_t diff = 0; diff < 4; diff++) { - fprintf(stream, "%c ZZ ATP PSV EVP HP DFP ATA LCK ESP EXP DIFF\n", - abbreviation_for_difficulty(diff)); - for (size_t z = 0; z < 0x60; z++) { - fprintf(stream, " %02zX ", z); - print_entry(stream, this->stats[diff][z]); - fputc('\n', stream); - } - } -} - -BattleParamsIndex::BattleParamsIndex( - shared_ptr data_on_ep1, - shared_ptr data_on_ep2, - shared_ptr data_on_ep4, - shared_ptr data_off_ep1, - shared_ptr data_off_ep2, - shared_ptr data_off_ep4) { - this->files[0][0].data = data_on_ep1; - this->files[0][1].data = data_on_ep2; - this->files[0][2].data = data_on_ep4; - this->files[1][0].data = data_off_ep1; - this->files[1][1].data = data_off_ep2; - this->files[1][2].data = data_off_ep4; - - for (uint8_t is_solo = 0; is_solo < 2; is_solo++) { - for (uint8_t episode = 0; episode < 3; episode++) { - auto& file = this->files[is_solo][episode]; - if (file.data->size() < sizeof(Table)) { - throw runtime_error(string_printf( - "battle params table size is incorrect (expected %zX bytes, have %zX bytes; is_solo=%hhu, episode=%hhu)", - sizeof(Table), file.data->size(), is_solo, episode)); - } - file.table = reinterpret_cast(file.data->data()); - } - } -} - -const BattleParamsIndex::Table& BattleParamsIndex::get_table(bool solo, Episode episode) const { - uint8_t ep_index; - switch (episode) { - case Episode::EP1: - ep_index = 0; - break; - case Episode::EP2: - ep_index = 1; - break; - case Episode::EP4: - ep_index = 2; - break; - default: - throw invalid_argument("invalid episode"); - } - - return *this->files[!!solo][ep_index].table; -} +#include "BattleParamsIndex.hh" + +#include +#include + +#include "Loggers.hh" +#include "PSOEncryption.hh" +#include "StaticGameData.hh" + +using namespace std; + +void BattleParamsIndex::Table::print(FILE* stream) const { + auto print_entry = +[](FILE* stream, const PlayerStats& e) { + fprintf(stream, + "%5hu %5hu %5hu %5hu %5hu %5hu %5hu %5hu %5" PRIu32 " %5" PRIu32, + e.char_stats.atp.load(), + e.char_stats.mst.load(), + e.char_stats.evp.load(), + e.char_stats.hp.load(), + e.char_stats.dfp.load(), + e.char_stats.ata.load(), + e.char_stats.lck.load(), + e.esp.load(), + e.experience.load(), + e.meseta.load()); + }; + + for (size_t diff = 0; diff < 4; diff++) { + fprintf(stream, "%c ZZ ATP PSV EVP HP DFP ATA LCK ESP EXP DIFF\n", + abbreviation_for_difficulty(diff)); + for (size_t z = 0; z < 0x60; z++) { + fprintf(stream, " %02zX ", z); + print_entry(stream, this->stats[diff][z]); + fputc('\n', stream); + } + } +} + +BattleParamsIndex::BattleParamsIndex( + shared_ptr data_on_ep1, + shared_ptr data_on_ep2, + shared_ptr data_on_ep4, + shared_ptr data_off_ep1, + shared_ptr data_off_ep2, + shared_ptr data_off_ep4) { + this->files[0][0].data = data_on_ep1; + this->files[0][1].data = data_on_ep2; + this->files[0][2].data = data_on_ep4; + this->files[1][0].data = data_off_ep1; + this->files[1][1].data = data_off_ep2; + this->files[1][2].data = data_off_ep4; + + for (uint8_t is_solo = 0; is_solo < 2; is_solo++) { + for (uint8_t episode = 0; episode < 3; episode++) { + auto& file = this->files[is_solo][episode]; + if (file.data->size() < sizeof(Table)) { + throw runtime_error(string_printf( + "battle params table size is incorrect (expected %zX bytes, have %zX bytes; is_solo=%hhu, episode=%hhu)", + sizeof(Table), file.data->size(), is_solo, episode)); + } + file.table = reinterpret_cast(file.data->data()); + } + } +} + +const BattleParamsIndex::Table& BattleParamsIndex::get_table(bool solo, Episode episode) const { + uint8_t ep_index; + switch (episode) { + case Episode::EP1: + ep_index = 0; + break; + case Episode::EP2: + ep_index = 1; + break; + case Episode::EP4: + ep_index = 2; + break; + default: + throw invalid_argument("invalid episode"); + } + + return *this->files[!!solo][ep_index].table; +} diff --git a/src/BattleParamsIndex.hh b/src/BattleParamsIndex.hh index dcd40028..f6cdf209 100644 --- a/src/BattleParamsIndex.hh +++ b/src/BattleParamsIndex.hh @@ -1,100 +1,100 @@ -#pragma once - -#include - -#include -#include -#include -#include -#include -#include - -#include "EnemyType.hh" -#include "LevelTable.hh" -#include "StaticGameData.hh" -#include "Text.hh" - -class BattleParamsIndex { -public: - // These files are little-endian, even on PSO GC. - - struct AttackData { - /* 00 */ le_int16_t unknown_a1; - /* 02 */ le_int16_t atp; - /* 04 */ le_int16_t ata_bonus; - /* 06 */ le_uint16_t unknown_a4; - /* 08 */ le_float distance_x; - /* 0C */ le_uint32_t angle_x; // Out of 0x10000 (high 16 bits are unused) - /* 10 */ le_float distance_y; - /* 14 */ le_uint16_t unknown_a8; - /* 16 */ le_uint16_t unknown_a9; - /* 18 */ le_uint16_t unknown_a10; - /* 1A */ le_uint16_t unknown_a11; - /* 1C */ le_uint32_t unknown_a12; - /* 20 */ le_uint32_t unknown_a13; - /* 24 */ le_uint32_t unknown_a14; - /* 28 */ le_uint32_t unknown_a15; - /* 2C */ le_uint32_t unknown_a16; - /* 30 */ - } __packed_ws__(AttackData, 0x30); - - struct ResistData { - /* 00 */ le_int16_t evp_bonus; - /* 02 */ le_uint16_t efr; - /* 04 */ le_uint16_t eic; - /* 06 */ le_uint16_t eth; - /* 08 */ le_uint16_t elt; - /* 0A */ le_uint16_t edk; - /* 0C */ le_uint32_t unknown_a6; - /* 10 */ le_uint32_t unknown_a7; - /* 14 */ le_uint32_t unknown_a8; - /* 18 */ le_uint32_t unknown_a9; - /* 1C */ le_int32_t dfp_bonus; - /* 20 */ - } __packed_ws__(ResistData, 0x20); - - struct MovementData { - /* 00 */ le_float idle_move_speed; - /* 04 */ le_float idle_animation_speed; - /* 08 */ le_float move_speed; - /* 0C */ le_float animation_speed; - /* 10 */ le_float unknown_a1; - /* 14 */ le_float unknown_a2; - /* 18 */ le_uint32_t unknown_a3; - /* 1C */ le_uint32_t unknown_a4; - /* 20 */ le_uint32_t unknown_a5; - /* 24 */ le_uint32_t unknown_a6; - /* 28 */ le_uint32_t unknown_a7; - /* 2C */ le_uint32_t unknown_a8; - /* 30 */ - } __packed_ws__(MovementData, 0x30); - - struct Table { - /* 0000 */ parray, 4> stats; - /* 3600 */ parray, 4> attack_data; - /* 7E00 */ parray, 4> resist_data; - /* AE00 */ parray, 4> movement_data; - /* F600 */ - - void print(FILE* stream) const; - } __packed_ws__(Table, 0xF600); - - BattleParamsIndex( - std::shared_ptr data_on_ep1, // BattleParamEntry_on.dat - std::shared_ptr data_on_ep2, // BattleParamEntry_lab_on.dat - std::shared_ptr data_on_ep4, // BattleParamEntry_ep4_on.dat - std::shared_ptr data_off_ep1, // BattleParamEntry.dat - std::shared_ptr data_off_ep2, // BattleParamEntry_lab.dat - std::shared_ptr data_off_ep4); // BattleParamEntry_ep4.dat - - const Table& get_table(bool solo, Episode episode) const; - -private: - struct File { - std::shared_ptr data; - const Table* table; - }; - - // Indexed as [online/offline][episode] - std::array, 2> files; -}; +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +#include "EnemyType.hh" +#include "LevelTable.hh" +#include "StaticGameData.hh" +#include "Text.hh" + +class BattleParamsIndex { +public: + // These files are little-endian, even on PSO GC. + + struct AttackData { + /* 00 */ le_int16_t unknown_a1; + /* 02 */ le_int16_t atp; + /* 04 */ le_int16_t ata_bonus; + /* 06 */ le_uint16_t unknown_a4; + /* 08 */ le_float distance_x; + /* 0C */ le_uint32_t angle_x; // Out of 0x10000 (high 16 bits are unused) + /* 10 */ le_float distance_y; + /* 14 */ le_uint16_t unknown_a8; + /* 16 */ le_uint16_t unknown_a9; + /* 18 */ le_uint16_t unknown_a10; + /* 1A */ le_uint16_t unknown_a11; + /* 1C */ le_uint32_t unknown_a12; + /* 20 */ le_uint32_t unknown_a13; + /* 24 */ le_uint32_t unknown_a14; + /* 28 */ le_uint32_t unknown_a15; + /* 2C */ le_uint32_t unknown_a16; + /* 30 */ + } __packed_ws__(AttackData, 0x30); + + struct ResistData { + /* 00 */ le_int16_t evp_bonus; + /* 02 */ le_uint16_t efr; + /* 04 */ le_uint16_t eic; + /* 06 */ le_uint16_t eth; + /* 08 */ le_uint16_t elt; + /* 0A */ le_uint16_t edk; + /* 0C */ le_uint32_t unknown_a6; + /* 10 */ le_uint32_t unknown_a7; + /* 14 */ le_uint32_t unknown_a8; + /* 18 */ le_uint32_t unknown_a9; + /* 1C */ le_int32_t dfp_bonus; + /* 20 */ + } __packed_ws__(ResistData, 0x20); + + struct MovementData { + /* 00 */ le_float idle_move_speed; + /* 04 */ le_float idle_animation_speed; + /* 08 */ le_float move_speed; + /* 0C */ le_float animation_speed; + /* 10 */ le_float unknown_a1; + /* 14 */ le_float unknown_a2; + /* 18 */ le_uint32_t unknown_a3; + /* 1C */ le_uint32_t unknown_a4; + /* 20 */ le_uint32_t unknown_a5; + /* 24 */ le_uint32_t unknown_a6; + /* 28 */ le_uint32_t unknown_a7; + /* 2C */ le_uint32_t unknown_a8; + /* 30 */ + } __packed_ws__(MovementData, 0x30); + + struct Table { + /* 0000 */ parray, 4> stats; + /* 3600 */ parray, 4> attack_data; + /* 7E00 */ parray, 4> resist_data; + /* AE00 */ parray, 4> movement_data; + /* F600 */ + + void print(FILE* stream) const; + } __packed_ws__(Table, 0xF600); + + BattleParamsIndex( + std::shared_ptr data_on_ep1, // BattleParamEntry_on.dat + std::shared_ptr data_on_ep2, // BattleParamEntry_lab_on.dat + std::shared_ptr data_on_ep4, // BattleParamEntry_ep4_on.dat + std::shared_ptr data_off_ep1, // BattleParamEntry.dat + std::shared_ptr data_off_ep2, // BattleParamEntry_lab.dat + std::shared_ptr data_off_ep4); // BattleParamEntry_ep4.dat + + const Table& get_table(bool solo, Episode episode) const; + +private: + struct File { + std::shared_ptr data; + const Table* table; + }; + + // Indexed as [online/offline][episode] + std::array, 2> files; +}; diff --git a/src/CatSession.cc b/src/CatSession.cc index 57bf8e10..3066e3c1 100644 --- a/src/CatSession.cc +++ b/src/CatSession.cc @@ -1,188 +1,188 @@ -#include "CatSession.hh" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "Loggers.hh" -#include "PSOProtocol.hh" -#include "ProxyCommands.hh" -#include "ReceiveCommands.hh" -#include "ReceiveSubcommands.hh" -#include "SendCommands.hh" - -using namespace std; - -CatSession::exit_shell::exit_shell() : runtime_error("shell exited") {} - -CatSession::CatSession( - shared_ptr base, - const struct sockaddr_storage& remote, - Version version, - shared_ptr bb_key_file) - : log(string_printf("[CatSession:%s] ", name_for_enum(version)), proxy_server_log.min_level), - base(base), - read_event(event_new(this->base.get(), 0, EV_READ | EV_PERSIST, CatSession::dispatch_read_stdin, this), event_free), - channel(version, 1, CatSession::dispatch_on_channel_input, CatSession::dispatch_on_channel_error, this, "CatSession"), - bb_key_file(bb_key_file) { - - if (remote.ss_family != AF_INET) { - throw runtime_error("remote is not AF_INET"); - } - - string netloc_str = render_sockaddr_storage(remote); - this->log.info("Connecting to %s", netloc_str.c_str()); - - struct bufferevent* bev = bufferevent_socket_new( - this->base.get(), -1, BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS); - if (!bev) { - throw runtime_error(string_printf("failed to open socket (%d)", EVUTIL_SOCKET_ERROR())); - } - this->channel.set_bufferevent(bev, 0); - - if (bufferevent_socket_connect(this->channel.bev.get(), - reinterpret_cast(&remote), sizeof(struct sockaddr_in)) != 0) { - throw runtime_error(string_printf("failed to connect (%d)", EVUTIL_SOCKET_ERROR())); - } - - event_add(this->read_event.get(), nullptr); - this->poll.add(0, POLLIN); -} - -void CatSession::execute_command(const std::string& command) { - string full_cmd = parse_data_string(command, nullptr, ParseDataFlags::ALLOW_FILES); - send_command_with_header(this->channel, full_cmd.data(), full_cmd.size()); -} - -void CatSession::dispatch_on_channel_input( - Channel& ch, uint16_t command, uint32_t flag, std::string& data) { - auto* session = reinterpret_cast(ch.context_obj); - session->on_channel_input(command, flag, data); -} - -void CatSession::on_channel_input( - uint16_t command, uint32_t flag, std::string& data) { - if (!uses_v4_encryption(this->channel.version)) { - if (command == 0x02 || command == 0x17 || command == 0x91 || command == 0x9B) { - const auto& cmd = check_size_t(data, 0xFFFF); - if (uses_v3_encryption(this->channel.version)) { - this->channel.crypt_in = make_shared(cmd.server_key); - this->channel.crypt_out = make_shared(cmd.client_key); - this->log.info("Enabled V3 encryption (server key %08" PRIX32 ", client key %08" PRIX32 ")", - cmd.server_key.load(), cmd.client_key.load()); - } else { // PC, DC, or patch server - this->channel.crypt_in = make_shared(cmd.server_key); - this->channel.crypt_out = make_shared(cmd.client_key); - this->log.info("Enabled V2 encryption (server key %08" PRIX32 ", client key %08" PRIX32 ")", - cmd.server_key.load(), cmd.client_key.load()); - } - } - } else { // BB - if (command == 0x03 || command == 0x9B) { - if (!this->bb_key_file) { - throw runtime_error("BB encryption requires a key file"); - } - const auto& cmd = check_size_t(data, 0xFFFF); - this->channel.crypt_in = make_shared(*this->bb_key_file, &cmd.server_key[0], sizeof(cmd.server_key)); - this->channel.crypt_out = make_shared(*this->bb_key_file, &cmd.client_key[0], sizeof(cmd.client_key)); - this->log.info("Enabled BB encryption"); - } - } - - // TODO: Use the iovec form of print_data here instead of - // prepend_command_header (which copies the string) - string full_cmd = prepend_command_header( - this->channel.version, this->channel.crypt_in.get(), command, flag, data); - print_data(stdout, full_cmd, 0, nullptr, PrintDataFlags::PRINT_ASCII | PrintDataFlags::OFFSET_16_BITS); -} - -void CatSession::dispatch_on_channel_error(Channel& ch, short events) { - auto* session = reinterpret_cast(ch.context_obj); - session->on_channel_error(events); -} - -void CatSession::on_channel_error(short events) { - if (events & BEV_EVENT_CONNECTED) { - this->log.info("Channel connected"); - } - if (events & BEV_EVENT_ERROR) { - int err = EVUTIL_SOCKET_ERROR(); - this->log.warning("Error %d (%s) in unlinked client stream", err, - evutil_socket_error_to_string(err)); - } - if (events & (BEV_EVENT_ERROR | BEV_EVENT_EOF)) { - this->log.info("Session endpoint has disconnected"); - this->channel.disconnect(); - event_base_loopexit(this->base.get(), nullptr); - } -} - -void CatSession::dispatch_read_stdin(evutil_socket_t, short, void* ctx) { - reinterpret_cast(ctx)->read_stdin(); -} - -void CatSession::read_stdin() { - bool any_command_read = false; - for (;;) { - auto poll_result = this->poll.poll(); - short fd_events = 0; - try { - fd_events = poll_result.at(0); - } catch (const out_of_range&) { - } - - if (!(fd_events & POLLIN)) { - break; - } - - string command(2048, '\0'); - if (!fgets(command.data(), command.size(), stdin)) { - if (!any_command_read) { - // ctrl+d probably; we should exit - fputc('\n', stderr); - event_base_loopexit(this->base.get(), nullptr); - return; - } else { - break; // probably not EOF; just no more commands for now - } - } - - // trim the extra data off the string - size_t len = strlen(command.c_str()); - if (len == 0) { - break; - } - if (command[len - 1] == '\n') { - len--; - } - command.resize(len); - any_command_read = true; - - try { - execute_command(command); - } catch (const exit_shell&) { - event_base_loopexit(this->base.get(), nullptr); - return; - } catch (const exception& e) { - fprintf(stderr, "FAILED: %s\n", e.what()); - } - } -} +#include "CatSession.hh" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Loggers.hh" +#include "PSOProtocol.hh" +#include "ProxyCommands.hh" +#include "ReceiveCommands.hh" +#include "ReceiveSubcommands.hh" +#include "SendCommands.hh" + +using namespace std; + +CatSession::exit_shell::exit_shell() : runtime_error("shell exited") {} + +CatSession::CatSession( + shared_ptr base, + const struct sockaddr_storage& remote, + Version version, + shared_ptr bb_key_file) + : log(string_printf("[CatSession:%s] ", name_for_enum(version)), proxy_server_log.min_level), + base(base), + read_event(event_new(this->base.get(), 0, EV_READ | EV_PERSIST, CatSession::dispatch_read_stdin, this), event_free), + channel(version, 1, CatSession::dispatch_on_channel_input, CatSession::dispatch_on_channel_error, this, "CatSession"), + bb_key_file(bb_key_file) { + + if (remote.ss_family != AF_INET) { + throw runtime_error("remote is not AF_INET"); + } + + string netloc_str = render_sockaddr_storage(remote); + this->log.info("Connecting to %s", netloc_str.c_str()); + + struct bufferevent* bev = bufferevent_socket_new( + this->base.get(), -1, BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS); + if (!bev) { + throw runtime_error(string_printf("failed to open socket (%d)", EVUTIL_SOCKET_ERROR())); + } + this->channel.set_bufferevent(bev, 0); + + if (bufferevent_socket_connect(this->channel.bev.get(), + reinterpret_cast(&remote), sizeof(struct sockaddr_in)) != 0) { + throw runtime_error(string_printf("failed to connect (%d)", EVUTIL_SOCKET_ERROR())); + } + + event_add(this->read_event.get(), nullptr); + this->poll.add(0, POLLIN); +} + +void CatSession::execute_command(const std::string& command) { + string full_cmd = parse_data_string(command, nullptr, ParseDataFlags::ALLOW_FILES); + send_command_with_header(this->channel, full_cmd.data(), full_cmd.size()); +} + +void CatSession::dispatch_on_channel_input( + Channel& ch, uint16_t command, uint32_t flag, std::string& data) { + auto* session = reinterpret_cast(ch.context_obj); + session->on_channel_input(command, flag, data); +} + +void CatSession::on_channel_input( + uint16_t command, uint32_t flag, std::string& data) { + if (!uses_v4_encryption(this->channel.version)) { + if (command == 0x02 || command == 0x17 || command == 0x91 || command == 0x9B) { + const auto& cmd = check_size_t(data, 0xFFFF); + if (uses_v3_encryption(this->channel.version)) { + this->channel.crypt_in = make_shared(cmd.server_key); + this->channel.crypt_out = make_shared(cmd.client_key); + this->log.info("Enabled V3 encryption (server key %08" PRIX32 ", client key %08" PRIX32 ")", + cmd.server_key.load(), cmd.client_key.load()); + } else { // PC, DC, or patch server + this->channel.crypt_in = make_shared(cmd.server_key); + this->channel.crypt_out = make_shared(cmd.client_key); + this->log.info("Enabled V2 encryption (server key %08" PRIX32 ", client key %08" PRIX32 ")", + cmd.server_key.load(), cmd.client_key.load()); + } + } + } else { // BB + if (command == 0x03 || command == 0x9B) { + if (!this->bb_key_file) { + throw runtime_error("BB encryption requires a key file"); + } + const auto& cmd = check_size_t(data, 0xFFFF); + this->channel.crypt_in = make_shared(*this->bb_key_file, &cmd.server_key[0], sizeof(cmd.server_key)); + this->channel.crypt_out = make_shared(*this->bb_key_file, &cmd.client_key[0], sizeof(cmd.client_key)); + this->log.info("Enabled BB encryption"); + } + } + + // TODO: Use the iovec form of print_data here instead of + // prepend_command_header (which copies the string) + string full_cmd = prepend_command_header( + this->channel.version, this->channel.crypt_in.get(), command, flag, data); + print_data(stdout, full_cmd, 0, nullptr, PrintDataFlags::PRINT_ASCII | PrintDataFlags::OFFSET_16_BITS); +} + +void CatSession::dispatch_on_channel_error(Channel& ch, short events) { + auto* session = reinterpret_cast(ch.context_obj); + session->on_channel_error(events); +} + +void CatSession::on_channel_error(short events) { + if (events & BEV_EVENT_CONNECTED) { + this->log.info("Channel connected"); + } + if (events & BEV_EVENT_ERROR) { + int err = EVUTIL_SOCKET_ERROR(); + this->log.warning("Error %d (%s) in unlinked client stream", err, + evutil_socket_error_to_string(err)); + } + if (events & (BEV_EVENT_ERROR | BEV_EVENT_EOF)) { + this->log.info("Session endpoint has disconnected"); + this->channel.disconnect(); + event_base_loopexit(this->base.get(), nullptr); + } +} + +void CatSession::dispatch_read_stdin(evutil_socket_t, short, void* ctx) { + reinterpret_cast(ctx)->read_stdin(); +} + +void CatSession::read_stdin() { + bool any_command_read = false; + for (;;) { + auto poll_result = this->poll.poll(); + short fd_events = 0; + try { + fd_events = poll_result.at(0); + } catch (const out_of_range&) { + } + + if (!(fd_events & POLLIN)) { + break; + } + + string command(2048, '\0'); + if (!fgets(command.data(), command.size(), stdin)) { + if (!any_command_read) { + // ctrl+d probably; we should exit + fputc('\n', stderr); + event_base_loopexit(this->base.get(), nullptr); + return; + } else { + break; // probably not EOF; just no more commands for now + } + } + + // trim the extra data off the string + size_t len = strlen(command.c_str()); + if (len == 0) { + break; + } + if (command[len - 1] == '\n') { + len--; + } + command.resize(len); + any_command_read = true; + + try { + execute_command(command); + } catch (const exit_shell&) { + event_base_loopexit(this->base.get(), nullptr); + return; + } catch (const exception& e) { + fprintf(stderr, "FAILED: %s\n", e.what()); + } + } +} diff --git a/src/CatSession.hh b/src/CatSession.hh index f3bcbad8..88f465c6 100644 --- a/src/CatSession.hh +++ b/src/CatSession.hh @@ -1,54 +1,54 @@ -#pragma once - -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "PSOEncryption.hh" -#include "PSOProtocol.hh" -#include "ServerState.hh" - -class CatSession { -public: - CatSession( - std::shared_ptr base, - const struct sockaddr_storage& remote, - Version version, - std::shared_ptr bb_key_file); - CatSession(const CatSession&) = delete; - CatSession(CatSession&&) = delete; - CatSession& operator=(const CatSession&) = delete; - CatSession& operator=(CatSession&&) = delete; - virtual ~CatSession() = default; - -protected: - PrefixedLogger log; - std::shared_ptr base; - std::unique_ptr read_event; - Poll poll; - - Channel channel; - std::shared_ptr bb_key_file; - - class exit_shell : public std::runtime_error { - public: - exit_shell(); - ~exit_shell() = default; - }; - - virtual void execute_command(const std::string& command); - - static void dispatch_read_stdin(evutil_socket_t fd, short events, void* ctx); - static void dispatch_on_channel_input(Channel& ch, uint16_t command, uint32_t flag, std::string& msg); - static void dispatch_on_channel_error(Channel& ch, short events); - void on_channel_input(uint16_t command, uint32_t flag, std::string& msg); - void on_channel_error(short events); - void read_stdin(); -}; +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "PSOEncryption.hh" +#include "PSOProtocol.hh" +#include "ServerState.hh" + +class CatSession { +public: + CatSession( + std::shared_ptr base, + const struct sockaddr_storage& remote, + Version version, + std::shared_ptr bb_key_file); + CatSession(const CatSession&) = delete; + CatSession(CatSession&&) = delete; + CatSession& operator=(const CatSession&) = delete; + CatSession& operator=(CatSession&&) = delete; + virtual ~CatSession() = default; + +protected: + PrefixedLogger log; + std::shared_ptr base; + std::unique_ptr read_event; + Poll poll; + + Channel channel; + std::shared_ptr bb_key_file; + + class exit_shell : public std::runtime_error { + public: + exit_shell(); + ~exit_shell() = default; + }; + + virtual void execute_command(const std::string& command); + + static void dispatch_read_stdin(evutil_socket_t fd, short events, void* ctx); + static void dispatch_on_channel_input(Channel& ch, uint16_t command, uint32_t flag, std::string& msg); + static void dispatch_on_channel_error(Channel& ch, short events); + void on_channel_input(uint16_t command, uint32_t flag, std::string& msg); + void on_channel_error(short events); + void read_stdin(); +}; diff --git a/src/Channel.cc b/src/Channel.cc index 762b00e4..3532726d 100644 --- a/src/Channel.cc +++ b/src/Channel.cc @@ -1,421 +1,421 @@ -#include "Channel.hh" - -#include -#include -#include -#include -#include -#include - -#include -#include - -#include "Loggers.hh" -#include "Version.hh" - -using namespace std; - -extern bool use_terminal_colors; - -static void flush_and_free_bufferevent(struct bufferevent* bev) { - bufferevent_flush(bev, EV_READ | EV_WRITE, BEV_FINISHED); - bufferevent_free(bev); -} - -Channel::Channel( - Version version, - uint8_t language, - on_command_received_t on_command_received, - on_error_t on_error, - void* context_obj, - const string& name, - TerminalFormat terminal_send_color, - TerminalFormat terminal_recv_color) - : bev(nullptr, flush_and_free_bufferevent), - virtual_network_id(0), - version(version), - language(language), - name(name), - terminal_send_color(terminal_send_color), - terminal_recv_color(terminal_recv_color), - on_command_received(on_command_received), - on_error(on_error), - context_obj(context_obj) { -} - -Channel::Channel( - struct bufferevent* bev, - uint64_t virtual_network_id, - Version version, - uint8_t language, - on_command_received_t on_command_received, - on_error_t on_error, - void* context_obj, - const string& name, - TerminalFormat terminal_send_color, - TerminalFormat terminal_recv_color) - : bev(nullptr, flush_and_free_bufferevent), - version(version), - language(language), - name(name), - terminal_send_color(terminal_send_color), - terminal_recv_color(terminal_recv_color), - on_command_received(on_command_received), - on_error(on_error), - context_obj(context_obj) { - this->set_bufferevent(bev, virtual_network_id); -} - -void Channel::replace_with( - Channel&& other, - on_command_received_t on_command_received, - on_error_t on_error, - void* context_obj, - const std::string& name) { - this->set_bufferevent(other.bev.release(), other.virtual_network_id); - this->local_addr = other.local_addr; - this->remote_addr = other.remote_addr; - this->version = other.version; - this->language = other.language; - this->crypt_in = other.crypt_in; - this->crypt_out = other.crypt_out; - this->name = name; - this->terminal_send_color = other.terminal_send_color; - this->terminal_recv_color = other.terminal_recv_color; - this->on_command_received = on_command_received; - this->on_error = on_error; - this->context_obj = context_obj; - other.disconnect(); // Clears crypts, addrs, etc. -} - -void Channel::set_bufferevent(struct bufferevent* bev, uint64_t virtual_network_id) { - this->bev.reset(bev); - this->virtual_network_id = virtual_network_id; - - if (this->bev.get()) { - int fd = bufferevent_getfd(this->bev.get()); - if (fd < 0) { - memset(&this->local_addr, 0, sizeof(this->local_addr)); - memset(&this->remote_addr, 0, sizeof(this->remote_addr)); - } else { - get_socket_addresses(fd, &this->local_addr, &this->remote_addr); - } - - bufferevent_setcb(this->bev.get(), &Channel::dispatch_on_input, nullptr, &Channel::dispatch_on_error, this); - bufferevent_enable(this->bev.get(), EV_READ | EV_WRITE); - - } else { - memset(&this->local_addr, 0, sizeof(this->local_addr)); - memset(&this->remote_addr, 0, sizeof(this->remote_addr)); - } -} - -void Channel::disconnect() { - if (this->bev.get()) { - // If the output buffer is not empty, move the bufferevent into the draining - // pool instead of disconnecting it, to make sure all the data gets sent. - struct evbuffer* out_buffer = bufferevent_get_output(this->bev.get()); - if (evbuffer_get_length(out_buffer) == 0) { - this->bev.reset(); // Destructor flushes and frees the bufferevent - } else { - // The callbacks will free it when all the data is sent or the client - // disconnects - - auto on_output = +[](struct bufferevent* bev, void*) -> void { - flush_and_free_bufferevent(bev); - }; - - auto on_error = +[](struct bufferevent* bev, short events, void*) -> void { - if (events & BEV_EVENT_ERROR) { - int err = EVUTIL_SOCKET_ERROR(); - channel_exceptions_log.warning( - "Disconnecting channel caused error %d (%s)", err, - evutil_socket_error_to_string(err)); - } - if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) { - bufferevent_flush(bev, EV_WRITE, BEV_FINISHED); - bufferevent_free(bev); - } - }; - - struct bufferevent* bev = this->bev.release(); - bufferevent_setcb(bev, nullptr, on_output, on_error, bev); - bufferevent_disable(bev, EV_READ); - } - } - - memset(&this->local_addr, 0, sizeof(this->local_addr)); - memset(&this->remote_addr, 0, sizeof(this->remote_addr)); - this->virtual_network_id = false; - this->crypt_in.reset(); - this->crypt_out.reset(); -} - -Channel::Message Channel::recv() { - struct evbuffer* buf = bufferevent_get_input(this->bev.get()); - - size_t header_size = (this->version == Version::BB_V4) ? 8 : 4; - PSOCommandHeader header; - if (evbuffer_copyout(buf, &header, header_size) < static_cast(header_size)) { - throw out_of_range("no command available"); - } - - if (this->crypt_in.get()) { - this->crypt_in->decrypt(&header, header_size, false); - } - - size_t command_logical_size = header.size(version); - - // If encryption is enabled, BB pads commands to 8-byte boundaries, and this - // is not reflected in the size field. This logic does not occur if encryption - // is not yet enabled. - size_t command_physical_size = (this->crypt_in.get() && (version == Version::BB_V4)) - ? ((command_logical_size + 7) & ~7) - : command_logical_size; - if (evbuffer_get_length(buf) < command_physical_size) { - throw out_of_range("no command available"); - } - - // If we get here, then there is a full command in the buffer. Some encryption - // algorithms' advancement depends on the decrypted data, so we have to - // actually decrypt the header again (with advance=true) to keep them in a - // consistent state. - - string header_data(header_size, '\0'); - if (evbuffer_remove(buf, header_data.data(), header_data.size()) < static_cast(header_data.size())) { - throw logic_error("enough bytes available, but could not remove them"); - } - if (this->crypt_in.get()) { - this->crypt_in->decrypt(header_data.data(), header_data.size()); - } - - string command_data(command_physical_size - header_size, '\0'); - if (evbuffer_remove(buf, command_data.data(), command_data.size()) < static_cast(command_data.size())) { - throw logic_error("enough bytes available, but could not remove them"); - } - - if (this->crypt_in.get()) { - // Some versions of PSO DC can send commands whose sizes are not a multiple - // of 4, but the server is expected to always use a multiple of 4 bytes when - // decrypting (the extra cipher bytes are lost). To emulate this behavior, - // we have to round up the size for DC commands here. - size_t orig_size = command_data.size(); - command_data.resize((orig_size + 3) & (~3), 0); - this->crypt_in->decrypt(command_data.data(), command_data.size()); - command_data.resize(orig_size); - } - command_data.resize(command_logical_size - header_size); - - if (command_data_log.should_log(LogLevel::INFO) && (this->terminal_recv_color != TerminalFormat::END)) { - if (use_terminal_colors && this->terminal_recv_color != TerminalFormat::NORMAL) { - print_color_escape(stderr, this->terminal_recv_color, TerminalFormat::BOLD, TerminalFormat::END); - } - - if (version == Version::BB_V4) { - command_data_log.info( - "Received from %s (version=BB command=%04hX flag=%08" PRIX32 ")", - this->name.c_str(), - header.command(this->version), - header.flag(this->version)); - } else { - command_data_log.info( - "Received from %s (version=%s command=%02hX flag=%02" PRIX32 ")", - this->name.c_str(), - name_for_enum(this->version), - header.command(this->version), - header.flag(this->version)); - } - - vector iovs; - iovs.emplace_back(iovec{.iov_base = header_data.data(), .iov_len = header_data.size()}); - iovs.emplace_back(iovec{.iov_base = command_data.data(), .iov_len = command_data.size()}); - print_data(stderr, iovs, 0, nullptr, PrintDataFlags::PRINT_ASCII | PrintDataFlags::DISABLE_COLOR | PrintDataFlags::OFFSET_16_BITS); - - if (use_terminal_colors && this->terminal_recv_color != TerminalFormat::NORMAL) { - print_color_escape(stderr, TerminalFormat::NORMAL, TerminalFormat::END); - } - } - - return { - .command = header.command(this->version), - .flag = header.flag(this->version), - .data = std::move(command_data), - }; -} - -void Channel::send(uint16_t cmd, uint32_t flag, bool silent) { - this->send(cmd, flag, nullptr, 0, silent); -} - -void Channel::send(uint16_t cmd, uint32_t flag, const std::vector> blocks, bool silent) { - if (!this->connected()) { - channel_exceptions_log.warning("Attempted to send command on closed channel; dropping data"); - return; - } - - size_t size = 0; - for (const auto& b : blocks) { - size += b.second; - } - - string send_data; - size_t logical_size; - size_t send_data_size = 0; - switch (this->version) { - case Version::DC_NTE: - case Version::DC_V1_11_2000_PROTOTYPE: - case Version::DC_V1: - case Version::DC_V2: - case Version::GC_NTE: - case Version::GC_V3: - case Version::GC_EP3_NTE: - case Version::GC_EP3: - case Version::XB_V3: { - PSOCommandHeaderDCV3 header; - if (this->crypt_out.get() && - (this->version != Version::DC_NTE) && - (this->version != Version::DC_V1_11_2000_PROTOTYPE) && - (this->version != Version::DC_V1)) { - send_data_size = (sizeof(header) + size + 3) & ~3; - } else { - send_data_size = (sizeof(header) + size); - } - logical_size = send_data_size; - header.command = cmd; - header.flag = flag; - header.size = send_data_size; - send_data.append(reinterpret_cast(&header), sizeof(header)); - break; - } - case Version::PC_PATCH: - case Version::BB_PATCH: - case Version::PC_NTE: - case Version::PC_V2: { - PSOCommandHeaderPC header; - if (this->crypt_out.get()) { - send_data_size = (sizeof(header) + size + 3) & ~3; - } else { - send_data_size = (sizeof(header) + size); - } - logical_size = send_data_size; - header.size = send_data_size; - header.command = cmd; - header.flag = flag; - send_data.append(reinterpret_cast(&header), sizeof(header)); - break; - } - case Version::BB_V4: { - // BB has an annoying behavior here: command lengths must be multiples of - // 4, but the actual data length must be a multiple of 8. If the size - // field is not divisible by 8, 4 extra bytes are sent anyway. This - // behavior only applies when encryption is enabled - any commands sent - // before encryption is enabled have no size restrictions (except they - // must include a full header and must fit in the client's receive - // buffer), and no implicit extra bytes are sent. - PSOCommandHeaderBB header; - if (this->crypt_out.get()) { - send_data_size = (sizeof(header) + size + 7) & ~7; - } else { - send_data_size = (sizeof(header) + size); - } - logical_size = (sizeof(header) + size + 3) & ~3; - header.size = logical_size; - header.command = cmd; - header.flag = flag; - send_data.append(reinterpret_cast(&header), sizeof(header)); - break; - } - - default: - throw logic_error("unimplemented game version in send_command"); - } - - // All versions of PSO I've seen (so far) have a receive buffer 0x7C00 - // bytes in size - if (send_data_size > 0x7C00) { - throw runtime_error("outbound command too large"); - } - - send_data.reserve(send_data_size); - for (const auto& b : blocks) { - send_data.append(reinterpret_cast(b.first), b.second); - } - send_data.resize(send_data_size, '\0'); - - if (!silent && (command_data_log.should_log(LogLevel::INFO)) && (this->terminal_send_color != TerminalFormat::END)) { - if (use_terminal_colors && this->terminal_send_color != TerminalFormat::NORMAL) { - print_color_escape(stderr, TerminalFormat::FG_YELLOW, TerminalFormat::BOLD, TerminalFormat::END); - } - if (version == Version::BB_V4) { - command_data_log.info("Sending to %s (version=BB command=%04hX flag=%08" PRIX32 ")", - this->name.c_str(), cmd, flag); - } else { - command_data_log.info("Sending to %s (version=%s command=%02hX flag=%02" PRIX32 ")", - this->name.c_str(), name_for_enum(version), cmd, flag); - } - print_data(stderr, send_data.data(), logical_size, 0, nullptr, PrintDataFlags::PRINT_ASCII | PrintDataFlags::DISABLE_COLOR | PrintDataFlags::OFFSET_16_BITS); - if (use_terminal_colors && this->terminal_send_color != TerminalFormat::NORMAL) { - print_color_escape(stderr, TerminalFormat::NORMAL, TerminalFormat::END); - } - } - - if (this->crypt_out.get()) { - this->crypt_out->encrypt(send_data.data(), send_data.size()); - } - - struct evbuffer* buf = bufferevent_get_output(this->bev.get()); - evbuffer_add(buf, send_data.data(), send_data.size()); -} - -void Channel::send(uint16_t cmd, uint32_t flag, const void* data, size_t size, bool silent) { - this->send(cmd, flag, {make_pair(data, size)}, silent); -} - -void Channel::send(uint16_t cmd, uint32_t flag, const string& data, bool silent) { - this->send(cmd, flag, data.data(), data.size(), silent); -} - -void Channel::send(const void* data, size_t size, bool silent) { - size_t header_size = (this->version == Version::BB_V4) ? 8 : 4; - const auto* header = reinterpret_cast(data); - this->send( - header->command(this->version), - header->flag(this->version), - reinterpret_cast(data) + header_size, - size - header_size, - silent); -} - -void Channel::send(const string& data, bool silent) { - return this->send(data.data(), data.size(), silent); -} - -void Channel::dispatch_on_input(struct bufferevent*, void* ctx) { - Channel* ch = reinterpret_cast(ctx); - // The client can be disconnected during on_command_received, so we have to - // make sure ch->bev is valid every time before calling recv() - while (ch->bev.get()) { - Message msg; - try { - msg = ch->recv(); - } catch (const out_of_range&) { - break; - } catch (const exception& e) { - channel_exceptions_log.warning("Error receiving on channel: %s", e.what()); - ch->on_error(*ch, BEV_EVENT_ERROR); - break; - } - if (ch->on_command_received) { - ch->on_command_received(*ch, msg.command, msg.flag, msg.data); - } - } -} - -void Channel::dispatch_on_error(struct bufferevent*, short events, void* ctx) { - Channel* ch = reinterpret_cast(ctx); - if (ch->on_error) { - ch->on_error(*ch, events); - } else { - ch->disconnect(); - } -} +#include "Channel.hh" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "Loggers.hh" +#include "Version.hh" + +using namespace std; + +extern bool use_terminal_colors; + +static void flush_and_free_bufferevent(struct bufferevent* bev) { + bufferevent_flush(bev, EV_READ | EV_WRITE, BEV_FINISHED); + bufferevent_free(bev); +} + +Channel::Channel( + Version version, + uint8_t language, + on_command_received_t on_command_received, + on_error_t on_error, + void* context_obj, + const string& name, + TerminalFormat terminal_send_color, + TerminalFormat terminal_recv_color) + : bev(nullptr, flush_and_free_bufferevent), + virtual_network_id(0), + version(version), + language(language), + name(name), + terminal_send_color(terminal_send_color), + terminal_recv_color(terminal_recv_color), + on_command_received(on_command_received), + on_error(on_error), + context_obj(context_obj) { +} + +Channel::Channel( + struct bufferevent* bev, + uint64_t virtual_network_id, + Version version, + uint8_t language, + on_command_received_t on_command_received, + on_error_t on_error, + void* context_obj, + const string& name, + TerminalFormat terminal_send_color, + TerminalFormat terminal_recv_color) + : bev(nullptr, flush_and_free_bufferevent), + version(version), + language(language), + name(name), + terminal_send_color(terminal_send_color), + terminal_recv_color(terminal_recv_color), + on_command_received(on_command_received), + on_error(on_error), + context_obj(context_obj) { + this->set_bufferevent(bev, virtual_network_id); +} + +void Channel::replace_with( + Channel&& other, + on_command_received_t on_command_received, + on_error_t on_error, + void* context_obj, + const std::string& name) { + this->set_bufferevent(other.bev.release(), other.virtual_network_id); + this->local_addr = other.local_addr; + this->remote_addr = other.remote_addr; + this->version = other.version; + this->language = other.language; + this->crypt_in = other.crypt_in; + this->crypt_out = other.crypt_out; + this->name = name; + this->terminal_send_color = other.terminal_send_color; + this->terminal_recv_color = other.terminal_recv_color; + this->on_command_received = on_command_received; + this->on_error = on_error; + this->context_obj = context_obj; + other.disconnect(); // Clears crypts, addrs, etc. +} + +void Channel::set_bufferevent(struct bufferevent* bev, uint64_t virtual_network_id) { + this->bev.reset(bev); + this->virtual_network_id = virtual_network_id; + + if (this->bev.get()) { + int fd = bufferevent_getfd(this->bev.get()); + if (fd < 0) { + memset(&this->local_addr, 0, sizeof(this->local_addr)); + memset(&this->remote_addr, 0, sizeof(this->remote_addr)); + } else { + get_socket_addresses(fd, &this->local_addr, &this->remote_addr); + } + + bufferevent_setcb(this->bev.get(), &Channel::dispatch_on_input, nullptr, &Channel::dispatch_on_error, this); + bufferevent_enable(this->bev.get(), EV_READ | EV_WRITE); + + } else { + memset(&this->local_addr, 0, sizeof(this->local_addr)); + memset(&this->remote_addr, 0, sizeof(this->remote_addr)); + } +} + +void Channel::disconnect() { + if (this->bev.get()) { + // If the output buffer is not empty, move the bufferevent into the draining + // pool instead of disconnecting it, to make sure all the data gets sent. + struct evbuffer* out_buffer = bufferevent_get_output(this->bev.get()); + if (evbuffer_get_length(out_buffer) == 0) { + this->bev.reset(); // Destructor flushes and frees the bufferevent + } else { + // The callbacks will free it when all the data is sent or the client + // disconnects + + auto on_output = +[](struct bufferevent* bev, void*) -> void { + flush_and_free_bufferevent(bev); + }; + + auto on_error = +[](struct bufferevent* bev, short events, void*) -> void { + if (events & BEV_EVENT_ERROR) { + int err = EVUTIL_SOCKET_ERROR(); + channel_exceptions_log.warning( + "Disconnecting channel caused error %d (%s)", err, + evutil_socket_error_to_string(err)); + } + if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) { + bufferevent_flush(bev, EV_WRITE, BEV_FINISHED); + bufferevent_free(bev); + } + }; + + struct bufferevent* bev = this->bev.release(); + bufferevent_setcb(bev, nullptr, on_output, on_error, bev); + bufferevent_disable(bev, EV_READ); + } + } + + memset(&this->local_addr, 0, sizeof(this->local_addr)); + memset(&this->remote_addr, 0, sizeof(this->remote_addr)); + this->virtual_network_id = false; + this->crypt_in.reset(); + this->crypt_out.reset(); +} + +Channel::Message Channel::recv() { + struct evbuffer* buf = bufferevent_get_input(this->bev.get()); + + size_t header_size = (this->version == Version::BB_V4) ? 8 : 4; + PSOCommandHeader header; + if (evbuffer_copyout(buf, &header, header_size) < static_cast(header_size)) { + throw out_of_range("no command available"); + } + + if (this->crypt_in.get()) { + this->crypt_in->decrypt(&header, header_size, false); + } + + size_t command_logical_size = header.size(version); + + // If encryption is enabled, BB pads commands to 8-byte boundaries, and this + // is not reflected in the size field. This logic does not occur if encryption + // is not yet enabled. + size_t command_physical_size = (this->crypt_in.get() && (version == Version::BB_V4)) + ? ((command_logical_size + 7) & ~7) + : command_logical_size; + if (evbuffer_get_length(buf) < command_physical_size) { + throw out_of_range("no command available"); + } + + // If we get here, then there is a full command in the buffer. Some encryption + // algorithms' advancement depends on the decrypted data, so we have to + // actually decrypt the header again (with advance=true) to keep them in a + // consistent state. + + string header_data(header_size, '\0'); + if (evbuffer_remove(buf, header_data.data(), header_data.size()) < static_cast(header_data.size())) { + throw logic_error("enough bytes available, but could not remove them"); + } + if (this->crypt_in.get()) { + this->crypt_in->decrypt(header_data.data(), header_data.size()); + } + + string command_data(command_physical_size - header_size, '\0'); + if (evbuffer_remove(buf, command_data.data(), command_data.size()) < static_cast(command_data.size())) { + throw logic_error("enough bytes available, but could not remove them"); + } + + if (this->crypt_in.get()) { + // Some versions of PSO DC can send commands whose sizes are not a multiple + // of 4, but the server is expected to always use a multiple of 4 bytes when + // decrypting (the extra cipher bytes are lost). To emulate this behavior, + // we have to round up the size for DC commands here. + size_t orig_size = command_data.size(); + command_data.resize((orig_size + 3) & (~3), 0); + this->crypt_in->decrypt(command_data.data(), command_data.size()); + command_data.resize(orig_size); + } + command_data.resize(command_logical_size - header_size); + + if (command_data_log.should_log(LogLevel::INFO) && (this->terminal_recv_color != TerminalFormat::END)) { + if (use_terminal_colors && this->terminal_recv_color != TerminalFormat::NORMAL) { + print_color_escape(stderr, this->terminal_recv_color, TerminalFormat::BOLD, TerminalFormat::END); + } + + if (version == Version::BB_V4) { + command_data_log.info( + "Received from %s (version=BB command=%04hX flag=%08" PRIX32 ")", + this->name.c_str(), + header.command(this->version), + header.flag(this->version)); + } else { + command_data_log.info( + "Received from %s (version=%s command=%02hX flag=%02" PRIX32 ")", + this->name.c_str(), + name_for_enum(this->version), + header.command(this->version), + header.flag(this->version)); + } + + vector iovs; + iovs.emplace_back(iovec{.iov_base = header_data.data(), .iov_len = header_data.size()}); + iovs.emplace_back(iovec{.iov_base = command_data.data(), .iov_len = command_data.size()}); + print_data(stderr, iovs, 0, nullptr, PrintDataFlags::PRINT_ASCII | PrintDataFlags::DISABLE_COLOR | PrintDataFlags::OFFSET_16_BITS); + + if (use_terminal_colors && this->terminal_recv_color != TerminalFormat::NORMAL) { + print_color_escape(stderr, TerminalFormat::NORMAL, TerminalFormat::END); + } + } + + return { + .command = header.command(this->version), + .flag = header.flag(this->version), + .data = std::move(command_data), + }; +} + +void Channel::send(uint16_t cmd, uint32_t flag, bool silent) { + this->send(cmd, flag, nullptr, 0, silent); +} + +void Channel::send(uint16_t cmd, uint32_t flag, const std::vector> blocks, bool silent) { + if (!this->connected()) { + channel_exceptions_log.warning("Attempted to send command on closed channel; dropping data"); + return; + } + + size_t size = 0; + for (const auto& b : blocks) { + size += b.second; + } + + string send_data; + size_t logical_size; + size_t send_data_size = 0; + switch (this->version) { + case Version::DC_NTE: + case Version::DC_V1_11_2000_PROTOTYPE: + case Version::DC_V1: + case Version::DC_V2: + case Version::GC_NTE: + case Version::GC_V3: + case Version::GC_EP3_NTE: + case Version::GC_EP3: + case Version::XB_V3: { + PSOCommandHeaderDCV3 header; + if (this->crypt_out.get() && + (this->version != Version::DC_NTE) && + (this->version != Version::DC_V1_11_2000_PROTOTYPE) && + (this->version != Version::DC_V1)) { + send_data_size = (sizeof(header) + size + 3) & ~3; + } else { + send_data_size = (sizeof(header) + size); + } + logical_size = send_data_size; + header.command = cmd; + header.flag = flag; + header.size = send_data_size; + send_data.append(reinterpret_cast(&header), sizeof(header)); + break; + } + case Version::PC_PATCH: + case Version::BB_PATCH: + case Version::PC_NTE: + case Version::PC_V2: { + PSOCommandHeaderPC header; + if (this->crypt_out.get()) { + send_data_size = (sizeof(header) + size + 3) & ~3; + } else { + send_data_size = (sizeof(header) + size); + } + logical_size = send_data_size; + header.size = send_data_size; + header.command = cmd; + header.flag = flag; + send_data.append(reinterpret_cast(&header), sizeof(header)); + break; + } + case Version::BB_V4: { + // BB has an annoying behavior here: command lengths must be multiples of + // 4, but the actual data length must be a multiple of 8. If the size + // field is not divisible by 8, 4 extra bytes are sent anyway. This + // behavior only applies when encryption is enabled - any commands sent + // before encryption is enabled have no size restrictions (except they + // must include a full header and must fit in the client's receive + // buffer), and no implicit extra bytes are sent. + PSOCommandHeaderBB header; + if (this->crypt_out.get()) { + send_data_size = (sizeof(header) + size + 7) & ~7; + } else { + send_data_size = (sizeof(header) + size); + } + logical_size = (sizeof(header) + size + 3) & ~3; + header.size = logical_size; + header.command = cmd; + header.flag = flag; + send_data.append(reinterpret_cast(&header), sizeof(header)); + break; + } + + default: + throw logic_error("unimplemented game version in send_command"); + } + + // All versions of PSO I've seen (so far) have a receive buffer 0x7C00 + // bytes in size + if (send_data_size > 0x7C00) { + throw runtime_error("outbound command too large"); + } + + send_data.reserve(send_data_size); + for (const auto& b : blocks) { + send_data.append(reinterpret_cast(b.first), b.second); + } + send_data.resize(send_data_size, '\0'); + + if (!silent && (command_data_log.should_log(LogLevel::INFO)) && (this->terminal_send_color != TerminalFormat::END)) { + if (use_terminal_colors && this->terminal_send_color != TerminalFormat::NORMAL) { + print_color_escape(stderr, TerminalFormat::FG_YELLOW, TerminalFormat::BOLD, TerminalFormat::END); + } + if (version == Version::BB_V4) { + command_data_log.info("Sending to %s (version=BB command=%04hX flag=%08" PRIX32 ")", + this->name.c_str(), cmd, flag); + } else { + command_data_log.info("Sending to %s (version=%s command=%02hX flag=%02" PRIX32 ")", + this->name.c_str(), name_for_enum(version), cmd, flag); + } + print_data(stderr, send_data.data(), logical_size, 0, nullptr, PrintDataFlags::PRINT_ASCII | PrintDataFlags::DISABLE_COLOR | PrintDataFlags::OFFSET_16_BITS); + if (use_terminal_colors && this->terminal_send_color != TerminalFormat::NORMAL) { + print_color_escape(stderr, TerminalFormat::NORMAL, TerminalFormat::END); + } + } + + if (this->crypt_out.get()) { + this->crypt_out->encrypt(send_data.data(), send_data.size()); + } + + struct evbuffer* buf = bufferevent_get_output(this->bev.get()); + evbuffer_add(buf, send_data.data(), send_data.size()); +} + +void Channel::send(uint16_t cmd, uint32_t flag, const void* data, size_t size, bool silent) { + this->send(cmd, flag, {make_pair(data, size)}, silent); +} + +void Channel::send(uint16_t cmd, uint32_t flag, const string& data, bool silent) { + this->send(cmd, flag, data.data(), data.size(), silent); +} + +void Channel::send(const void* data, size_t size, bool silent) { + size_t header_size = (this->version == Version::BB_V4) ? 8 : 4; + const auto* header = reinterpret_cast(data); + this->send( + header->command(this->version), + header->flag(this->version), + reinterpret_cast(data) + header_size, + size - header_size, + silent); +} + +void Channel::send(const string& data, bool silent) { + return this->send(data.data(), data.size(), silent); +} + +void Channel::dispatch_on_input(struct bufferevent*, void* ctx) { + Channel* ch = reinterpret_cast(ctx); + // The client can be disconnected during on_command_received, so we have to + // make sure ch->bev is valid every time before calling recv() + while (ch->bev.get()) { + Message msg; + try { + msg = ch->recv(); + } catch (const out_of_range&) { + break; + } catch (const exception& e) { + channel_exceptions_log.warning("Error receiving on channel: %s", e.what()); + ch->on_error(*ch, BEV_EVENT_ERROR); + break; + } + if (ch->on_command_received) { + ch->on_command_received(*ch, msg.command, msg.flag, msg.data); + } + } +} + +void Channel::dispatch_on_error(struct bufferevent*, short events, void* ctx) { + Channel* ch = reinterpret_cast(ctx); + if (ch->on_error) { + ch->on_error(*ch, events); + } else { + ch->disconnect(); + } +} diff --git a/src/Channel.hh b/src/Channel.hh index f5a02f5d..a6cc3685 100644 --- a/src/Channel.hh +++ b/src/Channel.hh @@ -1,103 +1,103 @@ -#pragma once - -#include - -#include -#include - -#include "PSOEncryption.hh" -#include "PSOProtocol.hh" -#include "Version.hh" - -struct Channel { - std::unique_ptr bev; - struct sockaddr_storage local_addr; - struct sockaddr_storage remote_addr; - uint64_t virtual_network_id; // 0 = normal TCP connection - - Version version; - uint8_t language; - std::shared_ptr crypt_in; - std::shared_ptr crypt_out; - - std::string name; - TerminalFormat terminal_send_color; - TerminalFormat terminal_recv_color; - - struct Message { - uint16_t command; - uint32_t flag; - std::string data; - }; - - typedef void (*on_command_received_t)(Channel&, uint16_t, uint32_t, std::string&); - typedef void (*on_error_t)(Channel&, short); - - on_command_received_t on_command_received; - on_error_t on_error; - void* context_obj; - - // Creates an unconnected channel - Channel( - Version version, - uint8_t language, - on_command_received_t on_command_received, - on_error_t on_error, - void* context_obj, - const std::string& name, - TerminalFormat terminal_send_color = TerminalFormat::END, - TerminalFormat terminal_recv_color = TerminalFormat::END); - // Creates a connected channel - Channel( - struct bufferevent* bev, - uint64_t virtual_network_id, - Version version, - uint8_t language, - on_command_received_t on_command_received, - on_error_t on_error, - void* context_obj, - const std::string& name = "", - TerminalFormat terminal_send_color = TerminalFormat::END, - TerminalFormat terminal_recv_color = TerminalFormat::END); - Channel(const Channel& other) = delete; - Channel(Channel&& other) = delete; - Channel& operator=(const Channel& other) = delete; - Channel& operator=(Channel&& other) = delete; - - void replace_with( - Channel&& other, - on_command_received_t on_command_received, - on_error_t on_error, - void* context_obj, - const std::string& name = ""); - - void set_bufferevent(struct bufferevent* bev, uint64_t virtual_network_id); - - inline bool connected() const { - return this->bev.get() != nullptr; - } - void disconnect(); - - // Receives a message. Throws std::out_of_range if no messages are available. - Message recv(); - - // Sends a message with an automatically-constructed header. - void send(uint16_t cmd, uint32_t flag = 0, bool silent = false); - void send(uint16_t cmd, uint32_t flag, const void* data, size_t size, bool silent = false); - void send(uint16_t cmd, uint32_t flag, const std::vector> blocks, bool silent = false); - void send(uint16_t cmd, uint32_t flag, const std::string& data, bool silent = false); - template - requires(!std::is_pointer_v) - void send(uint16_t cmd, uint32_t flag, const CmdT& data, bool silent = false) { - this->send(cmd, flag, &data, sizeof(data), silent); - } - - // Sends a message with a pre-existing header (as the first few bytes in the - // data) - void send(const void* data, size_t size, bool silent = false); - void send(const std::string& data, bool silent = false); - -private: - static void dispatch_on_input(struct bufferevent*, void* ctx); - static void dispatch_on_error(struct bufferevent*, short events, void* ctx); -}; +#pragma once + +#include + +#include +#include + +#include "PSOEncryption.hh" +#include "PSOProtocol.hh" +#include "Version.hh" + +struct Channel { + std::unique_ptr bev; + struct sockaddr_storage local_addr; + struct sockaddr_storage remote_addr; + uint64_t virtual_network_id; // 0 = normal TCP connection + + Version version; + uint8_t language; + std::shared_ptr crypt_in; + std::shared_ptr crypt_out; + + std::string name; + TerminalFormat terminal_send_color; + TerminalFormat terminal_recv_color; + + struct Message { + uint16_t command; + uint32_t flag; + std::string data; + }; + + typedef void (*on_command_received_t)(Channel&, uint16_t, uint32_t, std::string&); + typedef void (*on_error_t)(Channel&, short); + + on_command_received_t on_command_received; + on_error_t on_error; + void* context_obj; + + // Creates an unconnected channel + Channel( + Version version, + uint8_t language, + on_command_received_t on_command_received, + on_error_t on_error, + void* context_obj, + const std::string& name, + TerminalFormat terminal_send_color = TerminalFormat::END, + TerminalFormat terminal_recv_color = TerminalFormat::END); + // Creates a connected channel + Channel( + struct bufferevent* bev, + uint64_t virtual_network_id, + Version version, + uint8_t language, + on_command_received_t on_command_received, + on_error_t on_error, + void* context_obj, + const std::string& name = "", + TerminalFormat terminal_send_color = TerminalFormat::END, + TerminalFormat terminal_recv_color = TerminalFormat::END); + Channel(const Channel& other) = delete; + Channel(Channel&& other) = delete; + Channel& operator=(const Channel& other) = delete; + Channel& operator=(Channel&& other) = delete; + + void replace_with( + Channel&& other, + on_command_received_t on_command_received, + on_error_t on_error, + void* context_obj, + const std::string& name = ""); + + void set_bufferevent(struct bufferevent* bev, uint64_t virtual_network_id); + + inline bool connected() const { + return this->bev.get() != nullptr; + } + void disconnect(); + + // Receives a message. Throws std::out_of_range if no messages are available. + Message recv(); + + // Sends a message with an automatically-constructed header. + void send(uint16_t cmd, uint32_t flag = 0, bool silent = false); + void send(uint16_t cmd, uint32_t flag, const void* data, size_t size, bool silent = false); + void send(uint16_t cmd, uint32_t flag, const std::vector> blocks, bool silent = false); + void send(uint16_t cmd, uint32_t flag, const std::string& data, bool silent = false); + template + requires(!std::is_pointer_v) + void send(uint16_t cmd, uint32_t flag, const CmdT& data, bool silent = false) { + this->send(cmd, flag, &data, sizeof(data), silent); + } + + // Sends a message with a pre-existing header (as the first few bytes in the + // data) + void send(const void* data, size_t size, bool silent = false); + void send(const std::string& data, bool silent = false); + +private: + static void dispatch_on_input(struct bufferevent*, void* ctx); + static void dispatch_on_error(struct bufferevent*, short events, void* ctx); +}; diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index f314f328..65b9b44c 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -1,2603 +1,2603 @@ -#include "ChatCommands.hh" - -#include - -#include -#include -#include -#include -#include -#include - -#include "Client.hh" -#include "Lobby.hh" -#include "Loggers.hh" -#include "ProxyServer.hh" -#include "ReceiveCommands.hh" -#include "Revision.hh" -#include "SendCommands.hh" -#include "Server.hh" -#include "StaticGameData.hh" -#include "Text.hh" - -using namespace std; - -//////////////////////////////////////////////////////////////////////////////// -// Checks - -class precondition_failed { -public: - precondition_failed(const std::string& user_msg) : user_msg(user_msg) {} - ~precondition_failed() = default; - - const std::string& what() const { - return this->user_msg; - } - -private: - std::string user_msg; -}; - -static void check_account_flag(shared_ptr c, Account::Flag flag) { - if (!c->login) { - throw precondition_failed("$C6You are not\nlogged in."); - } - if (!c->login->account->check_flag(flag)) { - throw precondition_failed("$C6You do not have\npermission to\nrun this command."); - } -} - -static void check_version(shared_ptr c, Version version) { - if (c->version() != version) { - throw precondition_failed("$C6This command cannot\nbe used for your\nversion of PSO."); - } -} - -static void check_is_game(shared_ptr l, bool is_game) { - if (l->is_game() != is_game) { - throw precondition_failed(is_game ? "$C6This command cannot\nbe used in lobbies." : "$C6This command cannot\nbe used in games."); - } -} - -static void check_is_ep3(shared_ptr c, bool is_ep3) { - if (::is_ep3(c->version()) != is_ep3) { - throw precondition_failed(is_ep3 ? "$C6This command can only\nbe used in Episode 3." : "$C6This command cannot\nbe used in Episode 3."); - } -} - -static void check_debug_enabled(shared_ptr c) { - if (!c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { - throw precondition_failed("$C6This command can only\nbe run in debug mode\n(run %sdebug first)"); - } -} - -static void check_cheats_enabled(shared_ptr l, shared_ptr c) { - if (!l->check_flag(Lobby::Flag::CHEATS_ENABLED) && - !c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE)) { - throw precondition_failed("$C6This command can\nonly be used in\ncheat mode."); - } -} - -static void check_cheats_allowed(shared_ptr s, shared_ptr c) { - if ((s->cheat_mode_behavior == ServerState::BehaviorSwitch::OFF) && - !c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE)) { - throw precondition_failed("$C6Cheats are disabled\non this server."); - } -} - -static void check_cheats_allowed(shared_ptr s, shared_ptr ses) { - if ((s->cheat_mode_behavior == ServerState::BehaviorSwitch::OFF) && - (!ses->login || !ses->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE))) { - throw precondition_failed("$C6Cheats are disabled\non this proxy."); - } -} - -static void check_is_leader(shared_ptr l, shared_ptr c) { - if (l->leader_id != c->lobby_client_id) { - throw precondition_failed("$C6This command can\nonly be used by\nthe game leader."); - } -} - -//////////////////////////////////////////////////////////////////////////////// -// Message commands - -static void server_command_server_info(shared_ptr c, const std::string&) { - auto s = c->require_server_state(); - string uptime_str = format_duration(now() - s->creation_time); - send_text_message_printf(c, - "Uptime: $C6%s$C7\nLobbies: $C6%zu$C7\nClients: $C6%zu$C7(g) $C6%zu$C7(p)", - uptime_str.c_str(), - s->id_to_lobby.size(), - s->channel_to_client.size(), - s->proxy_server->num_sessions()); -} - -static void server_command_lobby_info(shared_ptr c, const std::string&) { - vector lines; - - auto l = c->lobby.lock(); - if (!l) { - lines.emplace_back("$C4No lobby info"); - - } else { - if (l->is_game()) { - if (!l->is_ep3()) { - if (l->max_level == 0xFFFFFFFF) { - lines.emplace_back(string_printf("$C6%08X$C7 L$C6%d+$C7", l->lobby_id, l->min_level + 1)); - } else { - lines.emplace_back(string_printf("$C6%08X$C7 L$C6%d-%d$C7", l->lobby_id, l->min_level + 1, l->max_level + 1)); - } - lines.emplace_back(string_printf("$C7Section ID: $C6%s$C7", name_for_section_id(l->effective_section_id()))); - - switch (l->drop_mode) { - case Lobby::DropMode::DISABLED: - lines.emplace_back("Drops disabled"); - break; - case Lobby::DropMode::CLIENT: - lines.emplace_back("Client item table"); - break; - case Lobby::DropMode::SERVER_SHARED: - lines.emplace_back("Server item table"); - break; - case Lobby::DropMode::SERVER_PRIVATE: - lines.emplace_back("Server indiv items"); - break; - case Lobby::DropMode::SERVER_DUPLICATE: - lines.emplace_back("Server dup items"); - break; - default: - lines.emplace_back("$C4Unknown drop mode$C7"); - } - if (l->check_flag(Lobby::Flag::CHEATS_ENABLED)) { - lines.emplace_back("Cheats enabled"); - } - - } else { - lines.emplace_back(string_printf("$C7State seed: $C6%08X$C7", l->random_seed)); - } - - } else { - lines.emplace_back(string_printf("$C7Lobby ID: $C6%08X$C7", l->lobby_id)); - } - - string slots_str = "Slots: "; - for (size_t z = 0; z < l->clients.size(); z++) { - if (!l->clients[z]) { - slots_str += string_printf("$C0%zX$C7", z); - } else { - bool is_self = l->clients[z] == c; - bool is_leader = z == l->leader_id; - if (is_self && is_leader) { - slots_str += string_printf("$C6%zX$C7", z); - } else if (is_self) { - slots_str += string_printf("$C2%zX$C7", z); - } else if (is_leader) { - slots_str += string_printf("$C4%zX$C7", z); - } else { - slots_str += string_printf("%zX", z); - } - } - } - lines.emplace_back(std::move(slots_str)); - } - - send_text_message(c, join(lines, "\n")); -} - -static void server_command_ping(shared_ptr c, const std::string&) { - c->ping_start_time = now(); - send_command(c, 0x1D, 0x00); -} - -static void proxy_command_ping(shared_ptr ses, const std::string&) { - ses->client_ping_start_time = now(); - ses->server_ping_start_time = now(); - - C_GuildCardSearch_40 cmd = {0x00010000, ses->remote_guild_card_number, ses->remote_guild_card_number}; - ses->client_channel.send(0x1D, 0x00); - ses->server_channel.send(0x40, 0x00, &cmd, sizeof(cmd)); -} - -static void proxy_command_lobby_info(shared_ptr ses, const std::string&) { - string msg; - // On non-masked-GC sessions (BB), there is no remote Guild Card number, so we - // don't show it. (The user can see it in the pause menu, unlike in masked-GC - // sessions like GC.) - if (ses->remote_guild_card_number >= 0) { - msg = string_printf("$C7GC: $C6%" PRId64 "$C7\n", ses->remote_guild_card_number); - } - msg += "Slots: "; - - for (size_t z = 0; z < ses->lobby_players.size(); z++) { - bool is_self = z == ses->lobby_client_id; - bool is_leader = z == ses->leader_client_id; - if (ses->lobby_players[z].guild_card_number == 0) { - msg += string_printf("$C0%zX$C7", z); - } else if (is_self && is_leader) { - msg += string_printf("$C6%zX$C7", z); - } else if (is_self) { - msg += string_printf("$C2%zX$C7", z); - } else if (is_leader) { - msg += string_printf("$C4%zX$C7", z); - } else { - msg += string_printf("%zX", z); - } - } - - vector cheats_tokens; - if (ses->config.check_flag(Client::Flag::INFINITE_HP_ENABLED)) { - cheats_tokens.emplace_back("HP"); - } - if (ses->config.check_flag(Client::Flag::INFINITE_TP_ENABLED)) { - cheats_tokens.emplace_back("TP"); - } - if (!cheats_tokens.empty()) { - msg += "\n$C7Cheats: $C6"; - msg += join(cheats_tokens, ","); - } - - vector behaviors_tokens; - if (ses->config.check_flag(Client::Flag::SWITCH_ASSIST_ENABLED)) { - behaviors_tokens.emplace_back("SWA"); - } - if (ses->config.check_flag(Client::Flag::PROXY_SAVE_FILES)) { - behaviors_tokens.emplace_back("SF"); - } - if (ses->config.check_flag(Client::Flag::PROXY_SUPPRESS_REMOTE_LOGIN)) { - behaviors_tokens.emplace_back("SL"); - } - if (ses->config.check_flag(Client::Flag::PROXY_BLOCK_FUNCTION_CALLS)) { - behaviors_tokens.emplace_back("BF"); - } - if (!behaviors_tokens.empty()) { - msg += "\n$C7Flags: $C6"; - msg += join(behaviors_tokens, ","); - } - - if (ses->config.override_section_id != 0xFF) { - msg += "\n$C7SecID*: $C6"; - msg += name_for_section_id(ses->config.override_section_id); - } - - send_text_message(ses->client_channel, msg); -} - -static void server_command_ax(shared_ptr c, const std::string& args) { - check_account_flag(c, Account::Flag::ANNOUNCE); - ax_messages_log.info("%s", args.c_str()); -} - -static void server_command_announce(shared_ptr c, const std::string& args) { - auto s = c->require_server_state(); - check_account_flag(c, Account::Flag::ANNOUNCE); - send_text_message(s, args); -} - -static void server_command_announce_mail(shared_ptr c, const std::string& args) { - auto s = c->require_server_state(); - check_account_flag(c, Account::Flag::ANNOUNCE); - send_simple_mail(s, 0, s->name, args); -} - -static void server_command_arrow(shared_ptr c, const std::string& args) { - auto l = c->require_lobby(); - c->lobby_arrow_color = stoull(args, nullptr, 0); - if (!l->is_game()) { - send_arrow_update(l); - } -} - -static void proxy_command_arrow(shared_ptr ses, const std::string& args) { - ses->server_channel.send(0x89, stoull(args, nullptr, 0)); -} - -static void server_command_debug(shared_ptr c, const std::string&) { - check_account_flag(c, Account::Flag::DEBUG); - c->config.toggle_flag(Client::Flag::DEBUG_ENABLED); - send_text_message_printf(c, "Debug %s", (c->config.check_flag(Client::Flag::DEBUG_ENABLED) ? "enabled" : "disabled")); -} - -static void server_command_quest(shared_ptr c, const std::string& args) { - auto s = c->require_server_state(); - auto l = c->require_lobby(); - if (!l->is_game()) { - throw precondition_failed("$C6Quests cannot be\nstarted from the\nlobby"); - } - - Version effective_version = is_ep3(c->version()) ? Version::GC_V3 : c->version(); - auto q = s->quest_index(effective_version)->get(stoul(args)); - if (!q) { - send_text_message(c, "$C6Quest not found"); - return; - } - - if (!c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { - if (l->count_clients() > 1) { - throw precondition_failed("$C6This command can only\nbe used with no\nother players present"); - } - if (!q->allow_start_from_chat_command) { - throw precondition_failed("$C6This quest cannot\nbe started with the\n%squest command"); - } - } - - set_lobby_quest(c->require_lobby(), q, true); -} - -static void server_command_qcheck(shared_ptr c, const std::string& args) { - auto l = c->require_lobby(); - uint16_t flag_num = stoul(args, nullptr, 0); - - if (l->is_game()) { - if (!l->quest_flags_known || l->quest_flags_known->get(l->difficulty, flag_num)) { - send_text_message_printf(c, "$C7Game: flag 0x%hX (%hu)\nis %s on %s", - flag_num, flag_num, - c->character()->quest_flags.get(l->difficulty, flag_num) ? "set" : "not set", - name_for_difficulty(l->difficulty)); - } else { - send_text_message_printf(c, "$C7Game: flag 0x%hX (%hu)\nis unknown on %s", - flag_num, flag_num, name_for_difficulty(l->difficulty)); - } - } else if (c->version() == Version::BB_V4) { - send_text_message_printf(c, "$C7Player: flag 0x%hX (%hu)\nis %s on %s", - flag_num, flag_num, - c->character()->quest_flags.get(l->difficulty, flag_num) ? "set" : "not set", - name_for_difficulty(l->difficulty)); - } -} - -static void server_command_swset_swclear(shared_ptr c, const std::string& args, bool should_set) { - check_debug_enabled(c); - auto l = c->require_lobby(); - if (!l->is_game()) { - send_text_message(c, "$C6This command cannot\nbe used in the lobby"); - return; - } - - auto tokens = split(args, ' '); - uint8_t floor, flag_num; - if (tokens.size() == 1) { - floor = c->floor; - flag_num = stoul(tokens[0], nullptr, 0); - } else if (tokens.size() == 2) { - floor = stoul(tokens[0], nullptr, 0); - flag_num = stoul(tokens[1], nullptr, 0); - } else { - send_text_message(c, "$C4Incorrect parameters"); - return; - } - - if (should_set) { - l->switch_flags->set(floor, flag_num); - } else { - l->switch_flags->clear(floor, flag_num); - } - - uint8_t cmd_flags = should_set ? 0x01 : 0x00; - G_SwitchStateChanged_6x05 cmd = {{0x05, 0x03, 0xFFFF}, 0, 0, flag_num, floor, cmd_flags}; - send_command_t(l, 0x60, 0x00, cmd); -} - -static void server_command_swset(shared_ptr c, const std::string& args) { - return server_command_swset_swclear(c, args, true); -} - -static void server_command_swclear(shared_ptr c, const std::string& args) { - return server_command_swset_swclear(c, args, false); -} - -static void proxy_command_swset_swclear(shared_ptr ses, const std::string& args, bool should_set) { - if (!ses->is_in_game) { - send_text_message(ses->client_channel, "$C6This command cannot\nbe used in the lobby"); - return; - } - - auto tokens = split(args, ' '); - uint8_t floor, flag_num; - if (tokens.size() == 1) { - floor = ses->floor; - flag_num = stoul(tokens[0], nullptr, 0); - } else if (tokens.size() == 2) { - floor = stoul(tokens[0], nullptr, 0); - flag_num = stoul(tokens[1], nullptr, 0); - } else { - send_text_message(ses->client_channel, "$C4Incorrect parameters"); - return; - } - - uint8_t cmd_flags = should_set ? 0x01 : 0x00; - G_SwitchStateChanged_6x05 cmd = {{0x05, 0x03, 0xFFFF}, 0, 0, flag_num, floor, cmd_flags}; - ses->client_channel.send(0x60, 0x00, &cmd, sizeof(cmd)); - ses->server_channel.send(0x60, 0x00, &cmd, sizeof(cmd)); -} - -static void proxy_command_swset(shared_ptr ses, const std::string& args) { - return proxy_command_swset_swclear(ses, args, true); -} - -static void proxy_command_swclear(shared_ptr ses, const std::string& args) { - return proxy_command_swset_swclear(ses, args, false); -} - -static void server_command_swsetall(shared_ptr c, const std::string&) { - check_debug_enabled(c); - auto l = c->require_lobby(); - if (!l->is_game()) { - send_text_message(c, "$C6This command cannot\nbe used in the lobby"); - return; - } - - l->switch_flags->data[c->floor].clear(0xFF); - - parray cmds; - for (size_t z = 0; z < cmds.size(); z++) { - auto& cmd = cmds[z]; - cmd.header.subcommand = 0x05; - cmd.header.size = 0x03; - cmd.header.object_id = 0xFFFF; - cmd.switch_flag_floor = c->floor; - cmd.switch_flag_num = z; - cmd.flags = 0x01; - } - cmds[0].flags = 0x03; // Play room unlock sound - send_command_t(l, 0x6C, 0x00, cmds); -} - -static void proxy_command_swsetall(shared_ptr ses, const std::string&) { - if (!ses->is_in_game) { - send_text_message(ses->client_channel, "$C6This command cannot\nbe used in the lobby"); - return; - } - - parray cmds; - for (size_t z = 0; z < cmds.size(); z++) { - auto& cmd = cmds[z]; - cmd.header.subcommand = 0x05; - cmd.header.size = 0x03; - cmd.header.object_id = 0xFFFF; - cmd.switch_flag_floor = ses->floor; - cmd.switch_flag_num = z; - cmd.flags = 0x01; - } - cmds[0].flags = 0x03; // Play room unlock sound - ses->client_channel.send(0x6C, 0x00, &cmds, sizeof(cmds)); - ses->server_channel.send(0x6C, 0x00, &cmds, sizeof(cmds)); -} - -static void server_command_qset_qclear(shared_ptr c, const std::string& args, bool should_set) { - check_debug_enabled(c); - auto l = c->require_lobby(); - if (!l->is_game()) { - send_text_message(c, "$C6This command cannot\nbe used in the lobby"); - return; - } - - uint16_t flag_num = stoul(args, nullptr, 0); - - if (l->is_game()) { - if (l->quest_flags_known) { - l->quest_flags_known->set(l->difficulty, flag_num); - } - if (should_set) { - l->quest_flag_values->set(l->difficulty, flag_num); - } else { - l->quest_flag_values->clear(l->difficulty, flag_num); - } - } - - auto p = c->character(false); - if (p) { - if (should_set) { - p->quest_flags.set(l->difficulty, flag_num); - } else { - p->quest_flags.clear(l->difficulty, flag_num); - } - } - - if (is_v1_or_v2(c->version())) { - G_UpdateQuestFlag_DC_PC_6x75 cmd = {{0x75, 0x02, 0x0000}, flag_num, should_set ? 0 : 1}; - send_command_t(l, 0x60, 0x00, cmd); - } else { - G_UpdateQuestFlag_V3_BB_6x75 cmd = {{{0x75, 0x03, 0x0000}, flag_num, should_set ? 0 : 1}, l->difficulty, 0x0000}; - send_command_t(l, 0x60, 0x00, cmd); - } -} - -static void server_command_qset(shared_ptr c, const std::string& args) { - return server_command_qset_qclear(c, args, true); -} - -static void server_command_qclear(shared_ptr c, const std::string& args) { - return server_command_qset_qclear(c, args, false); -} - -static void proxy_command_qset_qclear(shared_ptr ses, const std::string& args, bool should_set) { - if (!ses->is_in_game) { - send_text_message(ses->client_channel, "$C6This command cannot\nbe used in the lobby"); - return; - } - - uint16_t flag_num = stoul(args, nullptr, 0); - if (is_v1_or_v2(ses->version())) { - G_UpdateQuestFlag_DC_PC_6x75 cmd = {{0x75, 0x02, 0x0000}, flag_num, should_set ? 0 : 1}; - ses->client_channel.send(0x60, 0x00, &cmd, sizeof(cmd)); - ses->server_channel.send(0x60, 0x00, &cmd, sizeof(cmd)); - } else { - G_UpdateQuestFlag_V3_BB_6x75 cmd = {{{0x75, 0x03, 0x0000}, flag_num, should_set ? 0 : 1}, ses->lobby_difficulty, 0x0000}; - ses->client_channel.send(0x60, 0x00, &cmd, sizeof(cmd)); - ses->server_channel.send(0x60, 0x00, &cmd, sizeof(cmd)); - } -} - -static void proxy_command_qset(shared_ptr ses, const std::string& args) { - return proxy_command_qset_qclear(ses, args, true); -} - -static void proxy_command_qclear(shared_ptr ses, const std::string& args) { - return proxy_command_qset_qclear(ses, args, false); -} - -static void server_command_qgread(shared_ptr c, const std::string& args) { - uint8_t flag_num = stoul(args, nullptr, 0); - const auto& flags = c->character()->quest_counters; - if (flag_num >= flags.size()) { - send_text_message_printf(c, "$C7Flag number must be\nless than %zu", flags.size()); - } else { - send_text_message_printf(c, "$C7Quest counter %hhu\nhas value %" PRIu32, flag_num, flags[flag_num].load()); - } -} - -static void server_command_qfread(shared_ptr c, const std::string& args) { - auto s = c->require_server_state(); - - uint8_t counter_index; - uint32_t mask; - try { - const auto& def = s->quest_counter_fields.at(args); - counter_index = def.first; - mask = def.second; - } catch (const out_of_range&) { - send_text_message(c, "$C4Invalid field name"); - return; - } - if (mask == 0) { - throw runtime_error("invalid quest counter definition"); - } - - uint32_t counter_value = c->character()->quest_counters.at(counter_index) & mask; - - while (!(mask & 1)) { - mask >>= 1; - counter_value >>= 1; - } - - if (mask == 1) { - send_text_message_printf(c, "$C7Field %s\nhas value %s", args.c_str(), counter_value ? "TRUE" : "FALSE"); - } else { - send_text_message_printf(c, "$C7Field %s\nhas value %" PRIu32, args.c_str(), counter_value); - } -} - -static void server_command_qgwrite(shared_ptr c, const std::string& args) { - if (c->version() != Version::BB_V4) { - send_text_message(c, "$C6This command can\nonly be used on BB"); - return; - } - check_debug_enabled(c); - auto l = c->require_lobby(); - if (!l->is_game()) { - send_text_message(c, "$C6This command cannot\nbe used in the lobby"); - return; - } - - auto tokens = split(args, ' '); - if (tokens.size() != 2) { - send_text_message(c, "$C6Incorrect number\nof arguments"); - return; - } - - uint8_t flag_num = stoul(tokens[0], nullptr, 0); - uint32_t value = stoul(tokens[1], nullptr, 0); - auto& flags = c->character()->quest_counters; - if (flag_num >= flags.size()) { - send_text_message_printf(c, "$C7Flag number must be\nless than %zu", flags.size()); - } else { - c->character()->quest_counters[flag_num] = value; - G_SetQuestCounter_BB_6xD2 cmd = {{0xD2, sizeof(G_SetQuestCounter_BB_6xD2) / 4, c->lobby_client_id}, flag_num, value}; - send_command_t(c, 0x60, 0x00, cmd); - send_text_message_printf(c, "$C7Quest counter %hhu\nset to %" PRIu32, flag_num, value); - } -} - -static void server_command_qsync_qsyncall(shared_ptr c, const std::string& args, bool send_to_lobby) { - check_debug_enabled(c); - auto l = c->require_lobby(); - if (!l->is_game()) { - send_text_message(c, "$C6This command cannot\nbe used in the lobby"); - return; - } - - auto tokens = split(args, ' '); - if (tokens.size() != 2) { - send_text_message(c, "$C6Incorrect number of\narguments"); - return; - } - - G_SyncQuestRegister_6x77 cmd; - cmd.header = {0x77, 0x03, 0x0000}; - cmd.register_number = stoul(tokens[0].substr(1), nullptr, 0); - cmd.unused = 0; - if (tokens[0][0] == 'r') { - cmd.value.as_int = stoul(tokens[1], nullptr, 0); - } else if (tokens[0][0] == 'f') { - cmd.value.as_float = stof(tokens[1]); - } else { - send_text_message(c, "$C6First argument must\nbe a register"); - return; - } - if (send_to_lobby) { - send_command_t(l, 0x60, 0x00, cmd); - } else { - send_command_t(c, 0x60, 0x00, cmd); - } -} - -static void server_command_qsync(shared_ptr c, const std::string& args) { - server_command_qsync_qsyncall(c, args, false); -} - -static void server_command_qsyncall(shared_ptr c, const std::string& args) { - server_command_qsync_qsyncall(c, args, true); -} - -static void proxy_command_qsync_qsyncall(shared_ptr ses, const std::string& args, bool send_to_lobby) { - if (!ses->is_in_game) { - send_text_message(ses->client_channel, "$C6This command cannot\nbe used in the lobby"); - return; - } - - auto tokens = split(args, ' '); - if (tokens.size() != 2) { - send_text_message(ses->client_channel, "$C6Incorrect number of\narguments"); - return; - } - - G_SyncQuestRegister_6x77 cmd; - cmd.header = {0x77, 0x03, 0x0000}; - cmd.register_number = stoul(tokens[0].substr(1), nullptr, 0); - cmd.unused = 0; - if (tokens[0][0] == 'r') { - cmd.value.as_int = stoul(tokens[1], nullptr, 0); - } else if (tokens[0][0] == 'f') { - cmd.value.as_float = stof(tokens[1]); - } else { - send_text_message(ses->client_channel, "$C6First argument must\nbe a register"); - return; - } - ses->client_channel.send(0x60, 0x00, cmd); - if (send_to_lobby) { - ses->server_channel.send(0x60, 0x00, cmd); - } -} - -static void proxy_command_qsync(shared_ptr ses, const std::string& args) { - proxy_command_qsync_qsyncall(ses, args, false); -} - -static void proxy_command_qsyncall(shared_ptr ses, const std::string& args) { - proxy_command_qsync_qsyncall(ses, args, true); -} - -static void server_command_qcall(shared_ptr c, const std::string& args) { - check_debug_enabled(c); - - auto l = c->require_lobby(); - if (l->is_game() && l->quest) { - send_quest_function_call(c, stoul(args, nullptr, 0)); - } else { - send_text_message(c, "$C6You must be in a\nquest to use this\ncommand"); - } -} - -static void proxy_command_qcall(shared_ptr ses, const std::string& args) { - if (ses->is_in_game && ses->is_in_quest) { - send_quest_function_call(ses->client_channel, stoul(args, nullptr, 0)); - } else { - send_text_message(ses->client_channel, "$C6You must be in a\nquest to use this\ncommand"); - } -} - -static void server_command_show_material_counts(shared_ptr c, const std::string&) { - auto p = c->character(); - if (is_v1_or_v2(c->version())) { - send_text_message_printf(c, "%hhu HP, %hhu TP", - p->get_material_usage(PSOBBCharacterFile::MaterialType::HP), - p->get_material_usage(PSOBBCharacterFile::MaterialType::TP)); - } else { - send_text_message_printf(c, "%hhu HP, %hhu TP, %hhu POW\n%hhu MIND, %hhu EVADE\n%hhu DEF, %hhu LUCK", - p->get_material_usage(PSOBBCharacterFile::MaterialType::HP), - p->get_material_usage(PSOBBCharacterFile::MaterialType::TP), - p->get_material_usage(PSOBBCharacterFile::MaterialType::POWER), - p->get_material_usage(PSOBBCharacterFile::MaterialType::MIND), - p->get_material_usage(PSOBBCharacterFile::MaterialType::EVADE), - p->get_material_usage(PSOBBCharacterFile::MaterialType::DEF), - p->get_material_usage(PSOBBCharacterFile::MaterialType::LUCK)); - } -} - -static void server_command_auction(shared_ptr c, const std::string&) { - check_account_flag(c, Account::Flag::DEBUG); - auto l = c->require_lobby(); - if (l->is_game() && l->is_ep3()) { - G_InitiateCardAuction_Ep3_6xB5x42 cmd; - cmd.header.sender_client_id = c->lobby_client_id; - send_command_t(l, 0xC9, 0x00, cmd); - } -} - -static void proxy_command_auction(shared_ptr ses, const std::string&) { - G_InitiateCardAuction_Ep3_6xB5x42 cmd; - cmd.header.sender_client_id = ses->lobby_client_id; - ses->client_channel.send(0xC9, 0x00, &cmd, sizeof(cmd)); - ses->server_channel.send(0xC9, 0x00, &cmd, sizeof(cmd)); -} - -static void server_command_patch(shared_ptr c, const std::string& args) { - prepare_client_for_patches(c, [wc = weak_ptr(c), args]() { - auto c = wc.lock(); - if (!c) { - return; - } - try { - auto s = c->require_server_state(); - // Note: We can't look this up outside of the closure because - // c->specific_version can change during prepare_client_for_patches - auto fn = s->function_code_index->get_patch(args, c->config.specific_version); - send_function_call(c, fn); - c->function_call_response_queue.emplace_back(empty_function_call_response_handler); - } catch (const out_of_range&) { - send_text_message(c, "Invalid patch name"); - } - }); -} - -static void empty_patch_return_handler(uint32_t, uint32_t) {} - -static void proxy_command_patch(shared_ptr ses, const std::string& args) { - auto send_call = [args, ses](uint32_t specific_version, uint32_t) { - try { - if (ses->config.specific_version != specific_version) { - ses->config.specific_version = specific_version; - ses->log.info("Version detected as %08" PRIX32, ses->config.specific_version); - } - auto s = ses->require_server_state(); - auto fn = s->function_code_index->get_patch(args, ses->config.specific_version); - send_function_call(ses->client_channel, ses->config, fn); - // Don't forward the patch response to the server - ses->function_call_return_handler_queue.emplace_back(empty_patch_return_handler); - } catch (const out_of_range&) { - send_text_message(ses->client_channel, "Invalid patch name"); - } - }; - - auto send_version_detect_or_send_call = [args, ses, send_call]() { - bool is_gc = ::is_gc(ses->version()); - bool is_xb = (ses->version() == Version::XB_V3); - if ((is_gc || is_xb) && specific_version_is_indeterminate(ses->config.specific_version)) { - auto s = ses->require_server_state(); - send_function_call( - ses->client_channel, - ses->config, - s->function_code_index->name_to_function.at(is_xb ? "VersionDetectXB" : "VersionDetectGC")); - ses->function_call_return_handler_queue.emplace_back(send_call); - } else { - send_call(ses->config.specific_version, 0); - } - }; - - // This mirrors the implementation in prepare_client_for_patches - if (!ses->config.check_flag(Client::Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH)) { - auto s = ses->require_server_state(); - send_function_call( - ses->client_channel, ses->config, s->function_code_index->name_to_function.at("CacheClearFix-Phase1"), {}, "", 0, 0, 0x7F2734EC); - ses->function_call_return_handler_queue.emplace_back([s, ses, send_version_detect_or_send_call](uint32_t, uint32_t) -> void { - send_function_call( - ses->client_channel, ses->config, s->function_code_index->name_to_function.at("CacheClearFix-Phase2")); - ses->function_call_return_handler_queue.emplace_back([ses, send_version_detect_or_send_call](uint32_t, uint32_t) -> void { - ses->config.set_flag(Client::Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH); - send_version_detect_or_send_call(); - }); - }); - } else { - send_version_detect_or_send_call(); - } -} - -static void server_command_persist(shared_ptr c, const std::string&) { - auto l = c->require_lobby(); - if (l->check_flag(Lobby::Flag::DEFAULT)) { - send_text_message(c, "$C6Default lobbies\ncannot be marked\ntemporary"); - } else if (!l->is_game()) { - send_text_message(c, "$C6Private lobbies\ncannot be marked\npersistent"); - } else if (l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS) || l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)) { - send_text_message(c, "$C6Games cannot be\npersistent if a\nquest has already\nbegun"); - } else if (l->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM)) { - send_text_message(c, "$C6Spectator teams\ncannot be marked\npersistent"); - } else { - l->toggle_flag(Lobby::Flag::PERSISTENT); - send_text_message_printf(l, "Lobby persistence\n%s", l->check_flag(Lobby::Flag::PERSISTENT) ? "enabled" : "disabled"); - } -} - -static void server_command_exit(shared_ptr c, const std::string&) { - auto l = c->require_lobby(); - if (l->is_game()) { - if (l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS) || l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)) { - G_UnusedHeader cmd = {0x73, 0x01, 0x0000}; - c->channel.send(0x60, 0x00, cmd); - c->floor = 0; - c->recent_switch_flags.clear(); - } else if (is_ep3(c->version())) { - c->channel.send(0xED, 0x00); - } else { - send_text_message(c, "$C6You must return to\nthe lobby first"); - } - } else { - send_self_leave_notification(c); - if (!c->config.check_flag(Client::Flag::NO_D6)) { - send_message_box(c, ""); - } - - send_client_to_login_server(c); - } -} - -static void proxy_command_exit(shared_ptr ses, const std::string&) { - if (ses->is_in_game) { - if (is_ep3(ses->version())) { - ses->client_channel.send(0xED, 0x00); - } else if (ses->is_in_quest) { - G_UnusedHeader cmd = {0x73, 0x01, 0x0000}; - ses->client_channel.send(0x60, 0x00, cmd); - } else { - send_text_message(ses->client_channel, "$C6You must return to\nthe lobby first"); - } - } else { - ses->disconnect_action = ProxyServer::LinkedSession::DisconnectAction::CLOSE_IMMEDIATELY; - ses->send_to_game_server(); - } -} - -static void server_command_get_self_card(shared_ptr c, const std::string&) { - send_guild_card(c, c); -} - -static void proxy_command_get_player_card(shared_ptr ses, const std::string& args) { - - bool any_card_sent = false; - for (const auto& p : ses->lobby_players) { - if (!p.name.empty() && args == p.name) { - send_guild_card(ses->client_channel, p.guild_card_number, p.guild_card_number, p.name, "", "", p.language, p.section_id, p.char_class); - any_card_sent = true; - } - } - - if (!any_card_sent) { - try { - size_t index = stoull(args, nullptr, 0); - const auto& p = ses->lobby_players.at(index); - if (!p.name.empty()) { - send_guild_card(ses->client_channel, p.guild_card_number, p.guild_card_number, p.name, "", "", p.language, p.section_id, p.char_class); - } - } catch (const exception& e) { - send_text_message(ses->client_channel, "Error: " + remove_color(e.what())); - } - } -} - -static void server_command_send_client(shared_ptr c, const std::string& args) { - check_debug_enabled(c); - string data = parse_data_string(args); - data.resize((data.size() + 3) & (~3)); - c->channel.send(data); -} - -static void server_command_send_server(shared_ptr c, const std::string& args) { - check_debug_enabled(c); - string data = parse_data_string(args); - data.resize((data.size() + 3) & (~3)); - on_command_with_header(c, data); -} - -static void proxy_command_send_client(shared_ptr ses, const std::string& args) { - string data = parse_data_string(args); - data.resize((data.size() + 3) & (~3)); - ses->client_channel.send(data); -} - -static void proxy_command_send_server(shared_ptr ses, const std::string& args) { - string data = parse_data_string(args); - data.resize((data.size() + 3) & (~3)); - ses->server_channel.send(data); -} - -static void server_command_send_both(shared_ptr c, const std::string& args) { - server_command_send_client(c, args); - server_command_send_server(c, args); -} - -static void proxy_command_send_both(shared_ptr ses, const std::string& args) { - proxy_command_send_client(ses, args); - proxy_command_send_server(ses, args); -} - -//////////////////////////////////////////////////////////////////////////////// -// Lobby commands - -static void server_command_cheat(shared_ptr c, const std::string&) { - auto s = c->require_server_state(); - auto l = c->require_lobby(); - check_is_game(l, true); - check_is_leader(l, c); - if (l->check_flag(Lobby::Flag::CANNOT_CHANGE_CHEAT_MODE)) { - send_text_message(c, "$C6Cheat mode cannot\nbe changed on this\nserver"); - } else { - l->toggle_flag(Lobby::Flag::CHEATS_ENABLED); - send_text_message_printf(l, "Cheat mode %s", l->check_flag(Lobby::Flag::CHEATS_ENABLED) ? "enabled" : "disabled"); - - if (!c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE)) { - size_t default_min_level = s->default_min_level_for_game(l->base_version, l->episode, l->difficulty); - if (l->min_level < default_min_level) { - l->min_level = default_min_level; - send_text_message_printf(l, "$C6Minimum level set\nto %" PRIu32, l->min_level + 1); - } - } - } -} - -static void server_command_lobby_event(shared_ptr c, const std::string& args) { - auto l = c->require_lobby(); - check_is_game(l, false); - check_account_flag(c, Account::Flag::CHANGE_EVENT); - - uint8_t new_event = event_for_name(args); - if (new_event == 0xFF) { - send_text_message(c, "$C6No such lobby event"); - return; - } - - l->event = new_event; - send_change_event(l, l->event); -} - -static void proxy_command_lobby_event(shared_ptr ses, const std::string& args) { - if (args.empty()) { - ses->config.override_lobby_event = 0xFF; - } else { - uint8_t new_event = event_for_name(args); - if (new_event == 0xFF) { - send_text_message(ses->client_channel, "$C6No such lobby event"); - } else { - ses->config.override_lobby_event = new_event; - if (!is_v1_or_v2(ses->version())) { - ses->client_channel.send(0xDA, ses->config.override_lobby_event); - } - } - } -} - -static void server_command_lobby_event_all(shared_ptr c, const std::string& args) { - check_account_flag(c, Account::Flag::CHANGE_EVENT); - - uint8_t new_event = event_for_name(args); - if (new_event == 0xFF) { - send_text_message(c, "$C6No such lobby event"); - return; - } - - auto s = c->require_server_state(); - for (auto l : s->all_lobbies()) { - if (l->is_game() || !l->check_flag(Lobby::Flag::DEFAULT)) { - continue; - } - - l->event = new_event; - send_change_event(l, l->event); - } -} - -static void server_command_lobby_type(shared_ptr c, const std::string& args) { - auto l = c->require_lobby(); - check_is_game(l, false); - - uint8_t new_type; - if (args.empty()) { - new_type = 0x80; - } else { - new_type = lobby_type_for_name(args); - if (new_type == 0x80) { - send_text_message(c, "$C6No such lobby type"); - return; - } - } - - c->config.override_lobby_number = new_type; - send_join_lobby(c, l); -} - -static void proxy_command_lobby_type(shared_ptr ses, const std::string& args) { - uint8_t new_type; - if (args.empty()) { - new_type = 0x80; - } else { - new_type = lobby_type_for_name(args); - if (new_type == 0x80) { - send_text_message(ses->client_channel, "$C6No such lobby type"); - return; - } - } - ses->config.override_lobby_number = new_type; -} - -static string file_path_for_recording(const std::string& args, uint32_t account_id) { - for (char ch : args) { - if (ch <= 0x20 || ch > 0x7E || ch == '/') { - throw runtime_error("invalid recording name"); - } - } - return string_printf("system/ep3/battle-records/%010" PRIu32 "_%s.mzrd", account_id, args.c_str()); -} - -static void server_command_saverec(shared_ptr c, const std::string& args) { - if (!c->ep3_prev_battle_record) { - send_text_message(c, "$C4No finished\nrecording is\npresent"); - return; - } - string file_path = file_path_for_recording(args, c->login->account->account_id); - string data = c->ep3_prev_battle_record->serialize(); - save_file(file_path, data); - send_text_message(c, "$C7Recording saved"); - c->ep3_prev_battle_record.reset(); -} - -static void server_command_playrec(shared_ptr c, const std::string& args) { - if (!is_ep3(c->version())) { - send_text_message(c, "$C4This command can\nonly be used on\nEpisode 3"); - return; - } - - auto l = c->require_lobby(); - if (l->is_game() && l->battle_player) { - l->battle_player->start(); - } else if (!l->is_game()) { - string file_path = file_path_for_recording(args, c->login->account->account_id); - - auto s = c->require_server_state(); - string filename = args; - bool start_battle_player_immediately = (filename[0] == '!'); - if (start_battle_player_immediately) { - filename = filename.substr(1); - } - - string data; - try { - data = load_file(file_path); - } catch (const cannot_open_file&) { - send_text_message(c, "$C4The recording does\nnot exist"); - return; - } - auto record = make_shared(data); - auto battle_player = make_shared(record, s->game_server->get_base()); - auto game = create_game_generic( - s, c, args, "", Episode::EP3, GameMode::NORMAL, 0, false, nullptr, battle_player); - if (game) { - if (start_battle_player_immediately) { - game->set_flag(Lobby::Flag::START_BATTLE_PLAYER_IMMEDIATELY); - } - s->change_client_lobby(c, game); - c->config.set_flag(Client::Flag::LOADING); - c->log.info("LOADING flag set"); - } - } else { - send_text_message(c, "$C4This command cannot\nbe used in a game"); - } -} - -static void server_command_meseta(shared_ptr c, const std::string& args) { - check_is_ep3(c, true); - check_debug_enabled(c); - - uint32_t amount = stoul(args, nullptr, 0); - c->login->account->ep3_current_meseta += amount; - c->login->account->ep3_total_meseta_earned += amount; - c->login->account->save(); - send_ep3_rank_update(c); - send_text_message_printf(c, "You now have\n$C6%" PRIu32 "$C7 Meseta", c->login->account->ep3_current_meseta); -} - -//////////////////////////////////////////////////////////////////////////////// -// Game commands - -static void server_command_secid(shared_ptr c, const std::string& args) { - auto l = c->require_lobby(); - check_cheats_allowed(c->require_server_state(), c); - - uint8_t new_override_section_id; - - if (!args[0]) { - new_override_section_id = 0xFF; - send_text_message(c, "$C6Override section ID\nremoved"); - } else { - new_override_section_id = section_id_for_name(args); - if (new_override_section_id == 0xFF) { - send_text_message(c, "$C6Invalid section ID"); - return; - } else { - send_text_message_printf(c, "$C6Override section ID\nset to %s", name_for_section_id(new_override_section_id)); - } - } - - c->config.override_section_id = new_override_section_id; - if (l->is_game() && (l->leader_id == c->lobby_client_id)) { - l->override_section_id = new_override_section_id; - l->change_section_id(); - } -} - -static void proxy_command_secid(shared_ptr ses, const std::string& args) { - check_cheats_allowed(ses->require_server_state(), ses); - if (!args[0]) { - ses->config.override_section_id = 0xFF; - send_text_message(ses->client_channel, "$C6Override section ID\nremoved"); - } else { - uint8_t new_secid = section_id_for_name(args); - if (new_secid == 0xFF) { - send_text_message(ses->client_channel, "$C6Invalid section ID"); - } else { - ses->config.override_section_id = new_secid; - send_text_message_printf(ses->client_channel, "$C6Override section ID\nset to %s", name_for_section_id(new_secid)); - } - } -} - -static void server_command_variations(shared_ptr c, const std::string& args) { - // Note: This command is intentionally undocumented, since it's primarily used - // for testing. If we ever make it public, we should add some kind of user - // feedback (currently it sends no message when it runs). - auto s = c->require_server_state(); - auto l = c->require_lobby(); - check_is_game(l, false); - check_cheats_allowed(s, c); - - c->override_variations = make_unique>(); - c->override_variations->clear(0); - for (size_t z = 0; z < min(c->override_variations->size(), args.size()); z++) { - c->override_variations->at(z) = args[z] - '0'; - } -} - -static void server_command_rand(shared_ptr c, const std::string& args) { - auto s = c->require_server_state(); - auto l = c->require_lobby(); - check_is_game(l, false); - check_cheats_allowed(s, c); - - if (!args[0]) { - c->config.clear_flag(Client::Flag::USE_OVERRIDE_RANDOM_SEED); - c->config.override_random_seed = 0; - send_text_message(c, "$C6Override seed\nremoved"); - } else { - c->config.set_flag(Client::Flag::USE_OVERRIDE_RANDOM_SEED); - c->config.override_random_seed = stoul(args, 0, 16); - send_text_message(c, "$C6Override seed\nset"); - } -} - -static void proxy_command_rand(shared_ptr ses, const std::string& args) { - check_cheats_allowed(ses->require_server_state(), ses); - if (!args[0]) { - ses->config.clear_flag(Client::Flag::USE_OVERRIDE_RANDOM_SEED); - ses->config.override_random_seed = 0; - send_text_message(ses->client_channel, "$C6Override seed\nremoved"); - } else { - ses->config.set_flag(Client::Flag::USE_OVERRIDE_RANDOM_SEED); - ses->config.override_random_seed = stoul(args, 0, 16); - send_text_message(ses->client_channel, "$C6Override seed\nset"); - } -} - -static void server_command_password(shared_ptr c, const std::string& args) { - auto l = c->require_lobby(); - check_is_game(l, true); - check_is_leader(l, c); - - if (!args[0]) { - l->password.clear(); - send_text_message(l, "$C6Game unlocked"); - - } else { - l->password = args; - string escaped = remove_color(l->password); - send_text_message_printf(l, "$C6Game password:\n%s", escaped.c_str()); - } -} - -static void server_command_toggle_spectator_flag(shared_ptr c, const std::string&) { - auto l = c->require_lobby(); - check_is_game(l, true); - check_is_ep3(c, true); - if (!l->is_ep3()) { - throw logic_error("Episode 3 client in non-Episode 3 game"); - } - - // In non-tournament games, only the leader can do this; in a tournament - // match, the players don't have control over who the leader is, so we allow - // all players to use this command - if (!l->tournament_match) { - check_is_leader(l, c); - } - - if (l->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM)) { - send_text_message(c, "$C6This command cannot\nbe used in a spectator\nteam"); - } - - if (l->check_flag(Lobby::Flag::SPECTATORS_FORBIDDEN)) { - l->clear_flag(Lobby::Flag::SPECTATORS_FORBIDDEN); - send_text_message(l, "$C6Spectators allowed"); - - } else { - l->set_flag(Lobby::Flag::SPECTATORS_FORBIDDEN); - for (auto watcher_l : l->watcher_lobbies) { - send_command(watcher_l, 0xED, 0x00); - } - l->watcher_lobbies.clear(); - send_text_message(l, "$C6Spectators forbidden"); - } -} - -static void server_command_min_level(shared_ptr c, const std::string& args) { - auto l = c->require_lobby(); - check_is_game(l, true); - check_is_leader(l, c); - - size_t new_min_level = stoull(args) - 1; - - auto s = c->require_server_state(); - bool cheats_allowed = (l->check_flag(Lobby::Flag::CHEATS_ENABLED) || - c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE)); - if (!cheats_allowed) { - size_t default_min_level = s->default_min_level_for_game(l->base_version, l->episode, l->difficulty); - if (new_min_level < default_min_level) { - send_text_message_printf(c, "$C6Cannot set minimum\nlevel below %zu", default_min_level + 1); - return; - } - } - - l->min_level = new_min_level; - send_text_message_printf(l, "$C6Minimum level set\nto %" PRIu32, l->min_level + 1); -} - -static void server_command_max_level(shared_ptr c, const std::string& args) { - auto l = c->require_lobby(); - check_is_game(l, true); - check_is_leader(l, c); - - l->max_level = stoull(args) - 1; - if (l->max_level >= 200) { - l->max_level = 0xFFFFFFFF; - } - - if (l->max_level == 0xFFFFFFFF) { - send_text_message(l, "$C6Maximum level set\nto unlimited"); - } else { - send_text_message_printf(l, "$C6Maximum level set\nto %" PRIu32, l->max_level + 1); - } -} - -//////////////////////////////////////////////////////////////////////////////// -// Character commands - -static void server_command_edit(shared_ptr c, const std::string& args) { - auto s = c->require_server_state(); - auto l = c->require_lobby(); - check_is_game(l, false); - if (!is_v1_or_v2(c->version()) && (c->version() != Version::BB_V4)) { - throw precondition_failed("$C6This command cannot\nbe used for your\nversion of PSO."); - } - - bool cheats_allowed = ((s->cheat_mode_behavior != ServerState::BehaviorSwitch::OFF) || - c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE)); - - string encoded_args = tolower(args); - vector tokens = split(encoded_args, ' '); - - try { - auto p = c->character(); - if (tokens.at(0) == "atp" && cheats_allowed) { - p->disp.stats.char_stats.atp = stoul(tokens.at(1)); - } else if (tokens.at(0) == "mst" && cheats_allowed) { - p->disp.stats.char_stats.mst = stoul(tokens.at(1)); - } else if (tokens.at(0) == "evp" && cheats_allowed) { - p->disp.stats.char_stats.evp = stoul(tokens.at(1)); - } else if (tokens.at(0) == "hp" && cheats_allowed) { - p->disp.stats.char_stats.hp = stoul(tokens.at(1)); - } else if (tokens.at(0) == "dfp" && cheats_allowed) { - p->disp.stats.char_stats.dfp = stoul(tokens.at(1)); - } else if (tokens.at(0) == "ata" && cheats_allowed) { - p->disp.stats.char_stats.ata = stoul(tokens.at(1)); - } else if (tokens.at(0) == "lck" && cheats_allowed) { - p->disp.stats.char_stats.lck = stoul(tokens.at(1)); - } else if (tokens.at(0) == "meseta" && cheats_allowed) { - p->disp.stats.meseta = stoul(tokens.at(1)); - } else if (tokens.at(0) == "exp" && cheats_allowed) { - p->disp.stats.experience = stoul(tokens.at(1)); - } else if (tokens.at(0) == "level" && cheats_allowed) { - uint32_t level = stoul(tokens.at(1)) - 1; - auto level_table = s->level_table(c->version()); - level_table->reset_to_base(p->disp.stats, p->disp.visual.char_class); - level_table->advance_to_level(p->disp.stats, level, p->disp.visual.char_class); - } else if (tokens.at(0) == "namecolor") { - uint32_t new_color; - sscanf(tokens.at(1).c_str(), "%8X", &new_color); - p->disp.visual.name_color = new_color; - } else if (tokens.at(0) == "language" || tokens.at(0) == "lang") { - if (tokens.at(1).size() != 1) { - throw runtime_error("invalid language"); - } - uint8_t new_language = language_code_for_char(tokens.at(1).at(0)); - c->channel.language = new_language; - p->inventory.language = new_language; - p->guild_card.language = new_language; - auto sys = c->system_file(false); - if (sys) { - sys->base.language = new_language; - } - } else if (tokens.at(0) == "secid" && cheats_allowed) { - uint8_t secid = section_id_for_name(tokens.at(1)); - if (secid == 0xFF) { - send_text_message(c, "$C6No such section ID"); - return; - } else { - p->disp.visual.section_id = secid; - } - } else if (tokens.at(0) == "name") { - vector orig_tokens = split(args, ' '); - p->disp.name.encode(orig_tokens.at(1), p->inventory.language); - } else if (tokens.at(0) == "npc") { - if (tokens.at(1) == "none") { - p->disp.visual.extra_model = 0; - p->disp.visual.validation_flags &= 0xFD; - } else { - uint8_t npc = npc_for_name(tokens.at(1)); - if (npc == 0xFF) { - send_text_message(c, "$C6No such NPC"); - return; - } - p->disp.visual.extra_model = npc; - p->disp.visual.validation_flags |= 0x02; - } - } else if (tokens.at(0) == "tech" && cheats_allowed) { - uint8_t level = stoul(tokens.at(2)) - 1; - if (tokens.at(1) == "all") { - for (size_t x = 0; x < 0x14; x++) { - p->set_technique_level(x, level); - } - } else { - uint8_t tech_id = technique_for_name(tokens.at(1)); - if (tech_id == 0xFF) { - send_text_message(c, "$C6No such technique"); - return; - } - try { - p->set_technique_level(tech_id, level); - } catch (const out_of_range&) { - send_text_message(c, "$C6Invalid technique"); - return; - } - } - } else { - send_text_message(c, "$C6Unknown field"); - return; - } - } catch (const out_of_range&) { - send_text_message(c, "$C6Not enough arguments"); - return; - } - - // Reload the client in the lobby - send_player_leave_notification(l, c->lobby_client_id); - if (c->version() == Version::BB_V4) { - send_complete_player_bb(c); - } - c->v1_v2_last_reported_disp.reset(); - s->send_lobby_join_notifications(l, c); -} - -static void server_command_change_bank(shared_ptr c, const std::string& args) { - check_version(c, Version::BB_V4); - - if (c->config.check_flag(Client::Flag::AT_BANK_COUNTER)) { - throw runtime_error("cannot change banks while at the bank counter"); - } - if (c->has_overlay()) { - throw runtime_error("cannot change banks while Battle or Challenge is in progress"); - } - - ssize_t new_char_index = args.empty() ? (c->bb_character_index + 1) : stol(args, nullptr, 0); - - if (new_char_index == 0) { - if (c->use_shared_bank()) { - send_text_message_printf(c, "$C6Using shared bank (0)"); - } else { - send_text_message_printf(c, "$C6Created shared bank (0)"); - } - } else if (new_char_index <= 4) { - c->use_character_bank(new_char_index - 1); - auto bp = c->current_bank_character(); - - auto name = escape_player_name(bp->disp.name.decode(c->language())); - send_text_message_printf(c, "$C6Using %s\'s bank (%zu)", name.c_str(), new_char_index); - } else { - throw runtime_error("invalid bank number"); - } - - const auto& bank = c->current_bank(); - send_text_message_printf(c, "%" PRIu32 " items\n%" PRIu32 " Meseta", bank.num_items.load(), bank.meseta.load()); -} - -static void server_command_bbchar_savechar(shared_ptr c, const std::string& args, bool is_bb_conversion) { - auto s = c->require_server_state(); - auto l = c->require_lobby(); - check_is_game(l, false); - - auto pending_export = make_unique(); - - if (is_bb_conversion) { - vector tokens = split(args, ' '); - if (tokens.size() != 3) { - send_text_message(c, "$C6Incorrect argument count"); - return; - } - - // username/password are tokens[0] and [1] - pending_export->character_index = stoll(tokens[2]) - 1; - if ((pending_export->character_index > 3) || (pending_export->character_index < 0)) { - send_text_message(c, "$C6Player index must\nbe in range 1-4"); - return; - } - - try { - auto dest_login = s->account_index->from_bb_credentials(tokens[0], &tokens[1], false); - pending_export->dest_account = dest_login->account; - pending_export->dest_bb_license = dest_login->bb_license; - } catch (const exception& e) { - send_text_message_printf(c, "$C6Login failed: %s", e.what()); - return; - } - - } else { - pending_export->character_index = stoll(args) - 1; - if ((pending_export->character_index > 15) || (pending_export->character_index < 0)) { - send_text_message(c, "$C6Player index must\nbe in range 1-16"); - return; - } - pending_export->dest_account = c->login->account; - } - - c->pending_character_export = std::move(pending_export); - - // Request the player data. The client will respond with a 61 or 30, and the - // handler for either of those commands will execute the conversion - send_get_player_info(c, true); -} - -static void server_command_bbchar(shared_ptr c, const std::string& args) { - server_command_bbchar_savechar(c, args, true); -} - -static void server_command_savechar(shared_ptr c, const std::string& args) { - if (c->login->account->check_flag(Account::Flag::IS_SHARED_ACCOUNT)) { - send_text_message(c, "$C7This command cannot\nbe used on a shared\naccount"); - return; - } - server_command_bbchar_savechar(c, args, false); -} - -static void server_command_loadchar(shared_ptr c, const std::string& args) { - if (!is_v1_or_v2(c->version()) && - (c->version() != Version::GC_V3) && - (c->version() != Version::GC_NTE) && - (c->version() != Version::XB_V3) && - (c->version() != Version::BB_V4)) { - send_text_message(c, "$C7This command cannot\nbe used on your\ngame version"); - return; - } - if (c->login->account->check_flag(Account::Flag::IS_SHARED_ACCOUNT)) { - send_text_message(c, "$C7This command cannot\nbe used on a shared\naccount"); - return; - } - auto l = c->require_lobby(); - check_is_game(l, false); - - size_t index = stoull(args, nullptr, 0) - 1; - if (index > 15) { - send_text_message(c, "$C6Player index must\nbe in range 1-16"); - return; - } - c->load_backup_character(c->login->account->account_id, index); - - if (c->version() == Version::BB_V4) { - // On BB, it suffices to simply send the character file again - auto s = c->require_server_state(); - send_complete_player_bb(c); - send_player_leave_notification(l, c->lobby_client_id); - s->send_lobby_join_notifications(l, c); - - } else if ((c->version() == Version::DC_V2) || - (c->version() == Version::GC_NTE) || - (c->version() == Version::GC_V3) || - (c->version() == Version::XB_V3)) { - // TODO: Support extended player info on other versions - auto s = c->require_server_state(); - if (c->config.check_flag(Client::Flag::NO_SEND_FUNCTION_CALL) || - c->config.check_flag(Client::Flag::SEND_FUNCTION_CALL_CHECKSUM_ONLY)) { - send_text_message_printf(c, "Can\'t load character\ndata on this game\nversion"); - return; - } - - auto send_set_extended_player_info = [](shared_ptr c, shared_ptr char_file) -> void { - prepare_client_for_patches(c, [wc = weak_ptr(c), char_file]() { - auto c = wc.lock(); - if (!c) { - return; - } - try { - auto s = c->require_server_state(); - auto fn = s->function_code_index->get_patch("SetExtendedPlayerInfo", c->config.specific_version); - send_function_call(c, fn, {}, char_file.get(), sizeof(CharT)); - c->function_call_response_queue.emplace_back([wc = weak_ptr(c)](uint32_t, uint32_t) -> void { - auto c = wc.lock(); - if (!c) { - return; - } - auto l = c->lobby.lock(); - if (l) { - auto s = c->require_server_state(); - send_player_leave_notification(l, c->lobby_client_id); - s->send_lobby_join_notifications(l, c); - } - }); - } catch (const exception& e) { - c->log.warning("Failed to set extended player info: %s", e.what()); - send_text_message_printf(c, "Failed to set\nplayer info:\n%s", e.what()); - } - }); - }; - - if (c->version() == Version::DC_V2) { - auto dc_char = make_shared(c->character()->to_dc_v2()); - send_set_extended_player_info.operator()(c, dc_char); - } else if (c->version() == Version::GC_NTE) { - auto gc_char = make_shared(c->character()->to_gc_nte()); - send_set_extended_player_info.operator()(c, gc_char); - } else if (c->version() == Version::GC_V3) { - auto gc_char = make_shared(c->character()->to_gc()); - send_set_extended_player_info.operator()(c, gc_char); - } else if (c->version() == Version::XB_V3) { - if (!c->login || !c->login->xb_license) { - throw runtime_error("XB client is not logged in"); - } - auto xb_char = make_shared(c->character()->to_xb(c->login->xb_license->user_id)); - send_set_extended_player_info.operator()(c, xb_char); - } else { - throw logic_error("unimplemented extended player info version"); - } - - } else { - // On v1 and v2, the client will assign its character data from the lobby - // join command, so it suffices to just resend the join notification. - auto s = c->require_server_state(); - send_player_leave_notification(l, c->lobby_client_id); - s->send_lobby_join_notifications(l, c); - } -} - -static void server_command_save(shared_ptr c, const std::string&) { - check_version(c, Version::BB_V4); - try { - c->save_all(); - send_text_message(c, "All data saved"); - } catch (const exception& e) { - send_text_message(c, "Can\'t save data:\n" + remove_color(e.what())); - } - c->reschedule_save_game_data_event(); -} - -//////////////////////////////////////////////////////////////////////////////// -// Administration commands - -static string name_for_client(shared_ptr c) { - auto player = c->character(false); - if (player.get()) { - return escape_player_name(player->disp.name.decode(player->inventory.language)); - } - - if (c->login) { - return string_printf("SN:%" PRIu32, c->login->account->account_id); - } - - return "Player"; -} - -static void server_command_silence(shared_ptr c, const std::string& args) { - auto s = c->require_server_state(); - auto l = c->require_lobby(); - check_account_flag(c, Account::Flag::SILENCE_USER); - - auto target = s->find_client(&args); - if (!target->login) { - // this should be impossible, but I'll bet it's not actually - send_text_message(c, "$C6Client not logged in"); - return; - } - - if (target->login->account->check_flag(Account::Flag::SILENCE_USER)) { - send_text_message(c, "$C6You do not have\nsufficient privileges."); - return; - } - - target->can_chat = !target->can_chat; - string target_name = name_for_client(target); - send_text_message_printf(l, "$C6%s %ssilenced", target_name.c_str(), - target->can_chat ? "un" : ""); -} - -static void server_command_kick(shared_ptr c, const std::string& args) { - auto s = c->require_server_state(); - auto l = c->require_lobby(); - check_account_flag(c, Account::Flag::KICK_USER); - - auto target = s->find_client(&args); - if (!target->login) { - // This should be impossible, but I'll bet it's not actually - send_text_message(c, "$C6Client not logged in"); - return; - } - - if (target->login->account->check_flag(Account::Flag::KICK_USER)) { - send_text_message(c, "$C6You do not have\nsufficient privileges."); - return; - } - - send_message_box(target, "$C6You have been kicked off the server."); - target->should_disconnect = true; - string target_name = name_for_client(target); - send_text_message_printf(l, "$C6%s kicked off", target_name.c_str()); -} - -static void server_command_ban(shared_ptr c, const std::string& args) { - auto s = c->require_server_state(); - auto l = c->require_lobby(); - check_account_flag(c, Account::Flag::BAN_USER); - - size_t space_pos = args.find(' '); - if (space_pos == string::npos) { - send_text_message(c, "$C6Incorrect argument count"); - return; - } - - string identifier = args.substr(space_pos + 1); - auto target = s->find_client(&identifier); - if (!target->login) { - // This should be impossible, but I'll bet it's not actually - send_text_message(c, "$C6Client not logged in"); - return; - } - - if (target->login->account->check_flag(Account::Flag::BAN_USER)) { - send_text_message(c, "$C6You do not have\nsufficient privileges."); - return; - } - - uint64_t usecs = stoull(args, nullptr, 0) * 1000000; - - size_t unit_offset = 0; - for (; isdigit(args[unit_offset]); unit_offset++) - ; - if (args[unit_offset] == 'm') { - usecs *= 60; - } else if (args[unit_offset] == 'h') { - usecs *= 60 * 60; - } else if (args[unit_offset] == 'd') { - usecs *= 60 * 60 * 24; - } else if (args[unit_offset] == 'w') { - usecs *= 60 * 60 * 24 * 7; - } else if (args[unit_offset] == 'M') { - usecs *= 60 * 60 * 24 * 30; - } else if (args[unit_offset] == 'y') { - usecs *= 60 * 60 * 24 * 365; - } - - target->login->account->ban_end_time = now() + usecs; - target->login->account->save(); - send_message_box(target, "$C6You have been banned."); - target->should_disconnect = true; - string target_name = name_for_client(target); - send_text_message_printf(l, "$C6%s banned", target_name.c_str()); -} - -//////////////////////////////////////////////////////////////////////////////// -// Cheat commands - -static void server_command_warp(shared_ptr c, const std::string& args, bool is_warpall) { - auto s = c->require_server_state(); - auto l = c->require_lobby(); - check_is_game(l, true); - - uint32_t floor = stoul(args, nullptr, 0); - if (c->floor == floor) { - return; - } - - // Special case: $warp[me] 0 is allowed in boss arenas if the boss is already - // defeated, even if cheats are disabled. This is because if a player returns - // to a boss arena after a persistence gap in the game, the exit warp won't - // exist, so they need a way to get out. - bool should_check_cheats = is_warpall || (floor != 0) || !floor_is_boss_arena(l->episode, c->floor); - if (!should_check_cheats) { - for (const auto* event : l->map->get_events(c->floor)) { - if (!(event->flags & 0x18)) { - should_check_cheats = true; - break; - } - } - } - if (should_check_cheats) { - check_cheats_enabled(l, c); - } - - size_t limit = floor_limit_for_episode(l->episode); - if (limit == 0) { - return; - } else if (floor > limit) { - send_text_message_printf(c, "$C6Area numbers must\nbe %zu or less", limit); - return; - } - - if (is_warpall) { - send_warp(l, floor, false); - } else { - send_warp(c, floor, true); - } -} - -static void server_command_warpme(shared_ptr c, const std::string& args) { - server_command_warp(c, args, false); -} - -static void server_command_warpall(shared_ptr c, const std::string& args) { - server_command_warp(c, args, true); -} - -static void proxy_command_warp(shared_ptr ses, const std::string& args, bool is_warpall) { - auto s = ses->require_server_state(); - check_cheats_allowed(s, ses); - if (!ses->is_in_game) { - send_text_message(ses->client_channel, "$C6You must be in a\ngame to use this\ncommand"); - return; - } - uint32_t floor = stoul(args, nullptr, 0); - send_warp(ses->client_channel, ses->lobby_client_id, floor, !is_warpall); - if (is_warpall) { - send_warp(ses->server_channel, ses->lobby_client_id, floor, false); - } - ses->floor = floor; -} - -static void proxy_command_warpme(shared_ptr ses, const std::string& args) { - proxy_command_warp(ses, args, false); -} - -static void proxy_command_warpall(shared_ptr ses, const std::string& args) { - proxy_command_warp(ses, args, true); -} - -static void server_command_next(shared_ptr c, const std::string&) { - auto s = c->require_server_state(); - auto l = c->require_lobby(); - check_is_game(l, true); - check_cheats_enabled(l, c); - - size_t limit = floor_limit_for_episode(l->episode); - if (limit == 0) { - return; - } - send_warp(c, (c->floor + 1) % limit, true); -} - -static void proxy_command_next(shared_ptr ses, const std::string&) { - auto s = ses->require_server_state(); - check_cheats_allowed(s, ses); - if (!ses->is_in_game) { - send_text_message(ses->client_channel, "$C6You must be in a\ngame to use this\ncommand"); - return; - } - - ses->floor++; - send_warp(ses->client_channel, ses->lobby_client_id, ses->floor, true); -} - -static void server_command_where(shared_ptr c, const std::string&) { - auto l = c->require_lobby(); - send_text_message_printf(c, "$C7%01" PRIX32 ":%s X:%" PRId32 " Z:%" PRId32, - c->floor, short_name_for_floor(l->episode, c->floor), static_cast(c->x), static_cast(c->z)); - for (auto lc : l->clients) { - if (lc && (lc != c)) { - string name = lc->character()->disp.name.decode(lc->language()); - send_text_message_printf(c, "$C6%s$C7 %01" PRIX32 ":%s", - name.c_str(), lc->floor, short_name_for_floor(l->episode, lc->floor)); - } - } -} - -static void server_command_what(shared_ptr c, const std::string&) { - auto l = c->require_lobby(); - check_is_game(l, true); - - if (!episode_has_arpg_semantics(l->episode)) { - return; - } - - float min_dist2 = 0.0f; - shared_ptr nearest_fi; - for (const auto& it : l->floor_item_managers.at(c->floor).items) { - if (!it.second->visible_to_client(c->lobby_client_id)) { - continue; - } - float dx = it.second->x - c->x; - float dz = it.second->z - c->z; - float dist2 = (dx * dx) + (dz * dz); - if (!nearest_fi || (dist2 < min_dist2)) { - nearest_fi = it.second; - min_dist2 = dist2; - } - } - - if (!nearest_fi) { - send_text_message(c, "$C4No items are near you"); - } else { - auto s = c->require_server_state(); - string name = s->describe_item(c->version(), nearest_fi->data, true); - send_text_message(c, name); - } -} - -static void server_command_song(shared_ptr c, const std::string& args) { - check_is_ep3(c, true); - - uint32_t song = stoul(args, nullptr, 0); - send_ep3_change_music(c->channel, song); -} - -static void proxy_command_song(shared_ptr ses, const std::string& args) { - int32_t song = stol(args, nullptr, 0); - if (song < 0) { - song = -song; - send_ep3_change_music(ses->server_channel, song); - } - send_ep3_change_music(ses->client_channel, song); -} - -static void command_item_notifs(Channel& ch, Client::Config& config, const std::string& args) { - if (args == "every" || args == "everything") { - config.set_drop_notification_mode(Client::ItemDropNotificationMode::ALL_ITEMS_INCLUDING_MESETA); - send_text_message_printf(ch, "$C6Notifications enabled\nfor all items and\nMeseta"); - } else if (args == "all" || args == "on") { - config.set_drop_notification_mode(Client::ItemDropNotificationMode::ALL_ITEMS); - send_text_message_printf(ch, "$C6Notifications enabled\nfor all items"); - } else if (args == "rare" || args == "rares") { - config.set_drop_notification_mode(Client::ItemDropNotificationMode::RARES_ONLY); - send_text_message_printf(ch, "$C6Notifications enabled\nfor rare items only"); - } else if (args == "none" || args == "off") { - config.set_drop_notification_mode(Client::ItemDropNotificationMode::NOTHING); - send_text_message_printf(ch, "$C6Notifications disabled\nfor all items"); - } else { - send_text_message_printf(ch, "$C6You must specify\n$C6off$C7, $C6rare$C7, $C6on$C7, or\n$C6everything$C7"); - } -} - -static void server_command_item_notifs(shared_ptr c, const std::string& args) { - command_item_notifs(c->channel, c->config, args); -} - -static void proxy_command_item_notifs(shared_ptr ses, const std::string& args) { - command_item_notifs(ses->client_channel, ses->config, args); -} - -static void server_command_infinite_hp(shared_ptr c, const std::string&) { - auto s = c->require_server_state(); - auto l = c->require_lobby(); - check_is_game(l, true); - check_cheats_enabled(l, c); - - c->config.toggle_flag(Client::Flag::INFINITE_HP_ENABLED); - bool enabled = c->config.check_flag(Client::Flag::INFINITE_HP_ENABLED); - send_text_message_printf(c, "$C6Infinite HP %s", enabled ? "enabled" : "disabled"); - if (enabled && l->is_game()) { - send_remove_conditions(c); - } -} - -static void proxy_command_infinite_hp(shared_ptr ses, const std::string&) { - auto s = ses->require_server_state(); - check_cheats_allowed(s, ses); - ses->config.toggle_flag(Client::Flag::INFINITE_HP_ENABLED); - bool enabled = ses->config.check_flag(Client::Flag::INFINITE_HP_ENABLED); - send_text_message_printf(ses->client_channel, "$C6Infinite HP %s", enabled ? "enabled" : "disabled"); - if (enabled && ses->is_in_game) { - send_remove_conditions(ses->client_channel, ses->lobby_client_id); - send_remove_conditions(ses->server_channel, ses->lobby_client_id); - } -} - -static void server_command_infinite_tp(shared_ptr c, const std::string&) { - auto s = c->require_server_state(); - auto l = c->require_lobby(); - check_is_game(l, true); - check_cheats_enabled(l, c); - - c->config.toggle_flag(Client::Flag::INFINITE_TP_ENABLED); - send_text_message_printf(c, "$C6Infinite TP %s", c->config.check_flag(Client::Flag::INFINITE_TP_ENABLED) ? "enabled" : "disabled"); -} - -static void proxy_command_infinite_tp(shared_ptr ses, const std::string&) { - auto s = ses->require_server_state(); - check_cheats_allowed(s, ses); - ses->config.toggle_flag(Client::Flag::INFINITE_TP_ENABLED); - send_text_message_printf(ses->client_channel, "$C6Infinite TP %s", - ses->config.check_flag(Client::Flag::INFINITE_TP_ENABLED) ? "enabled" : "disabled"); -} - -static void server_command_switch_assist(shared_ptr c, const std::string&) { - auto s = c->require_server_state(); - auto l = c->require_lobby(); - check_is_game(l, true); - - c->config.toggle_flag(Client::Flag::SWITCH_ASSIST_ENABLED); - send_text_message_printf(c, "$C6Switch assist %s", - c->config.check_flag(Client::Flag::SWITCH_ASSIST_ENABLED) ? "enabled" : "disabled"); -} - -static void proxy_command_switch_assist(shared_ptr ses, const std::string&) { - auto s = ses->require_server_state(); - ses->config.toggle_flag(Client::Flag::SWITCH_ASSIST_ENABLED); - send_text_message_printf(ses->client_channel, "$C6Switch assist %s", - ses->config.check_flag(Client::Flag::SWITCH_ASSIST_ENABLED) ? "enabled" : "disabled"); -} - -static void server_command_dropmode(shared_ptr c, const std::string& args) { - auto l = c->require_lobby(); - check_is_game(l, true); - if (args.empty()) { - switch (l->drop_mode) { - case Lobby::DropMode::DISABLED: - send_text_message(c, "Drop mode: disabled"); - break; - case Lobby::DropMode::CLIENT: - send_text_message(c, "Drop mode: client"); - break; - case Lobby::DropMode::SERVER_SHARED: - send_text_message(c, "Drop mode: server\nshared"); - break; - case Lobby::DropMode::SERVER_PRIVATE: - send_text_message(c, "Drop mode: server\nprivate"); - break; - case Lobby::DropMode::SERVER_DUPLICATE: - send_text_message(c, "Drop mode: server\nduplicate"); - break; - } - - } else { - check_is_leader(l, c); - Lobby::DropMode new_mode; - if ((args == "none") || (args == "disabled")) { - new_mode = Lobby::DropMode::DISABLED; - } else if (args == "client") { - new_mode = Lobby::DropMode::CLIENT; - } else if ((args == "shared") || (args == "server")) { - new_mode = Lobby::DropMode::SERVER_SHARED; - } else if ((args == "private") || (args == "priv")) { - new_mode = Lobby::DropMode::SERVER_PRIVATE; - } else if ((args == "duplicate") || (args == "dup")) { - new_mode = Lobby::DropMode::SERVER_DUPLICATE; - } else { - send_text_message(c, "Invalid drop mode"); - return; - } - - if (!(l->allowed_drop_modes & (1 << static_cast(new_mode)))) { - send_text_message(c, "Drop mode not\nallowed"); - return; - } - - l->set_drop_mode(new_mode); - switch (l->drop_mode) { - case Lobby::DropMode::DISABLED: - send_text_message(l, "Item drops disabled"); - break; - case Lobby::DropMode::CLIENT: - send_text_message(l, "Item drops changed\nto client mode"); - break; - case Lobby::DropMode::SERVER_SHARED: - send_text_message(l, "Item drops changed\nto server shared\nmode"); - break; - case Lobby::DropMode::SERVER_PRIVATE: - send_text_message(l, "Item drops changed\nto server private\nmode"); - break; - case Lobby::DropMode::SERVER_DUPLICATE: - send_text_message(l, "Item drops changed\nto server duplicate\nmode"); - break; - } - } -} - -static void proxy_command_dropmode(shared_ptr ses, const std::string& args) { - check_cheats_allowed(ses->require_server_state(), ses); - - using DropMode = ProxyServer::LinkedSession::DropMode; - if (args.empty()) { - switch (ses->drop_mode) { - case DropMode::DISABLED: - send_text_message(ses->client_channel, "Drop mode: disabled"); - break; - case DropMode::PASSTHROUGH: - send_text_message(ses->client_channel, "Drop mode: default"); - break; - case DropMode::INTERCEPT: - send_text_message(ses->client_channel, "Drop mode: proxy"); - break; - } - - } else { - DropMode new_mode; - if ((args == "none") || (args == "disabled")) { - new_mode = DropMode::DISABLED; - } else if ((args == "default") || (args == "passthrough")) { - new_mode = DropMode::PASSTHROUGH; - } else if ((args == "proxy") || (args == "intercept")) { - new_mode = DropMode::INTERCEPT; - } else { - send_text_message(ses->client_channel, "Invalid drop mode"); - return; - } - - ses->set_drop_mode(new_mode); - switch (ses->drop_mode) { - case DropMode::DISABLED: - send_text_message(ses->client_channel, "Item drops disabled"); - break; - case DropMode::PASSTHROUGH: - send_text_message(ses->client_channel, "Item drops changed\nto default mode"); - break; - case DropMode::INTERCEPT: - send_text_message(ses->client_channel, "Item drops changed\nto proxy mode"); - break; - } - } -} - -static void server_command_item(shared_ptr c, const std::string& args) { - auto s = c->require_server_state(); - auto l = c->require_lobby(); - check_is_game(l, true); - check_cheats_enabled(l, c); - - ItemData item = s->parse_item_description(c->version(), args); - item.id = l->generate_item_id(c->lobby_client_id); - - if ((l->drop_mode == Lobby::DropMode::SERVER_PRIVATE) || (l->drop_mode == Lobby::DropMode::SERVER_DUPLICATE)) { - l->add_item(c->floor, item, c->x, c->z, (1 << c->lobby_client_id)); - send_drop_stacked_item_to_channel(s, c->channel, item, c->floor, c->x, c->z); - } else { - l->add_item(c->floor, item, c->x, c->z, 0x00F); - send_drop_stacked_item_to_lobby(l, item, c->floor, c->x, c->z); - } - - string name = s->describe_item(c->version(), item, true); - send_text_message(c, "$C7Item created:\n" + name); -} - -static void proxy_command_item(shared_ptr ses, const std::string& args) { - auto s = ses->require_server_state(); - check_cheats_allowed(s, ses); - if (ses->version() == Version::BB_V4) { - send_text_message(ses->client_channel, "$C6This command cannot\nbe used on the proxy\nserver in BB games"); - return; - } - if (!ses->is_in_game) { - send_text_message(ses->client_channel, "$C6You must be in\na game to use this\ncommand"); - return; - } - if (ses->lobby_client_id != ses->leader_client_id) { - send_text_message(ses->client_channel, "$C6You must be the\nleader to use this\ncommand"); - return; - } - - bool set_drop = (!args.empty() && (args[0] == '!')); - - ItemData item = s->parse_item_description(ses->version(), (set_drop ? args.substr(1) : args)); - item.id = random_object() | 0x80000000; - - if (set_drop) { - ses->next_drop_item = item; - - string name = s->describe_item(ses->version(), item, true); - send_text_message(ses->client_channel, "$C7Next drop:\n" + name); - - } else { - send_drop_stacked_item_to_channel(s, ses->client_channel, item, ses->floor, ses->x, ses->z); - send_drop_stacked_item_to_channel(s, ses->server_channel, item, ses->floor, ses->x, ses->z); - - string name = s->describe_item(ses->version(), item, true); - send_text_message(ses->client_channel, "$C7Item created:\n" + name); - } -} - -static void server_command_enable_ep3_battle_debug_menu(shared_ptr c, const std::string& args) { - check_debug_enabled(c); - - auto l = c->require_lobby(); - check_is_game(l, true); - check_is_ep3(c, true); - if (l->episode != Episode::EP3) { - throw logic_error("non-Ep3 client in Ep3 game"); - } - if (!l->ep3_server) { - send_text_message(c, "$C6Episode 3 server\nis not initialized"); - return; - } - - if (!args.empty()) { - l->ep3_server->override_environment_number = stoul(args, nullptr, 16); - send_text_message_printf(l, "$C6Override environment\nnumber set to %02hhX", l->ep3_server->override_environment_number); - } else if (l->ep3_server->override_environment_number == 0xFF) { - l->ep3_server->override_environment_number = 0x1A; - send_text_message(l, "$C6Battle setup debug\nmenu enabled"); - } else { - l->ep3_server->override_environment_number = 0xFF; - send_text_message(l, "$C6Battle setup debug\nmenu disabled"); - } -} - -static void server_command_ep3_infinite_time(shared_ptr c, const std::string&) { - auto l = c->require_lobby(); - check_is_game(l, true); - check_is_ep3(c, true); - check_is_leader(l, c); - - if (l->episode != Episode::EP3) { - throw logic_error("non-Ep3 client in Ep3 game"); - } - if (!l->ep3_server) { - send_text_message(c, "$C6Episode 3 server\nis not initialized"); - return; - } - if (l->ep3_server->setup_phase != Episode3::SetupPhase::REGISTRATION) { - send_text_message(c, "$C6Battle is already\nin progress"); - return; - } - - l->ep3_server->options.behavior_flags ^= Episode3::BehaviorFlag::DISABLE_TIME_LIMITS; - bool infinite_time_enabled = (l->ep3_server->options.behavior_flags & Episode3::BehaviorFlag::DISABLE_TIME_LIMITS); - send_text_message(l, infinite_time_enabled ? "$C6Infinite time enabled" : "$C6Infinite time disabled"); -} - -static void server_command_ep3_set_dice_range(shared_ptr c, const std::string& args) { - auto l = c->require_lobby(); - check_is_game(l, true); - check_is_ep3(c, true); - check_is_leader(l, c); - - if (l->episode != Episode::EP3) { - throw logic_error("non-Ep3 client in Ep3 game"); - } - if (!l->ep3_server) { - send_text_message(c, "$C6Episode 3 server\nis not initialized"); - return; - } - if (l->ep3_server->setup_phase != Episode3::SetupPhase::REGISTRATION) { - send_text_message(c, "$C6Battle is already\nin progress"); - return; - } - if (l->tournament_match) { - send_text_message(c, "$C6Cannot override\ndice ranges in a\ntournament"); - return; - } - - auto parse_dice_range = +[](const string& spec) -> uint8_t { - auto tokens = split(spec, '-'); - if (tokens.size() == 1) { - uint8_t v = stoull(spec); - return (v << 4) | (v & 0x0F); - } else if (tokens.size() == 2) { - return (stoull(tokens[0]) << 4) | (stoull(tokens[1]) & 0x0F); - } else { - throw runtime_error("invalid dice spec format"); - } - }; - - uint8_t def_dice_range = 0; - uint8_t atk_dice_range_2v1 = 0; - uint8_t def_dice_range_2v1 = 0; - for (const auto& spec : split(args, ' ')) { - auto tokens = split(spec, ':'); - if (tokens.size() != 2) { - send_text_message(c, "$C6Invalid dice spec\nformat"); - return; - } - if (tokens[0] == "d") { - def_dice_range = parse_dice_range(tokens[1]); - } else if (tokens[0] == "1") { - atk_dice_range_2v1 = parse_dice_range(tokens[1]); - def_dice_range_2v1 = atk_dice_range_2v1; - } else if (tokens[0] == "a1") { - atk_dice_range_2v1 = parse_dice_range(tokens[1]); - } else if (tokens[0] == "d1") { - def_dice_range_2v1 = parse_dice_range(tokens[1]); - } - } - - l->ep3_server->def_dice_value_range_override = def_dice_range; - l->ep3_server->atk_dice_value_range_2v1_override = atk_dice_range_2v1; - l->ep3_server->def_dice_value_range_2v1_override = def_dice_range_2v1; - - if (!def_dice_range && !atk_dice_range_2v1 && !def_dice_range_2v1) { - send_text_message_printf(l, "$C7Dice ranges reset\nto defaults"); - } else { - send_text_message_printf(l, "$C7Dice ranges changed:"); - if (def_dice_range) { - send_text_message_printf(l, "$C7DEF: $C6%hhu-%hhu", - static_cast(def_dice_range >> 4), static_cast(def_dice_range & 0x0F)); - } - if (atk_dice_range_2v1) { - send_text_message_printf(l, "$C7ATK (1p in 2v1): $C6%hhu-%hhu", - static_cast(atk_dice_range_2v1 >> 4), static_cast(atk_dice_range_2v1 & 0x0F)); - } - if (def_dice_range_2v1) { - send_text_message_printf(l, "$C7DEF (1p in 2v1): $C6%hhu-%hhu", - static_cast(def_dice_range_2v1 >> 4), static_cast(def_dice_range_2v1 & 0x0F)); - } - } -} - -static void server_command_ep3_replace_assist_card(shared_ptr c, const std::string& args) { - auto s = c->require_server_state(); - auto l = c->require_lobby(); - check_is_game(l, true); - check_is_ep3(c, true); - check_cheats_enabled(l, c); - - if (l->episode != Episode::EP3) { - throw logic_error("non-Ep3 client in Ep3 game"); - } - if (!l->ep3_server) { - send_text_message(c, "$C6Episode 3 server\nis not initialized"); - return; - } - if (l->ep3_server->setup_phase != Episode3::SetupPhase::MAIN_BATTLE) { - send_text_message(c, "$C6Battle has not\nyet begun"); - return; - } - if (args.empty()) { - send_text_message(c, "$C6Missing arguments"); - return; - } - - size_t client_id; - string card_name; - if (isdigit(args[0])) { - auto tokens = split(args, ' ', 1); - client_id = stoul(tokens.at(0), nullptr, 0) - 1; - card_name = tokens.at(1); - } else { - client_id = c->lobby_client_id; - card_name = args; - } - if (client_id >= 4) { - send_text_message(c, "$C6Invalid client ID"); - return; - } - - shared_ptr ce; - try { - ce = l->ep3_server->options.card_index->definition_for_name_normalized(card_name); - } catch (const out_of_range&) { - send_text_message(c, "$C6Card not found"); - return; - } - if (ce->def.type != Episode3::CardType::ASSIST) { - send_text_message(c, "$C6Card is not an\nAssist card"); - return; - } - l->ep3_server->force_replace_assist_card(client_id, ce->def.card_id); -} - -static void server_command_ep3_unset_field_character(shared_ptr c, const std::string& args) { - auto s = c->require_server_state(); - auto l = c->require_lobby(); - check_is_game(l, true); - check_is_ep3(c, true); - check_cheats_enabled(l, c); - - if (l->episode != Episode::EP3) { - throw logic_error("non-Ep3 client in Ep3 game"); - } - if (!l->ep3_server) { - send_text_message(c, "$C6Episode 3 server\nis not initialized"); - return; - } - if (l->ep3_server->setup_phase != Episode3::SetupPhase::MAIN_BATTLE) { - send_text_message(c, "$C6Battle has not\nyet begun"); - return; - } - - size_t index = stoull(args) - 1; - l->ep3_server->force_destroy_field_character(c->lobby_client_id, index); -} - -static void server_command_surrender(shared_ptr c, const std::string&) { - auto l = c->require_lobby(); - check_is_game(l, true); - check_is_ep3(c, true); - if (l->episode != Episode::EP3) { - throw logic_error("non-Ep3 client in Ep3 game"); - } - if (!l->ep3_server) { - send_text_message(c, "$C6Episode 3 server\nis not initialized"); - return; - } - if (l->ep3_server->setup_phase != Episode3::SetupPhase::MAIN_BATTLE) { - send_text_message(c, "$C6Battle has not\nyet started"); - return; - } - string name = remove_color(c->character()->disp.name.decode(c->language())); - send_text_message_printf(l, "$C6%s has\nsurrendered", name.c_str()); - for (const auto& watcher_l : l->watcher_lobbies) { - send_text_message_printf(watcher_l, "$C6%s has\nsurrendered", name.c_str()); - } - l->ep3_server->force_battle_result(c->lobby_client_id, false); -} - -static void server_command_get_ep3_battle_stat(shared_ptr c, const std::string& args) { - auto l = c->require_lobby(); - check_is_game(l, true); - check_is_ep3(c, true); - if (l->episode != Episode::EP3) { - throw logic_error("non-Ep3 client in Ep3 game"); - } - if (!l->ep3_server) { - send_text_message(c, "$C6Episode 3 server\nis not initialized"); - return; - } - if (l->ep3_server->setup_phase != Episode3::SetupPhase::MAIN_BATTLE) { - send_text_message(c, "$C6Battle has not\nyet started"); - return; - } - auto ps = l->ep3_server->player_states.at(c->lobby_client_id); - if (!ps) { - send_text_message(c, "$C6Player is missing"); - return; - } - uint8_t team_id = ps->get_team_id(); - if (team_id > 1) { - throw logic_error("team ID is incorrect"); - } - - if (args == "rank") { - float score = ps->stats.score(l->ep3_server->get_round_num()); - uint8_t rank = ps->stats.rank_for_score(score); - const char* rank_name = ps->stats.name_for_rank(rank); - send_text_message_printf(c, "$C7Score: %g\nRank: %hhu (%s)", score, rank, rank_name); - } else if (args == "duration") { - string s = format_duration(now() - l->ep3_server->battle_start_usecs); - send_text_message_printf(c, "$C7Duration: %s", s.c_str()); - } else if (args == "fcs-destroyed") { - send_text_message_printf(c, "$C7Team FCs destroyed:\n%" PRIu32, l->ep3_server->team_num_ally_fcs_destroyed[team_id]); - } else if (args == "cards-destroyed") { - send_text_message_printf(c, "$C7Team cards destroyed:\n%" PRIu32, l->ep3_server->team_num_cards_destroyed[team_id]); - } else if (args == "damage-given") { - send_text_message_printf(c, "$C7Damage given: %hu", ps->stats.damage_given.load()); - } else if (args == "damage-taken") { - send_text_message_printf(c, "$C7Damage taken: %hu", ps->stats.damage_taken.load()); - } else if (args == "opp-cards-destroyed") { - send_text_message_printf(c, "$C7Opp. cards destroyed:\n%hu", ps->stats.num_opponent_cards_destroyed.load()); - } else if (args == "own-cards-destroyed") { - send_text_message_printf(c, "$C7Own cards destroyed:\n%hu", ps->stats.num_owned_cards_destroyed.load()); - } else if (args == "move-distance") { - send_text_message_printf(c, "$C7Move distance: %hu", ps->stats.total_move_distance.load()); - } else if (args == "cards-set") { - send_text_message_printf(c, "$C7Cards set: %hu", ps->stats.num_cards_set.load()); - } else if (args == "fcs-set") { - send_text_message_printf(c, "$C7FC cards set: %hu", ps->stats.num_item_or_creature_cards_set.load()); - } else if (args == "attack-actions-set") { - send_text_message_printf(c, "$C7Attack actions set:\n%hu", ps->stats.num_attack_actions_set.load()); - } else if (args == "techs-set") { - send_text_message_printf(c, "$C7Techs set: %hu", ps->stats.num_tech_cards_set.load()); - } else if (args == "assists-set") { - send_text_message_printf(c, "$C7Assists set: %hu", ps->stats.num_assist_cards_set.load()); - } else if (args == "defenses-self") { - send_text_message_printf(c, "$C7Defenses on self:\n%hu", ps->stats.defense_actions_set_on_self.load()); - } else if (args == "defenses-ally") { - send_text_message_printf(c, "$C7Defenses on ally:\n%hu", ps->stats.defense_actions_set_on_ally.load()); - } else if (args == "cards-drawn") { - send_text_message_printf(c, "$C7Cards drawn: %hu", ps->stats.num_cards_drawn.load()); - } else if (args == "max-attack-damage") { - send_text_message_printf(c, "$C7Maximum attack damage:\n%hu", ps->stats.max_attack_damage.load()); - } else if (args == "max-combo") { - send_text_message_printf(c, "$C7Longest combo: %hu", ps->stats.max_attack_combo_size.load()); - } else if (args == "attacks-given") { - send_text_message_printf(c, "$C7Attacks given: %hu", ps->stats.num_attacks_given.load()); - } else if (args == "attacks-taken") { - send_text_message_printf(c, "$C7Attacks taken: %hu", ps->stats.num_attacks_taken.load()); - } else if (args == "sc-damage") { - send_text_message_printf(c, "$C7SC damage taken: %hu", ps->stats.sc_damage_taken.load()); - } else if (args == "damage-defended") { - send_text_message_printf(c, "$C7Damage defended: %hu", ps->stats.action_card_negated_damage.load()); - } else { - send_text_message(c, "$C6Unknown statistic"); - } -} - -//////////////////////////////////////////////////////////////////////////////// - -typedef void (*server_handler_t)(shared_ptr c, const std::string& args); -typedef void (*proxy_handler_t)(shared_ptr ses, const std::string& args); -struct ChatCommandDefinition { - server_handler_t server_handler; - proxy_handler_t proxy_handler; -}; - -static const unordered_map chat_commands({ - {"$allevent", {server_command_lobby_event_all, nullptr}}, - {"$ann", {server_command_announce, nullptr}}, - {"$ann!", {server_command_announce_mail, nullptr}}, - {"$arrow", {server_command_arrow, proxy_command_arrow}}, - {"$auction", {server_command_auction, proxy_command_auction}}, - {"$ax", {server_command_ax, nullptr}}, - {"$ban", {server_command_ban, nullptr}}, - {"$bank", {server_command_change_bank, nullptr}}, - {"$bbchar", {server_command_bbchar, nullptr}}, - {"$cheat", {server_command_cheat, nullptr}}, - {"$debug", {server_command_debug, nullptr}}, - {"$dicerange", {server_command_ep3_set_dice_range, nullptr}}, - {"$dropmode", {server_command_dropmode, proxy_command_dropmode}}, - {"$edit", {server_command_edit, nullptr}}, - {"$ep3battledebug", {server_command_enable_ep3_battle_debug_menu, nullptr}}, - {"$event", {server_command_lobby_event, proxy_command_lobby_event}}, - {"$exit", {server_command_exit, proxy_command_exit}}, - {"$gc", {server_command_get_self_card, proxy_command_get_player_card}}, - {"$infhp", {server_command_infinite_hp, proxy_command_infinite_hp}}, - {"$inftime", {server_command_ep3_infinite_time, nullptr}}, - {"$inftp", {server_command_infinite_tp, proxy_command_infinite_tp}}, - {"$item", {server_command_item, proxy_command_item}}, - {"$itemnotifs", {server_command_item_notifs, proxy_command_item_notifs}}, - {"$i", {server_command_item, proxy_command_item}}, - {"$kick", {server_command_kick, nullptr}}, - {"$li", {server_command_lobby_info, proxy_command_lobby_info}}, - {"$ln", {server_command_lobby_type, proxy_command_lobby_type}}, - {"$loadchar", {server_command_loadchar, nullptr}}, - {"$matcount", {server_command_show_material_counts, nullptr}}, - {"$maxlevel", {server_command_max_level, nullptr}}, - {"$meseta", {server_command_meseta, nullptr}}, - {"$minlevel", {server_command_min_level, nullptr}}, - {"$next", {server_command_next, proxy_command_next}}, - {"$password", {server_command_password, nullptr}}, - {"$patch", {server_command_patch, proxy_command_patch}}, - {"$persist", {server_command_persist, nullptr}}, - {"$ping", {server_command_ping, proxy_command_ping}}, - {"$playrec", {server_command_playrec, nullptr}}, - {"$qcall", {server_command_qcall, proxy_command_qcall}}, - {"$qcheck", {server_command_qcheck, nullptr}}, - {"$qclear", {server_command_qclear, proxy_command_qclear}}, - {"$qfread", {server_command_qfread, nullptr}}, - {"$qgread", {server_command_qgread, nullptr}}, - {"$qgwrite", {server_command_qgwrite, nullptr}}, - {"$qset", {server_command_qset, proxy_command_qset}}, - {"$qsync", {server_command_qsync, proxy_command_qsync}}, - {"$qsyncall", {server_command_qsyncall, proxy_command_qsyncall}}, - {"$quest", {server_command_quest, nullptr}}, - {"$rand", {server_command_rand, proxy_command_rand}}, - {"$save", {server_command_save, nullptr}}, - {"$savechar", {server_command_savechar, nullptr}}, - {"$saverec", {server_command_saverec, nullptr}}, - {"$sb", {server_command_send_both, proxy_command_send_both}}, - {"$sc", {server_command_send_client, proxy_command_send_client}}, - {"$secid", {server_command_secid, proxy_command_secid}}, - {"$setassist", {server_command_ep3_replace_assist_card, nullptr}}, - {"$si", {server_command_server_info, nullptr}}, - {"$silence", {server_command_silence, nullptr}}, - {"$song", {server_command_song, proxy_command_song}}, - {"$spec", {server_command_toggle_spectator_flag, nullptr}}, - {"$ss", {server_command_send_server, proxy_command_send_server}}, - {"$stat", {server_command_get_ep3_battle_stat, nullptr}}, - {"$surrender", {server_command_surrender, nullptr}}, - {"$swa", {server_command_switch_assist, proxy_command_switch_assist}}, - {"$swclear", {server_command_swclear, proxy_command_swclear}}, - {"$swset", {server_command_swset, proxy_command_swset}}, - {"$swsetall", {server_command_swsetall, proxy_command_swsetall}}, - {"$unset", {server_command_ep3_unset_field_character, nullptr}}, - {"$variations", {server_command_variations, nullptr}}, - {"$warp", {server_command_warpme, proxy_command_warpme}}, - {"$warpme", {server_command_warpme, proxy_command_warpme}}, - {"$warpall", {server_command_warpall, proxy_command_warpall}}, - {"$what", {server_command_what, nullptr}}, - {"$where", {server_command_where, nullptr}}, -}); - -struct SplitCommand { - string name; - string args; - - SplitCommand(const string& text) { - size_t space_pos = text.find(' '); - if (space_pos != string::npos) { - this->name = text.substr(0, space_pos); - this->args = text.substr(space_pos + 1); - } else { - this->name = text; - } - } -}; - -// This function is called every time any player sends a chat beginning with a -// dollar sign. It is this function's responsibility to see if the chat is a -// command, and to execute the command and block the chat if it is. -void on_chat_command(std::shared_ptr c, const std::string& text) { - SplitCommand cmd(text); - if (!cmd.name.empty() && cmd.name[0] == '@') { - cmd.name[0] = '$'; - } - - const ChatCommandDefinition* def = nullptr; - try { - def = &chat_commands.at(cmd.name); - } catch (const out_of_range&) { - send_text_message(c, "$C6Unknown command"); - return; - } - - if (!def->server_handler) { - send_text_message(c, "$C6Command not available\non game server"); - } else { - try { - def->server_handler(c, cmd.args); - } catch (const precondition_failed& e) { - send_text_message(c, e.what()); - } catch (const exception& e) { - send_text_message(c, "$C6Failed:\n" + remove_color(e.what())); - } - } -} - -void on_chat_command(shared_ptr ses, const std::string& text) { - SplitCommand cmd(text); - - const ChatCommandDefinition* def = nullptr; - try { - def = &chat_commands.at(cmd.name); - } catch (const out_of_range&) { - send_text_message(ses->client_channel, "$C6Unknown command"); - return; - } - - if (!def->proxy_handler) { - send_text_message(ses->client_channel, "$C6Command not available\non proxy server"); - } else { - try { - def->proxy_handler(ses, cmd.args); - } catch (const precondition_failed& e) { - send_text_message(ses->client_channel, e.what()); - } catch (const exception& e) { - send_text_message(ses->client_channel, "$C6Failed:\n" + remove_color(e.what())); - } - } -} +#include "ChatCommands.hh" + +#include + +#include +#include +#include +#include +#include +#include + +#include "Client.hh" +#include "Lobby.hh" +#include "Loggers.hh" +#include "ProxyServer.hh" +#include "ReceiveCommands.hh" +#include "Revision.hh" +#include "SendCommands.hh" +#include "Server.hh" +#include "StaticGameData.hh" +#include "Text.hh" + +using namespace std; + +//////////////////////////////////////////////////////////////////////////////// +// Checks + +class precondition_failed { +public: + precondition_failed(const std::string& user_msg) : user_msg(user_msg) {} + ~precondition_failed() = default; + + const std::string& what() const { + return this->user_msg; + } + +private: + std::string user_msg; +}; + +static void check_account_flag(shared_ptr c, Account::Flag flag) { + if (!c->login) { + throw precondition_failed("$C6You are not\nlogged in."); + } + if (!c->login->account->check_flag(flag)) { + throw precondition_failed("$C6You do not have\npermission to\nrun this command."); + } +} + +static void check_version(shared_ptr c, Version version) { + if (c->version() != version) { + throw precondition_failed("$C6This command cannot\nbe used for your\nversion of PSO."); + } +} + +static void check_is_game(shared_ptr l, bool is_game) { + if (l->is_game() != is_game) { + throw precondition_failed(is_game ? "$C6This command cannot\nbe used in lobbies." : "$C6This command cannot\nbe used in games."); + } +} + +static void check_is_ep3(shared_ptr c, bool is_ep3) { + if (::is_ep3(c->version()) != is_ep3) { + throw precondition_failed(is_ep3 ? "$C6This command can only\nbe used in Episode 3." : "$C6This command cannot\nbe used in Episode 3."); + } +} + +static void check_debug_enabled(shared_ptr c) { + if (!c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { + throw precondition_failed("$C6This command can only\nbe run in debug mode\n(run %sdebug first)"); + } +} + +static void check_cheats_enabled(shared_ptr l, shared_ptr c) { + if (!l->check_flag(Lobby::Flag::CHEATS_ENABLED) && + !c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE)) { + throw precondition_failed("$C6This command can\nonly be used in\ncheat mode."); + } +} + +static void check_cheats_allowed(shared_ptr s, shared_ptr c) { + if ((s->cheat_mode_behavior == ServerState::BehaviorSwitch::OFF) && + !c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE)) { + throw precondition_failed("$C6Cheats are disabled\non this server."); + } +} + +static void check_cheats_allowed(shared_ptr s, shared_ptr ses) { + if ((s->cheat_mode_behavior == ServerState::BehaviorSwitch::OFF) && + (!ses->login || !ses->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE))) { + throw precondition_failed("$C6Cheats are disabled\non this proxy."); + } +} + +static void check_is_leader(shared_ptr l, shared_ptr c) { + if (l->leader_id != c->lobby_client_id) { + throw precondition_failed("$C6This command can\nonly be used by\nthe game leader."); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Message commands + +static void server_command_server_info(shared_ptr c, const std::string&) { + auto s = c->require_server_state(); + string uptime_str = format_duration(now() - s->creation_time); + send_text_message_printf(c, + "Uptime: $C6%s$C7\nLobbies: $C6%zu$C7\nClients: $C6%zu$C7(g) $C6%zu$C7(p)", + uptime_str.c_str(), + s->id_to_lobby.size(), + s->channel_to_client.size(), + s->proxy_server->num_sessions()); +} + +static void server_command_lobby_info(shared_ptr c, const std::string&) { + vector lines; + + auto l = c->lobby.lock(); + if (!l) { + lines.emplace_back("$C4No lobby info"); + + } else { + if (l->is_game()) { + if (!l->is_ep3()) { + if (l->max_level == 0xFFFFFFFF) { + lines.emplace_back(string_printf("$C6%08X$C7 L$C6%d+$C7", l->lobby_id, l->min_level + 1)); + } else { + lines.emplace_back(string_printf("$C6%08X$C7 L$C6%d-%d$C7", l->lobby_id, l->min_level + 1, l->max_level + 1)); + } + lines.emplace_back(string_printf("$C7Section ID: $C6%s$C7", name_for_section_id(l->effective_section_id()))); + + switch (l->drop_mode) { + case Lobby::DropMode::DISABLED: + lines.emplace_back("Drops disabled"); + break; + case Lobby::DropMode::CLIENT: + lines.emplace_back("Client item table"); + break; + case Lobby::DropMode::SERVER_SHARED: + lines.emplace_back("Server item table"); + break; + case Lobby::DropMode::SERVER_PRIVATE: + lines.emplace_back("Server indiv items"); + break; + case Lobby::DropMode::SERVER_DUPLICATE: + lines.emplace_back("Server dup items"); + break; + default: + lines.emplace_back("$C4Unknown drop mode$C7"); + } + if (l->check_flag(Lobby::Flag::CHEATS_ENABLED)) { + lines.emplace_back("Cheats enabled"); + } + + } else { + lines.emplace_back(string_printf("$C7State seed: $C6%08X$C7", l->random_seed)); + } + + } else { + lines.emplace_back(string_printf("$C7Lobby ID: $C6%08X$C7", l->lobby_id)); + } + + string slots_str = "Slots: "; + for (size_t z = 0; z < l->clients.size(); z++) { + if (!l->clients[z]) { + slots_str += string_printf("$C0%zX$C7", z); + } else { + bool is_self = l->clients[z] == c; + bool is_leader = z == l->leader_id; + if (is_self && is_leader) { + slots_str += string_printf("$C6%zX$C7", z); + } else if (is_self) { + slots_str += string_printf("$C2%zX$C7", z); + } else if (is_leader) { + slots_str += string_printf("$C4%zX$C7", z); + } else { + slots_str += string_printf("%zX", z); + } + } + } + lines.emplace_back(std::move(slots_str)); + } + + send_text_message(c, join(lines, "\n")); +} + +static void server_command_ping(shared_ptr c, const std::string&) { + c->ping_start_time = now(); + send_command(c, 0x1D, 0x00); +} + +static void proxy_command_ping(shared_ptr ses, const std::string&) { + ses->client_ping_start_time = now(); + ses->server_ping_start_time = now(); + + C_GuildCardSearch_40 cmd = {0x00010000, ses->remote_guild_card_number, ses->remote_guild_card_number}; + ses->client_channel.send(0x1D, 0x00); + ses->server_channel.send(0x40, 0x00, &cmd, sizeof(cmd)); +} + +static void proxy_command_lobby_info(shared_ptr ses, const std::string&) { + string msg; + // On non-masked-GC sessions (BB), there is no remote Guild Card number, so we + // don't show it. (The user can see it in the pause menu, unlike in masked-GC + // sessions like GC.) + if (ses->remote_guild_card_number >= 0) { + msg = string_printf("$C7GC: $C6%" PRId64 "$C7\n", ses->remote_guild_card_number); + } + msg += "Slots: "; + + for (size_t z = 0; z < ses->lobby_players.size(); z++) { + bool is_self = z == ses->lobby_client_id; + bool is_leader = z == ses->leader_client_id; + if (ses->lobby_players[z].guild_card_number == 0) { + msg += string_printf("$C0%zX$C7", z); + } else if (is_self && is_leader) { + msg += string_printf("$C6%zX$C7", z); + } else if (is_self) { + msg += string_printf("$C2%zX$C7", z); + } else if (is_leader) { + msg += string_printf("$C4%zX$C7", z); + } else { + msg += string_printf("%zX", z); + } + } + + vector cheats_tokens; + if (ses->config.check_flag(Client::Flag::INFINITE_HP_ENABLED)) { + cheats_tokens.emplace_back("HP"); + } + if (ses->config.check_flag(Client::Flag::INFINITE_TP_ENABLED)) { + cheats_tokens.emplace_back("TP"); + } + if (!cheats_tokens.empty()) { + msg += "\n$C7Cheats: $C6"; + msg += join(cheats_tokens, ","); + } + + vector behaviors_tokens; + if (ses->config.check_flag(Client::Flag::SWITCH_ASSIST_ENABLED)) { + behaviors_tokens.emplace_back("SWA"); + } + if (ses->config.check_flag(Client::Flag::PROXY_SAVE_FILES)) { + behaviors_tokens.emplace_back("SF"); + } + if (ses->config.check_flag(Client::Flag::PROXY_SUPPRESS_REMOTE_LOGIN)) { + behaviors_tokens.emplace_back("SL"); + } + if (ses->config.check_flag(Client::Flag::PROXY_BLOCK_FUNCTION_CALLS)) { + behaviors_tokens.emplace_back("BF"); + } + if (!behaviors_tokens.empty()) { + msg += "\n$C7Flags: $C6"; + msg += join(behaviors_tokens, ","); + } + + if (ses->config.override_section_id != 0xFF) { + msg += "\n$C7SecID*: $C6"; + msg += name_for_section_id(ses->config.override_section_id); + } + + send_text_message(ses->client_channel, msg); +} + +static void server_command_ax(shared_ptr c, const std::string& args) { + check_account_flag(c, Account::Flag::ANNOUNCE); + ax_messages_log.info("%s", args.c_str()); +} + +static void server_command_announce(shared_ptr c, const std::string& args) { + auto s = c->require_server_state(); + check_account_flag(c, Account::Flag::ANNOUNCE); + send_text_message(s, args); +} + +static void server_command_announce_mail(shared_ptr c, const std::string& args) { + auto s = c->require_server_state(); + check_account_flag(c, Account::Flag::ANNOUNCE); + send_simple_mail(s, 0, s->name, args); +} + +static void server_command_arrow(shared_ptr c, const std::string& args) { + auto l = c->require_lobby(); + c->lobby_arrow_color = stoull(args, nullptr, 0); + if (!l->is_game()) { + send_arrow_update(l); + } +} + +static void proxy_command_arrow(shared_ptr ses, const std::string& args) { + ses->server_channel.send(0x89, stoull(args, nullptr, 0)); +} + +static void server_command_debug(shared_ptr c, const std::string&) { + check_account_flag(c, Account::Flag::DEBUG); + c->config.toggle_flag(Client::Flag::DEBUG_ENABLED); + send_text_message_printf(c, "Debug %s", (c->config.check_flag(Client::Flag::DEBUG_ENABLED) ? "enabled" : "disabled")); +} + +static void server_command_quest(shared_ptr c, const std::string& args) { + auto s = c->require_server_state(); + auto l = c->require_lobby(); + if (!l->is_game()) { + throw precondition_failed("$C6Quests cannot be\nstarted from the\nlobby"); + } + + Version effective_version = is_ep3(c->version()) ? Version::GC_V3 : c->version(); + auto q = s->quest_index(effective_version)->get(stoul(args)); + if (!q) { + send_text_message(c, "$C6Quest not found"); + return; + } + + if (!c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { + if (l->count_clients() > 1) { + throw precondition_failed("$C6This command can only\nbe used with no\nother players present"); + } + if (!q->allow_start_from_chat_command) { + throw precondition_failed("$C6This quest cannot\nbe started with the\n%squest command"); + } + } + + set_lobby_quest(c->require_lobby(), q, true); +} + +static void server_command_qcheck(shared_ptr c, const std::string& args) { + auto l = c->require_lobby(); + uint16_t flag_num = stoul(args, nullptr, 0); + + if (l->is_game()) { + if (!l->quest_flags_known || l->quest_flags_known->get(l->difficulty, flag_num)) { + send_text_message_printf(c, "$C7Game: flag 0x%hX (%hu)\nis %s on %s", + flag_num, flag_num, + c->character()->quest_flags.get(l->difficulty, flag_num) ? "set" : "not set", + name_for_difficulty(l->difficulty)); + } else { + send_text_message_printf(c, "$C7Game: flag 0x%hX (%hu)\nis unknown on %s", + flag_num, flag_num, name_for_difficulty(l->difficulty)); + } + } else if (c->version() == Version::BB_V4) { + send_text_message_printf(c, "$C7Player: flag 0x%hX (%hu)\nis %s on %s", + flag_num, flag_num, + c->character()->quest_flags.get(l->difficulty, flag_num) ? "set" : "not set", + name_for_difficulty(l->difficulty)); + } +} + +static void server_command_swset_swclear(shared_ptr c, const std::string& args, bool should_set) { + check_debug_enabled(c); + auto l = c->require_lobby(); + if (!l->is_game()) { + send_text_message(c, "$C6This command cannot\nbe used in the lobby"); + return; + } + + auto tokens = split(args, ' '); + uint8_t floor, flag_num; + if (tokens.size() == 1) { + floor = c->floor; + flag_num = stoul(tokens[0], nullptr, 0); + } else if (tokens.size() == 2) { + floor = stoul(tokens[0], nullptr, 0); + flag_num = stoul(tokens[1], nullptr, 0); + } else { + send_text_message(c, "$C4Incorrect parameters"); + return; + } + + if (should_set) { + l->switch_flags->set(floor, flag_num); + } else { + l->switch_flags->clear(floor, flag_num); + } + + uint8_t cmd_flags = should_set ? 0x01 : 0x00; + G_SwitchStateChanged_6x05 cmd = {{0x05, 0x03, 0xFFFF}, 0, 0, flag_num, floor, cmd_flags}; + send_command_t(l, 0x60, 0x00, cmd); +} + +static void server_command_swset(shared_ptr c, const std::string& args) { + return server_command_swset_swclear(c, args, true); +} + +static void server_command_swclear(shared_ptr c, const std::string& args) { + return server_command_swset_swclear(c, args, false); +} + +static void proxy_command_swset_swclear(shared_ptr ses, const std::string& args, bool should_set) { + if (!ses->is_in_game) { + send_text_message(ses->client_channel, "$C6This command cannot\nbe used in the lobby"); + return; + } + + auto tokens = split(args, ' '); + uint8_t floor, flag_num; + if (tokens.size() == 1) { + floor = ses->floor; + flag_num = stoul(tokens[0], nullptr, 0); + } else if (tokens.size() == 2) { + floor = stoul(tokens[0], nullptr, 0); + flag_num = stoul(tokens[1], nullptr, 0); + } else { + send_text_message(ses->client_channel, "$C4Incorrect parameters"); + return; + } + + uint8_t cmd_flags = should_set ? 0x01 : 0x00; + G_SwitchStateChanged_6x05 cmd = {{0x05, 0x03, 0xFFFF}, 0, 0, flag_num, floor, cmd_flags}; + ses->client_channel.send(0x60, 0x00, &cmd, sizeof(cmd)); + ses->server_channel.send(0x60, 0x00, &cmd, sizeof(cmd)); +} + +static void proxy_command_swset(shared_ptr ses, const std::string& args) { + return proxy_command_swset_swclear(ses, args, true); +} + +static void proxy_command_swclear(shared_ptr ses, const std::string& args) { + return proxy_command_swset_swclear(ses, args, false); +} + +static void server_command_swsetall(shared_ptr c, const std::string&) { + check_debug_enabled(c); + auto l = c->require_lobby(); + if (!l->is_game()) { + send_text_message(c, "$C6This command cannot\nbe used in the lobby"); + return; + } + + l->switch_flags->data[c->floor].clear(0xFF); + + parray cmds; + for (size_t z = 0; z < cmds.size(); z++) { + auto& cmd = cmds[z]; + cmd.header.subcommand = 0x05; + cmd.header.size = 0x03; + cmd.header.object_id = 0xFFFF; + cmd.switch_flag_floor = c->floor; + cmd.switch_flag_num = z; + cmd.flags = 0x01; + } + cmds[0].flags = 0x03; // Play room unlock sound + send_command_t(l, 0x6C, 0x00, cmds); +} + +static void proxy_command_swsetall(shared_ptr ses, const std::string&) { + if (!ses->is_in_game) { + send_text_message(ses->client_channel, "$C6This command cannot\nbe used in the lobby"); + return; + } + + parray cmds; + for (size_t z = 0; z < cmds.size(); z++) { + auto& cmd = cmds[z]; + cmd.header.subcommand = 0x05; + cmd.header.size = 0x03; + cmd.header.object_id = 0xFFFF; + cmd.switch_flag_floor = ses->floor; + cmd.switch_flag_num = z; + cmd.flags = 0x01; + } + cmds[0].flags = 0x03; // Play room unlock sound + ses->client_channel.send(0x6C, 0x00, &cmds, sizeof(cmds)); + ses->server_channel.send(0x6C, 0x00, &cmds, sizeof(cmds)); +} + +static void server_command_qset_qclear(shared_ptr c, const std::string& args, bool should_set) { + check_debug_enabled(c); + auto l = c->require_lobby(); + if (!l->is_game()) { + send_text_message(c, "$C6This command cannot\nbe used in the lobby"); + return; + } + + uint16_t flag_num = stoul(args, nullptr, 0); + + if (l->is_game()) { + if (l->quest_flags_known) { + l->quest_flags_known->set(l->difficulty, flag_num); + } + if (should_set) { + l->quest_flag_values->set(l->difficulty, flag_num); + } else { + l->quest_flag_values->clear(l->difficulty, flag_num); + } + } + + auto p = c->character(false); + if (p) { + if (should_set) { + p->quest_flags.set(l->difficulty, flag_num); + } else { + p->quest_flags.clear(l->difficulty, flag_num); + } + } + + if (is_v1_or_v2(c->version())) { + G_UpdateQuestFlag_DC_PC_6x75 cmd = {{0x75, 0x02, 0x0000}, flag_num, should_set ? 0 : 1}; + send_command_t(l, 0x60, 0x00, cmd); + } else { + G_UpdateQuestFlag_V3_BB_6x75 cmd = {{{0x75, 0x03, 0x0000}, flag_num, should_set ? 0 : 1}, l->difficulty, 0x0000}; + send_command_t(l, 0x60, 0x00, cmd); + } +} + +static void server_command_qset(shared_ptr c, const std::string& args) { + return server_command_qset_qclear(c, args, true); +} + +static void server_command_qclear(shared_ptr c, const std::string& args) { + return server_command_qset_qclear(c, args, false); +} + +static void proxy_command_qset_qclear(shared_ptr ses, const std::string& args, bool should_set) { + if (!ses->is_in_game) { + send_text_message(ses->client_channel, "$C6This command cannot\nbe used in the lobby"); + return; + } + + uint16_t flag_num = stoul(args, nullptr, 0); + if (is_v1_or_v2(ses->version())) { + G_UpdateQuestFlag_DC_PC_6x75 cmd = {{0x75, 0x02, 0x0000}, flag_num, should_set ? 0 : 1}; + ses->client_channel.send(0x60, 0x00, &cmd, sizeof(cmd)); + ses->server_channel.send(0x60, 0x00, &cmd, sizeof(cmd)); + } else { + G_UpdateQuestFlag_V3_BB_6x75 cmd = {{{0x75, 0x03, 0x0000}, flag_num, should_set ? 0 : 1}, ses->lobby_difficulty, 0x0000}; + ses->client_channel.send(0x60, 0x00, &cmd, sizeof(cmd)); + ses->server_channel.send(0x60, 0x00, &cmd, sizeof(cmd)); + } +} + +static void proxy_command_qset(shared_ptr ses, const std::string& args) { + return proxy_command_qset_qclear(ses, args, true); +} + +static void proxy_command_qclear(shared_ptr ses, const std::string& args) { + return proxy_command_qset_qclear(ses, args, false); +} + +static void server_command_qgread(shared_ptr c, const std::string& args) { + uint8_t flag_num = stoul(args, nullptr, 0); + const auto& flags = c->character()->quest_counters; + if (flag_num >= flags.size()) { + send_text_message_printf(c, "$C7Flag number must be\nless than %zu", flags.size()); + } else { + send_text_message_printf(c, "$C7Quest counter %hhu\nhas value %" PRIu32, flag_num, flags[flag_num].load()); + } +} + +static void server_command_qfread(shared_ptr c, const std::string& args) { + auto s = c->require_server_state(); + + uint8_t counter_index; + uint32_t mask; + try { + const auto& def = s->quest_counter_fields.at(args); + counter_index = def.first; + mask = def.second; + } catch (const out_of_range&) { + send_text_message(c, "$C4Invalid field name"); + return; + } + if (mask == 0) { + throw runtime_error("invalid quest counter definition"); + } + + uint32_t counter_value = c->character()->quest_counters.at(counter_index) & mask; + + while (!(mask & 1)) { + mask >>= 1; + counter_value >>= 1; + } + + if (mask == 1) { + send_text_message_printf(c, "$C7Field %s\nhas value %s", args.c_str(), counter_value ? "TRUE" : "FALSE"); + } else { + send_text_message_printf(c, "$C7Field %s\nhas value %" PRIu32, args.c_str(), counter_value); + } +} + +static void server_command_qgwrite(shared_ptr c, const std::string& args) { + if (c->version() != Version::BB_V4) { + send_text_message(c, "$C6This command can\nonly be used on BB"); + return; + } + check_debug_enabled(c); + auto l = c->require_lobby(); + if (!l->is_game()) { + send_text_message(c, "$C6This command cannot\nbe used in the lobby"); + return; + } + + auto tokens = split(args, ' '); + if (tokens.size() != 2) { + send_text_message(c, "$C6Incorrect number\nof arguments"); + return; + } + + uint8_t flag_num = stoul(tokens[0], nullptr, 0); + uint32_t value = stoul(tokens[1], nullptr, 0); + auto& flags = c->character()->quest_counters; + if (flag_num >= flags.size()) { + send_text_message_printf(c, "$C7Flag number must be\nless than %zu", flags.size()); + } else { + c->character()->quest_counters[flag_num] = value; + G_SetQuestCounter_BB_6xD2 cmd = {{0xD2, sizeof(G_SetQuestCounter_BB_6xD2) / 4, c->lobby_client_id}, flag_num, value}; + send_command_t(c, 0x60, 0x00, cmd); + send_text_message_printf(c, "$C7Quest counter %hhu\nset to %" PRIu32, flag_num, value); + } +} + +static void server_command_qsync_qsyncall(shared_ptr c, const std::string& args, bool send_to_lobby) { + check_debug_enabled(c); + auto l = c->require_lobby(); + if (!l->is_game()) { + send_text_message(c, "$C6This command cannot\nbe used in the lobby"); + return; + } + + auto tokens = split(args, ' '); + if (tokens.size() != 2) { + send_text_message(c, "$C6Incorrect number of\narguments"); + return; + } + + G_SyncQuestRegister_6x77 cmd; + cmd.header = {0x77, 0x03, 0x0000}; + cmd.register_number = stoul(tokens[0].substr(1), nullptr, 0); + cmd.unused = 0; + if (tokens[0][0] == 'r') { + cmd.value.as_int = stoul(tokens[1], nullptr, 0); + } else if (tokens[0][0] == 'f') { + cmd.value.as_float = stof(tokens[1]); + } else { + send_text_message(c, "$C6First argument must\nbe a register"); + return; + } + if (send_to_lobby) { + send_command_t(l, 0x60, 0x00, cmd); + } else { + send_command_t(c, 0x60, 0x00, cmd); + } +} + +static void server_command_qsync(shared_ptr c, const std::string& args) { + server_command_qsync_qsyncall(c, args, false); +} + +static void server_command_qsyncall(shared_ptr c, const std::string& args) { + server_command_qsync_qsyncall(c, args, true); +} + +static void proxy_command_qsync_qsyncall(shared_ptr ses, const std::string& args, bool send_to_lobby) { + if (!ses->is_in_game) { + send_text_message(ses->client_channel, "$C6This command cannot\nbe used in the lobby"); + return; + } + + auto tokens = split(args, ' '); + if (tokens.size() != 2) { + send_text_message(ses->client_channel, "$C6Incorrect number of\narguments"); + return; + } + + G_SyncQuestRegister_6x77 cmd; + cmd.header = {0x77, 0x03, 0x0000}; + cmd.register_number = stoul(tokens[0].substr(1), nullptr, 0); + cmd.unused = 0; + if (tokens[0][0] == 'r') { + cmd.value.as_int = stoul(tokens[1], nullptr, 0); + } else if (tokens[0][0] == 'f') { + cmd.value.as_float = stof(tokens[1]); + } else { + send_text_message(ses->client_channel, "$C6First argument must\nbe a register"); + return; + } + ses->client_channel.send(0x60, 0x00, cmd); + if (send_to_lobby) { + ses->server_channel.send(0x60, 0x00, cmd); + } +} + +static void proxy_command_qsync(shared_ptr ses, const std::string& args) { + proxy_command_qsync_qsyncall(ses, args, false); +} + +static void proxy_command_qsyncall(shared_ptr ses, const std::string& args) { + proxy_command_qsync_qsyncall(ses, args, true); +} + +static void server_command_qcall(shared_ptr c, const std::string& args) { + check_debug_enabled(c); + + auto l = c->require_lobby(); + if (l->is_game() && l->quest) { + send_quest_function_call(c, stoul(args, nullptr, 0)); + } else { + send_text_message(c, "$C6You must be in a\nquest to use this\ncommand"); + } +} + +static void proxy_command_qcall(shared_ptr ses, const std::string& args) { + if (ses->is_in_game && ses->is_in_quest) { + send_quest_function_call(ses->client_channel, stoul(args, nullptr, 0)); + } else { + send_text_message(ses->client_channel, "$C6You must be in a\nquest to use this\ncommand"); + } +} + +static void server_command_show_material_counts(shared_ptr c, const std::string&) { + auto p = c->character(); + if (is_v1_or_v2(c->version())) { + send_text_message_printf(c, "%hhu HP, %hhu TP", + p->get_material_usage(PSOBBCharacterFile::MaterialType::HP), + p->get_material_usage(PSOBBCharacterFile::MaterialType::TP)); + } else { + send_text_message_printf(c, "%hhu HP, %hhu TP, %hhu POW\n%hhu MIND, %hhu EVADE\n%hhu DEF, %hhu LUCK", + p->get_material_usage(PSOBBCharacterFile::MaterialType::HP), + p->get_material_usage(PSOBBCharacterFile::MaterialType::TP), + p->get_material_usage(PSOBBCharacterFile::MaterialType::POWER), + p->get_material_usage(PSOBBCharacterFile::MaterialType::MIND), + p->get_material_usage(PSOBBCharacterFile::MaterialType::EVADE), + p->get_material_usage(PSOBBCharacterFile::MaterialType::DEF), + p->get_material_usage(PSOBBCharacterFile::MaterialType::LUCK)); + } +} + +static void server_command_auction(shared_ptr c, const std::string&) { + check_account_flag(c, Account::Flag::DEBUG); + auto l = c->require_lobby(); + if (l->is_game() && l->is_ep3()) { + G_InitiateCardAuction_Ep3_6xB5x42 cmd; + cmd.header.sender_client_id = c->lobby_client_id; + send_command_t(l, 0xC9, 0x00, cmd); + } +} + +static void proxy_command_auction(shared_ptr ses, const std::string&) { + G_InitiateCardAuction_Ep3_6xB5x42 cmd; + cmd.header.sender_client_id = ses->lobby_client_id; + ses->client_channel.send(0xC9, 0x00, &cmd, sizeof(cmd)); + ses->server_channel.send(0xC9, 0x00, &cmd, sizeof(cmd)); +} + +static void server_command_patch(shared_ptr c, const std::string& args) { + prepare_client_for_patches(c, [wc = weak_ptr(c), args]() { + auto c = wc.lock(); + if (!c) { + return; + } + try { + auto s = c->require_server_state(); + // Note: We can't look this up outside of the closure because + // c->specific_version can change during prepare_client_for_patches + auto fn = s->function_code_index->get_patch(args, c->config.specific_version); + send_function_call(c, fn); + c->function_call_response_queue.emplace_back(empty_function_call_response_handler); + } catch (const out_of_range&) { + send_text_message(c, "Invalid patch name"); + } + }); +} + +static void empty_patch_return_handler(uint32_t, uint32_t) {} + +static void proxy_command_patch(shared_ptr ses, const std::string& args) { + auto send_call = [args, ses](uint32_t specific_version, uint32_t) { + try { + if (ses->config.specific_version != specific_version) { + ses->config.specific_version = specific_version; + ses->log.info("Version detected as %08" PRIX32, ses->config.specific_version); + } + auto s = ses->require_server_state(); + auto fn = s->function_code_index->get_patch(args, ses->config.specific_version); + send_function_call(ses->client_channel, ses->config, fn); + // Don't forward the patch response to the server + ses->function_call_return_handler_queue.emplace_back(empty_patch_return_handler); + } catch (const out_of_range&) { + send_text_message(ses->client_channel, "Invalid patch name"); + } + }; + + auto send_version_detect_or_send_call = [args, ses, send_call]() { + bool is_gc = ::is_gc(ses->version()); + bool is_xb = (ses->version() == Version::XB_V3); + if ((is_gc || is_xb) && specific_version_is_indeterminate(ses->config.specific_version)) { + auto s = ses->require_server_state(); + send_function_call( + ses->client_channel, + ses->config, + s->function_code_index->name_to_function.at(is_xb ? "VersionDetectXB" : "VersionDetectGC")); + ses->function_call_return_handler_queue.emplace_back(send_call); + } else { + send_call(ses->config.specific_version, 0); + } + }; + + // This mirrors the implementation in prepare_client_for_patches + if (!ses->config.check_flag(Client::Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH)) { + auto s = ses->require_server_state(); + send_function_call( + ses->client_channel, ses->config, s->function_code_index->name_to_function.at("CacheClearFix-Phase1"), {}, "", 0, 0, 0x7F2734EC); + ses->function_call_return_handler_queue.emplace_back([s, ses, send_version_detect_or_send_call](uint32_t, uint32_t) -> void { + send_function_call( + ses->client_channel, ses->config, s->function_code_index->name_to_function.at("CacheClearFix-Phase2")); + ses->function_call_return_handler_queue.emplace_back([ses, send_version_detect_or_send_call](uint32_t, uint32_t) -> void { + ses->config.set_flag(Client::Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH); + send_version_detect_or_send_call(); + }); + }); + } else { + send_version_detect_or_send_call(); + } +} + +static void server_command_persist(shared_ptr c, const std::string&) { + auto l = c->require_lobby(); + if (l->check_flag(Lobby::Flag::DEFAULT)) { + send_text_message(c, "$C6Default lobbies\ncannot be marked\ntemporary"); + } else if (!l->is_game()) { + send_text_message(c, "$C6Private lobbies\ncannot be marked\npersistent"); + } else if (l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS) || l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)) { + send_text_message(c, "$C6Games cannot be\npersistent if a\nquest has already\nbegun"); + } else if (l->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM)) { + send_text_message(c, "$C6Spectator teams\ncannot be marked\npersistent"); + } else { + l->toggle_flag(Lobby::Flag::PERSISTENT); + send_text_message_printf(l, "Lobby persistence\n%s", l->check_flag(Lobby::Flag::PERSISTENT) ? "enabled" : "disabled"); + } +} + +static void server_command_exit(shared_ptr c, const std::string&) { + auto l = c->require_lobby(); + if (l->is_game()) { + if (l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS) || l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)) { + G_UnusedHeader cmd = {0x73, 0x01, 0x0000}; + c->channel.send(0x60, 0x00, cmd); + c->floor = 0; + c->recent_switch_flags.clear(); + } else if (is_ep3(c->version())) { + c->channel.send(0xED, 0x00); + } else { + send_text_message(c, "$C6You must return to\nthe lobby first"); + } + } else { + send_self_leave_notification(c); + if (!c->config.check_flag(Client::Flag::NO_D6)) { + send_message_box(c, ""); + } + + send_client_to_login_server(c); + } +} + +static void proxy_command_exit(shared_ptr ses, const std::string&) { + if (ses->is_in_game) { + if (is_ep3(ses->version())) { + ses->client_channel.send(0xED, 0x00); + } else if (ses->is_in_quest) { + G_UnusedHeader cmd = {0x73, 0x01, 0x0000}; + ses->client_channel.send(0x60, 0x00, cmd); + } else { + send_text_message(ses->client_channel, "$C6You must return to\nthe lobby first"); + } + } else { + ses->disconnect_action = ProxyServer::LinkedSession::DisconnectAction::CLOSE_IMMEDIATELY; + ses->send_to_game_server(); + } +} + +static void server_command_get_self_card(shared_ptr c, const std::string&) { + send_guild_card(c, c); +} + +static void proxy_command_get_player_card(shared_ptr ses, const std::string& args) { + + bool any_card_sent = false; + for (const auto& p : ses->lobby_players) { + if (!p.name.empty() && args == p.name) { + send_guild_card(ses->client_channel, p.guild_card_number, p.guild_card_number, p.name, "", "", p.language, p.section_id, p.char_class); + any_card_sent = true; + } + } + + if (!any_card_sent) { + try { + size_t index = stoull(args, nullptr, 0); + const auto& p = ses->lobby_players.at(index); + if (!p.name.empty()) { + send_guild_card(ses->client_channel, p.guild_card_number, p.guild_card_number, p.name, "", "", p.language, p.section_id, p.char_class); + } + } catch (const exception& e) { + send_text_message(ses->client_channel, "Error: " + remove_color(e.what())); + } + } +} + +static void server_command_send_client(shared_ptr c, const std::string& args) { + check_debug_enabled(c); + string data = parse_data_string(args); + data.resize((data.size() + 3) & (~3)); + c->channel.send(data); +} + +static void server_command_send_server(shared_ptr c, const std::string& args) { + check_debug_enabled(c); + string data = parse_data_string(args); + data.resize((data.size() + 3) & (~3)); + on_command_with_header(c, data); +} + +static void proxy_command_send_client(shared_ptr ses, const std::string& args) { + string data = parse_data_string(args); + data.resize((data.size() + 3) & (~3)); + ses->client_channel.send(data); +} + +static void proxy_command_send_server(shared_ptr ses, const std::string& args) { + string data = parse_data_string(args); + data.resize((data.size() + 3) & (~3)); + ses->server_channel.send(data); +} + +static void server_command_send_both(shared_ptr c, const std::string& args) { + server_command_send_client(c, args); + server_command_send_server(c, args); +} + +static void proxy_command_send_both(shared_ptr ses, const std::string& args) { + proxy_command_send_client(ses, args); + proxy_command_send_server(ses, args); +} + +//////////////////////////////////////////////////////////////////////////////// +// Lobby commands + +static void server_command_cheat(shared_ptr c, const std::string&) { + auto s = c->require_server_state(); + auto l = c->require_lobby(); + check_is_game(l, true); + check_is_leader(l, c); + if (l->check_flag(Lobby::Flag::CANNOT_CHANGE_CHEAT_MODE)) { + send_text_message(c, "$C6Cheat mode cannot\nbe changed on this\nserver"); + } else { + l->toggle_flag(Lobby::Flag::CHEATS_ENABLED); + send_text_message_printf(l, "Cheat mode %s", l->check_flag(Lobby::Flag::CHEATS_ENABLED) ? "enabled" : "disabled"); + + if (!c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE)) { + size_t default_min_level = s->default_min_level_for_game(l->base_version, l->episode, l->difficulty); + if (l->min_level < default_min_level) { + l->min_level = default_min_level; + send_text_message_printf(l, "$C6Minimum level set\nto %" PRIu32, l->min_level + 1); + } + } + } +} + +static void server_command_lobby_event(shared_ptr c, const std::string& args) { + auto l = c->require_lobby(); + check_is_game(l, false); + check_account_flag(c, Account::Flag::CHANGE_EVENT); + + uint8_t new_event = event_for_name(args); + if (new_event == 0xFF) { + send_text_message(c, "$C6No such lobby event"); + return; + } + + l->event = new_event; + send_change_event(l, l->event); +} + +static void proxy_command_lobby_event(shared_ptr ses, const std::string& args) { + if (args.empty()) { + ses->config.override_lobby_event = 0xFF; + } else { + uint8_t new_event = event_for_name(args); + if (new_event == 0xFF) { + send_text_message(ses->client_channel, "$C6No such lobby event"); + } else { + ses->config.override_lobby_event = new_event; + if (!is_v1_or_v2(ses->version())) { + ses->client_channel.send(0xDA, ses->config.override_lobby_event); + } + } + } +} + +static void server_command_lobby_event_all(shared_ptr c, const std::string& args) { + check_account_flag(c, Account::Flag::CHANGE_EVENT); + + uint8_t new_event = event_for_name(args); + if (new_event == 0xFF) { + send_text_message(c, "$C6No such lobby event"); + return; + } + + auto s = c->require_server_state(); + for (auto l : s->all_lobbies()) { + if (l->is_game() || !l->check_flag(Lobby::Flag::DEFAULT)) { + continue; + } + + l->event = new_event; + send_change_event(l, l->event); + } +} + +static void server_command_lobby_type(shared_ptr c, const std::string& args) { + auto l = c->require_lobby(); + check_is_game(l, false); + + uint8_t new_type; + if (args.empty()) { + new_type = 0x80; + } else { + new_type = lobby_type_for_name(args); + if (new_type == 0x80) { + send_text_message(c, "$C6No such lobby type"); + return; + } + } + + c->config.override_lobby_number = new_type; + send_join_lobby(c, l); +} + +static void proxy_command_lobby_type(shared_ptr ses, const std::string& args) { + uint8_t new_type; + if (args.empty()) { + new_type = 0x80; + } else { + new_type = lobby_type_for_name(args); + if (new_type == 0x80) { + send_text_message(ses->client_channel, "$C6No such lobby type"); + return; + } + } + ses->config.override_lobby_number = new_type; +} + +static string file_path_for_recording(const std::string& args, uint32_t account_id) { + for (char ch : args) { + if (ch <= 0x20 || ch > 0x7E || ch == '/') { + throw runtime_error("invalid recording name"); + } + } + return string_printf("system/ep3/battle-records/%010" PRIu32 "_%s.mzrd", account_id, args.c_str()); +} + +static void server_command_saverec(shared_ptr c, const std::string& args) { + if (!c->ep3_prev_battle_record) { + send_text_message(c, "$C4No finished\nrecording is\npresent"); + return; + } + string file_path = file_path_for_recording(args, c->login->account->account_id); + string data = c->ep3_prev_battle_record->serialize(); + save_file(file_path, data); + send_text_message(c, "$C7Recording saved"); + c->ep3_prev_battle_record.reset(); +} + +static void server_command_playrec(shared_ptr c, const std::string& args) { + if (!is_ep3(c->version())) { + send_text_message(c, "$C4This command can\nonly be used on\nEpisode 3"); + return; + } + + auto l = c->require_lobby(); + if (l->is_game() && l->battle_player) { + l->battle_player->start(); + } else if (!l->is_game()) { + string file_path = file_path_for_recording(args, c->login->account->account_id); + + auto s = c->require_server_state(); + string filename = args; + bool start_battle_player_immediately = (filename[0] == '!'); + if (start_battle_player_immediately) { + filename = filename.substr(1); + } + + string data; + try { + data = load_file(file_path); + } catch (const cannot_open_file&) { + send_text_message(c, "$C4The recording does\nnot exist"); + return; + } + auto record = make_shared(data); + auto battle_player = make_shared(record, s->game_server->get_base()); + auto game = create_game_generic( + s, c, args, "", Episode::EP3, GameMode::NORMAL, 0, false, nullptr, battle_player); + if (game) { + if (start_battle_player_immediately) { + game->set_flag(Lobby::Flag::START_BATTLE_PLAYER_IMMEDIATELY); + } + s->change_client_lobby(c, game); + c->config.set_flag(Client::Flag::LOADING); + c->log.info("LOADING flag set"); + } + } else { + send_text_message(c, "$C4This command cannot\nbe used in a game"); + } +} + +static void server_command_meseta(shared_ptr c, const std::string& args) { + check_is_ep3(c, true); + check_debug_enabled(c); + + uint32_t amount = stoul(args, nullptr, 0); + c->login->account->ep3_current_meseta += amount; + c->login->account->ep3_total_meseta_earned += amount; + c->login->account->save(); + send_ep3_rank_update(c); + send_text_message_printf(c, "You now have\n$C6%" PRIu32 "$C7 Meseta", c->login->account->ep3_current_meseta); +} + +//////////////////////////////////////////////////////////////////////////////// +// Game commands + +static void server_command_secid(shared_ptr c, const std::string& args) { + auto l = c->require_lobby(); + check_cheats_allowed(c->require_server_state(), c); + + uint8_t new_override_section_id; + + if (!args[0]) { + new_override_section_id = 0xFF; + send_text_message(c, "$C6Override section ID\nremoved"); + } else { + new_override_section_id = section_id_for_name(args); + if (new_override_section_id == 0xFF) { + send_text_message(c, "$C6Invalid section ID"); + return; + } else { + send_text_message_printf(c, "$C6Override section ID\nset to %s", name_for_section_id(new_override_section_id)); + } + } + + c->config.override_section_id = new_override_section_id; + if (l->is_game() && (l->leader_id == c->lobby_client_id)) { + l->override_section_id = new_override_section_id; + l->change_section_id(); + } +} + +static void proxy_command_secid(shared_ptr ses, const std::string& args) { + check_cheats_allowed(ses->require_server_state(), ses); + if (!args[0]) { + ses->config.override_section_id = 0xFF; + send_text_message(ses->client_channel, "$C6Override section ID\nremoved"); + } else { + uint8_t new_secid = section_id_for_name(args); + if (new_secid == 0xFF) { + send_text_message(ses->client_channel, "$C6Invalid section ID"); + } else { + ses->config.override_section_id = new_secid; + send_text_message_printf(ses->client_channel, "$C6Override section ID\nset to %s", name_for_section_id(new_secid)); + } + } +} + +static void server_command_variations(shared_ptr c, const std::string& args) { + // Note: This command is intentionally undocumented, since it's primarily used + // for testing. If we ever make it public, we should add some kind of user + // feedback (currently it sends no message when it runs). + auto s = c->require_server_state(); + auto l = c->require_lobby(); + check_is_game(l, false); + check_cheats_allowed(s, c); + + c->override_variations = make_unique>(); + c->override_variations->clear(0); + for (size_t z = 0; z < min(c->override_variations->size(), args.size()); z++) { + c->override_variations->at(z) = args[z] - '0'; + } +} + +static void server_command_rand(shared_ptr c, const std::string& args) { + auto s = c->require_server_state(); + auto l = c->require_lobby(); + check_is_game(l, false); + check_cheats_allowed(s, c); + + if (!args[0]) { + c->config.clear_flag(Client::Flag::USE_OVERRIDE_RANDOM_SEED); + c->config.override_random_seed = 0; + send_text_message(c, "$C6Override seed\nremoved"); + } else { + c->config.set_flag(Client::Flag::USE_OVERRIDE_RANDOM_SEED); + c->config.override_random_seed = stoul(args, 0, 16); + send_text_message(c, "$C6Override seed\nset"); + } +} + +static void proxy_command_rand(shared_ptr ses, const std::string& args) { + check_cheats_allowed(ses->require_server_state(), ses); + if (!args[0]) { + ses->config.clear_flag(Client::Flag::USE_OVERRIDE_RANDOM_SEED); + ses->config.override_random_seed = 0; + send_text_message(ses->client_channel, "$C6Override seed\nremoved"); + } else { + ses->config.set_flag(Client::Flag::USE_OVERRIDE_RANDOM_SEED); + ses->config.override_random_seed = stoul(args, 0, 16); + send_text_message(ses->client_channel, "$C6Override seed\nset"); + } +} + +static void server_command_password(shared_ptr c, const std::string& args) { + auto l = c->require_lobby(); + check_is_game(l, true); + check_is_leader(l, c); + + if (!args[0]) { + l->password.clear(); + send_text_message(l, "$C6Game unlocked"); + + } else { + l->password = args; + string escaped = remove_color(l->password); + send_text_message_printf(l, "$C6Game password:\n%s", escaped.c_str()); + } +} + +static void server_command_toggle_spectator_flag(shared_ptr c, const std::string&) { + auto l = c->require_lobby(); + check_is_game(l, true); + check_is_ep3(c, true); + if (!l->is_ep3()) { + throw logic_error("Episode 3 client in non-Episode 3 game"); + } + + // In non-tournament games, only the leader can do this; in a tournament + // match, the players don't have control over who the leader is, so we allow + // all players to use this command + if (!l->tournament_match) { + check_is_leader(l, c); + } + + if (l->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM)) { + send_text_message(c, "$C6This command cannot\nbe used in a spectator\nteam"); + } + + if (l->check_flag(Lobby::Flag::SPECTATORS_FORBIDDEN)) { + l->clear_flag(Lobby::Flag::SPECTATORS_FORBIDDEN); + send_text_message(l, "$C6Spectators allowed"); + + } else { + l->set_flag(Lobby::Flag::SPECTATORS_FORBIDDEN); + for (auto watcher_l : l->watcher_lobbies) { + send_command(watcher_l, 0xED, 0x00); + } + l->watcher_lobbies.clear(); + send_text_message(l, "$C6Spectators forbidden"); + } +} + +static void server_command_min_level(shared_ptr c, const std::string& args) { + auto l = c->require_lobby(); + check_is_game(l, true); + check_is_leader(l, c); + + size_t new_min_level = stoull(args) - 1; + + auto s = c->require_server_state(); + bool cheats_allowed = (l->check_flag(Lobby::Flag::CHEATS_ENABLED) || + c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE)); + if (!cheats_allowed) { + size_t default_min_level = s->default_min_level_for_game(l->base_version, l->episode, l->difficulty); + if (new_min_level < default_min_level) { + send_text_message_printf(c, "$C6Cannot set minimum\nlevel below %zu", default_min_level + 1); + return; + } + } + + l->min_level = new_min_level; + send_text_message_printf(l, "$C6Minimum level set\nto %" PRIu32, l->min_level + 1); +} + +static void server_command_max_level(shared_ptr c, const std::string& args) { + auto l = c->require_lobby(); + check_is_game(l, true); + check_is_leader(l, c); + + l->max_level = stoull(args) - 1; + if (l->max_level >= 200) { + l->max_level = 0xFFFFFFFF; + } + + if (l->max_level == 0xFFFFFFFF) { + send_text_message(l, "$C6Maximum level set\nto unlimited"); + } else { + send_text_message_printf(l, "$C6Maximum level set\nto %" PRIu32, l->max_level + 1); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Character commands + +static void server_command_edit(shared_ptr c, const std::string& args) { + auto s = c->require_server_state(); + auto l = c->require_lobby(); + check_is_game(l, false); + if (!is_v1_or_v2(c->version()) && (c->version() != Version::BB_V4)) { + throw precondition_failed("$C6This command cannot\nbe used for your\nversion of PSO."); + } + + bool cheats_allowed = ((s->cheat_mode_behavior != ServerState::BehaviorSwitch::OFF) || + c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE)); + + string encoded_args = tolower(args); + vector tokens = split(encoded_args, ' '); + + try { + auto p = c->character(); + if (tokens.at(0) == "atp" && cheats_allowed) { + p->disp.stats.char_stats.atp = stoul(tokens.at(1)); + } else if (tokens.at(0) == "mst" && cheats_allowed) { + p->disp.stats.char_stats.mst = stoul(tokens.at(1)); + } else if (tokens.at(0) == "evp" && cheats_allowed) { + p->disp.stats.char_stats.evp = stoul(tokens.at(1)); + } else if (tokens.at(0) == "hp" && cheats_allowed) { + p->disp.stats.char_stats.hp = stoul(tokens.at(1)); + } else if (tokens.at(0) == "dfp" && cheats_allowed) { + p->disp.stats.char_stats.dfp = stoul(tokens.at(1)); + } else if (tokens.at(0) == "ata" && cheats_allowed) { + p->disp.stats.char_stats.ata = stoul(tokens.at(1)); + } else if (tokens.at(0) == "lck" && cheats_allowed) { + p->disp.stats.char_stats.lck = stoul(tokens.at(1)); + } else if (tokens.at(0) == "meseta" && cheats_allowed) { + p->disp.stats.meseta = stoul(tokens.at(1)); + } else if (tokens.at(0) == "exp" && cheats_allowed) { + p->disp.stats.experience = stoul(tokens.at(1)); + } else if (tokens.at(0) == "level" && cheats_allowed) { + uint32_t level = stoul(tokens.at(1)) - 1; + auto level_table = s->level_table(c->version()); + level_table->reset_to_base(p->disp.stats, p->disp.visual.char_class); + level_table->advance_to_level(p->disp.stats, level, p->disp.visual.char_class); + } else if (tokens.at(0) == "namecolor") { + uint32_t new_color; + sscanf(tokens.at(1).c_str(), "%8X", &new_color); + p->disp.visual.name_color = new_color; + } else if (tokens.at(0) == "language" || tokens.at(0) == "lang") { + if (tokens.at(1).size() != 1) { + throw runtime_error("invalid language"); + } + uint8_t new_language = language_code_for_char(tokens.at(1).at(0)); + c->channel.language = new_language; + p->inventory.language = new_language; + p->guild_card.language = new_language; + auto sys = c->system_file(false); + if (sys) { + sys->base.language = new_language; + } + } else if (tokens.at(0) == "secid" && cheats_allowed) { + uint8_t secid = section_id_for_name(tokens.at(1)); + if (secid == 0xFF) { + send_text_message(c, "$C6No such section ID"); + return; + } else { + p->disp.visual.section_id = secid; + } + } else if (tokens.at(0) == "name") { + vector orig_tokens = split(args, ' '); + p->disp.name.encode(orig_tokens.at(1), p->inventory.language); + } else if (tokens.at(0) == "npc") { + if (tokens.at(1) == "none") { + p->disp.visual.extra_model = 0; + p->disp.visual.validation_flags &= 0xFD; + } else { + uint8_t npc = npc_for_name(tokens.at(1)); + if (npc == 0xFF) { + send_text_message(c, "$C6No such NPC"); + return; + } + p->disp.visual.extra_model = npc; + p->disp.visual.validation_flags |= 0x02; + } + } else if (tokens.at(0) == "tech" && cheats_allowed) { + uint8_t level = stoul(tokens.at(2)) - 1; + if (tokens.at(1) == "all") { + for (size_t x = 0; x < 0x14; x++) { + p->set_technique_level(x, level); + } + } else { + uint8_t tech_id = technique_for_name(tokens.at(1)); + if (tech_id == 0xFF) { + send_text_message(c, "$C6No such technique"); + return; + } + try { + p->set_technique_level(tech_id, level); + } catch (const out_of_range&) { + send_text_message(c, "$C6Invalid technique"); + return; + } + } + } else { + send_text_message(c, "$C6Unknown field"); + return; + } + } catch (const out_of_range&) { + send_text_message(c, "$C6Not enough arguments"); + return; + } + + // Reload the client in the lobby + send_player_leave_notification(l, c->lobby_client_id); + if (c->version() == Version::BB_V4) { + send_complete_player_bb(c); + } + c->v1_v2_last_reported_disp.reset(); + s->send_lobby_join_notifications(l, c); +} + +static void server_command_change_bank(shared_ptr c, const std::string& args) { + check_version(c, Version::BB_V4); + + if (c->config.check_flag(Client::Flag::AT_BANK_COUNTER)) { + throw runtime_error("cannot change banks while at the bank counter"); + } + if (c->has_overlay()) { + throw runtime_error("cannot change banks while Battle or Challenge is in progress"); + } + + ssize_t new_char_index = args.empty() ? (c->bb_character_index + 1) : stol(args, nullptr, 0); + + if (new_char_index == 0) { + if (c->use_shared_bank()) { + send_text_message_printf(c, "$C6Using shared bank (0)"); + } else { + send_text_message_printf(c, "$C6Created shared bank (0)"); + } + } else if (new_char_index <= 4) { + c->use_character_bank(new_char_index - 1); + auto bp = c->current_bank_character(); + + auto name = escape_player_name(bp->disp.name.decode(c->language())); + send_text_message_printf(c, "$C6Using %s\'s bank (%zu)", name.c_str(), new_char_index); + } else { + throw runtime_error("invalid bank number"); + } + + const auto& bank = c->current_bank(); + send_text_message_printf(c, "%" PRIu32 " items\n%" PRIu32 " Meseta", bank.num_items.load(), bank.meseta.load()); +} + +static void server_command_bbchar_savechar(shared_ptr c, const std::string& args, bool is_bb_conversion) { + auto s = c->require_server_state(); + auto l = c->require_lobby(); + check_is_game(l, false); + + auto pending_export = make_unique(); + + if (is_bb_conversion) { + vector tokens = split(args, ' '); + if (tokens.size() != 3) { + send_text_message(c, "$C6Incorrect argument count"); + return; + } + + // username/password are tokens[0] and [1] + pending_export->character_index = stoll(tokens[2]) - 1; + if ((pending_export->character_index > 3) || (pending_export->character_index < 0)) { + send_text_message(c, "$C6Player index must\nbe in range 1-4"); + return; + } + + try { + auto dest_login = s->account_index->from_bb_credentials(tokens[0], &tokens[1], false); + pending_export->dest_account = dest_login->account; + pending_export->dest_bb_license = dest_login->bb_license; + } catch (const exception& e) { + send_text_message_printf(c, "$C6Login failed: %s", e.what()); + return; + } + + } else { + pending_export->character_index = stoll(args) - 1; + if ((pending_export->character_index > 15) || (pending_export->character_index < 0)) { + send_text_message(c, "$C6Player index must\nbe in range 1-16"); + return; + } + pending_export->dest_account = c->login->account; + } + + c->pending_character_export = std::move(pending_export); + + // Request the player data. The client will respond with a 61 or 30, and the + // handler for either of those commands will execute the conversion + send_get_player_info(c, true); +} + +static void server_command_bbchar(shared_ptr c, const std::string& args) { + server_command_bbchar_savechar(c, args, true); +} + +static void server_command_savechar(shared_ptr c, const std::string& args) { + if (c->login->account->check_flag(Account::Flag::IS_SHARED_ACCOUNT)) { + send_text_message(c, "$C7This command cannot\nbe used on a shared\naccount"); + return; + } + server_command_bbchar_savechar(c, args, false); +} + +static void server_command_loadchar(shared_ptr c, const std::string& args) { + if (!is_v1_or_v2(c->version()) && + (c->version() != Version::GC_V3) && + (c->version() != Version::GC_NTE) && + (c->version() != Version::XB_V3) && + (c->version() != Version::BB_V4)) { + send_text_message(c, "$C7This command cannot\nbe used on your\ngame version"); + return; + } + if (c->login->account->check_flag(Account::Flag::IS_SHARED_ACCOUNT)) { + send_text_message(c, "$C7This command cannot\nbe used on a shared\naccount"); + return; + } + auto l = c->require_lobby(); + check_is_game(l, false); + + size_t index = stoull(args, nullptr, 0) - 1; + if (index > 15) { + send_text_message(c, "$C6Player index must\nbe in range 1-16"); + return; + } + c->load_backup_character(c->login->account->account_id, index); + + if (c->version() == Version::BB_V4) { + // On BB, it suffices to simply send the character file again + auto s = c->require_server_state(); + send_complete_player_bb(c); + send_player_leave_notification(l, c->lobby_client_id); + s->send_lobby_join_notifications(l, c); + + } else if ((c->version() == Version::DC_V2) || + (c->version() == Version::GC_NTE) || + (c->version() == Version::GC_V3) || + (c->version() == Version::XB_V3)) { + // TODO: Support extended player info on other versions + auto s = c->require_server_state(); + if (c->config.check_flag(Client::Flag::NO_SEND_FUNCTION_CALL) || + c->config.check_flag(Client::Flag::SEND_FUNCTION_CALL_CHECKSUM_ONLY)) { + send_text_message_printf(c, "Can\'t load character\ndata on this game\nversion"); + return; + } + + auto send_set_extended_player_info = [](shared_ptr c, shared_ptr char_file) -> void { + prepare_client_for_patches(c, [wc = weak_ptr(c), char_file]() { + auto c = wc.lock(); + if (!c) { + return; + } + try { + auto s = c->require_server_state(); + auto fn = s->function_code_index->get_patch("SetExtendedPlayerInfo", c->config.specific_version); + send_function_call(c, fn, {}, char_file.get(), sizeof(CharT)); + c->function_call_response_queue.emplace_back([wc = weak_ptr(c)](uint32_t, uint32_t) -> void { + auto c = wc.lock(); + if (!c) { + return; + } + auto l = c->lobby.lock(); + if (l) { + auto s = c->require_server_state(); + send_player_leave_notification(l, c->lobby_client_id); + s->send_lobby_join_notifications(l, c); + } + }); + } catch (const exception& e) { + c->log.warning("Failed to set extended player info: %s", e.what()); + send_text_message_printf(c, "Failed to set\nplayer info:\n%s", e.what()); + } + }); + }; + + if (c->version() == Version::DC_V2) { + auto dc_char = make_shared(c->character()->to_dc_v2()); + send_set_extended_player_info.operator()(c, dc_char); + } else if (c->version() == Version::GC_NTE) { + auto gc_char = make_shared(c->character()->to_gc_nte()); + send_set_extended_player_info.operator()(c, gc_char); + } else if (c->version() == Version::GC_V3) { + auto gc_char = make_shared(c->character()->to_gc()); + send_set_extended_player_info.operator()(c, gc_char); + } else if (c->version() == Version::XB_V3) { + if (!c->login || !c->login->xb_license) { + throw runtime_error("XB client is not logged in"); + } + auto xb_char = make_shared(c->character()->to_xb(c->login->xb_license->user_id)); + send_set_extended_player_info.operator()(c, xb_char); + } else { + throw logic_error("unimplemented extended player info version"); + } + + } else { + // On v1 and v2, the client will assign its character data from the lobby + // join command, so it suffices to just resend the join notification. + auto s = c->require_server_state(); + send_player_leave_notification(l, c->lobby_client_id); + s->send_lobby_join_notifications(l, c); + } +} + +static void server_command_save(shared_ptr c, const std::string&) { + check_version(c, Version::BB_V4); + try { + c->save_all(); + send_text_message(c, "All data saved"); + } catch (const exception& e) { + send_text_message(c, "Can\'t save data:\n" + remove_color(e.what())); + } + c->reschedule_save_game_data_event(); +} + +//////////////////////////////////////////////////////////////////////////////// +// Administration commands + +static string name_for_client(shared_ptr c) { + auto player = c->character(false); + if (player.get()) { + return escape_player_name(player->disp.name.decode(player->inventory.language)); + } + + if (c->login) { + return string_printf("SN:%" PRIu32, c->login->account->account_id); + } + + return "Player"; +} + +static void server_command_silence(shared_ptr c, const std::string& args) { + auto s = c->require_server_state(); + auto l = c->require_lobby(); + check_account_flag(c, Account::Flag::SILENCE_USER); + + auto target = s->find_client(&args); + if (!target->login) { + // this should be impossible, but I'll bet it's not actually + send_text_message(c, "$C6Client not logged in"); + return; + } + + if (target->login->account->check_flag(Account::Flag::SILENCE_USER)) { + send_text_message(c, "$C6You do not have\nsufficient privileges."); + return; + } + + target->can_chat = !target->can_chat; + string target_name = name_for_client(target); + send_text_message_printf(l, "$C6%s %ssilenced", target_name.c_str(), + target->can_chat ? "un" : ""); +} + +static void server_command_kick(shared_ptr c, const std::string& args) { + auto s = c->require_server_state(); + auto l = c->require_lobby(); + check_account_flag(c, Account::Flag::KICK_USER); + + auto target = s->find_client(&args); + if (!target->login) { + // This should be impossible, but I'll bet it's not actually + send_text_message(c, "$C6Client not logged in"); + return; + } + + if (target->login->account->check_flag(Account::Flag::KICK_USER)) { + send_text_message(c, "$C6You do not have\nsufficient privileges."); + return; + } + + send_message_box(target, "$C6You have been kicked off the server."); + target->should_disconnect = true; + string target_name = name_for_client(target); + send_text_message_printf(l, "$C6%s kicked off", target_name.c_str()); +} + +static void server_command_ban(shared_ptr c, const std::string& args) { + auto s = c->require_server_state(); + auto l = c->require_lobby(); + check_account_flag(c, Account::Flag::BAN_USER); + + size_t space_pos = args.find(' '); + if (space_pos == string::npos) { + send_text_message(c, "$C6Incorrect argument count"); + return; + } + + string identifier = args.substr(space_pos + 1); + auto target = s->find_client(&identifier); + if (!target->login) { + // This should be impossible, but I'll bet it's not actually + send_text_message(c, "$C6Client not logged in"); + return; + } + + if (target->login->account->check_flag(Account::Flag::BAN_USER)) { + send_text_message(c, "$C6You do not have\nsufficient privileges."); + return; + } + + uint64_t usecs = stoull(args, nullptr, 0) * 1000000; + + size_t unit_offset = 0; + for (; isdigit(args[unit_offset]); unit_offset++) + ; + if (args[unit_offset] == 'm') { + usecs *= 60; + } else if (args[unit_offset] == 'h') { + usecs *= 60 * 60; + } else if (args[unit_offset] == 'd') { + usecs *= 60 * 60 * 24; + } else if (args[unit_offset] == 'w') { + usecs *= 60 * 60 * 24 * 7; + } else if (args[unit_offset] == 'M') { + usecs *= 60 * 60 * 24 * 30; + } else if (args[unit_offset] == 'y') { + usecs *= 60 * 60 * 24 * 365; + } + + target->login->account->ban_end_time = now() + usecs; + target->login->account->save(); + send_message_box(target, "$C6You have been banned."); + target->should_disconnect = true; + string target_name = name_for_client(target); + send_text_message_printf(l, "$C6%s banned", target_name.c_str()); +} + +//////////////////////////////////////////////////////////////////////////////// +// Cheat commands + +static void server_command_warp(shared_ptr c, const std::string& args, bool is_warpall) { + auto s = c->require_server_state(); + auto l = c->require_lobby(); + check_is_game(l, true); + + uint32_t floor = stoul(args, nullptr, 0); + if (c->floor == floor) { + return; + } + + // Special case: $warp[me] 0 is allowed in boss arenas if the boss is already + // defeated, even if cheats are disabled. This is because if a player returns + // to a boss arena after a persistence gap in the game, the exit warp won't + // exist, so they need a way to get out. + bool should_check_cheats = is_warpall || (floor != 0) || !floor_is_boss_arena(l->episode, c->floor); + if (!should_check_cheats) { + for (const auto* event : l->map->get_events(c->floor)) { + if (!(event->flags & 0x18)) { + should_check_cheats = true; + break; + } + } + } + if (should_check_cheats) { + check_cheats_enabled(l, c); + } + + size_t limit = floor_limit_for_episode(l->episode); + if (limit == 0) { + return; + } else if (floor > limit) { + send_text_message_printf(c, "$C6Area numbers must\nbe %zu or less", limit); + return; + } + + if (is_warpall) { + send_warp(l, floor, false); + } else { + send_warp(c, floor, true); + } +} + +static void server_command_warpme(shared_ptr c, const std::string& args) { + server_command_warp(c, args, false); +} + +static void server_command_warpall(shared_ptr c, const std::string& args) { + server_command_warp(c, args, true); +} + +static void proxy_command_warp(shared_ptr ses, const std::string& args, bool is_warpall) { + auto s = ses->require_server_state(); + check_cheats_allowed(s, ses); + if (!ses->is_in_game) { + send_text_message(ses->client_channel, "$C6You must be in a\ngame to use this\ncommand"); + return; + } + uint32_t floor = stoul(args, nullptr, 0); + send_warp(ses->client_channel, ses->lobby_client_id, floor, !is_warpall); + if (is_warpall) { + send_warp(ses->server_channel, ses->lobby_client_id, floor, false); + } + ses->floor = floor; +} + +static void proxy_command_warpme(shared_ptr ses, const std::string& args) { + proxy_command_warp(ses, args, false); +} + +static void proxy_command_warpall(shared_ptr ses, const std::string& args) { + proxy_command_warp(ses, args, true); +} + +static void server_command_next(shared_ptr c, const std::string&) { + auto s = c->require_server_state(); + auto l = c->require_lobby(); + check_is_game(l, true); + check_cheats_enabled(l, c); + + size_t limit = floor_limit_for_episode(l->episode); + if (limit == 0) { + return; + } + send_warp(c, (c->floor + 1) % limit, true); +} + +static void proxy_command_next(shared_ptr ses, const std::string&) { + auto s = ses->require_server_state(); + check_cheats_allowed(s, ses); + if (!ses->is_in_game) { + send_text_message(ses->client_channel, "$C6You must be in a\ngame to use this\ncommand"); + return; + } + + ses->floor++; + send_warp(ses->client_channel, ses->lobby_client_id, ses->floor, true); +} + +static void server_command_where(shared_ptr c, const std::string&) { + auto l = c->require_lobby(); + send_text_message_printf(c, "$C7%01" PRIX32 ":%s X:%" PRId32 " Z:%" PRId32, + c->floor, short_name_for_floor(l->episode, c->floor), static_cast(c->x), static_cast(c->z)); + for (auto lc : l->clients) { + if (lc && (lc != c)) { + string name = lc->character()->disp.name.decode(lc->language()); + send_text_message_printf(c, "$C6%s$C7 %01" PRIX32 ":%s", + name.c_str(), lc->floor, short_name_for_floor(l->episode, lc->floor)); + } + } +} + +static void server_command_what(shared_ptr c, const std::string&) { + auto l = c->require_lobby(); + check_is_game(l, true); + + if (!episode_has_arpg_semantics(l->episode)) { + return; + } + + float min_dist2 = 0.0f; + shared_ptr nearest_fi; + for (const auto& it : l->floor_item_managers.at(c->floor).items) { + if (!it.second->visible_to_client(c->lobby_client_id)) { + continue; + } + float dx = it.second->x - c->x; + float dz = it.second->z - c->z; + float dist2 = (dx * dx) + (dz * dz); + if (!nearest_fi || (dist2 < min_dist2)) { + nearest_fi = it.second; + min_dist2 = dist2; + } + } + + if (!nearest_fi) { + send_text_message(c, "$C4No items are near you"); + } else { + auto s = c->require_server_state(); + string name = s->describe_item(c->version(), nearest_fi->data, true); + send_text_message(c, name); + } +} + +static void server_command_song(shared_ptr c, const std::string& args) { + check_is_ep3(c, true); + + uint32_t song = stoul(args, nullptr, 0); + send_ep3_change_music(c->channel, song); +} + +static void proxy_command_song(shared_ptr ses, const std::string& args) { + int32_t song = stol(args, nullptr, 0); + if (song < 0) { + song = -song; + send_ep3_change_music(ses->server_channel, song); + } + send_ep3_change_music(ses->client_channel, song); +} + +static void command_item_notifs(Channel& ch, Client::Config& config, const std::string& args) { + if (args == "every" || args == "everything") { + config.set_drop_notification_mode(Client::ItemDropNotificationMode::ALL_ITEMS_INCLUDING_MESETA); + send_text_message_printf(ch, "$C6Notifications enabled\nfor all items and\nMeseta"); + } else if (args == "all" || args == "on") { + config.set_drop_notification_mode(Client::ItemDropNotificationMode::ALL_ITEMS); + send_text_message_printf(ch, "$C6Notifications enabled\nfor all items"); + } else if (args == "rare" || args == "rares") { + config.set_drop_notification_mode(Client::ItemDropNotificationMode::RARES_ONLY); + send_text_message_printf(ch, "$C6Notifications enabled\nfor rare items only"); + } else if (args == "none" || args == "off") { + config.set_drop_notification_mode(Client::ItemDropNotificationMode::NOTHING); + send_text_message_printf(ch, "$C6Notifications disabled\nfor all items"); + } else { + send_text_message_printf(ch, "$C6You must specify\n$C6off$C7, $C6rare$C7, $C6on$C7, or\n$C6everything$C7"); + } +} + +static void server_command_item_notifs(shared_ptr c, const std::string& args) { + command_item_notifs(c->channel, c->config, args); +} + +static void proxy_command_item_notifs(shared_ptr ses, const std::string& args) { + command_item_notifs(ses->client_channel, ses->config, args); +} + +static void server_command_infinite_hp(shared_ptr c, const std::string&) { + auto s = c->require_server_state(); + auto l = c->require_lobby(); + check_is_game(l, true); + check_cheats_enabled(l, c); + + c->config.toggle_flag(Client::Flag::INFINITE_HP_ENABLED); + bool enabled = c->config.check_flag(Client::Flag::INFINITE_HP_ENABLED); + send_text_message_printf(c, "$C6Infinite HP %s", enabled ? "enabled" : "disabled"); + if (enabled && l->is_game()) { + send_remove_conditions(c); + } +} + +static void proxy_command_infinite_hp(shared_ptr ses, const std::string&) { + auto s = ses->require_server_state(); + check_cheats_allowed(s, ses); + ses->config.toggle_flag(Client::Flag::INFINITE_HP_ENABLED); + bool enabled = ses->config.check_flag(Client::Flag::INFINITE_HP_ENABLED); + send_text_message_printf(ses->client_channel, "$C6Infinite HP %s", enabled ? "enabled" : "disabled"); + if (enabled && ses->is_in_game) { + send_remove_conditions(ses->client_channel, ses->lobby_client_id); + send_remove_conditions(ses->server_channel, ses->lobby_client_id); + } +} + +static void server_command_infinite_tp(shared_ptr c, const std::string&) { + auto s = c->require_server_state(); + auto l = c->require_lobby(); + check_is_game(l, true); + check_cheats_enabled(l, c); + + c->config.toggle_flag(Client::Flag::INFINITE_TP_ENABLED); + send_text_message_printf(c, "$C6Infinite TP %s", c->config.check_flag(Client::Flag::INFINITE_TP_ENABLED) ? "enabled" : "disabled"); +} + +static void proxy_command_infinite_tp(shared_ptr ses, const std::string&) { + auto s = ses->require_server_state(); + check_cheats_allowed(s, ses); + ses->config.toggle_flag(Client::Flag::INFINITE_TP_ENABLED); + send_text_message_printf(ses->client_channel, "$C6Infinite TP %s", + ses->config.check_flag(Client::Flag::INFINITE_TP_ENABLED) ? "enabled" : "disabled"); +} + +static void server_command_switch_assist(shared_ptr c, const std::string&) { + auto s = c->require_server_state(); + auto l = c->require_lobby(); + check_is_game(l, true); + + c->config.toggle_flag(Client::Flag::SWITCH_ASSIST_ENABLED); + send_text_message_printf(c, "$C6Switch assist %s", + c->config.check_flag(Client::Flag::SWITCH_ASSIST_ENABLED) ? "enabled" : "disabled"); +} + +static void proxy_command_switch_assist(shared_ptr ses, const std::string&) { + auto s = ses->require_server_state(); + ses->config.toggle_flag(Client::Flag::SWITCH_ASSIST_ENABLED); + send_text_message_printf(ses->client_channel, "$C6Switch assist %s", + ses->config.check_flag(Client::Flag::SWITCH_ASSIST_ENABLED) ? "enabled" : "disabled"); +} + +static void server_command_dropmode(shared_ptr c, const std::string& args) { + auto l = c->require_lobby(); + check_is_game(l, true); + if (args.empty()) { + switch (l->drop_mode) { + case Lobby::DropMode::DISABLED: + send_text_message(c, "Drop mode: disabled"); + break; + case Lobby::DropMode::CLIENT: + send_text_message(c, "Drop mode: client"); + break; + case Lobby::DropMode::SERVER_SHARED: + send_text_message(c, "Drop mode: server\nshared"); + break; + case Lobby::DropMode::SERVER_PRIVATE: + send_text_message(c, "Drop mode: server\nprivate"); + break; + case Lobby::DropMode::SERVER_DUPLICATE: + send_text_message(c, "Drop mode: server\nduplicate"); + break; + } + + } else { + check_is_leader(l, c); + Lobby::DropMode new_mode; + if ((args == "none") || (args == "disabled")) { + new_mode = Lobby::DropMode::DISABLED; + } else if (args == "client") { + new_mode = Lobby::DropMode::CLIENT; + } else if ((args == "shared") || (args == "server")) { + new_mode = Lobby::DropMode::SERVER_SHARED; + } else if ((args == "private") || (args == "priv")) { + new_mode = Lobby::DropMode::SERVER_PRIVATE; + } else if ((args == "duplicate") || (args == "dup")) { + new_mode = Lobby::DropMode::SERVER_DUPLICATE; + } else { + send_text_message(c, "Invalid drop mode"); + return; + } + + if (!(l->allowed_drop_modes & (1 << static_cast(new_mode)))) { + send_text_message(c, "Drop mode not\nallowed"); + return; + } + + l->set_drop_mode(new_mode); + switch (l->drop_mode) { + case Lobby::DropMode::DISABLED: + send_text_message(l, "Item drops disabled"); + break; + case Lobby::DropMode::CLIENT: + send_text_message(l, "Item drops changed\nto client mode"); + break; + case Lobby::DropMode::SERVER_SHARED: + send_text_message(l, "Item drops changed\nto server shared\nmode"); + break; + case Lobby::DropMode::SERVER_PRIVATE: + send_text_message(l, "Item drops changed\nto server private\nmode"); + break; + case Lobby::DropMode::SERVER_DUPLICATE: + send_text_message(l, "Item drops changed\nto server duplicate\nmode"); + break; + } + } +} + +static void proxy_command_dropmode(shared_ptr ses, const std::string& args) { + check_cheats_allowed(ses->require_server_state(), ses); + + using DropMode = ProxyServer::LinkedSession::DropMode; + if (args.empty()) { + switch (ses->drop_mode) { + case DropMode::DISABLED: + send_text_message(ses->client_channel, "Drop mode: disabled"); + break; + case DropMode::PASSTHROUGH: + send_text_message(ses->client_channel, "Drop mode: default"); + break; + case DropMode::INTERCEPT: + send_text_message(ses->client_channel, "Drop mode: proxy"); + break; + } + + } else { + DropMode new_mode; + if ((args == "none") || (args == "disabled")) { + new_mode = DropMode::DISABLED; + } else if ((args == "default") || (args == "passthrough")) { + new_mode = DropMode::PASSTHROUGH; + } else if ((args == "proxy") || (args == "intercept")) { + new_mode = DropMode::INTERCEPT; + } else { + send_text_message(ses->client_channel, "Invalid drop mode"); + return; + } + + ses->set_drop_mode(new_mode); + switch (ses->drop_mode) { + case DropMode::DISABLED: + send_text_message(ses->client_channel, "Item drops disabled"); + break; + case DropMode::PASSTHROUGH: + send_text_message(ses->client_channel, "Item drops changed\nto default mode"); + break; + case DropMode::INTERCEPT: + send_text_message(ses->client_channel, "Item drops changed\nto proxy mode"); + break; + } + } +} + +static void server_command_item(shared_ptr c, const std::string& args) { + auto s = c->require_server_state(); + auto l = c->require_lobby(); + check_is_game(l, true); + check_cheats_enabled(l, c); + + ItemData item = s->parse_item_description(c->version(), args); + item.id = l->generate_item_id(c->lobby_client_id); + + if ((l->drop_mode == Lobby::DropMode::SERVER_PRIVATE) || (l->drop_mode == Lobby::DropMode::SERVER_DUPLICATE)) { + l->add_item(c->floor, item, c->x, c->z, (1 << c->lobby_client_id)); + send_drop_stacked_item_to_channel(s, c->channel, item, c->floor, c->x, c->z); + } else { + l->add_item(c->floor, item, c->x, c->z, 0x00F); + send_drop_stacked_item_to_lobby(l, item, c->floor, c->x, c->z); + } + + string name = s->describe_item(c->version(), item, true); + send_text_message(c, "$C7Item created:\n" + name); +} + +static void proxy_command_item(shared_ptr ses, const std::string& args) { + auto s = ses->require_server_state(); + check_cheats_allowed(s, ses); + if (ses->version() == Version::BB_V4) { + send_text_message(ses->client_channel, "$C6This command cannot\nbe used on the proxy\nserver in BB games"); + return; + } + if (!ses->is_in_game) { + send_text_message(ses->client_channel, "$C6You must be in\na game to use this\ncommand"); + return; + } + if (ses->lobby_client_id != ses->leader_client_id) { + send_text_message(ses->client_channel, "$C6You must be the\nleader to use this\ncommand"); + return; + } + + bool set_drop = (!args.empty() && (args[0] == '!')); + + ItemData item = s->parse_item_description(ses->version(), (set_drop ? args.substr(1) : args)); + item.id = random_object() | 0x80000000; + + if (set_drop) { + ses->next_drop_item = item; + + string name = s->describe_item(ses->version(), item, true); + send_text_message(ses->client_channel, "$C7Next drop:\n" + name); + + } else { + send_drop_stacked_item_to_channel(s, ses->client_channel, item, ses->floor, ses->x, ses->z); + send_drop_stacked_item_to_channel(s, ses->server_channel, item, ses->floor, ses->x, ses->z); + + string name = s->describe_item(ses->version(), item, true); + send_text_message(ses->client_channel, "$C7Item created:\n" + name); + } +} + +static void server_command_enable_ep3_battle_debug_menu(shared_ptr c, const std::string& args) { + check_debug_enabled(c); + + auto l = c->require_lobby(); + check_is_game(l, true); + check_is_ep3(c, true); + if (l->episode != Episode::EP3) { + throw logic_error("non-Ep3 client in Ep3 game"); + } + if (!l->ep3_server) { + send_text_message(c, "$C6Episode 3 server\nis not initialized"); + return; + } + + if (!args.empty()) { + l->ep3_server->override_environment_number = stoul(args, nullptr, 16); + send_text_message_printf(l, "$C6Override environment\nnumber set to %02hhX", l->ep3_server->override_environment_number); + } else if (l->ep3_server->override_environment_number == 0xFF) { + l->ep3_server->override_environment_number = 0x1A; + send_text_message(l, "$C6Battle setup debug\nmenu enabled"); + } else { + l->ep3_server->override_environment_number = 0xFF; + send_text_message(l, "$C6Battle setup debug\nmenu disabled"); + } +} + +static void server_command_ep3_infinite_time(shared_ptr c, const std::string&) { + auto l = c->require_lobby(); + check_is_game(l, true); + check_is_ep3(c, true); + check_is_leader(l, c); + + if (l->episode != Episode::EP3) { + throw logic_error("non-Ep3 client in Ep3 game"); + } + if (!l->ep3_server) { + send_text_message(c, "$C6Episode 3 server\nis not initialized"); + return; + } + if (l->ep3_server->setup_phase != Episode3::SetupPhase::REGISTRATION) { + send_text_message(c, "$C6Battle is already\nin progress"); + return; + } + + l->ep3_server->options.behavior_flags ^= Episode3::BehaviorFlag::DISABLE_TIME_LIMITS; + bool infinite_time_enabled = (l->ep3_server->options.behavior_flags & Episode3::BehaviorFlag::DISABLE_TIME_LIMITS); + send_text_message(l, infinite_time_enabled ? "$C6Infinite time enabled" : "$C6Infinite time disabled"); +} + +static void server_command_ep3_set_dice_range(shared_ptr c, const std::string& args) { + auto l = c->require_lobby(); + check_is_game(l, true); + check_is_ep3(c, true); + check_is_leader(l, c); + + if (l->episode != Episode::EP3) { + throw logic_error("non-Ep3 client in Ep3 game"); + } + if (!l->ep3_server) { + send_text_message(c, "$C6Episode 3 server\nis not initialized"); + return; + } + if (l->ep3_server->setup_phase != Episode3::SetupPhase::REGISTRATION) { + send_text_message(c, "$C6Battle is already\nin progress"); + return; + } + if (l->tournament_match) { + send_text_message(c, "$C6Cannot override\ndice ranges in a\ntournament"); + return; + } + + auto parse_dice_range = +[](const string& spec) -> uint8_t { + auto tokens = split(spec, '-'); + if (tokens.size() == 1) { + uint8_t v = stoull(spec); + return (v << 4) | (v & 0x0F); + } else if (tokens.size() == 2) { + return (stoull(tokens[0]) << 4) | (stoull(tokens[1]) & 0x0F); + } else { + throw runtime_error("invalid dice spec format"); + } + }; + + uint8_t def_dice_range = 0; + uint8_t atk_dice_range_2v1 = 0; + uint8_t def_dice_range_2v1 = 0; + for (const auto& spec : split(args, ' ')) { + auto tokens = split(spec, ':'); + if (tokens.size() != 2) { + send_text_message(c, "$C6Invalid dice spec\nformat"); + return; + } + if (tokens[0] == "d") { + def_dice_range = parse_dice_range(tokens[1]); + } else if (tokens[0] == "1") { + atk_dice_range_2v1 = parse_dice_range(tokens[1]); + def_dice_range_2v1 = atk_dice_range_2v1; + } else if (tokens[0] == "a1") { + atk_dice_range_2v1 = parse_dice_range(tokens[1]); + } else if (tokens[0] == "d1") { + def_dice_range_2v1 = parse_dice_range(tokens[1]); + } + } + + l->ep3_server->def_dice_value_range_override = def_dice_range; + l->ep3_server->atk_dice_value_range_2v1_override = atk_dice_range_2v1; + l->ep3_server->def_dice_value_range_2v1_override = def_dice_range_2v1; + + if (!def_dice_range && !atk_dice_range_2v1 && !def_dice_range_2v1) { + send_text_message_printf(l, "$C7Dice ranges reset\nto defaults"); + } else { + send_text_message_printf(l, "$C7Dice ranges changed:"); + if (def_dice_range) { + send_text_message_printf(l, "$C7DEF: $C6%hhu-%hhu", + static_cast(def_dice_range >> 4), static_cast(def_dice_range & 0x0F)); + } + if (atk_dice_range_2v1) { + send_text_message_printf(l, "$C7ATK (1p in 2v1): $C6%hhu-%hhu", + static_cast(atk_dice_range_2v1 >> 4), static_cast(atk_dice_range_2v1 & 0x0F)); + } + if (def_dice_range_2v1) { + send_text_message_printf(l, "$C7DEF (1p in 2v1): $C6%hhu-%hhu", + static_cast(def_dice_range_2v1 >> 4), static_cast(def_dice_range_2v1 & 0x0F)); + } + } +} + +static void server_command_ep3_replace_assist_card(shared_ptr c, const std::string& args) { + auto s = c->require_server_state(); + auto l = c->require_lobby(); + check_is_game(l, true); + check_is_ep3(c, true); + check_cheats_enabled(l, c); + + if (l->episode != Episode::EP3) { + throw logic_error("non-Ep3 client in Ep3 game"); + } + if (!l->ep3_server) { + send_text_message(c, "$C6Episode 3 server\nis not initialized"); + return; + } + if (l->ep3_server->setup_phase != Episode3::SetupPhase::MAIN_BATTLE) { + send_text_message(c, "$C6Battle has not\nyet begun"); + return; + } + if (args.empty()) { + send_text_message(c, "$C6Missing arguments"); + return; + } + + size_t client_id; + string card_name; + if (isdigit(args[0])) { + auto tokens = split(args, ' ', 1); + client_id = stoul(tokens.at(0), nullptr, 0) - 1; + card_name = tokens.at(1); + } else { + client_id = c->lobby_client_id; + card_name = args; + } + if (client_id >= 4) { + send_text_message(c, "$C6Invalid client ID"); + return; + } + + shared_ptr ce; + try { + ce = l->ep3_server->options.card_index->definition_for_name_normalized(card_name); + } catch (const out_of_range&) { + send_text_message(c, "$C6Card not found"); + return; + } + if (ce->def.type != Episode3::CardType::ASSIST) { + send_text_message(c, "$C6Card is not an\nAssist card"); + return; + } + l->ep3_server->force_replace_assist_card(client_id, ce->def.card_id); +} + +static void server_command_ep3_unset_field_character(shared_ptr c, const std::string& args) { + auto s = c->require_server_state(); + auto l = c->require_lobby(); + check_is_game(l, true); + check_is_ep3(c, true); + check_cheats_enabled(l, c); + + if (l->episode != Episode::EP3) { + throw logic_error("non-Ep3 client in Ep3 game"); + } + if (!l->ep3_server) { + send_text_message(c, "$C6Episode 3 server\nis not initialized"); + return; + } + if (l->ep3_server->setup_phase != Episode3::SetupPhase::MAIN_BATTLE) { + send_text_message(c, "$C6Battle has not\nyet begun"); + return; + } + + size_t index = stoull(args) - 1; + l->ep3_server->force_destroy_field_character(c->lobby_client_id, index); +} + +static void server_command_surrender(shared_ptr c, const std::string&) { + auto l = c->require_lobby(); + check_is_game(l, true); + check_is_ep3(c, true); + if (l->episode != Episode::EP3) { + throw logic_error("non-Ep3 client in Ep3 game"); + } + if (!l->ep3_server) { + send_text_message(c, "$C6Episode 3 server\nis not initialized"); + return; + } + if (l->ep3_server->setup_phase != Episode3::SetupPhase::MAIN_BATTLE) { + send_text_message(c, "$C6Battle has not\nyet started"); + return; + } + string name = remove_color(c->character()->disp.name.decode(c->language())); + send_text_message_printf(l, "$C6%s has\nsurrendered", name.c_str()); + for (const auto& watcher_l : l->watcher_lobbies) { + send_text_message_printf(watcher_l, "$C6%s has\nsurrendered", name.c_str()); + } + l->ep3_server->force_battle_result(c->lobby_client_id, false); +} + +static void server_command_get_ep3_battle_stat(shared_ptr c, const std::string& args) { + auto l = c->require_lobby(); + check_is_game(l, true); + check_is_ep3(c, true); + if (l->episode != Episode::EP3) { + throw logic_error("non-Ep3 client in Ep3 game"); + } + if (!l->ep3_server) { + send_text_message(c, "$C6Episode 3 server\nis not initialized"); + return; + } + if (l->ep3_server->setup_phase != Episode3::SetupPhase::MAIN_BATTLE) { + send_text_message(c, "$C6Battle has not\nyet started"); + return; + } + auto ps = l->ep3_server->player_states.at(c->lobby_client_id); + if (!ps) { + send_text_message(c, "$C6Player is missing"); + return; + } + uint8_t team_id = ps->get_team_id(); + if (team_id > 1) { + throw logic_error("team ID is incorrect"); + } + + if (args == "rank") { + float score = ps->stats.score(l->ep3_server->get_round_num()); + uint8_t rank = ps->stats.rank_for_score(score); + const char* rank_name = ps->stats.name_for_rank(rank); + send_text_message_printf(c, "$C7Score: %g\nRank: %hhu (%s)", score, rank, rank_name); + } else if (args == "duration") { + string s = format_duration(now() - l->ep3_server->battle_start_usecs); + send_text_message_printf(c, "$C7Duration: %s", s.c_str()); + } else if (args == "fcs-destroyed") { + send_text_message_printf(c, "$C7Team FCs destroyed:\n%" PRIu32, l->ep3_server->team_num_ally_fcs_destroyed[team_id]); + } else if (args == "cards-destroyed") { + send_text_message_printf(c, "$C7Team cards destroyed:\n%" PRIu32, l->ep3_server->team_num_cards_destroyed[team_id]); + } else if (args == "damage-given") { + send_text_message_printf(c, "$C7Damage given: %hu", ps->stats.damage_given.load()); + } else if (args == "damage-taken") { + send_text_message_printf(c, "$C7Damage taken: %hu", ps->stats.damage_taken.load()); + } else if (args == "opp-cards-destroyed") { + send_text_message_printf(c, "$C7Opp. cards destroyed:\n%hu", ps->stats.num_opponent_cards_destroyed.load()); + } else if (args == "own-cards-destroyed") { + send_text_message_printf(c, "$C7Own cards destroyed:\n%hu", ps->stats.num_owned_cards_destroyed.load()); + } else if (args == "move-distance") { + send_text_message_printf(c, "$C7Move distance: %hu", ps->stats.total_move_distance.load()); + } else if (args == "cards-set") { + send_text_message_printf(c, "$C7Cards set: %hu", ps->stats.num_cards_set.load()); + } else if (args == "fcs-set") { + send_text_message_printf(c, "$C7FC cards set: %hu", ps->stats.num_item_or_creature_cards_set.load()); + } else if (args == "attack-actions-set") { + send_text_message_printf(c, "$C7Attack actions set:\n%hu", ps->stats.num_attack_actions_set.load()); + } else if (args == "techs-set") { + send_text_message_printf(c, "$C7Techs set: %hu", ps->stats.num_tech_cards_set.load()); + } else if (args == "assists-set") { + send_text_message_printf(c, "$C7Assists set: %hu", ps->stats.num_assist_cards_set.load()); + } else if (args == "defenses-self") { + send_text_message_printf(c, "$C7Defenses on self:\n%hu", ps->stats.defense_actions_set_on_self.load()); + } else if (args == "defenses-ally") { + send_text_message_printf(c, "$C7Defenses on ally:\n%hu", ps->stats.defense_actions_set_on_ally.load()); + } else if (args == "cards-drawn") { + send_text_message_printf(c, "$C7Cards drawn: %hu", ps->stats.num_cards_drawn.load()); + } else if (args == "max-attack-damage") { + send_text_message_printf(c, "$C7Maximum attack damage:\n%hu", ps->stats.max_attack_damage.load()); + } else if (args == "max-combo") { + send_text_message_printf(c, "$C7Longest combo: %hu", ps->stats.max_attack_combo_size.load()); + } else if (args == "attacks-given") { + send_text_message_printf(c, "$C7Attacks given: %hu", ps->stats.num_attacks_given.load()); + } else if (args == "attacks-taken") { + send_text_message_printf(c, "$C7Attacks taken: %hu", ps->stats.num_attacks_taken.load()); + } else if (args == "sc-damage") { + send_text_message_printf(c, "$C7SC damage taken: %hu", ps->stats.sc_damage_taken.load()); + } else if (args == "damage-defended") { + send_text_message_printf(c, "$C7Damage defended: %hu", ps->stats.action_card_negated_damage.load()); + } else { + send_text_message(c, "$C6Unknown statistic"); + } +} + +//////////////////////////////////////////////////////////////////////////////// + +typedef void (*server_handler_t)(shared_ptr c, const std::string& args); +typedef void (*proxy_handler_t)(shared_ptr ses, const std::string& args); +struct ChatCommandDefinition { + server_handler_t server_handler; + proxy_handler_t proxy_handler; +}; + +static const unordered_map chat_commands({ + {"$allevent", {server_command_lobby_event_all, nullptr}}, + {"$ann", {server_command_announce, nullptr}}, + {"$ann!", {server_command_announce_mail, nullptr}}, + {"$arrow", {server_command_arrow, proxy_command_arrow}}, + {"$auction", {server_command_auction, proxy_command_auction}}, + {"$ax", {server_command_ax, nullptr}}, + {"$ban", {server_command_ban, nullptr}}, + {"$bank", {server_command_change_bank, nullptr}}, + {"$bbchar", {server_command_bbchar, nullptr}}, + {"$cheat", {server_command_cheat, nullptr}}, + {"$debug", {server_command_debug, nullptr}}, + {"$dicerange", {server_command_ep3_set_dice_range, nullptr}}, + {"$dropmode", {server_command_dropmode, proxy_command_dropmode}}, + {"$edit", {server_command_edit, nullptr}}, + {"$ep3battledebug", {server_command_enable_ep3_battle_debug_menu, nullptr}}, + {"$event", {server_command_lobby_event, proxy_command_lobby_event}}, + {"$exit", {server_command_exit, proxy_command_exit}}, + {"$gc", {server_command_get_self_card, proxy_command_get_player_card}}, + {"$infhp", {server_command_infinite_hp, proxy_command_infinite_hp}}, + {"$inftime", {server_command_ep3_infinite_time, nullptr}}, + {"$inftp", {server_command_infinite_tp, proxy_command_infinite_tp}}, + {"$item", {server_command_item, proxy_command_item}}, + {"$itemnotifs", {server_command_item_notifs, proxy_command_item_notifs}}, + {"$i", {server_command_item, proxy_command_item}}, + {"$kick", {server_command_kick, nullptr}}, + {"$li", {server_command_lobby_info, proxy_command_lobby_info}}, + {"$ln", {server_command_lobby_type, proxy_command_lobby_type}}, + {"$loadchar", {server_command_loadchar, nullptr}}, + {"$matcount", {server_command_show_material_counts, nullptr}}, + {"$maxlevel", {server_command_max_level, nullptr}}, + {"$meseta", {server_command_meseta, nullptr}}, + {"$minlevel", {server_command_min_level, nullptr}}, + {"$next", {server_command_next, proxy_command_next}}, + {"$password", {server_command_password, nullptr}}, + {"$patch", {server_command_patch, proxy_command_patch}}, + {"$persist", {server_command_persist, nullptr}}, + {"$ping", {server_command_ping, proxy_command_ping}}, + {"$playrec", {server_command_playrec, nullptr}}, + {"$qcall", {server_command_qcall, proxy_command_qcall}}, + {"$qcheck", {server_command_qcheck, nullptr}}, + {"$qclear", {server_command_qclear, proxy_command_qclear}}, + {"$qfread", {server_command_qfread, nullptr}}, + {"$qgread", {server_command_qgread, nullptr}}, + {"$qgwrite", {server_command_qgwrite, nullptr}}, + {"$qset", {server_command_qset, proxy_command_qset}}, + {"$qsync", {server_command_qsync, proxy_command_qsync}}, + {"$qsyncall", {server_command_qsyncall, proxy_command_qsyncall}}, + {"$quest", {server_command_quest, nullptr}}, + {"$rand", {server_command_rand, proxy_command_rand}}, + {"$save", {server_command_save, nullptr}}, + {"$savechar", {server_command_savechar, nullptr}}, + {"$saverec", {server_command_saverec, nullptr}}, + {"$sb", {server_command_send_both, proxy_command_send_both}}, + {"$sc", {server_command_send_client, proxy_command_send_client}}, + {"$secid", {server_command_secid, proxy_command_secid}}, + {"$setassist", {server_command_ep3_replace_assist_card, nullptr}}, + {"$si", {server_command_server_info, nullptr}}, + {"$silence", {server_command_silence, nullptr}}, + {"$song", {server_command_song, proxy_command_song}}, + {"$spec", {server_command_toggle_spectator_flag, nullptr}}, + {"$ss", {server_command_send_server, proxy_command_send_server}}, + {"$stat", {server_command_get_ep3_battle_stat, nullptr}}, + {"$surrender", {server_command_surrender, nullptr}}, + {"$swa", {server_command_switch_assist, proxy_command_switch_assist}}, + {"$swclear", {server_command_swclear, proxy_command_swclear}}, + {"$swset", {server_command_swset, proxy_command_swset}}, + {"$swsetall", {server_command_swsetall, proxy_command_swsetall}}, + {"$unset", {server_command_ep3_unset_field_character, nullptr}}, + {"$variations", {server_command_variations, nullptr}}, + {"$warp", {server_command_warpme, proxy_command_warpme}}, + {"$warpme", {server_command_warpme, proxy_command_warpme}}, + {"$warpall", {server_command_warpall, proxy_command_warpall}}, + {"$what", {server_command_what, nullptr}}, + {"$where", {server_command_where, nullptr}}, +}); + +struct SplitCommand { + string name; + string args; + + SplitCommand(const string& text) { + size_t space_pos = text.find(' '); + if (space_pos != string::npos) { + this->name = text.substr(0, space_pos); + this->args = text.substr(space_pos + 1); + } else { + this->name = text; + } + } +}; + +// This function is called every time any player sends a chat beginning with a +// dollar sign. It is this function's responsibility to see if the chat is a +// command, and to execute the command and block the chat if it is. +void on_chat_command(std::shared_ptr c, const std::string& text) { + SplitCommand cmd(text); + if (!cmd.name.empty() && cmd.name[0] == '@') { + cmd.name[0] = '$'; + } + + const ChatCommandDefinition* def = nullptr; + try { + def = &chat_commands.at(cmd.name); + } catch (const out_of_range&) { + send_text_message(c, "$C6Unknown command"); + return; + } + + if (!def->server_handler) { + send_text_message(c, "$C6Command not available\non game server"); + } else { + try { + def->server_handler(c, cmd.args); + } catch (const precondition_failed& e) { + send_text_message(c, e.what()); + } catch (const exception& e) { + send_text_message(c, "$C6Failed:\n" + remove_color(e.what())); + } + } +} + +void on_chat_command(shared_ptr ses, const std::string& text) { + SplitCommand cmd(text); + + const ChatCommandDefinition* def = nullptr; + try { + def = &chat_commands.at(cmd.name); + } catch (const out_of_range&) { + send_text_message(ses->client_channel, "$C6Unknown command"); + return; + } + + if (!def->proxy_handler) { + send_text_message(ses->client_channel, "$C6Command not available\non proxy server"); + } else { + try { + def->proxy_handler(ses, cmd.args); + } catch (const precondition_failed& e) { + send_text_message(ses->client_channel, e.what()); + } catch (const exception& e) { + send_text_message(ses->client_channel, "$C6Failed:\n" + remove_color(e.what())); + } + } +} diff --git a/src/ChatCommands.hh b/src/ChatCommands.hh index dd13c8ad..e52f8358 100644 --- a/src/ChatCommands.hh +++ b/src/ChatCommands.hh @@ -1,14 +1,14 @@ -#pragma once - -#include - -#include -#include - -#include "Client.hh" -#include "Lobby.hh" -#include "ProxyServer.hh" -#include "ServerState.hh" - -void on_chat_command(std::shared_ptr c, const std::string& text); -void on_chat_command(std::shared_ptr ses, const std::string& text); +#pragma once + +#include + +#include +#include + +#include "Client.hh" +#include "Lobby.hh" +#include "ProxyServer.hh" +#include "ServerState.hh" + +void on_chat_command(std::shared_ptr c, const std::string& text); +void on_chat_command(std::shared_ptr ses, const std::string& text); diff --git a/src/ChoiceSearch.cc b/src/ChoiceSearch.cc index 84898792..e1d75d7f 100644 --- a/src/ChoiceSearch.cc +++ b/src/ChoiceSearch.cc @@ -1,150 +1,150 @@ -#include "ChoiceSearch.hh" - -#include -#include - -#include "Client.hh" - -using namespace std; - -const vector CHOICE_SEARCH_CATEGORIES({ - ChoiceSearchCategory{ - .id = 0x0001, - .name = "Level", - .choices = { - {0x0000, "Any"}, - {0x0001, "Own level +/- 5"}, - {0x0002, "Level 1-10"}, - {0x0003, "Level 11-20"}, - {0x0004, "Level 21-40"}, - {0x0005, "Level 41-60"}, - {0x0006, "Level 61-80"}, - {0x0007, "Level 81-100"}, - {0x0008, "Level 101-120"}, - {0x0009, "Level 121-160"}, - {0x000A, "Level 161-200"}, - }, - .client_matches = +[](shared_ptr searcher_c, shared_ptr target_c, uint16_t choice_id) -> bool { - if (choice_id == 0x0000) { - return true; - } - uint32_t target_level = target_c->character()->disp.stats.level + 1; - switch (choice_id) { - case 0x0001: - return (labs(static_cast(target_level - searcher_c->character()->disp.stats.level)) <= 5); - case 0x0002: - return (target_level <= 10); - case 0x0003: - return (target_level > 10) && (target_level <= 20); - case 0x0004: - return (target_level > 20) && (target_level <= 40); - case 0x0005: - return (target_level > 40) && (target_level <= 60); - case 0x0006: - return (target_level > 60) && (target_level <= 80); - case 0x0007: - return (target_level > 80) && (target_level <= 100); - case 0x0008: - return (target_level > 100) && (target_level <= 120); - case 0x0009: - return (target_level > 120) && (target_level <= 160); - case 0x000A: - return (target_level > 160) && (target_level <= 200); - } - return false; - }, - }, - ChoiceSearchCategory{ - .id = 0x0002, - .name = "Class", - .choices = { - {0x0000, "Any"}, - {0x0010, "Hunter"}, - {0x0001, "HUmar"}, - {0x0002, "HUnewearl"}, - {0x0003, "HUcast"}, - {0x000A, "HUcaseal"}, - {0x0011, "Ranger"}, - {0x0004, "RAmar"}, - {0x000C, "RAmarl"}, - {0x0005, "RAcast"}, - {0x0006, "RAcaseal"}, - {0x0012, "Force"}, - {0x000B, "FOmar"}, - {0x0007, "FOmarl"}, - {0x0008, "FOnewm"}, - {0x0009, "FOnewearl"}, - }, - .client_matches = +[](shared_ptr, shared_ptr target_c, uint16_t choice_id) -> bool { - switch (choice_id) { - case 0x0000: - return true; - case 0x0010: - return target_c->character()->disp.visual.class_flags & 0x20; - case 0x0011: - return target_c->character()->disp.visual.class_flags & 0x40; - case 0x0012: - return target_c->character()->disp.visual.class_flags & 0x80; - default: - return ((choice_id - 1) == target_c->character()->disp.visual.char_class); - } - }, - }, - ChoiceSearchCategory{ - .id = 0x0003, - .name = "Platform", - .choices = { - {0x0000, "Any"}, - {0x0001, "DC betas"}, - {0x0002, "DC V1"}, - {0x0003, "DC V2 / PC"}, - {0x0004, "GC / Xbox Episodes 1&2"}, - {0x0005, "GC Episode 3"}, - {0x0006, "BB"}, - }, - .client_matches = +[](shared_ptr, shared_ptr target_c, uint16_t choice_id) -> bool { - if (choice_id == 0x0000) { - return true; - } - switch (target_c->version()) { - case Version::DC_NTE: - case Version::DC_V1_11_2000_PROTOTYPE: - return (choice_id == 0x0001); - case Version::DC_V1: - return (choice_id == 0x0002); - case Version::DC_V2: - case Version::PC_NTE: - case Version::PC_V2: - return (choice_id == 0x0003); - case Version::GC_NTE: - case Version::GC_V3: - case Version::XB_V3: - return (choice_id == 0x0004); - case Version::GC_EP3_NTE: - case Version::GC_EP3: - return (choice_id == 0x0005); - case Version::BB_V4: - return (choice_id == 0x0006); - default: - return false; - } - }, - }, - ChoiceSearchCategory{ - .id = 0x0204, - .name = "Game mode", - .choices = { - {0x0000, "Any"}, - {0x0001, "Normal"}, - {0x0002, "Hard"}, - {0x0003, "Very Hard"}, - {0x0004, "Ultimate"}, - {0x0005, "Battle"}, - {0x0006, "Challenge"}, - }, - .client_matches = +[](shared_ptr, shared_ptr target_c, uint16_t choice_id) -> bool { - uint16_t target_choice_id = target_c->character()->choice_search_config.get_setting(0x0204); - return (choice_id == 0) || (target_choice_id == 0) || (choice_id == target_choice_id); - }, - }, -}); +#include "ChoiceSearch.hh" + +#include +#include + +#include "Client.hh" + +using namespace std; + +const vector CHOICE_SEARCH_CATEGORIES({ + ChoiceSearchCategory{ + .id = 0x0001, + .name = "Level", + .choices = { + {0x0000, "Any"}, + {0x0001, "Own level +/- 5"}, + {0x0002, "Level 1-10"}, + {0x0003, "Level 11-20"}, + {0x0004, "Level 21-40"}, + {0x0005, "Level 41-60"}, + {0x0006, "Level 61-80"}, + {0x0007, "Level 81-100"}, + {0x0008, "Level 101-120"}, + {0x0009, "Level 121-160"}, + {0x000A, "Level 161-200"}, + }, + .client_matches = +[](shared_ptr searcher_c, shared_ptr target_c, uint16_t choice_id) -> bool { + if (choice_id == 0x0000) { + return true; + } + uint32_t target_level = target_c->character()->disp.stats.level + 1; + switch (choice_id) { + case 0x0001: + return (labs(static_cast(target_level - searcher_c->character()->disp.stats.level)) <= 5); + case 0x0002: + return (target_level <= 10); + case 0x0003: + return (target_level > 10) && (target_level <= 20); + case 0x0004: + return (target_level > 20) && (target_level <= 40); + case 0x0005: + return (target_level > 40) && (target_level <= 60); + case 0x0006: + return (target_level > 60) && (target_level <= 80); + case 0x0007: + return (target_level > 80) && (target_level <= 100); + case 0x0008: + return (target_level > 100) && (target_level <= 120); + case 0x0009: + return (target_level > 120) && (target_level <= 160); + case 0x000A: + return (target_level > 160) && (target_level <= 200); + } + return false; + }, + }, + ChoiceSearchCategory{ + .id = 0x0002, + .name = "Class", + .choices = { + {0x0000, "Any"}, + {0x0010, "Hunter"}, + {0x0001, "HUmar"}, + {0x0002, "HUnewearl"}, + {0x0003, "HUcast"}, + {0x000A, "HUcaseal"}, + {0x0011, "Ranger"}, + {0x0004, "RAmar"}, + {0x000C, "RAmarl"}, + {0x0005, "RAcast"}, + {0x0006, "RAcaseal"}, + {0x0012, "Force"}, + {0x000B, "FOmar"}, + {0x0007, "FOmarl"}, + {0x0008, "FOnewm"}, + {0x0009, "FOnewearl"}, + }, + .client_matches = +[](shared_ptr, shared_ptr target_c, uint16_t choice_id) -> bool { + switch (choice_id) { + case 0x0000: + return true; + case 0x0010: + return target_c->character()->disp.visual.class_flags & 0x20; + case 0x0011: + return target_c->character()->disp.visual.class_flags & 0x40; + case 0x0012: + return target_c->character()->disp.visual.class_flags & 0x80; + default: + return ((choice_id - 1) == target_c->character()->disp.visual.char_class); + } + }, + }, + ChoiceSearchCategory{ + .id = 0x0003, + .name = "Platform", + .choices = { + {0x0000, "Any"}, + {0x0001, "DC betas"}, + {0x0002, "DC V1"}, + {0x0003, "DC V2 / PC"}, + {0x0004, "GC / Xbox Episodes 1&2"}, + {0x0005, "GC Episode 3"}, + {0x0006, "BB"}, + }, + .client_matches = +[](shared_ptr, shared_ptr target_c, uint16_t choice_id) -> bool { + if (choice_id == 0x0000) { + return true; + } + switch (target_c->version()) { + case Version::DC_NTE: + case Version::DC_V1_11_2000_PROTOTYPE: + return (choice_id == 0x0001); + case Version::DC_V1: + return (choice_id == 0x0002); + case Version::DC_V2: + case Version::PC_NTE: + case Version::PC_V2: + return (choice_id == 0x0003); + case Version::GC_NTE: + case Version::GC_V3: + case Version::XB_V3: + return (choice_id == 0x0004); + case Version::GC_EP3_NTE: + case Version::GC_EP3: + return (choice_id == 0x0005); + case Version::BB_V4: + return (choice_id == 0x0006); + default: + return false; + } + }, + }, + ChoiceSearchCategory{ + .id = 0x0204, + .name = "Game mode", + .choices = { + {0x0000, "Any"}, + {0x0001, "Normal"}, + {0x0002, "Hard"}, + {0x0003, "Very Hard"}, + {0x0004, "Ultimate"}, + {0x0005, "Battle"}, + {0x0006, "Challenge"}, + }, + .client_matches = +[](shared_ptr, shared_ptr target_c, uint16_t choice_id) -> bool { + uint16_t target_choice_id = target_c->character()->choice_search_config.get_setting(0x0204); + return (choice_id == 0) || (target_choice_id == 0) || (choice_id == target_choice_id); + }, + }, +}); diff --git a/src/ChoiceSearch.hh b/src/ChoiceSearch.hh index fed105de..15116fbd 100644 --- a/src/ChoiceSearch.hh +++ b/src/ChoiceSearch.hh @@ -1,64 +1,64 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include "Text.hh" - -class Client; - -template -struct ChoiceSearchConfigT { - using U16T = typename std::conditional::type; - using U32T = typename std::conditional::type; - - U32T disabled = 1; // 0 = enabled, 1 = disabled. Unused in command C3 - struct Entry { - U16T parent_choice_id = 0; - U16T choice_id = 0; - } __packed_ws__(Entry, 4); - parray entries; - - int32_t get_setting(uint16_t parent_choice_id) const { - for (size_t z = 0; z < this->entries.size(); z++) { - if (this->entries[z].parent_choice_id == parent_choice_id) { - return this->entries[z].choice_id; - } - } - return -1; - } - - operator ChoiceSearchConfigT() const { - ChoiceSearchConfigT ret; - ret.disabled = this->disabled.load(); - for (size_t z = 0; z < this->entries.size(); z++) { - auto& ret_e = ret.entries[z]; - const auto& this_e = this->entries[z]; - ret_e.parent_choice_id = this_e.parent_choice_id.load(); - ret_e.choice_id = this_e.choice_id.load(); - } - return ret; - } -} __packed__; - -using ChoiceSearchConfig = ChoiceSearchConfigT; -using ChoiceSearchConfigBE = ChoiceSearchConfigT; -check_struct_size(ChoiceSearchConfig, 0x18); -check_struct_size(ChoiceSearchConfigBE, 0x18); - -struct ChoiceSearchCategory { - struct Choice { - uint16_t id; - const char* name; - }; - - uint16_t id; - const char* name; - std::vector choices; - std::function searcher_c, std::shared_ptr target_c, uint16_t choice_id)> client_matches; -}; - -extern const std::vector CHOICE_SEARCH_CATEGORIES; +#pragma once + +#include +#include +#include +#include +#include + +#include "Text.hh" + +class Client; + +template +struct ChoiceSearchConfigT { + using U16T = typename std::conditional::type; + using U32T = typename std::conditional::type; + + U32T disabled = 1; // 0 = enabled, 1 = disabled. Unused in command C3 + struct Entry { + U16T parent_choice_id = 0; + U16T choice_id = 0; + } __packed_ws__(Entry, 4); + parray entries; + + int32_t get_setting(uint16_t parent_choice_id) const { + for (size_t z = 0; z < this->entries.size(); z++) { + if (this->entries[z].parent_choice_id == parent_choice_id) { + return this->entries[z].choice_id; + } + } + return -1; + } + + operator ChoiceSearchConfigT() const { + ChoiceSearchConfigT ret; + ret.disabled = this->disabled.load(); + for (size_t z = 0; z < this->entries.size(); z++) { + auto& ret_e = ret.entries[z]; + const auto& this_e = this->entries[z]; + ret_e.parent_choice_id = this_e.parent_choice_id.load(); + ret_e.choice_id = this_e.choice_id.load(); + } + return ret; + } +} __packed__; + +using ChoiceSearchConfig = ChoiceSearchConfigT; +using ChoiceSearchConfigBE = ChoiceSearchConfigT; +check_struct_size(ChoiceSearchConfig, 0x18); +check_struct_size(ChoiceSearchConfigBE, 0x18); + +struct ChoiceSearchCategory { + struct Choice { + uint16_t id; + const char* name; + }; + + uint16_t id; + const char* name; + std::vector choices; + std::function searcher_c, std::shared_ptr target_c, uint16_t choice_id)> client_matches; +}; + +extern const std::vector CHOICE_SEARCH_CATEGORIES; diff --git a/src/Client.cc b/src/Client.cc index af53ec15..5b233812 100644 --- a/src/Client.cc +++ b/src/Client.cc @@ -1,1120 +1,1120 @@ -#include "Client.hh" - -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include "IPStackSimulator.hh" -#include "Loggers.hh" -#include "SendCommands.hh" -#include "Server.hh" -#include "Version.hh" - -using namespace std; - -const uint64_t CLIENT_CONFIG_MAGIC = 0x8399AC32; - -static atomic next_id(1); - -void Client::Config::set_flags_for_version(Version version, int64_t sub_version) { - this->set_flag(Flag::PROXY_CHAT_COMMANDS_ENABLED); - - // BB shares some sub_version values with GC Episode 3, so we handle it - // separately first. - if (version == Version::BB_V4) { - this->set_flag(Flag::NO_D6); - this->set_flag(Flag::SAVE_ENABLED); - this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH); - return; - } - - switch (sub_version) { - case -1: // Initial check (before sub_version recognition) - switch (version) { - case Version::PC_PATCH: - case Version::BB_PATCH: - this->set_flag(Flag::NO_D6); - this->set_flag(Flag::NO_SEND_FUNCTION_CALL); - break; - case Version::DC_NTE: - case Version::DC_V1_11_2000_PROTOTYPE: - case Version::DC_V1: - this->set_flag(Flag::NO_D6); - this->set_flag(Flag::NO_SEND_FUNCTION_CALL); - break; - case Version::DC_V2: - this->set_flag(Flag::NO_D6); - this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH); - break; - case Version::PC_NTE: - case Version::PC_V2: - this->set_flag(Flag::NO_D6); - this->set_flag(Flag::SEND_FUNCTION_CALL_CHECKSUM_ONLY); - this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH); - break; - case Version::GC_NTE: - case Version::GC_V3: - case Version::GC_EP3_NTE: - case Version::GC_EP3: - break; - case Version::XB_V3: - // TODO: Do all versions of XB need this flag? US does, at least. - this->set_flag(Flag::NO_D6_AFTER_LOBBY); - this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH); - break; - default: - throw logic_error("invalid game version"); - } - break; - - case 0x20: // DCNTE, possibly also DCv1 JP - case 0x21: // DCv1 US - this->set_flag(Flag::NO_D6); - this->set_flag(Flag::NO_SEND_FUNCTION_CALL); - break; - case 0x22: // DCv1 EU 50Hz (presumably) - case 0x23: // DCv1 EU 60Hz (presumably) - this->set_flag(Flag::NO_D6); - this->set_flag(Flag::NO_SEND_FUNCTION_CALL); - break; - case 0x25: // DCv2 JP - case 0x26: // DCv2 US - case 0x27: // DCv2 EU 50Hz (presumably) - case 0x28: // DCv2 EU 60Hz (presumably) - this->set_flag(Flag::NO_D6); - this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH); - break; - case 0x29: // PC - this->set_flag(Flag::NO_D6); - this->set_flag(Flag::SEND_FUNCTION_CALL_CHECKSUM_ONLY); - this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH); - break; - case 0x30: // GC Ep1&2 GameJam demo, GC Ep1&2 Trial Edition, GC Ep1&2 JP v1.2, at least one version of XB - case 0x31: // GC Ep1&2 US v1.0, GC US v1.1, XB US - break; - case 0x32: // GC Ep1&2 EU 50Hz - case 0x33: // GC Ep1&2 EU 60Hz - case 0x34: // GC Ep1&2 JP v1.3 - this->set_flag(Flag::NO_D6_AFTER_LOBBY); - break; - case 0x35: // GC Ep1&2 JP v1.4 (Plus) - this->set_flag(Flag::NO_D6_AFTER_LOBBY); - this->set_flag(Flag::ENCRYPTED_SEND_FUNCTION_CALL); - this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH); - break; - case 0x3A: // GC Ep1&2 US v1.2 (Plus) GMK edition - this->set_flag(Flag::IS_CLIENT_CUSTOMIZATION); - [[fallthrough]]; - case 0x36: // GC Ep1&2 US v1.2 (Plus) - case 0x39: // GC Ep1&2 JP v1.5 (Plus) - this->set_flag(Flag::NO_D6_AFTER_LOBBY); - this->set_flag(Flag::NO_SEND_FUNCTION_CALL); - break; - case 0x40: // GC Ep3 JP and Trial Edition (and BB) - this->set_flag(Flag::NO_D6_AFTER_LOBBY); - this->set_flag(Flag::ENCRYPTED_SEND_FUNCTION_CALL); - this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH); - // sub_version can't be used to tell JP final and Trial Edition apart; we - // instead look at header.flag in the 61 command and set the version then. - break; - case 0x41: // GC Ep3 US (and BB) - this->set_flag(Flag::NO_D6_AFTER_LOBBY); - this->set_flag(Flag::USE_OVERFLOW_FOR_SEND_FUNCTION_CALL); - this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH); - break; - case 0x42: // GC Ep3 EU 50Hz - case 0x43: // GC Ep3 EU 60Hz - this->set_flag(Flag::NO_D6_AFTER_LOBBY); - this->set_flag(Flag::NO_SEND_FUNCTION_CALL); - break; - default: - throw runtime_error(string_printf("unknown sub_version %" PRIX64, sub_version)); - } -} - -Client::ItemDropNotificationMode Client::Config::get_drop_notification_mode() const { - uint8_t mode_s = (this->check_flag(Flag::ITEM_DROP_NOTIFICATIONS_1) ? 1 : 0) | - (this->check_flag(Flag::ITEM_DROP_NOTIFICATIONS_2) ? 2 : 0); - return static_cast(mode_s); -} - -void Client::Config::set_drop_notification_mode(ItemDropNotificationMode new_mode) { - uint8_t mode_s = static_cast(new_mode); - if (mode_s & 1) { - this->set_flag(Client::Flag::ITEM_DROP_NOTIFICATIONS_1); - } else { - this->clear_flag(Client::Flag::ITEM_DROP_NOTIFICATIONS_1); - } - if (mode_s & 2) { - this->set_flag(Client::Flag::ITEM_DROP_NOTIFICATIONS_2); - } else { - this->clear_flag(Client::Flag::ITEM_DROP_NOTIFICATIONS_2); - } -} - -bool Client::Config::should_update_vs(const Config& other) const { - constexpr uint64_t mask = static_cast(Flag::CLIENT_SIDE_MASK); - return ((this->enabled_flags ^ other.enabled_flags) & mask) || - (this->specific_version != other.specific_version) || - (this->override_random_seed != other.override_random_seed) || - (this->override_section_id != other.override_section_id) || - (this->override_lobby_event != other.override_lobby_event) || - (this->override_lobby_number != other.override_lobby_number) || - (this->proxy_destination_address != other.proxy_destination_address) || - (this->proxy_destination_port != other.proxy_destination_port); -} - -Client::Client( - shared_ptr server, - struct bufferevent* bev, - uint64_t virtual_network_id, - Version version, - ServerBehavior server_behavior) - : server(server), - id(next_id++), - log(string_printf("[C-%" PRIX64 "] ", this->id), client_log.min_level), - channel(bev, virtual_network_id, version, 1, nullptr, nullptr, this, "", TerminalFormat::FG_YELLOW, TerminalFormat::FG_GREEN), - server_behavior(server_behavior), - should_disconnect(false), - should_send_to_lobby_server(false), - should_send_to_proxy_server(false), - bb_connection_phase(0xFF), - ping_start_time(0), - sub_version(-1), - x(0.0f), - z(0.0f), - floor(0), - lobby_client_id(0), - lobby_arrow_color(0), - preferred_lobby_id(-1), - save_game_data_event( - event_new( - bufferevent_get_base(bev), -1, EV_TIMEOUT | EV_PERSIST, - &Client::dispatch_save_game_data, this), - event_free), - send_ping_event( - event_new( - bufferevent_get_base(bev), -1, EV_TIMEOUT, - &Client::dispatch_send_ping, this), - event_free), - idle_timeout_event( - event_new( - bufferevent_get_base(bev), -1, EV_TIMEOUT, - &Client::dispatch_idle_timeout, this), - event_free), - card_battle_table_number(-1), - card_battle_table_seat_number(0), - card_battle_table_seat_state(0), - last_game_info_requested(0), - should_update_play_time(false), - bb_character_index(-1), - next_exp_value(0), - can_chat(true), - dol_base_addr(0), - external_bank_character_index(-1), - last_play_time_update(0) { - this->update_channel_name(); - - this->config.set_flags_for_version(version, -1); - auto s = server->get_state(); - if (is_v1_or_v2(this->version()) ? s->default_rare_notifs_enabled_v1_v2 : s->default_rare_notifs_enabled_v3_v4) { - this->config.set_drop_notification_mode(ItemDropNotificationMode::RARES_ONLY); - } - this->config.specific_version = default_specific_version_for_version(version, -1); - - memset(&this->next_connection_addr, 0, sizeof(this->next_connection_addr)); - - this->reschedule_save_game_data_event(); - this->reschedule_ping_and_timeout_events(); - - // Don't print data sent to patch clients to the logs. The patch server - // protocol is fully understood and data logs for patch clients are generally - // more annoying than helpful at this point. - if ((s->hide_download_commands) && - ((this->channel.version == Version::PC_PATCH) || (this->channel.version == Version::BB_PATCH))) { - this->channel.terminal_recv_color = TerminalFormat::END; - this->channel.terminal_send_color = TerminalFormat::END; - } - - this->log.info("Created"); -} - -Client::~Client() { - if (!this->disconnect_hooks.empty()) { - this->log.warning("Disconnect hooks pending at client destruction time:"); - for (const auto& it : this->disconnect_hooks) { - this->log.warning(" %s", it.first.c_str()); - } - } - - if ((this->version() == Version::BB_V4) && (this->character_data.get())) { - this->save_all(); - } - this->log.info("Deleted"); -} - -void Client::update_channel_name() { - string ip_str = this->require_server_state()->format_address_for_channel_name( - this->channel.remote_addr, this->channel.virtual_network_id); - - auto player = this->character(false, false); - if (player) { - string name_str = player->disp.name.decode(this->language()); - this->channel.name = string_printf("C-%" PRIX64 " (%s) @ %s", this->id, name_str.c_str(), ip_str.c_str()); - } else { - this->channel.name = string_printf("C-%" PRIX64 " @ %s", this->id, ip_str.c_str()); - } -} - -void Client::reschedule_save_game_data_event() { - if (this->version() == Version::BB_V4) { - struct timeval tv = usecs_to_timeval(60000000); // 1 minute - event_add(this->save_game_data_event.get(), &tv); - } -} - -void Client::reschedule_ping_and_timeout_events() { - auto s = this->require_server_state(); - struct timeval ping_tv = usecs_to_timeval(s->client_ping_interval_usecs); - event_add(this->send_ping_event.get(), &ping_tv); - struct timeval idle_tv = usecs_to_timeval(s->client_idle_timeout_usecs); - event_add(this->idle_timeout_event.get(), &idle_tv); -} - -void Client::convert_account_to_temporary_if_nte() { - // If the session is a prototype version and the account was created and we - // should use a temporary account instead, delete the permanent account and - // replace it with a temporary account. - auto s = this->require_server_state(); - if (s->use_temp_accounts_for_prototypes && this->login->account_was_created && is_any_nte(this->version())) { - this->log.info("Client is a prototype version and the account was created during this session; converting permanent account to temporary account"); - this->login->account->is_temporary = true; - this->login->account->delete_file(); - this->login->account_was_created = false; - } -} - -shared_ptr Client::require_server_state() const { - auto server = this->server.lock(); - if (!server) { - throw logic_error("server is deleted"); - } - return server->get_state(); -} - -shared_ptr Client::require_lobby() const { - auto l = this->lobby.lock(); - if (!l) { - throw runtime_error("client not in any lobby"); - } - return l; -} - -shared_ptr Client::team() const { - if (!this->login) { - throw logic_error("Client::team called on client with no account"); - } - - if (this->login->account->bb_team_id == 0) { - return nullptr; - } - - auto p = this->character(false); - auto s = this->require_server_state(); - auto team = s->team_index->get_by_id(this->login->account->bb_team_id); - if (!team) { - this->log.info("Account contains a team ID, but the team does not exist; clearing team ID from account"); - this->login->account->bb_team_id = 0; - this->login->account->save(); - return nullptr; - } - - auto member_it = team->members.find(this->login->account->account_id); - if (member_it == team->members.end()) { - this->log.info("Account contains a team ID, but the team does not contain this member; clearing team ID from account"); - this->login->account->bb_team_id = 0; - this->login->account->save(); - return nullptr; - } - - // The team membership is valid, but the player name may be different; update - // the team membership if needed - if (p) { - auto& m = member_it->second; - string name = p->disp.name.decode(this->language()); - if (m.name != name) { - this->log.info("Updating player name in team config"); - s->team_index->update_member_name(this->login->account->account_id, name); - } - } - - return team; -} - -bool Client::evaluate_quest_availability_expression( - shared_ptr expr, - shared_ptr game, - uint8_t event, - uint8_t difficulty, - size_t num_players, - bool v1_present) const { - if (this->login && this->login->account->check_flag(Account::Flag::DISABLE_QUEST_REQUIREMENTS)) { - return true; - } - if (!expr) { - return true; - } - if (game && !game->quest_flag_values) { - throw logic_error("quest flags are missing from game"); - } - auto p = this->character(); - IntegralExpression::Env env = { - .flags = &p->quest_flags.data.at(difficulty), - .challenge_records = &p->challenge_records, - .team = this->team(), - .num_players = num_players, - .event = event, - .v1_present = v1_present, - }; - int64_t ret = expr->evaluate(env); - if (this->log.should_log(LogLevel::INFO)) { - string expr_str = expr->str(); - this->log.info("Evaluated integral expression %s => %s", expr_str.c_str(), ret ? "TRUE" : "FALSE"); - } - return ret; -} - -bool Client::can_see_quest( - shared_ptr q, - shared_ptr game, - uint8_t event, - uint8_t difficulty, - size_t num_players, - bool v1_present) const { - return this->evaluate_quest_availability_expression(q->available_expression, game, event, difficulty, num_players, v1_present); -} - -bool Client::can_play_quest( - shared_ptr q, - shared_ptr game, - uint8_t event, - uint8_t difficulty, - size_t num_players, - bool v1_present) const { - return this->evaluate_quest_availability_expression(q->enabled_expression, game, event, difficulty, num_players, v1_present); -} - -bool Client::can_use_chat_commands() const { - if (!this->login) { - return false; - } - if (this->login->account->check_flag(Account::Flag::ALWAYS_ENABLE_CHAT_COMMANDS)) { - return true; - } - return this->require_server_state()->enable_chat_commands; -} - -void Client::dispatch_save_game_data(evutil_socket_t, short, void* ctx) { - reinterpret_cast(ctx)->save_game_data(); -} - -void Client::save_game_data() { - if (this->version() != Version::BB_V4) { - throw logic_error("save_game_data called for non-BB client"); - } - if (this->character(false)) { - this->save_all(); - } -} - -void Client::dispatch_send_ping(evutil_socket_t, short, void* ctx) { - reinterpret_cast(ctx)->send_ping(); -} - -void Client::send_ping() { - if (!is_patch(this->version())) { - this->log.info("Sending ping command"); - // The game doesn't use this timestamp; we only use it for debugging purposes - be_uint64_t timestamp = now(); - try { - this->channel.send(0x1D, 0x00, ×tamp, sizeof(be_uint64_t)); - } catch (const exception& e) { - this->log.info("Failed to send ping: %s", e.what()); - } - } -} - -void Client::dispatch_idle_timeout(evutil_socket_t, short, void* ctx) { - reinterpret_cast(ctx)->idle_timeout(); -} - -void Client::idle_timeout() { - this->log.info("Idle timeout expired"); - auto s = this->server.lock(); - if (s) { - auto c = this->shared_from_this(); - s->disconnect_client(c); - } else { - this->log.info("Server is deleted; cannot disconnect client"); - } -} - -void Client::suspend_timeouts() { - event_del(this->send_ping_event.get()); - event_del(this->idle_timeout_event.get()); - this->log.info("Timeouts suspended"); -} - -void Client::create_battle_overlay(shared_ptr rules, shared_ptr level_table) { - this->overlay_character_data = make_shared(*this->character(true, false)); - - if (rules->weapon_and_armor_mode != BattleRules::WeaponAndArmorMode::ALLOW) { - this->overlay_character_data->inventory.remove_all_items_of_type(0); - this->overlay_character_data->inventory.remove_all_items_of_type(1); - } - if (rules->mag_mode == BattleRules::MagMode::FORBID_ALL) { - this->overlay_character_data->inventory.remove_all_items_of_type(2); - } - if (rules->tool_mode != BattleRules::ToolMode::ALLOW) { - this->overlay_character_data->inventory.remove_all_items_of_type(3); - } - if (rules->replace_char) { - // TODO: Shouldn't we clear other material usage here? It looks like the - // original code doesn't, but that seems wrong. - this->overlay_character_data->inventory.hp_from_materials = 0; - this->overlay_character_data->inventory.tp_from_materials = 0; - - uint32_t target_level = clamp(rules->char_level, 0, 199); - uint8_t char_class = this->overlay_character_data->disp.visual.char_class; - auto& stats = this->overlay_character_data->disp.stats; - - level_table->reset_to_base(stats, char_class); - level_table->advance_to_level(stats, target_level, char_class); - - stats.esp = 40; - stats.meseta = 300; - } - if (rules->tech_disk_mode == BattleRules::TechDiskMode::LIMIT_LEVEL) { - // TODO: Verify this is what the game actually does. - for (uint8_t tech_num = 0; tech_num < 0x13; tech_num++) { - uint8_t existing_level = this->overlay_character_data->get_technique_level(tech_num); - if ((existing_level != 0xFF) && (existing_level > rules->max_tech_level)) { - this->overlay_character_data->set_technique_level(tech_num, rules->max_tech_level); - } - } - } else if (rules->tech_disk_mode == BattleRules::TechDiskMode::FORBID_ALL) { - for (uint8_t tech_num = 0; tech_num < 0x13; tech_num++) { - this->overlay_character_data->set_technique_level(tech_num, 0xFF); - } - } - if (rules->meseta_mode != BattleRules::MesetaMode::ALLOW) { - this->overlay_character_data->disp.stats.meseta = 0; - } - if (rules->forbid_scape_dolls) { - this->overlay_character_data->inventory.remove_all_items_of_type(3, 9); - } -} - -void Client::create_challenge_overlay(Version version, size_t template_index, shared_ptr level_table) { - auto p = this->character(true, false); - const auto& tpl = get_challenge_template_definition(version, p->disp.visual.class_flags, template_index); - - this->overlay_character_data = make_shared(*p); - auto overlay = this->overlay_character_data; - - for (size_t z = 0; z < overlay->inventory.items.size(); z++) { - auto& i = overlay->inventory.items[z]; - i.present = 0; - i.unknown_a1 = 0; - i.extension_data1 = 0; - i.extension_data2 = 0; - i.flags = 0; - i.data = ItemData(); - } - - overlay->inventory.items[13].extension_data2 = 1; - - level_table->reset_to_base(overlay->disp.stats, overlay->disp.visual.char_class); - level_table->advance_to_level(overlay->disp.stats, tpl.level, overlay->disp.visual.char_class); - - overlay->disp.stats.esp = 40; - overlay->disp.stats.unknown_a3 = 10.0; - overlay->disp.stats.experience = level_table->stats_delta_for_level(overlay->disp.visual.char_class, overlay->disp.stats.level).experience; - overlay->disp.stats.meseta = 0; - overlay->clear_all_material_usage(); - for (size_t z = 0; z < 0x13; z++) { - overlay->set_technique_level(z, 0xFF); - } - - for (size_t z = 0; z < tpl.items.size(); z++) { - auto& inv_item = overlay->inventory.items[z]; - inv_item.present = tpl.items[z].present; - inv_item.unknown_a1 = tpl.items[z].unknown_a1; - inv_item.flags = tpl.items[z].flags; - inv_item.data = tpl.items[z].data; - } - overlay->inventory.num_items = tpl.items.size(); - - for (const auto& tech_level : tpl.tech_levels) { - overlay->set_technique_level(tech_level.tech_num, tech_level.level); - } -} - -void Client::import_blocked_senders(const parray& blocked_senders) { - this->blocked_senders.clear(); - for (size_t z = 0; z < blocked_senders.size(); z++) { - if (blocked_senders[z]) { - this->blocked_senders.emplace(blocked_senders[z]); - } - } -} - -shared_ptr Client::system_file(bool allow_load) { - if (!this->system_data && allow_load) { - this->load_all_files(); - } - return this->system_data; -} - -shared_ptr Client::system_file(bool allow_load) const { - if (!this->system_data.get() && allow_load) { - throw runtime_error("system data is not loaded"); - } - return this->system_data; -} - -shared_ptr Client::character(bool allow_load, bool allow_overlay) { - if (this->overlay_character_data && allow_overlay) { - return this->overlay_character_data; - } - if (!this->character_data && allow_load) { - if ((this->version() == Version::BB_V4) && (this->bb_character_index < 0)) { - throw runtime_error("character index not specified"); - } - this->load_all_files(); - } - return this->character_data; -} - -shared_ptr Client::character(bool allow_load, bool allow_overlay) const { - if (allow_overlay && this->overlay_character_data) { - return this->overlay_character_data; - } - if (!this->character_data && allow_load) { - throw runtime_error("character data is not loaded"); - } - return this->character_data; -} - -shared_ptr Client::guild_card_file(bool allow_load) { - if (!this->guild_card_data && allow_load) { - this->load_all_files(); - } - return this->guild_card_data; -} - -shared_ptr Client::guild_card_file(bool allow_load) const { - if (!this->guild_card_data && allow_load) { - throw runtime_error("account data is not loaded"); - } - return this->guild_card_data; -} - -string Client::system_filename() const { - if (this->version() != Version::BB_V4) { - throw logic_error("non-BB players do not have system data"); - } - if (!this->login || !this->login->bb_license) { - throw logic_error("client is not logged in"); - } - return string_printf("system/players/system_%s.psosys", this->login->bb_license->username.c_str()); -} - -string Client::character_filename(const std::string& bb_username, int8_t index) { - if (bb_username.empty()) { - throw logic_error("non-BB players do not have character data"); - } - if (index < 0) { - throw logic_error("character index is not set"); - } - return string_printf("system/players/player_%s_%hhd.psochar", bb_username.c_str(), index); -} - -string Client::backup_character_filename(uint32_t account_id, size_t index) { - return string_printf("system/players/backup_player_%" PRIu32 "_%zu.psochar", account_id, index); -} - -string Client::character_filename(int8_t index) const { - if (this->version() != Version::BB_V4) { - throw logic_error("non-BB players do not have character data"); - } - if (!this->login || !this->login->bb_license) { - throw logic_error("client is not logged in"); - } - return this->character_filename(this->login->bb_license->username, (index < 0) ? this->bb_character_index : index); -} - -string Client::guild_card_filename() const { - if (this->version() != Version::BB_V4) { - throw logic_error("non-BB players do not have character data"); - } - if (!this->login || !this->login->bb_license) { - throw logic_error("client is not logged in"); - } - return string_printf("system/players/guild_cards_%s.psocard", this->login->bb_license->username.c_str()); -} - -string Client::shared_bank_filename() const { - if (this->version() != Version::BB_V4) { - throw logic_error("non-BB players do not have character data"); - } - if (!this->login || !this->login->bb_license) { - throw logic_error("client is not logged in"); - } - return string_printf("system/players/shared_bank_%s.psobank", this->login->bb_license->username.c_str()); -} - -string Client::legacy_account_filename() const { - if (this->version() != Version::BB_V4) { - throw logic_error("non-BB players do not have character data"); - } - if (!this->login || !this->login->bb_license) { - throw logic_error("client is not logged in"); - } - return string_printf("system/players/account_%s.nsa", this->login->bb_license->username.c_str()); -} - -string Client::legacy_player_filename() const { - if (this->version() != Version::BB_V4) { - throw logic_error("non-BB players do not have character data"); - } - if (!this->login || !this->login->bb_license) { - throw logic_error("client is not logged in"); - } - if (this->bb_character_index < 0) { - throw logic_error("character index is not set"); - } - return string_printf( - "system/players/player_%s_%hhd.nsc", - this->login->bb_license->username.c_str(), - static_cast(this->bb_character_index + 1)); -} - -void Client::create_character_file( - uint32_t guild_card_number, - uint8_t language, - const PlayerDispDataBBPreview& preview, - shared_ptr level_table) { - this->character_data = PSOBBCharacterFile::create_from_preview(guild_card_number, language, preview, level_table); - this->save_character_file(); -} - -void Client::load_all_files() { - if (this->version() != Version::BB_V4) { - this->system_data = make_shared(); - this->character_data = make_shared(); - this->guild_card_data = make_shared(); - return; - } - if (!this->login || !this->login->bb_license) { - throw logic_error("cannot load BB player data until client is logged in"); - } - - this->system_data.reset(); - this->character_data.reset(); - this->guild_card_data.reset(); - - auto files_manager = this->require_server_state()->player_files_manager; - - string sys_filename = this->system_filename(); - this->system_data = files_manager->get_system(sys_filename); - if (this->system_data) { - player_data_log.info("Using loaded system file %s", sys_filename.c_str()); - } else if (isfile(sys_filename)) { - this->system_data = make_shared(load_object_file(sys_filename, true)); - files_manager->set_system(sys_filename, this->system_data); - player_data_log.info("Loaded system data from %s", sys_filename.c_str()); - } else { - player_data_log.info("System file is missing: %s", sys_filename.c_str()); - } - - if (this->bb_character_index >= 0) { - string char_filename = this->character_filename(); - this->character_data = files_manager->get_character(char_filename); - if (this->character_data) { - player_data_log.info("Using loaded character file %s", char_filename.c_str()); - } else if (isfile(char_filename)) { - auto f = fopen_unique(char_filename, "rb"); - auto header = freadx(f.get()); - if (header.size != 0x399C) { - throw runtime_error("incorrect size in character file header"); - } - if (header.command != 0x00E7) { - throw runtime_error("incorrect command in character file header"); - } - if (header.flag != 0x00000000) { - throw runtime_error("incorrect flag in character file header"); - } - static_assert(sizeof(PSOBBCharacterFile) + sizeof(PSOBBFullSystemFile) == 0x3994, ".psochar size is incorrect"); - this->character_data = make_shared(freadx(f.get())); - files_manager->set_character(char_filename, this->character_data); - player_data_log.info("Loaded character data from %s", char_filename.c_str()); - - // If there was no .psosys file, load the system file from the .psochar - // file instead - if (!this->system_data) { - this->system_data = make_shared(freadx(f.get())); - files_manager->set_system(sys_filename, this->system_data); - player_data_log.info("Loaded system data from %s", char_filename.c_str()); - } - - this->update_character_data_after_load(this->character_data); - this->system_data->base.language = this->language(); - - } else { - player_data_log.info("Character file is missing: %s", char_filename.c_str()); - } - } - - string card_filename = this->guild_card_filename(); - this->guild_card_data = files_manager->get_guild_card(card_filename); - if (this->guild_card_data) { - player_data_log.info("Using loaded Guild Card file %s", card_filename.c_str()); - } else if (isfile(card_filename)) { - this->guild_card_data = make_shared(load_object_file(card_filename)); - files_manager->set_guild_card(card_filename, this->guild_card_data); - player_data_log.info("Loaded Guild Card data from %s", card_filename.c_str()); - } else { - player_data_log.info("Guild Card file is missing: %s", card_filename.c_str()); - } - - // If any of the above files were missing, try to load from .nsa/.nsc files instead - if (!this->system_data || (!this->character_data && (this->bb_character_index >= 0)) || !this->guild_card_data) { - string nsa_filename = this->legacy_account_filename(); - shared_ptr nsa_data; - if (isfile(nsa_filename)) { - nsa_data = make_shared(load_object_file(nsa_filename)); - if (!nsa_data->signature.eq(LegacySavedAccountDataBB::SIGNATURE)) { - throw runtime_error("account data header is incorrect"); - } - if (!this->system_data) { - this->system_data = make_shared(nsa_data->system_file.base); - files_manager->set_system(sys_filename, this->system_data); - player_data_log.info("Loaded legacy system data from %s", nsa_filename.c_str()); - } - if (!this->guild_card_data) { - this->guild_card_data = make_shared(nsa_data->guild_card_file); - files_manager->set_guild_card(card_filename, this->guild_card_data); - player_data_log.info("Loaded legacy Guild Card data from %s", nsa_filename.c_str()); - } - } - - if (!this->system_data) { - this->system_data = make_shared(); - files_manager->set_system(sys_filename, this->system_data); - player_data_log.info("Created new system data"); - } - if (!this->guild_card_data) { - this->guild_card_data = make_shared(); - files_manager->set_guild_card(card_filename, this->guild_card_data); - player_data_log.info("Created new Guild Card data"); - } - - if (!this->character_data && (this->bb_character_index >= 0)) { - string nsc_filename = this->legacy_player_filename(); - auto nsc_data = load_object_file(nsc_filename); - if (nsc_data.signature == LegacySavedPlayerDataBB::SIGNATURE_V0) { - nsc_data.signature = LegacySavedPlayerDataBB::SIGNATURE_V0; - nsc_data.unused.clear(); - nsc_data.battle_records.place_counts.clear(0); - nsc_data.battle_records.disconnect_count = 0; - nsc_data.battle_records.unknown_a1.clear(0); - } else if (nsc_data.signature != LegacySavedPlayerDataBB::SIGNATURE_V1) { - throw runtime_error("legacy player data has incorrect signature"); - } - - this->character_data = make_shared(); - files_manager->set_character(this->character_filename(), this->character_data); - this->character_data->inventory = nsc_data.inventory; - this->character_data->disp = nsc_data.disp; - this->character_data->play_time_seconds = 0; - this->character_data->quest_flags = nsc_data.quest_flags; - this->character_data->death_count = nsc_data.death_count; - this->character_data->bank = nsc_data.bank; - this->character_data->guild_card.guild_card_number = this->login->account->account_id; - this->character_data->guild_card.name = nsc_data.disp.name; - this->character_data->guild_card.description = nsc_data.guild_card_description; - this->character_data->guild_card.present = 1; - this->character_data->guild_card.language = nsc_data.inventory.language; - this->character_data->guild_card.section_id = nsc_data.disp.visual.section_id; - this->character_data->guild_card.char_class = nsc_data.disp.visual.char_class; - this->character_data->auto_reply = nsc_data.auto_reply; - this->character_data->info_board = nsc_data.info_board; - this->character_data->battle_records = nsc_data.battle_records; - this->character_data->challenge_records = nsc_data.challenge_records; - this->character_data->tech_menu_shortcut_entries = nsc_data.tech_menu_shortcut_entries; - this->character_data->quest_counters = nsc_data.quest_counters; - if (nsa_data) { - this->character_data->option_flags = nsa_data->option_flags; - this->character_data->symbol_chats = nsa_data->symbol_chats; - this->character_data->shortcuts = nsa_data->shortcuts; - player_data_log.info("Loaded legacy player data from %s and %s", nsa_filename.c_str(), nsc_filename.c_str()); - } else { - player_data_log.info("Loaded legacy player data from %s", nsc_filename.c_str()); - } - this->update_character_data_after_load(this->character_data); - } - } - - this->blocked_senders.clear(); - for (size_t z = 0; z < this->guild_card_data->blocked.size(); z++) { - if (this->guild_card_data->blocked[z].present) { - this->blocked_senders.emplace(this->guild_card_data->blocked[z].guild_card_number); - } - } - - if (this->character_data) { - // Clear legacy play_time field - this->character_data->disp.name.clear_after_bytes(0x18); - this->login->account->auto_reply_message = this->character_data->auto_reply.decode(); - this->login->account->save(); - this->last_play_time_update = now(); - } -} - -void Client::update_character_data_after_load(shared_ptr charfile) { - charfile->import_tethealla_material_usage(this->require_server_state()->level_table(this->version())); - - uint8_t lang = this->language(); - player_data_log.info("Overriding language fields in save files with %02hhX (%c)", lang, char_for_language_code(lang)); - charfile->inventory.language = lang; - charfile->guild_card.language = lang; -} - -void Client::save_all() { - if (this->system_data) { - this->save_system_file(); - } - if (this->character_data) { - this->save_character_file(); - } - if (this->guild_card_data) { - this->save_guild_card_file(); - } - if (this->external_bank) { - string filename = this->shared_bank_filename(); - save_object_file(filename, *this->external_bank); - player_data_log.info("Saved shared bank file %s", filename.c_str()); - } - if (this->external_bank_character) { - this->save_character_file( - this->character_filename(this->external_bank_character_index), - this->system_data, - this->external_bank_character); - } -} - -void Client::save_system_file() const { - if (!this->system_data) { - throw logic_error("no system file loaded"); - } - string filename = this->system_filename(); - save_object_file(filename, *this->system_data); - player_data_log.info("Saved system file %s", filename.c_str()); -} - -void Client::save_character_file( - const string& filename, - shared_ptr system, - shared_ptr character) { - auto f = fopen_unique(filename, "wb"); - PSOCommandHeaderBB header = {sizeof(PSOCommandHeaderBB) + sizeof(PSOBBCharacterFile) + sizeof(PSOBBBaseSystemFile) + sizeof(PSOBBTeamMembership), 0x00E7, 0x00000000}; - fwritex(f.get(), header); - fwritex(f.get(), *character); - fwritex(f.get(), *system); - // TODO: Technically, we should write the actual team membership struct to the - // file here, but that would cause Client to depend on Account, which - // it currently does not. This data doesn't matter at all for correctness - // within newserv, since it ignores this data entirely and instead generates - // the membership struct from the team ID in the Account and the team's state. - // So, writing correct data here would mostly be for compatibility with other - // PSO servers. But if the other server is newserv, then this data would be - // used anyway, and if it's not, then it would presumably have a different set - // of teams with a different set of team IDs anyway, so the membership struct - // here would be useless either way. - static const PSOBBTeamMembership empty_membership; - fwritex(f.get(), empty_membership); - player_data_log.info("Saved character file %s", filename.c_str()); -} - -void Client::save_character_file() { - if (!this->system_data.get()) { - throw logic_error("no system file loaded"); - } - if (!this->character_data.get()) { - throw logic_error("no character file loaded"); - } - if (this->should_update_play_time) { - // This is slightly inaccurate, since fractions of a second are truncated - // off each time we save. I'm lazy, so insert shrug emoji here. - uint64_t t = now(); - uint64_t seconds = (t - this->last_play_time_update) / 1000000; - this->character_data->play_time_seconds += seconds; - player_data_log.info("Added %" PRIu64 " seconds to play time", seconds); - this->last_play_time_update = t; - } - - this->save_character_file(this->character_filename(), this->system_data, this->character_data); -} - -void Client::save_guild_card_file() const { - if (!this->guild_card_data.get()) { - throw logic_error("no Guild Card file loaded"); - } - string filename = this->guild_card_filename(); - save_object_file(filename, *this->guild_card_data); - player_data_log.info("Saved Guild Card file %s", filename.c_str()); -} - -void Client::load_backup_character(uint32_t account_id, size_t index) { - string filename = this->backup_character_filename(account_id, index); - auto f = fopen_unique(filename, "rb"); - auto header = freadx(f.get()); - if (header.size != 0x399C) { - throw runtime_error("incorrect size in character file header"); - } - if (header.command != 0x00E7) { - throw runtime_error("incorrect command in character file header"); - } - if (header.flag != 0x00000000) { - throw runtime_error("incorrect flag in character file header"); - } - this->character_data = make_shared(freadx(f.get())); - this->update_character_data_after_load(this->character_data); - this->v1_v2_last_reported_disp.reset(); -} - -void Client::save_and_unload_character() { - if (this->character_data) { - this->save_character_file(); - this->character_data.reset(); - this->log.info("Unloaded character"); - } -} - -PlayerBank200& Client::current_bank() { - if (this->external_bank) { - return *this->external_bank; - } else if (this->external_bank_character) { - return this->external_bank_character->bank; - } - return this->character()->bank; -} - -std::shared_ptr Client::current_bank_character() { - return this->external_bank_character ? this->external_bank_character : this->character(); -} - -void Client::use_default_bank() { - if (this->external_bank) { - string filename = this->shared_bank_filename(); - save_object_file(filename, *this->external_bank); - this->external_bank.reset(); - player_data_log.info("Detached shared bank %s", filename.c_str()); - } - if (this->external_bank_character) { - string filename = this->character_filename(this->external_bank_character_index); - this->save_character_file(filename, this->system_data, this->external_bank_character); - this->external_bank_character.reset(); - player_data_log.info("Detached character %s from bank", filename.c_str()); - } -} - -bool Client::use_shared_bank() { - this->use_default_bank(); - - string filename = this->shared_bank_filename(); - auto files_manager = this->require_server_state()->player_files_manager; - this->external_bank = files_manager->get_bank(filename); - if (this->external_bank) { - player_data_log.info("Using loaded shared bank %s", filename.c_str()); - return true; - } else if (isfile(filename)) { - this->external_bank = make_shared(load_object_file(filename)); - files_manager->set_bank(filename, this->external_bank); - player_data_log.info("Loaded shared bank %s", filename.c_str()); - return true; - } else { - this->external_bank = make_shared(); - files_manager->set_bank(filename, this->external_bank); - player_data_log.info("Created shared bank for %s", filename.c_str()); - return false; - } -} - -void Client::use_character_bank(int8_t index) { - this->use_default_bank(); - if (index != this->bb_character_index) { - auto files_manager = this->require_server_state()->player_files_manager; - - string filename = this->character_filename(index); - this->external_bank_character = files_manager->get_character(filename); - if (this->external_bank_character) { - this->external_bank_character_index = index; - player_data_log.info("Using loaded character file %s for external bank", filename.c_str()); - } else if (isfile(filename)) { - auto f = fopen_unique(filename, "rb"); - auto header = freadx(f.get()); - if (header.size != 0x399C) { - throw runtime_error("incorrect size in character file header"); - } - if (header.command != 0x00E7) { - throw runtime_error("incorrect command in character file header"); - } - if (header.flag != 0x00000000) { - throw runtime_error("incorrect flag in character file header"); - } - this->external_bank_character = make_shared(freadx(f.get())); - this->update_character_data_after_load(this->external_bank_character); - this->external_bank_character_index = index; - files_manager->set_character(filename, this->external_bank_character); - player_data_log.info("Loaded character data from %s for external bank", filename.c_str()); - } else { - throw runtime_error("character does not exist"); - } - } -} - -void Client::print_inventory(FILE* stream) const { - auto s = this->require_server_state(); - auto p = this->character(); - fprintf(stream, "[PlayerInventory] Meseta: %" PRIu32 "\n", p->disp.stats.meseta.load()); - fprintf(stream, "[PlayerInventory] %hhu items\n", p->inventory.num_items); - for (size_t x = 0; x < p->inventory.num_items; x++) { - const auto& item = p->inventory.items[x]; - auto hex = item.data.hex(); - auto name = s->describe_item(this->version(), item.data, false); - fprintf(stream, "[PlayerInventory] %2zu: [+%08" PRIX32 "] %s (%s)\n", x, item.flags.load(), hex.c_str(), name.c_str()); - } -} - -void Client::print_bank(FILE* stream) const { - auto s = this->require_server_state(); - auto p = this->character(); - fprintf(stream, "[PlayerBank] Meseta: %" PRIu32 "\n", p->bank.meseta.load()); - fprintf(stream, "[PlayerBank] %" PRIu32 " items\n", p->bank.num_items.load()); - for (size_t x = 0; x < p->bank.num_items; x++) { - const auto& item = p->bank.items[x]; - const char* present_token = item.present ? "" : " (missing present flag)"; - auto hex = item.data.hex(); - auto name = s->describe_item(this->version(), item.data, false); - fprintf(stream, "[PlayerBank] %3zu: %s (%s) (x%hu)%s\n", x, hex.c_str(), name.c_str(), item.amount.load(), present_token); - } -} +#include "Client.hh" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "IPStackSimulator.hh" +#include "Loggers.hh" +#include "SendCommands.hh" +#include "Server.hh" +#include "Version.hh" + +using namespace std; + +const uint64_t CLIENT_CONFIG_MAGIC = 0x8399AC32; + +static atomic next_id(1); + +void Client::Config::set_flags_for_version(Version version, int64_t sub_version) { + this->set_flag(Flag::PROXY_CHAT_COMMANDS_ENABLED); + + // BB shares some sub_version values with GC Episode 3, so we handle it + // separately first. + if (version == Version::BB_V4) { + this->set_flag(Flag::NO_D6); + this->set_flag(Flag::SAVE_ENABLED); + this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH); + return; + } + + switch (sub_version) { + case -1: // Initial check (before sub_version recognition) + switch (version) { + case Version::PC_PATCH: + case Version::BB_PATCH: + this->set_flag(Flag::NO_D6); + this->set_flag(Flag::NO_SEND_FUNCTION_CALL); + break; + case Version::DC_NTE: + case Version::DC_V1_11_2000_PROTOTYPE: + case Version::DC_V1: + this->set_flag(Flag::NO_D6); + this->set_flag(Flag::NO_SEND_FUNCTION_CALL); + break; + case Version::DC_V2: + this->set_flag(Flag::NO_D6); + this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH); + break; + case Version::PC_NTE: + case Version::PC_V2: + this->set_flag(Flag::NO_D6); + this->set_flag(Flag::SEND_FUNCTION_CALL_CHECKSUM_ONLY); + this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH); + break; + case Version::GC_NTE: + case Version::GC_V3: + case Version::GC_EP3_NTE: + case Version::GC_EP3: + break; + case Version::XB_V3: + // TODO: Do all versions of XB need this flag? US does, at least. + this->set_flag(Flag::NO_D6_AFTER_LOBBY); + this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH); + break; + default: + throw logic_error("invalid game version"); + } + break; + + case 0x20: // DCNTE, possibly also DCv1 JP + case 0x21: // DCv1 US + this->set_flag(Flag::NO_D6); + this->set_flag(Flag::NO_SEND_FUNCTION_CALL); + break; + case 0x22: // DCv1 EU 50Hz (presumably) + case 0x23: // DCv1 EU 60Hz (presumably) + this->set_flag(Flag::NO_D6); + this->set_flag(Flag::NO_SEND_FUNCTION_CALL); + break; + case 0x25: // DCv2 JP + case 0x26: // DCv2 US + case 0x27: // DCv2 EU 50Hz (presumably) + case 0x28: // DCv2 EU 60Hz (presumably) + this->set_flag(Flag::NO_D6); + this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH); + break; + case 0x29: // PC + this->set_flag(Flag::NO_D6); + this->set_flag(Flag::SEND_FUNCTION_CALL_CHECKSUM_ONLY); + this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH); + break; + case 0x30: // GC Ep1&2 GameJam demo, GC Ep1&2 Trial Edition, GC Ep1&2 JP v1.2, at least one version of XB + case 0x31: // GC Ep1&2 US v1.0, GC US v1.1, XB US + break; + case 0x32: // GC Ep1&2 EU 50Hz + case 0x33: // GC Ep1&2 EU 60Hz + case 0x34: // GC Ep1&2 JP v1.3 + this->set_flag(Flag::NO_D6_AFTER_LOBBY); + break; + case 0x35: // GC Ep1&2 JP v1.4 (Plus) + this->set_flag(Flag::NO_D6_AFTER_LOBBY); + this->set_flag(Flag::ENCRYPTED_SEND_FUNCTION_CALL); + this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH); + break; + case 0x3A: // GC Ep1&2 US v1.2 (Plus) GMK edition + this->set_flag(Flag::IS_CLIENT_CUSTOMIZATION); + [[fallthrough]]; + case 0x36: // GC Ep1&2 US v1.2 (Plus) + case 0x39: // GC Ep1&2 JP v1.5 (Plus) + this->set_flag(Flag::NO_D6_AFTER_LOBBY); + this->set_flag(Flag::NO_SEND_FUNCTION_CALL); + break; + case 0x40: // GC Ep3 JP and Trial Edition (and BB) + this->set_flag(Flag::NO_D6_AFTER_LOBBY); + this->set_flag(Flag::ENCRYPTED_SEND_FUNCTION_CALL); + this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH); + // sub_version can't be used to tell JP final and Trial Edition apart; we + // instead look at header.flag in the 61 command and set the version then. + break; + case 0x41: // GC Ep3 US (and BB) + this->set_flag(Flag::NO_D6_AFTER_LOBBY); + this->set_flag(Flag::USE_OVERFLOW_FOR_SEND_FUNCTION_CALL); + this->set_flag(Flag::SEND_FUNCTION_CALL_NO_CACHE_PATCH); + break; + case 0x42: // GC Ep3 EU 50Hz + case 0x43: // GC Ep3 EU 60Hz + this->set_flag(Flag::NO_D6_AFTER_LOBBY); + this->set_flag(Flag::NO_SEND_FUNCTION_CALL); + break; + default: + throw runtime_error(string_printf("unknown sub_version %" PRIX64, sub_version)); + } +} + +Client::ItemDropNotificationMode Client::Config::get_drop_notification_mode() const { + uint8_t mode_s = (this->check_flag(Flag::ITEM_DROP_NOTIFICATIONS_1) ? 1 : 0) | + (this->check_flag(Flag::ITEM_DROP_NOTIFICATIONS_2) ? 2 : 0); + return static_cast(mode_s); +} + +void Client::Config::set_drop_notification_mode(ItemDropNotificationMode new_mode) { + uint8_t mode_s = static_cast(new_mode); + if (mode_s & 1) { + this->set_flag(Client::Flag::ITEM_DROP_NOTIFICATIONS_1); + } else { + this->clear_flag(Client::Flag::ITEM_DROP_NOTIFICATIONS_1); + } + if (mode_s & 2) { + this->set_flag(Client::Flag::ITEM_DROP_NOTIFICATIONS_2); + } else { + this->clear_flag(Client::Flag::ITEM_DROP_NOTIFICATIONS_2); + } +} + +bool Client::Config::should_update_vs(const Config& other) const { + constexpr uint64_t mask = static_cast(Flag::CLIENT_SIDE_MASK); + return ((this->enabled_flags ^ other.enabled_flags) & mask) || + (this->specific_version != other.specific_version) || + (this->override_random_seed != other.override_random_seed) || + (this->override_section_id != other.override_section_id) || + (this->override_lobby_event != other.override_lobby_event) || + (this->override_lobby_number != other.override_lobby_number) || + (this->proxy_destination_address != other.proxy_destination_address) || + (this->proxy_destination_port != other.proxy_destination_port); +} + +Client::Client( + shared_ptr server, + struct bufferevent* bev, + uint64_t virtual_network_id, + Version version, + ServerBehavior server_behavior) + : server(server), + id(next_id++), + log(string_printf("[C-%" PRIX64 "] ", this->id), client_log.min_level), + channel(bev, virtual_network_id, version, 1, nullptr, nullptr, this, "", TerminalFormat::FG_YELLOW, TerminalFormat::FG_GREEN), + server_behavior(server_behavior), + should_disconnect(false), + should_send_to_lobby_server(false), + should_send_to_proxy_server(false), + bb_connection_phase(0xFF), + ping_start_time(0), + sub_version(-1), + x(0.0f), + z(0.0f), + floor(0), + lobby_client_id(0), + lobby_arrow_color(0), + preferred_lobby_id(-1), + save_game_data_event( + event_new( + bufferevent_get_base(bev), -1, EV_TIMEOUT | EV_PERSIST, + &Client::dispatch_save_game_data, this), + event_free), + send_ping_event( + event_new( + bufferevent_get_base(bev), -1, EV_TIMEOUT, + &Client::dispatch_send_ping, this), + event_free), + idle_timeout_event( + event_new( + bufferevent_get_base(bev), -1, EV_TIMEOUT, + &Client::dispatch_idle_timeout, this), + event_free), + card_battle_table_number(-1), + card_battle_table_seat_number(0), + card_battle_table_seat_state(0), + last_game_info_requested(0), + should_update_play_time(false), + bb_character_index(-1), + next_exp_value(0), + can_chat(true), + dol_base_addr(0), + external_bank_character_index(-1), + last_play_time_update(0) { + this->update_channel_name(); + + this->config.set_flags_for_version(version, -1); + auto s = server->get_state(); + if (is_v1_or_v2(this->version()) ? s->default_rare_notifs_enabled_v1_v2 : s->default_rare_notifs_enabled_v3_v4) { + this->config.set_drop_notification_mode(ItemDropNotificationMode::RARES_ONLY); + } + this->config.specific_version = default_specific_version_for_version(version, -1); + + memset(&this->next_connection_addr, 0, sizeof(this->next_connection_addr)); + + this->reschedule_save_game_data_event(); + this->reschedule_ping_and_timeout_events(); + + // Don't print data sent to patch clients to the logs. The patch server + // protocol is fully understood and data logs for patch clients are generally + // more annoying than helpful at this point. + if ((s->hide_download_commands) && + ((this->channel.version == Version::PC_PATCH) || (this->channel.version == Version::BB_PATCH))) { + this->channel.terminal_recv_color = TerminalFormat::END; + this->channel.terminal_send_color = TerminalFormat::END; + } + + this->log.info("Created"); +} + +Client::~Client() { + if (!this->disconnect_hooks.empty()) { + this->log.warning("Disconnect hooks pending at client destruction time:"); + for (const auto& it : this->disconnect_hooks) { + this->log.warning(" %s", it.first.c_str()); + } + } + + if ((this->version() == Version::BB_V4) && (this->character_data.get())) { + this->save_all(); + } + this->log.info("Deleted"); +} + +void Client::update_channel_name() { + string ip_str = this->require_server_state()->format_address_for_channel_name( + this->channel.remote_addr, this->channel.virtual_network_id); + + auto player = this->character(false, false); + if (player) { + string name_str = player->disp.name.decode(this->language()); + this->channel.name = string_printf("C-%" PRIX64 " (%s) @ %s", this->id, name_str.c_str(), ip_str.c_str()); + } else { + this->channel.name = string_printf("C-%" PRIX64 " @ %s", this->id, ip_str.c_str()); + } +} + +void Client::reschedule_save_game_data_event() { + if (this->version() == Version::BB_V4) { + struct timeval tv = usecs_to_timeval(60000000); // 1 minute + event_add(this->save_game_data_event.get(), &tv); + } +} + +void Client::reschedule_ping_and_timeout_events() { + auto s = this->require_server_state(); + struct timeval ping_tv = usecs_to_timeval(s->client_ping_interval_usecs); + event_add(this->send_ping_event.get(), &ping_tv); + struct timeval idle_tv = usecs_to_timeval(s->client_idle_timeout_usecs); + event_add(this->idle_timeout_event.get(), &idle_tv); +} + +void Client::convert_account_to_temporary_if_nte() { + // If the session is a prototype version and the account was created and we + // should use a temporary account instead, delete the permanent account and + // replace it with a temporary account. + auto s = this->require_server_state(); + if (s->use_temp_accounts_for_prototypes && this->login->account_was_created && is_any_nte(this->version())) { + this->log.info("Client is a prototype version and the account was created during this session; converting permanent account to temporary account"); + this->login->account->is_temporary = true; + this->login->account->delete_file(); + this->login->account_was_created = false; + } +} + +shared_ptr Client::require_server_state() const { + auto server = this->server.lock(); + if (!server) { + throw logic_error("server is deleted"); + } + return server->get_state(); +} + +shared_ptr Client::require_lobby() const { + auto l = this->lobby.lock(); + if (!l) { + throw runtime_error("client not in any lobby"); + } + return l; +} + +shared_ptr Client::team() const { + if (!this->login) { + throw logic_error("Client::team called on client with no account"); + } + + if (this->login->account->bb_team_id == 0) { + return nullptr; + } + + auto p = this->character(false); + auto s = this->require_server_state(); + auto team = s->team_index->get_by_id(this->login->account->bb_team_id); + if (!team) { + this->log.info("Account contains a team ID, but the team does not exist; clearing team ID from account"); + this->login->account->bb_team_id = 0; + this->login->account->save(); + return nullptr; + } + + auto member_it = team->members.find(this->login->account->account_id); + if (member_it == team->members.end()) { + this->log.info("Account contains a team ID, but the team does not contain this member; clearing team ID from account"); + this->login->account->bb_team_id = 0; + this->login->account->save(); + return nullptr; + } + + // The team membership is valid, but the player name may be different; update + // the team membership if needed + if (p) { + auto& m = member_it->second; + string name = p->disp.name.decode(this->language()); + if (m.name != name) { + this->log.info("Updating player name in team config"); + s->team_index->update_member_name(this->login->account->account_id, name); + } + } + + return team; +} + +bool Client::evaluate_quest_availability_expression( + shared_ptr expr, + shared_ptr game, + uint8_t event, + uint8_t difficulty, + size_t num_players, + bool v1_present) const { + if (this->login && this->login->account->check_flag(Account::Flag::DISABLE_QUEST_REQUIREMENTS)) { + return true; + } + if (!expr) { + return true; + } + if (game && !game->quest_flag_values) { + throw logic_error("quest flags are missing from game"); + } + auto p = this->character(); + IntegralExpression::Env env = { + .flags = &p->quest_flags.data.at(difficulty), + .challenge_records = &p->challenge_records, + .team = this->team(), + .num_players = num_players, + .event = event, + .v1_present = v1_present, + }; + int64_t ret = expr->evaluate(env); + if (this->log.should_log(LogLevel::INFO)) { + string expr_str = expr->str(); + this->log.info("Evaluated integral expression %s => %s", expr_str.c_str(), ret ? "TRUE" : "FALSE"); + } + return ret; +} + +bool Client::can_see_quest( + shared_ptr q, + shared_ptr game, + uint8_t event, + uint8_t difficulty, + size_t num_players, + bool v1_present) const { + return this->evaluate_quest_availability_expression(q->available_expression, game, event, difficulty, num_players, v1_present); +} + +bool Client::can_play_quest( + shared_ptr q, + shared_ptr game, + uint8_t event, + uint8_t difficulty, + size_t num_players, + bool v1_present) const { + return this->evaluate_quest_availability_expression(q->enabled_expression, game, event, difficulty, num_players, v1_present); +} + +bool Client::can_use_chat_commands() const { + if (!this->login) { + return false; + } + if (this->login->account->check_flag(Account::Flag::ALWAYS_ENABLE_CHAT_COMMANDS)) { + return true; + } + return this->require_server_state()->enable_chat_commands; +} + +void Client::dispatch_save_game_data(evutil_socket_t, short, void* ctx) { + reinterpret_cast(ctx)->save_game_data(); +} + +void Client::save_game_data() { + if (this->version() != Version::BB_V4) { + throw logic_error("save_game_data called for non-BB client"); + } + if (this->character(false)) { + this->save_all(); + } +} + +void Client::dispatch_send_ping(evutil_socket_t, short, void* ctx) { + reinterpret_cast(ctx)->send_ping(); +} + +void Client::send_ping() { + if (!is_patch(this->version())) { + this->log.info("Sending ping command"); + // The game doesn't use this timestamp; we only use it for debugging purposes + be_uint64_t timestamp = now(); + try { + this->channel.send(0x1D, 0x00, ×tamp, sizeof(be_uint64_t)); + } catch (const exception& e) { + this->log.info("Failed to send ping: %s", e.what()); + } + } +} + +void Client::dispatch_idle_timeout(evutil_socket_t, short, void* ctx) { + reinterpret_cast(ctx)->idle_timeout(); +} + +void Client::idle_timeout() { + this->log.info("Idle timeout expired"); + auto s = this->server.lock(); + if (s) { + auto c = this->shared_from_this(); + s->disconnect_client(c); + } else { + this->log.info("Server is deleted; cannot disconnect client"); + } +} + +void Client::suspend_timeouts() { + event_del(this->send_ping_event.get()); + event_del(this->idle_timeout_event.get()); + this->log.info("Timeouts suspended"); +} + +void Client::create_battle_overlay(shared_ptr rules, shared_ptr level_table) { + this->overlay_character_data = make_shared(*this->character(true, false)); + + if (rules->weapon_and_armor_mode != BattleRules::WeaponAndArmorMode::ALLOW) { + this->overlay_character_data->inventory.remove_all_items_of_type(0); + this->overlay_character_data->inventory.remove_all_items_of_type(1); + } + if (rules->mag_mode == BattleRules::MagMode::FORBID_ALL) { + this->overlay_character_data->inventory.remove_all_items_of_type(2); + } + if (rules->tool_mode != BattleRules::ToolMode::ALLOW) { + this->overlay_character_data->inventory.remove_all_items_of_type(3); + } + if (rules->replace_char) { + // TODO: Shouldn't we clear other material usage here? It looks like the + // original code doesn't, but that seems wrong. + this->overlay_character_data->inventory.hp_from_materials = 0; + this->overlay_character_data->inventory.tp_from_materials = 0; + + uint32_t target_level = clamp(rules->char_level, 0, 199); + uint8_t char_class = this->overlay_character_data->disp.visual.char_class; + auto& stats = this->overlay_character_data->disp.stats; + + level_table->reset_to_base(stats, char_class); + level_table->advance_to_level(stats, target_level, char_class); + + stats.esp = 40; + stats.meseta = 300; + } + if (rules->tech_disk_mode == BattleRules::TechDiskMode::LIMIT_LEVEL) { + // TODO: Verify this is what the game actually does. + for (uint8_t tech_num = 0; tech_num < 0x13; tech_num++) { + uint8_t existing_level = this->overlay_character_data->get_technique_level(tech_num); + if ((existing_level != 0xFF) && (existing_level > rules->max_tech_level)) { + this->overlay_character_data->set_technique_level(tech_num, rules->max_tech_level); + } + } + } else if (rules->tech_disk_mode == BattleRules::TechDiskMode::FORBID_ALL) { + for (uint8_t tech_num = 0; tech_num < 0x13; tech_num++) { + this->overlay_character_data->set_technique_level(tech_num, 0xFF); + } + } + if (rules->meseta_mode != BattleRules::MesetaMode::ALLOW) { + this->overlay_character_data->disp.stats.meseta = 0; + } + if (rules->forbid_scape_dolls) { + this->overlay_character_data->inventory.remove_all_items_of_type(3, 9); + } +} + +void Client::create_challenge_overlay(Version version, size_t template_index, shared_ptr level_table) { + auto p = this->character(true, false); + const auto& tpl = get_challenge_template_definition(version, p->disp.visual.class_flags, template_index); + + this->overlay_character_data = make_shared(*p); + auto overlay = this->overlay_character_data; + + for (size_t z = 0; z < overlay->inventory.items.size(); z++) { + auto& i = overlay->inventory.items[z]; + i.present = 0; + i.unknown_a1 = 0; + i.extension_data1 = 0; + i.extension_data2 = 0; + i.flags = 0; + i.data = ItemData(); + } + + overlay->inventory.items[13].extension_data2 = 1; + + level_table->reset_to_base(overlay->disp.stats, overlay->disp.visual.char_class); + level_table->advance_to_level(overlay->disp.stats, tpl.level, overlay->disp.visual.char_class); + + overlay->disp.stats.esp = 40; + overlay->disp.stats.unknown_a3 = 10.0; + overlay->disp.stats.experience = level_table->stats_delta_for_level(overlay->disp.visual.char_class, overlay->disp.stats.level).experience; + overlay->disp.stats.meseta = 0; + overlay->clear_all_material_usage(); + for (size_t z = 0; z < 0x13; z++) { + overlay->set_technique_level(z, 0xFF); + } + + for (size_t z = 0; z < tpl.items.size(); z++) { + auto& inv_item = overlay->inventory.items[z]; + inv_item.present = tpl.items[z].present; + inv_item.unknown_a1 = tpl.items[z].unknown_a1; + inv_item.flags = tpl.items[z].flags; + inv_item.data = tpl.items[z].data; + } + overlay->inventory.num_items = tpl.items.size(); + + for (const auto& tech_level : tpl.tech_levels) { + overlay->set_technique_level(tech_level.tech_num, tech_level.level); + } +} + +void Client::import_blocked_senders(const parray& blocked_senders) { + this->blocked_senders.clear(); + for (size_t z = 0; z < blocked_senders.size(); z++) { + if (blocked_senders[z]) { + this->blocked_senders.emplace(blocked_senders[z]); + } + } +} + +shared_ptr Client::system_file(bool allow_load) { + if (!this->system_data && allow_load) { + this->load_all_files(); + } + return this->system_data; +} + +shared_ptr Client::system_file(bool allow_load) const { + if (!this->system_data.get() && allow_load) { + throw runtime_error("system data is not loaded"); + } + return this->system_data; +} + +shared_ptr Client::character(bool allow_load, bool allow_overlay) { + if (this->overlay_character_data && allow_overlay) { + return this->overlay_character_data; + } + if (!this->character_data && allow_load) { + if ((this->version() == Version::BB_V4) && (this->bb_character_index < 0)) { + throw runtime_error("character index not specified"); + } + this->load_all_files(); + } + return this->character_data; +} + +shared_ptr Client::character(bool allow_load, bool allow_overlay) const { + if (allow_overlay && this->overlay_character_data) { + return this->overlay_character_data; + } + if (!this->character_data && allow_load) { + throw runtime_error("character data is not loaded"); + } + return this->character_data; +} + +shared_ptr Client::guild_card_file(bool allow_load) { + if (!this->guild_card_data && allow_load) { + this->load_all_files(); + } + return this->guild_card_data; +} + +shared_ptr Client::guild_card_file(bool allow_load) const { + if (!this->guild_card_data && allow_load) { + throw runtime_error("account data is not loaded"); + } + return this->guild_card_data; +} + +string Client::system_filename() const { + if (this->version() != Version::BB_V4) { + throw logic_error("non-BB players do not have system data"); + } + if (!this->login || !this->login->bb_license) { + throw logic_error("client is not logged in"); + } + return string_printf("system/players/system_%s.psosys", this->login->bb_license->username.c_str()); +} + +string Client::character_filename(const std::string& bb_username, int8_t index) { + if (bb_username.empty()) { + throw logic_error("non-BB players do not have character data"); + } + if (index < 0) { + throw logic_error("character index is not set"); + } + return string_printf("system/players/player_%s_%hhd.psochar", bb_username.c_str(), index); +} + +string Client::backup_character_filename(uint32_t account_id, size_t index) { + return string_printf("system/players/backup_player_%" PRIu32 "_%zu.psochar", account_id, index); +} + +string Client::character_filename(int8_t index) const { + if (this->version() != Version::BB_V4) { + throw logic_error("non-BB players do not have character data"); + } + if (!this->login || !this->login->bb_license) { + throw logic_error("client is not logged in"); + } + return this->character_filename(this->login->bb_license->username, (index < 0) ? this->bb_character_index : index); +} + +string Client::guild_card_filename() const { + if (this->version() != Version::BB_V4) { + throw logic_error("non-BB players do not have character data"); + } + if (!this->login || !this->login->bb_license) { + throw logic_error("client is not logged in"); + } + return string_printf("system/players/guild_cards_%s.psocard", this->login->bb_license->username.c_str()); +} + +string Client::shared_bank_filename() const { + if (this->version() != Version::BB_V4) { + throw logic_error("non-BB players do not have character data"); + } + if (!this->login || !this->login->bb_license) { + throw logic_error("client is not logged in"); + } + return string_printf("system/players/shared_bank_%s.psobank", this->login->bb_license->username.c_str()); +} + +string Client::legacy_account_filename() const { + if (this->version() != Version::BB_V4) { + throw logic_error("non-BB players do not have character data"); + } + if (!this->login || !this->login->bb_license) { + throw logic_error("client is not logged in"); + } + return string_printf("system/players/account_%s.nsa", this->login->bb_license->username.c_str()); +} + +string Client::legacy_player_filename() const { + if (this->version() != Version::BB_V4) { + throw logic_error("non-BB players do not have character data"); + } + if (!this->login || !this->login->bb_license) { + throw logic_error("client is not logged in"); + } + if (this->bb_character_index < 0) { + throw logic_error("character index is not set"); + } + return string_printf( + "system/players/player_%s_%hhd.nsc", + this->login->bb_license->username.c_str(), + static_cast(this->bb_character_index + 1)); +} + +void Client::create_character_file( + uint32_t guild_card_number, + uint8_t language, + const PlayerDispDataBBPreview& preview, + shared_ptr level_table) { + this->character_data = PSOBBCharacterFile::create_from_preview(guild_card_number, language, preview, level_table); + this->save_character_file(); +} + +void Client::load_all_files() { + if (this->version() != Version::BB_V4) { + this->system_data = make_shared(); + this->character_data = make_shared(); + this->guild_card_data = make_shared(); + return; + } + if (!this->login || !this->login->bb_license) { + throw logic_error("cannot load BB player data until client is logged in"); + } + + this->system_data.reset(); + this->character_data.reset(); + this->guild_card_data.reset(); + + auto files_manager = this->require_server_state()->player_files_manager; + + string sys_filename = this->system_filename(); + this->system_data = files_manager->get_system(sys_filename); + if (this->system_data) { + player_data_log.info("Using loaded system file %s", sys_filename.c_str()); + } else if (isfile(sys_filename)) { + this->system_data = make_shared(load_object_file(sys_filename, true)); + files_manager->set_system(sys_filename, this->system_data); + player_data_log.info("Loaded system data from %s", sys_filename.c_str()); + } else { + player_data_log.info("System file is missing: %s", sys_filename.c_str()); + } + + if (this->bb_character_index >= 0) { + string char_filename = this->character_filename(); + this->character_data = files_manager->get_character(char_filename); + if (this->character_data) { + player_data_log.info("Using loaded character file %s", char_filename.c_str()); + } else if (isfile(char_filename)) { + auto f = fopen_unique(char_filename, "rb"); + auto header = freadx(f.get()); + if (header.size != 0x399C) { + throw runtime_error("incorrect size in character file header"); + } + if (header.command != 0x00E7) { + throw runtime_error("incorrect command in character file header"); + } + if (header.flag != 0x00000000) { + throw runtime_error("incorrect flag in character file header"); + } + static_assert(sizeof(PSOBBCharacterFile) + sizeof(PSOBBFullSystemFile) == 0x3994, ".psochar size is incorrect"); + this->character_data = make_shared(freadx(f.get())); + files_manager->set_character(char_filename, this->character_data); + player_data_log.info("Loaded character data from %s", char_filename.c_str()); + + // If there was no .psosys file, load the system file from the .psochar + // file instead + if (!this->system_data) { + this->system_data = make_shared(freadx(f.get())); + files_manager->set_system(sys_filename, this->system_data); + player_data_log.info("Loaded system data from %s", char_filename.c_str()); + } + + this->update_character_data_after_load(this->character_data); + this->system_data->base.language = this->language(); + + } else { + player_data_log.info("Character file is missing: %s", char_filename.c_str()); + } + } + + string card_filename = this->guild_card_filename(); + this->guild_card_data = files_manager->get_guild_card(card_filename); + if (this->guild_card_data) { + player_data_log.info("Using loaded Guild Card file %s", card_filename.c_str()); + } else if (isfile(card_filename)) { + this->guild_card_data = make_shared(load_object_file(card_filename)); + files_manager->set_guild_card(card_filename, this->guild_card_data); + player_data_log.info("Loaded Guild Card data from %s", card_filename.c_str()); + } else { + player_data_log.info("Guild Card file is missing: %s", card_filename.c_str()); + } + + // If any of the above files were missing, try to load from .nsa/.nsc files instead + if (!this->system_data || (!this->character_data && (this->bb_character_index >= 0)) || !this->guild_card_data) { + string nsa_filename = this->legacy_account_filename(); + shared_ptr nsa_data; + if (isfile(nsa_filename)) { + nsa_data = make_shared(load_object_file(nsa_filename)); + if (!nsa_data->signature.eq(LegacySavedAccountDataBB::SIGNATURE)) { + throw runtime_error("account data header is incorrect"); + } + if (!this->system_data) { + this->system_data = make_shared(nsa_data->system_file.base); + files_manager->set_system(sys_filename, this->system_data); + player_data_log.info("Loaded legacy system data from %s", nsa_filename.c_str()); + } + if (!this->guild_card_data) { + this->guild_card_data = make_shared(nsa_data->guild_card_file); + files_manager->set_guild_card(card_filename, this->guild_card_data); + player_data_log.info("Loaded legacy Guild Card data from %s", nsa_filename.c_str()); + } + } + + if (!this->system_data) { + this->system_data = make_shared(); + files_manager->set_system(sys_filename, this->system_data); + player_data_log.info("Created new system data"); + } + if (!this->guild_card_data) { + this->guild_card_data = make_shared(); + files_manager->set_guild_card(card_filename, this->guild_card_data); + player_data_log.info("Created new Guild Card data"); + } + + if (!this->character_data && (this->bb_character_index >= 0)) { + string nsc_filename = this->legacy_player_filename(); + auto nsc_data = load_object_file(nsc_filename); + if (nsc_data.signature == LegacySavedPlayerDataBB::SIGNATURE_V0) { + nsc_data.signature = LegacySavedPlayerDataBB::SIGNATURE_V0; + nsc_data.unused.clear(); + nsc_data.battle_records.place_counts.clear(0); + nsc_data.battle_records.disconnect_count = 0; + nsc_data.battle_records.unknown_a1.clear(0); + } else if (nsc_data.signature != LegacySavedPlayerDataBB::SIGNATURE_V1) { + throw runtime_error("legacy player data has incorrect signature"); + } + + this->character_data = make_shared(); + files_manager->set_character(this->character_filename(), this->character_data); + this->character_data->inventory = nsc_data.inventory; + this->character_data->disp = nsc_data.disp; + this->character_data->play_time_seconds = 0; + this->character_data->quest_flags = nsc_data.quest_flags; + this->character_data->death_count = nsc_data.death_count; + this->character_data->bank = nsc_data.bank; + this->character_data->guild_card.guild_card_number = this->login->account->account_id; + this->character_data->guild_card.name = nsc_data.disp.name; + this->character_data->guild_card.description = nsc_data.guild_card_description; + this->character_data->guild_card.present = 1; + this->character_data->guild_card.language = nsc_data.inventory.language; + this->character_data->guild_card.section_id = nsc_data.disp.visual.section_id; + this->character_data->guild_card.char_class = nsc_data.disp.visual.char_class; + this->character_data->auto_reply = nsc_data.auto_reply; + this->character_data->info_board = nsc_data.info_board; + this->character_data->battle_records = nsc_data.battle_records; + this->character_data->challenge_records = nsc_data.challenge_records; + this->character_data->tech_menu_shortcut_entries = nsc_data.tech_menu_shortcut_entries; + this->character_data->quest_counters = nsc_data.quest_counters; + if (nsa_data) { + this->character_data->option_flags = nsa_data->option_flags; + this->character_data->symbol_chats = nsa_data->symbol_chats; + this->character_data->shortcuts = nsa_data->shortcuts; + player_data_log.info("Loaded legacy player data from %s and %s", nsa_filename.c_str(), nsc_filename.c_str()); + } else { + player_data_log.info("Loaded legacy player data from %s", nsc_filename.c_str()); + } + this->update_character_data_after_load(this->character_data); + } + } + + this->blocked_senders.clear(); + for (size_t z = 0; z < this->guild_card_data->blocked.size(); z++) { + if (this->guild_card_data->blocked[z].present) { + this->blocked_senders.emplace(this->guild_card_data->blocked[z].guild_card_number); + } + } + + if (this->character_data) { + // Clear legacy play_time field + this->character_data->disp.name.clear_after_bytes(0x18); + this->login->account->auto_reply_message = this->character_data->auto_reply.decode(); + this->login->account->save(); + this->last_play_time_update = now(); + } +} + +void Client::update_character_data_after_load(shared_ptr charfile) { + charfile->import_tethealla_material_usage(this->require_server_state()->level_table(this->version())); + + uint8_t lang = this->language(); + player_data_log.info("Overriding language fields in save files with %02hhX (%c)", lang, char_for_language_code(lang)); + charfile->inventory.language = lang; + charfile->guild_card.language = lang; +} + +void Client::save_all() { + if (this->system_data) { + this->save_system_file(); + } + if (this->character_data) { + this->save_character_file(); + } + if (this->guild_card_data) { + this->save_guild_card_file(); + } + if (this->external_bank) { + string filename = this->shared_bank_filename(); + save_object_file(filename, *this->external_bank); + player_data_log.info("Saved shared bank file %s", filename.c_str()); + } + if (this->external_bank_character) { + this->save_character_file( + this->character_filename(this->external_bank_character_index), + this->system_data, + this->external_bank_character); + } +} + +void Client::save_system_file() const { + if (!this->system_data) { + throw logic_error("no system file loaded"); + } + string filename = this->system_filename(); + save_object_file(filename, *this->system_data); + player_data_log.info("Saved system file %s", filename.c_str()); +} + +void Client::save_character_file( + const string& filename, + shared_ptr system, + shared_ptr character) { + auto f = fopen_unique(filename, "wb"); + PSOCommandHeaderBB header = {sizeof(PSOCommandHeaderBB) + sizeof(PSOBBCharacterFile) + sizeof(PSOBBBaseSystemFile) + sizeof(PSOBBTeamMembership), 0x00E7, 0x00000000}; + fwritex(f.get(), header); + fwritex(f.get(), *character); + fwritex(f.get(), *system); + // TODO: Technically, we should write the actual team membership struct to the + // file here, but that would cause Client to depend on Account, which + // it currently does not. This data doesn't matter at all for correctness + // within newserv, since it ignores this data entirely and instead generates + // the membership struct from the team ID in the Account and the team's state. + // So, writing correct data here would mostly be for compatibility with other + // PSO servers. But if the other server is newserv, then this data would be + // used anyway, and if it's not, then it would presumably have a different set + // of teams with a different set of team IDs anyway, so the membership struct + // here would be useless either way. + static const PSOBBTeamMembership empty_membership; + fwritex(f.get(), empty_membership); + player_data_log.info("Saved character file %s", filename.c_str()); +} + +void Client::save_character_file() { + if (!this->system_data.get()) { + throw logic_error("no system file loaded"); + } + if (!this->character_data.get()) { + throw logic_error("no character file loaded"); + } + if (this->should_update_play_time) { + // This is slightly inaccurate, since fractions of a second are truncated + // off each time we save. I'm lazy, so insert shrug emoji here. + uint64_t t = now(); + uint64_t seconds = (t - this->last_play_time_update) / 1000000; + this->character_data->play_time_seconds += seconds; + player_data_log.info("Added %" PRIu64 " seconds to play time", seconds); + this->last_play_time_update = t; + } + + this->save_character_file(this->character_filename(), this->system_data, this->character_data); +} + +void Client::save_guild_card_file() const { + if (!this->guild_card_data.get()) { + throw logic_error("no Guild Card file loaded"); + } + string filename = this->guild_card_filename(); + save_object_file(filename, *this->guild_card_data); + player_data_log.info("Saved Guild Card file %s", filename.c_str()); +} + +void Client::load_backup_character(uint32_t account_id, size_t index) { + string filename = this->backup_character_filename(account_id, index); + auto f = fopen_unique(filename, "rb"); + auto header = freadx(f.get()); + if (header.size != 0x399C) { + throw runtime_error("incorrect size in character file header"); + } + if (header.command != 0x00E7) { + throw runtime_error("incorrect command in character file header"); + } + if (header.flag != 0x00000000) { + throw runtime_error("incorrect flag in character file header"); + } + this->character_data = make_shared(freadx(f.get())); + this->update_character_data_after_load(this->character_data); + this->v1_v2_last_reported_disp.reset(); +} + +void Client::save_and_unload_character() { + if (this->character_data) { + this->save_character_file(); + this->character_data.reset(); + this->log.info("Unloaded character"); + } +} + +PlayerBank200& Client::current_bank() { + if (this->external_bank) { + return *this->external_bank; + } else if (this->external_bank_character) { + return this->external_bank_character->bank; + } + return this->character()->bank; +} + +std::shared_ptr Client::current_bank_character() { + return this->external_bank_character ? this->external_bank_character : this->character(); +} + +void Client::use_default_bank() { + if (this->external_bank) { + string filename = this->shared_bank_filename(); + save_object_file(filename, *this->external_bank); + this->external_bank.reset(); + player_data_log.info("Detached shared bank %s", filename.c_str()); + } + if (this->external_bank_character) { + string filename = this->character_filename(this->external_bank_character_index); + this->save_character_file(filename, this->system_data, this->external_bank_character); + this->external_bank_character.reset(); + player_data_log.info("Detached character %s from bank", filename.c_str()); + } +} + +bool Client::use_shared_bank() { + this->use_default_bank(); + + string filename = this->shared_bank_filename(); + auto files_manager = this->require_server_state()->player_files_manager; + this->external_bank = files_manager->get_bank(filename); + if (this->external_bank) { + player_data_log.info("Using loaded shared bank %s", filename.c_str()); + return true; + } else if (isfile(filename)) { + this->external_bank = make_shared(load_object_file(filename)); + files_manager->set_bank(filename, this->external_bank); + player_data_log.info("Loaded shared bank %s", filename.c_str()); + return true; + } else { + this->external_bank = make_shared(); + files_manager->set_bank(filename, this->external_bank); + player_data_log.info("Created shared bank for %s", filename.c_str()); + return false; + } +} + +void Client::use_character_bank(int8_t index) { + this->use_default_bank(); + if (index != this->bb_character_index) { + auto files_manager = this->require_server_state()->player_files_manager; + + string filename = this->character_filename(index); + this->external_bank_character = files_manager->get_character(filename); + if (this->external_bank_character) { + this->external_bank_character_index = index; + player_data_log.info("Using loaded character file %s for external bank", filename.c_str()); + } else if (isfile(filename)) { + auto f = fopen_unique(filename, "rb"); + auto header = freadx(f.get()); + if (header.size != 0x399C) { + throw runtime_error("incorrect size in character file header"); + } + if (header.command != 0x00E7) { + throw runtime_error("incorrect command in character file header"); + } + if (header.flag != 0x00000000) { + throw runtime_error("incorrect flag in character file header"); + } + this->external_bank_character = make_shared(freadx(f.get())); + this->update_character_data_after_load(this->external_bank_character); + this->external_bank_character_index = index; + files_manager->set_character(filename, this->external_bank_character); + player_data_log.info("Loaded character data from %s for external bank", filename.c_str()); + } else { + throw runtime_error("character does not exist"); + } + } +} + +void Client::print_inventory(FILE* stream) const { + auto s = this->require_server_state(); + auto p = this->character(); + fprintf(stream, "[PlayerInventory] Meseta: %" PRIu32 "\n", p->disp.stats.meseta.load()); + fprintf(stream, "[PlayerInventory] %hhu items\n", p->inventory.num_items); + for (size_t x = 0; x < p->inventory.num_items; x++) { + const auto& item = p->inventory.items[x]; + auto hex = item.data.hex(); + auto name = s->describe_item(this->version(), item.data, false); + fprintf(stream, "[PlayerInventory] %2zu: [+%08" PRIX32 "] %s (%s)\n", x, item.flags.load(), hex.c_str(), name.c_str()); + } +} + +void Client::print_bank(FILE* stream) const { + auto s = this->require_server_state(); + auto p = this->character(); + fprintf(stream, "[PlayerBank] Meseta: %" PRIu32 "\n", p->bank.meseta.load()); + fprintf(stream, "[PlayerBank] %" PRIu32 " items\n", p->bank.num_items.load()); + for (size_t x = 0; x < p->bank.num_items; x++) { + const auto& item = p->bank.items[x]; + const char* present_token = item.present ? "" : " (missing present flag)"; + auto hex = item.data.hex(); + auto name = s->describe_item(this->version(), item.data, false); + fprintf(stream, "[PlayerBank] %3zu: %s (%s) (x%hu)%s\n", x, hex.c_str(), name.c_str(), item.amount.load(), present_token); + } +} diff --git a/src/Client.hh b/src/Client.hh index 4fb7e185..e9df1cff 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -1,412 +1,412 @@ -#pragma once - -#include - -#include -#include - -#include "Account.hh" -#include "Channel.hh" -#include "CommandFormats.hh" -#include "Episode3/BattleRecord.hh" -#include "Episode3/Tournament.hh" -#include "FileContentsCache.hh" -#include "FunctionCompiler.hh" -#include "PSOEncryption.hh" -#include "PSOProtocol.hh" -#include "PatchFileIndex.hh" -#include "Quest.hh" -#include "QuestScript.hh" -#include "TeamIndex.hh" -#include "Text.hh" - -extern const uint64_t CLIENT_CONFIG_MAGIC; - -class Server; -struct Lobby; -class Parsed6x70Data; - -class Client : public std::enable_shared_from_this { -public: - enum class Flag : uint64_t { - // clang-format off - - // This mask specifies which flags are sent to the client - // TODO: It'd be nice to use a pattern here (e.g. all server-side flags are - // in the high bits) but that would require re-recording or manually - // rewriting all the tests - CLIENT_SIDE_MASK = 0xFF3CFFFF7C0FFFFB, - - // Version-related flags - CHECKED_FOR_DC_V1_PROTOTYPE = 0x0000000000000002, - NO_D6_AFTER_LOBBY = 0x0000000000000100, - NO_D6 = 0x0000000000000200, - FORCE_ENGLISH_LANGUAGE_BB = 0x0000000000000400, - - // Flags describing the behavior for send_function_call - NO_SEND_FUNCTION_CALL = 0x0000000000001000, - ENCRYPTED_SEND_FUNCTION_CALL = 0x0000000000002000, - SEND_FUNCTION_CALL_CHECKSUM_ONLY = 0x0000000000004000, - SEND_FUNCTION_CALL_NO_CACHE_PATCH = 0x0000000000008000, - USE_OVERFLOW_FOR_SEND_FUNCTION_CALL = 0x0000000000010000, - - // State flags - LOADING = 0x0000000000100000, // Server-side only - LOADING_QUEST = 0x0000000000200000, // Server-side only - LOADING_RUNNING_JOINABLE_QUEST = 0x0000000000400000, // Server-side only - LOADING_TOURNAMENT = 0x0000000000800000, // Server-side only - IN_INFORMATION_MENU = 0x0000000001000000, // Server-side only - AT_WELCOME_MESSAGE = 0x0000000002000000, // Server-side only - SAVE_ENABLED = 0x0000000004000000, - HAS_EP3_CARD_DEFS = 0x0000000008000000, - HAS_EP3_MEDIA_UPDATES = 0x0000000010000000, - USE_OVERRIDE_RANDOM_SEED = 0x0000000020000000, - HAS_GUILD_CARD_NUMBER = 0x0000000040000000, - HAS_AUTO_PATCHES = 0x0000004000000000, - AT_BANK_COUNTER = 0x0000000080000000, // Server-side only - SHOULD_SEND_ARTIFICIAL_ITEM_STATE = 0x0001000000000000, // Server-side only - SHOULD_SEND_ARTIFICIAL_ENEMY_AND_SET_STATE = 0x0040000000000000, // Server-side only - SHOULD_SEND_ARTIFICIAL_OBJECT_STATE = 0x0080000000000000, // Server-side only - SHOULD_SEND_ARTIFICIAL_FLAG_STATE = 0x0002000000000000, // Server-side only - SHOULD_SEND_ARTIFICIAL_PLAYER_STATES = 0x0200000000000000, // Server-side only - SHOULD_SEND_ENABLE_SAVE = 0x0004000000000000, - SWITCH_ASSIST_ENABLED = 0x0000000100000000, - IS_CLIENT_CUSTOMIZATION = 0x0100000000000000, - - // Cheat mode and option flags - INFINITE_HP_ENABLED = 0x0000000200000000, - INFINITE_TP_ENABLED = 0x0000000400000000, - DEBUG_ENABLED = 0x0000000800000000, - ITEM_DROP_NOTIFICATIONS_1 = 0x0010000000000000, - ITEM_DROP_NOTIFICATIONS_2 = 0x0020000000000000, - - // Proxy option flags - PROXY_SAVE_FILES = 0x0000001000000000, - PROXY_CHAT_COMMANDS_ENABLED = 0x0000002000000000, - PROXY_PLAYER_NOTIFICATIONS_ENABLED = 0x0000008000000000, - PROXY_SUPPRESS_CLIENT_PINGS = 0x0000010000000000, - PROXY_SUPPRESS_REMOTE_LOGIN = 0x0000020000000000, - PROXY_ZERO_REMOTE_GUILD_CARD = 0x0000040000000000, - PROXY_EP3_INFINITE_MESETA_ENABLED = 0x0000080000000000, - PROXY_EP3_INFINITE_TIME_ENABLED = 0x0000100000000000, - PROXY_RED_NAME_ENABLED = 0x0000200000000000, - PROXY_BLANK_NAME_ENABLED = 0x0000400000000000, - PROXY_BLOCK_FUNCTION_CALLS = 0x0000800000000000, - PROXY_EP3_UNMASK_WHISPERS = 0x0008000000000000, - // clang-format on - }; - enum class ItemDropNotificationMode { - NOTHING = 0, - RARES_ONLY = 1, - ALL_ITEMS = 2, - ALL_ITEMS_INCLUDING_MESETA = 3, - }; - - static constexpr uint64_t DEFAULT_FLAGS = static_cast(Flag::PROXY_CHAT_COMMANDS_ENABLED); - - struct Config { - uint64_t enabled_flags = DEFAULT_FLAGS; // Client::Flag enum - uint32_t specific_version = 0; - int32_t override_random_seed = 0; - uint8_t override_section_id = 0xFF; // FF = no override - uint8_t override_lobby_event = 0xFF; // FF = no override - uint8_t override_lobby_number = 0x80; // 80 = no override - uint32_t proxy_destination_address = 0; - uint16_t proxy_destination_port = 0; - - Config() = default; - - bool operator==(const Config& other) const = default; - bool operator!=(const Config& other) const = default; - - bool should_update_vs(const Config& other) const; - - [[nodiscard]] static inline bool check_flag(uint64_t enabled_flags, Flag flag) { - return !!(enabled_flags & static_cast(flag)); - } - - [[nodiscard]] inline bool check_flag(Flag flag) const { - return this->check_flag(this->enabled_flags, flag); - } - inline void set_flag(Flag flag) { - this->enabled_flags |= static_cast(flag); - } - inline void clear_flag(Flag flag) { - this->enabled_flags &= (~static_cast(flag)); - } - inline void toggle_flag(Flag flag) { - this->enabled_flags ^= static_cast(flag); - } - - void set_flags_for_version(Version version, int64_t sub_version); - - ItemDropNotificationMode get_drop_notification_mode() const; - void set_drop_notification_mode(ItemDropNotificationMode new_mode); - - template - void parse_from(const parray& data) { - StringReader r(data.data(), data.size()); - if (r.get_u32l() != CLIENT_CONFIG_MAGIC) { - throw std::invalid_argument("config signature is incorrect"); - } - this->specific_version = r.get_u32l(); - this->enabled_flags = r.get_u64l(); - this->override_random_seed = r.get_u32l(); - this->proxy_destination_address = r.get_u32b(); - this->proxy_destination_port = r.get_u16l(); - this->override_section_id = r.get_u8(); - this->override_lobby_event = r.get_u8(); - this->override_lobby_number = r.get_u8(); - } - - template - void serialize_into(parray& data) const { - StringWriter w; - w.put_u32l(CLIENT_CONFIG_MAGIC); - w.put_u32l(this->specific_version); - w.put_u64l(this->enabled_flags & static_cast(Flag::CLIENT_SIDE_MASK)); - w.put_u32l(this->override_random_seed); - w.put_u32b(this->proxy_destination_address); - w.put_u16l(this->proxy_destination_port); - w.put_u8(this->override_section_id); - w.put_u8(this->override_lobby_event); - w.put_u8(this->override_lobby_number); - - const auto& s = w.str(); - for (size_t z = 0; z < s.size(); z++) { - data[z] = s[z]; - } - data.clear_after(s.size(), 0xFF); - } - }; - - std::weak_ptr server; - uint64_t id; - PrefixedLogger log; - - std::shared_ptr login; - - // Network - Channel channel; - struct sockaddr_storage next_connection_addr; - ServerBehavior server_behavior; - bool should_disconnect; - bool should_send_to_lobby_server; - bool should_send_to_proxy_server; - std::unordered_map> disconnect_hooks; - std::shared_ptr xb_netloc; - parray xb_9E_unknown_a1a; - uint8_t bb_connection_phase; - uint64_t ping_start_time; - - // Lobby/positioning - Config config; - Config synced_config; - std::unique_ptr> override_variations; - int32_t sub_version; - float x; - float z; - uint32_t floor; - std::weak_ptr lobby; - uint8_t lobby_client_id; - uint8_t lobby_arrow_color; - int64_t preferred_lobby_id; // <0 = no preference - - std::unique_ptr save_game_data_event; - std::unique_ptr send_ping_event; - std::unique_ptr idle_timeout_event; - int16_t card_battle_table_number; - uint16_t card_battle_table_seat_number; - uint16_t card_battle_table_seat_state; - std::weak_ptr ep3_tournament_team; - std::shared_ptr ep3_prev_battle_record; - std::shared_ptr last_menu_sent; - uint32_t last_game_info_requested; - struct JoinCommand { - uint16_t command; - uint32_t flag; - std::string data; - }; - std::unique_ptr> game_join_command_queue; - - // Character / game data - struct PendingItemTrade { - uint8_t other_client_id; - bool confirmed; // true if client has sent a D2 command - std::vector items; - }; - struct PendingCardTrade { - uint8_t other_client_id; - bool confirmed; // true if client has sent an EE D2 command - std::vector> card_to_count; - }; - bool should_update_play_time; - std::unordered_set blocked_senders; - std::unique_ptr v1_v2_last_reported_disp; - std::shared_ptr last_reported_6x70; - // These are null unless the client is within the trade sequence (D0-D4 or EE commands) - std::unique_ptr pending_item_trade; - std::unique_ptr pending_card_trade; - uint32_t telepipe_lobby_id; - G_SetTelepipeState_6x68 telepipe_state; - std::shared_ptr ep3_config; // Null for non-Ep3 - int8_t bb_character_index; - ItemData bb_identify_result; - std::array, 3> bb_shop_contents; - - // Miscellaneous (used by chat commands) - uint32_t next_exp_value; // next EXP value to give - RecentSwitchFlags recent_switch_flags; // used for switch assist - bool can_chat; - struct PendingCharacterExport { - std::shared_ptr dest_account; - ssize_t character_index = -1; - std::shared_ptr dest_bb_license; // Only used for $bbchar; null for $savechar - }; - std::unique_ptr pending_character_export; - std::deque> function_call_response_queue; - - // File loading state - uint32_t dol_base_addr; - std::shared_ptr loading_dol_file; - std::unordered_map> sending_files; - - Client( - std::shared_ptr server, - struct bufferevent* bev, - uint64_t virtual_network_id, - Version version, - ServerBehavior server_behavior); - ~Client(); - - void update_channel_name(); - - void reschedule_save_game_data_event(); - void reschedule_ping_and_timeout_events(); - - inline Version version() const { - return this->channel.version; - } - inline uint8_t language() const { - return this->channel.language; - } - - void convert_account_to_temporary_if_nte(); - - void sync_config(); - - std::shared_ptr require_server_state() const; - std::shared_ptr require_lobby() const; - - std::shared_ptr team() const; - - bool evaluate_quest_availability_expression( - std::shared_ptr expr, - std::shared_ptr game, - uint8_t event, - uint8_t difficulty, - size_t num_players, - bool v1_present) const; - bool can_see_quest( - std::shared_ptr q, - std::shared_ptr game, - uint8_t event, - uint8_t difficulty, - size_t num_players, - bool v1_present) const; - bool can_play_quest( - std::shared_ptr q, - std::shared_ptr game, - uint8_t event, - uint8_t difficulty, - size_t num_players, - bool v1_present) const; - - bool can_use_chat_commands() const; - - static void dispatch_save_game_data(evutil_socket_t, short, void* ctx); - void save_game_data(); - static void dispatch_send_ping(evutil_socket_t, short, void* ctx); - void send_ping(); - static void dispatch_idle_timeout(evutil_socket_t, short, void* ctx); - void idle_timeout(); - - void suspend_timeouts(); - - const std::string& get_bb_username() const; - void set_bb_username(const std::string& bb_username); - - void create_battle_overlay(std::shared_ptr rules, std::shared_ptr level_table); - void create_challenge_overlay(Version version, size_t template_index, std::shared_ptr level_table); - inline void delete_overlay() { - this->overlay_character_data.reset(); - } - inline bool has_overlay() const { - return this->overlay_character_data.get() != nullptr; - } - - void import_blocked_senders(const parray& blocked_senders); - - std::shared_ptr system_file(bool allow_load = true); - std::shared_ptr character(bool allow_load = true, bool allow_overlay = true); - std::shared_ptr guild_card_file(bool allow_load = true); - std::shared_ptr system_file(bool allow_load = true) const; - std::shared_ptr character(bool allow_load = true, bool allow_overlay = true) const; - std::shared_ptr guild_card_file(bool allow_load = true) const; - - void create_character_file( - uint32_t guild_card_number, - uint8_t language, - const PlayerDispDataBBPreview& preview, - std::shared_ptr level_table); - - std::string system_filename() const; - static std::string character_filename(const std::string& bb_username, int8_t index); - static std::string backup_character_filename(uint32_t account_id, size_t index); - std::string character_filename(int8_t index = -1) const; - std::string guild_card_filename() const; - std::string shared_bank_filename() const; - - std::string legacy_player_filename() const; - std::string legacy_account_filename() const; - - void save_all(); - void save_system_file() const; - static void save_character_file( - const std::string& filename, - std::shared_ptr sys, - std::shared_ptr character); - // Note: This function is not const because it updates the player's play time. - void save_character_file(); - void save_guild_card_file() const; - - void load_backup_character(uint32_t account_id, size_t index); - void save_and_unload_character(); - - PlayerBank200& current_bank(); - std::shared_ptr current_bank_character(); - bool use_shared_bank(); // Returns true if the bank exists; false if it was created - void use_character_bank(int8_t bb_character_index); - void use_default_bank(); - - void print_inventory(FILE* stream) const; - void print_bank(FILE* stream) const; - -private: - // The overlay character data is used in battle and challenge modes, when - // character data is temporarily replaced in-game. In other play modes and in - // lobbies, overlay_character_data is null. - std::shared_ptr system_data; - std::shared_ptr overlay_character_data; - std::shared_ptr character_data; - std::shared_ptr guild_card_data; - std::shared_ptr external_bank; - std::shared_ptr external_bank_character; - int8_t external_bank_character_index; - uint64_t last_play_time_update; - - void save_and_clear_external_bank(); - - void load_all_files(); - void update_character_data_after_load(std::shared_ptr character_data); -}; +#pragma once + +#include + +#include +#include + +#include "Account.hh" +#include "Channel.hh" +#include "CommandFormats.hh" +#include "Episode3/BattleRecord.hh" +#include "Episode3/Tournament.hh" +#include "FileContentsCache.hh" +#include "FunctionCompiler.hh" +#include "PSOEncryption.hh" +#include "PSOProtocol.hh" +#include "PatchFileIndex.hh" +#include "Quest.hh" +#include "QuestScript.hh" +#include "TeamIndex.hh" +#include "Text.hh" + +extern const uint64_t CLIENT_CONFIG_MAGIC; + +class Server; +struct Lobby; +class Parsed6x70Data; + +class Client : public std::enable_shared_from_this { +public: + enum class Flag : uint64_t { + // clang-format off + + // This mask specifies which flags are sent to the client + // TODO: It'd be nice to use a pattern here (e.g. all server-side flags are + // in the high bits) but that would require re-recording or manually + // rewriting all the tests + CLIENT_SIDE_MASK = 0xFF3CFFFF7C0FFFFB, + + // Version-related flags + CHECKED_FOR_DC_V1_PROTOTYPE = 0x0000000000000002, + NO_D6_AFTER_LOBBY = 0x0000000000000100, + NO_D6 = 0x0000000000000200, + FORCE_ENGLISH_LANGUAGE_BB = 0x0000000000000400, + + // Flags describing the behavior for send_function_call + NO_SEND_FUNCTION_CALL = 0x0000000000001000, + ENCRYPTED_SEND_FUNCTION_CALL = 0x0000000000002000, + SEND_FUNCTION_CALL_CHECKSUM_ONLY = 0x0000000000004000, + SEND_FUNCTION_CALL_NO_CACHE_PATCH = 0x0000000000008000, + USE_OVERFLOW_FOR_SEND_FUNCTION_CALL = 0x0000000000010000, + + // State flags + LOADING = 0x0000000000100000, // Server-side only + LOADING_QUEST = 0x0000000000200000, // Server-side only + LOADING_RUNNING_JOINABLE_QUEST = 0x0000000000400000, // Server-side only + LOADING_TOURNAMENT = 0x0000000000800000, // Server-side only + IN_INFORMATION_MENU = 0x0000000001000000, // Server-side only + AT_WELCOME_MESSAGE = 0x0000000002000000, // Server-side only + SAVE_ENABLED = 0x0000000004000000, + HAS_EP3_CARD_DEFS = 0x0000000008000000, + HAS_EP3_MEDIA_UPDATES = 0x0000000010000000, + USE_OVERRIDE_RANDOM_SEED = 0x0000000020000000, + HAS_GUILD_CARD_NUMBER = 0x0000000040000000, + HAS_AUTO_PATCHES = 0x0000004000000000, + AT_BANK_COUNTER = 0x0000000080000000, // Server-side only + SHOULD_SEND_ARTIFICIAL_ITEM_STATE = 0x0001000000000000, // Server-side only + SHOULD_SEND_ARTIFICIAL_ENEMY_AND_SET_STATE = 0x0040000000000000, // Server-side only + SHOULD_SEND_ARTIFICIAL_OBJECT_STATE = 0x0080000000000000, // Server-side only + SHOULD_SEND_ARTIFICIAL_FLAG_STATE = 0x0002000000000000, // Server-side only + SHOULD_SEND_ARTIFICIAL_PLAYER_STATES = 0x0200000000000000, // Server-side only + SHOULD_SEND_ENABLE_SAVE = 0x0004000000000000, + SWITCH_ASSIST_ENABLED = 0x0000000100000000, + IS_CLIENT_CUSTOMIZATION = 0x0100000000000000, + + // Cheat mode and option flags + INFINITE_HP_ENABLED = 0x0000000200000000, + INFINITE_TP_ENABLED = 0x0000000400000000, + DEBUG_ENABLED = 0x0000000800000000, + ITEM_DROP_NOTIFICATIONS_1 = 0x0010000000000000, + ITEM_DROP_NOTIFICATIONS_2 = 0x0020000000000000, + + // Proxy option flags + PROXY_SAVE_FILES = 0x0000001000000000, + PROXY_CHAT_COMMANDS_ENABLED = 0x0000002000000000, + PROXY_PLAYER_NOTIFICATIONS_ENABLED = 0x0000008000000000, + PROXY_SUPPRESS_CLIENT_PINGS = 0x0000010000000000, + PROXY_SUPPRESS_REMOTE_LOGIN = 0x0000020000000000, + PROXY_ZERO_REMOTE_GUILD_CARD = 0x0000040000000000, + PROXY_EP3_INFINITE_MESETA_ENABLED = 0x0000080000000000, + PROXY_EP3_INFINITE_TIME_ENABLED = 0x0000100000000000, + PROXY_RED_NAME_ENABLED = 0x0000200000000000, + PROXY_BLANK_NAME_ENABLED = 0x0000400000000000, + PROXY_BLOCK_FUNCTION_CALLS = 0x0000800000000000, + PROXY_EP3_UNMASK_WHISPERS = 0x0008000000000000, + // clang-format on + }; + enum class ItemDropNotificationMode { + NOTHING = 0, + RARES_ONLY = 1, + ALL_ITEMS = 2, + ALL_ITEMS_INCLUDING_MESETA = 3, + }; + + static constexpr uint64_t DEFAULT_FLAGS = static_cast(Flag::PROXY_CHAT_COMMANDS_ENABLED); + + struct Config { + uint64_t enabled_flags = DEFAULT_FLAGS; // Client::Flag enum + uint32_t specific_version = 0; + int32_t override_random_seed = 0; + uint8_t override_section_id = 0xFF; // FF = no override + uint8_t override_lobby_event = 0xFF; // FF = no override + uint8_t override_lobby_number = 0x80; // 80 = no override + uint32_t proxy_destination_address = 0; + uint16_t proxy_destination_port = 0; + + Config() = default; + + bool operator==(const Config& other) const = default; + bool operator!=(const Config& other) const = default; + + bool should_update_vs(const Config& other) const; + + [[nodiscard]] static inline bool check_flag(uint64_t enabled_flags, Flag flag) { + return !!(enabled_flags & static_cast(flag)); + } + + [[nodiscard]] inline bool check_flag(Flag flag) const { + return this->check_flag(this->enabled_flags, flag); + } + inline void set_flag(Flag flag) { + this->enabled_flags |= static_cast(flag); + } + inline void clear_flag(Flag flag) { + this->enabled_flags &= (~static_cast(flag)); + } + inline void toggle_flag(Flag flag) { + this->enabled_flags ^= static_cast(flag); + } + + void set_flags_for_version(Version version, int64_t sub_version); + + ItemDropNotificationMode get_drop_notification_mode() const; + void set_drop_notification_mode(ItemDropNotificationMode new_mode); + + template + void parse_from(const parray& data) { + StringReader r(data.data(), data.size()); + if (r.get_u32l() != CLIENT_CONFIG_MAGIC) { + throw std::invalid_argument("config signature is incorrect"); + } + this->specific_version = r.get_u32l(); + this->enabled_flags = r.get_u64l(); + this->override_random_seed = r.get_u32l(); + this->proxy_destination_address = r.get_u32b(); + this->proxy_destination_port = r.get_u16l(); + this->override_section_id = r.get_u8(); + this->override_lobby_event = r.get_u8(); + this->override_lobby_number = r.get_u8(); + } + + template + void serialize_into(parray& data) const { + StringWriter w; + w.put_u32l(CLIENT_CONFIG_MAGIC); + w.put_u32l(this->specific_version); + w.put_u64l(this->enabled_flags & static_cast(Flag::CLIENT_SIDE_MASK)); + w.put_u32l(this->override_random_seed); + w.put_u32b(this->proxy_destination_address); + w.put_u16l(this->proxy_destination_port); + w.put_u8(this->override_section_id); + w.put_u8(this->override_lobby_event); + w.put_u8(this->override_lobby_number); + + const auto& s = w.str(); + for (size_t z = 0; z < s.size(); z++) { + data[z] = s[z]; + } + data.clear_after(s.size(), 0xFF); + } + }; + + std::weak_ptr server; + uint64_t id; + PrefixedLogger log; + + std::shared_ptr login; + + // Network + Channel channel; + struct sockaddr_storage next_connection_addr; + ServerBehavior server_behavior; + bool should_disconnect; + bool should_send_to_lobby_server; + bool should_send_to_proxy_server; + std::unordered_map> disconnect_hooks; + std::shared_ptr xb_netloc; + parray xb_9E_unknown_a1a; + uint8_t bb_connection_phase; + uint64_t ping_start_time; + + // Lobby/positioning + Config config; + Config synced_config; + std::unique_ptr> override_variations; + int32_t sub_version; + float x; + float z; + uint32_t floor; + std::weak_ptr lobby; + uint8_t lobby_client_id; + uint8_t lobby_arrow_color; + int64_t preferred_lobby_id; // <0 = no preference + + std::unique_ptr save_game_data_event; + std::unique_ptr send_ping_event; + std::unique_ptr idle_timeout_event; + int16_t card_battle_table_number; + uint16_t card_battle_table_seat_number; + uint16_t card_battle_table_seat_state; + std::weak_ptr ep3_tournament_team; + std::shared_ptr ep3_prev_battle_record; + std::shared_ptr last_menu_sent; + uint32_t last_game_info_requested; + struct JoinCommand { + uint16_t command; + uint32_t flag; + std::string data; + }; + std::unique_ptr> game_join_command_queue; + + // Character / game data + struct PendingItemTrade { + uint8_t other_client_id; + bool confirmed; // true if client has sent a D2 command + std::vector items; + }; + struct PendingCardTrade { + uint8_t other_client_id; + bool confirmed; // true if client has sent an EE D2 command + std::vector> card_to_count; + }; + bool should_update_play_time; + std::unordered_set blocked_senders; + std::unique_ptr v1_v2_last_reported_disp; + std::shared_ptr last_reported_6x70; + // These are null unless the client is within the trade sequence (D0-D4 or EE commands) + std::unique_ptr pending_item_trade; + std::unique_ptr pending_card_trade; + uint32_t telepipe_lobby_id; + G_SetTelepipeState_6x68 telepipe_state; + std::shared_ptr ep3_config; // Null for non-Ep3 + int8_t bb_character_index; + ItemData bb_identify_result; + std::array, 3> bb_shop_contents; + + // Miscellaneous (used by chat commands) + uint32_t next_exp_value; // next EXP value to give + RecentSwitchFlags recent_switch_flags; // used for switch assist + bool can_chat; + struct PendingCharacterExport { + std::shared_ptr dest_account; + ssize_t character_index = -1; + std::shared_ptr dest_bb_license; // Only used for $bbchar; null for $savechar + }; + std::unique_ptr pending_character_export; + std::deque> function_call_response_queue; + + // File loading state + uint32_t dol_base_addr; + std::shared_ptr loading_dol_file; + std::unordered_map> sending_files; + + Client( + std::shared_ptr server, + struct bufferevent* bev, + uint64_t virtual_network_id, + Version version, + ServerBehavior server_behavior); + ~Client(); + + void update_channel_name(); + + void reschedule_save_game_data_event(); + void reschedule_ping_and_timeout_events(); + + inline Version version() const { + return this->channel.version; + } + inline uint8_t language() const { + return this->channel.language; + } + + void convert_account_to_temporary_if_nte(); + + void sync_config(); + + std::shared_ptr require_server_state() const; + std::shared_ptr require_lobby() const; + + std::shared_ptr team() const; + + bool evaluate_quest_availability_expression( + std::shared_ptr expr, + std::shared_ptr game, + uint8_t event, + uint8_t difficulty, + size_t num_players, + bool v1_present) const; + bool can_see_quest( + std::shared_ptr q, + std::shared_ptr game, + uint8_t event, + uint8_t difficulty, + size_t num_players, + bool v1_present) const; + bool can_play_quest( + std::shared_ptr q, + std::shared_ptr game, + uint8_t event, + uint8_t difficulty, + size_t num_players, + bool v1_present) const; + + bool can_use_chat_commands() const; + + static void dispatch_save_game_data(evutil_socket_t, short, void* ctx); + void save_game_data(); + static void dispatch_send_ping(evutil_socket_t, short, void* ctx); + void send_ping(); + static void dispatch_idle_timeout(evutil_socket_t, short, void* ctx); + void idle_timeout(); + + void suspend_timeouts(); + + const std::string& get_bb_username() const; + void set_bb_username(const std::string& bb_username); + + void create_battle_overlay(std::shared_ptr rules, std::shared_ptr level_table); + void create_challenge_overlay(Version version, size_t template_index, std::shared_ptr level_table); + inline void delete_overlay() { + this->overlay_character_data.reset(); + } + inline bool has_overlay() const { + return this->overlay_character_data.get() != nullptr; + } + + void import_blocked_senders(const parray& blocked_senders); + + std::shared_ptr system_file(bool allow_load = true); + std::shared_ptr character(bool allow_load = true, bool allow_overlay = true); + std::shared_ptr guild_card_file(bool allow_load = true); + std::shared_ptr system_file(bool allow_load = true) const; + std::shared_ptr character(bool allow_load = true, bool allow_overlay = true) const; + std::shared_ptr guild_card_file(bool allow_load = true) const; + + void create_character_file( + uint32_t guild_card_number, + uint8_t language, + const PlayerDispDataBBPreview& preview, + std::shared_ptr level_table); + + std::string system_filename() const; + static std::string character_filename(const std::string& bb_username, int8_t index); + static std::string backup_character_filename(uint32_t account_id, size_t index); + std::string character_filename(int8_t index = -1) const; + std::string guild_card_filename() const; + std::string shared_bank_filename() const; + + std::string legacy_player_filename() const; + std::string legacy_account_filename() const; + + void save_all(); + void save_system_file() const; + static void save_character_file( + const std::string& filename, + std::shared_ptr sys, + std::shared_ptr character); + // Note: This function is not const because it updates the player's play time. + void save_character_file(); + void save_guild_card_file() const; + + void load_backup_character(uint32_t account_id, size_t index); + void save_and_unload_character(); + + PlayerBank200& current_bank(); + std::shared_ptr current_bank_character(); + bool use_shared_bank(); // Returns true if the bank exists; false if it was created + void use_character_bank(int8_t bb_character_index); + void use_default_bank(); + + void print_inventory(FILE* stream) const; + void print_bank(FILE* stream) const; + +private: + // The overlay character data is used in battle and challenge modes, when + // character data is temporarily replaced in-game. In other play modes and in + // lobbies, overlay_character_data is null. + std::shared_ptr system_data; + std::shared_ptr overlay_character_data; + std::shared_ptr character_data; + std::shared_ptr guild_card_data; + std::shared_ptr external_bank; + std::shared_ptr external_bank_character; + int8_t external_bank_character_index; + uint64_t last_play_time_update; + + void save_and_clear_external_bank(); + + void load_all_files(); + void update_character_data_after_load(std::shared_ptr character_data); +}; diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 9d8d6add..668102bd 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -1,7233 +1,7233 @@ -#pragma once - -#include -#include -#include -#include - -#include "Episode3/DataIndexes.hh" -#include "Episode3/DeckState.hh" -#include "Episode3/MapState.hh" -#include "Episode3/PlayerStateSubordinates.hh" -#include "PSOProtocol.hh" -#include "PlayerSubordinates.hh" -#include "SaveFileFormats.hh" -#include "Text.hh" - -// This file is newserv's canonical reference of the PSO client/server protocol. - -// For the unfamiliar, the le_uint and be_uint types (from phosg/Encoding.hh) -// are the same as normal uint types, but are explicitly little-endian or -// big-endian. The parray type (from Text.hh) is the same as a standard array, -// but has various safety and convenience features so we don't have to use -// easy-to-mess-up functions like memset/memcpy and strncpy. The pstring types -// (also from Text.hh) are like std::strings, but have an explicit encoding. -// They can be implicitly converted to and from std::strings, and will encode -// or decode their specified encoding when doing so. (The default encoding is -// UTF-8 everywhere in the server code.) - -// Struct names are like [S|C|SC]_CommandName_[Versions]_Numbers -// S/C denotes who sends the command (S = server, C = client, SC = both) -// If versions are not specified, the format is the same for all versions. - -// The version tokens are as follows: -// DCv1 = PSO Dreamcast v1 -// DCv2 = PSO Dreamcast v2 -// DC = Both DCv1 and DCv2 -// PC = PSO PC (v2) -// GC = PSO GC Episodes 1&2 and/or Episode 3 -// XB = PSO Xbox Episodes 1&2 -// BB = PSO Blue Burst -// V3 = PSO GC and PSO Xbox (these versions are similar and share many formats) - -// For variable-length commands, generally a zero-length array is included on -// the end of the struct if the command is received by newserv, and is omitted -// if it's sent by newserv. In the latter case, we often use StringWriter to -// construct the command data instead. - -// Structures are sorted by command number. Long BB commands are placed in order -// according to their low byte; for example, command 01EB is in position as EB. - -// Text escape codes - -// Most text fields allow the use of various escape codes to change decoding, -// change color, or create symbols. These escape codes are always preceded by a -// tab character (0x09, or '\t'). For brevity, we generally refer to them with $ -// instead in newserv, since the server substitutes most usage of $ in player- -// provided text with \t. The escape codes are: -// - Language codes -// - - $E: Set text interpretation to English / use Roman font -// - - $J: Set text interpretation to Japanese / use Japanese font -// - - $B: Use Simplified Chinese font (PC/BB) -// - - $T: Use Traditional Chinese font (PC/BB) -// - - $K: Use Korean font (PC/BB) -// - Color codes -// - - $C0: Black (000000) -// - - $C1: Blue (0000FF) -// - - $C2: Green (00FF00) -// - - $C3: Cyan (00FFFF) -// - - $C4: Red (FF0000) -// - - $C5: Magenta (FF00FF) -// - - $C6: Yellow (FFFF00) -// - - $C7: White (FFFFFF) -// - - $C8: Pink (FF8080) -// - - $C9: Violet (8080FF) -// - - $CG: Orange pulse (FFE000 + darkenings thereof; v2 and later only) -// - - $Ca: Orange (F5A052; Episode 3 only) -// - Special character codes (Ep3 only) -// - - $B: Dash + small bullet -// - - $D: Large bullet -// - - $F: Female symbol -// - - $I: Infinity -// - - $M: Male symbol -// - - $O: Open circle -// - - $R: Solid circle -// - - $S: Star-like ability symbol -// - - $X: Cross -// - - $d: Down arrow -// - - $l: Left arrow -// - - $r: Right arrow -// - - $u: Up arrow - -//////////////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// -// PATCH SERVER COMMANDS /////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -// The patch protocol is identical between PSO PC and PSO BB (the only versions -// on which it is used). - -// A patch server session generally goes like this: -// Server: 02 (unencrypted) -// (all the following commands encrypted with PSO V2 encryption, even on BB) -// Client: 02 -// Server: 04 -// Client: 04 -// If client's login information is wrong and server chooses to reject it: -// Server: 15 -// Server disconnects -// Otherwise: -// Server: 13 (if desired) -// Server: 0B -// Server: 09 (with directory name ".") -// For each directory to be checked: -// Server: 09 -// Server: (commands to check subdirectories - more 09/0A/0C) -// For each file in the directory: -// Server: 0C -// Server: 0A -// Server: 0D -// For each 0C sent by the server earlier: -// Client: 0F -// Client: 10 -// If there are any files to be updated: -// Server: 11 -// For each directory containing files to be updated: -// Server: 09 -// Server: (commands to update subdirectories) -// For each file to be updated in this directory: -// Server: 06 -// Server: 07 (possibly multiple 07s if the file is large) -// Server: 08 -// Server: 0A -// Server: 12 -// Server disconnects - -// 00: Invalid command -// 01: Invalid command - -// 02 (S->C): Start encryption -// Client will respond with an 02 command. -// All commands after this command will be encrypted with PSO V2 encryption. -// If this command is sent during an encrypted session, the client will not -// reject it; it will simply re-initialize its encryption state and respond with -// an 02 as normal. -// The copyright field in the below structure must contain the following text: -// "Patch Server. Copyright SonicTeam, LTD. 2001" - -struct S_ServerInit_Patch_02 { - pstring copyright; - le_uint32_t server_key = 0; // Key for commands sent by server - le_uint32_t client_key = 0; // Key for commands sent by client - // The client rejects the command if it's larger than this size, so we can't - // add the after_message like we do in the other server init commands. -} __packed_ws__(S_ServerInit_Patch_02, 0x48); - -// 02 (C->S): Encryption started -// No arguments - -// 03: Invalid command - -// 04 (S->C): Request login information -// No arguments -// Client will respond with an 04 command. - -// 04 (C->S): Log in (patch) -// The email field is always blank on BB. It may be blank on PC too, so this -// cannot be used to determine the game version used by a patch client. - -struct C_Login_Patch_04 { - parray unused; - pstring username; - pstring password; - pstring email; -} __packed_ws__(C_Login_Patch_04, 0x6C); - -// 05 (S->C): Disconnect -// No arguments -// This command is not used in the normal flow (described above). Generally the -// server should disconnect after sending a 12 or 15 command instead of an 05. - -// 06 (S->C): Open file for writing - -struct S_OpenFile_Patch_06 { - le_uint32_t unknown_a1 = 0; - le_uint32_t size = 0; - pstring filename; -} __packed_ws__(S_OpenFile_Patch_06, 0x38); - -// 07 (S->C): Write file -// The client's handler table says this command's maximum size is 0x6010 -// including the header, but the only servers I've seen use this command limit -// chunks to 0x4010 (including the header). Unlike the game server's 13 and A7 -// commands, the chunks do not need to be the same size - the game opens the -// file with the "a+b" mode each time it is written, so the new data is always -// appended to the end. - -struct S_WriteFileHeader_Patch_07 { - le_uint32_t chunk_index = 0; - le_uint32_t chunk_checksum = 0; // CRC32 of the following chunk data - le_uint32_t chunk_size = 0; - // The chunk data immediately follows here -} __packed_ws__(S_WriteFileHeader_Patch_07, 0x0C); - -// 08 (S->C): Close current file -// The unused field is optional. It's not clear whether this field was ever -// used; it could be a remnant from pre-release testing, or someone could have -// simply set the maximum size of this command incorrectly. - -struct S_CloseCurrentFile_Patch_08 { - le_uint32_t unused = 0; -} __packed_ws__(S_CloseCurrentFile_Patch_08, 4); - -// 09 (S->C): Enter directory - -struct S_EnterDirectory_Patch_09 { - pstring name; -} __packed_ws__(S_EnterDirectory_Patch_09, 0x40); - -// 0A (S->C): Exit directory -// No arguments - -// 0B (S->C): Start patch session and go to patch root directory -// No arguments - -// 0C (S->C): File checksum request - -struct S_FileChecksumRequest_Patch_0C { - le_uint32_t request_id = 0; - pstring filename; -} __packed_ws__(S_FileChecksumRequest_Patch_0C, 0x24); - -// 0D (S->C): End of file checksum requests -// No arguments - -// 0E: Invalid command - -// 0F (C->S): File information - -struct C_FileInformation_Patch_0F { - le_uint32_t request_id = 0; // Matches request_id from an earlier 0C command - le_uint32_t checksum = 0; // CRC32 of the file's data - le_uint32_t size = 0; -} __packed_ws__(C_FileInformation_Patch_0F, 0x0C); - -// 10 (C->S): End of file information command list -// No arguments - -// 11 (S->C): Start file downloads - -struct S_StartFileDownloads_Patch_11 { - le_uint32_t total_bytes = 0; - le_uint32_t num_files = 0; -} __packed_ws__(S_StartFileDownloads_Patch_11, 0x08); - -// 12 (S->C): End patch session successfully -// No arguments - -// 13 (S->C): Message box -// Same format and usage as commands 1A/D5 on the game server (described below). -// On PSOBB, the message box appears in the upper half of the screen and -// functions like a normal PSO message box - that is, you can use color escapes -// (\tCG, for example) and lines are terminated with \n. On PSOPC, the message -// appears in a Windows edit control, so the text functions differently: line -// breaks must be \r\n and standard PSO color escapes don't work. The maximum -// size of this command is 0x2004 bytes, including the header. - -// 14 (S->C): Reconnect -// Same format and usage as command 19 on the game server (described below), -// except the port field is big-endian for some reason. - -template -struct S_ReconnectT { - be_uint32_t address = 0; - PortT port = 0; - le_uint16_t unused = 0; -} __packed__; -using S_Reconnect_Patch_14 = S_ReconnectT; -check_struct_size(S_Reconnect_Patch_14, 0x08); - -// 15 (S->C): Login failure -// No arguments -// The client shows a message like "Incorrect game ID or password" and -// disconnects. - -// No commands beyond 15 are valid on the patch server. - -//////////////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// -// GAME SERVER COMMANDS //////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -// 00: Invalid command - -// 01 (S->C): Lobby message box -// Internal name: RcvError -// A small message box appears in lower-right corner, and the player must press -// a key to continue. The maximum length of the message is 0x200 bytes. -// Internally, PSO calls this RcvError, since it's generally used to tell the -// player why they can't do something (e.g. join a full game). -// This format is shared by multiple commands; for all of them except 06 (S->C), -// the guild_card_number field is unused and should be 0. -// On BB, this command may be sent as 0001 or 0101; in the latter case, the -// message box appears in the lower-left corner instead. - -struct SC_TextHeader_01_06_11_B0_EE { - le_uint32_t unused = 0; - le_uint32_t guild_card_number = 0; - // Text immediately follows here -} __packed_ws__(SC_TextHeader_01_06_11_B0_EE, 8); - -// 02 (S->C): Start encryption (except on BB) -// Internal name: RcvPsoConnectV2 -// This command should be used for non-initial sessions (after the client has -// already selected a ship, for example). Command 17 should be used instead for -// the first connection. -// All commands after this command will be encrypted with PSO V2 encryption on -// DC, PC, and GC Episodes 1&2 Trial Edition, or PSO V3 encryption on other V3 -// versions. -// DCv1 clients will respond with an (encrypted) 93 command. -// DCv2 and PC clients will respond with an (encrypted) 9A or 9D command. -// V3 clients will respond with an (encrypted) 9A or 9E command, except for GC -// Episodes 1&2 Trial Edition, which behaves like PC. -// The copyright field in the below structure must contain the following text: -// "DreamCast Lobby Server. Copyright SEGA Enterprises. 1999" -// (The above text is required on all versions that use this command, including -// those versions that don't run on the DreamCast.) - -struct S_ServerInitDefault_DC_PC_V3_02_17_91_9B { - pstring copyright; - le_uint32_t server_key = 0; // Key for data sent by server - le_uint32_t client_key = 0; // Key for data sent by client -} __packed_ws__(S_ServerInitDefault_DC_PC_V3_02_17_91_9B, 0x48); - -template -struct S_ServerInitWithAfterMessageT_DC_PC_V3_02_17_91_9B { - S_ServerInitDefault_DC_PC_V3_02_17_91_9B basic_cmd; - // This field is not part of SEGA's implementation; the client ignores it. - // newserv sends a message here disavowing the preceding copyright notice. - pstring after_message; -} __packed__; - -// 03 (C->S): Legacy register (non-BB) -// Internal name: SndRegist - -struct C_LegacyLogin_PC_V3_03 { - /* 00 */ le_uint64_t unused = 0; // Same as unused field in 9D/9E - /* 08 */ le_uint32_t sub_version = 0; - /* 0C */ uint8_t is_extended = 0; - /* 0D */ uint8_t language = 0; - /* 0E */ le_uint16_t unknown_a2 = 0; - // Note: These are suffixed with 2 since they come from the same source data - // as the corresponding fields in 9D/9E. (Even though serial_number and - // serial_number2 have the same contents in 9E, they do not come from the same - // field on the client's connection context object.) - /* 10 */ pstring serial_number2; - /* 20 */ pstring access_key2; - /* 30 */ -} __packed_ws__(C_LegacyLogin_PC_V3_03, 0x30); - -// 03 (S->C): Legacy register result (non-BB) -// Internal name: RcvRegist -// header.flag specifies if the password was correct. If header.flag is 0, the -// password saved to the memory card (if any) is deleted and the client is -// disconnected. If header.flag is nonzero, the client responds with an 04 -// command. Curiously, it looks like even DCv1 doesn't use this command in its -// standard login sequence, so this may be a relic from very early development. -// Even more curiously, DCv2 (and no other PSO version) has a behavior that -// appears to be some kind of anti-cheating mechanism: if any byte in the memory -// range 8C004000-8C007FFF is not zero, the handler for this command loops -// infinitely doing nothing. -// No other arguments - -// 03 (S->C): Start encryption (BB) -// Client will respond with an (encrypted) 93 command. -// All commands after this command will be encrypted with PSO BB encryption. -// The copyright field in the below structure must contain the following text: -// "Phantasy Star Online Blue Burst Game Server. Copyright 1999-2004 SONICTEAM." - -struct S_ServerInitDefault_BB_03_9B { - pstring copyright; - parray server_key; - parray client_key; -} __packed_ws__(S_ServerInitDefault_BB_03_9B, 0xC0); - -template -struct S_ServerInitWithAfterMessageT_BB_03_9B { - S_ServerInitDefault_BB_03_9B basic_cmd; - // As in 02, this field is not part of SEGA's implementation. - pstring after_message; -} __packed__; - -// 04 (C->S): Legacy login -// Internal name: SndLogin2 -// Curiously, there is a SndLogin3 function, but it does not send anything. -// See comments on non-BB 03 (S->C). This is likely a relic of an older, -// now-unused sequence. Like 03, this command isn't used by any PSO version that -// newserv supports. -// header.flag is nonzero, but it's not clear what it's used for. - -struct C_LegacyLogin_PC_V3_04 { - /* 00 */ le_uint64_t unused1 = 0; // Same as unused field in 9D/9E - /* 08 */ le_uint32_t sub_version = 0; - /* 0C */ uint8_t is_extended = 0; - /* 0D */ uint8_t language = 0; - /* 0E */ le_uint16_t unknown_a2 = 0; - /* 10 */ pstring serial_number; - /* 20 */ pstring access_key; - /* 30 */ -} __packed_ws__(C_LegacyLogin_PC_V3_04, 0x30); - -struct C_LegacyLogin_BB_04 { - parray unknown_a1; - pstring username; - pstring password; -} __packed_ws__(C_LegacyLogin_BB_04, 0x2C); - -// 04 (S->C): Set guild card number and update client config ("security data") -// Internal name: RcvLogin -// header.flag specifies an error code; the format described below is only used -// if this code is 0 (no error). Otherwise, the command has no arguments. -// Error codes (on GC): -// 01 = Line is busy (103) -// 02 = Already logged in (104) -// 03 = Incorrect password (106) -// 04 = Account suspended (107) -// 05 = Server down for maintenance (108) -// 06 = Incorrect password (127) -// Any other nonzero value = Generic failure (101) -// The client config field in this command is ignored by pre-V3 clients as well -// as Episodes 1&2 Trial Edition. All other V3 clients save it as opaque data to -// be returned in a 9E or 9F command later. newserv sends the client config -// anyway to clients that ignore it. -// The client will respond with a 96 command, but only the first time it -// receives this command - for later 04 commands, the client will still update -// its client config but will not respond. Changing the security data at any -// time seems ok, but changing the guild card number of a client after it's -// initially set can confuse the client, and (on pre-V3 clients) possibly -// corrupt the character data. For this reason, newserv tries pretty hard to -// hide the remote guild card number when clients connect to the proxy server. -// BB clients have multiple client configs; this command sets the client config -// that is returned by the 9E and 9F commands, but does not affect the client -// config set by the E6 command (and returned in the 93 command). In most cases, -// E6 should be used for BB clients instead of 04. - -struct S_UpdateClientConfig_DC_PC_04 { - // Note: What we call player_tag here is actually three fields: two uint8_ts - // followed by a le_uint16_t. It's unknown what the uint8_t fields are for - // (they seem to always be zero), but the le_uint16_t is likely a boolean - // which denotes whether the player is present or not (for example, in lobby - // data structures). For historical and simplicity reasons, newserv combines - // these three fields into one, which takes on the value 0x00010000 when a - // player is present and zero when none is present. - le_uint32_t player_tag = 0x00010000; - le_uint32_t guild_card_number = 0; -} __packed_ws__(S_UpdateClientConfig_DC_PC_04, 8); - -struct S_UpdateClientConfig_V3_04 { - le_uint32_t player_tag = 0x00010000; - le_uint32_t guild_card_number = 0; - // This field is opaque to the client; it will send back the contents verbatim - // in its next 9E command (or on request via 9F). - parray client_config; -} __packed_ws__(S_UpdateClientConfig_V3_04, 0x28); - -struct S_UpdateClientConfig_BB_04 { - le_uint32_t player_tag = 0x00010000; - le_uint32_t guild_card_number = 0; - parray client_config; -} __packed_ws__(S_UpdateClientConfig_BB_04, 0x30); - -// 05: Disconnect -// Internal name: SndLogout -// No arguments -// Sending this command to a client will cause it to disconnect. There's no -// advantage to doing this over simply closing the TCP connection. Clients will -// send this command to the server when they are about to disconnect, but the -// server does not need to close the connection when it receives this command -// (and in some cases, the client will send multiple 05 commands before actually -// disconnecting). - -// 06: Chat -// Internal name: RcvChat and SndChat -// Server->client format is the same as the 01 command; guild_card_number -// is unused and set to zero. The maximum size of the message is 0x200 bytes. -// Client->server format is the same as the 01 command also. -// When sent by the client, the text field includes only the message. When sent -// by the server, the text field includes the origin player's name, followed by -// a tab character (\x09), followed by the message. -// During Episode 3 battles, the first byte of an inbound 06 command's message -// is interpreted differently. It should be treated as a bit field, with the low -// 4 bits intended as masks for who can see the message. If the low bit (1) is -// set, for example, then the chat message displays as " (whisper)" on player -// 0's screen regardless of the message contents. The next bit (2) hides the -// message from player 1, etc. The high 4 bits of this byte appear not to be -// used, but are often nonzero and set to the value 4. (This is probably done so -// that the field is always a valid ASCII character and also never terminates -// the chat string accidentally.) We call this byte private_flags in the places -// where newserv uses it. - -// 07 (S->C): Ship or block select menu -// Internal name: RcvDirList -// This command triggers a general form of blocking menu, which was used for -// both the ship select and block select menus by Sega (and all other private -// servers except newserv, it seems). Curiously, the string "RcvBlockList" -// appears in PSO v1 and v2, but it is not used, implying that at some point -// there was a separate command to send the block list, but it was scrapped. -// Perhaps this was used for command A1, which is identical to 07 and A0 in all -// versions of PSO (except DC NTE). -// The menu is titles "Ship Select" unless the first menu item begins with the -// text "BLOCK" (all caps), in which case it is titled "Block Select". - -// Command is a list of these; header.flag is the entry count. The first entry -// is not included in the count and does not appear on the client. The text of -// the first entry becomes the ship name when the client joins a lobby. -template -struct S_MenuEntryT { - le_uint32_t menu_id = 0; - le_uint32_t item_id = 0; - le_uint16_t flags = 0x0F04; // Should be this value, apparently - pstring text; -} __packed__; -using S_MenuEntry_PC_BB_07_1F = S_MenuEntryT; -using S_MenuEntry_DC_V3_07_1F = S_MenuEntryT; -check_struct_size(S_MenuEntry_PC_BB_07_1F, 0x2C); -check_struct_size(S_MenuEntry_DC_V3_07_1F, 0x1C); - -// 08 (C->S): Request game list -// Internal name: SndGameList -// No arguments - -// 08 (S->C): Game list -// Internal name: RcvGameList -// Client responds with 09 and 10 commands (or nothing if the player cancels). - -// Command is a list of these; header.flag is the entry count. The first entry -// is not included in the count and does not appear on the client. -template -struct S_GameMenuEntryT { - le_uint32_t menu_id = 0; - le_uint32_t game_id = 0; - // difficulty_tag is 0x0A on Episode 3; on all other versions, it's - // difficulty + 0x22 (so 0x25 means Ultimate, for example) - uint8_t difficulty_tag = 0; - uint8_t num_players = 0; - pstring name; - // The episode field is used differently by different versions: - // - On DCv1, PC, and GC Episode 3, the value is ignored. - // - On DCv2, 1 means v1 players can't join the game, and 0 means they can. - // - On GC Ep1&2, 0x40 means Episode 1, and 0x41 means Episode 2. - // - On BB, 0x40/0x41 mean Episodes 1/2 as on GC, and 0x43 means Episode 4. - uint8_t episode = 0; - // Flags: - // 01 = Send name? (client sends the name field in the 10 command if this - // item is chosen, but it's blank) - // 02 = Locked (lock icon appears in menu; player is prompted for password if - // they choose this game) - // 04 = In battle (Episode 3; a sword icon appears in menu) - // 04 = Disabled (BB; used for solo games) - // 10 = Is battle mode - // 20 = Is challenge mode - // 40 = Is v2 only (DCv2/PC); name renders in orange - // 40 = Is Episode 1 (V3/BB) - // 80 = Is Episode 2 (V3/BB) - // C0 = Is Episode 4 (BB) - uint8_t flags = 0; -} __packed__; -using S_GameMenuEntry_PC_BB_08 = S_GameMenuEntryT; -using S_GameMenuEntry_DC_V3_08_Ep3_E6 = S_GameMenuEntryT; -check_struct_size(S_GameMenuEntry_PC_BB_08, 0x2C); -check_struct_size(S_GameMenuEntry_DC_V3_08_Ep3_E6, 0x1C); - -// 09 (C->S): Menu item info request -// Internal name: SndInfo -// Server will respond with an 11 command, or an A3 or A5 if the specified menu -// is the quest menu. - -struct C_MenuItemInfoRequest_09 { - le_uint32_t menu_id = 0; - le_uint32_t item_id = 0; -} __packed_ws__(C_MenuItemInfoRequest_09, 8); - -// 0B: Invalid command - -// 0C (C->S): Create game (DCv1) -// Same format as C1, but fields not supported by v1 (e.g. episode, v2 mode) -// are unused. - -// 0D: Invalid command - -// 0E (S->C): Incomplete/legacy join game (non-BB) -// Internal name: RcvStartGame -// header.flag = number of valid entries in lobby_data - -// This command appears to be a vestige of very early development; its -// second-phase handler is missing even in the earliest public prototype of PSO -// (DC NTE), and the command format is missing some important information -// necessary to start a game on any version. - -// There is a failure mode in the 0E command handler that causes the thread -// receiving the command to loop infinitely doing nothing, effectively -// softlocking the game. This happens if the local player's Guild Card number -// doesn't match any of the lobby_data entries. (Notably, only the first -// (header.flag) entries are checked.) -// If the local player's Guild Card number does match one of the entries, the -// command does not softlock, but instead does nothing because the 0E -// second-phase handler is missing. - -template -struct SC_MeetUserExtensionT { - struct LobbyReference { - le_uint32_t menu_id = 0; - le_uint32_t item_id = 0; - } __packed_ws__(LobbyReference, 8); - - /* 00 */ parray lobby_refs; - /* 40 */ le_uint32_t unknown_a2 = 0; - /* 44 */ pstring player_name; - /* 64 (or 84 on UTF16 versions) */ -} __packed__; -using SC_MeetUserExtension_DC_V3 = SC_MeetUserExtensionT; -using SC_MeetUserExtension_PC_BB = SC_MeetUserExtensionT; -check_struct_size(SC_MeetUserExtension_DC_V3, 0x64); -check_struct_size(SC_MeetUserExtension_PC_BB, 0x84); - -struct S_LegacyJoinGame_PC_0E { - struct LobbyData { - le_uint32_t player_tag = 0; - le_uint32_t guild_card_number = 0; - pstring name; - } __packed_ws__(LobbyData, 0x18); - - parray lobby_data; - parray unknown_a3; -} __packed_ws__(S_LegacyJoinGame_PC_0E, 0x80); - -struct S_LegacyJoinGame_GC_0E { - parray lobby_data; - SC_MeetUserExtension_DC_V3 meet_user_extension; - parray unknown_a3; -} __packed_ws__(S_LegacyJoinGame_GC_0E, 0xE8); - -struct S_LegacyJoinGame_XB_0E { - struct LobbyData { - le_uint32_t player_tag = 0; - le_uint32_t guild_card_number = 0; - pstring name; - } __packed_ws__(LobbyData, 0x20); - parray lobby_data; - SC_MeetUserExtension_DC_V3 meet_user_extension; - parray unknown_a3; -} __packed_ws__(S_LegacyJoinGame_XB_0E, 0xE8); - -// 0F: Invalid command - -// 10 (C->S): Menu selection -// Internal name: SndAction -// header.flag contains two flags: 02 specifies if a password is present, and 01 -// specifies... something else. These two bits directly correspond to the two -// lowest bits in the flags field of the game menu: 02 specifies that the game -// is locked, but the function of 01 is unknown. -// Annoyingly, the no-arguments form of the command can have any flag value, so -// it doesn't suffice to check the flag value to know which format is being -// used! - -struct C_MenuSelection_10_Flag00 { - le_uint32_t menu_id = 0; - le_uint32_t item_id = 0; -} __packed_ws__(C_MenuSelection_10_Flag00, 8); - -template -struct C_MenuSelectionT_10_Flag01 { - C_MenuSelection_10_Flag00 basic_cmd; - pstring unknown_a1; -} __packed__; -using C_MenuSelection_DC_V3_10_Flag01 = C_MenuSelectionT_10_Flag01; -using C_MenuSelection_PC_BB_10_Flag01 = C_MenuSelectionT_10_Flag01; -check_struct_size(C_MenuSelection_DC_V3_10_Flag01, 0x18); -check_struct_size(C_MenuSelection_PC_BB_10_Flag01, 0x28); - -template -struct C_MenuSelectionT_10_Flag02 { - C_MenuSelection_10_Flag00 basic_cmd; - pstring password; -} __packed__; -using C_MenuSelection_DC_V3_10_Flag02 = C_MenuSelectionT_10_Flag02; -using C_MenuSelection_PC_BB_10_Flag02 = C_MenuSelectionT_10_Flag02; -check_struct_size(C_MenuSelection_DC_V3_10_Flag02, 0x18); -check_struct_size(C_MenuSelection_PC_BB_10_Flag02, 0x28); - -template -struct C_MenuSelectionT_10_Flag03 { - C_MenuSelection_10_Flag00 basic_cmd; - pstring unknown_a1; - pstring password; -} __packed__; -using C_MenuSelection_DC_V3_10_Flag03 = C_MenuSelectionT_10_Flag03; -using C_MenuSelection_PC_BB_10_Flag03 = C_MenuSelectionT_10_Flag03; -check_struct_size(C_MenuSelection_DC_V3_10_Flag03, 0x28); -check_struct_size(C_MenuSelection_PC_BB_10_Flag03, 0x48); - -// 11 (S->C): Ship info -// Internal name: RcvMessage -// Same format as 01 command. The text appears in a small box in the lower-left -// corner (on V3/BB) or lower-right corner of the screen. - -// 12 (S->C): Valid but ignored (all versions) -// Internal name: RcvBaner -// This command's internal name is possibly a misspelling of "banner", which -// could be an early version of the 1A/D5 (large message box) commands, or of -// BB's 00EE (scrolling message) command; however, the existence of -// RcvBanerHead (16) seems to contradict this hypothesis since a text message -// would not require a separate header command. Even on DC NTE, this command -// does nothing, so this must have been scrapped very early in development. - -// 13 (S->C): Write online quest file -// Internal name: RcvDownLoad -// Used for downloading online quests. For download quests (to be saved to the -// memory card), use A7 instead. -// All chunks except the last must have 0x400 data bytes. When downloading an -// online quest, the .bin and .dat chunks may be interleaved (although newserv -// currently sends them sequentially). There is a client bug in BB (and -// probably all other versions) where if the quest file's size is a multiple -// of 0x400, the last chunk will have size 0x400, and the client will never -// consider the download complete since it only checks if the last chunk has -// size < 0x400; it does not check if all expected bytes have been received. -// To work around this, newserv appends an extra zero byte if the quest file's -// size is a multiple of 0x400; this byte will be ignored since the PRS -// decompression algorithm contains a stop command, so it will never read it. - -// header.flag = file chunk index (start offset / 0x400) -struct S_WriteFile_13_A7 { - pstring filename; - parray data; - le_uint32_t data_size = 0; -} __packed_ws__(S_WriteFile_13_A7, 0x414); - -// 13 (C->S): Confirm file write (V3/BB) -// Client sends this in response to each 13 sent by the server. It appears -// these are only sent by V3 and BB - PSO DC and PC do not send these. - -// header.flag = file chunk index (same as in the 13/A7 sent by the server) -struct C_WriteFileConfirmation_V3_BB_13_A7 { - pstring filename; -} __packed_ws__(C_WriteFileConfirmation_V3_BB_13_A7, 0x10); - -// 14 (S->C): Valid but ignored (all versions) -// Internal name: RcvUpLoad -// Based on its internal name, this command seems like the logical opposite of -// 13 (quest file download, named RcvDownLoad internally). However, even in DC -// NTE, this command does nothing, so it must have been scrapped very early in -// development. There is a SndUpLoad string in the DC versions, but the -// corresponding function was deleted. - -// 15: Invalid command - -// 16 (S->C): Valid but ignored (all versions) -// Internal name: RcvBanerHead -// It's not clear what this command was supposed to do, but it's likely related -// to 12 in some way. Like 12, this command does nothing, even on DC NTE. - -// 17 (S->C): Start encryption at login server (except on BB) -// Internal name: RcvPsoRegistConnectV2 -// Same format and usage as 02 command, but a different copyright string: -// "DreamCast Port Map. Copyright SEGA Enterprises. 1999" -// Unlike the 02 command, V3 clients will respond with a DB command when they -// receive a 17 command in any online session, with the exception of Episodes -// 1&2 trial edition (which responds with a 9A). DCv1 will respond with a 90. DC -// NTE will respond with an 8B. Other non-V3 clients will respond with a 9A or -// 9D. - -// 18 (S->C): Account verification result (PC/V3) -// Behaves exactly the same as 9A (S->C). No arguments except header.flag. - -// 19 (S->C): Reconnect to different address -// Internal name: RcvPort -// Client will disconnect, and reconnect to the given address/port. Encryption -// will be disabled on the new connection; the server should send an appropriate -// command to enable it when the client connects. -// Note: PSO XB seems to ignore the address field, which makes sense given its -// networking architecture. - -using S_Reconnect_19 = S_ReconnectT; -check_struct_size(S_Reconnect_19, 8); - -// Because PSO PC and some versions of PSO DC/GC use the same port but different -// protocols, we use a specially-crafted 19 command to send them to two -// different ports depending on the client version. I first saw this technique -// used by Schthack; I don't know if it was his original creation. - -struct S_ReconnectSplit_19 { - be_uint32_t pc_address = 0; - le_uint16_t pc_port = 0; - parray unused1; - uint8_t gc_command = 0x19; - uint8_t gc_flag = 0; - le_uint16_t gc_size = 0x97; - be_uint32_t gc_address = 0; - le_uint16_t gc_port = 0; - parray unused2; -} __packed_ws__(S_ReconnectSplit_19, 0xAC); - -// 1A (S->C): Large message box -// Internal name: RcvText -// On V3, client will sometimes respond with a D6 command (see D6 for more -// information). -// Contents are plain text. There must be at least one null character ('\0') -// before the end of the command data. There is a bug in V3 (and possibly all -// versions) where if this command is sent after the client has joined a lobby, -// the chat log window contents will appear in the message box, prepended to -// the message text from the command. -// The maximum length of the message is 0x400 bytes. This is the only -// difference between this command and the D5 command. - -// 1B (S->C): Valid but ignored (all versions) -// Internal name: RcvBattleData -// This command does nothing in all PSO versions. There is a SndBattleData -// string in the DC versions, but the corresponding function was deleted. - -// 1C (S->C): Valid but ignored (all versions) -// Internal name: RcvSystemFile -// This command does nothing in all PSO versions. - -// 1D: Ping -// Internal name: RcvPing -// No arguments -// When sent to the client, the client will respond with a 1D command. Data sent -// by the server is ignored; the client always sends a 1D command with no data. - -// 1E: Invalid command - -// 1F (C->S): Request information menu -// Internal name: SndTextList -// No arguments -// This command is used in PSO DC and PC. It exists in V3 as well but is -// apparently unused. - -// 1F (S->C): Information menu -// Internal name: RcvTextList -// Same format and usage as 07 command, except: -// - The menu title will say "Information" instead of "Ship Select". -// - There is no way to request details before selecting a menu item (the client -// will not send 09 commands). -// - The player can press a button (B on GC, for example) to close the menu -// without selecting anything, unlike the ship select menu. The client does -// not send anything when this happens. - -// 20: Invalid command - -// 21: GameGuard control (old versions of BB) -// Format unknown - -// 0022: GameGuard check (BB) - -// Command 0022 is a 16-byte challenge (sent in the data field) using the -// following structure. - -struct SC_GameGuardCheck_BB_0022 { - parray data; -} __packed_ws__(SC_GameGuardCheck_BB_0022, 0x10); - -// 0122 (C->S): Time deviation (BB) -// This command is sent when the client executes a quest opcode 5D (gettime) and -// the returned timestamp is before the previous timestamp returned, but not by -// too much - it seems the game only considers deltas between 3 seconds and 30 -// minutes suspicious for these purposes. - -// 23 (S->C): Momoka Item Exchange result (BB) -// Sent in response to a 6xD9 command from the client. -// header.flag indicates if an item was exchanged: 0 means success, 1 means -// failure. - -// 24 (S->C): Secret Lottery Ticket exchange result (BB) -// Sent in response to a 6xDE command from the client. -// header.flag indicates whether the client had any Secret Lottery Tickets in -// their inventory (and hence could participate): 0 means success, 1 means -// failure. - -struct S_ExchangeSecretLotteryTicketResult_BB_24 { - // These fields map to unknown_a1 and unknown_a2 in the 6xDE command (but - // their order is swapped here). - le_uint16_t function_id = 0; - uint8_t start_index = 0; - uint8_t unused = 0; - parray unknown_a3; -} __packed_ws__(S_ExchangeSecretLotteryTicketResult_BB_24, 0x24); - -// 25 (S->C): Gallon's Plan result (BB) -// Sent in response to a 6xE1 command from the client. - -struct S_GallonPlanResult_BB_25 { - le_uint16_t function_id = 0; - uint8_t offset1 = 0; - uint8_t offset2 = 0; - uint8_t value1 = 0; - uint8_t value2 = 0; - le_uint16_t unused = 0; -} __packed_ws__(S_GallonPlanResult_BB_25, 8); - -// 26: Invalid command -// 27: Invalid command -// 28: Invalid command -// 29: Invalid command -// 2A: Invalid command -// 2B: Invalid command -// 2C: Invalid command -// 2D: Invalid command -// 2E: Invalid command -// 2F: Invalid command -// 30: Invalid command -// 31: Invalid command -// 32: Invalid command -// 33: Invalid command -// 34: Invalid command -// 35: Invalid command -// 36: Invalid command -// 37: Invalid command -// 38: Invalid command -// 39: Invalid command -// 3A: Invalid command -// 3B: Invalid command -// 3C: Invalid command -// 3D: Invalid command -// 3E: Invalid command -// 3F: Invalid command - -// 40 (C->S): Guild card search -// Internal name: SndFindUser -// There is an unused command named SndFavorite in the DC versions of PSO, -// which may have been related to this command. SndFavorite seems to be -// completely unused; its sender function was optimized out of all known -// builds, leaving only its name string remaining. -// The server should respond with a 41 command if the target is online. If the -// target is not online, the server doesn't respond at all. - -struct C_GuildCardSearch_40 { - le_uint32_t player_tag = 0x00010000; - le_uint32_t searcher_guild_card_number = 0; - le_uint32_t target_guild_card_number = 0; -} __packed_ws__(C_GuildCardSearch_40, 0x0C); - -// 41 (S->C): Guild card search result -// Internal name: RcvUserAns - -template -struct S_GuildCardSearchResultT { - le_uint32_t player_tag = 0x00010000; - le_uint32_t searcher_guild_card_number = 0; - le_uint32_t result_guild_card_number = 0; - HeaderT reconnect_command_header; // Ignored by the client - S_Reconnect_19 reconnect_command; - // The format of this string is "GAME-NAME,BLOCK##,SERVER-NAME". If the result - // player is not in a game, GAME-NAME should be the lobby name - for standard - // lobbies this is "BLOCK-"; for CARD lobbies this is - // "BLOCK-C". - pstring location_string; - // If the player chooses to meet the user, this extension data is sent in the - // login command (9D/9E) after connecting to the server designated in - // reconnect_command. When processing the 9D/9E, newserv uses only the - // lobby_id field within, but it fills in all fields when sending a 41. - SC_MeetUserExtensionT extension; -} __packed__; -using S_GuildCardSearchResult_PC_41 = S_GuildCardSearchResultT; -using S_GuildCardSearchResult_DC_V3_41 = S_GuildCardSearchResultT; -using S_GuildCardSearchResult_BB_41 = S_GuildCardSearchResultT; -check_struct_size(S_GuildCardSearchResult_PC_41, 0x124); -check_struct_size(S_GuildCardSearchResult_DC_V3_41, 0xC0); -check_struct_size(S_GuildCardSearchResult_BB_41, 0x128); - -// 42: Invalid command -// 43: Invalid command - -// 44 (S->C): Open file for download -// Internal name: RcvDownLoadHead -// Used for downloading online quests. The client will react to a 44 command if -// the filename ends in .bin or .dat. -// For download quests (to be saved to the memory card) and GBA games, the A6 -// command is used instead. The client will react to A6 if the filename ends in -// .bin/.dat (quests), .pvr (textures), or .gba (GameBoy Advance games). -// It appears that the .gba handler for A6 was not deleted in PSO XB, even -// though it doesn't make sense for an XB client to receive such a file. - -struct S_OpenFile_DC_44_A6 { - pstring name; // Should begin with "PSO/" - // The type field is only used for download quests (A6); it is ignored for - // online quests (44). The following values are valid for A6: - // 0 = download quest (client expects .bin and .dat files) - // 1 = download quest (client expects .bin, .dat, and .pvr files) - // 2 = GBA game (GC only; client expects .gba file only) - // 3 = Episode 3 download quest (Ep3 only; client expects .bin file only) - // There is a bug in the type logic: an A6 command always overwrites the - // current download type even if the filename doesn't end in .bin, .dat, .pvr, - // or .gba. This may lead to a resource exhaustion bug if exploited carefully, - // but I haven't verified this. Generally the server should send all files for - // a given piece of content with the same type in each file's A6 command. - uint8_t type = 0; - pstring filename; - le_uint32_t file_size = 0; -} __packed_ws__(S_OpenFile_DC_44_A6, 0x38); - -struct S_OpenFile_PC_GC_44_A6 { - pstring name; // Should begin with "PSO/" - le_uint16_t type = 0; - pstring filename; - le_uint32_t file_size = 0; -} __packed_ws__(S_OpenFile_PC_GC_44_A6, 0x38); - -// Curiously, PSO XB expects an extra 0x18 bytes at the end of this command, but -// those extra bytes are unused, and the client does not fail if they're -// omitted. -struct S_OpenFile_XB_44_A6 : S_OpenFile_PC_GC_44_A6 { - pstring xb_filename; - le_uint32_t content_meta; - parray unused2; -} __packed_ws__(S_OpenFile_XB_44_A6, 0x50); - -struct S_OpenFile_BB_44_A6 { - parray unused; - le_uint16_t type = 0; - pstring filename; - le_uint32_t file_size = 0; - pstring name; -} __packed_ws__(S_OpenFile_BB_44_A6, 0x50); - -// 44 (C->S): Confirm open file (V3/BB) -// Client sends this in response to each 44 sent by the server. - -// header.flag = quest number (sort of - seems like the client just echoes -// whatever the server sent in its header.flag field. Also quest numbers can be -// > 0xFF so the flag is essentially meaningless) -struct C_OpenFileConfirmation_44_A6 { - pstring filename; -} __packed_ws__(C_OpenFileConfirmation_44_A6, 0x10); - -// 45: Invalid command -// 46: Invalid command -// 47: Invalid command -// 48: Invalid command -// 49: Invalid command -// 4A: Invalid command -// 4B: Invalid command -// 4C: Invalid command -// 4D: Invalid command -// 4E: Invalid command -// 4F: Invalid command -// 50: Invalid command -// 51: Invalid command -// 52: Invalid command -// 53: Invalid command -// 54: Invalid command -// 55: Invalid command -// 56: Invalid command -// 57: Invalid command -// 58: Invalid command -// 59: Invalid command -// 5A: Invalid command -// 5B: Invalid command -// 5C: Invalid command -// 5D: Invalid command -// 5E: Invalid command -// 5F: Invalid command - -// 60: Broadcast command -// Internal name: SndPsoData -// When a client sends this command, the server should forward it to all players -// in the same game/lobby, except the player who originally sent the command. -// See ReceiveSubcommands or the subcommand index below for details on contents. -// The data in this command may be up to 0x400 bytes in length. If it's larger, -// the client will exhibit undefined behavior. - -// 61 (C->S): Player data -// Internal name: SndCharaDataV2 (SndCharaData in DCv1) -// header.flag specifies the format version, which is related to (but not -// identical to) the game's major version. For example, the format version is 01 -// on DC v1, 02 on PSO PC, 03 on PSO GC, XB, and BB, and 04 on Episode 3. -// Upon joining a game, the client assigns inventory item IDs sequentially as -// (0x00010000 + (0x00200000 * lobby_client_id) + x). So, for example, player -// 3's 8th item's ID would become 0x00610007. The item IDs from the last game -// the player was in will appear in their inventory in this command. -// Note: If the client is in a game at the time this command is received, the -// inventory sent by the client only includes items that would not disappear if -// the client crashes! Essentially, it reflects the saved state of the player's -// character rather than the live state. - -struct PlayerRecordsEntry_DC { - /* 00 */ le_uint32_t client_id = 0; - /* 04 */ PlayerRecordsChallengeDC challenge; - /* A4 */ PlayerRecordsBattle battle; - /* BC */ -} __packed_ws__(PlayerRecordsEntry_DC, 0xBC); - -struct PlayerRecordsEntry_PC { - /* 00 */ le_uint32_t client_id = 0; - /* 04 */ PlayerRecordsChallengePC challenge; - /* DC */ PlayerRecordsBattle battle; - /* F4 */ -} __packed_ws__(PlayerRecordsEntry_PC, 0xF4); - -struct PlayerRecordsEntry_V3 { - /* 0000 */ le_uint32_t client_id = 0; - /* 0004 */ PlayerRecordsChallengeV3 challenge; - /* 0104 */ PlayerRecordsBattle battle; - /* 011C */ -} __packed_ws__(PlayerRecordsEntry_V3, 0x011C); - -struct PlayerRecordsEntry_BB { - /* 0000 */ le_uint32_t client_id = 0; - /* 0004 */ PlayerRecordsChallengeBB challenge; - /* 0144 */ PlayerRecordsBattle battle; - /* 015C */ -} __packed_ws__(PlayerRecordsEntry_BB, 0x015C); - -struct C_CharacterData_DCv1_61_98 { - /* 0000 */ PlayerInventory inventory; - /* 034C */ PlayerDispDataDCPCV3 disp; - /* 041C */ -} __packed_ws__(C_CharacterData_DCv1_61_98, 0x041C); - -struct C_CharacterData_DCv2_61_98 { - /* 0000 */ PlayerInventory inventory; - /* 034C */ PlayerDispDataDCPCV3 disp; - /* 041C */ PlayerRecordsEntry_DC records; - /* 04D8 */ ChoiceSearchConfig choice_search_config; - /* 04F0 */ -} __packed_ws__(C_CharacterData_DCv2_61_98, 0x04F0); - -struct C_CharacterData_PC_61_98 { - /* 0000 */ PlayerInventory inventory; - /* 034C */ PlayerDispDataDCPCV3 disp; - /* 041C */ PlayerRecordsEntry_PC records; - /* 0510 */ ChoiceSearchConfig choice_search_config; - /* 0528 */ parray blocked_senders; - /* 05A0 */ le_uint32_t auto_reply_enabled = 0; - // The auto-reply message can be up to 0x200 characters. If it's shorter than - // that, the client truncates the command after the first null value (rounded - // up to the next 4-byte boundary). - /* 05A4 */ // uint16_t auto_reply[...EOF]; -} __packed_ws__(C_CharacterData_PC_61_98, 0x5A4); - -struct C_CharacterData_GCNTE_61_98 { - /* 0000 */ PlayerInventory inventory; - /* 034C */ PlayerDispDataDCPCV3 disp; - /* 041C */ PlayerRecordsEntry_DC records; - /* 04D8 */ ChoiceSearchConfig choice_search_config; - /* 04F0 */ parray blocked_senders; - /* 0568 */ le_uint32_t auto_reply_enabled = 0; - // The auto-reply message can be up to 0x200 bytes. If it's shorter than that, - // the client truncates the command after the first zero byte (rounded up to - // the next 4-byte boundary). - /* 056C */ // char auto_reply[...EOF]; -} __packed_ws__(C_CharacterData_GCNTE_61_98, 0x56C); - -struct C_CharacterData_V3_61_98 { - /* 0000 */ PlayerInventory inventory; - /* 034C */ PlayerDispDataDCPCV3 disp; - /* 041C */ PlayerRecordsEntry_V3 records; - /* 0538 */ ChoiceSearchConfig choice_search_config; - /* 0550 */ pstring info_board; - /* 05FC */ parray blocked_senders; - /* 0674 */ le_uint32_t auto_reply_enabled = 0; - // The auto-reply message can be up to 0x200 bytes. If it's shorter than that, - // the client truncates the command after the first zero byte (rounded up to - // the next 4-byte boundary). - /* 0678 */ // char auto_reply[...EOF]; -} __packed_ws__(C_CharacterData_V3_61_98, 0x678); - -struct C_CharacterData_Ep3_61_98 { - /* 0000 */ PlayerInventory inventory; - /* 034C */ PlayerDispDataDCPCV3 disp; - /* 041C */ PlayerRecordsEntry_V3 records; - /* 0538 */ ChoiceSearchConfig choice_search_config; - /* 0550 */ pstring info_board; - /* 05FC */ parray blocked_senders; - /* 0674 */ le_uint32_t auto_reply_enabled = 0; - /* 0678 */ pstring auto_reply; - /* 0724 */ Episode3::PlayerConfig ep3_config; - /* 2A74 */ -} __packed_ws__(C_CharacterData_Ep3_61_98, 0x2A74); - -struct C_CharacterData_BB_61_98 { - /* 0000 */ PlayerInventory inventory; - /* 034C */ PlayerDispDataBB disp; - /* 04DC */ PlayerRecordsEntry_BB records; - /* 0638 */ ChoiceSearchConfig choice_search_config; - /* 0650 */ pstring info_board; - /* 07A8 */ parray blocked_senders; - /* 0820 */ le_uint32_t auto_reply_enabled = 0; - // Like on V3, the client truncates the command if the auto reply message is - // shorter than 0x200 bytes. - /* 0824 */ // uint16_t auto_reply[...EOF]; -} __packed_ws__(C_CharacterData_BB_61_98, 0x824); - -// 62: Target command -// Internal name: SndPsoData2 -// When a client sends this command, the server should forward it to the player -// identified by header.flag in the same game/lobby, even if that player is the -// player who originally sent it. -// See ReceiveSubcommands or the subcommand index below for details on contents. -// The data in this command may be up to 0x400 bytes in length. If it's larger, -// the client will exhibit undefined behavior. - -// 63: Invalid command - -// 64 (S->C): Join game -// Internal name: RcvStartGame3 - -// This is sent to the joining player; the other players get a 65 instead. -// Note that (except on Episode 3) this command does not include the player's -// disp or inventory data. The clients in the game are responsible for sending -// that data to each other during the join process with 60/62/6C/6D commands. - -// Curiously, this command is named RcvStartGame3 internally, while 0E is named -// RcvStartGame. The string RcvStartGame2 appears in the DC versions, but it -// seems the relevant code was deleted - there are no references to the string. -// Based on the large gap between commands 0E and 64, we can't guess at which -// command number RcvStartGame2 might have been. - -// Header flag = entry count -template -struct S_JoinGameT_DC_PC { - // Note: It seems Sega servers sent uninitialized memory in the variations - // field when sending this command to start an Episode 3 tournament game. This - // can be misleading when reading old logs from those days, but the Episode 3 - // client really does ignore it. - parray variations; - // Unlike lobby join commands, these are filled in in their slot positions. - // That is, if there's only one player in a game with ID 2, then the first two - // of these are blank and the player's data is in the third entry here. - parray lobby_data; - uint8_t client_id = 0; - uint8_t leader_id = 0; - uint8_t disable_udp = 1; - uint8_t difficulty = 0; - uint8_t battle_mode = 0; - uint8_t event = 0; - uint8_t section_id = 0; - uint8_t challenge_mode = 0; - le_uint32_t rare_seed = 0; -} __packed__; - -struct S_JoinGame_DCNTE_64 { - uint8_t client_id = 0; - uint8_t leader_id = 0; - uint8_t disable_udp = 1; - uint8_t unused = 0; - parray variations; - parray lobby_data; -} __packed_ws__(S_JoinGame_DCNTE_64, 0x104); -using S_JoinGame_DC_64 = S_JoinGameT_DC_PC; -using S_JoinGame_PC_64 = S_JoinGameT_DC_PC; -check_struct_size(S_JoinGame_DC_64, 0x10C); -check_struct_size(S_JoinGame_PC_64, 0x14C); - -struct S_JoinGame_GC_64 : S_JoinGameT_DC_PC { - uint8_t episode = 0; - parray unused; -} __packed_ws__(S_JoinGame_GC_64, 0x110); - -struct S_JoinGame_Ep3_64 : S_JoinGame_GC_64 { - // This field is only present if the game (and client) is Episode 3. Similarly - // to lobby_data in the base struct, all four of these are always present and - // they are filled in in slot positions. - struct Ep3PlayerEntry { - PlayerInventory inventory; - PlayerDispDataDCPCV3 disp; - } __packed_ws__(Ep3PlayerEntry, 0x41C); - parray players_ep3; -} __packed_ws__(S_JoinGame_Ep3_64, 0x1180); - -struct S_JoinGame_XB_64 : S_JoinGameT_DC_PC { - uint8_t episode = 0; - parray unused; - parray unknown_a1; -} __packed_ws__(S_JoinGame_XB_64, 0x1D8); - -struct S_JoinGame_BB_64 : S_JoinGameT_DC_PC { - uint8_t episode = 0; - uint8_t unused1 = 1; - uint8_t solo_mode = 0; - uint8_t unused2 = 0; -} __packed_ws__(S_JoinGame_BB_64, 0x1A0); - -// 65 (S->C): Add player to game -// Internal name: RcvBurstGame -// When a player joins an existing game, the joining player receives a 64 -// command (described above), and the players already in the game receive a 65 -// command containing only the joining player's data. - -struct LobbyFlags_DCNTE { - uint8_t client_id = 0; - uint8_t leader_id = 0; - uint8_t disable_udp = 1; - uint8_t unused = 0; -} __packed_ws__(LobbyFlags_DCNTE, 4); - -struct LobbyFlags { - uint8_t client_id = 0; - uint8_t leader_id = 0; - uint8_t disable_udp = 1; - uint8_t lobby_number = 0; - uint8_t block_number = 0; - uint8_t unknown_a1 = 0; - uint8_t event = 0; - uint8_t unknown_a2 = 0; - le_uint32_t unused = 0; -} __packed_ws__(LobbyFlags, 0x0C); - -// Header flag = entry count (always 1 for 65 and 68; up to 0x0C for 67) -template -struct S_JoinLobbyT { - LobbyFlagsT lobby_flags; - struct Entry { - LobbyDataT lobby_data; - PlayerInventory inventory; - DispDataT disp; - } __packed__; - // Note: not all of these will be filled in and sent if the lobby isn't full - // (the command size will be shorter than this struct's size) - parray entries; - - static inline size_t size(size_t used_entries) { - return offsetof(S_JoinLobbyT, entries) + used_entries * sizeof(Entry); - } -} __packed__; -using S_JoinLobby_DCNTE_65_67_68 = S_JoinLobbyT; -using S_JoinLobby_PC_65_67_68 = S_JoinLobbyT; -using S_JoinLobby_DC_GC_65_67_68_Ep3_EB = S_JoinLobbyT; -using S_JoinLobby_BB_65_67_68 = S_JoinLobbyT; -check_struct_size(S_JoinLobby_DCNTE_65_67_68, 0x32D4); -check_struct_size(S_JoinLobby_PC_65_67_68, 0x339C); -check_struct_size(S_JoinLobby_DC_GC_65_67_68_Ep3_EB, 0x32DC); -check_struct_size(S_JoinLobby_BB_65_67_68, 0x3D8C); - -struct S_JoinLobby_XB_65_67_68 { - LobbyFlags lobby_flags; - parray unknown_a4; - struct Entry { - PlayerLobbyDataXB lobby_data; - PlayerInventory inventory; - PlayerDispDataDCPCV3 disp; - } __packed_ws__(Entry, 0x468); - // Note: not all of these will be filled in and sent if the lobby isn't full - // (the command size will be shorter than this struct's size) - parray entries; - - static inline size_t size(size_t used_entries) { - return offsetof(S_JoinLobby_XB_65_67_68, entries) + used_entries * sizeof(Entry); - } -} __packed_ws__(S_JoinLobby_XB_65_67_68, 0x3504); - -// 66 (S->C): Remove player from game -// Internal name: RcvExitGame -// This is sent to all players in a game except the leaving player. -// header.flag should be set to the leaving player ID (same as client_id). - -struct S_LeaveLobby_66_69_Ep3_E9 { - uint8_t client_id = 0; - uint8_t leader_id = 0; - // Note: disable_udp only has an effect for games; it is unused for lobbies - // and spectator teams. - uint8_t disable_udp = 1; - uint8_t unused = 0; -} __packed_ws__(S_LeaveLobby_66_69_Ep3_E9, 4); - -// 67 (S->C): Join lobby -// Internal name: RcvStartLobby2 -// This is sent to the joining player; the other players receive a 68 instead. -// Same format as 65 command, but used for lobbies instead of games. - -// Curiously, this command is named RcvStartLobby2 internally, but there is no -// command named RcvStartLobby. The string "RcvStartLobby" does appear in the DC -// game executable, but it appears the relevant code was deleted. - -// 68 (S->C): Add player to lobby -// Internal name: RcvBurstLobby -// Same format as 65 command, but used for lobbies instead of games. -// The command only includes the joining player's data. - -// 69 (S->C): Remove player from lobby -// Internal name: RcvExitLobby -// Same format as 66 command, but used for lobbies instead of games. - -// 6A: Invalid command -// 6B: Invalid command - -// 6C: Broadcast command -// Internal name: RcvPsoDataLong and SndPsoDataLong -// Same format and usage as 60 command, but with no size limit. - -// 6D: Target command -// Internal name: RcvPsoDataLong and SndPsoDataLong2 -// Same format and usage as 62 command, but with no size limit. - -// 6E: Invalid command - -// 6F (C->S): Done loading -// Internal name: SndBurstEnd -// This command is sent when a player is done loading and other players can then -// join the game. On BB, this command is sent a 006F after loading into a game, -// or as 016F after loading a joinable quest. (This means when a BB client joins -// a game with a quest in progress, they will send 006F when they're ready to -// receive the quest files, and 016F when they're actually ready to play.) - -// 70: Invalid command -// 71: Invalid command -// 72: Invalid command -// 73: Invalid command -// 74: Invalid command -// 75: Invalid command -// 76: Invalid command -// 77: Invalid command -// 78: Invalid command -// 79: Invalid command -// 7A: Invalid command -// 7B: Invalid command -// 7C: Invalid command -// 7D: Invalid command -// 7E: Invalid command -// 7F: Invalid command - -// 80: Valid but ignored (all versions except BB) -// Internal names: RcvGenerateID and SndGenerateID -// This command appears to be used to set the next item ID for the given player -// slot. PSO V3 and later accept this command, but ignore it entirely. Notably, -// no version of PSO except for DC NTE ever sends this command - it's likely it -// was used to implement some item ID sync semantics that were later changed to -// use the leader as the source of truth. - -struct C_GenerateID_DCNTE_80 { - le_uint32_t id = 0; - uint8_t unused1 = 0; // Always 0 - uint8_t unused2 = 0; // Always 0 - le_uint16_t unused3 = 0; // Always 0 - parray unused4; // Client sends uninitialized data here -} __packed_ws__(C_GenerateID_DCNTE_80, 0x0C); - -struct S_GenerateID_DC_PC_V3_80 { - le_uint32_t client_id = 0; - le_uint32_t unused = 0; - le_uint32_t next_item_id = 0; -} __packed_ws__(S_GenerateID_DC_PC_V3_80, 0x0C); - -// 81: Simple mail -// Internal name: RcvChatMessage and SndChatMessage -// Format is the same in both directions. The server should forward the command -// to the player with to_guild_card_number, if they are online. If they are not -// online, the server may store it for later delivery, send their auto-reply -// message back to the original sender, or simply drop the message. -// On GC (and probably other versions too) the unused space after the text -// contains uninitialized memory when the client sends this command. newserv -// clears the uninitialized data for security reasons before forwarding. - -struct SC_SimpleMail_PC_81 { - // If player_tag and from_guild_card_number are zero, the message cannot be - // replied to. - le_uint32_t player_tag = 0x00010000; - le_uint32_t from_guild_card_number = 0; - pstring from_name; - le_uint32_t to_guild_card_number = 0; - pstring text; -} __packed_ws__(SC_SimpleMail_PC_81, 0x42C); - -struct SC_SimpleMail_DC_V3_81 { - le_uint32_t player_tag = 0x00010000; - le_uint32_t from_guild_card_number = 0; - pstring from_name; - le_uint32_t to_guild_card_number = 0; - pstring text; -} __packed_ws__(SC_SimpleMail_DC_V3_81, 0x21C); - -struct SC_SimpleMail_BB_81 { - le_uint32_t player_tag = 0x00010000; - le_uint32_t from_guild_card_number = 0; - pstring from_name; - le_uint32_t to_guild_card_number = 0; - pstring received_date; - pstring text; -} __packed_ws__(SC_SimpleMail_BB_81, 0x454); - -// 82: Invalid command - -// 83 (S->C): Lobby menu -// Internal name: RcvRoomInfo -// Curiously, there is a SndRoomInfo string in the DC versions. Perhaps in an -// early (pre-NTE) build, the client had to request the lobby menu from the -// server, and SndRoomInfo was the command to do so. The code to send this -// command must have been removed before DC NTE. -// This command sets the menu item IDs that the client uses for the lobby -// teleporter menu. On DCv1, the client expects 10 entries here; on all other -// versions except Episode 3, the client expects 15 items here; on Episode 3, -// the client expects 20 items here. Sending more or fewer items does not -// change the lobby count on the client. If fewer entries are sent, the menu -// item IDs for some lobbies will not be set, and the client will likely send -// 84 commands that don't make sense if the player chooses one of lobbies with -// unset IDs. On Episode 3, the CARD lobbies are the last five entries, even -// though they appear at the top of the list on the player's screen. - -// Command is a list of these; header.flag is the entry count (10, 15 or 20) -struct S_LobbyListEntry_83 { - le_uint32_t menu_id = 0; - le_uint32_t item_id = 0; - le_uint32_t unused = 0; -} __packed_ws__(S_LobbyListEntry_83, 0x0C); - -// 84 (C->S): Choose lobby -// Internal name: SndRoomChange - -struct C_LobbySelection_84 { - le_uint32_t menu_id = 0; - le_uint32_t item_id = 0; -} __packed_ws__(C_LobbySelection_84, 8); - -// 85: Invalid command -// 86: Invalid command -// 87: Invalid command - -// 88 (C->S): Account check (DC NTE only) -// The server should respond with an 88 command. - -struct C_Login_DCNTE_88 { - pstring serial_number; - pstring access_key; -} __packed_ws__(C_Login_DCNTE_88, 0x22); - -// 88 (S->C): Account check result (DC NTE only) -// No arguments except header.flag. -// If header.flag is zero, client will respond with an 8A command. Otherwise, it -// will respond with an 8B command. This is the same behavior as for the 18 -// command (and in fact, the client handler is shared between both commands.) - -// 88 (S->C): Update lobby arrows (except DC NTE) -// If this command is sent while a client is joining a lobby, the client may -// ignore it. For this reason, the server should wait a few seconds after a -// client joins a lobby before sending an 88 command. -// This command is not supported on DC v1. - -// Command is a list of these; header.flag is the entry count. There should -// be an update for every player in the lobby in this command, even if their -// arrow color isn't being changed. -struct S_ArrowUpdateEntry_88 { - le_uint32_t player_tag = 0x00010000; - le_uint32_t guild_card_number = 0; - // Values for arrow_color: - // any number not specified below (including 00) - none - // 01 - red - // 02 - blue - // 03 - green - // 04 - yellow - // 05 - purple - // 06 - cyan - // 07 - orange - // 08 - pink - // 09 - white - // 0A - white - // 0B - white - // 0C - black - le_uint32_t arrow_color = 0; -} __packed_ws__(S_ArrowUpdateEntry_88, 0x0C); - -// 89 (C->S): Set lobby arrow -// header.flag = arrow color number (see above); no other arguments. -// Server should send an 88 command to all players in the lobby. - -// 89 (S->C): Start encryption at login server (DC NTE) -// Behaves exactly the same as the 17 command. - -// 8A (C->S): Connection information (DC NTE only) -// The server should respond with an 8A command. - -struct C_ConnectionInfo_DCNTE_8A { - pstring hardware_id; - le_uint32_t sub_version = 0x20; - le_uint32_t unknown_a1 = 0; - pstring username; - pstring password; - pstring email_address; // From Sylverant documentation -} __packed_ws__(C_ConnectionInfo_DCNTE_8A, 0xA0); - -// 8A (S->C): Connection information result (DC NTE only) -// header.flag is a success flag. If 0 is sent, the client shows an error -// message and disconnects. Otherwise, the client responds with an 8B command. - -// 8A (C->S): Request lobby/game name (except DC NTE) -// No arguments - -// 8A (S->C): Lobby/game name (except DC NTE) -// Contents is a string containing the lobby or game name. All versions after -// DCv1 (including the August 2001 DCv2 prototype) send an 8A command to request -// the team name after joining a game. The response is used to handle the -// team_name token in quest strings, and appears in some Challenge Mode -// information windows. -// Even though this was only ever used to retrieve the game name, Sega's -// original servers also replied to 8A if it was sent in a lobby. They would -// return a string like "LOBBY01" in that case. - -// 8B: Log in (DC NTE only) - -struct C_Login_DCNTE_8B { - le_uint32_t player_tag = 0x00010000; - le_uint32_t guild_card_number = 0; - parray hardware_id; - le_uint32_t sub_version = 0x20; - uint8_t is_extended = 0; - uint8_t language = 0; - parray unused1; - pstring serial_number; - pstring access_key; - pstring username; - pstring password; - pstring name; - parray unused; -} __packed_ws__(C_Login_DCNTE_8B, 0xAC); - -struct C_LoginExtended_DCNTE_8B : C_Login_DCNTE_8B { - SC_MeetUserExtension_DC_V3 extension; -} __packed_ws__(C_LoginExtended_DCNTE_8B, 0x110); - -// 8C: Invalid command - -// 8D (S->C): Request player data (DC NTE only) -// Behaves the same as 95 (S->C) on all other versions, but DC NTE crashes if it -// receives 95. - -// 8E: Ship select menu (DC NTE) -// Behaves exactly the same as the A0 command (in both directions). - -// 8F: Block select menu (DC NTE) -// Behaves exactly the same as the A1 command (in both directions). - -// 90 (C->S): V1 login (DC/PC/V3) -// This command is used during the DCv1 login sequence; a DCv1 client will -// respond to a 17 command with an (encrypted) 90. If a V3 client receives a 91 -// command, however, it will also send a 90 in response, though the contents -// will be blank (all zeroes). - -struct C_LoginV1_DC_PC_V3_90 { - pstring serial_number; - pstring access_key; - // Note: There is a bug in the Japanese and prototype versions of DCv1 that - // cause the client to send this command despite its size not being a - // multiple of 4. This is fixed in later versions, so we handle both cases in - // the receive handler. -} __packed_ws__(C_LoginV1_DC_PC_V3_90, 0x22); - -// 90 (S->C): Account verification result (V3) -// Behaves exactly the same as 9A (S->C). No arguments except header.flag. - -// 91 (S->C): Start encryption at login server (legacy; non-BB only) -// Internal name: RcvPsoRegistConnect -// Same format and usage as 17 command, except the client will respond with a 90 -// command. On versions that support it, this is strictly less useful than the -// 17 command. Curiously, this command appears to have been implemented after -// the 17 command since it's missing from the DC NTE version, but the 17 command -// is named RcvPsoRegistConnectV2 whereas 91 is simply RcvPsoRegistConnect. It's -// likely that after DC NTE, Sega simply changed the command numbers for this -// group of commands from 88-8F to 90-A1 (so DC NTE's 89 command became the 91 -// command in all later versions). - -// 92 (C->S): Register (DC) - -struct C_RegisterV1_DC_92 { - parray unknown_a1; - uint8_t is_extended = 0; // TODO: This is a guess - uint8_t language = 0; // TODO: This is a guess; verify it - parray unknown_a3; - pstring hardware_id; - pstring unknown_a4; - pstring email; // According to Sylverant documentation -} __packed_ws__(C_RegisterV1_DC_92, 0xA0); - -// 92 (S->C): Register result (non-BB) -// Internal name: RcvPsoRegist -// Same format and usage as 9C (S->C) command. - -// 93 (C->S): Log in (DCv1) - -struct C_LoginV1_DC_93 { - le_uint32_t player_tag = 0x00010000; - le_uint32_t guild_card_number = 0; - le_uint32_t unknown_a1 = 0; - le_uint32_t unknown_a2 = 0; - le_uint32_t sub_version = 0; - uint8_t is_extended = 0; - uint8_t language = 0; - parray unused1; - pstring serial_number; - pstring access_key; - pstring hardware_id; - pstring unknown_a3; - pstring name; - parray unused2; -} __packed_ws__(C_LoginV1_DC_93, 0xAC); - -struct C_LoginExtendedV1_DC_93 : C_LoginV1_DC_93 { - SC_MeetUserExtension_DC_V3 extension; -} __packed_ws__(C_LoginExtendedV1_DC_93, 0x110); - -// 93 (C->S): Log in (BB) - -struct C_LoginBase_BB_93 { - le_uint32_t player_tag = 0x00010000; - le_uint32_t guild_card_number = 0; - le_uint32_t sub_version = 0; - uint8_t language = 0; - int8_t character_slot = 0; - // Values for connection_phase: - // 00 - initial connection (client will request system file, characters, etc.) - // 01 - choose character - // 02 - create character - // 03 - apply updates from dressing room - // 04 - login server - // 05 - lobby server - // 06 - lobby server (with Meet User fields specified) - uint8_t connection_phase = 0; - uint8_t client_code = 0; - le_uint32_t security_token = 0; - pstring username; - pstring password; - - // These fields map to the same fields in SC_MeetUserExtensionT. There is no - // equivalent of the name field from that structure on BB (though newserv - // doesn't use it anyway). - le_uint32_t menu_id = 0; - le_uint32_t preferred_lobby_id = 0; -} __packed_ws__(C_LoginBase_BB_93, 0x7C); - -struct C_LoginWithoutHardwareInfo_BB_93 : C_LoginBase_BB_93 { - // Note: Unlike other versions, BB puts the version string in the client - // config at connect time. So the first time the server gets this command, it - // will be something like "Ver. 1.24.3". This format is used on older client - // versions (before 1.23.8?) - parray client_config; -} __packed_ws__(C_LoginWithoutHardwareInfo_BB_93, 0xA4); - -struct C_LoginWithHardwareInfo_BB_93 : C_LoginBase_BB_93 { - // See the comment in the above structure. This format is used on newer client - // versions. - parray hardware_info; - parray client_config; -} __packed_ws__(C_LoginWithHardwareInfo_BB_93, 0xAC); - -// 94: Invalid command - -// 95 (S->C): Request player data -// Internal name: RcvRecognition -// No arguments -// For some reason, some servers send high values in the header.flag field here. -// The header.flag field is completely unused by the client, however - sending -// zero works just fine. The original Sega servers had some uninitialized memory -// bugs, of which that may have been one, and other private servers may have -// just duplicated Sega's behavior verbatim. -// Client will respond with a 61 command. - -// 96 (C->S): Character save information -// Internal name: SndSaveCountCheck - -struct C_CharSaveInfo_DCv2_PC_V3_BB_96 { - // The creation timestamp is the number of seconds since 12:00AM on 1 January - // 2000. Instead of computing this directly from the TBR (on PSO GC), the game - // uses localtime(), then converts that to the desired timestamp. The leap - // year correction in the latter phase of this computation seems incorrect; it - // adds a day in 2002, 2006, etc. instead of 2004, 2008, etc. See - // compute_psogc_timestamp in SaveFileFormats.cc for details. - le_uint32_t creation_timestamp = 0; - // This field counts certain events on a per-character basis. One of the - // relevant events is the act of sending a 96 command; another is the act of - // receiving a 97 command (to which the client responds with a B1 command). - // Presumably Sega's original implementation could keep track of this value - // for each character and could therefore tell if a character had connected to - // an unofficial server between connections to Sega's servers. - le_uint32_t event_counter = 0; -} __packed_ws__(C_CharSaveInfo_DCv2_PC_V3_BB_96, 8); - -// 97 (S->C): Save to memory card -// Internal name: RcvSaveCountCheck -// No arguments -// Internally, this command is called RcvSaveCountCheck, even though the counter -// in the 96 command (to which 97 is a reply) counts more events than saves. -// Sending this command with header.flag == 0 will show a message saying that -// "character data was improperly saved", and will delete the character's items -// and challenge mode records. newserv (and all other unofficial servers) always -// send this command with flag == 1, which causes the client to save normally. -// If a PSO PC client receives this command multiple times during a session, the -// player will see the "character data may be damaged" message and be asked if -// they want to restore the pre-session backup data. -// Client will respond with a B1 command if header.flag is nonzero. - -// 98 (C->S): Leave game -// Internal name: SndUpdateCharaDataV2 (SndUpdateCharaData in DCv1) -// Same format as 61 command. The server should update its view of the client's -// player data and remove the client from the game it's in (if any), but should -// NOT assign it to an available lobby. The client will send an 84 when it's -// ready to join a lobby. - -// 99 (C->S): Server time accepted -// Internal name: SndPsoDirList -// No arguments -// This command's internal name suggests that it's actually a request for the -// ship select menu, but it's only sent as the response to a B1 command (server -// time) and the client doesn't set any state to indicate it's waiting for a -// ship select menu, so we just treat it as confirmation of a received B1 -// command instead. - -// 9A (C->S): Initial login (no password or client config) -// Internal name: RcvPsoRegistCheck -// Not used on DCv1 - that version uses 90 instead. - -struct C_Login_DC_PC_V3_9A { - pstring v1_serial_number; - pstring v1_access_key; - pstring serial_number; - pstring access_key; - le_uint32_t player_tag = 0x00010000; - le_uint32_t guild_card_number = 0; - le_uint32_t sub_version = 0; - pstring serial_number2; // On DCv2, this is the hardware ID - pstring access_key2; - pstring email_address; -} __packed_ws__(C_Login_DC_PC_V3_9A, 0xDC); - -// 9A (S->C): Account verification result -// Internal name: RcvPsoRegistCheckV2 -// The result code is sent in the header.flag field. Result codes: -// 00 = license ok (don't save to memory card; client responds with 9D/9E) -// 01 = registration required (client responds with a 9C command) -// 02 = license ok (save to memory card; client responds with 9D/9E) -// For all of the below cases, the client doesn't respond and displays a message -// box describing the error. When the player presses a button, the client then -// disconnects. -// 03 = access key invalid (125) (client deletes saved license info) -// 04 = serial number invalid (126) (client deletes saved license info) -// 07 = invalid Hunter's License (117) -// 08 = Hunter's License expired (116) -// 0B = HL not registered under this serial number/access key (112) -// 0C = HL not registered under this serial number/access key (113) -// 0D = HL not registered under this serial number/access key (114) -// 0E = connection error (115) -// 0F = connection suspended (111) -// 10 = connection suspended (111) -// 11 = Hunter's License expired (116) -// 12 = invalid Hunter's License (117) -// 13 = servers under maintenance (118) -// Seems like most (all?) of the rest of the codes are "network error" (119). - -// 9B (S->C): Secondary server init (non-BB, non-DCv1) -// Behaves exactly the same as 17 (S->C). - -// 9B (S->C): Secondary server init (BB) -// Format is the same as 03 (and the client uses the same encryption afterward). -// The only differences that 9B has from 03: -// - 9B does not work during the data-server phase (before the client has -// reached the ship select menu), whereas 03 does. -// - For command 9B, the copyright string must be -// "PSO NEW PM Server. Copyright 1999-2002 SONICTEAM.". -// - The client will respond with a command DB instead of a command 93. - -// 9C (C->S): Register -// Internal name: SndPsoRegist -// It appears PSO GC sends uninitialized data in the header.flag field here. - -struct C_Register_DC_PC_V3_9C { - le_uint64_t unused = 0; - le_uint32_t sub_version = 0; - uint8_t unused1 = 0; - uint8_t language = 0; - parray unused2; - pstring serial_number; // On XB, this is the XBL gamertag - pstring access_key; // On XB, this is the XBL user ID - pstring password; // On XB, this contains "xbox-pso" -} __packed_ws__(C_Register_DC_PC_V3_9C, 0xA0); - -struct C_Register_BB_9C { - le_uint32_t sub_version = 0; - uint8_t unused1 = 0; - uint8_t language = 0; - parray unused2; - pstring username; - pstring password; - pstring game_tag; // "psopc2" on BB -} __packed_ws__(C_Register_BB_9C, 0x98); - -// 9C (S->C): Register result -// Internal name: RcvPsoRegistV2 -// On GC, the only possible error here seems to be wrong password (127) which is -// displayed if the header.flag field is zero. On DCv2/PC, the error text says -// something like "registration failed" instead. If header.flag is nonzero, the -// client proceeds with the login procedure by sending a 9D or 9E. - -// 9D (C->S): Log in without client config (DCv2/PC/GC) -// Not used on DCv1 - that version uses 93 instead. -// Not used on most versions of V3 - the client sends 9E instead. The one -// type of PSO V3 that uses 9D is the Trial Edition of Episodes 1&2. -// The extended version of this command is sent if the client has not yet -// received an 04 (in which case the extended fields are blank) or if the client -// selected the Meet User option, in which case it specifies the requested lobby -// by its menu ID and item ID. - -struct C_Login_DC_PC_GC_9D { - /* 00 */ le_uint32_t player_tag = 0x00010000; // 0x00010000 if guild card is set (via 04) - /* 04 */ le_uint32_t guild_card_number = 0; // 0xFFFFFFFF if not set - /* 08 */ le_uint32_t unused1 = 0; - /* 0C */ le_uint32_t unused2 = 0; - /* 10 */ le_uint32_t sub_version = 0; - /* 14 */ uint8_t is_extended = 0; // If 1, structure has extended format - /* 15 */ uint8_t language = 0; // 0 = JP, 1 = EN, 2 = DE, 3 = FR, 4 = ES - /* 16 */ parray unused3; // Always zeroes - /* 18 */ pstring v1_serial_number; - /* 28 */ pstring v1_access_key; - /* 38 */ pstring serial_number; // On XB, this is the XBL gamertag - /* 48 */ pstring access_key; // On XB, this is the XBL user ID - /* 58 */ pstring serial_number2; // On XB, this is the XBL gamertag - /* 88 */ pstring access_key2; // On XB, this is the XBL user ID - /* B8 */ pstring name; - /* C8 */ -} __packed_ws__(C_Login_DC_PC_GC_9D, 0xC8); - -struct C_LoginExtended_DC_GC_9D : C_Login_DC_PC_GC_9D { - SC_MeetUserExtension_DC_V3 extension; -} __packed_ws__(C_LoginExtended_DC_GC_9D, 0x12C); - -struct C_LoginExtended_PC_9D : C_Login_DC_PC_GC_9D { - SC_MeetUserExtension_PC_BB extension; -} __packed_ws__(C_LoginExtended_PC_9D, 0x14C); - -// 9E (C->S): Log in with client config (V3/BB) -// Not used on GC Episodes 1&2 Trial Edition. -// The extended version of this command is used in the same circumstances as -// when PSO PC uses the extended version of the 9D command. -// PSO XB does not send the client config (security data) in the 9E command, -// even though there is a space for it. The server must use 9F instead to -// retrieve the client config. -// header.flag is 1 if the client has UDP disabled. - -struct C_Login_GC_9E : C_Login_DC_PC_GC_9D { - parray client_config; -} __packed_ws__(C_Login_GC_9E, 0xE8); - -struct C_LoginExtended_GC_9E : C_Login_GC_9E { - SC_MeetUserExtension_DC_V3 extension; -} __packed_ws__(C_LoginExtended_GC_9E, 0x14C); - -struct C_Login_XB_9E : C_Login_DC_PC_GC_9D { - parray unused; - XBNetworkLocation netloc; - parray unknown_a1a; - le_uint32_t xb_user_id_high = 0; - le_uint32_t xb_user_id_low = 0; - le_uint32_t unknown_a1b = 0; -} __packed_ws__(C_Login_XB_9E, 0x130); - -struct C_LoginExtended_XB_9E : C_Login_XB_9E { - SC_MeetUserExtension_DC_V3 extension; -} __packed_ws__(C_LoginExtended_XB_9E, 0x194); - -struct C_LoginExtended_BB_9E { - /* 0000 */ le_uint32_t player_tag = 0x00010000; - /* 0004 */ le_uint32_t guild_card_number = 0; // == account_id when on newserv - /* 0008 */ le_uint32_t sub_version = 0; - /* 000C */ le_uint32_t unknown_a1 = 0; - /* 0010 */ le_uint32_t unknown_a2 = 0; - /* 0014 */ pstring unknown_a3; // Always blank? - /* 0024 */ pstring unknown_a4; // == "?" - /* 0034 */ pstring unknown_a5; // Always blank? - /* 0044 */ pstring unknown_a6; // Always blank? - /* 0054 */ pstring username; - /* 0084 */ pstring password; - /* 00B4 */ pstring guild_card_number_str; - /* 00C4 */ parray unknown_a7; - /* 00EC */ SC_MeetUserExtension_PC_BB extension; - /* 0170 */ -} __packed_ws__(C_LoginExtended_BB_9E, 0x170); - -// 9F (S->C): Request client config / security data (V3/BB) -// This command is not valid on PSO GC Episodes 1&2 Trial Edition, nor any -// pre-V3 PSO versions. Client will respond with a 9F command. -// No arguments - -// 9F (C->S): Client config / security data response (V3/BB) -// The data is opaque to the client, as described at the top of this file. -// If newserv ever sent a 9F command (it currently does not). On BB, this -// command does not work during the data server phase. - -struct C_ClientConfig_V3_9F { - parray data; -} __packed_ws__(C_ClientConfig_V3_9F, 0x20); - -struct C_ClientConfig_BB_9F { - parray data; -} __packed_ws__(C_ClientConfig_BB_9F, 0x28); - -// A0 (C->S): Change ship -// Internal name: SndShipList -// This structure is for documentation only; newserv ignores the arguments here. - -struct C_ChangeShipOrBlock_A0_A1 { - le_uint32_t player_tag = 0x00010000; - le_uint32_t guild_card_number = 0; - parray unused; -} __packed_ws__(C_ChangeShipOrBlock_A0_A1, 0x18); - -// A0 (S->C): Ship select menu -// Same as 07 command. - -// A1 (C->S): Change block -// Internal name: SndBlockList -// Same format as A0. As with A0, newserv ignores the arguments. - -// A1 (S->C): Block select menu -// Same as 07 command. - -// A2 (C->S): Request quest menu -// No arguments - -// A2 (S->C): Quest menu -// Client will respond with an 09, 10, or A9 command. For 09, the server should -// send the category or quest description via an A3 response; for 10, the server -// should send another quest menu (if a category was chosen), or send the quest -// data with 44/13 commands; for A9, the server does not need to respond. - -template -struct S_QuestMenuEntryT { - // Note: The game treats menu_id as two 8-bit fields followed by a 16-bit - // field. In most situations, this is opaque to the server, so we treat it as - // a single 32-bit field; however, in the case of the quest menu, the first - // and second bytes have meaning on the client. - // - // The first byte is used as the quest episode number, which is only relevant - // for showing the Challenge Mode times window when a quest is selected. - // This byte must be set correctly on the quest category entry, not the quest - // itself, so the Episode 1 Challenge quests category should have a value of - // 1 in this byte, and the Episode 2 Challenge quests category should have a - // value of 2. (This is not the only condition required for the Challenge - // Mode times window to work; see the description of command A3 also.) - // - // The second byte of the menu ID is used to determine which icon appears to - // the left of the quest name. - // Specifically: - // 0 = online quest icon (green diamond) - // 1 = download quest icon (green square with outlined diamond) - // 2 = completed download quest icon (orange square with outlined diamond) - // Anything else = same as 1 - le_uint32_t menu_id = 0; - le_uint32_t item_id = 0; - pstring name; - pstring short_description; -} __packed__; -using S_QuestMenuEntry_PC_A2_A4 = S_QuestMenuEntryT; -using S_QuestMenuEntry_DC_GC_A2_A4 = S_QuestMenuEntryT; -using S_QuestMenuEntry_XB_A2_A4 = S_QuestMenuEntryT; -check_struct_size(S_QuestMenuEntry_PC_A2_A4, 0x128); -check_struct_size(S_QuestMenuEntry_DC_GC_A2_A4, 0x98); -check_struct_size(S_QuestMenuEntry_XB_A2_A4, 0xA8); - -struct S_QuestMenuEntry_BB_A2_A4 { - le_uint32_t menu_id = 0; - le_uint32_t item_id = 0; - pstring name; - pstring short_description; - // If this field is set, a yellow hex icon is displayed instead of the green - // or orange diamond icon, and the quest is grayed out and cannot be selected. - // This field is ignored if the icon type (see S_QuestMenuEntry) isn't 1 or 2. - uint8_t disabled = 0; - parray unused; -} __packed_ws__(S_QuestMenuEntry_BB_A2_A4, 0x13C); - -// A3 (S->C): Quest information -// Same format as 1A/D5 command (plain text). The header.flag field is used to -// inform the client of the Challenge stage number, so it can show the correct -// timing window when the stage is selected. The Episode 1 stage numbers should -// be specified as 51-59 (decimal) in header.flag, and the Episode 2 stage -// numbers should be specified as 61-65 (decimal). If the header.flag value is -// outside this range, it is ignored, and the Challenge Mode times window does -// not update. - -// A4 (S->C): Download quest menu -// Internal name: RcvQuestList -// Same format as A2, but can be used when not in a game. The client responds -// similarly as for command A2 with the following differences: -// - Descriptions should be sent with the A5 command instead of A3. -// - If a quest is chosen, it should be sent with A6/A7 commands rather than -// 44/13, and it must be in a different encrypted format. The download quest -// format is documented in create_download_quest_file in Quest.cc. -// - After the download is done, or if the player cancels the menu, the client -// sends an A0 command instead of A9. - -// A5 (S->C): Download quest information -// Same format as 1A/D5 command (plain text) - -// A6: Open file for download -// Internal name: RcvVMDownLoadHead -// Same format as 44. See the description of 44 for some notes on the -// differences between the two commands. -// Like the 44 command, the client->server form of this command is only used on -// V3 and BB. - -// A7: Write download file -// Internal name: RcvVMDownLoad -// Same format as 13. -// Like the 13 command, the client->server form of this command is only used on -// V3 and BB. - -// A8: Invalid command - -// A9 (C->S): Quest menu closed (canceled) -// Internal name: SndQuestEnd -// No arguments -// This command is sent when the in-game quest menu (A2) is closed. This is used -// by the server to unlock the game if the players don't select a quest, since -// players are forbidden from joining while the quest menu is open. When the -// download quest menu is closed, either by downloading a quest or canceling, -// the client sends A0 instead. -// Curiously, PSO GC sends uninitialized data in header.flag. - -// AA (C->S): Send quest statistic (V3/BB) -// This command is generated when an opcode F92E is executed in a quest. -// The server should respond with an AB command. -// This command is likely never sent by PSO GC Episodes 1&2 Trial Edition, -// because the following command (AB) is definitely not valid on that version. - -struct C_SendQuestStatistic_V3_BB_AA { - le_uint16_t stat_id1 = 0; - le_uint16_t unused = 0; - le_uint16_t function_id1 = 0; - le_uint16_t function_id2 = 0; - parray params; -} __packed_ws__(C_SendQuestStatistic_V3_BB_AA, 0x28); - -// AB (S->C): Call quest function (V3/BB) -// This command is not valid on PSO GC Episodes 1&2 Trial Edition. -// Upon receipt, the client starts a quest thread running the given function. -// Probably this is supposed to be one of the function IDs previously sent in -// the AA command, but the client does not check for this. The server can -// presumably use this command to call any function at any time during a quest. - -struct S_CallQuestFunction_V3_BB_AB { - le_uint16_t function_id = 0; - parray unused; -} __packed_ws__(S_CallQuestFunction_V3_BB_AB, 4); - -// AC: Quest barrier (V3/BB) -// No arguments; header.flag must be 0 (or else the client disconnects) -// After a quest begins loading in a game (the server sends 44/13 commands to -// each player with the quest's data), each player will send an AC to the server -// when it has parsed the quest and is ready to start. When all players in a -// game have sent an AC to the server, the server should send them all an AC, -// which starts the quest for all players at (approximately) the same time. -// Sending this command to a GC client when it is not waiting to start a quest -// will cause it to crash. -// This command is not valid on PSO GC Episodes 1&2 Trial Edition. - -// AD: Invalid command -// AE: Invalid command -// AF: Invalid command - -// B0 (S->C): Text message -// Internal name: RcvEmergencyCall -// Same format as 01 command. This command is supported on DCv1 and all later -// versions, but not on prototype versions or DC NTE. -// The message appears as an overlay on the right side of the screen. The player -// doesn't do anything to dismiss it; it will disappear after a few seconds. - -// B1 (C->S): Request server time -// Internal name: GetServerTime -// No arguments -// Server will respond with a B1 command. - -// B1 (S->C): Server time -// Internal name: RcvServerTime -// This command is supported on DCv1 and all later versions, but not on -// prototype versions or DC NTE. -// Contents is a string like "%Y:%m:%d: %H:%M:%S.000" (the space is not a typo). -// For example: 2022:03:30: 15:36:42.000 -// It seems the client ignores the date part and the milliseconds part; only the -// hour, minute, and second fields are actually used. -// This command can be sent even if it's not requested by the client (with B1). -// For example, some servers send this command every time a client joins a game. -// The time_flags fields are used on V3 and later. Time flags are a 24-bit -// little-endian integer, only 2 bits of which are used: -// time_flags_low & 1 specifies whether the client should send a ping (1D) -// every 900 frames (30 seconds). -// time_flags_low & 2 disables system interrupts during a part of the GBA game -// loading procedure. (Predictably, this is only used on GC versions.) It's -// not clear what the downstream effects of this are, or why the server -// should have any control over this behavior in the first place. -// Client will respond with a 99 command. - -struct S_ServerTime_B1 { - /* 00 */ pstring time_str; - /* 19 */ uint8_t time_flags_low = 0x01; - /* 1A */ uint8_t time_flags_mid = 0x00; - /* 1B */ uint8_t time_flags_high = 0x00; - /* 1C */ -} __packed_ws__(S_ServerTime_B1, 0x1C); - -// B2 (S->C): Execute code and/or checksum memory (DCv2 and all later versions) -// Internal name: RcvProgramPatch -// Client will respond with a B3 command with the same header.flag value as was -// sent in the B2. -// On PSO PC, the code section (if included in the B2 command) is parsed and -// relocated, but is not actually executed, so the return_value field in the -// resulting B3 command is always 0. The checksum functionality does work on PSO -// PC, just like the other versions. -// This command doesn't work on some PSO GC versions, namely the later JP PSO -// Plus (v1.5), US PSO Plus (v1.2), or US/EU Episode 3. Sega presumably removed -// it after taking heat from Nintendo about enabling homebrew on the GameCube. -// On the earlier JP PSO Plus (v1.4) and JP Episode 3, this command is -// implemented as described here, with some additional compression and -// encryption steps added, similarly to how download quests are encoded. See -// send_function_call in SendCommands.cc for more details on how this works. - -// newserv supports exploiting a bug in the USA version of Episode 3, which -// re-enables the use of this command on that version of the game. See -// system/client-functions/Episode3USAQuestBufferOverflow.ppc.s for further -// details. - -struct S_ExecuteCode_B2 { - // If code_size == 0, no code is executed, but checksumming may still occur. - // In that case, this structure is the entire body of the command (no footer - // is sent). - le_uint32_t code_size = 0; // Size of code (following this struct) and footer - le_uint32_t checksum_start = 0; // May be null if size is zero - le_uint32_t checksum_size = 0; // If zero, no checksum is computed - // The code immediately follows, ending with an S_ExecuteCode_Footer_B2 -} __packed_ws__(S_ExecuteCode_B2, 0x0C); - -template -struct S_ExecuteCode_FooterT_B2 { - using U16T = typename std::conditional::type; - using U32T = typename std::conditional::type; - - // Relocations is a list of words (le_uint16_t on DC/PC/XB/BB, be_uint16_t on - // GC) containing the number of doublewords (uint32_t) to skip for each - // relocation. The relocation pointer starts immediately after the - // checksum_size field in the header, and advances by the value of one - // relocation word (times 4) before each relocation. At each relocated - // doubleword, the address of the first byte of the code (after checksum_size) - // is added to the existing value. - // For example, if the code segment contains the following data (where R - // specifies doublewords to relocate): - // RR RR RR RR ?? ?? ?? ?? ?? ?? ?? ?? RR RR RR RR - // RR RR RR RR ?? ?? ?? ?? RR RR RR RR - // then the relocation words should be 0000, 0003, 0001, and 0002. - // If there is a small number of relocations, they may be placed in the unused - // fields of this structure to save space and/or confuse reverse engineers. - // The game never accesses the last 12 bytes of this structure unless - // relocations_offset points there, so those 12 bytes may also be omitted from - // the command entirely (without changing code_size - so code_size would - // technically extend beyond the end of the B2 command). - U32T relocations_offset = 0; // Relative to code base (after checksum_size) - U32T num_relocations = 0; - parray unused1; - // entrypoint_offset is doubly indirect - it points to a pointer to a 32-bit - // value that itself is the actual entrypoint. This is presumably done so the - // entrypoint can be optionally relocated. - U32T entrypoint_addr_offset = 0; // Relative to code base (after checksum_size). - parray unused2; -} __packed__; -using S_ExecuteCode_Footer_GC_B2 = S_ExecuteCode_FooterT_B2; -using S_ExecuteCode_Footer_DC_PC_XB_BB_B2 = S_ExecuteCode_FooterT_B2; -check_struct_size(S_ExecuteCode_Footer_GC_B2, 0x20); -check_struct_size(S_ExecuteCode_Footer_DC_PC_XB_BB_B2, 0x20); - -// B3 (C->S): Execute code and/or checksum memory result -// Not used on versions that don't support the B2 command (see above). - -struct C_ExecuteCodeResult_B3 { - // On DC, return_value has the value in r0 when the function returns. - // On PC, return_value is always 0. - // On GC, return_value has the value in r3 when the function returns. - // On XB and BB, return_value has the value in eax when the function returns. - // If code_size was 0 in the B2 command, return_value is always 0. - le_uint32_t return_value = 0; - le_uint32_t checksum = 0; // 0 if no checksum was computed -} __packed_ws__(C_ExecuteCodeResult_B3, 8); - -// B4: Invalid command -// B5: Invalid command -// B6: Invalid command - -// B7 (S->C): Rank update (Episode 3) - -struct S_RankUpdate_Ep3_B7 { - // If rank is not zero, the client sets its rank text to ":", - // truncated to 11 characters. If rank is zero, the client uses rank_text - // without modifying it. - le_uint32_t rank = 0; - pstring rank_text; - le_uint32_t current_meseta = 0; - le_uint32_t total_meseta_earned = 0; - le_uint32_t unlocked_jukebox_songs = 0xFFFFFFFF; -} __packed_ws__(S_RankUpdate_Ep3_B7, 0x1C); - -// B7 (C->S): Confirm rank update (Episode 3) -// No arguments -// The client sends this after it receives a B7 from the server. - -// B8 (S->C): Update card definitions (Episode 3) -// Contents is a single le_uint32_t specifying the size of the (PRS-compressed) -// data, followed immediately by the data. The maximum size of the compressed -// data is 0x9000 bytes, although the receive buffer size limit applies first in -// practice, which limits this to 0x7BF8 bytes. The maximum size of the -// decompressed data is 0x36EC0 bytes on retail Episode 3, or 0x32960 bytes on -// Trial Edition. -// Note: PSO BB accepts this command as well, but ignores it. - -// B8 (C->S): Confirm updated card definitions (Episode 3) -// No arguments -// The client sends this after it receives a B8 from the server. - -// B9 (S->C): Update CARD lobby media (Episode 3) -// This command is not valid on Episode 3 Trial Edition. - -struct S_UpdateMediaHeader_Ep3_B9 { - // Valid values for the type field: - // 1: Texture set (GVM file) - // 2: Model - // 3: Animation - // 4: Delete all previous media updates - // Any other value: entire command is ignored - // A texture can be displayed without a model or animation. A model requires a - // texture (sent in a separate B9 command with the same location_flags), but - // does not require an animation - it will just stand still without one. An - // animation requires both a texture and model with the same location_flags. - // For models and animations, the game looks for various tokens in the - // decompressed data; specifically '****', 'GCAM', 'GJBM', 'GJTL', 'GLIM', - // 'GMDM', 'GSSM', 'NCAM', 'NJBM', 'NJCA', 'NLIM', 'NMDM', and 'NSSM'. - le_uint32_t type = 0; - // location_flags is a bit field specifying where the banner or object should - // appear. The bits are: - // 00000001: South above-counter banner (facing away from teleporters) - // 00000002: West above-counter banner - // 00000004: North above-counter banner (facing toward jukebox) - // 00000008: East above-counter banner - // 00000010: Banner above west (left) teleporter - // 00000020: Banner above east (right) teleporter - // 00000040: Banner at south end of lobby (opposite the jukebox) - // 00000080: Immediately left of 00000040 - // 00000100: Immediately right of 00000040 - // 00000200: Same as 00000080, but further left and at a slight inward angle - // 00000400: Same as 00000100, but further right and at a slight inward angle - // 00000800: Banner at north end of lobby, above the jukebox - // 00001000: Immediately right of 00000800 - // 00002000: Immediately left of 00000800 - // 00004000: Same as 00001000, but further right and at a slight inward angle - // 00008000: Same as 00002000, but further left and at a slight inward angle - // 00010000: Banners at west AND east ends of lobby, next to battle tables - // 00020000: Immediately left of 00010000 (2 banners) - // 00040000: Immediately right of 00010000 (2 banners) - // 00080000: Banners on southwest AND southeast ends of the lobby - // 00100000: Banners on south-southwest AND south-southeast ends of the lobby - // 00200000: Floor banners in front of the counter (4 banners) - // 00400000: Banners on both small walls in front of the battle tables - // 00800000: On southern platform - // 01000000: In front of jukebox - // 02000000: In western battle table corner (next to 4-player tables) - // 04000000: In eastern battle table corner (next to 2-player tables) - // 08000000: In southeastern battle table corner (next to 2-player tables) - // 10000000: In southwestern battle table corner (next to 4-player tables) - // 20000000: Just north-northwest of the counter - // 40000000: In front of the small wall in front of the 2-player battle tables - // 80000000: Inside the lobby counter, facing southeast - // Positions 00800000 and above appear to be intended for models and not - // banners - if a banner is sent in these locations, it appears sideways and - // halfway submerged in the floor, and has no collision. Furthermore, it seems - // that up to 8 different banners or models may be set simultaneously (though - // each may appear in more than one position). If 8 banners or objects already - // exist, further media sent via B9 is ignored. - le_uint32_t location_flags = 0x00000000; - // This field specifies the size of the compressed data. The uncompressed size - // is not sent anywhere in this command. - le_uint16_t size = 0; - le_uint16_t unused = 0; - // The PRS-compressed data immediately follows this header. The maximum size - // of the compressed data is 0x3800 bytes, and it must decompress to fewer - // than 0x37000 bytes of output. If either of these limits are violated, the - // client ignores the command. -} __packed_ws__(S_UpdateMediaHeader_Ep3_B9, 0x0C); - -// B9 (C->S): Confirm media update (Episode 3) -// No arguments -// This command is not valid on Episode 3 Trial Edition. -// The client sends this even if it ignores the contents of a B9 command. - -// BA: Meseta transaction (Episode 3) -// This command is not valid on Episode 3 Trial Edition. -// header.flag specifies the transaction purpose. Specific known values: -// 00 = unknown -// 01 = Initialize Meseta subsystem (C->S; always has a value of 0) -// 02 = Spend meseta (at e.g. lobby jukebox or Pinz's shop) (C->S) -// 03 = Spend meseta response (S->C; request_token must match the last token -// sent by client) -// 04 = unknown (C->S; request_token must match the last token sent by client) - -struct C_MesetaTransaction_Ep3_BA { - le_uint32_t transaction_num = 0; - le_uint32_t value = 0; - le_uint32_t request_token = 0; -} __packed_ws__(C_MesetaTransaction_Ep3_BA, 0x0C); - -struct S_MesetaTransaction_Ep3_BA { - le_uint32_t current_meseta = 0; - le_uint32_t total_meseta_earned = 0; - le_uint32_t request_token = 0; // Should match the token sent by the client -} __packed_ws__(S_MesetaTransaction_Ep3_BA, 0x0C); - -// BB (S->C): Tournament match information (Episode 3) -// This command is not valid on Episode 3 Trial Edition. Because of this, it -// must have been added fairly late in development, but it seems to be unused, -// perhaps because the E1/E3 commands are generally more useful... but the E1/E3 -// commands exist in Trial Edition! So why was this added? Was it just never -// finished? We may never know... -// header.flag is the number of valid match entries. - -struct S_TournamentMatchInformation_Ep3_BB { - pstring tournament_name; - struct TeamEntry { - le_uint16_t win_count = 0; - le_uint16_t is_active = 0; - pstring name; - } __packed_ws__(TeamEntry, 0x24); - parray team_entries; - le_uint16_t num_teams = 0; - le_uint16_t unknown_a3 = 0; // Probably actually unused - struct MatchEntry { - pstring name; - uint8_t locked = 0; - uint8_t count = 0; - uint8_t max_count = 0; - uint8_t unused = 0; - } __packed_ws__(MatchEntry, 0x24); - parray match_entries; -} __packed_ws__(S_TournamentMatchInformation_Ep3_BB, 0xDA4); - -// BC: Invalid command -// BD: Invalid command -// BE: Invalid command -// BF: Invalid command - -// C0 (C->S): Request choice search options (DCv2 and later versions) -// Internal name: GetChoiceList -// No arguments -// Server should respond with a C0 command (described below). - -// C0 (S->C): Choice search options (DCv2 and later versions) -// Internal name: RcvChoiceList - -// Command is a list of these; header.flag is the entry count (incl. top-level). -template -struct S_ChoiceSearchEntryT { - // Category IDs are nonzero; if the high byte of the ID is nonzero then the - // category can be set by the user at any time; otherwise it can't. - le_uint16_t parent_choice_id = 0; // 0 for top-level categories - le_uint16_t choice_id = 0; - pstring text; -} __packed__; -using S_ChoiceSearchEntry_DC_V3_C0 = S_ChoiceSearchEntryT; -using S_ChoiceSearchEntry_PC_BB_C0 = S_ChoiceSearchEntryT; -check_struct_size(S_ChoiceSearchEntry_DC_V3_C0, 0x20); -check_struct_size(S_ChoiceSearchEntry_PC_BB_C0, 0x3C); - -// Top-level categories are things like "Level", "Class", etc. -// Choices for each top-level category immediately follow the category, so -// a reasonable order of items is (for example): -// 00 00 11 01 "Preferred difficulty" -// 11 01 01 01 "Normal" -// 11 01 02 01 "Hard" -// 11 01 03 01 "Very Hard" -// 11 01 04 01 "Ultimate" -// 00 00 22 00 "Character class" -// 22 00 01 00 "HUmar" -// 22 00 02 00 "HUnewearl" -// etc. - -// C1 (C->S): Create game (DCv2 and later versions) -// Internal name: SndCreateGame - -template -struct C_CreateGameBaseT { - // menu_id and item_id are only used for the E7 (create spectator team) form - // of this command - le_uint32_t menu_id = 0; - le_uint32_t item_id = 0; - pstring name; - pstring password; -} __packed__; -using C_CreateGame_DCNTE = C_CreateGameBaseT; -check_struct_size(C_CreateGame_DCNTE, 0x28); - -template -struct C_CreateGameT : C_CreateGameBaseT { - uint8_t difficulty = 0; // 0-3 (always 0 on Episode 3) - uint8_t battle_mode = 0; // 0 or 1 (always 0 on Episode 3) - // Note: Episode 3 uses the challenge mode flag for view battle permissions. - // 0 = view battle allowed; 1 = not allowed - uint8_t challenge_mode = 0; // 0 or 1 - // Note: According to the Sylverant wiki, in v2-land, the episode field has a - // different meaning: if set to 0, the game can be joined by v1 and v2 - // players; if set to 1, it's v2-only. - uint8_t episode = 0; // 1-4 on V3+ (3 on Episode 3); unused on DC/PC -} __packed__; -using C_CreateGame_DC_V3_0C_C1_Ep3_EC = C_CreateGameT; -using C_CreateGame_PC_C1 = C_CreateGameT; -check_struct_size(C_CreateGame_DC_V3_0C_C1_Ep3_EC, 0x2C); -check_struct_size(C_CreateGame_PC_C1, 0x4C); - -struct C_CreateGame_BB_C1 : C_CreateGameT { - uint8_t solo_mode = 0; - parray unused2; -} __packed_ws__(C_CreateGame_BB_C1, 0x50); - -// C2 (C->S): Set choice search parameters (DCv2 and later versions) -// Internal name: PutChoiceList -// Server does not respond. -// Contents is a ChoiceSearchConfig, which is defined in PlayerSubordinates.hh. - -// C3 (C->S): Execute choice search (DCv2 and later versions) -// Internal name: SndChoiceSeq -// Same format as C2. The disabled field is unused. -// Server should respond with a C4 command. - -// C4 (S->C): Choice search results (DCv2 and later versions) -// Internal name: RcvChoiceAns -// There is a bug that can cause the client to crash or display garbage if this -// command is sent with no entries. To work around this, newserv sends a blank -// entry (but still with header.flag = 0) if there are no results. - -// Command is a list of these; header.flag is the entry count -template -struct S_ChoiceSearchResultEntryT_C4 { - le_uint32_t guild_card_number = 0; - pstring name; - pstring info_string; // Usually something like " Lvl " - // Format is stricter here; this is "LOBBYNAME,BLOCKNUM,SHIPNAME" - // If target is in game, for example, "Game Name,BLOCK01,Alexandria" - // If target is in lobby, for example, "BLOCK01-1,BLOCK01,Alexandria" - pstring location_string; - HeaderT reconnect_command_header; // Ignored by the client - S_Reconnect_19 reconnect_command; - SC_MeetUserExtensionT meet_user; -} __packed__; -using S_ChoiceSearchResultEntry_DC_V3_C4 = S_ChoiceSearchResultEntryT_C4; -using S_ChoiceSearchResultEntry_PC_C4 = S_ChoiceSearchResultEntryT_C4; -using S_ChoiceSearchResultEntry_BB_C4 = S_ChoiceSearchResultEntryT_C4; -check_struct_size(S_ChoiceSearchResultEntry_DC_V3_C4, 0xD4); -check_struct_size(S_ChoiceSearchResultEntry_PC_C4, 0x154); -check_struct_size(S_ChoiceSearchResultEntry_BB_C4, 0x158); - -// C5 (S->C): Player records update (DCv2 and later versions) -// Internal name: RcvChallengeData -// Command is a list of PlayerRecordsEntry structures; header.flag specifies -// the entry count. -// The server sends this command when a player joins a lobby to update the -// challenge mode records of all the present players. - -// C6 (C->S): Set blocked senders list (V3/BB) -// The command always contains the same number of entries, even if the entries -// at the end are blank (zero). - -template -struct C_SetBlockedSendersT_C6 { - parray blocked_senders; -} __packed__; -using C_SetBlockedSenders_V3_C6 = C_SetBlockedSendersT_C6<30>; -using C_SetBlockedSenders_BB_C6 = C_SetBlockedSendersT_C6<28>; -check_struct_size(C_SetBlockedSenders_V3_C6, 0x78); -check_struct_size(C_SetBlockedSenders_BB_C6, 0x70); - -// C7 (C->S): Enable simple mail auto-reply (V3/BB) -// Same format as 1A/D5 command (plain text). -// Server does not respond - -// C8 (C->S): Disable simple mail auto-reply (V3/BB) -// No arguments -// Server does not respond - -// C9: Broadcast command (Episode 3) -// Internal name: SndCardClientData -// Same as 60, but should be forwarded only to Episode 3 clients. -// newserv uses this command for all server-generated events (in response to CA -// commands), except for map data requests. This differs from Sega's original -// implementation, which sent CA responses via 60 commands instead. - -// C9 (C->S): Change connection status (Xbox) -// header.flag specifies if the player's online status should be hidden; 1 means -// shown, 2 means hidden. - -// CA (C->S): Server data request (Episode 3) -// Internal name: SndCardServerData -// The CA command format is the same as that of the 6xB3 commands, and the -// subsubcommands formats are shared as well. Unlike the 6x commands, the server -// is expected to respond to the command appropriately instead of forwarding it. -// Because the formats are shared, the 6xB3 commands are also referred to as CAx -// commands in the comments and structure names. - -// CB: Broadcast command (Episode 3) -// Internal name: SndKansenPsoData -// Same as 60, but only send to Episode 3 clients. -// This command's format is identical to C9, except that CB is not valid on -// Episode 3 Trial Edition (whereas C9 is valid). -// Unlike the 6x and C9 commands, subcommands sent with the CB command are -// forwarded from spectator teams to the primary team. The client only uses this -// behavior for the 6xBE command (sound chat), and newserv enforces that no -// other subcommand can be sent via CB. - -// CC (S->C): Confirm tournament entry (Episode 3) -// This command is not valid on Episode 3 Trial Edition. -// header.flag determines the client's registration state - 1 if the client is -// registered for the tournament, 0 if not. -// This command controls what's shown in the Check Tactics pane in the pause -// menu. If the client is registered (header.flag==1), the option is enabled and -// the bracket data in the command is shown there, and a third pane on the -// Status item shows the other information (tournament name, ship name, and -// start time). If the client is not registered, the Check Tactics option is -// disabled and the Status item has only two panes. - -struct S_ConfirmTournamentEntry_Ep3_CC { - pstring tournament_name; - le_uint16_t num_teams = 0; - le_uint16_t players_per_team = 0; - le_uint16_t unknown_a2 = 0; - le_uint16_t unknown_a3 = 0; - pstring server_name; - pstring start_time; // e.g. "15:09:30" or "13:03 PST" - struct TeamEntry { - le_uint16_t win_count = 0; - le_uint16_t is_active = 0; - pstring name; - } __packed_ws__(TeamEntry, 0x24); - parray team_entries; -} __packed_ws__(S_ConfirmTournamentEntry_Ep3_CC, 0x508); - -// CD: Invalid command -// CE: Invalid command -// CF: Invalid command - -// D0 (C->S): Start trade sequence (V3/BB) -// The trade window sequence is a bit complicated. The normal flow is: -// - Clients sync trade state with 6xA6 commands -// - When both have confirmed, one client (the initiator) sends a D0 -// - Server sends a D1 to the non-initiator -// - Non-initiator sends a D0 -// - Server sends a D1 to both clients -// - Both clients delete the sent items from their inventories (and send the -// appropriate subcommand) -// - Both clients send a D2 (similarly to how AC works, the server should not -// proceed until both D2s are received) -// - Server sends a D3 to both clients with each other's data from their D0s, -// followed immediately by a D4 01 to both clients, which completes the trade -// - Both clients send the appropriate subcommand to create inventory items -// TODO: On BB, is the server responsible for sending the appropriate item -// delete/create subcommands? -// At any point if an error occurs, either client may send a D4 00, which -// cancels the entire sequence. The server should then send D4 00 to both -// clients. -// TODO: The server should presumably also send a D4 00 if either client -// disconnects during the sequence. - -struct SC_TradeItems_D0_D3 { // D0 when sent by client, D3 when sent by server - le_uint16_t target_client_id = 0; - le_uint16_t item_count = 0; - // Note: PSO GC sends uninitialized data in the unused entries of this - // command. newserv parses and regenerates the item data when sending D3, - // which effectively erases the uninitialized data. - parray item_datas; -} __packed_ws__(SC_TradeItems_D0_D3, 0x284); - -// D1 (S->C): Advance trade state (V3/BB) -// No arguments -// See D0 description for usage information. - -// D2 (C->S): Trade can proceed (V3/BB) -// No arguments -// See D0 description for usage information. - -// D3 (S->C): Execute trade (V3/BB) -// On V3, this command has the same format as D0. See the D0 description for -// usage information. -// On BB, this command has no arguments (and the server generates the -// appropriate delete and create inventory item commands), but the D3 command -// must still must be sent before the D4 command to advance the trade state. - -// D4 (C->S): Trade failed (V3/BB) -// No arguments -// See D0 description for usage information. - -// D4 (S->C): Trade complete (V3/BB) -// header.flag must be 0 (trade failed) or 1 (trade complete). -// See D0 description for usage information. - -// D5: Large message box (V3/BB) -// Same as 1A command, except the maximum length of the message is 0x1000 bytes. - -// D6 (C->S): Large message box closed (V3) -// No arguments -// DC, PC, and BB do not send this command at all. GC US v1.0 and v1.1 will send -// this command when any large message box (1A/D5) is closed; GC Plus and -// Episode 3 will send D6 only for large message boxes that occur before the -// client has joined a lobby. (After joining a lobby, large message boxes will -// still be displayed if sent by the server, but the client won't send a D6 when -// they are closed.) In some of these versions, there is a bug that sets an -// incorrect interaction mode when the message box is closed while the player is -// in the lobby; some servers (e.g. Schtserv) send a lobby welcome message -// anyway, along with an 01 (lobby message box) which properly sets the -// interaction mode when closed. - -// D7 (C->S): Request GBA game file (V3) -// This command is sent when the client executes the file_dl_req (F8C0) quest -// opcode. header.flag contains the value of the opcode's first argument; the -// second argument is a pointer to the filename. -// The server should send the requested file using A6/A7 commands; if the file -// does not exist, the server should reply with a D7 command. -// This command exists on XB as well, but it presumably is never sent by the -// client. - -struct C_GBAGameRequest_V3_D7 { - pstring filename; -} __packed_ws__(C_GBAGameRequest_V3_D7, 0x10); - -// D7 (S->C): GBA file not found (V3/BB) -// No arguments -// This command is not valid on PSO GC Episodes 1&2 Trial Edition. -// This command tells the client that the file it requested via a D7 command -// does not exist. This causes the F8C1 (get_dl_status) quest opcode to return -// 0 (file not found), rather than 1 (download in progress) or 2 (complete). -// PSO BB accepts but completely ignores this command. - -// D8 (C->S): Info board request (V3/BB) -// No arguments -// The server should respond with a D8 command (described below). - -// D8 (S->C): Info board contents (V3/BB) -// This command is not valid on PSO GC Episodes 1&2 Trial Edition. - -// Command is a list of these; header.flag is the entry count. There should be -// one entry for each player in the current lobby/game. -template -struct S_InfoBoardEntryT_D8 { - pstring name; - pstring message; -} __packed__; -using S_InfoBoardEntry_V3_D8 = S_InfoBoardEntryT_D8; -using S_InfoBoardEntry_BB_D8 = S_InfoBoardEntryT_D8; -check_struct_size(S_InfoBoardEntry_V3_D8, 0xBC); -check_struct_size(S_InfoBoardEntry_BB_D8, 0x178); - -// D9 (C->S): Write info board (V3/BB) -// Contents are plain text, like 1A/D5. -// Server does not respond - -// DA (S->C): Change lobby event (V3/BB) -// header.flag = new event number; no other arguments. -// This command is not valid on PSO GC Episodes 1&2 Trial Edition. - -// DB (C->S): Verify license (V3/BB) -// Server should respond with a 9A command. - -struct C_VerifyAccount_V3_DB { - pstring unused; - pstring serial_number; // On XB, this is the XBL gamertag - pstring access_key; // On XB, this is the XBL user ID - pstring unused2; - le_uint32_t sub_version = 0; - pstring serial_number2; // On XB, this is the XBL gamertag - pstring access_key2; // On XB, this is the XBL user ID - pstring password; // On XB, this contains "xbox-pso" -} __packed_ws__(C_VerifyAccount_V3_DB, 0xDC); - -// Note: This login pathway generally isn't used on BB (and isn't supported at -// all during the data server phase). All current servers use 03/93 instead. -struct C_VerifyAccount_BB_DB { - // Note: These four fields are likely the same as those used in BB's 9E - pstring unknown_a3; // Always blank? - pstring unknown_a4; // == "?" - pstring unknown_a5; // Always blank? - pstring unknown_a6; // Always blank? - le_uint32_t sub_version = 0; - pstring username; - pstring password; - pstring game_tag; // "psopc2" -} __packed_ws__(C_VerifyAccount_BB_DB, 0xD4); - -// DC: Set battle in progress flag (Episode 3) -// No arguments except header.flag when sent by the client. When header.flag is -// 1, the game should be locked - no players should be allowed to join. In this -// case, the client waits for the server to respond with another DC command -// before proceeding with battle setup. When header.flag is 0, the game should -// be unlocked, and the client does not wait for a response from the server. - -// DC: Guild card data (BB) - -struct S_GuildCardHeader_BB_01DC { - le_uint32_t unknown = 1; - le_uint32_t filesize = 0x0000D590; - le_uint32_t checksum = 0; // CRC32 of entire guild card file (0xD590 bytes) -} __packed_ws__(S_GuildCardHeader_BB_01DC, 0x0C); - -struct S_GuildCardFileChunk_02DC { - le_uint32_t unknown = 0; // 0 - le_uint32_t chunk_index = 0; - parray data; // Command may be shorter if this is the last chunk -} __packed_ws__(S_GuildCardFileChunk_02DC, 0x6808); - -struct C_GuildCardDataRequest_BB_03DC { - le_uint32_t unknown = 0; - le_uint32_t chunk_index = 0; - le_uint32_t cont = 0; -} __packed_ws__(C_GuildCardDataRequest_BB_03DC, 0x0C); - -// DD (S->C): Send quest state to joining player (BB) -// When a player joins a game with a quest already in progress, the server -// should send this command to the leader. header.flag is the client ID that the -// leader should send quest state to; the leader will then send a series of -// target commands (62/6D) that the server can forward to the joining player. -// No other arguments - -// DE (S->C): Rare monster list (BB) - -struct S_RareMonsterList_BB_DE { - // Unused entries are set to FFFF - parray enemy_indexes; -} __packed_ws__(S_RareMonsterList_BB_DE, 0x20); - -// DF (C->S): Set Challenge Mode parameters (BB) -// This command has 7 subcommands, most of which are self-explanatory. - -struct C_SetChallengeModeStageNumber_BB_01DF { - le_uint32_t stage = 0; -} __packed_ws__(C_SetChallengeModeStageNumber_BB_01DF, 4); - -struct C_SetChallengeModeCharacterTemplate_BB_02DF { - le_uint32_t template_index = 0; -} __packed_ws__(C_SetChallengeModeCharacterTemplate_BB_02DF, 4); - -struct C_SetChallengeModeDifficulty_BB_03DF { - // No existing challenge mode quest sets this to a value other than zero. - le_uint32_t difficulty = 0; -} __packed_ws__(C_SetChallengeModeDifficulty_BB_03DF, 4); - -struct C_SetChallengeModeEXPMultiplier_BB_04DF { - le_float exp_multiplier = 1.0f; -} __packed_ws__(C_SetChallengeModeEXPMultiplier_BB_04DF, 4); - -struct C_SetChallengeRankText_BB_05DF { - le_uint32_t rank_color = 0; // ARGB8888 - pstring rank_text; -} __packed_ws__(C_SetChallengeRankText_BB_05DF, 0x1C); - -// This is sent once for each rank (so, 3 times in total) -struct C_SetChallengeRankThreshold_BB_06DF { - le_uint32_t rank = 0; // 0 = B, 1 = A, 2 = S - le_uint32_t seconds = 0; - le_uint32_t rank_bitmask = 0; // 1 = B, 2 = A, 4 = S -} __packed_ws__(C_SetChallengeRankThreshold_BB_06DF, 0x0C); - -struct C_CreateChallengeModeAwardItem_BB_07DF { - le_uint32_t prize_rank = 0xFFFFFFFF; // 0 = B, 1 = A, 2 = S, anything else = error - le_uint32_t rank_bitmask = 0; // Same as in 06DF - ItemData item; -} __packed_ws__(C_CreateChallengeModeAwardItem_BB_07DF, 0x1C); - -// E0 (S->C): Tournament list (Episode 3) -// The client will send 09 and 10 commands to inspect or enter a tournament. The -// server should respond to an 09 command with an E3 command; the server should -// respond to a 10 command with an E2 command. -// header.flag is the count of filled-in entries. - -struct S_TournamentList_Ep3NTE_E0 { - struct Entry { - le_uint32_t menu_id = 0; - le_uint32_t item_id = 0; - uint8_t unknown_a1 = 0; - uint8_t locked = 0; - uint8_t state = 0; - uint8_t unknown_a2 = 0; - le_uint32_t start_time = 0; // In seconds since Unix epoch - pstring name; - le_uint16_t num_teams = 0; - le_uint16_t max_teams = 0; - } __packed_ws__(Entry, 0x34); - parray entries; -} __packed_ws__(S_TournamentList_Ep3NTE_E0, 0x680); - -struct S_TournamentList_Ep3_E0 { - struct Entry { - le_uint32_t menu_id = 0; - le_uint32_t item_id = 0; - uint8_t unknown_a1 = 0; - uint8_t locked = 0; // If nonzero, the lock icon appears in the menu - // Values for the state field: - // 00 = Preparing - // 01 = 1st Round - // 02 = 2nd Round - // 03 = 3rd Round - // 04 = Semifinals - // 05 = Entries no longer accepted - // 06 = Finals - // 07 = Preparing for Battle - // 08 = Battle in progress - // 09 = Preparing to view Battle - // 0A = Viewing a Battle - // Values beyond 0A don't appear to cause problems, but cause strings to - // appear that are obviously not intended to appear in the tournament list, - // like "View the board" and "Board: Write". (In fact, some of the strings - // listed above may be unintended for this menu as well.) - uint8_t state = 0; - uint8_t unknown_a2 = 0; - le_uint32_t start_time = 0; // In seconds since Unix epoch - pstring name; - le_uint16_t num_teams = 0; - le_uint16_t max_teams = 0; - le_uint16_t unknown_a3 = 0xFFFF; - le_uint16_t unknown_a4 = 0xFFFF; - } __packed_ws__(Entry, 0x38); - parray entries; -} __packed_ws__(S_TournamentList_Ep3_E0, 0x700); - -// E0 (C->S): Request system file (BB) -// No arguments. The server should respond with an E1 or E2 command. - -// E1 (S->C): Game information (Episode 3) -// The header.flag argument determines which fields are valid (and which panes -// should be shown in the information window). The values are the same as for -// the E3 command, but each value only makes sense for one command. That is, 00, -// 01, and 04 should be used with the E1 command, while 02, 03, and 05 should be -// used with the E3 command. See the E3 command for descriptions of what each -// flag value means. - -template -struct S_GameInformationBaseT_Ep3_E1 { - /* 0000 */ pstring game_name; - struct PlayerEntry { - pstring name; // From disp.name - pstring description; // Usually something like "FOmarl CLv30 J" - } __packed_ws__(PlayerEntry, 0x30); - /* 0020 */ parray player_entries; - /* 00E0 */ pstring map_name; - /* 0100 */ RulesT rules; - /* 0114 */ parray spectator_entries; - /* 0294 */ -} __packed__; -using S_GameInformation_Ep3NTE_E1 = S_GameInformationBaseT_Ep3_E1; -using S_GameInformation_Ep3_E1 = S_GameInformationBaseT_Ep3_E1; -check_struct_size(S_GameInformation_Ep3NTE_E1, 0x28C); -check_struct_size(S_GameInformation_Ep3_E1, 0x294); - -// E1 (S->C): System file created (BB) -// This seems to take the place of 00E2 in certain cases. Perhaps it was used -// when a client hadn't logged in before and didn't have a system file, so the -// client should use appropriate defaults. - -struct S_SystemFileCreated_00E1_BB { - // If success is not equal to 1, the client shows a message saying "Forced - // server disconnect (907)" and disconnects. Otherwise, the client proceeeds - // as if it had received an 00E2 command, and sends its first 00E3. - le_uint32_t success = 1; -} __packed_ws__(S_SystemFileCreated_00E1_BB, 4); - -// E2 (C->S): Tournament control (Episode 3) -// No arguments (in any of its forms) except header.flag, which determines ths -// command's meaning. Specifically: -// flag=00: request tournament list (server responds with E0) -// flag=01: check tournament (server responds with E2) -// flag=02: cancel tournament entry (server responds with CC) -// flag=03: create tournament spectator team (server responds with E0) -// flag=04: join tournament spectator team (server responds with E0) -// In case 02, the resulting CC command has header.flag = 0 to indicate the -// player is no longer registered. -// In cases 03 and 04, the client handles follow-ups differently from the 00 -// case. In case 04, the client will send a 10 (to which the server responds -// with an E2), but when a choice is made from that menu, the client sends E6 01 -// instead of 10. In case 03, the flow is similar, but the client sends E7 -// instead of E6 01. -// newserv responds with a standard ship info box (11), but this seems not to be -// intended since it partially overlaps some windows. - -// E2 (S->C): Tournament entry list (Episode 3) -// Client may send 09 commands if the player presses X. It's not clear what the -// server should respond with in this case. -// If the player selects an entry slot, client will respond with a long-form 10 -// command (the Flag03 variant); in this case, unknown_a1 is the team name, and -// password is the team password. The server should respond to that with a CC -// command. - -struct S_TournamentEntryList_Ep3_E2 { - le_uint16_t players_per_team = 0; - le_uint16_t unused = 0; - struct Entry { - le_uint32_t menu_id = 0; - le_uint32_t item_id = 0; - uint8_t unknown_a1 = 0; - // If locked is nonzero, a lock icon appears next to this team and the - // player is prompted for a password if they select this team. - uint8_t locked = 0; - // State values: - // 00 = empty (team_name is ignored; entry is selectable) - // 01 = present, joinable (team_name renders in white) - // 02 = present, finalized (team_name renders in yellow) - // If state is any other value, the entry renders as if its state were 02, - // but cannot be selected at all (the menu cursor simply skips over it). - uint8_t state = 0; - uint8_t unknown_a2 = 0; - pstring name; - } __packed_ws__(Entry, 0x2C); - parray entries; -} __packed_ws__(S_TournamentEntryList_Ep3_E2, 0x584); - -// E2 (S->C): Set system file contents (BB) -// See PSOBBFullSystemFile in SaveFileFormats.hh for format - -// E3 (S->C): Game or tournament info (Episode 3) -// The header.flag argument determines which fields are valid (and which panes -// should be shown in the information window). The values are: -// flag=00: Opponents pane only -// flag=01: Opponents and Rules panes -// flag=02: Rules and bracket panes (the bracket pane uses the tournament's name -// as its title) -// flag=03: Opponents, Rules, and bracket panes -// flag=04: Spectators and Opponents pane -// flag=05: Spectators, Opponents, Rules, and bracket panes -// Sending other values in the header.flag field results in a blank info window -// with unintended strings appearing in the window title. -// Presumably the cases above would be used in different scenarios, probably: -// 00: When inspecting a non-tournament game with no battle in progress -// 01: When inspecting a non-tournament game with a battle in progress -// 02: When inspecting tournaments that have not yet started -// 03: When inspecting a tournament match -// 04: When inspecting a non-tournament spectator team -// 05: When inspecting a tournament spectator team -// The 00, 01, and 04 cases don't really make sense, because the E1 command is -// more appropriate for inspecting non-tournament games. - -template -struct S_TournamentGameDetailsBaseT_Ep3_E3 { - // These fields are used only if the Rules pane is shown - /* 0000 */ pstring name; - /* 0020 */ pstring map_name; - /* 0040 */ RulesT rules; - - // This field is used only if the bracket pane is shown - struct BracketEntry { - le_uint16_t win_count = 0; - le_uint16_t is_active = 0; - pstring team_name; - parray unused; - } __packed_ws__(BracketEntry, 0x24); - /* 0054 */ parray bracket_entries; - - // This field is used only if the Opponents pane is shown. If players_per_team - // is 2, all fields are shown; if player_per_team is 1, team_name and - // players[1] is ignored (only players[0] is shown). - struct PlayerEntry { - pstring name; - pstring description; // Usually something like "RAmarl CLv24 E" - } __packed_ws__(PlayerEntry, 0x30); - struct TeamEntry { - pstring team_name; - parray players; - } __packed_ws__(TeamEntry, 0x70); - /* 04D4 */ parray team_entries; - - /* 05B4 */ le_uint16_t num_bracket_entries = 0; - /* 05B6 */ le_uint16_t players_per_team = 0; - /* 05B8 */ le_uint16_t unknown_a4 = 0; - /* 05BA */ le_uint16_t num_spectators = 0; - /* 05BC */ parray spectator_entries; - /* 073C */ -} __packed__; -using S_TournamentGameDetails_Ep3NTE_E3 = S_TournamentGameDetailsBaseT_Ep3_E3; -using S_TournamentGameDetails_Ep3_E3 = S_TournamentGameDetailsBaseT_Ep3_E3; -check_struct_size(S_TournamentGameDetails_Ep3NTE_E3, 0x734); -check_struct_size(S_TournamentGameDetails_Ep3_E3, 0x73C); - -// E3 (C->S): Player preview request (BB) - -struct C_PlayerPreviewRequest_BB_E3 { - le_int32_t character_index = 0; - le_uint32_t unused = 0; -} __packed_ws__(C_PlayerPreviewRequest_BB_E3, 0x08); - -// E4: CARD lobby battle table state (Episode 3) -// When client sends an E4, server should respond with another E4 (but these -// commands have different formats). -// When the client has received an E4 command in which all entries have state 0 -// or 2, the client will stop the player from moving and show a message saying -// that the game will begin shortly. The server should send a 64 command shortly -// thereafter. - -// header.flag = seated state (1 = present, 0 = leaving) -struct C_CardBattleTableState_Ep3_E4 { - le_uint16_t table_number = 0; - le_uint16_t seat_number = 0; -} __packed_ws__(C_CardBattleTableState_Ep3_E4, 4); - -// header.flag = table number -struct S_CardBattleTableState_Ep3_E4 { - struct Entry { - // State values: - // 0 = no player present - // 1 = player present, not confirmed - // 2 = player present, confirmed - // 3 = player present, declined - le_uint16_t state = 0; - le_uint16_t unknown_a1 = 0; - le_uint32_t guild_card_number = 0; - } __packed_ws__(Entry, 8); - parray entries; -} __packed_ws__(S_CardBattleTableState_Ep3_E4, 0x20); - -// E4 (S->C): Player choice or no player present (BB) - -struct S_ApprovePlayerChoice_BB_00E4 { - le_int32_t character_index = 0; - le_uint32_t result = 0; // 1 = approved -} __packed_ws__(S_ApprovePlayerChoice_BB_00E4, 8); - -struct S_PlayerPreview_NoPlayer_BB_00E4 { - le_int32_t character_index = 0; - le_uint32_t error = 0; // 2 = no player present -} __packed_ws__(S_PlayerPreview_NoPlayer_BB_00E4, 8); - -// E5 (C->S): Confirm CARD lobby battle table choice (Episode 3) -// header.flag specifies whether the client answered "Yes" (1) or "No" (0). - -struct S_CardBattleTableConfirmation_Ep3_E5 { - le_uint16_t table_number = 0; - le_uint16_t seat_number = 0; -} __packed_ws__(S_CardBattleTableConfirmation_Ep3_E5, 4); - -// E5 (S->C): Player preview (BB) -// E5 (C->S): Create character (BB) - -struct SC_PlayerPreview_CreateCharacter_BB_00E5 { - le_int32_t character_index = 0; - PlayerDispDataBBPreview preview; -} __packed_ws__(SC_PlayerPreview_CreateCharacter_BB_00E5, 0x80); - -// E6 (C->S): Spectator team control (Episode 3) - -// With header.flag == 0, this command has no arguments and is used for -// requesting the spectator team list. The server responds with an E6 command. - -// With header.flag == 1, this command is used for joining a tournament -// spectator team. The following arguments are given in this form: - -struct C_JoinSpectatorTeam_Ep3_E6_Flag01 { - le_uint32_t menu_id = 0; - le_uint32_t item_id = 0; -} __packed_ws__(C_JoinSpectatorTeam_Ep3_E6_Flag01, 8); - -// E6 (S->C): Spectator team list (Episode 3) -// Same format as 08 command. - -// E6 (S->C): Set guild card number and update client config (BB) -// This command sets the player's guild card number. During the data server -// phase, it also sets the client config and enabled features (these fields are -// ignored during the game server phase). - -struct S_ClientInit_BB_00E6 { - le_uint32_t error_code = 0; - le_uint32_t player_tag = 0x00010000; - le_uint32_t guild_card_number = 0; - // If security_token is zero, the client scrambles client_config before - // sending it back in a later 93 command. See scramble_bb_security_data in - // ReceiveCommands.cc for details on how this is done. - le_uint32_t security_token = 0; - parray client_config; - uint8_t can_create_team = 1; - uint8_t episode_4_unlocked = 1; - parray unused; -} __packed_ws__(S_ClientInit_BB_00E6, 0x3C); - -// E7 (C->S): Create spectator team (Episode 3) -// This command is used to create speectator teams for both tournaments and -// regular games. The server should be able to tell these cases apart by the -// menu and/or item ID. - -struct C_CreateSpectatorTeam_Ep3_E7 { - le_uint32_t menu_id = 0; - le_uint32_t item_id = 0; - pstring name; - pstring password; - le_uint32_t unused = 0; -} __packed_ws__(C_CreateSpectatorTeam_Ep3_E7, 0x2C); - -// E7 (S->C): Tournament entry list for spectating (Episode 3) -// Same format as E2 command. - -// E7: Sync save files (BB) - -struct SC_SyncSaveFiles_BB_E7 { - /* 0000 */ PSOBBCharacterFile char_file; - /* 2EA4 */ PSOBBFullSystemFile system_file; - /* 3994 */ -} __packed_ws__(SC_SyncSaveFiles_BB_E7, 0x3994); - -// E8 (S->C): Join spectator team (Episode 3) -// header.flag = player count (including spectators) -// The client will crash if leader_id == client_id. Presumably one of the -// primary game's players should be the leader (this is what newserv does). - -struct S_JoinSpectatorTeam_Ep3_E8 { - /* 0000 */ parray variations; // unused - struct PlayerEntry { - /* 0000 */ PlayerLobbyDataDCGC lobby_data; - /* 0020 */ PlayerInventory inventory; - /* 036C */ PlayerDispDataDCPCV3 disp; - /* 043C */ - } __packed_ws__(PlayerEntry, 0x43C); - /* 0080 */ parray players; - /* 1170 */ uint8_t client_id = 0; - /* 1171 */ uint8_t leader_id = 0; - /* 1172 */ uint8_t disable_udp = 1; - /* 1173 */ uint8_t difficulty = 0; - /* 1174 */ uint8_t battle_mode = 0; - /* 1175 */ uint8_t event = 0; - /* 1176 */ uint8_t section_id = 0; - /* 1177 */ uint8_t challenge_mode = 0; - /* 1178 */ le_uint32_t rare_seed = 0; - /* 117C */ uint8_t episode = 0; - /* 117D */ parray unused; - struct SpectatorEntry { - // It seems that at some point Sega intended to show each player's rank in - // spectator teams. The unused1 and unused3 fields are intended for the - // player's encrypted rank text and rank color (according to old Sega logs), - // but the client ignores them. It's not clear what unused4 may have been - // for, but the client also completely ignores it. - /* 00 */ le_uint32_t player_tag = 0; - /* 04 */ le_uint32_t guild_card_number = 0; - /* 08 */ pstring name; - /* 18 */ pstring unused1; - /* 28 */ uint8_t present = 0; - /* 29 */ uint8_t unused2 = 0; - /* 2A */ le_uint16_t level = 0; - /* 2C */ le_uint32_t unused3 = 0xFFFFFFFF; - /* 30 */ le_uint32_t name_color = 0xFFFFFFFF; // ARGB8888 - /* 34 */ parray unused4; - /* 38 */ - } __packed_ws__(SpectatorEntry, 0x38); - // Somewhat misleadingly, this array also includes the players actually in the - // battle - they appear in the first positions. Presumably the first 4 are - // always for battlers, and the last 8 are always for spectators. - /* 1180 */ parray entries; - /* 1420 */ pstring spectator_team_name; - // This field doesn't appear to be actually used by the game, but some servers - // send it anyway (and the game ignores it) - /* 1440 */ parray spectator_players; - /* 3620 */ -} __packed_ws__(S_JoinSpectatorTeam_Ep3_E8, 0x3620); - -// E8 (C->S): Guild card commands (BB) - -// 01E8 (C->S): Check guild card file checksum - -// This struct is for documentation purposes only; newserv ignores the contents -// of this command. -struct C_GuildCardChecksum_01E8 { - le_uint32_t checksum = 0; - le_uint32_t unused = 0; -} __packed_ws__(C_GuildCardChecksum_01E8, 8); - -// 02E8 (S->C): Accept/decline guild card file checksum -// If needs_update is nonzero, the client will request the guild card file by -// sending an 03E8 command. If needs_update is zero, the client will skip -// downloading the guild card file and send a 04EB command (requesting the -// stream file) instead. - -struct S_GuildCardChecksumResponse_BB_02E8 { - le_uint32_t needs_update = 0; - le_uint32_t unused = 0; -} __packed_ws__(S_GuildCardChecksumResponse_BB_02E8, 8); - -// 03E8 (C->S): Request guild card file -// No arguments -// Server should send the guild card file data using DC commands. - -// 04E8 (C->S): Add guild card -// Format is GuildCardBB (see PlayerSubordinates.hh) - -// 05E8 (C->S): Delete guild card - -struct C_DeleteGuildCard_BB_05E8_08E8 { - le_uint32_t guild_card_number = 0; -} __packed_ws__(C_DeleteGuildCard_BB_05E8_08E8, 4); - -// 06E8 (C->S): Update (overwrite) guild card -// Note: This command is also sent when the player writes a comment on their own -// guild card. -// Format is GuildCardBB (see PlayerSubordinates.hh) - -// 07E8 (C->S): Add blocked user -// Format is GuildCardBB (see PlayerSubordinates.hh) - -// 08E8 (C->S): Delete blocked user -// Same format as 05E8. - -// 09E8 (C->S): Write comment - -struct C_WriteGuildCardComment_BB_09E8 { - le_uint32_t guild_card_number = 0; - pstring comment; -} __packed_ws__(C_WriteGuildCardComment_BB_09E8, 0xB4); - -// 0AE8 (C->S): Swap positions of guild cards in list - -struct C_SwapGuildCardPositions_BB_0AE8 { - le_uint32_t guild_card_number1 = 0; - le_uint32_t guild_card_number2 = 0; -} __packed_ws__(C_SwapGuildCardPositions_BB_0AE8, 8); - -// E9 (S->C): Remove player from spectator team (Episode 3) -// Same format as 66/69 commands. Like 69 (and unlike 66), the disable_udp field -// is unused in command E9. When a spectator leaves a spectator team, the -// primary players should receive a 6xB4x52 command to update their spectator -// counts. - -// EA (S->C): Timed message box (Episode 3) -// The message appears in the upper half of the screen; the box is as wide as -// the 1A/D5 box but is vertically shorter. The box cannot be dismissed or -// interacted with by the player in any way; it disappears by itself after the -// given number of frames. -// header.flag appears to be relevant - the handler's behavior is different if -// it's 1 (vs. any other value). There don't seem to be any in-game behavioral -// differences though. - -struct S_TimedMessageBoxHeader_Ep3_EA { - le_uint32_t duration = 0; // In frames; 30 frames = 1 second - // Message data follows here (up to 0x1000 chars) -} __packed_ws__(S_TimedMessageBoxHeader_Ep3_EA, 4); - -// EA: Team control (BB) - -// 01EA (C->S): Create team - -struct C_CreateTeam_BB_01EA { - pstring name; -} __packed_ws__(C_CreateTeam_BB_01EA, 0x20); - -// 02EA (S->C): Create team result -// No arguments except header.flag, which specifies the error code. Values: -// 0 = success -// 1 = generic error -// 2 = name already registered -// 3 = generic error -// 4 = generic error -// 5 = generic error -// 6 = generic error -// Anything else = command is ignored - -// 03EA (C->S): Add team member - -struct C_AddOrRemoveTeamMember_BB_03EA_05EA { - le_uint32_t guild_card_number = 0; -} __packed_ws__(C_AddOrRemoveTeamMember_BB_03EA_05EA, 4); - -// 04EA (S->C): Add team member result -// No arguments except header.flag, which specifies the error code. Values: -// 0 = success -// 5 = team is full -// Anything else = generic error - -// 05EA (C->S): Remove team member -// Same format as 03EA. - -// 06EA (S->C): Remove team member result -// No arguments except header.flag, which specifies the error code. 0 means -// success, but it's not known what any other values mean. The client expects -// the error code to be less than 7. - -// 07EA: Team chat - -struct SC_TeamChat_BB_07EA { - pstring sender_name; - // Text follows here. The message is truncated by the client if it is longer - // than 0x8F wchar_ts. -} __packed_ws__(SC_TeamChat_BB_07EA, 0x20); - -// 08EA (C->S): Get team member list -// No arguments - -// 09EA (S->C): Team member list - -struct S_TeamMemberList_BB_09EA { - le_uint32_t entry_count = 0; - struct Entry { - // This is displayed as "<%04d> %s" % (rank, name) - le_uint32_t rank = 0; - le_uint32_t privilege_level = 0; // 0x10 or 0x20 = green, 0x30 = blue, 0x40 = red, anything else = white - le_uint32_t guild_card_number = 0; - pstring name; - } __packed_ws__(Entry, 0x2C); - // Variable-length field: - // Entry entries[entry_count]; -} __packed_ws__(S_TeamMemberList_BB_09EA, 4); - -// 0CEA (S->C): Unknown -// The client appears to ignore this command. - -struct S_Unknown_BB_0CEA { - parray unknown_a1; - // Text follows here -} __packed_ws__(S_Unknown_BB_0CEA, 0x20); - -// 0DEA (C->S): Get team name -// No arguments - -// 0EEA (S->C): Team name - -struct S_TeamName_BB_0EEA { - parray unused; - pstring team_name; -} __packed_ws__(S_TeamName_BB_0EEA, 0x30); - -// 0FEA (C->S): Set team flag -// The client also accepts this command but completely ignores it. - -struct C_SetTeamFlag_BB_0FEA { - parray flag_data; -} __packed_ws__(C_SetTeamFlag_BB_0FEA, 0x800); - -// 10EA: Delete team (C->S) and result (S->C) -// No arguments (C->S) -// No arguments except header.flag (S->C) - -// 11EA: Change team member privilege level -// The format below is used only when the client sends this command; when the -// server sends it, only header.flag is used. As with various other team -// commands, header.flag specifies the error code in this case. -// header.flag specifies the new privilege level for the specified team member. -// Known values: 0 = normal, 0x30 = leader, 0x40 = master - -struct C_ChangeTeamMemberPrivilegeLevel_BB_11EA { - le_uint32_t guild_card_number = 0; -} __packed_ws__(C_ChangeTeamMemberPrivilegeLevel_BB_11EA, 4); - -// 12EA (S->C): Team membership information -// If the client is not in a team, all fields should be zero. - -struct S_TeamMembershipInformation_BB_12EA { - le_uint32_t unknown_a1 = 0; - le_uint32_t guild_card_number = 0; - le_uint32_t team_id = 0; - le_uint32_t unknown_a4 = 0; - le_uint32_t unknown_a6 = 0; - uint8_t privilege_level = 0; - uint8_t team_member_count = 0; - uint8_t unknown_a8 = 0; - uint8_t unknown_a9 = 0; - pstring team_name; -} __packed_ws__(S_TeamMembershipInformation_BB_12EA, 0x38); - -// 13EA: Team info for lobby players -// header.flag specifies the number of entries. - -struct S_TeamInfoForPlayer_BB_13EA_15EA_Entry { - // The client uses the first four of these to determine if the player is in a - // team or not - if they are all zero, the player is not in a team. - /* 0000 */ le_uint32_t guild_card_number = 0; - /* 0004 */ le_uint32_t team_id = 0; - /* 0008 */ le_uint32_t reward_flags = 0; - /* 000C */ le_uint32_t unknown_a6 = 0; - /* 0010 */ uint8_t privilege_level = 0; - /* 0011 */ uint8_t team_member_count = 0; - /* 0012 */ uint8_t unknown_a8 = 0; - /* 0013 */ uint8_t unknown_a9 = 0; - /* 0014 */ pstring team_name; - /* 0034 */ le_uint32_t guild_card_number2 = 0; - /* 0038 */ le_uint32_t lobby_client_id = 0; - /* 003C */ pstring player_name; - /* 005C */ parray flag_data; - /* 085C */ -} __packed_ws__(S_TeamInfoForPlayer_BB_13EA_15EA_Entry, 0x85C); - -// 14EA (C->S): Get team info for lobby players -// No arguments. Client always sends 1 in the header.flag field. - -// 15EA (S->C): Team info for lobby players -// header.flag specifies the number of entries. The entry format appears to be -// the same as for the 13EA command. - -// 16EA (S->C): Transfer item via Simple Mail result -// No arguments except header.flag, which is 0 if the transfer failed and -// nonzero if it succeeded. - -// 18EA: Intra-team ranking information -// No arguments (C->S) - -struct S_IntraTeamRanking_BB_18EA { - /* 0000 */ le_uint32_t ranking_points = 0; - /* 0004 */ le_uint32_t unknown_a2 = 0; - /* 0008 */ le_uint32_t points_remaining = 0; - /* 000C */ le_uint32_t num_entries = 1; - struct Entry { - /* 00 */ le_uint32_t rank = 0; - /* 04 */ le_uint32_t privilege_level = 0; - /* 08 */ le_uint32_t guild_card_number = 0; - /* 0C */ pstring player_name; - /* 2C */ le_uint32_t points = 0; - /* 30 */ - } __packed_ws__(Entry, 0x30); - // Variable-length field: - /* 0010 */ // Entry entries[num_entries]; -} __packed_ws__(S_IntraTeamRanking_BB_18EA, 0x10); - -// 19EA: Team reward list -// No arguments (C->S) - -struct S_TeamRewardList_BB_19EA_1AEA { - le_uint32_t num_entries; - struct Entry { - /* 0000 */ pstring name; - /* 0080 */ pstring description; - /* 0180 */ le_uint32_t team_points = 0; - /* 0184 */ le_uint32_t reward_id = 0; - /* 0188 */ - } __packed_ws__(Entry, 0x188); - // Variable length field: - // Entry entries[num_entries]; -} __packed_ws__(S_TeamRewardList_BB_19EA_1AEA, 4); - -// 1AEA: Team rewards available for purchase -// Same format as 19EA. - -// 1BEA (C->S): Buy team reward -// No arguments except header.flag, which specifies a reward_id from a preceding -// 1AEA command. - -// 1CEA: Cross-team ranking information -// No arguments when sent by the client. - -struct S_CrossTeamRanking_BB_1CEA { - le_uint32_t num_entries; - struct Entry { - /* 00 */ pstring team_name; - /* 20 */ le_uint32_t team_points = 0; - /* 24 */ le_uint32_t unknown_a1 = 0; - /* 28 */ - } __packed_ws__(Entry, 0x28); - // Variable length field: - // Entry entries[num_entries]; -} __packed_ws__(S_CrossTeamRanking_BB_1CEA, 4); - -// 1DEA (S->C): Update team rewards bitmask -// header.flag specifies the new rewards bitmask. - -// 1EEA (C->S): Rename team -// header.flag is used, but it's unknown what the value means. - -struct C_RenameTeam_BB_1EEA { - pstring new_team_name; -} __packed_ws__(C_RenameTeam_BB_1EEA, 0x20); - -// 1FEA (S->C): Rename team result -// This command behaves like 02EA, but is sent in response to 1EEA instead. - -// 20EA: Unknown -// header.flag is used, but no other arguments. When sent by the server, -// header.flag is an error code, similar to various other result commands in -// this section. - -// EB (S->C): Add player to spectator team (Episode 3) -// Same format and usage as 65 and 68 commands, but sent to spectators in a -// spectator team. -// This command is used to add both primary players and spectators - if the -// client ID in .lobby_data is 0-3, it's a primary player, otherwise it's a -// spectator. (In the case of a primary player joining, the other primary -// players in the game receive a 65 command rather than an EB command to notify -// them of the joining player; in the case of a joining spectator, the primary -// players receive a 6xB4x52 instead.) - -// 01EB (S->C): Send stream file index (BB) - -// Command is a list of these; header.flag is the entry count. -struct S_StreamFileIndexEntry_BB_01EB { - le_uint32_t size = 0; - le_uint32_t checksum = 0; // CRC32 of file data - le_uint32_t offset = 0; // offset in stream (== sum of all previous files' sizes) - pstring filename; -} __packed_ws__(S_StreamFileIndexEntry_BB_01EB, 0x4C); - -// 02EB (S->C): Send stream file chunk (BB) - -struct S_StreamFileChunk_BB_02EB { - le_uint32_t chunk_index = 0; - parray data; -} __packed_ws__(S_StreamFileChunk_BB_02EB, 0x6804); - -// 03EB (C->S): Request a specific stream file chunk -// header.flag is the chunk index. Server should respond with a 02EB command. - -// 04EB (C->S): Request stream file header -// No arguments -// Server should respond with a 01EB command. - -// EC (C->S): Create game (Episode 3) -// Same format as C1; some fields are unused (e.g. episode, difficulty). - -// EC (C->S): Leave character select (BB) - -struct C_LeaveCharacterSelect_BB_00EC { - // Reason codes: - // 0 = canceled - // 1 = recreate character - // 2 = dressing room - le_uint32_t reason = 0; -} __packed_ws__(C_LeaveCharacterSelect_BB_00EC, 4); - -// ED (S->C): Force leave lobby/game (Episode 3) -// No arguments -// This command forces the client out of the game or lobby they're currently in -// and sends them to the lobby. If the client is in a lobby (and not a game), -// the client sends a 98 in response as if they were in a game. Curiously, the -// client also sends a meseta transaction (BA) with a value of zero before -// sending an 84 to be added to a lobby. This is used when a spectator team is -// disbanded because the target game ends. - -// ED (C->S): Update save file data (BB) -// There are several subcommands (noted in the structs below) that each update a -// specific kind of data. -// TODO: Actually define these structures and don't just treat them as raw data - -struct C_UpdateOptionFlags_BB_01ED { - le_uint32_t option_flags = 0; -} __packed_ws__(C_UpdateOptionFlags_BB_01ED, 4); - -struct C_UpdateSymbolChats_BB_02ED { - parray symbol_chats; -} __packed_ws__(C_UpdateSymbolChats_BB_02ED, 0x4E0); - -struct C_UpdateChatShortcuts_BB_03ED { - parray chat_shortcuts; -} __packed_ws__(C_UpdateChatShortcuts_BB_03ED, 0xA40); - -struct C_UpdateKeyConfig_BB_04ED { - parray key_config; -} __packed_ws__(C_UpdateKeyConfig_BB_04ED, 0x16C); - -struct C_UpdatePadConfig_BB_05ED { - parray pad_config; -} __packed_ws__(C_UpdatePadConfig_BB_05ED, 0x38); - -struct C_UpdateTechMenu_BB_06ED { - parray tech_menu; -} __packed_ws__(C_UpdateTechMenu_BB_06ED, 0x28); - -struct C_UpdateCustomizeMenu_BB_07ED { - parray customize; -} __packed_ws__(C_UpdateCustomizeMenu_BB_07ED, 0xE8); - -struct C_UpdateChallengeRecords_BB_08ED { - PlayerRecordsChallengeBB records; -} __packed_ws__(C_UpdateChallengeRecords_BB_08ED, 0x140); - -// EE: Trade cards (Episode 3) -// This command has different forms depending on the header.flag value; the flag -// values match the command numbers from the Episodes 1&2 trade window sequence. -// The sequence of events with the EE command also matches that of the Episodes -// 1&2 trade window; see the description of the D0 command above for details. - -// EE D0 (C->S): Begin trade -struct SC_TradeCards_Ep3_EE_FlagD0_FlagD3 { - le_uint16_t target_client_id = 0; - le_uint16_t entry_count = 0; - struct Entry { - le_uint32_t card_type = 0; - le_uint32_t count = 0; - } __packed_ws__(Entry, 8); - parray entries; -} __packed_ws__(SC_TradeCards_Ep3_EE_FlagD0_FlagD3, 0x24); - -// EE D1 (S->C): Advance trade state -struct S_AdvanceCardTradeState_Ep3_EE_FlagD1 { - le_uint32_t unused = 0; -} __packed_ws__(S_AdvanceCardTradeState_Ep3_EE_FlagD1, 4); - -// EE D2 (C->S): Trade can proceed -// No arguments - -// EE D3 (S->C): Execute trade -// Same format as EE D0 - -// EE D4 (C->S): Trade failed -// EE D4 (S->C): Trade complete - -struct S_CardTradeComplete_Ep3_EE_FlagD4 { - le_uint32_t success = 0; // 0 = failed, 1 = success, anything else = invalid -} __packed_ws__(S_CardTradeComplete_Ep3_EE_FlagD4, 4); - -// EE (S->C): Scrolling message (BB) -// Same format as 01. The message appears at the top of the screen and slowly -// scrolls to the left. The maximum length of the message is 0x400 bytes (0x200 -// UTF-16 characters). - -// EF (C->S): Join card auction (Episode 3) -// When a card auction is ready to begin, the leader sends this command to -// request the card list. The server then sends an EF command to all players -// to start the auction. - -// EF (S->C): Start card auction (Episode 3) - -struct S_StartCardAuction_Ep3_EF { - le_uint16_t points_available = 0; - le_uint16_t unused = 0; - struct Entry { - le_uint16_t card_id = 0xFFFF; // Must be < 0x02F1 - le_uint16_t min_price = 0; // Must be > 0 and < 100 - } __packed_ws__(Entry, 4); - parray entries; -} __packed_ws__(S_StartCardAuction_Ep3_EF, 0x54); - -// EF (S->C): Set or disable shutdown command (BB) -// All variants of EF except 00EF cause the given Windows shell command to be -// run (via ShellExecuteA) just before the game exits normally. There can be at -// most one shutdown command at a time; a later EF command will overwrite the -// previous EF command's effects. The 00EF command deletes the previous shutdown -// command if any was present, causing no command to run when the game closes. -// There is no indication to the player when a shutdown command has been set. - -// This command is likely just a vestigial debugging feature that Sega left in, -// but it presents a fairly obvious security risk. There is no way for the -// server to know whether an EF command it sent has actually executed on the -// client, so newserv's proxy unconditionally blocks this command. - -struct S_SetShutdownCommand_BB_01EF { - pstring command; -} __packed_ws__(S_SetShutdownCommand_BB_01EF, 0x200); - -// F0 (S->C): Force update player lobby data (BB) -// Format is PlayerLobbyDataBB (in PlayerSubordinates.hh). This command -// overwrites the lobby data for the player given by .client_id without -// reloading the game or lobby. - -// This command probably exists to handle cases like the following: -// 1. Player A is in a team and is not the team master. Player A creates a game. -// 2. The master of the team changes to player B during this game. -// 3. Player B then joins the game that A is in. -// Some effects (e.g. Commander Blade) depend on the team master ID in the -// PlayerLobbyDataBB structure, and this is the only way to update that -// structure without reloading the lobby or game. If this command did not exist, -// then player A would not know that B was the master when they join the game, -// so A would not see the bonus from Commander Blade if B uses it. - -// F1: Invalid command -// F2: Invalid command -// F3: Invalid command -// F4: Invalid command -// F5: Invalid command -// F6: Invalid command -// F7: Invalid command -// F8: Invalid command -// F9: Invalid command -// FA: Invalid command -// FB: Invalid command -// FC: Invalid command -// FD: Invalid command -// FE: Invalid command -// FF: Invalid command - -// Removed commands - -// There is evidence that some commands and features were fully removed from -// PSO at some point. - -// There is a command named RcvGamePause in all DC versions of PSO, but its -// handler function is missing. It's likely there was a way to actually pause -// the game during early development, but it was removed, likely because it'd -// be a fairly poor player experience. - -// There are two commands named SndGameStatus and SndGameCondition in the DC -// versions, but their sender functions are missing in all versions. It's not -// clear what exactly they would have sent, or when they would have been -// triggered. - -// Finally, there is a function named SndPsoGetText which was in DCv1 and DCv2, -// but not in DC NTE or the December 2000 prototype. This may have been a way -// for the server to prompt the user to input some text. As with the other -// unused functions, the code was removed, leaving only the function name. - -//////////////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// -// GAME SUBCOMMANDS //////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -// The game subcommands are used in commands 60, 62, 6C, 6D, C9, and CB. These -// are laid out similarly as above. These structs start with G_ to indicate that -// they are (usually) bidirectional, and are (usually) generated by clients and -// consumed by clients. Generally in newserv source, these commands are referred -// to as (for example) 6x02, etc., referencing the fact that they are almost -// always sent via a command starting with the hex digit 6. - -// All game subcommands have the same header format, which is one of: -// - XX SS ... -// - XX 00 ?? ?? TT TT TT TT ... -// where X is the subcommand number (e.g. in 6xA2, it would be A2), S is the -// size in words of the entire subcommand (that is, overall size in bytes / 4), -// and T is the overall size in bytes. The second form is generally only used -// when the overall size in bytes is 0x400 or longer (so the S field doesn't -// suffice to describe its length), but it may also be used in some cases where -// the subcommand is shorter. -// Multiple subcommands may be sent in the same 6x command. It seems the client -// never sends commands like this, but newserv generates commands containing -// multiple subcommands in some situations (for example, the implementation of -// infinite HP does this). -// If any subcommand or group thereof is longer than 0x400 bytes, the 6C or 6D -// commands must be used. The 60 and 62 commands exhibit undefined behavior if -// this limit is exceeded. - -// Some subcommands are "protected" on V3 and later (not including GC NTE); -// these commands are blocked by the client if they affect the local player. If -// a V3 or later client receives a protected subcommand that would affect its -// own player, it instead ignores the entire subcommand. This means that the -// server or other players cannot send these subcommands to affect other -// players; they can only send these commands to inform other clients about -// changes or actions from their own player. -// The protected subcommands are marked (protected) in the listings below. - -// These common structures are used my many subcommands. -struct G_ClientIDHeader { - uint8_t subcommand = 0; - uint8_t size = 0; - le_uint16_t client_id = 0; // <= 12 -} __packed_ws__(G_ClientIDHeader, 4); -struct G_EnemyIDHeader { - uint8_t subcommand = 0; - uint8_t size = 0; - le_uint16_t enemy_id = 0; // In [0x1000, 0x4000); not the same as enemy_index! -} __packed_ws__(G_EnemyIDHeader, 4); -struct G_ObjectIDHeader { - uint8_t subcommand = 0; - uint8_t size = 0; - le_uint16_t object_id = 0; // >= 0x4000, != 0xFFFF -} __packed_ws__(G_ObjectIDHeader, 4); -struct G_ParameterHeader { - uint8_t subcommand = 0; - uint8_t size = 0; - le_uint16_t param = 0; -} __packed_ws__(G_ParameterHeader, 4); -struct G_UnusedHeader { - uint8_t subcommand = 0; - uint8_t size = 0; - le_uint16_t unused = 0; -} __packed_ws__(G_UnusedHeader, 4); - -template -struct G_ExtendedHeaderT { - HeaderT basic_header; - le_uint32_t size = 0; -} __packed__; - -// 6x00: Invalid subcommand -// 6x01: Invalid subcommand - -// 6x02: Unknown -// This subcommand is completely ignored on V3. -// TODO: It is not ignored on V1 and V2. Figure out what it does and document it. - -// 6x03: Unknown -// This subcommand is completely ignored on V3. -// TODO: It is not ignored on V1 and V2. Figure out what it does and document it. - -// 6x04: Unknown - -struct G_Unknown_6x04 { - G_ClientIDHeader header; - le_uint16_t unknown_a1 = 0; - le_uint16_t unknown_a2 = 0; -} __packed_ws__(G_Unknown_6x04, 8); - -// 6x05: Switch state changed -// Some things that don't look like switches are implemented as switches using -// this subcommand. For example, when all enemies in a room are defeated, this -// subcommand is used to unlock the doors. -// Note: In the client, this is a subclass of 6x04, similar to how 6xA2 is a -// subclass of 6x60. - -struct G_SwitchStateChanged_6x05 { - // Note: header.object_id is 0xFFFF for room clear when all enemies defeated - G_ObjectIDHeader header; - // TODO: Some of these might be big-endian on GC; it only byteswaps - // switch_flag_num. Are the others actually uint16, or are they uint8[2]? - le_uint16_t unknown_a1 = 0; - le_uint16_t unknown_a2 = 0; - le_uint16_t switch_flag_num = 0; - uint8_t switch_flag_floor = 0; - // Only two bits in flags have meanings: - // 01 - set unlock flag (if not set, the flag is cleared instead) - // 02 - play room unlock sound if floor matches client's floor - uint8_t flags = 0; -} __packed_ws__(G_SwitchStateChanged_6x05, 0x0C); - -// 6x06: Send guild card - -struct G_SendGuildCard_DCNTE_6x06 { - G_UnusedHeader header; - GuildCardDCNTE guild_card; - uint8_t unused; -} __packed_ws__(G_SendGuildCard_DCNTE_6x06, 0x80); - -struct G_SendGuildCard_DC_6x06 { - G_UnusedHeader header; - GuildCardDC guild_card; - parray unused; -} __packed_ws__(G_SendGuildCard_DC_6x06, 0x84); - -struct G_SendGuildCard_PC_6x06 { - G_UnusedHeader header; - GuildCardPC guild_card; -} __packed_ws__(G_SendGuildCard_PC_6x06, 0xF4); - -struct G_SendGuildCard_GCNTE_6x06 { - G_UnusedHeader header; - GuildCardGCNTE guild_card; -} __packed_ws__(G_SendGuildCard_GCNTE_6x06, 0xA8); - -struct G_SendGuildCard_GC_6x06 { - G_UnusedHeader header; - GuildCardGC guild_card; -} __packed_ws__(G_SendGuildCard_GC_6x06, 0x94); - -struct G_SendGuildCard_XB_6x06 { - G_UnusedHeader header; - GuildCardXB guild_card; -} __packed_ws__(G_SendGuildCard_XB_6x06, 0x230); - -struct G_SendGuildCard_BB_6x06 { - G_UnusedHeader header; - GuildCardBB guild_card; -} __packed_ws__(G_SendGuildCard_BB_6x06, 0x10C); - -// 6x07: Symbol chat - -struct G_SymbolChat_6x07 { - G_UnusedHeader header; - le_uint32_t client_id = 0; - SymbolChat data; -} __packed_ws__(G_SymbolChat_6x07, 0x44); - -// 6x08: Invalid subcommand - -// 6x09: Unknown - -struct G_Unknown_6x09 { - G_EnemyIDHeader header; -} __packed_ws__(G_Unknown_6x09, 4); - -// 6x0A: Update enemy state - -template -struct G_UpdateEnemyStateT_6x0A { - G_EnemyIDHeader header; - le_uint16_t enemy_index = 0; // [0, 0xB50) - le_uint16_t total_damage = 0; - // Flags: - // 00000400 - should play hit animation - // 00000800 - is dead - typename std::conditional_t flags = 0; -} __packed__; -using G_UpdateEnemyState_GC_6x0A = G_UpdateEnemyStateT_6x0A; -using G_UpdateEnemyState_DC_PC_XB_BB_6x0A = G_UpdateEnemyStateT_6x0A; -check_struct_size(G_UpdateEnemyState_GC_6x0A, 0x0C); -check_struct_size(G_UpdateEnemyState_DC_PC_XB_BB_6x0A, 0x0C); - -// 6x0B: Update object state - -struct G_UpdateObjectState_6x0B { - G_ClientIDHeader header; - le_uint32_t flags = 0; - le_uint32_t object_index = 0; -} __packed_ws__(G_UpdateObjectState_6x0B, 0x0C); - -// 6x0C: Add condition (poison/slow/etc.) (protected on V3/V4) -// 6x0D: Remove condition (poison/slow/etc.) (protected on V3/V4) - -struct G_AddOrRemoveCondition_6x0C_6x0D { - G_ClientIDHeader header; - le_uint32_t unknown_a1 = 0; // Probably condition type - le_uint32_t unknown_a2 = 0; -} __packed_ws__(G_AddOrRemoveCondition_6x0C_6x0D, 0x0C); - -// 6x0E: Unknown (protected on V3/V4) - -struct G_Unknown_6x0E { - G_ClientIDHeader header; -} __packed_ws__(G_Unknown_6x0E, 4); - -// 6x0F: Invalid subcommand - -// 6x10: Unknown (not valid on Episode 3) - -struct G_Unknown_6x10_6x11_6x12_6x14 { - G_EnemyIDHeader header; - le_uint16_t unknown_a2 = 0; - le_uint16_t unknown_a3 = 0; - le_uint32_t unknown_a4 = 0; -} __packed_ws__(G_Unknown_6x10_6x11_6x12_6x14, 0x0C); - -// 6x11: Unknown (not valid on Episode 3) -// Same format as 6x10 - -// 6x12: Dragon boss actions (not valid on Episode 3) - -template -struct G_DragonBossActionsT_6x12 { - using F32T = typename std::conditional::type; - G_EnemyIDHeader header; - le_uint16_t unknown_a2 = 0; - le_uint16_t unknown_a3 = 0; - le_uint32_t unknown_a4 = 0; - F32T x = 0.0f; - F32T z = 0.0f; -} __packed__; -using G_DragonBossActions_DC_PC_XB_BB_6x12 = G_DragonBossActionsT_6x12; -using G_DragonBossActions_GC_6x12 = G_DragonBossActionsT_6x12; -check_struct_size(G_DragonBossActions_DC_PC_XB_BB_6x12, 0x14); -check_struct_size(G_DragonBossActions_GC_6x12, 0x14); - -// 6x13: De Rol Le boss actions (not valid on Episode 3) - -struct G_DeRolLeBossActions_6x13 { - G_EnemyIDHeader header; - le_uint16_t unknown_a2 = 0; - le_uint16_t unknown_a3 = 0; -} __packed_ws__(G_DeRolLeBossActions_6x13, 8); - -// 6x14: De Rol Le boss actions (not valid on Episode 3) -// Same format as 6x10 - -// 6x15: Vol Opt boss actions (not valid on Episode 3) - -struct G_VolOptBossActions_6x15 { - G_EnemyIDHeader header; - le_uint16_t unknown_a2 = 0; - le_uint16_t unknown_a3 = 0; - le_uint16_t unknown_a4 = 0; - le_uint16_t unknown_a5 = 0; -} __packed_ws__(G_VolOptBossActions_6x15, 0x0C); - -// 6x16: Vol Opt boss actions (not valid on Episode 3) - -struct G_VolOptBossActions_6x16 { - G_UnusedHeader header; - parray unknown_a2; - le_uint16_t unknown_a3 = 0; -} __packed_ws__(G_VolOptBossActions_6x16, 0x0C); - -// 6x17: Vol Opt phase 2 boss actions (not valid on Episode 3) - -struct G_VolOpt2BossActions_6x17 { - G_ClientIDHeader header; - le_float unknown_a2 = 0.0f; - le_float unknown_a3 = 0.0f; - le_float unknown_a4 = 0.0f; - le_uint32_t unknown_a5 = 0; -} __packed_ws__(G_VolOpt2BossActions_6x17, 0x14); - -// 6x18: Vol Opt phase 2 boss actions (not valid on Episode 3) - -struct G_VolOpt2BossActions_6x18 { - G_ClientIDHeader header; - parray unknown_a2; -} __packed_ws__(G_VolOpt2BossActions_6x18, 0x0C); - -// 6x19: Dark Falz boss actions (not valid on Episode 3) - -struct G_DarkFalzActions_6x19 { - G_EnemyIDHeader header; - le_uint16_t unknown_a2 = 0; - le_uint16_t unknown_a3 = 0; - le_uint32_t unknown_a4 = 0; - le_uint32_t unused = 0; -} __packed_ws__(G_DarkFalzActions_6x19, 0x10); - -// 6x1A: Invalid subcommand - -// 6x1B: Unknown (not valid on Episode 3) (protected on V3/V4) - -struct G_Unknown_6x1B { - G_ClientIDHeader header; -} __packed_ws__(G_Unknown_6x1B, 4); - -// 6x1C: Destroy NPC (protected on V3/V4) - -struct G_DestroyNPC_6x1C { - G_ClientIDHeader header; -} __packed_ws__(G_DestroyNPC_6x1C, 4); - -// 6x1D: Invalid subcommand -// 6x1E: Invalid subcommand - -// 6x1F: Set player floor and request positions - -struct G_SetPlayerFloor_DCNTE_6x1F { - G_ClientIDHeader header; -} __packed_ws__(G_SetPlayerFloor_DCNTE_6x1F, 4); - -struct G_SetPlayerFloor_6x1F { - G_ClientIDHeader header; - le_int32_t floor = 0; -} __packed_ws__(G_SetPlayerFloor_6x1F, 8); - -// 6x20: Set position (protected on V3/V4) -// Existing clients send this in response to a 6x1F command when a new client -// joins a lobby or game, so the new client knows where to place them. - -struct G_SetPosition_6x20 { - G_ClientIDHeader header; - le_int32_t floor = 0; - le_float x = 0.0f; - le_float y = 0.0f; - le_float z = 0.0f; - le_uint32_t unknown_a1 = 0; -} __packed_ws__(G_SetPosition_6x20, 0x18); - -// 6x21: Inter-level warp (protected on V3/V4) - -struct G_InterLevelWarp_6x21 { - G_ClientIDHeader header; - le_int32_t floor = 0; -} __packed_ws__(G_InterLevelWarp_6x21, 8); - -// 6x22: Set player invisible (protected on V3/V4) -// 6x23: Set player visible (protected on V3/V4) -// These are generally used while a player is in the process of changing floors. - -struct G_SetPlayerVisibility_6x22_6x23 { - G_ClientIDHeader header; -} __packed_ws__(G_SetPlayerVisibility_6x22_6x23, 4); - -// 6x24: Teleport player (protected on V3/V4) - -struct G_TeleportPlayer_6x24 { - G_ClientIDHeader header; - le_uint32_t unknown_a1 = 0; - le_float x = 0.0f; - le_float y = 0.0f; - le_float z = 0.0f; -} __packed_ws__(G_TeleportPlayer_6x24, 0x14); - -// 6x25: Equip item (protected on V3/V4) - -struct G_EquipItem_6x25 { - G_ClientIDHeader header; - le_uint32_t item_id = 0; - // Values here match the EquipSlot enum (in ItemData.hh) - le_uint32_t equip_slot = 0; -} __packed_ws__(G_EquipItem_6x25, 0x0C); - -// 6x26: Unequip item (protected on V3/V4) - -struct G_UnequipItem_6x26 { - G_ClientIDHeader header; - le_uint32_t item_id = 0; - le_uint32_t unused = 0; -} __packed_ws__(G_UnequipItem_6x26, 0x0C); - -// 6x27: Use item (protected on V3/V4) - -struct G_UseItem_6x27 { - G_ClientIDHeader header; - le_uint32_t item_id = 0; -} __packed_ws__(G_UseItem_6x27, 8); - -// 6x28: Feed MAG (protected on V3/V4) - -struct G_FeedMAG_6x28 { - G_ClientIDHeader header; - le_uint32_t mag_item_id = 0; - le_uint32_t fed_item_id = 0; -} __packed_ws__(G_FeedMAG_6x28, 0x0C); - -// 6x29: Delete inventory item (via bank deposit / sale / feeding MAG) (protected on V3 but not V4) -// This subcommand is also used for reducing the size of stacks - if amount is -// less than the stack count, the item is not deleted and its ID remains valid. - -struct G_DeleteInventoryItem_6x29 { - G_ClientIDHeader header; - le_uint32_t item_id = 0; - le_uint32_t amount = 0; -} __packed_ws__(G_DeleteInventoryItem_6x29, 0x0C); - -// 6x2A: Drop item (protected on V3/V4) - -struct G_DropItem_6x2A { - G_ClientIDHeader header; - le_uint16_t unknown_a1 = 0; // Should be 1... maybe amount? - le_uint16_t floor = 0; - le_uint32_t item_id = 0; - le_float x = 0.0f; - le_float y = 0.0f; - le_float z = 0.0f; -} __packed_ws__(G_DropItem_6x2A, 0x18); - -// 6x2B: Create item in inventory (e.g. via tekker or bank withdraw) (protected on V3/V4) -// On BB, the 6xBE command is used instead of 6x2B to create inventory items. - -struct G_CreateInventoryItem_DC_6x2B { - G_ClientIDHeader header; - ItemData item_data; -} __packed_ws__(G_CreateInventoryItem_DC_6x2B, 0x18); - -struct G_CreateInventoryItem_PC_V3_BB_6x2B : G_CreateInventoryItem_DC_6x2B { - uint8_t unused1 = 0; - uint8_t unknown_a2 = 0; - parray unused2 = 0; -} __packed_ws__(G_CreateInventoryItem_PC_V3_BB_6x2B, 0x1C); - -// 6x2C: Talk to NPC (protected on V3/V4) - -struct G_TalkToNPC_6x2C { - G_ClientIDHeader header; - le_uint16_t unknown_a1 = 0; - le_uint16_t unknown_a2 = 0; - le_float unknown_a3 = 0.0f; - le_float unknown_a4 = 0.0f; - le_float unknown_a5 = 0.0f; -} __packed_ws__(G_TalkToNPC_6x2C, 0x14); - -// 6x2D: Done talking to NPC (protected on V3/V4) - -struct G_EndTalkToNPC_6x2D { - G_ClientIDHeader header; -} __packed_ws__(G_EndTalkToNPC_6x2D, 4); - -// 6x2E: Set and/or clear player flags (protected on V3/V4) - -struct G_SetOrClearPlayerFlags_6x2E { - G_ClientIDHeader header; - le_uint32_t and_mask = 0; - le_uint32_t or_mask = 0; -} __packed_ws__(G_SetOrClearPlayerFlags_6x2E, 0x0C); - -// 6x2F: Change player HP - -struct G_ChangePlayerHP_6x2F { - G_UnusedHeader header; - le_uint32_t type = 0; // 0 = set HP, 1 = add/subtract HP, 2 = add/sub fixed HP - le_uint16_t amount = 0; - le_uint16_t client_id = 0; -} __packed_ws__(G_ChangePlayerHP_6x2F, 0x0C); - -// 6x30: Level up - -struct G_LevelUp_DCNTE_6x30 { - G_ClientIDHeader header; -} __packed_ws__(G_LevelUp_DCNTE_6x30, 4); - -struct G_LevelUp_6x30 { - G_ClientIDHeader header; - le_uint16_t atp = 0; - le_uint16_t mst = 0; - le_uint16_t evp = 0; - le_uint16_t hp = 0; - le_uint16_t dfp = 0; - le_uint16_t ata = 0; - le_uint16_t level = 0; - le_uint16_t unknown_a1 = 0; // Must be 0 or 1 -} __packed_ws__(G_LevelUp_6x30, 0x14); - -// 6x31: Resurrect player (protected on V3/V4) - -struct G_UseMedicalCenter_6x31 { - G_ClientIDHeader header; -} __packed_ws__(G_UseMedicalCenter_6x31, 4); - -// 6x32: Resurrect player (Medical Center) - -struct G_Unknown_6x32 { - G_UnusedHeader header; -} __packed_ws__(G_Unknown_6x32, 4); - -// 6x33: Resurrect player (with Moon Atomizer) (protected on V3/V4) - -struct G_RevivePlayer_6x33 { - G_ClientIDHeader header; - le_uint16_t client_id2 = 0; - le_uint16_t unused = 0; -} __packed_ws__(G_RevivePlayer_6x33, 8); - -// 6x34: Unknown -// This subcommand is completely ignored (at least, by PSO GC). - -// 6x35: Invalid subcommand - -// 6x36: Unknown (supported; game only) -// This subcommand is completely ignored (at least, by PSO GC). - -// 6x37: Photon blast (protected on V3/V4) - -struct G_PhotonBlast_6x37 { - G_ClientIDHeader header; - le_uint16_t unknown_a1 = 0; - le_uint16_t unused = 0; -} __packed_ws__(G_PhotonBlast_6x37, 8); - -// 6x38: Donate to photon blast (protected on V3/V4) - -struct G_Unknown_6x38 { - G_ClientIDHeader header; - le_uint16_t unknown_a1 = 0; - le_uint16_t unused = 0; -} __packed_ws__(G_Unknown_6x38, 8); - -// 6x39: Photon blast ready (protected on V3/V4) - -struct G_PhotonBlastReady_6x38 { - G_ClientIDHeader header; -} __packed_ws__(G_PhotonBlastReady_6x38, 4); - -// 6x3A: Unknown (supported; game only) (protected on V3/V4) - -struct G_Unknown_6x3A { - G_ClientIDHeader header; -} __packed_ws__(G_Unknown_6x3A, 4); - -// 6x3B: Unknown (supported; lobby & game) (protected on V3/V4) - -struct G_Unknown_6x3B { - G_ClientIDHeader header; -} __packed_ws__(G_Unknown_6x3B, 4); - -// 6x3C: Unknown (DCv1 and earlier) -// This command has a handler, but it does nothing, even on DC NTE. - -// 6x3D: Invalid subcommand - -// 6x3E: Stop moving (protected on V3/V4) - -struct G_StopAtPosition_6x3E { - G_ClientIDHeader header; - le_uint16_t unknown_a1 = 0; - le_uint16_t angle = 0; - le_int16_t floor = 0; - le_int16_t room = 0; - le_float x = 0.0f; - le_float y = 0.0f; - le_float z = 0.0f; -} __packed_ws__(G_StopAtPosition_6x3E, 0x18); - -// 6x3F: Set position (protected on V3/V4) - -struct G_SetPosition_6x3F { - G_ClientIDHeader header; - le_uint16_t unknown_a1 = 0; - le_uint16_t angle = 0; - le_int16_t floor = 0; - le_int16_t room = 0; - le_float x = 0.0f; - le_float y = 0.0f; - le_float z = 0.0f; -} __packed_ws__(G_SetPosition_6x3F, 0x18); - -// 6x40: Walk (protected on V3/V4) - -struct G_WalkToPosition_6x40 { - G_ClientIDHeader header; - le_float x = 0.0f; - le_float z = 0.0f; - le_uint32_t action = 0; -} __packed_ws__(G_WalkToPosition_6x40, 0x10); - -// 6x41: Unknown -// This subcommand is completely ignored by v2 and later. - -struct G_Unknown_6x41 { - G_ClientIDHeader header; - le_float x = 0.0f; - le_float z = 0.0f; -} __packed_ws__(G_Unknown_6x41, 0x0C); - -// 6x42: Run (protected on V3/V4) - -struct G_RunToPosition_6x42 { - G_ClientIDHeader header; - le_float x = 0.0f; - le_float z = 0.0f; -} __packed_ws__(G_RunToPosition_6x42, 0x0C); - -// 6x43: First attack (protected on V3/V4) -// 6x44: Second attack (protected on V3/V4) -// 6x45: Third attack (protected on V3/V4) - -struct G_Attack_6x43_6x44_6x45 { - G_ClientIDHeader header; - le_uint16_t unknown_a1 = 0; - le_uint16_t unknown_a2 = 0; -} __packed_ws__(G_Attack_6x43_6x44_6x45, 8); - -// 6x46: Attack finished (sent after each of 43, 44, and 45) (protected on V3/V4) -// The number of targets is not bounds-checked during byteswapping on GC -// clients. The client only expects up to 10 entries here, so if the number of -// targets is too large, the client will byteswap the function's return address -// on the stack, and it will crash. - -struct TargetEntry { - le_uint16_t entity_id = 0; - le_uint16_t unknown_a2 = 0; -} __packed_ws__(TargetEntry, 4); - -struct G_AttackFinished_6x46 { - G_ClientIDHeader header; - le_uint32_t count = 0; - // The client may send a shorter command if not all of these are used. - parray targets; -} __packed_ws__(G_AttackFinished_6x46, 0x30); - -// 6x47: Cast technique (protected on V3/V4) -// On GC, this command has the same bounds-check bug as 6x46. - -struct G_CastTechnique_6x47 { - G_ClientIDHeader header; - uint8_t technique_number = 0; - uint8_t unused = 0; // Must not be negative - // Note: The level here isn't the actual tech level that was cast, if the - // actual level is > 15. In that case, a 6x8D is sent first, which contains - // the additional level which is added to this level at cast time. They - // probably did this for legacy reasons when dealing with v1/v2 - // compatibility, and never cleaned it up. - uint8_t level = 0; - uint8_t target_count = 0; // Must be in [0, 10] - // The client may send a shorter command if not all of these are used. - parray targets; -} __packed_ws__(G_CastTechnique_6x47, 0x30); - -// 6x48: Cast technique complete (protected on V3/V4) - -struct G_CastTechniqueComplete_6x48 { - G_ClientIDHeader header; - le_uint16_t technique_number = 0; - // This level matches the level sent in the 6x47 command, even if that level - // was overridden by a preceding 6x8D command. - le_uint16_t level = 0; -} __packed_ws__(G_CastTechniqueComplete_6x48, 8); - -// 6x49: Execute Photon Blast (protected on V3/V4) -// On GC, this command has the same bounds-check bug as 6x46. - -struct G_ExecutePhotonBlast_6x49 { - G_ClientIDHeader header; - uint8_t unknown_a1 = 0; - uint8_t unknown_a2 = 0; - le_uint16_t target_count = 0; - le_uint16_t unknown_a3 = 0; - le_uint16_t unknown_a4 = 0; - // The client may send a shorter command if not all of these are used. - parray targets; -} __packed_ws__(G_ExecutePhotonBlast_6x49, 0x34); - -// 6x4A: Fully shield attack (protected on V3/V4) - -struct G_ShieldAttack_6x4A { - G_ClientIDHeader header; -} __packed_ws__(G_ShieldAttack_6x4A, 4); - -// 6x4B: Hit by enemy (protected on V3/V4) -// 6x4C: Hit by enemy (protected on V3/V4) - -struct G_HitByEnemy_6x4B_6x4C { - G_ClientIDHeader header; - le_uint16_t angle = 0; - le_uint16_t damage = 0; - le_float x_velocity = 0.0f; - le_float z_velocity = 0.0f; -} __packed_ws__(G_HitByEnemy_6x4B_6x4C, 0x10); - -// 6x4D: Player died (protected on V3/V4) - -struct G_PlayerDied_6x4D { - G_ClientIDHeader header; - le_uint32_t unknown_a1 = 0; -} __packed_ws__(G_PlayerDied_6x4D, 8); - -// 6x4E: Player is dead can be revived (protected on V3/V4) - -struct G_PlayerRevivable_6x4E { - G_ClientIDHeader header; -} __packed_ws__(G_PlayerRevivable_6x4E, 4); - -// 6x4F: Player revived (protected on V3/V4) - -struct G_PlayerRevived_6x4F { - G_ClientIDHeader header; -} __packed_ws__(G_PlayerRevived_6x4F, 4); - -// 6x50: Switch interaction (protected on V3/V4) - -struct G_SwitchInteraction_6x50 { - G_ClientIDHeader header; - le_uint32_t unknown_a1 = 0; -} __packed_ws__(G_SwitchInteraction_6x50, 8); - -// 6x51: Invalid subcommand - -// 6x52: Set animation state (protected on V3/V4) - -struct G_SetAnimationState_6x52 { - G_ClientIDHeader header; - le_uint16_t animation = 0; - le_uint16_t unknown_a2 = 0; - le_uint32_t angle = 0; -} __packed_ws__(G_SetAnimationState_6x52, 0x0C); - -// 6x53: Unknown (supported; game only) (protected on V3/V4) - -struct G_Unknown_6x53 { - G_ClientIDHeader header; -} __packed_ws__(G_Unknown_6x53, 4); - -// 6x54: Unknown -// This subcommand is completely ignored (at least, by PSO GC). - -struct G_Unknown_6x54 { - G_ClientIDHeader header; -} __packed_ws__(G_Unknown_6x54, 4); - -// 6x55: Intra-map warp (protected on V3/V4) - -struct G_IntraMapWarp_6x55 { - G_ClientIDHeader header; - le_uint32_t unknown_a1 = 0; - le_float x1 = 0.0f; - le_float y1 = 0.0f; - le_float z1 = 0.0f; - le_float x2 = 0.0f; - le_float y2 = 0.0f; - le_float z2 = 0.0f; -} __packed_ws__(G_IntraMapWarp_6x55, 0x20); - -// 6x56: Unknown (supported; game) (protected on V3/V4) - -struct G_Unknown_6x56 { - G_ClientIDHeader header; - le_uint32_t unknown_a1 = 0; - le_float x = 0.0f; - le_float y = 0.0f; - le_float z = 0.0f; -} __packed_ws__(G_Unknown_6x56, 0x14); - -// 6x57: Unknown (supported; lobby & game) (protected on V3/V4) - -struct G_Unknown_6x57 { - G_ClientIDHeader header; -} __packed_ws__(G_Unknown_6x57, 4); - -// 6x58: Lobby animation (protected on V3/V4) - -struct G_LobbyAnimation_6x58 { - G_ClientIDHeader header; - le_uint16_t animation_number = 0; - le_uint16_t unused = 0; -} __packed_ws__(G_LobbyAnimation_6x58, 8); - -// 6x59: Pick up item - -struct G_PickUpItem_6x59 { - G_ClientIDHeader header; - le_uint16_t client_id2 = 0; - le_uint16_t floor = 0; - le_uint32_t item_id = 0; -} __packed_ws__(G_PickUpItem_6x59, 0x0C); - -// 6x5A: Request to pick up item - -struct G_PickUpItemRequest_6x5A { - G_ClientIDHeader header; - le_uint32_t item_id = 0; - le_uint16_t floor = 0; - le_uint16_t unused = 0; -} __packed_ws__(G_PickUpItemRequest_6x5A, 0x0C); - -// 6x5B: Unknown (DCv1 and earlier) -// This command has a handler, but it does nothing, even on DC NTE. - -// 6x5C: Destroy floor item -// Same format as 6x63. It appears this version should not be used because it -// removes the item from the floor just like 6x63 does, but 6x5C doesn't call -// the item's destructor. - -// 6x5D: Drop meseta or stacked item -// On DC NTE, this command has the same format, but is subcommand 6x4F instead. - -struct G_DropStackedItem_DC_6x5D { - G_ClientIDHeader header; - le_uint16_t floor = 0; - le_uint16_t unknown_a2 = 0; // Corresponds to FloorItem::unknown_a2 - le_float x = 0.0f; - le_float z = 0.0f; - ItemData item_data; -} __packed_ws__(G_DropStackedItem_DC_6x5D, 0x24); - -struct G_DropStackedItem_PC_V3_BB_6x5D : G_DropStackedItem_DC_6x5D { - le_uint32_t unused3 = 0; -} __packed_ws__(G_DropStackedItem_PC_V3_BB_6x5D, 0x28); - -// 6x5E: Buy item at shop - -struct G_BuyShopItem_6x5E { - G_ClientIDHeader header; - ItemData item_data; -} __packed_ws__(G_BuyShopItem_6x5E, 0x18); - -// 6x5F: Drop item from box/enemy - -struct FloorItem { - /* 00 */ uint8_t floor = 0; - /* 01 */ uint8_t from_enemy = 0; - /* 02 */ le_uint16_t entity_id = 0; // < 0x0B50 if from_enemy != 0; otherwise < 0x0BA0 - /* 04 */ le_float x = 0.0f; - /* 08 */ le_float z = 0.0f; - /* 0C */ le_uint16_t unknown_a2 = 0; - // The drop number is scoped to the floor and increments by 1 each time an - // item is dropped. The last item dropped in each floor has drop_number equal - // to total_items_dropped_per_floor[floor - 1] - 1. - /* 0E */ le_uint16_t drop_number = 0; - /* 10 */ ItemData item; - /* 24 */ -} __packed_ws__(FloorItem, 0x24); - -struct G_DropItem_DC_6x5F { - G_UnusedHeader header; - FloorItem item; -} __packed_ws__(G_DropItem_DC_6x5F, 0x28); - -struct G_DropItem_PC_V3_BB_6x5F : G_DropItem_DC_6x5F { - le_uint32_t unused3 = 0; -} __packed_ws__(G_DropItem_PC_V3_BB_6x5F, 0x2C); - -// 6x60: Request for item drop (handled by the server on BB) - -struct G_StandardDropItemRequest_DC_6x60 { - /* 00 */ G_UnusedHeader header; - /* 04 */ uint8_t floor = 0; - /* 05 */ uint8_t rt_index = 0; - /* 06 */ le_uint16_t entity_id = 0; - /* 08 */ le_float x = 0.0f; - /* 0C */ le_float z = 0.0f; - /* 10 */ le_uint16_t section = 0; - /* 12 */ le_uint16_t ignore_def = 0; - /* 14 */ -} __packed_ws__(G_StandardDropItemRequest_DC_6x60, 0x14); - -struct G_StandardDropItemRequest_PC_V3_BB_6x60 : G_StandardDropItemRequest_DC_6x60 { - /* 14 */ uint8_t effective_area = 0; - /* 15 */ parray unused; - /* 18 */ -} __packed_ws__(G_StandardDropItemRequest_PC_V3_BB_6x60, 0x18); - -// 6x61: Activate MAG effect - -struct G_ActivateMagEffect_6x61 { - G_UnusedHeader header; - le_uint32_t mag_item_id = 0; - le_uint32_t effect_number = 0; -} __packed_ws__(G_ActivateMagEffect_6x61, 0x0C); - -// 6x62: Unknown -// This command has a handler, but it does nothing even on DC NTE. - -// 6x63: Destroy floor item (used when too many items have been dropped) - -struct G_DestroyFloorItem_6x5C_6x63 { - G_UnusedHeader header; - le_uint32_t item_id = 0; - le_uint32_t floor = 0; -} __packed_ws__(G_DestroyFloorItem_6x5C_6x63, 0x0C); - -// 6x64: Unknown (not valid on Episode 3) -// This command has a handler, but it does nothing even on DC NTE. - -// 6x65: Unknown (not valid on Episode 3) -// This command has a handler, but it does nothing even on DC NTE. - -// 6x66: Use star atomizer - -struct G_UseStarAtomizer_6x66 { - G_UnusedHeader header; - parray target_client_ids; -} __packed_ws__(G_UseStarAtomizer_6x66, 0x0C); - -// 6x67: Trigger set event - -struct G_TriggerSetEvent_6x67 { - G_UnusedHeader header; - le_uint32_t floor = 0; - le_uint32_t event_id = 0; // NOT event index - le_uint32_t client_id = 0; -} __packed_ws__(G_TriggerSetEvent_6x67, 0x10); - -// 6x68: Set telepipe state - -struct G_SetTelepipeState_6x68 { - G_UnusedHeader header; - le_uint16_t client_id2 = 0; - le_uint16_t floor = 0; - le_uint16_t unknown_b1 = 0; - uint8_t unknown_b2 = 0; - uint8_t unknown_b3 = 0; - le_float x = 0.0f; - le_float y = 0.0f; - le_float z = 0.0f; - le_uint32_t unknown_a3 = 0; -} __packed_ws__(G_SetTelepipeState_6x68, 0x1C); - -// 6x69: NPC control -// Note: NPCs cannot be destroyed with 6x69; 6x1C is used instead for that. - -struct G_NPCControl_6x69 { - G_UnusedHeader header; - le_uint16_t param1; // Commands 0/3: state; command 1: npc_entity_id; command 2: unknown - le_uint16_t param2; // Commands 0/3: npc_entity_id; commands 1/2: unused - le_uint16_t command = 0; // 0 = create follower NPC, 1 = stop acting, 2 = start acting, 3 = create attacker NPC - le_uint16_t param3; // Commands 0/3: npc_template_index; commands 1/2: unused -} __packed_ws__(G_NPCControl_6x69, 0x0C); - -// 6x6A: Use boss warp (not valid on Episode 3) - -struct G_UseBossWarp_6x6A { - G_ClientIDHeader header; - le_uint16_t unknown_a1 = 0; - le_uint16_t unused = 0; -} __packed_ws__(G_UseBossWarp_6x6A, 8); - -// 6x6B: Sync enemy state (used while loading into game) - -struct G_SyncGameStateHeader_DCNTE_6x6B_6x6C_6x6D_6x6E { - G_ExtendedHeaderT header; - le_uint32_t decompressed_size = 0; - // BC0-compressed data follows here (see bc0_decompress) -} __packed_ws__(G_SyncGameStateHeader_DCNTE_6x6B_6x6C_6x6D_6x6E, 0x0C); - -struct G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E { - G_ExtendedHeaderT header; - le_uint32_t decompressed_size = 0; - le_uint32_t compressed_size = 0; // Must be <= subcommand_size - 0x10 - // BC0-compressed data follows here (see bc0_decompress) -} __packed_ws__(G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E, 0x10); - -// Decompressed format is a list of these -struct G_SyncEnemyState_6x6B_Entry_Decompressed { - le_uint32_t flags = 0; // Same as flags in 6x0A - le_uint16_t item_drop_id = 0; - le_uint16_t total_damage = 0; // Same as in 6x0A - uint8_t red_buff_type = 0; - uint8_t red_buff_level = 0; - uint8_t blue_buff_type = 0; - uint8_t blue_buff_level = 0; -} __packed_ws__(G_SyncEnemyState_6x6B_Entry_Decompressed, 0x0C); - -// 6x6C: Sync object state (used while loading into game) -// Compressed format is the same as 6x6B. - -// Decompressed format is a list of these -struct G_SyncObjectState_6x6C_Entry_Decompressed { - le_uint16_t flags = 0; - le_uint16_t item_drop_id = 0; -} __packed_ws__(G_SyncObjectState_6x6C_Entry_Decompressed, 4); - -// 6x6D: Sync item state (used while loading into game) -// Internal name: RcvItemCondition -// Compressed format is the same as 6x6B. - -// There is a bug in the client that can cause desync between players' item IDs -// if the 6x6D command is sent too quickly. Under normal operation, the client -// keeps track of the next item ID to be assigned for items it creates, and uses -// that to assign item IDs to its inventory items when it's done loading (just -// before it sends the 6F command). The loading process triggered by the 64 -// (game join) command resets these next item ID variables to their default -// values, and the 6x6D command sent by the leader resets them again to match -// the corresponding variables in the leader's item state. However, the loading -// process doesn't actually start until the next frame after the 64 command is -// received, so if the 6x6D command is received on the same frame as the 64 -// command, it will set the next item ID variables correctly, and the loading -// process will then clear all of them on the next frame. The client will then -// assign its own inventory item IDs based on the default base item ID, which -// will result in incorrect IDs if another player had previously been in the -// game in the same slot (since the leader's next item ID for the joining player -// will not match the default value). Fortunately, the game processes commands -// in two phases: first, it receives as much data as possible, then it processes -// as many commands as possible. So, to prevent this bug, we delay all commands -// after a 64 is sent until the client responds to a ping command (sent -// immediately after the 64 command), which ensures that the 64 and 6x6D -// commands cannot be processed on the same frame. - -struct G_SyncItemState_6x6D_Decompressed { - // Note: 16 vs. 15 is not a bug here - there really is an extra field in the - // next drop number vs. the floor item count. Despite this, Pioneer 2 or Lab - // (floor 0) isn't included in next_drop_number_per_floor (so Forest 1 is [0] - // in that array) but it is included in floor_item_count_per_floor (so Forest - // 1 is [1] there). - /* 00 */ parray next_drop_number_per_floor; - // Only [0]-[3] in this array are ever actually used in normal gameplay, but - // the client fills in all 12 of these with reasonable values. - /* 20 */ parray next_item_id_per_player; - /* 50 */ parray floor_item_count_per_floor; - // Variable-length field: - /* 8C */ // FloorItem items[sum(floor_item_count_per_floor)]; -} __packed_ws__(G_SyncItemState_6x6D_Decompressed, 0x8C); - -// 6x6E: Sync set flag state (used while loading into game) -// Compressed format is the same as 6x6B. - -struct G_SyncSetFlagState_6x6E_Decompressed { - le_uint16_t total_size = 0; // == sum of the following 3 fields - le_uint16_t entity_set_flags_size = 0; - le_uint16_t event_set_flags_size = 0; - le_uint16_t switch_flags_size = 0; - // Variable-length fields follow here: - // EntitySetFlags entity_set_flags; // Total size is entity_set_flags_size - // le_uint16_t event_set_flags[event_set_flags_size / 2]; // Same order as in map files (NOT sorted by event_id) - // SwitchFlags switch_flags; // 0x200 bytes (0x10 floors) on v1 and earlier; 0x240 bytes (0x12 floors) on v2 and later - - struct EntitySetFlags { - le_uint32_t object_set_flags_offset = 0; - le_uint32_t num_object_sets = 0; - le_uint32_t enemy_set_flags_offset = 0; - le_uint32_t num_enemy_sets = 0; - // Variable-length fields follow here: - // le_uint16_t object_set_flags[num_object_sets]; - // le_uint16_t enemy_set_flags[num_enemy_sets]; - } __packed_ws__(EntitySetFlags, 0x10); -} __packed_ws__(G_SyncSetFlagState_6x6E_Decompressed, 8); - -// 6x6F: Set quest flags (used while loading into game) - -struct G_SetQuestFlags_DCv1_6x6F { - G_UnusedHeader header; - QuestFlagsV1 quest_flags; -} __packed_ws__(G_SetQuestFlags_DCv1_6x6F, 0x184); - -struct G_SetQuestFlags_V2_V3_6x6F { - G_UnusedHeader header; - QuestFlags quest_flags; -} __packed_ws__(G_SetQuestFlags_V2_V3_6x6F, 0x204); - -struct G_SetQuestFlags_BB_6x6F { - G_UnusedHeader header; - QuestFlags quest_flags; - // If use_apply_mask is 1, only the flags set in bb_quest_flag_apply_mask - // (in PlayerSubordinates.cc) are overwritten on the receiving client's end. - // The client always sends this with use_apply_mask = 1. - le_uint32_t use_apply_mask = 1; -} __packed_ws__(G_SetQuestFlags_BB_6x6F, 0x208); - -// 6x70: Sync player disp data and inventory (used while loading into game) -// Annoyingly, they didn't use the same format as the 65/67/68 commands here, -// and instead rearranged a bunch of things. This is presumably because this -// structure also includes transient state (e.g. current HP). - -struct Telepipe6x70 { - /* 00 */ le_uint16_t owner_client_id = 0xFFFF; - /* 02 */ le_uint16_t floor = 0; - /* 04 */ le_uint32_t unknown_a1 = 0; - /* 08 */ le_float x = 0.0f; - /* 0C */ le_float y = 0.0f; - /* 10 */ le_float z = 0.0f; - /* 14 */ le_uint32_t unknown_a3 = 0; - /* 18 */ le_uint32_t unknown_a4 = 0x0000FFFF; -} __packed_ws__(Telepipe6x70, 0x1C); - -struct G_Unknown_6x70_SubA1 { - // This is used in all versions of this command except DCNTE and 11/2000. - /* 00 */ le_uint16_t unknown_a1 = 0; - /* 02 */ le_uint16_t unknown_a2 = 0; - /* 04 */ le_uint32_t unknown_a3 = 0; - /* 08 */ le_float unknown_a4 = 0.0f; - /* 0C */ le_uint32_t unknown_a5 = 0; - /* 10 */ le_uint32_t unknown_a6 = 0; -} __packed_ws__(G_Unknown_6x70_SubA1, 0x14); - -struct G_Unknown_6x70_SubA2 { - // This is used in all versions of this command except DCNTE and 11/2000. - /* 00 */ le_uint32_t unknown_a1 = 0; - /* 04 */ le_float unknown_a2 = 0.0f; - /* 08 */ le_uint32_t unknown_a3 = 0; -} __packed_ws__(G_Unknown_6x70_SubA2, 0x0C); - -struct G_SyncPlayerDispAndInventory_BaseDCNTE { - /* 0000 */ le_uint16_t client_id = 0; - /* 0002 */ le_uint16_t unknown_a1 = 0; - /* 0004 */ le_uint32_t flags1 = 0; - /* 0008 */ le_float x = 0.0f; - /* 000C */ le_float y = 0.0f; - /* 0010 */ le_float z = 0.0f; - /* 0014 */ le_uint32_t angle_x = 0; - /* 0018 */ le_uint32_t angle_y = 0; - /* 001C */ le_uint32_t angle_z = 0; - /* 0020 */ le_uint16_t unknown_a3a = 0; - /* 0022 */ le_uint16_t current_hp = 0; -} __packed_ws__(G_SyncPlayerDispAndInventory_BaseDCNTE, 0x24); - -struct G_SyncPlayerDispAndInventory_DCNTE_6x70 { - // Offsets in this struct are relative to the overall command header - /* 0004 */ G_ExtendedHeaderT header = {{0x60, 0x00, 0x0000}, sizeof(G_SyncPlayerDispAndInventory_DCNTE_6x70)}; - /* 000C */ G_SyncPlayerDispAndInventory_BaseDCNTE base; - // The following two fields appear to contain uninitialized data - /* 0030 */ le_uint32_t unknown_a5 = 0; - /* 0034 */ le_uint32_t unknown_a6 = 0; - /* 0038 */ Telepipe6x70 telepipe; - /* 0054 */ le_uint32_t unknown_a8 = 0; - /* 0058 */ parray unknown_a9; - /* 0068 */ le_uint32_t area = 0; - /* 006C */ le_uint32_t flags2 = 0; - /* 0070 */ PlayerVisualConfig visual; - /* 00C0 */ PlayerStats stats; - /* 00E4 */ le_uint32_t num_items = 0; - /* 00E8 */ parray items; - /* 0430 */ -} __packed_ws__(G_SyncPlayerDispAndInventory_DCNTE_6x70, 0x42C); - -struct G_SyncPlayerDispAndInventory_DC112000_6x70 { - // Offsets in this struct are relative to the overall command header - /* 0004 */ G_ExtendedHeaderT header = {{0x67, 0x00, 0x0000}, sizeof(G_SyncPlayerDispAndInventory_DC112000_6x70)}; - /* 000C */ G_SyncPlayerDispAndInventory_BaseDCNTE base; - /* 0030 */ le_uint16_t bonus_hp_from_materials = 0; - /* 0032 */ le_uint16_t bonus_tp_from_materials = 0; - /* 0034 */ parray unknown_a5; - /* 0044 */ Telepipe6x70 telepipe; - /* 0060 */ le_uint32_t unknown_a8 = 0; - /* 0064 */ parray unknown_a9; - /* 0074 */ le_uint32_t area = 0; - /* 0078 */ le_uint32_t flags2 = 0; - /* 007C */ PlayerVisualConfig visual; - /* 00CC */ PlayerStats stats; - /* 00F0 */ le_uint32_t num_items = 0; - /* 00F4 */ parray items; - /* 043C */ -} __packed_ws__(G_SyncPlayerDispAndInventory_DC112000_6x70, 0x438); - -struct G_SyncPlayerDispAndInventory_BaseV1 { - /* 0000 */ G_SyncPlayerDispAndInventory_BaseDCNTE base; - /* 0024 */ le_uint16_t bonus_hp_from_materials = 0; - /* 0026 */ le_uint16_t bonus_tp_from_materials = 0; - /* 0028 */ parray unknown_a4; - /* 0064 */ le_uint32_t language = 0; - /* 0068 */ le_uint32_t player_tag = 0; - /* 006C */ le_uint32_t guild_card_number = 0; - /* 0070 */ le_uint32_t unknown_a6 = 0; - /* 0074 */ le_uint32_t battle_team_number = 0; - /* 0078 */ Telepipe6x70 telepipe; - /* 0094 */ le_uint32_t unknown_a8 = 0; - /* 0098 */ G_Unknown_6x70_SubA1 unknown_a9; - /* 00AC */ le_uint32_t area = 0; - /* 00B0 */ le_uint32_t flags2 = 0; - /* 00B4 */ parray technique_levels_v1 = 0xFF; // Last byte is uninitialized - /* 00C8 */ PlayerVisualConfig visual; - /* 0118 */ -} __packed_ws__(G_SyncPlayerDispAndInventory_BaseV1, 0x118); - -struct G_SyncPlayerDispAndInventory_DC_PC_6x70 { - // Offsets in this struct are relative to the overall command header - /* 0004 */ G_ExtendedHeaderT header = {{0x70, 0x00, 0x0000}, sizeof(G_SyncPlayerDispAndInventory_DC_PC_6x70)}; - /* 000C */ G_SyncPlayerDispAndInventory_BaseV1 base; - /* 0124 */ PlayerStats stats; - /* 0148 */ le_uint32_t num_items = 0; - /* 014C */ parray items; - /* 0494 */ -} __packed_ws__(G_SyncPlayerDispAndInventory_DC_PC_6x70, 0x490); - -// GC NTE also uses this format. -struct G_SyncPlayerDispAndInventory_GC_6x70 { - // Offsets in this struct are relative to the overall command header - /* 0004 */ G_ExtendedHeaderT header = {{0x70, 0x00, 0x0000}, sizeof(G_SyncPlayerDispAndInventory_GC_6x70)}; - /* 000C */ G_SyncPlayerDispAndInventory_BaseV1 base; - /* 0124 */ PlayerStats stats; - /* 0148 */ le_uint32_t num_items = 0; - /* 014C */ parray items; - /* 0494 */ le_uint32_t floor = 0; - /* 0498 */ -} __packed_ws__(G_SyncPlayerDispAndInventory_GC_6x70, 0x494); - -struct G_SyncPlayerDispAndInventory_XB_6x70 { - // Offsets in this struct are relative to the overall command header - /* 0004 */ G_ExtendedHeaderT header = {{0x70, 0x00, 0x0000}, sizeof(G_SyncPlayerDispAndInventory_XB_6x70)}; - /* 000C */ G_SyncPlayerDispAndInventory_BaseV1 base; - /* 0124 */ PlayerStats stats; - /* 0148 */ le_uint32_t num_items = 0; - /* 014C */ parray items; - /* 0494 */ le_uint32_t floor = 0; - /* 0498 */ le_uint32_t xb_user_id_high = 0; - /* 049C */ le_uint32_t xb_user_id_low = 0; - /* 04A0 */ le_uint32_t unknown_a16 = 0; - /* 04A4 */ -} __packed_ws__(G_SyncPlayerDispAndInventory_XB_6x70, 0x4A0); - -struct G_SyncPlayerDispAndInventory_BB_6x70 { - // Offsets in this struct are relative to the overall command header - /* 0008 */ G_ExtendedHeaderT header = {{0x70, 0x00, 0x0000}, sizeof(G_SyncPlayerDispAndInventory_BB_6x70)}; - /* 0010 */ G_SyncPlayerDispAndInventory_BaseV1 base; - /* 0128 */ pstring name; - /* 0148 */ PlayerStats stats; - /* 016C */ le_uint32_t num_items = 0; - /* 0170 */ parray items; - /* 04B8 */ le_uint32_t floor = 0; - /* 04BC */ le_uint32_t xb_user_id_high = 0; - /* 04C0 */ le_uint32_t xb_user_id_low = 0; - /* 04C4 */ le_uint32_t unknown_a16 = 0; - /* 04C8 */ -} __packed_ws__(G_SyncPlayerDispAndInventory_BB_6x70, 0x4C0); - -// 6x71: Unblock game join (used while loading into game) - -struct G_UnblockGameJoin_6x71 { - G_UnusedHeader header; -} __packed_ws__(G_UnblockGameJoin_6x71, 4); - -// 6x72: Player done loading into game - -struct G_DoneLoadingIntoGame_6x72 { - G_UnusedHeader header; -} __packed_ws__(G_DoneLoadingIntoGame_6x72, 4); - -// 6x73: Exit quest -// This command misbehaves if sent in a lobby or in a game when no quest is -// loaded. - -struct G_ExitQuest_6x73 { - G_UnusedHeader header; -} __packed_ws__(G_ExitQuest_6x73, 4); - -// 6x74: Word select -// There is a bug in PSO GC with regard to this command: the client does not -// byteswap the header, which means the client_id field is big-endian. - -template -struct G_WordSelectT_6x74 { - using U16T = typename std::conditional::type; - uint8_t subcommand = 0; - uint8_t size = 0; - U16T client_id = 0; - WordSelectMessage message; -} __packed__; -using G_WordSelect_6x74 = G_WordSelectT_6x74; -using G_WordSelectBE_6x74 = G_WordSelectT_6x74; -check_struct_size(G_WordSelect_6x74, 0x20); -check_struct_size(G_WordSelectBE_6x74, 0x20); - -// 6x75: Update quest flag - -struct G_UpdateQuestFlag_DC_PC_6x75 { - G_UnusedHeader header; - le_uint16_t flag = 0; // Must be < 0x400 - le_uint16_t action = 0; // 0 = set flag, 1 = clear flag -} __packed_ws__(G_UpdateQuestFlag_DC_PC_6x75, 8); - -struct G_UpdateQuestFlag_V3_BB_6x75 : G_UpdateQuestFlag_DC_PC_6x75 { - le_uint16_t difficulty = 0; - le_uint16_t unused = 0; -} __packed_ws__(G_UpdateQuestFlag_V3_BB_6x75, 0x0C); - -// 6x76: Set entity set flags -// This command can only be used to set set flags, since the game performs a -// bitwise OR operation instead of a simple assignment. - -struct G_SetEntitySetFlags_6x76 { - G_EnemyIDHeader header; // 1000-3FFF = enemy, 4000-FFFF = object - le_uint16_t floor = 0; - le_uint16_t flags = 0; -} __packed_ws__(G_SetEntitySetFlags_6x76, 8); - -// 6x77: Sync quest register -// This is sent by the client when an opcode D9 is executed within a quest. - -struct G_SyncQuestRegister_6x77 { - G_UnusedHeader header; - le_uint16_t register_number = 0; // Must be < 0x100 - le_uint16_t unused = 0; - union { - le_uint32_t as_int; - le_float as_float; - } __packed__ value; -} __packed_ws__(G_SyncQuestRegister_6x77, 0x0C); - -// 6x78: Unknown - -struct G_Unknown_6x78 { - G_UnusedHeader header; - le_uint16_t client_id = 0; // Must be < 12 - le_uint16_t unused1 = 0; - le_uint32_t unused2 = 0; -} __packed_ws__(G_Unknown_6x78, 0x0C); - -// 6x79: Lobby 14/15 gogo ball (soccer game) - -struct G_GogoBall_6x79 { - G_UnusedHeader header; - le_uint32_t unknown_a1 = 0; - le_uint32_t unknown_a2 = 0; - le_float unknown_a3 = 0.0f; - le_float unknown_a4 = 0.0f; - uint8_t unknown_a5 = 0; - parray unused; -} __packed_ws__(G_GogoBall_6x79, 0x18); - -// 6x7A: Unknown (protected on V3/V4) - -struct G_Unknown_6x7A { - G_ClientIDHeader header; -} __packed_ws__(G_Unknown_6x7A, 4); - -// 6x7B: Unknown (protected on V3/V4) - -struct G_Unknown_6x7B { - G_ClientIDHeader header; -} __packed_ws__(G_Unknown_6x7B, 4); - -// 6x7C: Set Challenge records (not valid on Episode 3) - -struct G_SetChallengeRecordsBase_6x7C { - G_UnusedHeader header; - le_uint16_t client_id = 0; - parray unknown_a1; -} __packed_ws__(G_SetChallengeRecordsBase_6x7C, 8); - -struct G_SetChallengeRecords_DC_6x7C : G_SetChallengeRecordsBase_6x7C { - PlayerRecordsChallengeDC records; -} __packed_ws__(G_SetChallengeRecords_DC_6x7C, 0xA8); -struct G_SetChallengeRecords_PC_6x7C : G_SetChallengeRecordsBase_6x7C { - PlayerRecordsChallengePC records; -} __packed_ws__(G_SetChallengeRecords_PC_6x7C, 0xE0); -struct G_SetChallengeRecords_V3_6x7C : G_SetChallengeRecordsBase_6x7C { - PlayerRecordsChallengeV3 records; -} __packed_ws__(G_SetChallengeRecords_V3_6x7C, 0x108); -struct G_SetChallengeRecords_BB_6x7C : G_SetChallengeRecordsBase_6x7C { - PlayerRecordsChallengeBB records; -} __packed_ws__(G_SetChallengeRecords_BB_6x7C, 0x148); - -// 6x7D: Set battle mode data (not valid on Episode 3) - -struct G_SetBattleModeData_6x7D { - G_UnusedHeader header; - // Values for what (0-6; values 7 and above are not valid): - // 0 = Unknown (params[0] and [1] are used) - // 1 = Does nothing - // 2 = Unknown (no params are used) - // 3 = Set player score (params[0] = client ID, [1] = score) - // 4 = Unknown (params[0] = client ID) - // 5 = Unknown (no params are used) - // 6 = Unknown (all params are used) - uint8_t what = 0; - uint8_t unknown_a1 = 0; // Only used when what == 0 - uint8_t unused = 0; - uint8_t is_alive = 0; // Only used when what == 3 - parray params; -} __packed_ws__(G_SetBattleModeData_6x7D, 0x18); - -// 6x7E: Unknown (not valid on Episode 3) -// This subcommand is completely ignored (at least, by PSO GC). - -// 6x7F: Battle scores and places (not valid on Episode 3) - -template -struct G_BattleScoresT_6x7F { - using U16T = typename std::conditional::type; - using U32T = typename std::conditional::type; - struct Entry { - U16T client_id = 0; - U16T place = 0; - U32T score = 0; - } __packed_ws__(Entry, 8); - G_UnusedHeader header; - parray entries; -} __packed__; -using G_BattleScores_6x7F = G_BattleScoresT_6x7F; -using G_BattleScoresBE_6x7F = G_BattleScoresT_6x7F; -check_struct_size(G_BattleScores_6x7F, 0x24); -check_struct_size(G_BattleScoresBE_6x7F, 0x24); - -// 6x80: Trigger trap (not valid on Episode 3) - -struct G_TriggerTrap_6x80 { - G_ClientIDHeader header; - le_uint16_t unknown_a1 = 0; - le_uint16_t unknown_a2 = 0; -} __packed_ws__(G_TriggerTrap_6x80, 8); - -// 6x81: Set drop weapon on death flag (protected on V3/V4) - -struct G_SetDropWeaponOnDeathFlag_6x81 { - G_ClientIDHeader header; -} __packed_ws__(G_SetDropWeaponOnDeathFlag_6x81, 4); - -// 6x82: Clear drop weapon on death flag (protected on V3/V4) - -struct G_ClearDropWeaponOnDeathFlag_6x82 { - G_ClientIDHeader header; -} __packed_ws__(G_ClearDropWeaponOnDeathFlag_6x82, 4); - -// 6x83: Place trap (protected on V3/V4) - -struct G_PlaceTrap_6x83 { - G_ClientIDHeader header; - le_uint16_t trap_type = 0; - le_uint16_t unknown_a2 = 0; -} __packed_ws__(G_PlaceTrap_6x83, 8); - -// 6x84: Vol Opt boss actions (not valid on Episode 3) -// Same format and usage as 6x16, except unknown_a2 is ignored in 6x84. - -struct G_VolOptBossActions_6x84 { - G_UnusedHeader header; - parray unknown_a1; - le_uint16_t unknown_a2 = 0; - le_uint16_t unknown_a3 = 0; - le_uint16_t unused = 0; -} __packed_ws__(G_VolOptBossActions_6x84, 0x10); - -// 6x85: Unknown (supported; game only; not valid on Episode 3) - -struct G_Unknown_6x85 { - G_UnusedHeader header; - le_uint16_t unknown_a1 = 0; // Command is ignored unless this is 0 - parray unknown_a2; // Only the first 3 appear to be used -} __packed_ws__(G_Unknown_6x85, 0x14); - -// 6x86: Hit destructible object (not valid on Episode 3) - -struct G_HitDestructibleObject_6x86 { - G_ObjectIDHeader header; - le_uint32_t unknown_a1 = 0; - le_uint32_t unknown_a2 = 0; - le_uint16_t unknown_a3 = 0; - le_uint16_t unknown_a4 = 0; -} __packed_ws__(G_HitDestructibleObject_6x86, 0x10); - -// 6x87: Shrink player (protected on V3/V4) - -struct G_ShrinkPlayer_6x87 { - G_ClientIDHeader header; - le_float unknown_a1 = 0.0f; -} __packed_ws__(G_ShrinkPlayer_6x87, 8); - -// 6x88: Restore shrunken player (protected on V3/V4) - -struct G_RestoreShrunkenPlayer_6x88 { - G_ClientIDHeader header; -} __packed_ws__(G_RestoreShrunkenPlayer_6x88, 4); - -// 6x89: Player killed by monster (protected on V3/V4) - -struct G_PlayerKilledByMonster_6x89 { - G_ClientIDHeader header; - le_uint16_t unknown_a1 = 0; - le_uint16_t unused = 0; -} __packed_ws__(G_PlayerKilledByMonster_6x89, 8); - -// 6x8A: Show Challenge time records window (not valid on Episode 3) -// The leader sends this command to tell other clients to show, hide, or update -// the window that shows Challenge Mode stage competion and time records during -// Challenge quest selection. - -struct G_ShowChallengeTimeRecordsWindow_6x8A { - G_ClientIDHeader header; - // Values for which (decimal): - // 0 = hide window - // 1 = show Episode 1 completion state per player - // 2 = show Episode 2 completion state per player - // 3-11 = show times for Episode 1 stage (which - 2) - // 12-16 = show times for Episode 2 stage (which - 11) - // Anything else = command is ignored - le_uint32_t which = 0; -} __packed_ws__(G_ShowChallengeTimeRecordsWindow_6x8A, 8); - -// 6x8B: Unknown (not valid on Episode 3) -// This command has a handler, but it does nothing. - -// 6x8C: Unknown (not valid on Episode 3) -// This command has a handler, but it does nothing. - -// 6x8D: Set technique level override (protected on V3/V4) -// This command is sent immediately before 6x47 if the technique level is above -// 15. Presumably this was done for compatibility between v1 and v2. - -struct G_SetTechniqueLevelOverride_6x8D { - G_ClientIDHeader header; - uint8_t level_upgrade = 0; - uint8_t unused1 = 0; - le_uint16_t unused2 = 0; -} __packed_ws__(G_SetTechniqueLevelOverride_6x8D, 8); - -// 6x8E: Unknown (not valid on Episode 3) -// This command has a handler, but it does nothing. - -// 6x8F: Unknown (not valid on Episode 3) - -struct G_Unknown_6x8F { - G_ClientIDHeader header; - le_uint16_t client_id2 = 0; - le_uint16_t unknown_a1 = 0; -} __packed_ws__(G_Unknown_6x8F, 8); - -// 6x90: Unknown (not valid on Episode 3) (protected on V3/V4) - -struct G_Unknown_6x90 { - G_ClientIDHeader header; - le_uint32_t unknown_a1 = 0; -} __packed_ws__(G_Unknown_6x90, 8); - -// 6x91: Unknown (supported; game only) - -struct G_Unknown_6x91 { - G_ObjectIDHeader header; - le_uint32_t unknown_a1 = 0; - le_uint32_t unknown_a2 = 0; - le_uint16_t unknown_a3 = 0; - le_uint16_t unknown_a4 = 0; - le_uint16_t switch_flag_num = 0; - uint8_t should_set = 0; // The switch flag is only set if this is equal to 1; otherwise it's cleared - uint8_t switch_flag_floor = 0; -} __packed_ws__(G_Unknown_6x91, 0x14); - -// 6x92: Unknown (not valid on Episode 3) - -struct G_Unknown_6x92 { - G_UnusedHeader header; - le_uint32_t unknown_a1 = 0; - le_float unknown_a2 = 0.0f; -} __packed_ws__(G_Unknown_6x92, 0x0C); - -// 6x93: Activate timed switch (not valid on Episode 3) - -struct G_ActivateTimedSwitch_6x93 { - G_UnusedHeader header; - le_uint16_t switch_flag_floor = 0; - le_uint16_t switch_flag_num = 0; - uint8_t should_set = 0; // The switch flag is only set if this is equal to 1; otherwise it's cleared - parray unused; -} __packed_ws__(G_ActivateTimedSwitch_6x93, 0x0C); - -// 6x94: Warp (not valid on Episode 3) - -struct G_InterLevelWarp_6x94 { - G_UnusedHeader header; - le_uint16_t floor = 0; - parray unused; -} __packed_ws__(G_InterLevelWarp_6x94, 8); - -// 6x95: Unknown (not valid on Episode 3) - -struct G_Unknown_6x95 { - G_UnusedHeader header; - le_uint32_t client_id = 0; - ChallengeTime challenge_time; - le_uint32_t unused1 = 0; - le_uint32_t unused2 = 0; -} __packed_ws__(G_Unknown_6x95, 0x14); - -// 6x96: Unknown (not valid on Episode 3) -// This command has a handler, but it does nothing. - -// 6x97: Select Challenge Mode failure option (not valid on Episode 3) - -struct G_SelectChallengeModeFailureOption_6x97 { - G_UnusedHeader header; - le_uint32_t unused1 = 0; - le_uint32_t is_retry = 0; - le_uint32_t unused2 = 0; - le_uint32_t unused3 = 0; -} __packed_ws__(G_SelectChallengeModeFailureOption_6x97, 0x14); - -// 6x98: Unknown -// This subcommand is completely ignored (at least, by PSO GC). - -// 6x99: Unknown -// This subcommand is completely ignored (at least, by PSO GC). - -// 6x9A: Update player stat (not valid on Episode 3) - -struct G_UpdatePlayerStat_6x9A { - G_ClientIDHeader header; - le_uint16_t client_id2 = 0; - // Values for what: - // 0 = subtract HP - // 1 = subtract TP - // 2 = subtract Meseta - // 3 = add HP - // 4 = add TP - uint8_t what = 0; - uint8_t amount = 0; -} __packed_ws__(G_UpdatePlayerStat_6x9A, 8); - -// 6x9B: Level up all techniques (protected on V3/V4) -// Used in battle mode if the rules specify that techniques should level up -// upon character death. - -struct G_LevelUpAllTechniques_6x9B { - G_UnusedHeader header; - uint8_t num_levels = 0; - parray unused; -} __packed_ws__(G_LevelUpAllTechniques_6x9B, 8); - -// 6x9C: Unknown (supported; game only; not valid on Episode 3) - -struct G_Unknown_6x9C { - G_EnemyIDHeader header; - le_uint32_t unknown_a1 = 0; -} __packed_ws__(G_Unknown_6x9C, 8); - -// 6x9D: Unknown (not valid on Episode 3) - -struct G_Unknown_6x9D { - G_UnusedHeader header; - le_uint32_t client_id2 = 0; -} __packed_ws__(G_Unknown_6x9D, 8); - -// 6x9E: Play camera shutter sound -// This subcommand is only used on PSO PC and PC NTE. It is not implemented (and -// therefore ignored) by all prior versions, and all later versions have -// handlers for this command, but the handlers do nothing. - -struct G_PlayerCameraShutterSound_6x9E { - G_ClientIDHeader header; -} __packed_ws__(G_PlayerCameraShutterSound_6x9E, 4); - -// 6x9F: Gal Gryphon boss actions (not valid on pre-V3 or Episode 3) - -struct G_GalGryphonBossActions_6x9F { - G_EnemyIDHeader header; - le_uint32_t unknown_a1 = 0; - le_float unknown_a2 = 0.0f; - le_float unknown_a3 = 0.0f; -} __packed_ws__(G_GalGryphonBossActions_6x9F, 0x10); - -// 6xA0: Gal Gryphon boss actions (not valid on pre-V3 or Episode 3) - -struct G_GalGryphonBossActions_6xA0 { - G_EnemyIDHeader header; - le_float x = 0.0f; - le_float y = 0.0f; - le_float z = 0.0f; - le_uint32_t unknown_a1 = 0; - le_uint16_t unknown_a2 = 0; - le_uint16_t unknown_a3 = 0; - parray unknown_a4; -} __packed_ws__(G_GalGryphonBossActions_6xA0, 0x28); - -// 6xA1: Revive player (not valid on pre-V3) (protected on V3/V4) - -struct G_RevivePlayer_V3_BB_6xA1 { - G_ClientIDHeader header; -} __packed_ws__(G_RevivePlayer_V3_BB_6xA1, 4); - -// 6xA2: Specializable item drop request (not valid on pre-V3; handled by -// server on BB) - -struct G_SpecializableItemDropRequest_6xA2 : G_StandardDropItemRequest_PC_V3_BB_6x60 { - /* 18 */ le_float param3 = 0.0f; - /* 1C */ le_uint32_t param4 = 0; - /* 20 */ le_uint32_t param5 = 0; - /* 24 */ le_uint32_t param6 = 0; - /* 28 */ -} __packed_ws__(G_SpecializableItemDropRequest_6xA2, 0x28); - -// 6xA3: Olga Flow boss actions (not valid on pre-V3 or Episode 3) - -struct G_OlgaFlowBossActions_6xA3 { - G_EnemyIDHeader header; - uint8_t unknown_a1 = 0; - uint8_t unknown_a2 = 0; - parray unknown_a3; -} __packed_ws__(G_OlgaFlowBossActions_6xA3, 8); - -// 6xA4: Olga Flow phase 1 boss actions (not valid on pre-V3 or Episode 3) - -struct G_OlgaFlowPhase1BossActions_6xA4 { - G_EnemyIDHeader header; - uint8_t what = 0; - parray unknown_a3; -} __packed_ws__(G_OlgaFlowPhase1BossActions_6xA4, 8); - -// 6xA5: Olga Flow phase 2 boss actions (not valid on pre-V3 or Episode 3) - -struct G_OlgaFlowPhase2BossActions_6xA5 { - G_EnemyIDHeader header; - uint8_t what = 0; - parray unknown_a3; -} __packed_ws__(G_OlgaFlowPhase2BossActions_6xA5, 8); - -// 6xA6: Modify trade proposal (not valid on pre-V3) - -struct G_ModifyTradeProposal_6xA6 { - G_ClientIDHeader header; - uint8_t unknown_a1 = 0; // Must be < 8 - uint8_t unknown_a2 = 0; - parray unknown_a3; - le_uint32_t unknown_a4 = 0; - le_uint32_t unknown_a5 = 0; -} __packed_ws__(G_ModifyTradeProposal_6xA6, 0x10); - -// 6xA7: Unknown (not valid on pre-V3) -// This subcommand is completely ignored. - -// 6xA8: Gol Dragon boss actions (not valid on pre-V3 or Episode 3) - -template -struct G_GolDragonBossActionsT_6xA8 { - using F32T = typename std::conditional::type; - G_EnemyIDHeader header; - le_uint16_t unknown_a2 = 0; - le_uint16_t unknown_a3 = 0; - le_uint32_t unknown_a4 = 0; - F32T x = 0.0f; - F32T z = 0.0f; - uint8_t unknown_a5 = 0; - parray unused; -} __packed__; -using G_GolDragonBossActions_XB_BB_6xA8 = G_GolDragonBossActionsT_6xA8; -using G_GolDragonBossActions_GC_6xA8 = G_GolDragonBossActionsT_6xA8; -check_struct_size(G_GolDragonBossActions_XB_BB_6xA8, 0x18); -check_struct_size(G_GolDragonBossActions_GC_6xA8, 0x18); - -// 6xA9: Barba Ray boss actions (not valid on pre-V3 or Episode 3) - -struct G_BarbaRayBossActions_6xA9 { - G_EnemyIDHeader header; - le_uint16_t unknown_a1 = 0; - le_uint16_t unknown_a2 = 0; -} __packed_ws__(G_BarbaRayBossActions_6xA9, 8); - -// 6xAA: Barba Ray boss actions (not valid on pre-V3 or Episode 3) - -struct G_BarbaRayBossActions_6xAA { - G_EnemyIDHeader header; - le_uint16_t unknown_a1 = 0; - le_uint16_t unknown_a2 = 0; - le_uint32_t unknown_a3 = 0; -} __packed_ws__(G_BarbaRayBossActions_6xAA, 0x0C); - -// 6xAB: Create lobby chair (not valid on pre-V3) (protected on V3/V4) -// This command's appears to be different on GC NTE than on any other version. -// It's not known what it does. - -struct G_Unknown_GCNTE_6xAB { - G_EnemyIDHeader header; - le_uint16_t unknown_a1 = 0; - le_uint16_t unknown_a2 = 0; -} __packed_ws__(G_Unknown_GCNTE_6xAB, 8); - -struct G_CreateLobbyChair_6xAB { - G_ClientIDHeader header; - le_uint16_t unknown_a1 = 0; - le_uint16_t unknown_a2 = 0; -} __packed_ws__(G_CreateLobbyChair_6xAB, 8); - -// 6xAC: Unknown (not valid on pre-V3) (protected on V3/V4) -// This command's appears to be different on GC NTE than on any other version. -// It also seems that no version (other than perhaps GC NTE) ever sends this -// command. - -struct G_Unknown_GCNTE_6xAC { - G_EnemyIDHeader header; - le_uint16_t unknown_a1 = 0; - le_uint16_t unknown_a2 = 0; - le_uint32_t unknown_a3 = 0; -} __packed_ws__(G_Unknown_GCNTE_6xAC, 0x0C); - -struct G_Unknown_6xAC { - G_ClientIDHeader header; - le_uint32_t num_items = 0; - parray item_ids; -} __packed_ws__(G_Unknown_6xAC, 0x80); - -// 6xAD: Olga Flow subordinate boss actions (not valid on pre-V3, Episode 3, or -// GC Trial Edition) - -struct G_OlgaFlowSubordinateBossActions_6xAD { - G_UnusedHeader header; - // The first byte in this array seems to have a special meaning - parray unknown_a1; -} __packed_ws__(G_OlgaFlowSubordinateBossActions_6xAD, 0x44); - -// 6xAE: Set lobby chair state (sent by existing clients at join time) -// This subcommand is not valid on DC, PC, or GC Trial Edition. - -struct G_SetLobbyChairState_6xAE { - G_ClientIDHeader header; - le_uint16_t unknown_a1 = 0; - le_uint16_t unknown_a2 = 0; - le_uint32_t unknown_a3 = 0; - le_uint32_t unknown_a4 = 0; -} __packed_ws__(G_SetLobbyChairState_6xAE, 0x10); - -// 6xAF: Turn lobby chair (not valid on pre-V3 or GC Trial Edition) (protected on V3/V4) - -struct G_TurnLobbyChair_6xAF { - G_ClientIDHeader header; - le_uint32_t angle = 0; // In range [0x0000, 0xFFFF] -} __packed_ws__(G_TurnLobbyChair_6xAF, 8); - -// 6xB0: Move lobby chair (not valid on pre-V3 or GC Trial Edition) (protected on V3/V4) - -struct G_MoveLobbyChair_6xB0 { - G_ClientIDHeader header; - le_uint32_t unknown_a1 = 0; -} __packed_ws__(G_MoveLobbyChair_6xB0, 8); - -// 6xB1: Unknown (not valid on pre-V3 or GC Trial Edition) -// This subcommand is completely ignored. - -// 6xB2: Play sound from player (not valid on pre-V3 or GC Trial Edition) -// This command is sent when a snapshot is taken on PSO GC, but it can be used -// to play any sound, centered on the local player. If localize is FFFF, then -// the sound is not centered on the local player and is just played globally. - -struct G_PlaySoundFromPlayer_6xB2 { - G_UnusedHeader header; - uint8_t floor = 0; - uint8_t unused = 0; - le_uint16_t localize = 0; - le_uint32_t sound_id = 0; // 0x00051720 = camera shutter sound -} __packed_ws__(G_PlaySoundFromPlayer_6xB2, 0x0C); - -// 6xB3: Unknown (Xbox; voice chat) - -struct G_Unknown_XB_6xB3 { - G_ClientIDHeader header; - le_uint32_t num_frames; - // (0x0A * num_frames) bytes of data follows here. -} __packed_ws__(G_Unknown_XB_6xB3, 8); - -// 6xB3: CARD battle server data request (Episode 3) - -// CARD battle subcommands have multiple subsubcommands, which we name 6xBYxZZ, -// where Y = 3, 4, 5, or 6, and ZZ is any byte. The formats of these -// subsubcommands are described at the end of this file. - -// The common format for CARD battle subcommand headers is: -struct G_CardBattleCommandHeader { - uint8_t subcommand = 0x00; - uint8_t size = 0x00; - le_uint16_t unused1 = 0x0000; - uint8_t subsubcommand = 0x00; // See 6xBx subcommand table (after this table) - uint8_t sender_client_id = 0x00; - // If mask_key is nonzero, the remainder of the data (after unused2 in this - // struct) is encrypted using a simple algorithm, which is implemented in - // set_mask_for_ep3_game_command in SendCommands.cc. The Episode 3 client - // never sends commands that have a nonzero value in this field, but it does - // properly handle received commands with nonzero values in this field. - // This only applies to Episode 3 final - the Trial Edition does not support - // masking and may send uninitialized data in this field. - uint8_t mask_key = 0x00; - uint8_t unused2 = 0x00; -} __packed_ws__(G_CardBattleCommandHeader, 8); - -// Unlike all other 6x subcommands, the 6xB3 subcommand is sent to the server in -// a CA command instead of a 6x, C9, or CB command. (For this reason, we -// generally refer to 6xB3xZZ commands as CAxZZ commands instead.) The server is -// expected to reply to CA commands with one or more 6xB4 subcommands instead of -// forwarding them. The logic for doing so is implemented in Episode3/Server.cc -// and the surrounding classes. - -// The 6xB3 subcommand has a longer header than 6xB4 and 6xB5. This header is -// common to all 6xB3x (CAx) subcommands. -struct G_CardServerDataCommandHeader { - /* 00 */ uint8_t subcommand = 0xB3; - /* 01 */ uint8_t size = 0x00; - /* 02 */ le_uint16_t unused1 = 0x0000; - /* 04 */ uint8_t subsubcommand = 0x00; // See 6xBx subcommand table (after this table) - /* 05 */ uint8_t sender_client_id = 0x00; - /* 06 */ uint8_t mask_key = 0x00; // Same meaning as in G_CardBattleCommandHeader - /* 07 */ uint8_t unused2 = 0x00; - /* 08 */ be_uint32_t sequence_num = 0; - /* 0C */ be_uint32_t context_token = 0; - /* 10 */ -} __packed_ws__(G_CardServerDataCommandHeader, 0x10); - -// 6xB4: Unknown (Xbox; voice chat) - -struct G_Unknown_XB_6xB4 { - G_ClientIDHeader header; - le_uint32_t unknown_a1; -} __packed_ws__(G_Unknown_XB_6xB4, 8); - -// 6xB4: CARD battle server response (Episode 3) - see 6xB3 above -// 6xB5: CARD battle client command (Episode 3) - see 6xB3 above - -// 6xB5: BB shop request (handled by the server) - -struct G_ShopContentsRequest_BB_6xB5 { - G_UnusedHeader header; - le_uint32_t shop_type = 0; -} __packed_ws__(G_ShopContentsRequest_BB_6xB5, 8); - -// 6xB6: Episode 3 map list and map contents (server->client only) -// Unlike 6xB3-6xB5, these commands cannot be masked. Also unlike 6xB3-6xB5, -// there are only two subsubcommands, so we list them inline here. -// These subcommands can be rather large, so they should be sent with the 6C -// command instead of the 60 command. (The difference in header format, -// including the extended size field, is likely the reason for 6xB6 being a -// separate subcommand from the other CARD battle subcommands.) - -struct G_MapSubsubcommand_Ep3_6xB6 { - G_ExtendedHeaderT header; - uint8_t subsubcommand = 0; // 0x40 or 0x41 - parray unused; -} __packed_ws__(G_MapSubsubcommand_Ep3_6xB6, 0x0C); - -struct G_MapList_Ep3_6xB6x40 { - G_MapSubsubcommand_Ep3_6xB6 header; - le_uint16_t compressed_data_size = 0; - le_uint16_t unused = 0; - // PRS-compressed map list data follows here. newserv generates this from the - // map index when requested; see the MapList struct in Episode3/DataIndexes.hh - // and Episode3::MapIndex::get_compressed_map_list for details on the format. -} __packed_ws__(G_MapList_Ep3_6xB6x40, 0x10); - -struct G_MapData_Ep3_6xB6x41 { - G_MapSubsubcommand_Ep3_6xB6 header; - le_uint32_t map_number = 0; - le_uint16_t compressed_data_size = 0; - le_uint16_t unused = 0; - // PRS-compressed map data follows here (which decompresses to an - // Episode3::MapDefinition). -} __packed_ws__(G_MapData_Ep3_6xB6x41, 0x14); - -// 6xB6: BB shop contents (server->client only) - -struct G_ShopContents_BB_6xB6 { - G_UnusedHeader header; - uint8_t shop_type = 0; - uint8_t num_items = 0; - le_uint16_t unused = 0; - // Note: data2d of these entries should be the price - parray item_datas; -} __packed_ws__(G_ShopContents_BB_6xB6, 0x198); - -// 6xB7: Alias for 6xB3 (Episode 3 Trial Edition) -// This command behaves exactly the same as 6xB3. This alias exists only in -// Episode 3 Trial Edition; it was removed in the final release. - -// 6xB7: BB buy shop item (handled by the server) - -struct G_BuyShopItem_BB_6xB7 { - G_UnusedHeader header; - le_uint32_t shop_item_id = 0; - uint8_t shop_type = 0; - uint8_t item_index = 0; - uint8_t amount = 0; - uint8_t unknown_a1 = 0; // TODO: Probably actually unused; verify this -} __packed_ws__(G_BuyShopItem_BB_6xB7, 0x0C); - -// 6xB8: Alias for 6xB4 (Episode 3 Trial Edition) -// This command behaves exactly the same as 6xB4. This alias exists only in -// Episode 3 Trial Edition; it was removed in the final release. - -// 6xB8: BB identify item request (via tekker) (handled by the server) - -struct G_IdentifyItemRequest_6xB8 { - G_UnusedHeader header; - le_uint32_t item_id = 0; -} __packed_ws__(G_IdentifyItemRequest_6xB8, 8); - -// 6xB9: Alias for 6xB5 (Episode 3 Trial Edition) -// This command behaves exactly the same as 6xB5. This alias exists only in -// Episode 3 Trial Edition; it was removed in the final release. - -// 6xB9: BB provisional tekker result - -struct G_IdentifyResult_BB_6xB9 { - G_ClientIDHeader header; - ItemData item_data; -} __packed_ws__(G_IdentifyResult_BB_6xB9, 0x18); - -// 6xBA: Sync card trade state (Episode 3) -// This command calls various member functions in TCardTradeServer. - -struct G_SyncCardTradeState_Ep3_6xBA { - G_ClientIDHeader header; - le_uint16_t what = 0; // Low byte must be < 9; this indexes into a handler table - le_uint16_t unknown_a2 = 0; - le_uint32_t unknown_a3 = 0; - le_uint32_t unknown_a4 = 0; -} __packed_ws__(G_SyncCardTradeState_Ep3_6xBA, 0x10); - -// 6xBA: BB accept tekker result (handled by the server) - -struct G_AcceptItemIdentification_BB_6xBA { - G_UnusedHeader header; - le_uint32_t item_id = 0; -} __packed_ws__(G_AcceptItemIdentification_BB_6xBA, 8); - -// 6xBB: Sync card trade state (Episode 3) -// This command calls various member functions in TCardTradeServer. -// TODO: Certain invalid values for slot/args in this command can crash the -// client (what is properly bounds-checked). Find out the actual limits for -// slot/args and make newserv enforce them. - -struct G_SyncCardTradeState_Ep3_6xBB { - G_ClientIDHeader header; - le_uint16_t what = 0; // Must be < 5; this indexes into a jump table - le_uint16_t slot = 0; - parray args; -} __packed_ws__(G_SyncCardTradeState_Ep3_6xBB, 0x18); - -// 6xBB: BB bank request (handled by the server) - -// 6xBC: Card counts (Episode 3) -// This is sent by the client in response to a 6xB5x38 command. -// It's possible that this is an early, now-unused implementation of the CAx49 -// command. When the client receives this command, it copies the data into a -// globally-allocated array, but nothing reads from this array. Curiously, this -// command is smaller than 0x400 bytes, but uses the extended subcommand format -// anyway (and uses the 6D command rather than 62). - -struct G_CardCounts_Ep3_6xBC { - G_UnusedHeader header; - le_uint32_t size = 0; - parray unknown_a1; - // The client sends uninitialized data in this field - parray unused; -} __packed_ws__(G_CardCounts_Ep3_6xBC, 0x2FC); - -// 6xBC: BB bank contents (server->client only) - -struct G_BankContentsHeader_BB_6xBC { - G_ExtendedHeaderT header; - le_uint32_t checksum = 0; // can be random; client won't notice - le_uint32_t num_items = 0; - le_uint32_t meseta = 0; - // Item data follows -} __packed_ws__(G_BankContentsHeader_BB_6xBC, 0x14); - -// 6xBD: Word select during battle (Episode 3; not Trial Edition) - -// Note: This structure does not have a normal header - the client ID field is -// big-endian! -struct G_WordSelectDuringBattle_Ep3_6xBD { - G_ClientIDHeader header; - le_uint16_t unknown_a1 = 0; - le_uint16_t unknown_a2 = 0; - parray entries; - le_uint32_t unknown_a4 = 0; - le_uint32_t unknown_a5 = 0; - // This field has the same meaning as the first byte in an 06 command's - // message when sent during an Episode 3 battle. - uint8_t private_flags = 0; - parray unused; -} __packed_ws__(G_WordSelectDuringBattle_Ep3_6xBD, 0x24); - -// 6xBD: BB bank action (take/deposit meseta/item) (handled by the server) - -struct G_BankAction_BB_6xBD { - G_UnusedHeader header; - le_uint32_t item_id = 0; - le_uint32_t meseta_amount = 0; - uint8_t action = 0; // 0 = deposit, 1 = take, 3 = done (close bank window) - uint8_t item_amount = 0; - le_uint16_t item_index = 0; // 0xFFFF = meseta -} __packed_ws__(G_BankAction_BB_6xBD, 0x10); - -// 6xBE: Sound chat (Episode 3; not Trial Edition) -// This is the only subcommand ever sent with the CB command. - -struct G_SoundChat_Ep3_6xBE { - G_UnusedHeader header; - le_uint32_t sound_id = 0; // Must be < 0x27 - be_uint32_t unused = 0; -} __packed_ws__(G_SoundChat_Ep3_6xBE, 0x0C); - -// 6xBE: BB create inventory item (server->client only) - -struct G_CreateInventoryItem_BB_6xBE { - G_ClientIDHeader header; - ItemData item_data; - le_uint32_t unused = 0; -} __packed_ws__(G_CreateInventoryItem_BB_6xBE, 0x1C); - -// 6xBF: Change lobby music (Episode 3; not Trial Edition) - -struct G_ChangeLobbyMusic_Ep3_6xBF { - G_UnusedHeader header; - le_uint32_t song_number = 0; // Must be < 0x34 -} __packed_ws__(G_ChangeLobbyMusic_Ep3_6xBF, 8); - -// 6xBF: Give EXP (BB) (server->client only) - -struct G_GiveExperience_BB_6xBF { - G_ClientIDHeader header; - le_uint32_t amount = 0; -} __packed_ws__(G_GiveExperience_BB_6xBF, 8); - -// 6xC0: Sell item at shop (BB) (protected on V3/V4) - -struct G_SellItemAtShop_BB_6xC0 { - G_UnusedHeader header; - le_uint32_t item_id = 0; - le_uint32_t amount = 0; -} __packed_ws__(G_SellItemAtShop_BB_6xC0, 0x0C); - -// 6xC1: Invite to team (BB) -// 6xC2: Accept invitation to team (BB) - -struct G_TeamInvitationAction_BB_6xC1_6xC2_6xCD_6xCE { - G_ClientIDHeader header; - le_uint32_t guild_card_number = 0; - le_uint32_t action = 0; // 0 or 1 for 6xC1, 2 (or not 2) for 6xC2 - parray unknown_a1; -} __packed_ws__(G_TeamInvitationAction_BB_6xC1_6xC2_6xCD_6xCE, 0x60); - -// 6xC3: Split stacked item (BB; handled by the server) -// Note: This is not sent if an entire stack is dropped; in that case, a normal -// item drop subcommand is generated instead. - -struct G_SplitStackedItem_BB_6xC3 { - G_ClientIDHeader header; - le_uint16_t floor = 0; - le_uint16_t unused2 = 0; - le_float x = 0.0f; - le_float z = 0.0f; - le_uint32_t item_id = 0; - le_uint32_t amount = 0; -} __packed_ws__(G_SplitStackedItem_BB_6xC3, 0x18); - -// 6xC4: Sort inventory (BB; handled by the server) - -struct G_SortInventory_BB_6xC4 { - G_UnusedHeader header; - parray item_ids; -} __packed_ws__(G_SortInventory_BB_6xC4, 0x7C); - -// 6xC5: Medical center used (BB) - -struct G_MedicalCenterUsed_BB_6xC5 { - G_ClientIDHeader header; -} __packed_ws__(G_MedicalCenterUsed_BB_6xC5, 4); - -// 6xC6: Steal experience (BB) - -struct G_StealEXP_BB_6xC6 { - G_ClientIDHeader header; - le_uint16_t entity_id = 0; - le_uint16_t enemy_index = 0; -} __packed_ws__(G_StealEXP_BB_6xC6, 8); - -// 6xC7: Charge attack (BB) - -struct G_ChargeAttack_BB_6xC7 { - G_ClientIDHeader header; - // Tethealla (at least, the ancient public version of it) treats this as - // signed, and gives the player money in that case. We don't do so. - le_uint32_t meseta_amount = 0; -} __packed_ws__(G_ChargeAttack_BB_6xC7, 8); - -// 6xC8: Enemy EXP request (BB; handled by the server) - -struct G_EnemyEXPRequest_BB_6xC8 { - G_EnemyIDHeader header; - le_uint16_t enemy_index = 0; - le_uint16_t requesting_client_id = 0; - uint8_t is_killer = 0; - parray unused; -} __packed_ws__(G_EnemyEXPRequest_BB_6xC8, 0x0C); - -// 6xC9: Adjust player Meseta (BB; handled by server) - -struct G_AdjustPlayerMeseta_BB_6xC9 { - G_UnusedHeader header; - le_int32_t amount = 0; -} __packed_ws__(G_AdjustPlayerMeseta_BB_6xC9, 8); - -// 6xCA: Request item reward from quest (BB; handled by server) - -struct G_ItemRewardRequest_BB_6xCA { - G_UnusedHeader header; - ItemData item_data; -} __packed_ws__(G_ItemRewardRequest_BB_6xCA, 0x18); - -// 6xCB: Transfer item via mail message (BB) - -struct G_TransferItemViaMailMessage_BB_6xCB { - G_ClientIDHeader header; - le_uint32_t item_id = 0; - le_uint32_t amount = 0; - le_uint32_t target_guild_card_number = 0; -} __packed_ws__(G_TransferItemViaMailMessage_BB_6xCB, 0x10); - -// 6xCC: Exchange item for team points (BB) (protected on V3/V4) - -struct G_ExchangeItemForTeamPoints_BB_6xCC { - G_ClientIDHeader header; - le_uint32_t item_id = 0; - le_uint32_t amount = 0; -} __packed_ws__(G_ExchangeItemForTeamPoints_BB_6xCC, 0x0C); - -// 6xCD: Transfer master (BB) -// 6xCE: Accept master transfer (BB) -// Same format as 6xC1 - -// 6xCF: Start battle (BB) - -struct G_StartBattle_BB_6xCF { - G_UnusedHeader header; - BattleRules rules; -} __packed_ws__(G_StartBattle_BB_6xCF, 0x34); - -// 6xD0: Battle mode level up (BB; handled by server) -// Requests the client to be leveled up by num_levels levels. The server should -// respond with a 6x30 command. - -struct G_BattleModeLevelUp_BB_6xD0 { - G_ClientIDHeader header; - le_uint32_t num_levels = 0; -} __packed_ws__(G_BattleModeLevelUp_BB_6xD0, 8); - -// 6xD1: Request Challenge Mode grave recovery item (BB; handled by server) - -struct G_ChallengeModeGraveRecoveryItemRequest_BB_6xD1 { - G_ClientIDHeader header; - le_uint16_t floor = 0; - le_uint16_t unknown_a1 = 0; - le_float x = 0; - le_float z = 0; - le_uint32_t item_type = 0; // Should be < 6 -} __packed_ws__(G_ChallengeModeGraveRecoveryItemRequest_BB_6xD1, 0x14); - -// 6xD2: Set quest counter (BB) -// Writes 4 bytes to the 32-bit field specified by index. - -struct G_SetQuestCounter_BB_6xD2 { - G_ClientIDHeader header; - le_uint32_t index = 0; // There are 0x10 of them (0x00-0x0F) - le_uint32_t value = 0; -} __packed_ws__(G_SetQuestCounter_BB_6xD2, 0x0C); - -// 6xD3: Invalid subcommand - -// 6xD4: Unknown (BB) - -struct G_Unknown_BB_6xD4 { - G_UnusedHeader header; - le_uint16_t action = 0; // Must be in [0, 5] - uint8_t unknown_a1 = 0; // Must be in [0, 15] - uint8_t unused = 0; -} __packed_ws__(G_Unknown_BB_6xD4, 8); - -// 6xD5: Exchange item in quest (BB; handled by server) -// The client sends this when it executes an F953 quest opcode. - -struct G_ExchangeItemInQuest_BB_6xD5 { - G_ClientIDHeader header; - ItemData find_item; // Only data1[0]-[2] are used - ItemData replace_item; // Only data1[0]-[2] are used - le_uint16_t success_function_id = 0; - le_uint16_t failure_function_id = 0; -} __packed_ws__(G_ExchangeItemInQuest_BB_6xD5, 0x30); - -// 6xD6: Wrap item (BB; handled by server) - -struct G_WrapItem_BB_6xD6 { - G_ClientIDHeader header; - ItemData item; - uint8_t unknown_a1 = 0; - parray unused; -} __packed_ws__(G_WrapItem_BB_6xD6, 0x1C); - -// 6xD7: Paganini Photon Drop exchange (BB; handled by server) -// The client sends this when it executes an F955 quest opcode. - -struct G_PaganiniPhotonDropExchange_BB_6xD7 { - G_ClientIDHeader header; - ItemData new_item; // Only data1[0]-[2] are used - le_uint16_t success_function_id = 0; - le_uint16_t failure_function_id = 0; -} __packed_ws__(G_PaganiniPhotonDropExchange_BB_6xD7, 0x1C); - -// 6xD8: Add S-rank weapon special (BB; handled by server) -// The client sends this when it executes an F956 quest opcode. - -struct G_AddSRankWeaponSpecial_BB_6xD8 { - G_ClientIDHeader header; - ItemData unknown_a1; // Only data1[0]-[2] are used - le_uint32_t item_id = 0; - le_uint32_t special_type = 0; - le_uint16_t success_function_id = 0; - le_uint16_t failure_function_id = 0; -} __packed_ws__(G_AddSRankWeaponSpecial_BB_6xD8, 0x24); - -// 6xD9: Momoka item exchange (BB; handled by server) -// The client sends this when it executes an F95B quest opcode. - -struct G_MomokaItemExchange_BB_6xD9 { - G_ClientIDHeader header; - ItemData find_item; // Only data1[0]-[2] are used - ItemData replace_item; // Only data1[0]-[2] are used - le_uint32_t unknown_a3 = 0; - le_uint32_t unknown_a4 = 0; - le_uint16_t unknown_a5 = 0; - le_uint16_t unknown_a6 = 0; -} __packed_ws__(G_MomokaItemExchange_BB_6xD9, 0x38); - -// 6xDA: Upgrade weapon attribute (BB; handled by server) -// The client sends this when it executes an F957 or F958 quest opcode. - -struct G_UpgradeWeaponAttribute_BB_6xDA { - G_ClientIDHeader header; - ItemData item; // Only data1[0-2] are used (argsA[1-3]) - le_uint32_t item_id = 0; // argsA[0] - le_uint32_t attribute = 0; // argsA[4] - le_uint32_t payment_count = 0; // Number of PD or PS (argsA[5]) - le_uint32_t payment_type = 0; // 0 = Photon Drops, 1 = Photon Spheres - le_uint16_t success_function_id = 0; // argsA[6] - le_uint16_t failure_function_id = 0; // argsA[7] -} __packed_ws__(G_UpgradeWeaponAttribute_BB_6xDA, 0x2C); - -// 6xDB: Exchange item in quest (BB) - -struct G_ExchangeItemInQuest_BB_6xDB { - G_ClientIDHeader header; - le_uint32_t unknown_a1 = 0; - le_uint32_t item_id = 0; - le_uint32_t amount = 0; -} __packed_ws__(G_ExchangeItemInQuest_BB_6xDB, 0x10); - -// 6xDC: Saint-Million boss actions (BB) - -struct G_SaintMillionBossActions_BB_6xDC { - G_UnusedHeader header; - le_uint16_t unknown_a1 = 0; - le_uint16_t unknown_a2 = 0; -} __packed_ws__(G_SaintMillionBossActions_BB_6xDC, 8); - -// 6xDD: Set EXP multiplier (BB) -// header.param specifies the EXP multiplier. It is 1-based, so the value 2 -// means all EXP is doubled, for example. - -struct G_SetEXPMultiplier_BB_6xDD { - G_ParameterHeader header; -} __packed_ws__(G_SetEXPMultiplier_BB_6xDD, 4); - -// 6xDE: Exchange Secret Lottery Ticket (BB; handled by server) -// The client sends this when it executes an F95C quest opcode. - -struct G_ExchangeSecretLotteryTicket_BB_6xDE { - G_ClientIDHeader header; - uint8_t index = 0; - uint8_t function_id1 = 0; - le_uint16_t function_id2 = 0; -} __packed_ws__(G_ExchangeSecretLotteryTicket_BB_6xDE, 8); - -// 6xDF: Exchange Photon Crystals (BB; handled by server) -// The client sends this when it executes an F95D quest opcode. - -struct G_ExchangePhotonCrystals_BB_6xDF { - G_ClientIDHeader header; -} __packed_ws__(G_ExchangePhotonCrystals_BB_6xDF, 4); - -// 6xE0: Request item drop from quest (BB; handled by server) -// The client sends this when it executes an F95E quest opcode. - -struct G_RequestItemDropFromQuest_BB_6xE0 { - G_ClientIDHeader header; - uint8_t floor = 0; - uint8_t type = 0; // argsA[0] - uint8_t unknown_a3 = 0; - uint8_t unused = 0; - le_float x = 0.0f; // argsA[1] - le_float z = 0.0f; // argsA[2] -} __packed_ws__(G_RequestItemDropFromQuest_BB_6xE0, 0x10); - -// 6xE1: Exchange Photon Tickets (BB; handled by server) -// The client sends this when it executes an F95F quest opcode. - -struct G_ExchangePhotonTickets_BB_6xE1 { - G_ClientIDHeader header; - uint8_t unknown_a1 = 0; // argsA[0] - uint8_t unknown_a2 = 0; // argsA[1] - uint8_t result_index = 0; // argsA[2] - uint8_t unused = 0; - le_uint16_t function_id1 = 0; // argsA[3] - le_uint16_t unknown_a5 = 0; // argsA[4] -} __packed_ws__(G_ExchangePhotonTickets_BB_6xE1, 0x0C); - -// 6xE2: Get Meseta slot prize (BB) -// The client sends this when it executes an F960 quest opcode. - -struct G_GetMesetaSlotPrize_BB_6xE2 { - G_ClientIDHeader header; - uint8_t result_tier; // This contains the argument value from the F960 opcode - uint8_t floor; - uint8_t unknown_a2; - uint8_t unused; - le_float x; // TODO: Verify this guess - le_float z; // TODO: Verify this guess -} __packed_ws__(G_GetMesetaSlotPrize_BB_6xE2, 0x10); - -// 6xE3: Set Meseta slot prize result (BB) -// The client only uses this to populate the quest text -// replacement token. - -struct G_SetMesetaSlotPrizeResult_BB_6xE3 { - G_ClientIDHeader header; - ItemData item; -} __packed_ws__(G_SetMesetaSlotPrizeResult_BB_6xE3, 0x18); - -// 6xE4: Invalid subcommand -// 6xE5: Invalid subcommand -// 6xE6: Invalid subcommand -// 6xE7: Invalid subcommand -// 6xE8: Invalid subcommand -// 6xE9: Invalid subcommand -// 6xEA: Invalid subcommand -// 6xEB: Invalid subcommand -// 6xEC: Invalid subcommand -// 6xED: Invalid subcommand -// 6xEE: Invalid subcommand -// 6xEF: Invalid subcommand -// 6xF0: Invalid subcommand -// 6xF1: Invalid subcommand -// 6xF2: Invalid subcommand -// 6xF3: Invalid subcommand -// 6xF4: Invalid subcommand -// 6xF5: Invalid subcommand -// 6xF6: Invalid subcommand -// 6xF7: Invalid subcommand -// 6xF8: Invalid subcommand -// 6xF9: Invalid subcommand -// 6xFA: Invalid subcommand -// 6xFB: Invalid subcommand -// 6xFC: Invalid subcommand -// 6xFD: Invalid subcommand -// 6xFE: Invalid subcommand -// 6xFF: Invalid subcommand - -//////////////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// -// EPISODE 3 CARD BATTLE SUBSUBCOMMANDS //////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -// The Episode 3 CARD battle subsubcommands are used in commands 6xB3, 6xB4, and -// 6xB5. Note that even though there's no overlap in the subsubcommand number -// space, the various subsubcommands must be used with the correct 6xBx -// subcommand - the client will ignore the command if sent via the wrong 6xBx -// subcommand. (For example, sending a 6xB5x02 command will do nothing because -// subsubcommand 02 is only valid for the 6xB4 subcommand.) This table is known -// to be complete, so invalid commands are not listed. - -// In general, 6xB3 (CAx) commands are sent by the client when it wants to take -// an action that affects game state held on the server side. The server will -// send one or more 6xB4 commands in response to update all clients' views of -// the game state. 6xB5 commands do not affect state held on the server side, -// and are generally only of concern on the client side. - -// 6xB4x02: Update hand and equips - -struct G_UpdateHand_Ep3_6xB4x02 { - /* 00 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateHand_Ep3_6xB4x02) / 4, 0, 0x02, 0, 0, 0}; - /* 08 */ le_uint16_t client_id = 0; - /* 0A */ le_uint16_t unused = 0; - /* 0C */ Episode3::HandAndEquipState state; - /* 60 */ -} __packed_ws__(G_UpdateHand_Ep3_6xB4x02, 0x60); - -// 6xB4x03: Set state flags - -struct G_SetStateFlags_Ep3_6xB4x03 { - /* 00 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetStateFlags_Ep3_6xB4x03) / 4, 0, 0x03, 0, 0, 0}; - /* 08 */ Episode3::StateFlags state; - /* 20 */ -} __packed_ws__(G_SetStateFlags_Ep3_6xB4x03, 0x20); - -// 6xB4x04: Update SC/FC short statuses - -struct G_UpdateShortStatuses_Ep3_6xB4x04 { - /* 0000 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateShortStatuses_Ep3_6xB4x04) / 4, 0, 0x04, 0, 0, 0}; - /* 0008 */ le_uint16_t client_id = 0; - /* 000A */ le_uint16_t unused = 0; - // The slots in this array have heterogeneous meanings. Specifically: - // [0] is the SC card status - // [1] through [6] are hand cards - // [7] through [14] are set FC cards (items/creatures) - // [15] is the set assist card - /* 000C */ parray card_statuses; - /* 010C */ -} __packed_ws__(G_UpdateShortStatuses_Ep3_6xB4x04, 0x10C); - -// 6xB4x05: Update map state -// TODO: This structure is different on Ep3 NTE because the Rules structure is -// shorter. Define an appropriate structure for this. - -struct G_UpdateMap_Ep3NTE_6xB4x05 { - /* 0000 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateMap_Ep3NTE_6xB4x05) / 4, 0, 0x05, 0, 0, 0}; - /* 0008 */ Episode3::MapAndRulesStateTrial state; - /* 0138 */ uint8_t start_battle = 0; - /* 0139 */ parray unused; - /* 013C */ -} __packed_ws__(G_UpdateMap_Ep3NTE_6xB4x05, 0x13C); - -struct G_UpdateMap_Ep3_6xB4x05 { - /* 0000 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateMap_Ep3_6xB4x05) / 4, 0, 0x05, 0, 0, 0}; - /* 0008 */ Episode3::MapAndRulesState state; - /* 0140 */ uint8_t start_battle = 0; - /* 0141 */ parray unused; - /* 0144 */ -} __packed_ws__(G_UpdateMap_Ep3_6xB4x05, 0x144); - -// 6xB4x06: Apply condition effect - -struct G_ApplyConditionEffect_Ep3_6xB4x06 { - /* 00 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_ApplyConditionEffect_Ep3_6xB4x06) / 4, 0, 0x06, 0, 0, 0}; - /* 08 */ Episode3::EffectResult effect; - /* 14 */ -} __packed_ws__(G_ApplyConditionEffect_Ep3_6xB4x06, 0x14); - -// 6xB4x07: Set battle decks - -struct G_UpdateDecks_Ep3_6xB4x07 { - /* 0000 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateDecks_Ep3_6xB4x07) / 4, 0, 0x07, 0, 0, 0}; - /* 0008 */ parray entries_present; - /* 000C */ parray entries; - /* 016C */ -} __packed_ws__(G_UpdateDecks_Ep3_6xB4x07, 0x16C); - -// 6xB4x09: Set action state - -struct G_SetActionState_Ep3_6xB4x09 { - /* 00 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetActionState_Ep3_6xB4x09) / 4, 0, 0x09, 0, 0, 0}; - /* 08 */ le_uint16_t client_id = 0; - /* 0A */ parray unknown_a1; - /* 0C */ Episode3::ActionState state; - /* 70 */ -} __packed_ws__(G_SetActionState_Ep3_6xB4x09, 0x70); - -// 6xB4x0A: Update action chain and metadata -// This command is used by Trial Edition. The final version sends 6xB4x4C, -// 6xB4x4D, and 6xB4x4E instead, but still has a handler for this command. - -struct G_UpdateActionChainAndMetadata_Ep3NTE_6xB4x0A { - /* 0000 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateActionChainAndMetadata_Ep3NTE_6xB4x0A) / 4, 0, 0x0A, 0, 0, 0}; - /* 0008 */ le_uint16_t client_id = 0; - // set_index must be 0xFF, or be in the range [0, 9]. If it's 0xFF, all nine - // chains and metadatas are cleared for the client; otherwise, the provided - // chain and metadata are copied into the slot specified by set_index. - /* 000A */ int8_t index = 0; - /* 000B */ uint8_t unused = 0; - /* 000C */ Episode3::ActionChainWithCondsTrial chain; - /* 010C */ Episode3::ActionMetadata metadata; - /* 0180 */ -} __packed_ws__(G_UpdateActionChainAndMetadata_Ep3NTE_6xB4x0A, 0x180); - -struct G_UpdateActionChainAndMetadata_Ep3_6xB4x0A { - /* 0000 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateActionChainAndMetadata_Ep3_6xB4x0A) / 4, 0, 0x0A, 0, 0, 0}; - /* 0008 */ le_uint16_t client_id = 0; - /* 000A */ int8_t index = 0; - /* 000B */ uint8_t unused = 0; - /* 000C */ Episode3::ActionChainWithConds chain; - /* 010C */ Episode3::ActionMetadata metadata; - /* 0180 */ -} __packed_ws__(G_UpdateActionChainAndMetadata_Ep3_6xB4x0A, 0x180); - -// 6xB3x0B / CAx0B: Redraw initial hand (immediately before battle) - -struct G_RedrawInitialHand_Ep3_CAx0B { - G_CardServerDataCommandHeader header = {0xB3, sizeof(G_RedrawInitialHand_Ep3_CAx0B) / 4, 0, 0x0B, 0, 0, 0, 0, 0}; - le_uint16_t client_id = 0; - parray unused2; -} __packed_ws__(G_RedrawInitialHand_Ep3_CAx0B, 0x14); - -// 6xB3x0C / CAx0C: End initial redraw phase - -struct G_EndInitialRedrawPhase_Ep3_CAx0C { - G_CardServerDataCommandHeader header = {0xB3, sizeof(G_EndInitialRedrawPhase_Ep3_CAx0C) / 4, 0, 0x0C, 0, 0, 0, 0, 0}; - le_uint16_t client_id = 0; - parray unused2; -} __packed_ws__(G_EndInitialRedrawPhase_Ep3_CAx0C, 0x14); - -// 6xB3x0D / CAx0D: End non-action phase -// This command is sent when the client has no more actions to take during the -// current phase. This command isn't used for ending the attack or defense -// phases; for those phases, CAx12 and CAx28 are used instead. - -struct G_EndNonAttackPhase_Ep3_CAx0D { - G_CardServerDataCommandHeader header = {0xB3, sizeof(G_EndNonAttackPhase_Ep3_CAx0D) / 4, 0, 0x0D, 0, 0, 0, 0, 0}; - le_uint16_t client_id = 0; - le_uint16_t battle_phase = 0; // Only used on NTE - parray unused2; -} __packed_ws__(G_EndNonAttackPhase_Ep3_CAx0D, 0x1C); - -// 6xB3x0E / CAx0E: Discard card from hand - -struct G_DiscardCardFromHand_Ep3_CAx0E { - G_CardServerDataCommandHeader header = {0xB3, sizeof(G_DiscardCardFromHand_Ep3_CAx0E) / 4, 0, 0x0E, 0, 0, 0, 0, 0}; - le_uint16_t client_id = 0; - le_uint16_t card_ref = 0xFFFF; -} __packed_ws__(G_DiscardCardFromHand_Ep3_CAx0E, 0x14); - -// 6xB3x0F / CAx0F: Set card from hand - -struct G_SetCardFromHand_Ep3_CAx0F { - G_CardServerDataCommandHeader header = {0xB3, sizeof(G_SetCardFromHand_Ep3_CAx0F) / 4, 0, 0x0F, 0, 0, 0, 0, 0}; - le_uint16_t client_id = 0; - le_uint16_t card_ref = 0xFFFF; - le_uint16_t set_index = 0; - le_uint16_t assist_target_player = 0; - Episode3::Location loc; -} __packed_ws__(G_SetCardFromHand_Ep3_CAx0F, 0x1C); - -// 6xB3x10 / CAx10: Move field character - -struct G_MoveFieldCharacter_Ep3_CAx10 { - G_CardServerDataCommandHeader header = {0xB3, sizeof(G_MoveFieldCharacter_Ep3_CAx10) / 4, 0, 0x10, 0, 0, 0, 0, 0}; - le_uint16_t client_id = 0; - le_uint16_t set_index = 0; - Episode3::Location loc; -} __packed_ws__(G_MoveFieldCharacter_Ep3_CAx10, 0x18); - -// 6xB3x11 / CAx11: Enqueue action (play card(s) during action phase) -// This command is used for playing both attacks (and the associated action -// cards), and for playing defense cards. In the attack case, this command is -// sent once for each attack (even if it includes multiple cards); in the -// defense case, this command is sent once for each defense card. - -struct G_EnqueueAttackOrDefense_Ep3_CAx11 { - G_CardServerDataCommandHeader header = {0xB3, sizeof(G_EnqueueAttackOrDefense_Ep3_CAx11) / 4, 0, 0x11, 0, 0, 0, 0, 0}; - le_uint16_t client_id = 0; - parray unused2; - Episode3::ActionState entry; -} __packed_ws__(G_EnqueueAttackOrDefense_Ep3_CAx11, 0x78); - -// 6xB3x12 / CAx12: End attack list (done playing cards during action phase) -// This command informs the server that the client is done playing attacks in -// the current round. (In the defense phase, CAx28 is used instead.) - -struct G_EndAttackList_Ep3_CAx12 { - G_CardServerDataCommandHeader header = {0xB3, sizeof(G_EndAttackList_Ep3_CAx12) / 4, 0, 0x12, 0, 0, 0, 0, 0}; - le_uint16_t client_id = 0; - parray unused2; -} __packed_ws__(G_EndAttackList_Ep3_CAx12, 0x14); - -// 6xB3x13 / CAx13: Set map state during setup - -struct G_SetMapState_Ep3NTE_CAx13 { - G_CardServerDataCommandHeader header = {0xB3, sizeof(G_SetMapState_Ep3NTE_CAx13) / 4, 0, 0x13, 0, 0, 0, 0, 0}; - Episode3::MapAndRulesStateTrial map_and_rules_state; - Episode3::OverlayState overlay_state; -} __packed_ws__(G_SetMapState_Ep3NTE_CAx13, 0x2B4); - -struct G_SetMapState_Ep3_CAx13 { - G_CardServerDataCommandHeader header = {0xB3, sizeof(G_SetMapState_Ep3_CAx13) / 4, 0, 0x13, 0, 0, 0, 0, 0}; - Episode3::MapAndRulesState map_and_rules_state; - Episode3::OverlayState overlay_state; -} __packed_ws__(G_SetMapState_Ep3_CAx13, 0x2BC); - -// 6xB3x14 / CAx14: Set player deck during setup - -struct G_SetPlayerDeck_Ep3_CAx14 { - /* 00 */ G_CardServerDataCommandHeader header = {0xB3, sizeof(G_SetPlayerDeck_Ep3_CAx14) / 4, 0, 0x14, 0, 0, 0, 0, 0}; - /* 10 */ le_uint16_t client_id = 0; - /* 12 */ uint8_t is_cpu_player = 0; - /* 13 */ uint8_t unused2 = 0; - /* 14 */ Episode3::DeckEntry entry; - /* 6C */ -} __packed_ws__(G_SetPlayerDeck_Ep3_CAx14, 0x6C); - -// 6xB3x15 / CAx15: Hard-reset server state -// This command appears to be completely unused; the client never sends it. - -struct G_HardResetServerState_Ep3_CAx15 { - G_CardServerDataCommandHeader header = {0xB3, sizeof(G_HardResetServerState_Ep3_CAx15) / 4, 0, 0x15, 0, 0, 0, 0, 0}; - // No arguments -} __packed_ws__(G_HardResetServerState_Ep3_CAx15, 0x10); - -// 6xB5x17: Unknown -// TODO: Document this from Episode 3 client/server disassembly - -struct G_Unknown_Ep3_6xB5x17 { - G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_Ep3_6xB5x17) / 4, 0, 0x17, 0, 0, 0}; - // No arguments -} __packed_ws__(G_Unknown_Ep3_6xB5x17, 8); - -// 6xB5x1A: Force disconnect -// This command seems to cause the client to unconditionally disconnect. The -// player is returned to the main menu (the "The line was disconnected" message -// box is skipped). Unlike all other known ways to disconnect, the client does -// not save when it receives this command, and instead returns directly to the -// main menu. - -struct G_ForceDisconnect_Ep3_6xB5x1A { - G_CardBattleCommandHeader header = {0xB5, sizeof(G_ForceDisconnect_Ep3_6xB5x1A) / 4, 0, 0x1A, 0, 0, 0}; - // No arguments -} __packed_ws__(G_ForceDisconnect_Ep3_6xB5x1A, 8); - -// 6xB3x1B / CAx1B: Set player name during setup -// Curiously, this command can be used during a non-setup phase; the server -// should ignore the command's contents but still send a 6xB4x1C in response. - -struct G_SetPlayerName_Ep3_CAx1B { - G_CardServerDataCommandHeader header = {0xB3, sizeof(G_SetPlayerName_Ep3_CAx1B) / 4, 0, 0x1B, 0, 0, 0, 0, 0}; - Episode3::NameEntry entry; -} __packed_ws__(G_SetPlayerName_Ep3_CAx1B, 0x24); - -// 6xB4x1C: Set all player names - -struct G_SetPlayerNames_Ep3_6xB4x1C { - G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetPlayerNames_Ep3_6xB4x1C) / 4, 0, 0x1C, 0, 0, 0}; - parray entries; -} __packed_ws__(G_SetPlayerNames_Ep3_6xB4x1C, 0x58); - -// 6xB3x1D / CAx1D: Request for battle start -// The battle actually begins when the server sends a state flags update (in -// response to this command) that includes RegistrationPhase::BATTLE_STARTED and -// a SetupPhase value other than REGISTRATION. - -struct G_StartBattle_Ep3_CAx1D { - G_CardServerDataCommandHeader header = {0xB3, sizeof(G_StartBattle_Ep3_CAx1D) / 4, 0, 0x1D, 0, 0, 0, 0, 0}; -} __packed_ws__(G_StartBattle_Ep3_CAx1D, 0x10); - -// 6xB4x1E: Action result - -struct G_ActionResult_Ep3_6xB4x1E { - /* 00 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_ActionResult_Ep3_6xB4x1E) / 4, 0, 0x1E, 0, 0, 0}; - /* 08 */ be_uint32_t sequence_num = 0; - /* 0C */ uint8_t error_code = 0; - /* 0D */ uint8_t response_phase = 0; - /* 0E */ parray unused; - /* 10 */ -} __packed_ws__(G_ActionResult_Ep3_6xB4x1E, 0x10); - -// 6xB4x1F: Set context token -// This token is sent back in the context_token field of all CA commands from -// the client. It seems Sega never used this functionality. - -struct G_SetContextToken_Ep3_6xB4x1F { - G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetContextToken_Ep3_6xB4x1F) / 4, 0, 0x1F, 0, 0, 0}; - // Note that this field is little-endian, but the corresponding context_token - // field in G_CardServerDataCommandHeader is big-endian! - le_uint32_t context_token = 0; -} __packed_ws__(G_SetContextToken_Ep3_6xB4x1F, 0x0C); - -// 6xB5x20: Unknown -// TODO: Document this from Episode 3 client/server disassembly - -struct G_Unknown_Ep3_6xB5x20 { - G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_Ep3_6xB5x20) / 4, 0, 0x20, 0, 0, 0}; - le_uint32_t player_tag = 0x00010000; - le_uint32_t guild_card_number = 0; - uint8_t client_id = 0; - parray unused; -} __packed_ws__(G_Unknown_Ep3_6xB5x20, 0x14); - -// 6xB3x21 / CAx21: End battle - -struct G_EndBattle_Ep3_CAx21 { - G_CardServerDataCommandHeader header = {0xB3, sizeof(G_EndBattle_Ep3_CAx21) / 4, 0, 0x21, 0, 0, 0, 0, 0}; - le_uint32_t unused2 = 0; -} __packed_ws__(G_EndBattle_Ep3_CAx21, 0x14); - -// 6xB4x22: Unknown -// This command appears to be completely unused. The client's handler for this -// command sets a flag on some data structure if it exists, but it appears that -// that data structure is never allocated. - -struct G_Unknown_Ep3_6xB4x22 { - G_CardBattleCommandHeader header = {0xB4, sizeof(G_Unknown_Ep3_6xB4x22) / 4, 0, 0x22, 0, 0, 0}; - // No arguments -} __packed_ws__(G_Unknown_Ep3_6xB4x22, 8); - -// 6xB4x23: Unknown -// This command was actually sent by Sega's original servers, but it does -// nothing on the client. - -struct G_Unknown_Ep3_6xB4x23 { - G_CardBattleCommandHeader header = {0xB4, sizeof(G_Unknown_Ep3_6xB4x23) / 4, 0, 0x23, 0, 0, 0}; - uint8_t present = 0; // Handler expects this to be equal to 1 - uint8_t client_id = 0; - parray unused; -} __packed_ws__(G_Unknown_Ep3_6xB4x23, 0x0C); - -// 6xB5x27: Unknown -// TODO: Document this from Episode 3 client/server disassembly - -struct G_Unknown_Ep3_6xB5x27 { - G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_Ep3_6xB5x27) / 4, 0, 0x27, 0, 0, 0}; - // Note: This command uses header_b1 as well, which looks like another client - // ID (it must be < 4, though it does not always match unknown_a1 below). - le_uint32_t unknown_a1 = 0; // Probably client ID (must be < 4) - le_uint32_t unknown_a2 = 0; // Must be < 0x10 - le_uint32_t unknown_a3 = 0; - le_uint32_t unused = 0; // Curiously, this usually contains a memory address -} __packed_ws__(G_Unknown_Ep3_6xB5x27, 0x18); - -// 6xB3x28 / CAx28: End defense list -// This command informs the server that the client is done playing defense -// cards. (In the attack phase, CAx12 is used instead.) - -struct G_EndDefenseList_Ep3_CAx28 { - G_CardServerDataCommandHeader header = {0xB3, sizeof(G_EndDefenseList_Ep3_CAx28) / 4, 0, 0x28, 0, 0, 0, 0, 0}; - uint8_t unused1 = 0; - uint8_t client_id = 0; - parray unused2; -} __packed_ws__(G_EndDefenseList_Ep3_CAx28, 0x14); - -// 6xB4x29: Set action state -// TODO: How is this different from 6xB4x09? It looks like the server never -// sends this. - -struct G_SetActionState_Ep3_6xB4x29 { - /* 00 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetActionState_Ep3_6xB4x29) / 4, 0, 0x29, 0, 0, 0}; - /* 08 */ uint8_t unknown_a1 = 0; - /* 09 */ parray unknown_a2; - /* 0C */ Episode3::ActionState state; - /* 70 */ -} __packed_ws__(G_SetActionState_Ep3_6xB4x29, 0x70); - -// 6xB4x2A: Unknown -// TODO: Document this from Episode 3 client/server disassembly - -struct G_Unknown_Ep3_6xB4x2A { - G_CardBattleCommandHeader header = {0xB4, sizeof(G_Unknown_Ep3_6xB4x2A) / 4, 0, 0x2A, 0, 0, 0}; - parray unknown_a1; - le_uint16_t unknown_a2 = 0; - parray unused; -} __packed_ws__(G_Unknown_Ep3_6xB4x2A, 0x10); - -// 6xB3x2B / CAx2B: Legacy set card -// It seems Sega's servers completely ignored this command. The command name is -// based on a debug message found nearby. - -struct G_ExecLegacyCard_Ep3_CAx2B { - G_CardServerDataCommandHeader header = {0xB3, sizeof(G_ExecLegacyCard_Ep3_CAx2B) / 4, 0, 0x2B, 0, 0, 0, 0, 0}; - le_uint16_t unused2 = 0; - parray unused3; -} __packed_ws__(G_ExecLegacyCard_Ep3_CAx2B, 0x14); - -// 6xB4x2C: Unknown - -struct G_Unknown_Ep3_6xB4x2C { - /* 00 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_Unknown_Ep3_6xB4x2C) / 4, 0, 0x2C, 0, 0, 0}; - /* 08 */ uint8_t change_type = 0; - /* 09 */ uint8_t client_id = 0; - /* 0A */ parray card_refs; - /* 10 */ Episode3::Location loc; - /* 14 */ parray unknown_a2; - /* 1C */ -} __packed_ws__(G_Unknown_Ep3_6xB4x2C, 0x1C); - -// 6xB5x2D: Unknown -// TODO: Document this from Episode 3 client/server disassembly - -struct G_Unknown_Ep3_6xB5x2D { - G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_Ep3_6xB5x2D) / 4, 0, 0x2D, 0, 0, 0}; - // This array is indexed by client ID. When a client receives this command, it - // sends a 6x70 command to itself. It's not clear what the function of this is - // intended to be. - // TODO: Figure out if tournament fast loading can be implemented using this - // to fix the stuck-in-wall glitch. - parray unknown_a1; -} __packed_ws__(G_Unknown_Ep3_6xB5x2D, 0x0C); - -// 6xB5x2E: Notify other players that battle is about to end - -struct G_BattleEndNotification_Ep3_6xB5x2E { - G_CardBattleCommandHeader header = {0xB5, sizeof(G_BattleEndNotification_Ep3_6xB5x2E) / 4, 0, 0x2E, 0, 0, 0}; - uint8_t unknown_a1 = 0; // Command ignored unless this is 0 or 1 - parray unused; -} __packed_ws__(G_BattleEndNotification_Ep3_6xB5x2E, 0x0C); - -// 6xB5x2F: Set deck in battle setup menu - -struct G_SetDeckInBattleSetupMenu_Ep3_6xB5x2F { - G_CardBattleCommandHeader header = {0xB5, sizeof(G_SetDeckInBattleSetupMenu_Ep3_6xB5x2F) / 4, 0, 0x2F, 0, 0, 0}; - parray unknown_a1; - parray unknown_a2; - pstring deck_name; - parray unknown_a3; - le_uint16_t unknown_a4 = 0; - parray card_ids; - parray unused; - le_uint32_t unknown_a5 = 0; - le_uint16_t unknown_a6 = 0; - le_uint16_t unknown_a7 = 0; -} __packed_ws__(G_SetDeckInBattleSetupMenu_Ep3_6xB5x2F, 0x8C); - -// 6xB5x30: Unknown -// The client never sends this command, and when the client received this -// command, it does nothing. - -struct G_Unknown_Ep3_6xB5x30 { - G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_Ep3_6xB5x30) / 4, 0, 0x30, 0, 0, 0}; - // No arguments -} __packed_ws__(G_Unknown_Ep3_6xB5x30, 8); - -// 6xB5x31: Confirm deck selection - -struct G_ConfirmDeckSelection_Ep3_6xB5x31 { - G_CardBattleCommandHeader header = {0xB5, sizeof(G_ConfirmDeckSelection_Ep3_6xB5x31) / 4, 0, 0x31, 0, 0, 0}; - // Note: This command uses header_b1 for... something. - uint8_t unknown_a1 = 0; // Must be 0 or 1 - uint8_t unknown_a2 = 0; // Must be < 4 - uint8_t unknown_a3 = 0; // Must be < 4 - uint8_t unknown_a4 = 0; // Must be < 0x14 - uint8_t unknown_a5 = 0; // Used as an array index - parray unused; -} __packed_ws__(G_ConfirmDeckSelection_Ep3_6xB5x31, 0x10); - -// 6xB5x32: Move shared menu cursor - -struct G_MoveSharedMenuCursor_Ep3_6xB5x32 { - G_CardBattleCommandHeader header = {0xB5, sizeof(G_MoveSharedMenuCursor_Ep3_6xB5x32) / 4, 0, 0x32, 0, 0, 0}; - le_uint16_t selected_item_index = 0xFFFF; - le_uint16_t chosen_item_index = 0xFFFF; - uint8_t unknown_a1 = 0; - uint8_t unknown_a2 = 0; - uint8_t unknown_a3 = 0; - uint8_t unknown_a4 = 0; - uint8_t unknown_a5 = 0; - parray unused; -} __packed_ws__(G_MoveSharedMenuCursor_Ep3_6xB5x32, 0x14); - -// 6xB4x33: Subtract ally ATK points (e.g. for photon blast) - -struct G_SubtractAllyATKPoints_Ep3_6xB4x33 { - /* 00 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_SubtractAllyATKPoints_Ep3_6xB4x33) / 4, 0, 0x33, 0, 0, 0}; - /* 08 */ uint8_t client_id = 0; - /* 09 */ uint8_t ally_cost = 0; - /* 0A */ le_uint16_t card_ref = 0xFFFF; - /* 0C */ -} __packed_ws__(G_SubtractAllyATKPoints_Ep3_6xB4x33, 0x0C); - -// 6xB3x34 / CAx34: Photon blast request - -struct G_PhotonBlastRequest_Ep3_CAx34 { - G_CardServerDataCommandHeader header = {0xB3, sizeof(G_PhotonBlastRequest_Ep3_CAx34) / 4, 0, 0x34, 0, 0, 0, 0, 0}; - uint8_t ally_client_id = 0; - uint8_t reason = 0; - le_uint16_t card_ref = 0xFFFF; -} __packed_ws__(G_PhotonBlastRequest_Ep3_CAx34, 0x14); - -// 6xB4x35: Update photon blast status - -struct G_PhotonBlastStatus_Ep3_6xB4x35 { - /* 00 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_PhotonBlastStatus_Ep3_6xB4x35) / 4, 0, 0x35, 0, 0, 0}; - /* 08 */ uint8_t client_id = 0; - /* 09 */ uint8_t accepted = 0; - /* 0A */ le_uint16_t card_ref = 0xFFFF; - /* 0C */ -} __packed_ws__(G_PhotonBlastStatus_Ep3_6xB4x35, 0x0C); - -// 6xB5x36: Recreate player -// Setting client_id to a value 4 or greater while in a game causes the player -// to be temporarily replaced with a default HUmar and placed inside the central -// column in the Morgue, rendering them unable to move. The only ways out of -// this predicament appear to be either to disconnect (e.g. select Quit Game -// from the pause menu) or receive an ED (force leave game) command. - -struct G_RecreatePlayer_Ep3_6xB5x36 { - G_CardBattleCommandHeader header = {0xB5, sizeof(G_RecreatePlayer_Ep3_6xB5x36) / 4, 0, 0x36, 0, 0, 0}; - uint8_t client_id = 0; - parray unused; -} __packed_ws__(G_RecreatePlayer_Ep3_6xB5x36, 0x0C); - -// 6xB3x37 / CAx37: Ready to advance from starting rolls phase - -struct G_AdvanceFromStartingRollsPhase_Ep3_CAx37 { - G_CardServerDataCommandHeader header = {0xB3, sizeof(G_AdvanceFromStartingRollsPhase_Ep3_CAx37) / 4, 0, 0x37, 0, 0, 0, 0, 0}; - uint8_t client_id = 0; - parray unused2; -} __packed_ws__(G_AdvanceFromStartingRollsPhase_Ep3_CAx37, 0x14); - -// 6xB5x38: Card counts request -// This command causes the client identified by requested_client_id to send a -// 6xBC command to the client identified by reply_to_client_id (privately, via -// the 6D command). This appears to be unused; it is likely superseded by the -// CAx49 command. - -struct G_CardCountsRequest_Ep3_6xB5x38 { - G_CardBattleCommandHeader header = {0xB5, sizeof(G_CardCountsRequest_Ep3_6xB5x38) / 4, 0, 0x38, 0, 0, 0}; - uint8_t requested_client_id = 0; - uint8_t reply_to_client_id = 0; - parray unused; -} __packed_ws__(G_CardCountsRequest_Ep3_6xB5x38, 0x0C); - -// 6xB4x39: Update all player statistics - -struct G_UpdateAllPlayerStatistics_Ep3NTE_6xB4x39 { - /* 00 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateAllPlayerStatistics_Ep3NTE_6xB4x39) / 4, 0, 0x39, 0, 0, 0}; - /* 08 */ parray stats; - /* 58 */ -} __packed_ws__(G_UpdateAllPlayerStatistics_Ep3NTE_6xB4x39, 0x58); - -struct G_UpdateAllPlayerStatistics_Ep3_6xB4x39 { - /* 00 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateAllPlayerStatistics_Ep3_6xB4x39) / 4, 0, 0x39, 0, 0, 0}; - /* 08 */ parray stats; - /* A8 */ -} __packed_ws__(G_UpdateAllPlayerStatistics_Ep3_6xB4x39, 0xA8); - -// 6xB3x3A / CAx3A: Overall time limit expired -// It seems Sega's servers completely ignored this command and used server-side -// timing instead. newserv does the same. - -struct G_OverallTimeLimitExpired_Ep3_CAx3A { - G_CardServerDataCommandHeader header = {0xB3, sizeof(G_OverallTimeLimitExpired_Ep3_CAx3A) / 4, 0, 0x3A, 0, 0, 0, 0, 0}; -} __packed_ws__(G_OverallTimeLimitExpired_Ep3_CAx3A, 0x10); - -// 6xB4x3B: Load current environment -// This command is used to send spectators in a spectator team to the main -// battle. A 6xB4x05 and 6xB6x41 command shouldhave been sent before this, to -// set the map state that should appear for the new spectator. - -struct G_LoadCurrentEnvironment_Ep3_6xB4x3B { - G_CardBattleCommandHeader header = {0xB4, sizeof(G_LoadCurrentEnvironment_Ep3_6xB4x3B) / 4, 0, 0x3B, 0, 0, 0}; - parray unused; -} __packed_ws__(G_LoadCurrentEnvironment_Ep3_6xB4x3B, 0x0C); - -// 6xB5x3C: Set player substatus -// This command sets the text that appears under the player's name in the HUD. - -struct G_SetPlayerSubstatus_Ep3_6xB5x3C { - // Note: header.sender_client_id specifies which client's status to update - G_CardBattleCommandHeader header = {0xB5, sizeof(G_SetPlayerSubstatus_Ep3_6xB5x3C) / 4, 0, 0x3C, 0, 0, 0}; - // Status values: - // 00 (or any value not listed below) = (nothing) - // 01 = Editing - // 02 = Trading... - // 03 = At Counter - uint8_t status = 0; - parray unused; -} __packed_ws__(G_SetPlayerSubstatus_Ep3_6xB5x3C, 0x0C); - -// 6xB4x3D: Set tournament player decks -// This is sent before the counter sequence in a tournament game, to reserve the -// player and COM slots and set the map number. - -struct G_SetTournamentPlayerDecks_Ep3_6xB4x3D_Entry { - /* 00 */ uint8_t type = 0; // 0 = no player, 1 = human, 2 = COM - /* 01 */ pstring player_name; - /* 11 */ pstring deck_name; // Only used for COM players - /* 21 */ parray unknown_a1; - /* 26 */ parray card_ids; // Can be blank for human players - /* 64 */ uint8_t client_id = 0; // Unused for COMs - /* 65 */ uint8_t unknown_a4 = 0; - /* 66 */ le_uint16_t unknown_a2 = 0; - /* 68 */ le_uint16_t unknown_a3 = 0; - /* 6A */ -} __packed_ws__(G_SetTournamentPlayerDecks_Ep3_6xB4x3D_Entry, 0x6A); - -struct G_SetTournamentPlayerDecks_Ep3NTE_6xB4x3D { - /* 0000 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetTournamentPlayerDecks_Ep3NTE_6xB4x3D) / 4, 0, 0x3D, 0, 0, 0}; - /* 0008 */ Episode3::RulesTrial rules; - /* 0014 */ parray entries; - /* 01BC */ le_uint32_t map_number = 0; - /* 01C0 */ uint8_t player_slot = 0; // Which deck slot is editable by the client - /* 01C1 */ uint8_t unknown_a3 = 0; - /* 01C2 */ uint8_t unknown_a4 = 0; - /* 01C3 */ uint8_t unknown_a5 = 0; - /* 01C4 */ -} __packed_ws__(G_SetTournamentPlayerDecks_Ep3NTE_6xB4x3D, 0x1C4); - -struct G_SetTournamentPlayerDecks_Ep3_6xB4x3D { - /* 0000 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetTournamentPlayerDecks_Ep3_6xB4x3D) / 4, 0, 0x3D, 0, 0, 0}; - /* 0008 */ Episode3::Rules rules; - /* 001C */ parray entries; - /* 01C4 */ le_uint32_t map_number = 0; - /* 01C8 */ uint8_t player_slot = 0; // Which deck slot is editable by the client - /* 01C9 */ uint8_t unknown_a3 = 0; - /* 01CA */ uint8_t unknown_a4 = 0; - /* 01CB */ uint8_t unknown_a5 = 0; - /* 01CC */ -} __packed_ws__(G_SetTournamentPlayerDecks_Ep3_6xB4x3D, 0x1CC); - -// 6xB5x3E: Make card auction bid - -struct G_MakeCardAuctionBid_Ep3_6xB5x3E { - G_CardBattleCommandHeader header = {0xB5, sizeof(G_MakeCardAuctionBid_Ep3_6xB5x3E) / 4, 0, 0x3E, 0, 0, 0}; - // Note: This command uses header.unknown_a1 for the bidder's client ID. - uint8_t card_index = 0; // Index of card in EF command - uint8_t bid_value = 0; // 1-99 - parray unused; -} __packed_ws__(G_MakeCardAuctionBid_Ep3_6xB5x3E, 0x0C); - -// 6xB5x3F: Open blocking menu -// This command opens a shared menu between all clients in a game. The client -// specified in .client_id is able to control the menu; the other clients see -// that player's actions but cannot control anything. - -struct G_OpenBlockingMenu_Ep3_6xB5x3F { - G_CardBattleCommandHeader header = {0xB5, sizeof(G_OpenBlockingMenu_Ep3_6xB5x3F) / 4, 0, 0x3F, 0, 0, 0}; - // Menu type should be one of these values: - // 0x01/0x02 = battle prep menu - // 0x11 = card auction counter menu (join or cancel) - // 0x12 = go directly to card auction state (client sends EF command) - // Other values will likely crash the client. - int8_t menu_type = 0; // Must be in the range [-1, 0x14] - uint8_t client_id = 0; - parray unused1; - le_uint32_t unknown_a3 = 0; - parray unused2; -} __packed_ws__(G_OpenBlockingMenu_Ep3_6xB5x3F, 0x14); - -// 6xB3x40 / CAx40: Request map list -// The server should respond with a 6xB6x40 command. - -struct G_MapListRequest_Ep3_CAx40 { - G_CardServerDataCommandHeader header = {0xB3, sizeof(G_MapListRequest_Ep3_CAx40) / 4, 0, 0x40, 0, 0, 0, 0, 0}; -} __packed_ws__(G_MapListRequest_Ep3_CAx40, 0x10); - -// 6xB3x41 / CAx41: Request map data -// The server should respond with a 6xB6x41 command containing the definition of -// the specified map. - -struct G_MapDataRequest_Ep3_CAx41 { - G_CardServerDataCommandHeader header = {0xB3, sizeof(G_MapDataRequest_Ep3_CAx41) / 4, 0, 0x41, 0, 0, 0, 0, 0}; - le_uint32_t map_number = 0; -} __packed_ws__(G_MapDataRequest_Ep3_CAx41, 0x14); - -// 6xB5x42: Initiate card auction -// Sending this command to a client has the same effect as sending a 6xB5x3F -// command to tell it to open the auction menu. (This works even if the client -// doesn't have a VIP card or there are fewer than 4 players in the current -// game.) Under normal operation, the server doesn't need to do this - the -// client sends this when all of the following conditions are met: -// 1. The client has a VIP card. (This is stored client-side in seq flag 7000.) -// 2. The client is in a game with 4 players. -// 3. All clients are at the auction counter. - -struct G_InitiateCardAuction_Ep3_6xB5x42 { - G_CardBattleCommandHeader header = {0xB5, sizeof(G_InitiateCardAuction_Ep3_6xB5x42) / 4, 0, 0x42, 0, 0, 0}; - // This command uses header.unknown_a1 (probably for the client's ID). -} __packed_ws__(G_InitiateCardAuction_Ep3_6xB5x42, 8); - -// 6xB5x43: Unknown -// This command stores the card IDs and counts in a global array on the client, -// but this array is never read from. It's likely this is a remnant of an -// unimplemented or removed feature, or an earlier implementation of the card -// trade window. - -struct G_Unknown_Ep3_6xB5x43 { - G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_Ep3_6xB5x43) / 4, 0, 0x43, 0, 0, 0}; - struct Entry { - // Both fields here are masked. To get the actual values used by the game, - // XOR the values here with 0x39AB. - le_uint16_t masked_card_id = 0xFFFF; // Must be < 0x2F1 (when unmasked) - le_uint16_t masked_count = 0; // Must be in [1, 99] (when unmasked) - } __packed_ws__(Entry, 4); - parray entries; -} __packed_ws__(G_Unknown_Ep3_6xB5x43, 0x58); - -// 6xB5x44: Card auction bid summary - -struct G_CardAuctionBidSummary_Ep3_6xB5x44 { - G_CardBattleCommandHeader header = {0xB5, sizeof(G_CardAuctionBidSummary_Ep3_6xB5x44) / 4, 0, 0x44, 0, 0, 0}; - // Note: This command uses header.unknown_a1 for the bidder's client ID. - parray bids; // In same order as cards in the EF command -} __packed_ws__(G_CardAuctionBidSummary_Ep3_6xB5x44, 0x18); - -// 6xB5x45: Card auction results - -struct G_CardAuctionResults_Ep3_6xB5x45 { - G_CardBattleCommandHeader header = {0xB5, sizeof(G_CardAuctionResults_Ep3_6xB5x45) / 4, 0, 0x45, 0, 0, 0}; - // Note: This command uses header.unknown_a1 for the sender's client ID. - // This array is indexed by [card_index][client_id], and contains the final - // bid for each player on each card (or 0 if they did not bid on that card). - parray, 8> bids_by_player; -} __packed_ws__(G_CardAuctionResults_Ep3_6xB5x45, 0x48); - -// 6xB4x46: Server version strings -// This command doesn't seem to be necessary to actually play the game; the -// client just copies the included strings to global buffers and then ignores -// them. Sega's servers sent this twice for each battle, however: once after the -// initial setup phase (before starter rolls) and once when the results screen -// appeared. The second instance of this command appears to be caused by them -// recreating the TCardServer object (implemented in newserv's Episode3::Server) -// in order to support multiple sequential battles in the same team. - -struct G_ServerVersionStrings_Ep3_6xB4x46 { - G_CardBattleCommandHeader header = {0xB4, sizeof(G_ServerVersionStrings_Ep3_6xB4x46) / 4, 0, 0x46, 0, 0, 0}; - // In all of the examples (from Sega's servers) that I've seen of this - // command, these fields have the following values: - // version_signature = "[V1][FINAL2.0] 03/09/13 15:30 by K.Toya" - // date_str1 = "Mar 7 2007 21:42:40" - // In the client, date_str1 is different: - // version_signature = "[V1][FINAL2.0] 03/09/13 15:30 by K.Toya" - // date_str1 = "Jan 21 2004 18:36:47' - // Presumably if any logs exist from before 7 March 2007, they would have a - // different date_str1, but the unchanged version_signature likely means that - // Sega never made any code changes on the server side. - pstring version_signature; - pstring date_str1; // Probably card definitions revision date - // In Sega's implementation, it seems this field is blank when starting a - // battle, and contains the current time (in the format "YYYY/MM/DD hh:mm:ss") - // when ending a battle. This may have been used for identifying debug logs. - pstring date_str2; - // It seems Sega used to send 0 here when starting a battle, and 0x04157580 - // when ending a battle. Since the field is unused by the client, it's not - // clear what that value means, if anything. This behavior may be another - // uninitialized memory bug in the server implementation (of which there are - // many other examples). - le_uint32_t unused = 0; -} __packed_ws__(G_ServerVersionStrings_Ep3_6xB4x46, 0xCC); - -struct G_ServerVersionStrings_Ep3NTE_6xB4x46 { - G_CardBattleCommandHeader header = {0xB4, sizeof(G_ServerVersionStrings_Ep3NTE_6xB4x46) / 4, 0, 0x46, 0, 0, 0}; - // Ep3 NTE uses the following strings: - // "03/05/29 18:00 by K.Toya" - pstring version_signature; - // "Jun 11 2003 05:02:36" - pstring date_str1; -} __packed_ws__(G_ServerVersionStrings_Ep3NTE_6xB4x46, 0x88); - -// 6xB5x47: Set spectator's CARD level -// header.sender_client_id is the spectator's client ID. - -struct G_SetSpectatorCARDLevel_Ep3_6xB5x47 { - G_CardBattleCommandHeader header = {0xB5, sizeof(G_SetSpectatorCARDLevel_Ep3_6xB5x47) / 4, 0, 0x47, 0, 0, 0}; - le_uint32_t clv = 0; -} __packed_ws__(G_SetSpectatorCARDLevel_Ep3_6xB5x47, 0x0C); - -// 6xB3x48 / CAx48: End turn - -struct G_EndTurn_Ep3_CAx48 { - G_CardServerDataCommandHeader header = {0xB3, sizeof(G_EndTurn_Ep3_CAx48) / 4, 0, 0x48, 0, 0, 0, 0, 0}; - uint8_t client_id = 0; - parray unused2; -} __packed_ws__(G_EndTurn_Ep3_CAx48, 0x14); - -// 6xB3x49 / CAx49: Card counts -// This command is sent when a client joins a game, but it is completely ignored -// by the original Episode 3 server. Sega presumably could have used this to -// detect the presence of unreleased cards to ban cheaters, but the effects of -// the non-saveable Have All Cards AR code don't appear in this data, so this -// would have been ineffective. There appears to be a place where Sega's server -// intended to use this data, however - the deck verification function takes a -// pointer to the card counts array, but Sega's implementation always passes -// null there, which skips the owned card count check. newserv uses this data at -// that callsite to implement one of the deck validity checks. -// Episode 3 Trial Edition does not send this command. - -struct G_CardCounts_Ep3_CAx49 { - G_CardServerDataCommandHeader header = {0xB3, sizeof(G_CardCounts_Ep3_CAx49) / 4, 0, 0x49, 0, 0, 0, 0, 0}; - uint8_t basis = 0; - parray unused; - // This is encrypted with the trivial algorithm (see decrypt_trivial_gci_data) - // using the basis in the preceding field - parray card_id_to_count; -} __packed_ws__(G_CardCounts_Ep3_CAx49, 0x304); - -// 6xB4x4A: Add to set card log -// This command is not valid on Episode 3 Trial Edition. -// TODO: Document this from Episode 3 client/server disassembly - -struct G_AddToSetCardlog_Ep3_6xB4x4A { - G_CardBattleCommandHeader header = {0xB4, sizeof(G_AddToSetCardlog_Ep3_6xB4x4A) / 4, 0, 0x4A, 0, 0, 0}; - // Note: entry_count appears not to be bounds-checked; presumably the server - // could send up to 0xFF entries, but those after the 8th would not be - // byteswapped before the client handles them. - uint8_t client_id = 0; - uint8_t entry_count = 0; - le_uint16_t round_num = 0; - parray card_refs; -} __packed_ws__(G_AddToSetCardlog_Ep3_6xB4x4A, 0x1C); - -// 6xB4x4B: Set EX result values -// This command is not valid on Episode 3 Trial Edition. -// This command specifies how much EX the player should get based on the -// difference between their level and the levels of the players they defeated or -// were defeated by. (For multi-player opponent teams, the average of the -// opponents' levels is used.) The game scans the appropriate list for the entry -// whose threshold is less than or equal to than the level difference, and -// returns the corresponding value. For example, if the first two entries in the -// win list are {20, 40} and {10, 30}, and the player defeats an opponent who is -// 15 levels above the player's level, the player will get 30 EX when they win -// the battle. If all thresholds are greater than the level difference, the last -// entry's value is used. Finally, if the opponent team has no humans on it, the -// resulting EX values are divided by 2 (so in the example above, the player -// would only get 15 EX for defeating COMs). - -// If any entry in either list has .value < -100 or > 100, the entire command is -// ignored and the EX thresholds and values are reset to their default values. -// These default values are: -// win_entries = {50, 100}, {30, 80}, {15, 70}, {10, 55}, {7, 45}, {4, 35}, -// {1, 25}, {-1, 20}, {-9, 15}, {0, 10} -// lose_entries = {1, 0}, {-2, 0}, {-3, 0}, {-4, 0}, {-5, 0}, {-6, 0}, {-7, 0}, -// {-10, -10}, {-30, -10}, {0, -15} - -struct G_SetEXResultValues_Ep3_6xB4x4B { - G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetEXResultValues_Ep3_6xB4x4B) / 4, 0, 0x4B, 0, 0, 0}; - struct Entry { - le_int16_t threshold = 0; - le_int16_t value = 0; - } __packed_ws__(Entry, 4); - parray win_entries; - parray lose_entries; -} __packed_ws__(G_SetEXResultValues_Ep3_6xB4x4B, 0x58); - -// 6xB4x4C: Update action chain -// This command is not valid on Episode 3 Trial Edition. - -struct G_UpdateActionChain_Ep3_6xB4x4C { - G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateActionChain_Ep3_6xB4x4C) / 4, 0, 0x4C, 0, 0, 0}; - uint8_t client_id = 0; - int8_t index = 0; - parray unused; - Episode3::ActionChain chain; -} __packed_ws__(G_UpdateActionChain_Ep3_6xB4x4C, 0x7C); - -// 6xB4x4D: Update action metadata -// This command is not valid on Episode 3 Trial Edition. - -struct G_UpdateActionMetadata_Ep3_6xB4x4D { - G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateActionMetadata_Ep3_6xB4x4D) / 4, 0, 0x4D, 0, 0, 0}; - uint8_t client_id = 0; - int8_t index = 0; - parray unused; - Episode3::ActionMetadata metadata; -} __packed_ws__(G_UpdateActionMetadata_Ep3_6xB4x4D, 0x80); - -// 6xB4x4E: Update card conditions -// This command is not valid on Episode 3 Trial Edition. - -struct G_UpdateCardConditions_Ep3_6xB4x4E { - G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateCardConditions_Ep3_6xB4x4E) / 4, 0, 0x4E, 0, 0, 0}; - uint8_t client_id = 0; - int8_t index = 0; - parray unused; - parray conditions; -} __packed_ws__(G_UpdateCardConditions_Ep3_6xB4x4E, 0x9C); - -// 6xB4x4F: Clear set card conditions -// This command is not valid on Episode 3 Trial Edition. - -struct G_ClearSetCardConditions_Ep3_6xB4x4F { - G_CardBattleCommandHeader header = {0xB4, sizeof(G_ClearSetCardConditions_Ep3_6xB4x4F) / 4, 0, 0x4F, 0, 0, 0}; - uint8_t client_id = 0; - uint8_t unused = 0; - // For each 1 bit in this mask, the conditions of the corresponding card - // should be deleted. The low bit corresponds to the SC card; the next bit - // corresponds to set slot 0, the next bit to set slot 1, etc. (The upper 7 - // bits of this field are unused.) - le_uint16_t clear_mask = 0; -} __packed_ws__(G_ClearSetCardConditions_Ep3_6xB4x4F, 0x0C); - -// 6xB4x50: Set trap tile locations -// This command is not valid on Episode 3 Trial Edition. - -struct G_SetTrapTileLocations_Ep3_6xB4x50 { - G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetTrapTileLocations_Ep3_6xB4x50) / 4, 0, 0x50, 0, 0, 0}; - // Each entry in this array corresponds to one of the 5 trap types, in order. - // Each entry is an [x, y] pair; if that trap type is not present, its - // location entry is FF FF. - parray, 5> locations; - parray unused; -} __packed_ws__(G_SetTrapTileLocations_Ep3_6xB4x50, 0x14); - -// 6xB4x51: Tournament match result -// This command is not valid on Episode 3 Trial Edition. -// This is sent as soon as the battle result is determined (before the battle -// results screen). If the client is in tournament mode (tournament_flag is 1 in -// the StateFlags struct), then it will use this information to show the -// tournament match result screen before the battle results screen. - -struct G_TournamentMatchResult_Ep3_6xB4x51 { - G_CardBattleCommandHeader header = {0xB4, sizeof(G_TournamentMatchResult_Ep3_6xB4x51) / 4, 0, 0x51, 0, 0, 0}; - pstring match_description; - struct NamesEntry { - pstring team_name; - parray, 2> player_names; - } __packed_ws__(NamesEntry, 0x40); - parray names_entries; - le_uint16_t unused1 = 0; - // If round_num is equal to 6, the "On to the next battle..." text is replaced - // with "Congratulations!" and some flashier graphics. This is used for the - // final match. - le_uint16_t round_num = 0; - le_uint16_t num_players_per_team = 0; - le_uint16_t winner_team_id = 0; - le_uint32_t meseta_amount = 0; - // This field apparently is supposed to contain a %s token (as for printf) - // which is replaced with meseta_amount. The results screen animates this text - // counting up from 0 to meseta_amount. - pstring meseta_reward_text; -} __packed_ws__(G_TournamentMatchResult_Ep3_6xB4x51, 0xF4); - -// 6xB4x52: Set game metadata -// This command is not valid on Episode 3 Trial Edition. -// This is sent to all players in a game and all attached spectator teams when -// any player joins or leaves any spectator team watching the same game. - -struct G_SetGameMetadata_Ep3_6xB4x52 { - G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetGameMetadata_Ep3_6xB4x52) / 4, 0, 0x52, 0, 0, 0}; - // This field appears before the slash in the spectators' HUD. Presumably this - // is used to indicate how many spectators are in the current spectator team. - // In the primary game (watched lobby), this is presumably unused. - le_uint16_t local_spectators = 0; // Clamped to [0, 999] by the client - // This field appears after the slash in the spectators' HUD. This is used to - // indicate how many spectators there are in all spectator teams attached to - // the same battle. - // This field also controls the icon shown in the primary game. If this field - // is nonzero, an icon appears in the middle of the screen during battle when - // the details view is enabled (by pressing Z). However, the number of people - // visible in the icon doesn't match the value in this field. Specifically: - // 0 = no icon - // 1 = icon with a single spectator (green) - // 2-4 = icon with 3 spectators (blue) - // 5-10 = icon with 5 spectators (yellow) - // 11-29 = icon with 8 spectators (purple) - // 30+ = icon with 12 spectators (red) - le_uint16_t total_spectators = 0; // Clamped to [0, 999] by the client - le_uint16_t unused = 0; - // If text_size is not zero, the text is shown in the top bar instead of the - // usual message ("Viewing Battle", "Time left: XX:XX", and the like). - le_uint16_t text_size = 0; - pstring text; -} __packed_ws__(G_SetGameMetadata_Ep3_6xB4x52, 0x110); - -// 6xB4x53: Reject battle start request -// This command is not valid on Episode 3 Trial Edition. -// This is sent in response to a CAx1D command if setup isn't complete (e.g. if -// some names/decks are missing or invalid). Under normal operation, this should -// never happen. -// Note: It seems the client ignores everything in this structure; the command -// handler just sets a global state flag and returns immediately. - -struct G_RejectBattleStartRequest_Ep3_6xB4x53 { - G_CardBattleCommandHeader header = {0xB4, sizeof(G_RejectBattleStartRequest_Ep3_6xB4x53) / 4, 0, 0x53, 0, 0, 0}; - Episode3::SetupPhase setup_phase; - Episode3::RegistrationPhase registration_phase; - parray unused; - Episode3::MapAndRulesState state; -} __packed_ws__(G_RejectBattleStartRequest_Ep3_6xB4x53, 0x144); - -//////////////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// -// EXTENDED COMMANDS /////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -// These commands are not part of the official protocol; newserv uses these to -// implement extended functionality. - -// 30 (C->S): Extended player info -// Requested with the GetExtendedPlayerInfo patch. Format depends on version: -// DC v2: PSODCV2CharacterFile -// GC v3: PSOGCCharacterFile::Character -// XB v3: PSOXBCharacterFileCharacter +#pragma once + +#include +#include +#include +#include + +#include "Episode3/DataIndexes.hh" +#include "Episode3/DeckState.hh" +#include "Episode3/MapState.hh" +#include "Episode3/PlayerStateSubordinates.hh" +#include "PSOProtocol.hh" +#include "PlayerSubordinates.hh" +#include "SaveFileFormats.hh" +#include "Text.hh" + +// This file is newserv's canonical reference of the PSO client/server protocol. + +// For the unfamiliar, the le_uint and be_uint types (from phosg/Encoding.hh) +// are the same as normal uint types, but are explicitly little-endian or +// big-endian. The parray type (from Text.hh) is the same as a standard array, +// but has various safety and convenience features so we don't have to use +// easy-to-mess-up functions like memset/memcpy and strncpy. The pstring types +// (also from Text.hh) are like std::strings, but have an explicit encoding. +// They can be implicitly converted to and from std::strings, and will encode +// or decode their specified encoding when doing so. (The default encoding is +// UTF-8 everywhere in the server code.) + +// Struct names are like [S|C|SC]_CommandName_[Versions]_Numbers +// S/C denotes who sends the command (S = server, C = client, SC = both) +// If versions are not specified, the format is the same for all versions. + +// The version tokens are as follows: +// DCv1 = PSO Dreamcast v1 +// DCv2 = PSO Dreamcast v2 +// DC = Both DCv1 and DCv2 +// PC = PSO PC (v2) +// GC = PSO GC Episodes 1&2 and/or Episode 3 +// XB = PSO Xbox Episodes 1&2 +// BB = PSO Blue Burst +// V3 = PSO GC and PSO Xbox (these versions are similar and share many formats) + +// For variable-length commands, generally a zero-length array is included on +// the end of the struct if the command is received by newserv, and is omitted +// if it's sent by newserv. In the latter case, we often use StringWriter to +// construct the command data instead. + +// Structures are sorted by command number. Long BB commands are placed in order +// according to their low byte; for example, command 01EB is in position as EB. + +// Text escape codes + +// Most text fields allow the use of various escape codes to change decoding, +// change color, or create symbols. These escape codes are always preceded by a +// tab character (0x09, or '\t'). For brevity, we generally refer to them with $ +// instead in newserv, since the server substitutes most usage of $ in player- +// provided text with \t. The escape codes are: +// - Language codes +// - - $E: Set text interpretation to English / use Roman font +// - - $J: Set text interpretation to Japanese / use Japanese font +// - - $B: Use Simplified Chinese font (PC/BB) +// - - $T: Use Traditional Chinese font (PC/BB) +// - - $K: Use Korean font (PC/BB) +// - Color codes +// - - $C0: Black (000000) +// - - $C1: Blue (0000FF) +// - - $C2: Green (00FF00) +// - - $C3: Cyan (00FFFF) +// - - $C4: Red (FF0000) +// - - $C5: Magenta (FF00FF) +// - - $C6: Yellow (FFFF00) +// - - $C7: White (FFFFFF) +// - - $C8: Pink (FF8080) +// - - $C9: Violet (8080FF) +// - - $CG: Orange pulse (FFE000 + darkenings thereof; v2 and later only) +// - - $Ca: Orange (F5A052; Episode 3 only) +// - Special character codes (Ep3 only) +// - - $B: Dash + small bullet +// - - $D: Large bullet +// - - $F: Female symbol +// - - $I: Infinity +// - - $M: Male symbol +// - - $O: Open circle +// - - $R: Solid circle +// - - $S: Star-like ability symbol +// - - $X: Cross +// - - $d: Down arrow +// - - $l: Left arrow +// - - $r: Right arrow +// - - $u: Up arrow + +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +// PATCH SERVER COMMANDS /////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +// The patch protocol is identical between PSO PC and PSO BB (the only versions +// on which it is used). + +// A patch server session generally goes like this: +// Server: 02 (unencrypted) +// (all the following commands encrypted with PSO V2 encryption, even on BB) +// Client: 02 +// Server: 04 +// Client: 04 +// If client's login information is wrong and server chooses to reject it: +// Server: 15 +// Server disconnects +// Otherwise: +// Server: 13 (if desired) +// Server: 0B +// Server: 09 (with directory name ".") +// For each directory to be checked: +// Server: 09 +// Server: (commands to check subdirectories - more 09/0A/0C) +// For each file in the directory: +// Server: 0C +// Server: 0A +// Server: 0D +// For each 0C sent by the server earlier: +// Client: 0F +// Client: 10 +// If there are any files to be updated: +// Server: 11 +// For each directory containing files to be updated: +// Server: 09 +// Server: (commands to update subdirectories) +// For each file to be updated in this directory: +// Server: 06 +// Server: 07 (possibly multiple 07s if the file is large) +// Server: 08 +// Server: 0A +// Server: 12 +// Server disconnects + +// 00: Invalid command +// 01: Invalid command + +// 02 (S->C): Start encryption +// Client will respond with an 02 command. +// All commands after this command will be encrypted with PSO V2 encryption. +// If this command is sent during an encrypted session, the client will not +// reject it; it will simply re-initialize its encryption state and respond with +// an 02 as normal. +// The copyright field in the below structure must contain the following text: +// "Patch Server. Copyright SonicTeam, LTD. 2001" + +struct S_ServerInit_Patch_02 { + pstring copyright; + le_uint32_t server_key = 0; // Key for commands sent by server + le_uint32_t client_key = 0; // Key for commands sent by client + // The client rejects the command if it's larger than this size, so we can't + // add the after_message like we do in the other server init commands. +} __packed_ws__(S_ServerInit_Patch_02, 0x48); + +// 02 (C->S): Encryption started +// No arguments + +// 03: Invalid command + +// 04 (S->C): Request login information +// No arguments +// Client will respond with an 04 command. + +// 04 (C->S): Log in (patch) +// The email field is always blank on BB. It may be blank on PC too, so this +// cannot be used to determine the game version used by a patch client. + +struct C_Login_Patch_04 { + parray unused; + pstring username; + pstring password; + pstring email; +} __packed_ws__(C_Login_Patch_04, 0x6C); + +// 05 (S->C): Disconnect +// No arguments +// This command is not used in the normal flow (described above). Generally the +// server should disconnect after sending a 12 or 15 command instead of an 05. + +// 06 (S->C): Open file for writing + +struct S_OpenFile_Patch_06 { + le_uint32_t unknown_a1 = 0; + le_uint32_t size = 0; + pstring filename; +} __packed_ws__(S_OpenFile_Patch_06, 0x38); + +// 07 (S->C): Write file +// The client's handler table says this command's maximum size is 0x6010 +// including the header, but the only servers I've seen use this command limit +// chunks to 0x4010 (including the header). Unlike the game server's 13 and A7 +// commands, the chunks do not need to be the same size - the game opens the +// file with the "a+b" mode each time it is written, so the new data is always +// appended to the end. + +struct S_WriteFileHeader_Patch_07 { + le_uint32_t chunk_index = 0; + le_uint32_t chunk_checksum = 0; // CRC32 of the following chunk data + le_uint32_t chunk_size = 0; + // The chunk data immediately follows here +} __packed_ws__(S_WriteFileHeader_Patch_07, 0x0C); + +// 08 (S->C): Close current file +// The unused field is optional. It's not clear whether this field was ever +// used; it could be a remnant from pre-release testing, or someone could have +// simply set the maximum size of this command incorrectly. + +struct S_CloseCurrentFile_Patch_08 { + le_uint32_t unused = 0; +} __packed_ws__(S_CloseCurrentFile_Patch_08, 4); + +// 09 (S->C): Enter directory + +struct S_EnterDirectory_Patch_09 { + pstring name; +} __packed_ws__(S_EnterDirectory_Patch_09, 0x40); + +// 0A (S->C): Exit directory +// No arguments + +// 0B (S->C): Start patch session and go to patch root directory +// No arguments + +// 0C (S->C): File checksum request + +struct S_FileChecksumRequest_Patch_0C { + le_uint32_t request_id = 0; + pstring filename; +} __packed_ws__(S_FileChecksumRequest_Patch_0C, 0x24); + +// 0D (S->C): End of file checksum requests +// No arguments + +// 0E: Invalid command + +// 0F (C->S): File information + +struct C_FileInformation_Patch_0F { + le_uint32_t request_id = 0; // Matches request_id from an earlier 0C command + le_uint32_t checksum = 0; // CRC32 of the file's data + le_uint32_t size = 0; +} __packed_ws__(C_FileInformation_Patch_0F, 0x0C); + +// 10 (C->S): End of file information command list +// No arguments + +// 11 (S->C): Start file downloads + +struct S_StartFileDownloads_Patch_11 { + le_uint32_t total_bytes = 0; + le_uint32_t num_files = 0; +} __packed_ws__(S_StartFileDownloads_Patch_11, 0x08); + +// 12 (S->C): End patch session successfully +// No arguments + +// 13 (S->C): Message box +// Same format and usage as commands 1A/D5 on the game server (described below). +// On PSOBB, the message box appears in the upper half of the screen and +// functions like a normal PSO message box - that is, you can use color escapes +// (\tCG, for example) and lines are terminated with \n. On PSOPC, the message +// appears in a Windows edit control, so the text functions differently: line +// breaks must be \r\n and standard PSO color escapes don't work. The maximum +// size of this command is 0x2004 bytes, including the header. + +// 14 (S->C): Reconnect +// Same format and usage as command 19 on the game server (described below), +// except the port field is big-endian for some reason. + +template +struct S_ReconnectT { + be_uint32_t address = 0; + PortT port = 0; + le_uint16_t unused = 0; +} __packed__; +using S_Reconnect_Patch_14 = S_ReconnectT; +check_struct_size(S_Reconnect_Patch_14, 0x08); + +// 15 (S->C): Login failure +// No arguments +// The client shows a message like "Incorrect game ID or password" and +// disconnects. + +// No commands beyond 15 are valid on the patch server. + +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +// GAME SERVER COMMANDS //////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +// 00: Invalid command + +// 01 (S->C): Lobby message box +// Internal name: RcvError +// A small message box appears in lower-right corner, and the player must press +// a key to continue. The maximum length of the message is 0x200 bytes. +// Internally, PSO calls this RcvError, since it's generally used to tell the +// player why they can't do something (e.g. join a full game). +// This format is shared by multiple commands; for all of them except 06 (S->C), +// the guild_card_number field is unused and should be 0. +// On BB, this command may be sent as 0001 or 0101; in the latter case, the +// message box appears in the lower-left corner instead. + +struct SC_TextHeader_01_06_11_B0_EE { + le_uint32_t unused = 0; + le_uint32_t guild_card_number = 0; + // Text immediately follows here +} __packed_ws__(SC_TextHeader_01_06_11_B0_EE, 8); + +// 02 (S->C): Start encryption (except on BB) +// Internal name: RcvPsoConnectV2 +// This command should be used for non-initial sessions (after the client has +// already selected a ship, for example). Command 17 should be used instead for +// the first connection. +// All commands after this command will be encrypted with PSO V2 encryption on +// DC, PC, and GC Episodes 1&2 Trial Edition, or PSO V3 encryption on other V3 +// versions. +// DCv1 clients will respond with an (encrypted) 93 command. +// DCv2 and PC clients will respond with an (encrypted) 9A or 9D command. +// V3 clients will respond with an (encrypted) 9A or 9E command, except for GC +// Episodes 1&2 Trial Edition, which behaves like PC. +// The copyright field in the below structure must contain the following text: +// "DreamCast Lobby Server. Copyright SEGA Enterprises. 1999" +// (The above text is required on all versions that use this command, including +// those versions that don't run on the DreamCast.) + +struct S_ServerInitDefault_DC_PC_V3_02_17_91_9B { + pstring copyright; + le_uint32_t server_key = 0; // Key for data sent by server + le_uint32_t client_key = 0; // Key for data sent by client +} __packed_ws__(S_ServerInitDefault_DC_PC_V3_02_17_91_9B, 0x48); + +template +struct S_ServerInitWithAfterMessageT_DC_PC_V3_02_17_91_9B { + S_ServerInitDefault_DC_PC_V3_02_17_91_9B basic_cmd; + // This field is not part of SEGA's implementation; the client ignores it. + // newserv sends a message here disavowing the preceding copyright notice. + pstring after_message; +} __packed__; + +// 03 (C->S): Legacy register (non-BB) +// Internal name: SndRegist + +struct C_LegacyLogin_PC_V3_03 { + /* 00 */ le_uint64_t unused = 0; // Same as unused field in 9D/9E + /* 08 */ le_uint32_t sub_version = 0; + /* 0C */ uint8_t is_extended = 0; + /* 0D */ uint8_t language = 0; + /* 0E */ le_uint16_t unknown_a2 = 0; + // Note: These are suffixed with 2 since they come from the same source data + // as the corresponding fields in 9D/9E. (Even though serial_number and + // serial_number2 have the same contents in 9E, they do not come from the same + // field on the client's connection context object.) + /* 10 */ pstring serial_number2; + /* 20 */ pstring access_key2; + /* 30 */ +} __packed_ws__(C_LegacyLogin_PC_V3_03, 0x30); + +// 03 (S->C): Legacy register result (non-BB) +// Internal name: RcvRegist +// header.flag specifies if the password was correct. If header.flag is 0, the +// password saved to the memory card (if any) is deleted and the client is +// disconnected. If header.flag is nonzero, the client responds with an 04 +// command. Curiously, it looks like even DCv1 doesn't use this command in its +// standard login sequence, so this may be a relic from very early development. +// Even more curiously, DCv2 (and no other PSO version) has a behavior that +// appears to be some kind of anti-cheating mechanism: if any byte in the memory +// range 8C004000-8C007FFF is not zero, the handler for this command loops +// infinitely doing nothing. +// No other arguments + +// 03 (S->C): Start encryption (BB) +// Client will respond with an (encrypted) 93 command. +// All commands after this command will be encrypted with PSO BB encryption. +// The copyright field in the below structure must contain the following text: +// "Phantasy Star Online Blue Burst Game Server. Copyright 1999-2004 SONICTEAM." + +struct S_ServerInitDefault_BB_03_9B { + pstring copyright; + parray server_key; + parray client_key; +} __packed_ws__(S_ServerInitDefault_BB_03_9B, 0xC0); + +template +struct S_ServerInitWithAfterMessageT_BB_03_9B { + S_ServerInitDefault_BB_03_9B basic_cmd; + // As in 02, this field is not part of SEGA's implementation. + pstring after_message; +} __packed__; + +// 04 (C->S): Legacy login +// Internal name: SndLogin2 +// Curiously, there is a SndLogin3 function, but it does not send anything. +// See comments on non-BB 03 (S->C). This is likely a relic of an older, +// now-unused sequence. Like 03, this command isn't used by any PSO version that +// newserv supports. +// header.flag is nonzero, but it's not clear what it's used for. + +struct C_LegacyLogin_PC_V3_04 { + /* 00 */ le_uint64_t unused1 = 0; // Same as unused field in 9D/9E + /* 08 */ le_uint32_t sub_version = 0; + /* 0C */ uint8_t is_extended = 0; + /* 0D */ uint8_t language = 0; + /* 0E */ le_uint16_t unknown_a2 = 0; + /* 10 */ pstring serial_number; + /* 20 */ pstring access_key; + /* 30 */ +} __packed_ws__(C_LegacyLogin_PC_V3_04, 0x30); + +struct C_LegacyLogin_BB_04 { + parray unknown_a1; + pstring username; + pstring password; +} __packed_ws__(C_LegacyLogin_BB_04, 0x2C); + +// 04 (S->C): Set guild card number and update client config ("security data") +// Internal name: RcvLogin +// header.flag specifies an error code; the format described below is only used +// if this code is 0 (no error). Otherwise, the command has no arguments. +// Error codes (on GC): +// 01 = Line is busy (103) +// 02 = Already logged in (104) +// 03 = Incorrect password (106) +// 04 = Account suspended (107) +// 05 = Server down for maintenance (108) +// 06 = Incorrect password (127) +// Any other nonzero value = Generic failure (101) +// The client config field in this command is ignored by pre-V3 clients as well +// as Episodes 1&2 Trial Edition. All other V3 clients save it as opaque data to +// be returned in a 9E or 9F command later. newserv sends the client config +// anyway to clients that ignore it. +// The client will respond with a 96 command, but only the first time it +// receives this command - for later 04 commands, the client will still update +// its client config but will not respond. Changing the security data at any +// time seems ok, but changing the guild card number of a client after it's +// initially set can confuse the client, and (on pre-V3 clients) possibly +// corrupt the character data. For this reason, newserv tries pretty hard to +// hide the remote guild card number when clients connect to the proxy server. +// BB clients have multiple client configs; this command sets the client config +// that is returned by the 9E and 9F commands, but does not affect the client +// config set by the E6 command (and returned in the 93 command). In most cases, +// E6 should be used for BB clients instead of 04. + +struct S_UpdateClientConfig_DC_PC_04 { + // Note: What we call player_tag here is actually three fields: two uint8_ts + // followed by a le_uint16_t. It's unknown what the uint8_t fields are for + // (they seem to always be zero), but the le_uint16_t is likely a boolean + // which denotes whether the player is present or not (for example, in lobby + // data structures). For historical and simplicity reasons, newserv combines + // these three fields into one, which takes on the value 0x00010000 when a + // player is present and zero when none is present. + le_uint32_t player_tag = 0x00010000; + le_uint32_t guild_card_number = 0; +} __packed_ws__(S_UpdateClientConfig_DC_PC_04, 8); + +struct S_UpdateClientConfig_V3_04 { + le_uint32_t player_tag = 0x00010000; + le_uint32_t guild_card_number = 0; + // This field is opaque to the client; it will send back the contents verbatim + // in its next 9E command (or on request via 9F). + parray client_config; +} __packed_ws__(S_UpdateClientConfig_V3_04, 0x28); + +struct S_UpdateClientConfig_BB_04 { + le_uint32_t player_tag = 0x00010000; + le_uint32_t guild_card_number = 0; + parray client_config; +} __packed_ws__(S_UpdateClientConfig_BB_04, 0x30); + +// 05: Disconnect +// Internal name: SndLogout +// No arguments +// Sending this command to a client will cause it to disconnect. There's no +// advantage to doing this over simply closing the TCP connection. Clients will +// send this command to the server when they are about to disconnect, but the +// server does not need to close the connection when it receives this command +// (and in some cases, the client will send multiple 05 commands before actually +// disconnecting). + +// 06: Chat +// Internal name: RcvChat and SndChat +// Server->client format is the same as the 01 command; guild_card_number +// is unused and set to zero. The maximum size of the message is 0x200 bytes. +// Client->server format is the same as the 01 command also. +// When sent by the client, the text field includes only the message. When sent +// by the server, the text field includes the origin player's name, followed by +// a tab character (\x09), followed by the message. +// During Episode 3 battles, the first byte of an inbound 06 command's message +// is interpreted differently. It should be treated as a bit field, with the low +// 4 bits intended as masks for who can see the message. If the low bit (1) is +// set, for example, then the chat message displays as " (whisper)" on player +// 0's screen regardless of the message contents. The next bit (2) hides the +// message from player 1, etc. The high 4 bits of this byte appear not to be +// used, but are often nonzero and set to the value 4. (This is probably done so +// that the field is always a valid ASCII character and also never terminates +// the chat string accidentally.) We call this byte private_flags in the places +// where newserv uses it. + +// 07 (S->C): Ship or block select menu +// Internal name: RcvDirList +// This command triggers a general form of blocking menu, which was used for +// both the ship select and block select menus by Sega (and all other private +// servers except newserv, it seems). Curiously, the string "RcvBlockList" +// appears in PSO v1 and v2, but it is not used, implying that at some point +// there was a separate command to send the block list, but it was scrapped. +// Perhaps this was used for command A1, which is identical to 07 and A0 in all +// versions of PSO (except DC NTE). +// The menu is titles "Ship Select" unless the first menu item begins with the +// text "BLOCK" (all caps), in which case it is titled "Block Select". + +// Command is a list of these; header.flag is the entry count. The first entry +// is not included in the count and does not appear on the client. The text of +// the first entry becomes the ship name when the client joins a lobby. +template +struct S_MenuEntryT { + le_uint32_t menu_id = 0; + le_uint32_t item_id = 0; + le_uint16_t flags = 0x0F04; // Should be this value, apparently + pstring text; +} __packed__; +using S_MenuEntry_PC_BB_07_1F = S_MenuEntryT; +using S_MenuEntry_DC_V3_07_1F = S_MenuEntryT; +check_struct_size(S_MenuEntry_PC_BB_07_1F, 0x2C); +check_struct_size(S_MenuEntry_DC_V3_07_1F, 0x1C); + +// 08 (C->S): Request game list +// Internal name: SndGameList +// No arguments + +// 08 (S->C): Game list +// Internal name: RcvGameList +// Client responds with 09 and 10 commands (or nothing if the player cancels). + +// Command is a list of these; header.flag is the entry count. The first entry +// is not included in the count and does not appear on the client. +template +struct S_GameMenuEntryT { + le_uint32_t menu_id = 0; + le_uint32_t game_id = 0; + // difficulty_tag is 0x0A on Episode 3; on all other versions, it's + // difficulty + 0x22 (so 0x25 means Ultimate, for example) + uint8_t difficulty_tag = 0; + uint8_t num_players = 0; + pstring name; + // The episode field is used differently by different versions: + // - On DCv1, PC, and GC Episode 3, the value is ignored. + // - On DCv2, 1 means v1 players can't join the game, and 0 means they can. + // - On GC Ep1&2, 0x40 means Episode 1, and 0x41 means Episode 2. + // - On BB, 0x40/0x41 mean Episodes 1/2 as on GC, and 0x43 means Episode 4. + uint8_t episode = 0; + // Flags: + // 01 = Send name? (client sends the name field in the 10 command if this + // item is chosen, but it's blank) + // 02 = Locked (lock icon appears in menu; player is prompted for password if + // they choose this game) + // 04 = In battle (Episode 3; a sword icon appears in menu) + // 04 = Disabled (BB; used for solo games) + // 10 = Is battle mode + // 20 = Is challenge mode + // 40 = Is v2 only (DCv2/PC); name renders in orange + // 40 = Is Episode 1 (V3/BB) + // 80 = Is Episode 2 (V3/BB) + // C0 = Is Episode 4 (BB) + uint8_t flags = 0; +} __packed__; +using S_GameMenuEntry_PC_BB_08 = S_GameMenuEntryT; +using S_GameMenuEntry_DC_V3_08_Ep3_E6 = S_GameMenuEntryT; +check_struct_size(S_GameMenuEntry_PC_BB_08, 0x2C); +check_struct_size(S_GameMenuEntry_DC_V3_08_Ep3_E6, 0x1C); + +// 09 (C->S): Menu item info request +// Internal name: SndInfo +// Server will respond with an 11 command, or an A3 or A5 if the specified menu +// is the quest menu. + +struct C_MenuItemInfoRequest_09 { + le_uint32_t menu_id = 0; + le_uint32_t item_id = 0; +} __packed_ws__(C_MenuItemInfoRequest_09, 8); + +// 0B: Invalid command + +// 0C (C->S): Create game (DCv1) +// Same format as C1, but fields not supported by v1 (e.g. episode, v2 mode) +// are unused. + +// 0D: Invalid command + +// 0E (S->C): Incomplete/legacy join game (non-BB) +// Internal name: RcvStartGame +// header.flag = number of valid entries in lobby_data + +// This command appears to be a vestige of very early development; its +// second-phase handler is missing even in the earliest public prototype of PSO +// (DC NTE), and the command format is missing some important information +// necessary to start a game on any version. + +// There is a failure mode in the 0E command handler that causes the thread +// receiving the command to loop infinitely doing nothing, effectively +// softlocking the game. This happens if the local player's Guild Card number +// doesn't match any of the lobby_data entries. (Notably, only the first +// (header.flag) entries are checked.) +// If the local player's Guild Card number does match one of the entries, the +// command does not softlock, but instead does nothing because the 0E +// second-phase handler is missing. + +template +struct SC_MeetUserExtensionT { + struct LobbyReference { + le_uint32_t menu_id = 0; + le_uint32_t item_id = 0; + } __packed_ws__(LobbyReference, 8); + + /* 00 */ parray lobby_refs; + /* 40 */ le_uint32_t unknown_a2 = 0; + /* 44 */ pstring player_name; + /* 64 (or 84 on UTF16 versions) */ +} __packed__; +using SC_MeetUserExtension_DC_V3 = SC_MeetUserExtensionT; +using SC_MeetUserExtension_PC_BB = SC_MeetUserExtensionT; +check_struct_size(SC_MeetUserExtension_DC_V3, 0x64); +check_struct_size(SC_MeetUserExtension_PC_BB, 0x84); + +struct S_LegacyJoinGame_PC_0E { + struct LobbyData { + le_uint32_t player_tag = 0; + le_uint32_t guild_card_number = 0; + pstring name; + } __packed_ws__(LobbyData, 0x18); + + parray lobby_data; + parray unknown_a3; +} __packed_ws__(S_LegacyJoinGame_PC_0E, 0x80); + +struct S_LegacyJoinGame_GC_0E { + parray lobby_data; + SC_MeetUserExtension_DC_V3 meet_user_extension; + parray unknown_a3; +} __packed_ws__(S_LegacyJoinGame_GC_0E, 0xE8); + +struct S_LegacyJoinGame_XB_0E { + struct LobbyData { + le_uint32_t player_tag = 0; + le_uint32_t guild_card_number = 0; + pstring name; + } __packed_ws__(LobbyData, 0x20); + parray lobby_data; + SC_MeetUserExtension_DC_V3 meet_user_extension; + parray unknown_a3; +} __packed_ws__(S_LegacyJoinGame_XB_0E, 0xE8); + +// 0F: Invalid command + +// 10 (C->S): Menu selection +// Internal name: SndAction +// header.flag contains two flags: 02 specifies if a password is present, and 01 +// specifies... something else. These two bits directly correspond to the two +// lowest bits in the flags field of the game menu: 02 specifies that the game +// is locked, but the function of 01 is unknown. +// Annoyingly, the no-arguments form of the command can have any flag value, so +// it doesn't suffice to check the flag value to know which format is being +// used! + +struct C_MenuSelection_10_Flag00 { + le_uint32_t menu_id = 0; + le_uint32_t item_id = 0; +} __packed_ws__(C_MenuSelection_10_Flag00, 8); + +template +struct C_MenuSelectionT_10_Flag01 { + C_MenuSelection_10_Flag00 basic_cmd; + pstring unknown_a1; +} __packed__; +using C_MenuSelection_DC_V3_10_Flag01 = C_MenuSelectionT_10_Flag01; +using C_MenuSelection_PC_BB_10_Flag01 = C_MenuSelectionT_10_Flag01; +check_struct_size(C_MenuSelection_DC_V3_10_Flag01, 0x18); +check_struct_size(C_MenuSelection_PC_BB_10_Flag01, 0x28); + +template +struct C_MenuSelectionT_10_Flag02 { + C_MenuSelection_10_Flag00 basic_cmd; + pstring password; +} __packed__; +using C_MenuSelection_DC_V3_10_Flag02 = C_MenuSelectionT_10_Flag02; +using C_MenuSelection_PC_BB_10_Flag02 = C_MenuSelectionT_10_Flag02; +check_struct_size(C_MenuSelection_DC_V3_10_Flag02, 0x18); +check_struct_size(C_MenuSelection_PC_BB_10_Flag02, 0x28); + +template +struct C_MenuSelectionT_10_Flag03 { + C_MenuSelection_10_Flag00 basic_cmd; + pstring unknown_a1; + pstring password; +} __packed__; +using C_MenuSelection_DC_V3_10_Flag03 = C_MenuSelectionT_10_Flag03; +using C_MenuSelection_PC_BB_10_Flag03 = C_MenuSelectionT_10_Flag03; +check_struct_size(C_MenuSelection_DC_V3_10_Flag03, 0x28); +check_struct_size(C_MenuSelection_PC_BB_10_Flag03, 0x48); + +// 11 (S->C): Ship info +// Internal name: RcvMessage +// Same format as 01 command. The text appears in a small box in the lower-left +// corner (on V3/BB) or lower-right corner of the screen. + +// 12 (S->C): Valid but ignored (all versions) +// Internal name: RcvBaner +// This command's internal name is possibly a misspelling of "banner", which +// could be an early version of the 1A/D5 (large message box) commands, or of +// BB's 00EE (scrolling message) command; however, the existence of +// RcvBanerHead (16) seems to contradict this hypothesis since a text message +// would not require a separate header command. Even on DC NTE, this command +// does nothing, so this must have been scrapped very early in development. + +// 13 (S->C): Write online quest file +// Internal name: RcvDownLoad +// Used for downloading online quests. For download quests (to be saved to the +// memory card), use A7 instead. +// All chunks except the last must have 0x400 data bytes. When downloading an +// online quest, the .bin and .dat chunks may be interleaved (although newserv +// currently sends them sequentially). There is a client bug in BB (and +// probably all other versions) where if the quest file's size is a multiple +// of 0x400, the last chunk will have size 0x400, and the client will never +// consider the download complete since it only checks if the last chunk has +// size < 0x400; it does not check if all expected bytes have been received. +// To work around this, newserv appends an extra zero byte if the quest file's +// size is a multiple of 0x400; this byte will be ignored since the PRS +// decompression algorithm contains a stop command, so it will never read it. + +// header.flag = file chunk index (start offset / 0x400) +struct S_WriteFile_13_A7 { + pstring filename; + parray data; + le_uint32_t data_size = 0; +} __packed_ws__(S_WriteFile_13_A7, 0x414); + +// 13 (C->S): Confirm file write (V3/BB) +// Client sends this in response to each 13 sent by the server. It appears +// these are only sent by V3 and BB - PSO DC and PC do not send these. + +// header.flag = file chunk index (same as in the 13/A7 sent by the server) +struct C_WriteFileConfirmation_V3_BB_13_A7 { + pstring filename; +} __packed_ws__(C_WriteFileConfirmation_V3_BB_13_A7, 0x10); + +// 14 (S->C): Valid but ignored (all versions) +// Internal name: RcvUpLoad +// Based on its internal name, this command seems like the logical opposite of +// 13 (quest file download, named RcvDownLoad internally). However, even in DC +// NTE, this command does nothing, so it must have been scrapped very early in +// development. There is a SndUpLoad string in the DC versions, but the +// corresponding function was deleted. + +// 15: Invalid command + +// 16 (S->C): Valid but ignored (all versions) +// Internal name: RcvBanerHead +// It's not clear what this command was supposed to do, but it's likely related +// to 12 in some way. Like 12, this command does nothing, even on DC NTE. + +// 17 (S->C): Start encryption at login server (except on BB) +// Internal name: RcvPsoRegistConnectV2 +// Same format and usage as 02 command, but a different copyright string: +// "DreamCast Port Map. Copyright SEGA Enterprises. 1999" +// Unlike the 02 command, V3 clients will respond with a DB command when they +// receive a 17 command in any online session, with the exception of Episodes +// 1&2 trial edition (which responds with a 9A). DCv1 will respond with a 90. DC +// NTE will respond with an 8B. Other non-V3 clients will respond with a 9A or +// 9D. + +// 18 (S->C): Account verification result (PC/V3) +// Behaves exactly the same as 9A (S->C). No arguments except header.flag. + +// 19 (S->C): Reconnect to different address +// Internal name: RcvPort +// Client will disconnect, and reconnect to the given address/port. Encryption +// will be disabled on the new connection; the server should send an appropriate +// command to enable it when the client connects. +// Note: PSO XB seems to ignore the address field, which makes sense given its +// networking architecture. + +using S_Reconnect_19 = S_ReconnectT; +check_struct_size(S_Reconnect_19, 8); + +// Because PSO PC and some versions of PSO DC/GC use the same port but different +// protocols, we use a specially-crafted 19 command to send them to two +// different ports depending on the client version. I first saw this technique +// used by Schthack; I don't know if it was his original creation. + +struct S_ReconnectSplit_19 { + be_uint32_t pc_address = 0; + le_uint16_t pc_port = 0; + parray unused1; + uint8_t gc_command = 0x19; + uint8_t gc_flag = 0; + le_uint16_t gc_size = 0x97; + be_uint32_t gc_address = 0; + le_uint16_t gc_port = 0; + parray unused2; +} __packed_ws__(S_ReconnectSplit_19, 0xAC); + +// 1A (S->C): Large message box +// Internal name: RcvText +// On V3, client will sometimes respond with a D6 command (see D6 for more +// information). +// Contents are plain text. There must be at least one null character ('\0') +// before the end of the command data. There is a bug in V3 (and possibly all +// versions) where if this command is sent after the client has joined a lobby, +// the chat log window contents will appear in the message box, prepended to +// the message text from the command. +// The maximum length of the message is 0x400 bytes. This is the only +// difference between this command and the D5 command. + +// 1B (S->C): Valid but ignored (all versions) +// Internal name: RcvBattleData +// This command does nothing in all PSO versions. There is a SndBattleData +// string in the DC versions, but the corresponding function was deleted. + +// 1C (S->C): Valid but ignored (all versions) +// Internal name: RcvSystemFile +// This command does nothing in all PSO versions. + +// 1D: Ping +// Internal name: RcvPing +// No arguments +// When sent to the client, the client will respond with a 1D command. Data sent +// by the server is ignored; the client always sends a 1D command with no data. + +// 1E: Invalid command + +// 1F (C->S): Request information menu +// Internal name: SndTextList +// No arguments +// This command is used in PSO DC and PC. It exists in V3 as well but is +// apparently unused. + +// 1F (S->C): Information menu +// Internal name: RcvTextList +// Same format and usage as 07 command, except: +// - The menu title will say "Information" instead of "Ship Select". +// - There is no way to request details before selecting a menu item (the client +// will not send 09 commands). +// - The player can press a button (B on GC, for example) to close the menu +// without selecting anything, unlike the ship select menu. The client does +// not send anything when this happens. + +// 20: Invalid command + +// 21: GameGuard control (old versions of BB) +// Format unknown + +// 0022: GameGuard check (BB) + +// Command 0022 is a 16-byte challenge (sent in the data field) using the +// following structure. + +struct SC_GameGuardCheck_BB_0022 { + parray data; +} __packed_ws__(SC_GameGuardCheck_BB_0022, 0x10); + +// 0122 (C->S): Time deviation (BB) +// This command is sent when the client executes a quest opcode 5D (gettime) and +// the returned timestamp is before the previous timestamp returned, but not by +// too much - it seems the game only considers deltas between 3 seconds and 30 +// minutes suspicious for these purposes. + +// 23 (S->C): Momoka Item Exchange result (BB) +// Sent in response to a 6xD9 command from the client. +// header.flag indicates if an item was exchanged: 0 means success, 1 means +// failure. + +// 24 (S->C): Secret Lottery Ticket exchange result (BB) +// Sent in response to a 6xDE command from the client. +// header.flag indicates whether the client had any Secret Lottery Tickets in +// their inventory (and hence could participate): 0 means success, 1 means +// failure. + +struct S_ExchangeSecretLotteryTicketResult_BB_24 { + // These fields map to unknown_a1 and unknown_a2 in the 6xDE command (but + // their order is swapped here). + le_uint16_t function_id = 0; + uint8_t start_index = 0; + uint8_t unused = 0; + parray unknown_a3; +} __packed_ws__(S_ExchangeSecretLotteryTicketResult_BB_24, 0x24); + +// 25 (S->C): Gallon's Plan result (BB) +// Sent in response to a 6xE1 command from the client. + +struct S_GallonPlanResult_BB_25 { + le_uint16_t function_id = 0; + uint8_t offset1 = 0; + uint8_t offset2 = 0; + uint8_t value1 = 0; + uint8_t value2 = 0; + le_uint16_t unused = 0; +} __packed_ws__(S_GallonPlanResult_BB_25, 8); + +// 26: Invalid command +// 27: Invalid command +// 28: Invalid command +// 29: Invalid command +// 2A: Invalid command +// 2B: Invalid command +// 2C: Invalid command +// 2D: Invalid command +// 2E: Invalid command +// 2F: Invalid command +// 30: Invalid command +// 31: Invalid command +// 32: Invalid command +// 33: Invalid command +// 34: Invalid command +// 35: Invalid command +// 36: Invalid command +// 37: Invalid command +// 38: Invalid command +// 39: Invalid command +// 3A: Invalid command +// 3B: Invalid command +// 3C: Invalid command +// 3D: Invalid command +// 3E: Invalid command +// 3F: Invalid command + +// 40 (C->S): Guild card search +// Internal name: SndFindUser +// There is an unused command named SndFavorite in the DC versions of PSO, +// which may have been related to this command. SndFavorite seems to be +// completely unused; its sender function was optimized out of all known +// builds, leaving only its name string remaining. +// The server should respond with a 41 command if the target is online. If the +// target is not online, the server doesn't respond at all. + +struct C_GuildCardSearch_40 { + le_uint32_t player_tag = 0x00010000; + le_uint32_t searcher_guild_card_number = 0; + le_uint32_t target_guild_card_number = 0; +} __packed_ws__(C_GuildCardSearch_40, 0x0C); + +// 41 (S->C): Guild card search result +// Internal name: RcvUserAns + +template +struct S_GuildCardSearchResultT { + le_uint32_t player_tag = 0x00010000; + le_uint32_t searcher_guild_card_number = 0; + le_uint32_t result_guild_card_number = 0; + HeaderT reconnect_command_header; // Ignored by the client + S_Reconnect_19 reconnect_command; + // The format of this string is "GAME-NAME,BLOCK##,SERVER-NAME". If the result + // player is not in a game, GAME-NAME should be the lobby name - for standard + // lobbies this is "BLOCK-"; for CARD lobbies this is + // "BLOCK-C". + pstring location_string; + // If the player chooses to meet the user, this extension data is sent in the + // login command (9D/9E) after connecting to the server designated in + // reconnect_command. When processing the 9D/9E, newserv uses only the + // lobby_id field within, but it fills in all fields when sending a 41. + SC_MeetUserExtensionT extension; +} __packed__; +using S_GuildCardSearchResult_PC_41 = S_GuildCardSearchResultT; +using S_GuildCardSearchResult_DC_V3_41 = S_GuildCardSearchResultT; +using S_GuildCardSearchResult_BB_41 = S_GuildCardSearchResultT; +check_struct_size(S_GuildCardSearchResult_PC_41, 0x124); +check_struct_size(S_GuildCardSearchResult_DC_V3_41, 0xC0); +check_struct_size(S_GuildCardSearchResult_BB_41, 0x128); + +// 42: Invalid command +// 43: Invalid command + +// 44 (S->C): Open file for download +// Internal name: RcvDownLoadHead +// Used for downloading online quests. The client will react to a 44 command if +// the filename ends in .bin or .dat. +// For download quests (to be saved to the memory card) and GBA games, the A6 +// command is used instead. The client will react to A6 if the filename ends in +// .bin/.dat (quests), .pvr (textures), or .gba (GameBoy Advance games). +// It appears that the .gba handler for A6 was not deleted in PSO XB, even +// though it doesn't make sense for an XB client to receive such a file. + +struct S_OpenFile_DC_44_A6 { + pstring name; // Should begin with "PSO/" + // The type field is only used for download quests (A6); it is ignored for + // online quests (44). The following values are valid for A6: + // 0 = download quest (client expects .bin and .dat files) + // 1 = download quest (client expects .bin, .dat, and .pvr files) + // 2 = GBA game (GC only; client expects .gba file only) + // 3 = Episode 3 download quest (Ep3 only; client expects .bin file only) + // There is a bug in the type logic: an A6 command always overwrites the + // current download type even if the filename doesn't end in .bin, .dat, .pvr, + // or .gba. This may lead to a resource exhaustion bug if exploited carefully, + // but I haven't verified this. Generally the server should send all files for + // a given piece of content with the same type in each file's A6 command. + uint8_t type = 0; + pstring filename; + le_uint32_t file_size = 0; +} __packed_ws__(S_OpenFile_DC_44_A6, 0x38); + +struct S_OpenFile_PC_GC_44_A6 { + pstring name; // Should begin with "PSO/" + le_uint16_t type = 0; + pstring filename; + le_uint32_t file_size = 0; +} __packed_ws__(S_OpenFile_PC_GC_44_A6, 0x38); + +// Curiously, PSO XB expects an extra 0x18 bytes at the end of this command, but +// those extra bytes are unused, and the client does not fail if they're +// omitted. +struct S_OpenFile_XB_44_A6 : S_OpenFile_PC_GC_44_A6 { + pstring xb_filename; + le_uint32_t content_meta; + parray unused2; +} __packed_ws__(S_OpenFile_XB_44_A6, 0x50); + +struct S_OpenFile_BB_44_A6 { + parray unused; + le_uint16_t type = 0; + pstring filename; + le_uint32_t file_size = 0; + pstring name; +} __packed_ws__(S_OpenFile_BB_44_A6, 0x50); + +// 44 (C->S): Confirm open file (V3/BB) +// Client sends this in response to each 44 sent by the server. + +// header.flag = quest number (sort of - seems like the client just echoes +// whatever the server sent in its header.flag field. Also quest numbers can be +// > 0xFF so the flag is essentially meaningless) +struct C_OpenFileConfirmation_44_A6 { + pstring filename; +} __packed_ws__(C_OpenFileConfirmation_44_A6, 0x10); + +// 45: Invalid command +// 46: Invalid command +// 47: Invalid command +// 48: Invalid command +// 49: Invalid command +// 4A: Invalid command +// 4B: Invalid command +// 4C: Invalid command +// 4D: Invalid command +// 4E: Invalid command +// 4F: Invalid command +// 50: Invalid command +// 51: Invalid command +// 52: Invalid command +// 53: Invalid command +// 54: Invalid command +// 55: Invalid command +// 56: Invalid command +// 57: Invalid command +// 58: Invalid command +// 59: Invalid command +// 5A: Invalid command +// 5B: Invalid command +// 5C: Invalid command +// 5D: Invalid command +// 5E: Invalid command +// 5F: Invalid command + +// 60: Broadcast command +// Internal name: SndPsoData +// When a client sends this command, the server should forward it to all players +// in the same game/lobby, except the player who originally sent the command. +// See ReceiveSubcommands or the subcommand index below for details on contents. +// The data in this command may be up to 0x400 bytes in length. If it's larger, +// the client will exhibit undefined behavior. + +// 61 (C->S): Player data +// Internal name: SndCharaDataV2 (SndCharaData in DCv1) +// header.flag specifies the format version, which is related to (but not +// identical to) the game's major version. For example, the format version is 01 +// on DC v1, 02 on PSO PC, 03 on PSO GC, XB, and BB, and 04 on Episode 3. +// Upon joining a game, the client assigns inventory item IDs sequentially as +// (0x00010000 + (0x00200000 * lobby_client_id) + x). So, for example, player +// 3's 8th item's ID would become 0x00610007. The item IDs from the last game +// the player was in will appear in their inventory in this command. +// Note: If the client is in a game at the time this command is received, the +// inventory sent by the client only includes items that would not disappear if +// the client crashes! Essentially, it reflects the saved state of the player's +// character rather than the live state. + +struct PlayerRecordsEntry_DC { + /* 00 */ le_uint32_t client_id = 0; + /* 04 */ PlayerRecordsChallengeDC challenge; + /* A4 */ PlayerRecordsBattle battle; + /* BC */ +} __packed_ws__(PlayerRecordsEntry_DC, 0xBC); + +struct PlayerRecordsEntry_PC { + /* 00 */ le_uint32_t client_id = 0; + /* 04 */ PlayerRecordsChallengePC challenge; + /* DC */ PlayerRecordsBattle battle; + /* F4 */ +} __packed_ws__(PlayerRecordsEntry_PC, 0xF4); + +struct PlayerRecordsEntry_V3 { + /* 0000 */ le_uint32_t client_id = 0; + /* 0004 */ PlayerRecordsChallengeV3 challenge; + /* 0104 */ PlayerRecordsBattle battle; + /* 011C */ +} __packed_ws__(PlayerRecordsEntry_V3, 0x011C); + +struct PlayerRecordsEntry_BB { + /* 0000 */ le_uint32_t client_id = 0; + /* 0004 */ PlayerRecordsChallengeBB challenge; + /* 0144 */ PlayerRecordsBattle battle; + /* 015C */ +} __packed_ws__(PlayerRecordsEntry_BB, 0x015C); + +struct C_CharacterData_DCv1_61_98 { + /* 0000 */ PlayerInventory inventory; + /* 034C */ PlayerDispDataDCPCV3 disp; + /* 041C */ +} __packed_ws__(C_CharacterData_DCv1_61_98, 0x041C); + +struct C_CharacterData_DCv2_61_98 { + /* 0000 */ PlayerInventory inventory; + /* 034C */ PlayerDispDataDCPCV3 disp; + /* 041C */ PlayerRecordsEntry_DC records; + /* 04D8 */ ChoiceSearchConfig choice_search_config; + /* 04F0 */ +} __packed_ws__(C_CharacterData_DCv2_61_98, 0x04F0); + +struct C_CharacterData_PC_61_98 { + /* 0000 */ PlayerInventory inventory; + /* 034C */ PlayerDispDataDCPCV3 disp; + /* 041C */ PlayerRecordsEntry_PC records; + /* 0510 */ ChoiceSearchConfig choice_search_config; + /* 0528 */ parray blocked_senders; + /* 05A0 */ le_uint32_t auto_reply_enabled = 0; + // The auto-reply message can be up to 0x200 characters. If it's shorter than + // that, the client truncates the command after the first null value (rounded + // up to the next 4-byte boundary). + /* 05A4 */ // uint16_t auto_reply[...EOF]; +} __packed_ws__(C_CharacterData_PC_61_98, 0x5A4); + +struct C_CharacterData_GCNTE_61_98 { + /* 0000 */ PlayerInventory inventory; + /* 034C */ PlayerDispDataDCPCV3 disp; + /* 041C */ PlayerRecordsEntry_DC records; + /* 04D8 */ ChoiceSearchConfig choice_search_config; + /* 04F0 */ parray blocked_senders; + /* 0568 */ le_uint32_t auto_reply_enabled = 0; + // The auto-reply message can be up to 0x200 bytes. If it's shorter than that, + // the client truncates the command after the first zero byte (rounded up to + // the next 4-byte boundary). + /* 056C */ // char auto_reply[...EOF]; +} __packed_ws__(C_CharacterData_GCNTE_61_98, 0x56C); + +struct C_CharacterData_V3_61_98 { + /* 0000 */ PlayerInventory inventory; + /* 034C */ PlayerDispDataDCPCV3 disp; + /* 041C */ PlayerRecordsEntry_V3 records; + /* 0538 */ ChoiceSearchConfig choice_search_config; + /* 0550 */ pstring info_board; + /* 05FC */ parray blocked_senders; + /* 0674 */ le_uint32_t auto_reply_enabled = 0; + // The auto-reply message can be up to 0x200 bytes. If it's shorter than that, + // the client truncates the command after the first zero byte (rounded up to + // the next 4-byte boundary). + /* 0678 */ // char auto_reply[...EOF]; +} __packed_ws__(C_CharacterData_V3_61_98, 0x678); + +struct C_CharacterData_Ep3_61_98 { + /* 0000 */ PlayerInventory inventory; + /* 034C */ PlayerDispDataDCPCV3 disp; + /* 041C */ PlayerRecordsEntry_V3 records; + /* 0538 */ ChoiceSearchConfig choice_search_config; + /* 0550 */ pstring info_board; + /* 05FC */ parray blocked_senders; + /* 0674 */ le_uint32_t auto_reply_enabled = 0; + /* 0678 */ pstring auto_reply; + /* 0724 */ Episode3::PlayerConfig ep3_config; + /* 2A74 */ +} __packed_ws__(C_CharacterData_Ep3_61_98, 0x2A74); + +struct C_CharacterData_BB_61_98 { + /* 0000 */ PlayerInventory inventory; + /* 034C */ PlayerDispDataBB disp; + /* 04DC */ PlayerRecordsEntry_BB records; + /* 0638 */ ChoiceSearchConfig choice_search_config; + /* 0650 */ pstring info_board; + /* 07A8 */ parray blocked_senders; + /* 0820 */ le_uint32_t auto_reply_enabled = 0; + // Like on V3, the client truncates the command if the auto reply message is + // shorter than 0x200 bytes. + /* 0824 */ // uint16_t auto_reply[...EOF]; +} __packed_ws__(C_CharacterData_BB_61_98, 0x824); + +// 62: Target command +// Internal name: SndPsoData2 +// When a client sends this command, the server should forward it to the player +// identified by header.flag in the same game/lobby, even if that player is the +// player who originally sent it. +// See ReceiveSubcommands or the subcommand index below for details on contents. +// The data in this command may be up to 0x400 bytes in length. If it's larger, +// the client will exhibit undefined behavior. + +// 63: Invalid command + +// 64 (S->C): Join game +// Internal name: RcvStartGame3 + +// This is sent to the joining player; the other players get a 65 instead. +// Note that (except on Episode 3) this command does not include the player's +// disp or inventory data. The clients in the game are responsible for sending +// that data to each other during the join process with 60/62/6C/6D commands. + +// Curiously, this command is named RcvStartGame3 internally, while 0E is named +// RcvStartGame. The string RcvStartGame2 appears in the DC versions, but it +// seems the relevant code was deleted - there are no references to the string. +// Based on the large gap between commands 0E and 64, we can't guess at which +// command number RcvStartGame2 might have been. + +// Header flag = entry count +template +struct S_JoinGameT_DC_PC { + // Note: It seems Sega servers sent uninitialized memory in the variations + // field when sending this command to start an Episode 3 tournament game. This + // can be misleading when reading old logs from those days, but the Episode 3 + // client really does ignore it. + parray variations; + // Unlike lobby join commands, these are filled in in their slot positions. + // That is, if there's only one player in a game with ID 2, then the first two + // of these are blank and the player's data is in the third entry here. + parray lobby_data; + uint8_t client_id = 0; + uint8_t leader_id = 0; + uint8_t disable_udp = 1; + uint8_t difficulty = 0; + uint8_t battle_mode = 0; + uint8_t event = 0; + uint8_t section_id = 0; + uint8_t challenge_mode = 0; + le_uint32_t rare_seed = 0; +} __packed__; + +struct S_JoinGame_DCNTE_64 { + uint8_t client_id = 0; + uint8_t leader_id = 0; + uint8_t disable_udp = 1; + uint8_t unused = 0; + parray variations; + parray lobby_data; +} __packed_ws__(S_JoinGame_DCNTE_64, 0x104); +using S_JoinGame_DC_64 = S_JoinGameT_DC_PC; +using S_JoinGame_PC_64 = S_JoinGameT_DC_PC; +check_struct_size(S_JoinGame_DC_64, 0x10C); +check_struct_size(S_JoinGame_PC_64, 0x14C); + +struct S_JoinGame_GC_64 : S_JoinGameT_DC_PC { + uint8_t episode = 0; + parray unused; +} __packed_ws__(S_JoinGame_GC_64, 0x110); + +struct S_JoinGame_Ep3_64 : S_JoinGame_GC_64 { + // This field is only present if the game (and client) is Episode 3. Similarly + // to lobby_data in the base struct, all four of these are always present and + // they are filled in in slot positions. + struct Ep3PlayerEntry { + PlayerInventory inventory; + PlayerDispDataDCPCV3 disp; + } __packed_ws__(Ep3PlayerEntry, 0x41C); + parray players_ep3; +} __packed_ws__(S_JoinGame_Ep3_64, 0x1180); + +struct S_JoinGame_XB_64 : S_JoinGameT_DC_PC { + uint8_t episode = 0; + parray unused; + parray unknown_a1; +} __packed_ws__(S_JoinGame_XB_64, 0x1D8); + +struct S_JoinGame_BB_64 : S_JoinGameT_DC_PC { + uint8_t episode = 0; + uint8_t unused1 = 1; + uint8_t solo_mode = 0; + uint8_t unused2 = 0; +} __packed_ws__(S_JoinGame_BB_64, 0x1A0); + +// 65 (S->C): Add player to game +// Internal name: RcvBurstGame +// When a player joins an existing game, the joining player receives a 64 +// command (described above), and the players already in the game receive a 65 +// command containing only the joining player's data. + +struct LobbyFlags_DCNTE { + uint8_t client_id = 0; + uint8_t leader_id = 0; + uint8_t disable_udp = 1; + uint8_t unused = 0; +} __packed_ws__(LobbyFlags_DCNTE, 4); + +struct LobbyFlags { + uint8_t client_id = 0; + uint8_t leader_id = 0; + uint8_t disable_udp = 1; + uint8_t lobby_number = 0; + uint8_t block_number = 0; + uint8_t unknown_a1 = 0; + uint8_t event = 0; + uint8_t unknown_a2 = 0; + le_uint32_t unused = 0; +} __packed_ws__(LobbyFlags, 0x0C); + +// Header flag = entry count (always 1 for 65 and 68; up to 0x0C for 67) +template +struct S_JoinLobbyT { + LobbyFlagsT lobby_flags; + struct Entry { + LobbyDataT lobby_data; + PlayerInventory inventory; + DispDataT disp; + } __packed__; + // Note: not all of these will be filled in and sent if the lobby isn't full + // (the command size will be shorter than this struct's size) + parray entries; + + static inline size_t size(size_t used_entries) { + return offsetof(S_JoinLobbyT, entries) + used_entries * sizeof(Entry); + } +} __packed__; +using S_JoinLobby_DCNTE_65_67_68 = S_JoinLobbyT; +using S_JoinLobby_PC_65_67_68 = S_JoinLobbyT; +using S_JoinLobby_DC_GC_65_67_68_Ep3_EB = S_JoinLobbyT; +using S_JoinLobby_BB_65_67_68 = S_JoinLobbyT; +check_struct_size(S_JoinLobby_DCNTE_65_67_68, 0x32D4); +check_struct_size(S_JoinLobby_PC_65_67_68, 0x339C); +check_struct_size(S_JoinLobby_DC_GC_65_67_68_Ep3_EB, 0x32DC); +check_struct_size(S_JoinLobby_BB_65_67_68, 0x3D8C); + +struct S_JoinLobby_XB_65_67_68 { + LobbyFlags lobby_flags; + parray unknown_a4; + struct Entry { + PlayerLobbyDataXB lobby_data; + PlayerInventory inventory; + PlayerDispDataDCPCV3 disp; + } __packed_ws__(Entry, 0x468); + // Note: not all of these will be filled in and sent if the lobby isn't full + // (the command size will be shorter than this struct's size) + parray entries; + + static inline size_t size(size_t used_entries) { + return offsetof(S_JoinLobby_XB_65_67_68, entries) + used_entries * sizeof(Entry); + } +} __packed_ws__(S_JoinLobby_XB_65_67_68, 0x3504); + +// 66 (S->C): Remove player from game +// Internal name: RcvExitGame +// This is sent to all players in a game except the leaving player. +// header.flag should be set to the leaving player ID (same as client_id). + +struct S_LeaveLobby_66_69_Ep3_E9 { + uint8_t client_id = 0; + uint8_t leader_id = 0; + // Note: disable_udp only has an effect for games; it is unused for lobbies + // and spectator teams. + uint8_t disable_udp = 1; + uint8_t unused = 0; +} __packed_ws__(S_LeaveLobby_66_69_Ep3_E9, 4); + +// 67 (S->C): Join lobby +// Internal name: RcvStartLobby2 +// This is sent to the joining player; the other players receive a 68 instead. +// Same format as 65 command, but used for lobbies instead of games. + +// Curiously, this command is named RcvStartLobby2 internally, but there is no +// command named RcvStartLobby. The string "RcvStartLobby" does appear in the DC +// game executable, but it appears the relevant code was deleted. + +// 68 (S->C): Add player to lobby +// Internal name: RcvBurstLobby +// Same format as 65 command, but used for lobbies instead of games. +// The command only includes the joining player's data. + +// 69 (S->C): Remove player from lobby +// Internal name: RcvExitLobby +// Same format as 66 command, but used for lobbies instead of games. + +// 6A: Invalid command +// 6B: Invalid command + +// 6C: Broadcast command +// Internal name: RcvPsoDataLong and SndPsoDataLong +// Same format and usage as 60 command, but with no size limit. + +// 6D: Target command +// Internal name: RcvPsoDataLong and SndPsoDataLong2 +// Same format and usage as 62 command, but with no size limit. + +// 6E: Invalid command + +// 6F (C->S): Done loading +// Internal name: SndBurstEnd +// This command is sent when a player is done loading and other players can then +// join the game. On BB, this command is sent a 006F after loading into a game, +// or as 016F after loading a joinable quest. (This means when a BB client joins +// a game with a quest in progress, they will send 006F when they're ready to +// receive the quest files, and 016F when they're actually ready to play.) + +// 70: Invalid command +// 71: Invalid command +// 72: Invalid command +// 73: Invalid command +// 74: Invalid command +// 75: Invalid command +// 76: Invalid command +// 77: Invalid command +// 78: Invalid command +// 79: Invalid command +// 7A: Invalid command +// 7B: Invalid command +// 7C: Invalid command +// 7D: Invalid command +// 7E: Invalid command +// 7F: Invalid command + +// 80: Valid but ignored (all versions except BB) +// Internal names: RcvGenerateID and SndGenerateID +// This command appears to be used to set the next item ID for the given player +// slot. PSO V3 and later accept this command, but ignore it entirely. Notably, +// no version of PSO except for DC NTE ever sends this command - it's likely it +// was used to implement some item ID sync semantics that were later changed to +// use the leader as the source of truth. + +struct C_GenerateID_DCNTE_80 { + le_uint32_t id = 0; + uint8_t unused1 = 0; // Always 0 + uint8_t unused2 = 0; // Always 0 + le_uint16_t unused3 = 0; // Always 0 + parray unused4; // Client sends uninitialized data here +} __packed_ws__(C_GenerateID_DCNTE_80, 0x0C); + +struct S_GenerateID_DC_PC_V3_80 { + le_uint32_t client_id = 0; + le_uint32_t unused = 0; + le_uint32_t next_item_id = 0; +} __packed_ws__(S_GenerateID_DC_PC_V3_80, 0x0C); + +// 81: Simple mail +// Internal name: RcvChatMessage and SndChatMessage +// Format is the same in both directions. The server should forward the command +// to the player with to_guild_card_number, if they are online. If they are not +// online, the server may store it for later delivery, send their auto-reply +// message back to the original sender, or simply drop the message. +// On GC (and probably other versions too) the unused space after the text +// contains uninitialized memory when the client sends this command. newserv +// clears the uninitialized data for security reasons before forwarding. + +struct SC_SimpleMail_PC_81 { + // If player_tag and from_guild_card_number are zero, the message cannot be + // replied to. + le_uint32_t player_tag = 0x00010000; + le_uint32_t from_guild_card_number = 0; + pstring from_name; + le_uint32_t to_guild_card_number = 0; + pstring text; +} __packed_ws__(SC_SimpleMail_PC_81, 0x42C); + +struct SC_SimpleMail_DC_V3_81 { + le_uint32_t player_tag = 0x00010000; + le_uint32_t from_guild_card_number = 0; + pstring from_name; + le_uint32_t to_guild_card_number = 0; + pstring text; +} __packed_ws__(SC_SimpleMail_DC_V3_81, 0x21C); + +struct SC_SimpleMail_BB_81 { + le_uint32_t player_tag = 0x00010000; + le_uint32_t from_guild_card_number = 0; + pstring from_name; + le_uint32_t to_guild_card_number = 0; + pstring received_date; + pstring text; +} __packed_ws__(SC_SimpleMail_BB_81, 0x454); + +// 82: Invalid command + +// 83 (S->C): Lobby menu +// Internal name: RcvRoomInfo +// Curiously, there is a SndRoomInfo string in the DC versions. Perhaps in an +// early (pre-NTE) build, the client had to request the lobby menu from the +// server, and SndRoomInfo was the command to do so. The code to send this +// command must have been removed before DC NTE. +// This command sets the menu item IDs that the client uses for the lobby +// teleporter menu. On DCv1, the client expects 10 entries here; on all other +// versions except Episode 3, the client expects 15 items here; on Episode 3, +// the client expects 20 items here. Sending more or fewer items does not +// change the lobby count on the client. If fewer entries are sent, the menu +// item IDs for some lobbies will not be set, and the client will likely send +// 84 commands that don't make sense if the player chooses one of lobbies with +// unset IDs. On Episode 3, the CARD lobbies are the last five entries, even +// though they appear at the top of the list on the player's screen. + +// Command is a list of these; header.flag is the entry count (10, 15 or 20) +struct S_LobbyListEntry_83 { + le_uint32_t menu_id = 0; + le_uint32_t item_id = 0; + le_uint32_t unused = 0; +} __packed_ws__(S_LobbyListEntry_83, 0x0C); + +// 84 (C->S): Choose lobby +// Internal name: SndRoomChange + +struct C_LobbySelection_84 { + le_uint32_t menu_id = 0; + le_uint32_t item_id = 0; +} __packed_ws__(C_LobbySelection_84, 8); + +// 85: Invalid command +// 86: Invalid command +// 87: Invalid command + +// 88 (C->S): Account check (DC NTE only) +// The server should respond with an 88 command. + +struct C_Login_DCNTE_88 { + pstring serial_number; + pstring access_key; +} __packed_ws__(C_Login_DCNTE_88, 0x22); + +// 88 (S->C): Account check result (DC NTE only) +// No arguments except header.flag. +// If header.flag is zero, client will respond with an 8A command. Otherwise, it +// will respond with an 8B command. This is the same behavior as for the 18 +// command (and in fact, the client handler is shared between both commands.) + +// 88 (S->C): Update lobby arrows (except DC NTE) +// If this command is sent while a client is joining a lobby, the client may +// ignore it. For this reason, the server should wait a few seconds after a +// client joins a lobby before sending an 88 command. +// This command is not supported on DC v1. + +// Command is a list of these; header.flag is the entry count. There should +// be an update for every player in the lobby in this command, even if their +// arrow color isn't being changed. +struct S_ArrowUpdateEntry_88 { + le_uint32_t player_tag = 0x00010000; + le_uint32_t guild_card_number = 0; + // Values for arrow_color: + // any number not specified below (including 00) - none + // 01 - red + // 02 - blue + // 03 - green + // 04 - yellow + // 05 - purple + // 06 - cyan + // 07 - orange + // 08 - pink + // 09 - white + // 0A - white + // 0B - white + // 0C - black + le_uint32_t arrow_color = 0; +} __packed_ws__(S_ArrowUpdateEntry_88, 0x0C); + +// 89 (C->S): Set lobby arrow +// header.flag = arrow color number (see above); no other arguments. +// Server should send an 88 command to all players in the lobby. + +// 89 (S->C): Start encryption at login server (DC NTE) +// Behaves exactly the same as the 17 command. + +// 8A (C->S): Connection information (DC NTE only) +// The server should respond with an 8A command. + +struct C_ConnectionInfo_DCNTE_8A { + pstring hardware_id; + le_uint32_t sub_version = 0x20; + le_uint32_t unknown_a1 = 0; + pstring username; + pstring password; + pstring email_address; // From Sylverant documentation +} __packed_ws__(C_ConnectionInfo_DCNTE_8A, 0xA0); + +// 8A (S->C): Connection information result (DC NTE only) +// header.flag is a success flag. If 0 is sent, the client shows an error +// message and disconnects. Otherwise, the client responds with an 8B command. + +// 8A (C->S): Request lobby/game name (except DC NTE) +// No arguments + +// 8A (S->C): Lobby/game name (except DC NTE) +// Contents is a string containing the lobby or game name. All versions after +// DCv1 (including the August 2001 DCv2 prototype) send an 8A command to request +// the team name after joining a game. The response is used to handle the +// team_name token in quest strings, and appears in some Challenge Mode +// information windows. +// Even though this was only ever used to retrieve the game name, Sega's +// original servers also replied to 8A if it was sent in a lobby. They would +// return a string like "LOBBY01" in that case. + +// 8B: Log in (DC NTE only) + +struct C_Login_DCNTE_8B { + le_uint32_t player_tag = 0x00010000; + le_uint32_t guild_card_number = 0; + parray hardware_id; + le_uint32_t sub_version = 0x20; + uint8_t is_extended = 0; + uint8_t language = 0; + parray unused1; + pstring serial_number; + pstring access_key; + pstring username; + pstring password; + pstring name; + parray unused; +} __packed_ws__(C_Login_DCNTE_8B, 0xAC); + +struct C_LoginExtended_DCNTE_8B : C_Login_DCNTE_8B { + SC_MeetUserExtension_DC_V3 extension; +} __packed_ws__(C_LoginExtended_DCNTE_8B, 0x110); + +// 8C: Invalid command + +// 8D (S->C): Request player data (DC NTE only) +// Behaves the same as 95 (S->C) on all other versions, but DC NTE crashes if it +// receives 95. + +// 8E: Ship select menu (DC NTE) +// Behaves exactly the same as the A0 command (in both directions). + +// 8F: Block select menu (DC NTE) +// Behaves exactly the same as the A1 command (in both directions). + +// 90 (C->S): V1 login (DC/PC/V3) +// This command is used during the DCv1 login sequence; a DCv1 client will +// respond to a 17 command with an (encrypted) 90. If a V3 client receives a 91 +// command, however, it will also send a 90 in response, though the contents +// will be blank (all zeroes). + +struct C_LoginV1_DC_PC_V3_90 { + pstring serial_number; + pstring access_key; + // Note: There is a bug in the Japanese and prototype versions of DCv1 that + // cause the client to send this command despite its size not being a + // multiple of 4. This is fixed in later versions, so we handle both cases in + // the receive handler. +} __packed_ws__(C_LoginV1_DC_PC_V3_90, 0x22); + +// 90 (S->C): Account verification result (V3) +// Behaves exactly the same as 9A (S->C). No arguments except header.flag. + +// 91 (S->C): Start encryption at login server (legacy; non-BB only) +// Internal name: RcvPsoRegistConnect +// Same format and usage as 17 command, except the client will respond with a 90 +// command. On versions that support it, this is strictly less useful than the +// 17 command. Curiously, this command appears to have been implemented after +// the 17 command since it's missing from the DC NTE version, but the 17 command +// is named RcvPsoRegistConnectV2 whereas 91 is simply RcvPsoRegistConnect. It's +// likely that after DC NTE, Sega simply changed the command numbers for this +// group of commands from 88-8F to 90-A1 (so DC NTE's 89 command became the 91 +// command in all later versions). + +// 92 (C->S): Register (DC) + +struct C_RegisterV1_DC_92 { + parray unknown_a1; + uint8_t is_extended = 0; // TODO: This is a guess + uint8_t language = 0; // TODO: This is a guess; verify it + parray unknown_a3; + pstring hardware_id; + pstring unknown_a4; + pstring email; // According to Sylverant documentation +} __packed_ws__(C_RegisterV1_DC_92, 0xA0); + +// 92 (S->C): Register result (non-BB) +// Internal name: RcvPsoRegist +// Same format and usage as 9C (S->C) command. + +// 93 (C->S): Log in (DCv1) + +struct C_LoginV1_DC_93 { + le_uint32_t player_tag = 0x00010000; + le_uint32_t guild_card_number = 0; + le_uint32_t unknown_a1 = 0; + le_uint32_t unknown_a2 = 0; + le_uint32_t sub_version = 0; + uint8_t is_extended = 0; + uint8_t language = 0; + parray unused1; + pstring serial_number; + pstring access_key; + pstring hardware_id; + pstring unknown_a3; + pstring name; + parray unused2; +} __packed_ws__(C_LoginV1_DC_93, 0xAC); + +struct C_LoginExtendedV1_DC_93 : C_LoginV1_DC_93 { + SC_MeetUserExtension_DC_V3 extension; +} __packed_ws__(C_LoginExtendedV1_DC_93, 0x110); + +// 93 (C->S): Log in (BB) + +struct C_LoginBase_BB_93 { + le_uint32_t player_tag = 0x00010000; + le_uint32_t guild_card_number = 0; + le_uint32_t sub_version = 0; + uint8_t language = 0; + int8_t character_slot = 0; + // Values for connection_phase: + // 00 - initial connection (client will request system file, characters, etc.) + // 01 - choose character + // 02 - create character + // 03 - apply updates from dressing room + // 04 - login server + // 05 - lobby server + // 06 - lobby server (with Meet User fields specified) + uint8_t connection_phase = 0; + uint8_t client_code = 0; + le_uint32_t security_token = 0; + pstring username; + pstring password; + + // These fields map to the same fields in SC_MeetUserExtensionT. There is no + // equivalent of the name field from that structure on BB (though newserv + // doesn't use it anyway). + le_uint32_t menu_id = 0; + le_uint32_t preferred_lobby_id = 0; +} __packed_ws__(C_LoginBase_BB_93, 0x7C); + +struct C_LoginWithoutHardwareInfo_BB_93 : C_LoginBase_BB_93 { + // Note: Unlike other versions, BB puts the version string in the client + // config at connect time. So the first time the server gets this command, it + // will be something like "Ver. 1.24.3". This format is used on older client + // versions (before 1.23.8?) + parray client_config; +} __packed_ws__(C_LoginWithoutHardwareInfo_BB_93, 0xA4); + +struct C_LoginWithHardwareInfo_BB_93 : C_LoginBase_BB_93 { + // See the comment in the above structure. This format is used on newer client + // versions. + parray hardware_info; + parray client_config; +} __packed_ws__(C_LoginWithHardwareInfo_BB_93, 0xAC); + +// 94: Invalid command + +// 95 (S->C): Request player data +// Internal name: RcvRecognition +// No arguments +// For some reason, some servers send high values in the header.flag field here. +// The header.flag field is completely unused by the client, however - sending +// zero works just fine. The original Sega servers had some uninitialized memory +// bugs, of which that may have been one, and other private servers may have +// just duplicated Sega's behavior verbatim. +// Client will respond with a 61 command. + +// 96 (C->S): Character save information +// Internal name: SndSaveCountCheck + +struct C_CharSaveInfo_DCv2_PC_V3_BB_96 { + // The creation timestamp is the number of seconds since 12:00AM on 1 January + // 2000. Instead of computing this directly from the TBR (on PSO GC), the game + // uses localtime(), then converts that to the desired timestamp. The leap + // year correction in the latter phase of this computation seems incorrect; it + // adds a day in 2002, 2006, etc. instead of 2004, 2008, etc. See + // compute_psogc_timestamp in SaveFileFormats.cc for details. + le_uint32_t creation_timestamp = 0; + // This field counts certain events on a per-character basis. One of the + // relevant events is the act of sending a 96 command; another is the act of + // receiving a 97 command (to which the client responds with a B1 command). + // Presumably Sega's original implementation could keep track of this value + // for each character and could therefore tell if a character had connected to + // an unofficial server between connections to Sega's servers. + le_uint32_t event_counter = 0; +} __packed_ws__(C_CharSaveInfo_DCv2_PC_V3_BB_96, 8); + +// 97 (S->C): Save to memory card +// Internal name: RcvSaveCountCheck +// No arguments +// Internally, this command is called RcvSaveCountCheck, even though the counter +// in the 96 command (to which 97 is a reply) counts more events than saves. +// Sending this command with header.flag == 0 will show a message saying that +// "character data was improperly saved", and will delete the character's items +// and challenge mode records. newserv (and all other unofficial servers) always +// send this command with flag == 1, which causes the client to save normally. +// If a PSO PC client receives this command multiple times during a session, the +// player will see the "character data may be damaged" message and be asked if +// they want to restore the pre-session backup data. +// Client will respond with a B1 command if header.flag is nonzero. + +// 98 (C->S): Leave game +// Internal name: SndUpdateCharaDataV2 (SndUpdateCharaData in DCv1) +// Same format as 61 command. The server should update its view of the client's +// player data and remove the client from the game it's in (if any), but should +// NOT assign it to an available lobby. The client will send an 84 when it's +// ready to join a lobby. + +// 99 (C->S): Server time accepted +// Internal name: SndPsoDirList +// No arguments +// This command's internal name suggests that it's actually a request for the +// ship select menu, but it's only sent as the response to a B1 command (server +// time) and the client doesn't set any state to indicate it's waiting for a +// ship select menu, so we just treat it as confirmation of a received B1 +// command instead. + +// 9A (C->S): Initial login (no password or client config) +// Internal name: RcvPsoRegistCheck +// Not used on DCv1 - that version uses 90 instead. + +struct C_Login_DC_PC_V3_9A { + pstring v1_serial_number; + pstring v1_access_key; + pstring serial_number; + pstring access_key; + le_uint32_t player_tag = 0x00010000; + le_uint32_t guild_card_number = 0; + le_uint32_t sub_version = 0; + pstring serial_number2; // On DCv2, this is the hardware ID + pstring access_key2; + pstring email_address; +} __packed_ws__(C_Login_DC_PC_V3_9A, 0xDC); + +// 9A (S->C): Account verification result +// Internal name: RcvPsoRegistCheckV2 +// The result code is sent in the header.flag field. Result codes: +// 00 = license ok (don't save to memory card; client responds with 9D/9E) +// 01 = registration required (client responds with a 9C command) +// 02 = license ok (save to memory card; client responds with 9D/9E) +// For all of the below cases, the client doesn't respond and displays a message +// box describing the error. When the player presses a button, the client then +// disconnects. +// 03 = access key invalid (125) (client deletes saved license info) +// 04 = serial number invalid (126) (client deletes saved license info) +// 07 = invalid Hunter's License (117) +// 08 = Hunter's License expired (116) +// 0B = HL not registered under this serial number/access key (112) +// 0C = HL not registered under this serial number/access key (113) +// 0D = HL not registered under this serial number/access key (114) +// 0E = connection error (115) +// 0F = connection suspended (111) +// 10 = connection suspended (111) +// 11 = Hunter's License expired (116) +// 12 = invalid Hunter's License (117) +// 13 = servers under maintenance (118) +// Seems like most (all?) of the rest of the codes are "network error" (119). + +// 9B (S->C): Secondary server init (non-BB, non-DCv1) +// Behaves exactly the same as 17 (S->C). + +// 9B (S->C): Secondary server init (BB) +// Format is the same as 03 (and the client uses the same encryption afterward). +// The only differences that 9B has from 03: +// - 9B does not work during the data-server phase (before the client has +// reached the ship select menu), whereas 03 does. +// - For command 9B, the copyright string must be +// "PSO NEW PM Server. Copyright 1999-2002 SONICTEAM.". +// - The client will respond with a command DB instead of a command 93. + +// 9C (C->S): Register +// Internal name: SndPsoRegist +// It appears PSO GC sends uninitialized data in the header.flag field here. + +struct C_Register_DC_PC_V3_9C { + le_uint64_t unused = 0; + le_uint32_t sub_version = 0; + uint8_t unused1 = 0; + uint8_t language = 0; + parray unused2; + pstring serial_number; // On XB, this is the XBL gamertag + pstring access_key; // On XB, this is the XBL user ID + pstring password; // On XB, this contains "xbox-pso" +} __packed_ws__(C_Register_DC_PC_V3_9C, 0xA0); + +struct C_Register_BB_9C { + le_uint32_t sub_version = 0; + uint8_t unused1 = 0; + uint8_t language = 0; + parray unused2; + pstring username; + pstring password; + pstring game_tag; // "psopc2" on BB +} __packed_ws__(C_Register_BB_9C, 0x98); + +// 9C (S->C): Register result +// Internal name: RcvPsoRegistV2 +// On GC, the only possible error here seems to be wrong password (127) which is +// displayed if the header.flag field is zero. On DCv2/PC, the error text says +// something like "registration failed" instead. If header.flag is nonzero, the +// client proceeds with the login procedure by sending a 9D or 9E. + +// 9D (C->S): Log in without client config (DCv2/PC/GC) +// Not used on DCv1 - that version uses 93 instead. +// Not used on most versions of V3 - the client sends 9E instead. The one +// type of PSO V3 that uses 9D is the Trial Edition of Episodes 1&2. +// The extended version of this command is sent if the client has not yet +// received an 04 (in which case the extended fields are blank) or if the client +// selected the Meet User option, in which case it specifies the requested lobby +// by its menu ID and item ID. + +struct C_Login_DC_PC_GC_9D { + /* 00 */ le_uint32_t player_tag = 0x00010000; // 0x00010000 if guild card is set (via 04) + /* 04 */ le_uint32_t guild_card_number = 0; // 0xFFFFFFFF if not set + /* 08 */ le_uint32_t unused1 = 0; + /* 0C */ le_uint32_t unused2 = 0; + /* 10 */ le_uint32_t sub_version = 0; + /* 14 */ uint8_t is_extended = 0; // If 1, structure has extended format + /* 15 */ uint8_t language = 0; // 0 = JP, 1 = EN, 2 = DE, 3 = FR, 4 = ES + /* 16 */ parray unused3; // Always zeroes + /* 18 */ pstring v1_serial_number; + /* 28 */ pstring v1_access_key; + /* 38 */ pstring serial_number; // On XB, this is the XBL gamertag + /* 48 */ pstring access_key; // On XB, this is the XBL user ID + /* 58 */ pstring serial_number2; // On XB, this is the XBL gamertag + /* 88 */ pstring access_key2; // On XB, this is the XBL user ID + /* B8 */ pstring name; + /* C8 */ +} __packed_ws__(C_Login_DC_PC_GC_9D, 0xC8); + +struct C_LoginExtended_DC_GC_9D : C_Login_DC_PC_GC_9D { + SC_MeetUserExtension_DC_V3 extension; +} __packed_ws__(C_LoginExtended_DC_GC_9D, 0x12C); + +struct C_LoginExtended_PC_9D : C_Login_DC_PC_GC_9D { + SC_MeetUserExtension_PC_BB extension; +} __packed_ws__(C_LoginExtended_PC_9D, 0x14C); + +// 9E (C->S): Log in with client config (V3/BB) +// Not used on GC Episodes 1&2 Trial Edition. +// The extended version of this command is used in the same circumstances as +// when PSO PC uses the extended version of the 9D command. +// PSO XB does not send the client config (security data) in the 9E command, +// even though there is a space for it. The server must use 9F instead to +// retrieve the client config. +// header.flag is 1 if the client has UDP disabled. + +struct C_Login_GC_9E : C_Login_DC_PC_GC_9D { + parray client_config; +} __packed_ws__(C_Login_GC_9E, 0xE8); + +struct C_LoginExtended_GC_9E : C_Login_GC_9E { + SC_MeetUserExtension_DC_V3 extension; +} __packed_ws__(C_LoginExtended_GC_9E, 0x14C); + +struct C_Login_XB_9E : C_Login_DC_PC_GC_9D { + parray unused; + XBNetworkLocation netloc; + parray unknown_a1a; + le_uint32_t xb_user_id_high = 0; + le_uint32_t xb_user_id_low = 0; + le_uint32_t unknown_a1b = 0; +} __packed_ws__(C_Login_XB_9E, 0x130); + +struct C_LoginExtended_XB_9E : C_Login_XB_9E { + SC_MeetUserExtension_DC_V3 extension; +} __packed_ws__(C_LoginExtended_XB_9E, 0x194); + +struct C_LoginExtended_BB_9E { + /* 0000 */ le_uint32_t player_tag = 0x00010000; + /* 0004 */ le_uint32_t guild_card_number = 0; // == account_id when on newserv + /* 0008 */ le_uint32_t sub_version = 0; + /* 000C */ le_uint32_t unknown_a1 = 0; + /* 0010 */ le_uint32_t unknown_a2 = 0; + /* 0014 */ pstring unknown_a3; // Always blank? + /* 0024 */ pstring unknown_a4; // == "?" + /* 0034 */ pstring unknown_a5; // Always blank? + /* 0044 */ pstring unknown_a6; // Always blank? + /* 0054 */ pstring username; + /* 0084 */ pstring password; + /* 00B4 */ pstring guild_card_number_str; + /* 00C4 */ parray unknown_a7; + /* 00EC */ SC_MeetUserExtension_PC_BB extension; + /* 0170 */ +} __packed_ws__(C_LoginExtended_BB_9E, 0x170); + +// 9F (S->C): Request client config / security data (V3/BB) +// This command is not valid on PSO GC Episodes 1&2 Trial Edition, nor any +// pre-V3 PSO versions. Client will respond with a 9F command. +// No arguments + +// 9F (C->S): Client config / security data response (V3/BB) +// The data is opaque to the client, as described at the top of this file. +// If newserv ever sent a 9F command (it currently does not). On BB, this +// command does not work during the data server phase. + +struct C_ClientConfig_V3_9F { + parray data; +} __packed_ws__(C_ClientConfig_V3_9F, 0x20); + +struct C_ClientConfig_BB_9F { + parray data; +} __packed_ws__(C_ClientConfig_BB_9F, 0x28); + +// A0 (C->S): Change ship +// Internal name: SndShipList +// This structure is for documentation only; newserv ignores the arguments here. + +struct C_ChangeShipOrBlock_A0_A1 { + le_uint32_t player_tag = 0x00010000; + le_uint32_t guild_card_number = 0; + parray unused; +} __packed_ws__(C_ChangeShipOrBlock_A0_A1, 0x18); + +// A0 (S->C): Ship select menu +// Same as 07 command. + +// A1 (C->S): Change block +// Internal name: SndBlockList +// Same format as A0. As with A0, newserv ignores the arguments. + +// A1 (S->C): Block select menu +// Same as 07 command. + +// A2 (C->S): Request quest menu +// No arguments + +// A2 (S->C): Quest menu +// Client will respond with an 09, 10, or A9 command. For 09, the server should +// send the category or quest description via an A3 response; for 10, the server +// should send another quest menu (if a category was chosen), or send the quest +// data with 44/13 commands; for A9, the server does not need to respond. + +template +struct S_QuestMenuEntryT { + // Note: The game treats menu_id as two 8-bit fields followed by a 16-bit + // field. In most situations, this is opaque to the server, so we treat it as + // a single 32-bit field; however, in the case of the quest menu, the first + // and second bytes have meaning on the client. + // + // The first byte is used as the quest episode number, which is only relevant + // for showing the Challenge Mode times window when a quest is selected. + // This byte must be set correctly on the quest category entry, not the quest + // itself, so the Episode 1 Challenge quests category should have a value of + // 1 in this byte, and the Episode 2 Challenge quests category should have a + // value of 2. (This is not the only condition required for the Challenge + // Mode times window to work; see the description of command A3 also.) + // + // The second byte of the menu ID is used to determine which icon appears to + // the left of the quest name. + // Specifically: + // 0 = online quest icon (green diamond) + // 1 = download quest icon (green square with outlined diamond) + // 2 = completed download quest icon (orange square with outlined diamond) + // Anything else = same as 1 + le_uint32_t menu_id = 0; + le_uint32_t item_id = 0; + pstring name; + pstring short_description; +} __packed__; +using S_QuestMenuEntry_PC_A2_A4 = S_QuestMenuEntryT; +using S_QuestMenuEntry_DC_GC_A2_A4 = S_QuestMenuEntryT; +using S_QuestMenuEntry_XB_A2_A4 = S_QuestMenuEntryT; +check_struct_size(S_QuestMenuEntry_PC_A2_A4, 0x128); +check_struct_size(S_QuestMenuEntry_DC_GC_A2_A4, 0x98); +check_struct_size(S_QuestMenuEntry_XB_A2_A4, 0xA8); + +struct S_QuestMenuEntry_BB_A2_A4 { + le_uint32_t menu_id = 0; + le_uint32_t item_id = 0; + pstring name; + pstring short_description; + // If this field is set, a yellow hex icon is displayed instead of the green + // or orange diamond icon, and the quest is grayed out and cannot be selected. + // This field is ignored if the icon type (see S_QuestMenuEntry) isn't 1 or 2. + uint8_t disabled = 0; + parray unused; +} __packed_ws__(S_QuestMenuEntry_BB_A2_A4, 0x13C); + +// A3 (S->C): Quest information +// Same format as 1A/D5 command (plain text). The header.flag field is used to +// inform the client of the Challenge stage number, so it can show the correct +// timing window when the stage is selected. The Episode 1 stage numbers should +// be specified as 51-59 (decimal) in header.flag, and the Episode 2 stage +// numbers should be specified as 61-65 (decimal). If the header.flag value is +// outside this range, it is ignored, and the Challenge Mode times window does +// not update. + +// A4 (S->C): Download quest menu +// Internal name: RcvQuestList +// Same format as A2, but can be used when not in a game. The client responds +// similarly as for command A2 with the following differences: +// - Descriptions should be sent with the A5 command instead of A3. +// - If a quest is chosen, it should be sent with A6/A7 commands rather than +// 44/13, and it must be in a different encrypted format. The download quest +// format is documented in create_download_quest_file in Quest.cc. +// - After the download is done, or if the player cancels the menu, the client +// sends an A0 command instead of A9. + +// A5 (S->C): Download quest information +// Same format as 1A/D5 command (plain text) + +// A6: Open file for download +// Internal name: RcvVMDownLoadHead +// Same format as 44. See the description of 44 for some notes on the +// differences between the two commands. +// Like the 44 command, the client->server form of this command is only used on +// V3 and BB. + +// A7: Write download file +// Internal name: RcvVMDownLoad +// Same format as 13. +// Like the 13 command, the client->server form of this command is only used on +// V3 and BB. + +// A8: Invalid command + +// A9 (C->S): Quest menu closed (canceled) +// Internal name: SndQuestEnd +// No arguments +// This command is sent when the in-game quest menu (A2) is closed. This is used +// by the server to unlock the game if the players don't select a quest, since +// players are forbidden from joining while the quest menu is open. When the +// download quest menu is closed, either by downloading a quest or canceling, +// the client sends A0 instead. +// Curiously, PSO GC sends uninitialized data in header.flag. + +// AA (C->S): Send quest statistic (V3/BB) +// This command is generated when an opcode F92E is executed in a quest. +// The server should respond with an AB command. +// This command is likely never sent by PSO GC Episodes 1&2 Trial Edition, +// because the following command (AB) is definitely not valid on that version. + +struct C_SendQuestStatistic_V3_BB_AA { + le_uint16_t stat_id1 = 0; + le_uint16_t unused = 0; + le_uint16_t function_id1 = 0; + le_uint16_t function_id2 = 0; + parray params; +} __packed_ws__(C_SendQuestStatistic_V3_BB_AA, 0x28); + +// AB (S->C): Call quest function (V3/BB) +// This command is not valid on PSO GC Episodes 1&2 Trial Edition. +// Upon receipt, the client starts a quest thread running the given function. +// Probably this is supposed to be one of the function IDs previously sent in +// the AA command, but the client does not check for this. The server can +// presumably use this command to call any function at any time during a quest. + +struct S_CallQuestFunction_V3_BB_AB { + le_uint16_t function_id = 0; + parray unused; +} __packed_ws__(S_CallQuestFunction_V3_BB_AB, 4); + +// AC: Quest barrier (V3/BB) +// No arguments; header.flag must be 0 (or else the client disconnects) +// After a quest begins loading in a game (the server sends 44/13 commands to +// each player with the quest's data), each player will send an AC to the server +// when it has parsed the quest and is ready to start. When all players in a +// game have sent an AC to the server, the server should send them all an AC, +// which starts the quest for all players at (approximately) the same time. +// Sending this command to a GC client when it is not waiting to start a quest +// will cause it to crash. +// This command is not valid on PSO GC Episodes 1&2 Trial Edition. + +// AD: Invalid command +// AE: Invalid command +// AF: Invalid command + +// B0 (S->C): Text message +// Internal name: RcvEmergencyCall +// Same format as 01 command. This command is supported on DCv1 and all later +// versions, but not on prototype versions or DC NTE. +// The message appears as an overlay on the right side of the screen. The player +// doesn't do anything to dismiss it; it will disappear after a few seconds. + +// B1 (C->S): Request server time +// Internal name: GetServerTime +// No arguments +// Server will respond with a B1 command. + +// B1 (S->C): Server time +// Internal name: RcvServerTime +// This command is supported on DCv1 and all later versions, but not on +// prototype versions or DC NTE. +// Contents is a string like "%Y:%m:%d: %H:%M:%S.000" (the space is not a typo). +// For example: 2022:03:30: 15:36:42.000 +// It seems the client ignores the date part and the milliseconds part; only the +// hour, minute, and second fields are actually used. +// This command can be sent even if it's not requested by the client (with B1). +// For example, some servers send this command every time a client joins a game. +// The time_flags fields are used on V3 and later. Time flags are a 24-bit +// little-endian integer, only 2 bits of which are used: +// time_flags_low & 1 specifies whether the client should send a ping (1D) +// every 900 frames (30 seconds). +// time_flags_low & 2 disables system interrupts during a part of the GBA game +// loading procedure. (Predictably, this is only used on GC versions.) It's +// not clear what the downstream effects of this are, or why the server +// should have any control over this behavior in the first place. +// Client will respond with a 99 command. + +struct S_ServerTime_B1 { + /* 00 */ pstring time_str; + /* 19 */ uint8_t time_flags_low = 0x01; + /* 1A */ uint8_t time_flags_mid = 0x00; + /* 1B */ uint8_t time_flags_high = 0x00; + /* 1C */ +} __packed_ws__(S_ServerTime_B1, 0x1C); + +// B2 (S->C): Execute code and/or checksum memory (DCv2 and all later versions) +// Internal name: RcvProgramPatch +// Client will respond with a B3 command with the same header.flag value as was +// sent in the B2. +// On PSO PC, the code section (if included in the B2 command) is parsed and +// relocated, but is not actually executed, so the return_value field in the +// resulting B3 command is always 0. The checksum functionality does work on PSO +// PC, just like the other versions. +// This command doesn't work on some PSO GC versions, namely the later JP PSO +// Plus (v1.5), US PSO Plus (v1.2), or US/EU Episode 3. Sega presumably removed +// it after taking heat from Nintendo about enabling homebrew on the GameCube. +// On the earlier JP PSO Plus (v1.4) and JP Episode 3, this command is +// implemented as described here, with some additional compression and +// encryption steps added, similarly to how download quests are encoded. See +// send_function_call in SendCommands.cc for more details on how this works. + +// newserv supports exploiting a bug in the USA version of Episode 3, which +// re-enables the use of this command on that version of the game. See +// system/client-functions/Episode3USAQuestBufferOverflow.ppc.s for further +// details. + +struct S_ExecuteCode_B2 { + // If code_size == 0, no code is executed, but checksumming may still occur. + // In that case, this structure is the entire body of the command (no footer + // is sent). + le_uint32_t code_size = 0; // Size of code (following this struct) and footer + le_uint32_t checksum_start = 0; // May be null if size is zero + le_uint32_t checksum_size = 0; // If zero, no checksum is computed + // The code immediately follows, ending with an S_ExecuteCode_Footer_B2 +} __packed_ws__(S_ExecuteCode_B2, 0x0C); + +template +struct S_ExecuteCode_FooterT_B2 { + using U16T = typename std::conditional::type; + using U32T = typename std::conditional::type; + + // Relocations is a list of words (le_uint16_t on DC/PC/XB/BB, be_uint16_t on + // GC) containing the number of doublewords (uint32_t) to skip for each + // relocation. The relocation pointer starts immediately after the + // checksum_size field in the header, and advances by the value of one + // relocation word (times 4) before each relocation. At each relocated + // doubleword, the address of the first byte of the code (after checksum_size) + // is added to the existing value. + // For example, if the code segment contains the following data (where R + // specifies doublewords to relocate): + // RR RR RR RR ?? ?? ?? ?? ?? ?? ?? ?? RR RR RR RR + // RR RR RR RR ?? ?? ?? ?? RR RR RR RR + // then the relocation words should be 0000, 0003, 0001, and 0002. + // If there is a small number of relocations, they may be placed in the unused + // fields of this structure to save space and/or confuse reverse engineers. + // The game never accesses the last 12 bytes of this structure unless + // relocations_offset points there, so those 12 bytes may also be omitted from + // the command entirely (without changing code_size - so code_size would + // technically extend beyond the end of the B2 command). + U32T relocations_offset = 0; // Relative to code base (after checksum_size) + U32T num_relocations = 0; + parray unused1; + // entrypoint_offset is doubly indirect - it points to a pointer to a 32-bit + // value that itself is the actual entrypoint. This is presumably done so the + // entrypoint can be optionally relocated. + U32T entrypoint_addr_offset = 0; // Relative to code base (after checksum_size). + parray unused2; +} __packed__; +using S_ExecuteCode_Footer_GC_B2 = S_ExecuteCode_FooterT_B2; +using S_ExecuteCode_Footer_DC_PC_XB_BB_B2 = S_ExecuteCode_FooterT_B2; +check_struct_size(S_ExecuteCode_Footer_GC_B2, 0x20); +check_struct_size(S_ExecuteCode_Footer_DC_PC_XB_BB_B2, 0x20); + +// B3 (C->S): Execute code and/or checksum memory result +// Not used on versions that don't support the B2 command (see above). + +struct C_ExecuteCodeResult_B3 { + // On DC, return_value has the value in r0 when the function returns. + // On PC, return_value is always 0. + // On GC, return_value has the value in r3 when the function returns. + // On XB and BB, return_value has the value in eax when the function returns. + // If code_size was 0 in the B2 command, return_value is always 0. + le_uint32_t return_value = 0; + le_uint32_t checksum = 0; // 0 if no checksum was computed +} __packed_ws__(C_ExecuteCodeResult_B3, 8); + +// B4: Invalid command +// B5: Invalid command +// B6: Invalid command + +// B7 (S->C): Rank update (Episode 3) + +struct S_RankUpdate_Ep3_B7 { + // If rank is not zero, the client sets its rank text to ":", + // truncated to 11 characters. If rank is zero, the client uses rank_text + // without modifying it. + le_uint32_t rank = 0; + pstring rank_text; + le_uint32_t current_meseta = 0; + le_uint32_t total_meseta_earned = 0; + le_uint32_t unlocked_jukebox_songs = 0xFFFFFFFF; +} __packed_ws__(S_RankUpdate_Ep3_B7, 0x1C); + +// B7 (C->S): Confirm rank update (Episode 3) +// No arguments +// The client sends this after it receives a B7 from the server. + +// B8 (S->C): Update card definitions (Episode 3) +// Contents is a single le_uint32_t specifying the size of the (PRS-compressed) +// data, followed immediately by the data. The maximum size of the compressed +// data is 0x9000 bytes, although the receive buffer size limit applies first in +// practice, which limits this to 0x7BF8 bytes. The maximum size of the +// decompressed data is 0x36EC0 bytes on retail Episode 3, or 0x32960 bytes on +// Trial Edition. +// Note: PSO BB accepts this command as well, but ignores it. + +// B8 (C->S): Confirm updated card definitions (Episode 3) +// No arguments +// The client sends this after it receives a B8 from the server. + +// B9 (S->C): Update CARD lobby media (Episode 3) +// This command is not valid on Episode 3 Trial Edition. + +struct S_UpdateMediaHeader_Ep3_B9 { + // Valid values for the type field: + // 1: Texture set (GVM file) + // 2: Model + // 3: Animation + // 4: Delete all previous media updates + // Any other value: entire command is ignored + // A texture can be displayed without a model or animation. A model requires a + // texture (sent in a separate B9 command with the same location_flags), but + // does not require an animation - it will just stand still without one. An + // animation requires both a texture and model with the same location_flags. + // For models and animations, the game looks for various tokens in the + // decompressed data; specifically '****', 'GCAM', 'GJBM', 'GJTL', 'GLIM', + // 'GMDM', 'GSSM', 'NCAM', 'NJBM', 'NJCA', 'NLIM', 'NMDM', and 'NSSM'. + le_uint32_t type = 0; + // location_flags is a bit field specifying where the banner or object should + // appear. The bits are: + // 00000001: South above-counter banner (facing away from teleporters) + // 00000002: West above-counter banner + // 00000004: North above-counter banner (facing toward jukebox) + // 00000008: East above-counter banner + // 00000010: Banner above west (left) teleporter + // 00000020: Banner above east (right) teleporter + // 00000040: Banner at south end of lobby (opposite the jukebox) + // 00000080: Immediately left of 00000040 + // 00000100: Immediately right of 00000040 + // 00000200: Same as 00000080, but further left and at a slight inward angle + // 00000400: Same as 00000100, but further right and at a slight inward angle + // 00000800: Banner at north end of lobby, above the jukebox + // 00001000: Immediately right of 00000800 + // 00002000: Immediately left of 00000800 + // 00004000: Same as 00001000, but further right and at a slight inward angle + // 00008000: Same as 00002000, but further left and at a slight inward angle + // 00010000: Banners at west AND east ends of lobby, next to battle tables + // 00020000: Immediately left of 00010000 (2 banners) + // 00040000: Immediately right of 00010000 (2 banners) + // 00080000: Banners on southwest AND southeast ends of the lobby + // 00100000: Banners on south-southwest AND south-southeast ends of the lobby + // 00200000: Floor banners in front of the counter (4 banners) + // 00400000: Banners on both small walls in front of the battle tables + // 00800000: On southern platform + // 01000000: In front of jukebox + // 02000000: In western battle table corner (next to 4-player tables) + // 04000000: In eastern battle table corner (next to 2-player tables) + // 08000000: In southeastern battle table corner (next to 2-player tables) + // 10000000: In southwestern battle table corner (next to 4-player tables) + // 20000000: Just north-northwest of the counter + // 40000000: In front of the small wall in front of the 2-player battle tables + // 80000000: Inside the lobby counter, facing southeast + // Positions 00800000 and above appear to be intended for models and not + // banners - if a banner is sent in these locations, it appears sideways and + // halfway submerged in the floor, and has no collision. Furthermore, it seems + // that up to 8 different banners or models may be set simultaneously (though + // each may appear in more than one position). If 8 banners or objects already + // exist, further media sent via B9 is ignored. + le_uint32_t location_flags = 0x00000000; + // This field specifies the size of the compressed data. The uncompressed size + // is not sent anywhere in this command. + le_uint16_t size = 0; + le_uint16_t unused = 0; + // The PRS-compressed data immediately follows this header. The maximum size + // of the compressed data is 0x3800 bytes, and it must decompress to fewer + // than 0x37000 bytes of output. If either of these limits are violated, the + // client ignores the command. +} __packed_ws__(S_UpdateMediaHeader_Ep3_B9, 0x0C); + +// B9 (C->S): Confirm media update (Episode 3) +// No arguments +// This command is not valid on Episode 3 Trial Edition. +// The client sends this even if it ignores the contents of a B9 command. + +// BA: Meseta transaction (Episode 3) +// This command is not valid on Episode 3 Trial Edition. +// header.flag specifies the transaction purpose. Specific known values: +// 00 = unknown +// 01 = Initialize Meseta subsystem (C->S; always has a value of 0) +// 02 = Spend meseta (at e.g. lobby jukebox or Pinz's shop) (C->S) +// 03 = Spend meseta response (S->C; request_token must match the last token +// sent by client) +// 04 = unknown (C->S; request_token must match the last token sent by client) + +struct C_MesetaTransaction_Ep3_BA { + le_uint32_t transaction_num = 0; + le_uint32_t value = 0; + le_uint32_t request_token = 0; +} __packed_ws__(C_MesetaTransaction_Ep3_BA, 0x0C); + +struct S_MesetaTransaction_Ep3_BA { + le_uint32_t current_meseta = 0; + le_uint32_t total_meseta_earned = 0; + le_uint32_t request_token = 0; // Should match the token sent by the client +} __packed_ws__(S_MesetaTransaction_Ep3_BA, 0x0C); + +// BB (S->C): Tournament match information (Episode 3) +// This command is not valid on Episode 3 Trial Edition. Because of this, it +// must have been added fairly late in development, but it seems to be unused, +// perhaps because the E1/E3 commands are generally more useful... but the E1/E3 +// commands exist in Trial Edition! So why was this added? Was it just never +// finished? We may never know... +// header.flag is the number of valid match entries. + +struct S_TournamentMatchInformation_Ep3_BB { + pstring tournament_name; + struct TeamEntry { + le_uint16_t win_count = 0; + le_uint16_t is_active = 0; + pstring name; + } __packed_ws__(TeamEntry, 0x24); + parray team_entries; + le_uint16_t num_teams = 0; + le_uint16_t unknown_a3 = 0; // Probably actually unused + struct MatchEntry { + pstring name; + uint8_t locked = 0; + uint8_t count = 0; + uint8_t max_count = 0; + uint8_t unused = 0; + } __packed_ws__(MatchEntry, 0x24); + parray match_entries; +} __packed_ws__(S_TournamentMatchInformation_Ep3_BB, 0xDA4); + +// BC: Invalid command +// BD: Invalid command +// BE: Invalid command +// BF: Invalid command + +// C0 (C->S): Request choice search options (DCv2 and later versions) +// Internal name: GetChoiceList +// No arguments +// Server should respond with a C0 command (described below). + +// C0 (S->C): Choice search options (DCv2 and later versions) +// Internal name: RcvChoiceList + +// Command is a list of these; header.flag is the entry count (incl. top-level). +template +struct S_ChoiceSearchEntryT { + // Category IDs are nonzero; if the high byte of the ID is nonzero then the + // category can be set by the user at any time; otherwise it can't. + le_uint16_t parent_choice_id = 0; // 0 for top-level categories + le_uint16_t choice_id = 0; + pstring text; +} __packed__; +using S_ChoiceSearchEntry_DC_V3_C0 = S_ChoiceSearchEntryT; +using S_ChoiceSearchEntry_PC_BB_C0 = S_ChoiceSearchEntryT; +check_struct_size(S_ChoiceSearchEntry_DC_V3_C0, 0x20); +check_struct_size(S_ChoiceSearchEntry_PC_BB_C0, 0x3C); + +// Top-level categories are things like "Level", "Class", etc. +// Choices for each top-level category immediately follow the category, so +// a reasonable order of items is (for example): +// 00 00 11 01 "Preferred difficulty" +// 11 01 01 01 "Normal" +// 11 01 02 01 "Hard" +// 11 01 03 01 "Very Hard" +// 11 01 04 01 "Ultimate" +// 00 00 22 00 "Character class" +// 22 00 01 00 "HUmar" +// 22 00 02 00 "HUnewearl" +// etc. + +// C1 (C->S): Create game (DCv2 and later versions) +// Internal name: SndCreateGame + +template +struct C_CreateGameBaseT { + // menu_id and item_id are only used for the E7 (create spectator team) form + // of this command + le_uint32_t menu_id = 0; + le_uint32_t item_id = 0; + pstring name; + pstring password; +} __packed__; +using C_CreateGame_DCNTE = C_CreateGameBaseT; +check_struct_size(C_CreateGame_DCNTE, 0x28); + +template +struct C_CreateGameT : C_CreateGameBaseT { + uint8_t difficulty = 0; // 0-3 (always 0 on Episode 3) + uint8_t battle_mode = 0; // 0 or 1 (always 0 on Episode 3) + // Note: Episode 3 uses the challenge mode flag for view battle permissions. + // 0 = view battle allowed; 1 = not allowed + uint8_t challenge_mode = 0; // 0 or 1 + // Note: According to the Sylverant wiki, in v2-land, the episode field has a + // different meaning: if set to 0, the game can be joined by v1 and v2 + // players; if set to 1, it's v2-only. + uint8_t episode = 0; // 1-4 on V3+ (3 on Episode 3); unused on DC/PC +} __packed__; +using C_CreateGame_DC_V3_0C_C1_Ep3_EC = C_CreateGameT; +using C_CreateGame_PC_C1 = C_CreateGameT; +check_struct_size(C_CreateGame_DC_V3_0C_C1_Ep3_EC, 0x2C); +check_struct_size(C_CreateGame_PC_C1, 0x4C); + +struct C_CreateGame_BB_C1 : C_CreateGameT { + uint8_t solo_mode = 0; + parray unused2; +} __packed_ws__(C_CreateGame_BB_C1, 0x50); + +// C2 (C->S): Set choice search parameters (DCv2 and later versions) +// Internal name: PutChoiceList +// Server does not respond. +// Contents is a ChoiceSearchConfig, which is defined in PlayerSubordinates.hh. + +// C3 (C->S): Execute choice search (DCv2 and later versions) +// Internal name: SndChoiceSeq +// Same format as C2. The disabled field is unused. +// Server should respond with a C4 command. + +// C4 (S->C): Choice search results (DCv2 and later versions) +// Internal name: RcvChoiceAns +// There is a bug that can cause the client to crash or display garbage if this +// command is sent with no entries. To work around this, newserv sends a blank +// entry (but still with header.flag = 0) if there are no results. + +// Command is a list of these; header.flag is the entry count +template +struct S_ChoiceSearchResultEntryT_C4 { + le_uint32_t guild_card_number = 0; + pstring name; + pstring info_string; // Usually something like " Lvl " + // Format is stricter here; this is "LOBBYNAME,BLOCKNUM,SHIPNAME" + // If target is in game, for example, "Game Name,BLOCK01,Alexandria" + // If target is in lobby, for example, "BLOCK01-1,BLOCK01,Alexandria" + pstring location_string; + HeaderT reconnect_command_header; // Ignored by the client + S_Reconnect_19 reconnect_command; + SC_MeetUserExtensionT meet_user; +} __packed__; +using S_ChoiceSearchResultEntry_DC_V3_C4 = S_ChoiceSearchResultEntryT_C4; +using S_ChoiceSearchResultEntry_PC_C4 = S_ChoiceSearchResultEntryT_C4; +using S_ChoiceSearchResultEntry_BB_C4 = S_ChoiceSearchResultEntryT_C4; +check_struct_size(S_ChoiceSearchResultEntry_DC_V3_C4, 0xD4); +check_struct_size(S_ChoiceSearchResultEntry_PC_C4, 0x154); +check_struct_size(S_ChoiceSearchResultEntry_BB_C4, 0x158); + +// C5 (S->C): Player records update (DCv2 and later versions) +// Internal name: RcvChallengeData +// Command is a list of PlayerRecordsEntry structures; header.flag specifies +// the entry count. +// The server sends this command when a player joins a lobby to update the +// challenge mode records of all the present players. + +// C6 (C->S): Set blocked senders list (V3/BB) +// The command always contains the same number of entries, even if the entries +// at the end are blank (zero). + +template +struct C_SetBlockedSendersT_C6 { + parray blocked_senders; +} __packed__; +using C_SetBlockedSenders_V3_C6 = C_SetBlockedSendersT_C6<30>; +using C_SetBlockedSenders_BB_C6 = C_SetBlockedSendersT_C6<28>; +check_struct_size(C_SetBlockedSenders_V3_C6, 0x78); +check_struct_size(C_SetBlockedSenders_BB_C6, 0x70); + +// C7 (C->S): Enable simple mail auto-reply (V3/BB) +// Same format as 1A/D5 command (plain text). +// Server does not respond + +// C8 (C->S): Disable simple mail auto-reply (V3/BB) +// No arguments +// Server does not respond + +// C9: Broadcast command (Episode 3) +// Internal name: SndCardClientData +// Same as 60, but should be forwarded only to Episode 3 clients. +// newserv uses this command for all server-generated events (in response to CA +// commands), except for map data requests. This differs from Sega's original +// implementation, which sent CA responses via 60 commands instead. + +// C9 (C->S): Change connection status (Xbox) +// header.flag specifies if the player's online status should be hidden; 1 means +// shown, 2 means hidden. + +// CA (C->S): Server data request (Episode 3) +// Internal name: SndCardServerData +// The CA command format is the same as that of the 6xB3 commands, and the +// subsubcommands formats are shared as well. Unlike the 6x commands, the server +// is expected to respond to the command appropriately instead of forwarding it. +// Because the formats are shared, the 6xB3 commands are also referred to as CAx +// commands in the comments and structure names. + +// CB: Broadcast command (Episode 3) +// Internal name: SndKansenPsoData +// Same as 60, but only send to Episode 3 clients. +// This command's format is identical to C9, except that CB is not valid on +// Episode 3 Trial Edition (whereas C9 is valid). +// Unlike the 6x and C9 commands, subcommands sent with the CB command are +// forwarded from spectator teams to the primary team. The client only uses this +// behavior for the 6xBE command (sound chat), and newserv enforces that no +// other subcommand can be sent via CB. + +// CC (S->C): Confirm tournament entry (Episode 3) +// This command is not valid on Episode 3 Trial Edition. +// header.flag determines the client's registration state - 1 if the client is +// registered for the tournament, 0 if not. +// This command controls what's shown in the Check Tactics pane in the pause +// menu. If the client is registered (header.flag==1), the option is enabled and +// the bracket data in the command is shown there, and a third pane on the +// Status item shows the other information (tournament name, ship name, and +// start time). If the client is not registered, the Check Tactics option is +// disabled and the Status item has only two panes. + +struct S_ConfirmTournamentEntry_Ep3_CC { + pstring tournament_name; + le_uint16_t num_teams = 0; + le_uint16_t players_per_team = 0; + le_uint16_t unknown_a2 = 0; + le_uint16_t unknown_a3 = 0; + pstring server_name; + pstring start_time; // e.g. "15:09:30" or "13:03 PST" + struct TeamEntry { + le_uint16_t win_count = 0; + le_uint16_t is_active = 0; + pstring name; + } __packed_ws__(TeamEntry, 0x24); + parray team_entries; +} __packed_ws__(S_ConfirmTournamentEntry_Ep3_CC, 0x508); + +// CD: Invalid command +// CE: Invalid command +// CF: Invalid command + +// D0 (C->S): Start trade sequence (V3/BB) +// The trade window sequence is a bit complicated. The normal flow is: +// - Clients sync trade state with 6xA6 commands +// - When both have confirmed, one client (the initiator) sends a D0 +// - Server sends a D1 to the non-initiator +// - Non-initiator sends a D0 +// - Server sends a D1 to both clients +// - Both clients delete the sent items from their inventories (and send the +// appropriate subcommand) +// - Both clients send a D2 (similarly to how AC works, the server should not +// proceed until both D2s are received) +// - Server sends a D3 to both clients with each other's data from their D0s, +// followed immediately by a D4 01 to both clients, which completes the trade +// - Both clients send the appropriate subcommand to create inventory items +// TODO: On BB, is the server responsible for sending the appropriate item +// delete/create subcommands? +// At any point if an error occurs, either client may send a D4 00, which +// cancels the entire sequence. The server should then send D4 00 to both +// clients. +// TODO: The server should presumably also send a D4 00 if either client +// disconnects during the sequence. + +struct SC_TradeItems_D0_D3 { // D0 when sent by client, D3 when sent by server + le_uint16_t target_client_id = 0; + le_uint16_t item_count = 0; + // Note: PSO GC sends uninitialized data in the unused entries of this + // command. newserv parses and regenerates the item data when sending D3, + // which effectively erases the uninitialized data. + parray item_datas; +} __packed_ws__(SC_TradeItems_D0_D3, 0x284); + +// D1 (S->C): Advance trade state (V3/BB) +// No arguments +// See D0 description for usage information. + +// D2 (C->S): Trade can proceed (V3/BB) +// No arguments +// See D0 description for usage information. + +// D3 (S->C): Execute trade (V3/BB) +// On V3, this command has the same format as D0. See the D0 description for +// usage information. +// On BB, this command has no arguments (and the server generates the +// appropriate delete and create inventory item commands), but the D3 command +// must still must be sent before the D4 command to advance the trade state. + +// D4 (C->S): Trade failed (V3/BB) +// No arguments +// See D0 description for usage information. + +// D4 (S->C): Trade complete (V3/BB) +// header.flag must be 0 (trade failed) or 1 (trade complete). +// See D0 description for usage information. + +// D5: Large message box (V3/BB) +// Same as 1A command, except the maximum length of the message is 0x1000 bytes. + +// D6 (C->S): Large message box closed (V3) +// No arguments +// DC, PC, and BB do not send this command at all. GC US v1.0 and v1.1 will send +// this command when any large message box (1A/D5) is closed; GC Plus and +// Episode 3 will send D6 only for large message boxes that occur before the +// client has joined a lobby. (After joining a lobby, large message boxes will +// still be displayed if sent by the server, but the client won't send a D6 when +// they are closed.) In some of these versions, there is a bug that sets an +// incorrect interaction mode when the message box is closed while the player is +// in the lobby; some servers (e.g. Schtserv) send a lobby welcome message +// anyway, along with an 01 (lobby message box) which properly sets the +// interaction mode when closed. + +// D7 (C->S): Request GBA game file (V3) +// This command is sent when the client executes the file_dl_req (F8C0) quest +// opcode. header.flag contains the value of the opcode's first argument; the +// second argument is a pointer to the filename. +// The server should send the requested file using A6/A7 commands; if the file +// does not exist, the server should reply with a D7 command. +// This command exists on XB as well, but it presumably is never sent by the +// client. + +struct C_GBAGameRequest_V3_D7 { + pstring filename; +} __packed_ws__(C_GBAGameRequest_V3_D7, 0x10); + +// D7 (S->C): GBA file not found (V3/BB) +// No arguments +// This command is not valid on PSO GC Episodes 1&2 Trial Edition. +// This command tells the client that the file it requested via a D7 command +// does not exist. This causes the F8C1 (get_dl_status) quest opcode to return +// 0 (file not found), rather than 1 (download in progress) or 2 (complete). +// PSO BB accepts but completely ignores this command. + +// D8 (C->S): Info board request (V3/BB) +// No arguments +// The server should respond with a D8 command (described below). + +// D8 (S->C): Info board contents (V3/BB) +// This command is not valid on PSO GC Episodes 1&2 Trial Edition. + +// Command is a list of these; header.flag is the entry count. There should be +// one entry for each player in the current lobby/game. +template +struct S_InfoBoardEntryT_D8 { + pstring name; + pstring message; +} __packed__; +using S_InfoBoardEntry_V3_D8 = S_InfoBoardEntryT_D8; +using S_InfoBoardEntry_BB_D8 = S_InfoBoardEntryT_D8; +check_struct_size(S_InfoBoardEntry_V3_D8, 0xBC); +check_struct_size(S_InfoBoardEntry_BB_D8, 0x178); + +// D9 (C->S): Write info board (V3/BB) +// Contents are plain text, like 1A/D5. +// Server does not respond + +// DA (S->C): Change lobby event (V3/BB) +// header.flag = new event number; no other arguments. +// This command is not valid on PSO GC Episodes 1&2 Trial Edition. + +// DB (C->S): Verify license (V3/BB) +// Server should respond with a 9A command. + +struct C_VerifyAccount_V3_DB { + pstring unused; + pstring serial_number; // On XB, this is the XBL gamertag + pstring access_key; // On XB, this is the XBL user ID + pstring unused2; + le_uint32_t sub_version = 0; + pstring serial_number2; // On XB, this is the XBL gamertag + pstring access_key2; // On XB, this is the XBL user ID + pstring password; // On XB, this contains "xbox-pso" +} __packed_ws__(C_VerifyAccount_V3_DB, 0xDC); + +// Note: This login pathway generally isn't used on BB (and isn't supported at +// all during the data server phase). All current servers use 03/93 instead. +struct C_VerifyAccount_BB_DB { + // Note: These four fields are likely the same as those used in BB's 9E + pstring unknown_a3; // Always blank? + pstring unknown_a4; // == "?" + pstring unknown_a5; // Always blank? + pstring unknown_a6; // Always blank? + le_uint32_t sub_version = 0; + pstring username; + pstring password; + pstring game_tag; // "psopc2" +} __packed_ws__(C_VerifyAccount_BB_DB, 0xD4); + +// DC: Set battle in progress flag (Episode 3) +// No arguments except header.flag when sent by the client. When header.flag is +// 1, the game should be locked - no players should be allowed to join. In this +// case, the client waits for the server to respond with another DC command +// before proceeding with battle setup. When header.flag is 0, the game should +// be unlocked, and the client does not wait for a response from the server. + +// DC: Guild card data (BB) + +struct S_GuildCardHeader_BB_01DC { + le_uint32_t unknown = 1; + le_uint32_t filesize = 0x0000D590; + le_uint32_t checksum = 0; // CRC32 of entire guild card file (0xD590 bytes) +} __packed_ws__(S_GuildCardHeader_BB_01DC, 0x0C); + +struct S_GuildCardFileChunk_02DC { + le_uint32_t unknown = 0; // 0 + le_uint32_t chunk_index = 0; + parray data; // Command may be shorter if this is the last chunk +} __packed_ws__(S_GuildCardFileChunk_02DC, 0x6808); + +struct C_GuildCardDataRequest_BB_03DC { + le_uint32_t unknown = 0; + le_uint32_t chunk_index = 0; + le_uint32_t cont = 0; +} __packed_ws__(C_GuildCardDataRequest_BB_03DC, 0x0C); + +// DD (S->C): Send quest state to joining player (BB) +// When a player joins a game with a quest already in progress, the server +// should send this command to the leader. header.flag is the client ID that the +// leader should send quest state to; the leader will then send a series of +// target commands (62/6D) that the server can forward to the joining player. +// No other arguments + +// DE (S->C): Rare monster list (BB) + +struct S_RareMonsterList_BB_DE { + // Unused entries are set to FFFF + parray enemy_indexes; +} __packed_ws__(S_RareMonsterList_BB_DE, 0x20); + +// DF (C->S): Set Challenge Mode parameters (BB) +// This command has 7 subcommands, most of which are self-explanatory. + +struct C_SetChallengeModeStageNumber_BB_01DF { + le_uint32_t stage = 0; +} __packed_ws__(C_SetChallengeModeStageNumber_BB_01DF, 4); + +struct C_SetChallengeModeCharacterTemplate_BB_02DF { + le_uint32_t template_index = 0; +} __packed_ws__(C_SetChallengeModeCharacterTemplate_BB_02DF, 4); + +struct C_SetChallengeModeDifficulty_BB_03DF { + // No existing challenge mode quest sets this to a value other than zero. + le_uint32_t difficulty = 0; +} __packed_ws__(C_SetChallengeModeDifficulty_BB_03DF, 4); + +struct C_SetChallengeModeEXPMultiplier_BB_04DF { + le_float exp_multiplier = 1.0f; +} __packed_ws__(C_SetChallengeModeEXPMultiplier_BB_04DF, 4); + +struct C_SetChallengeRankText_BB_05DF { + le_uint32_t rank_color = 0; // ARGB8888 + pstring rank_text; +} __packed_ws__(C_SetChallengeRankText_BB_05DF, 0x1C); + +// This is sent once for each rank (so, 3 times in total) +struct C_SetChallengeRankThreshold_BB_06DF { + le_uint32_t rank = 0; // 0 = B, 1 = A, 2 = S + le_uint32_t seconds = 0; + le_uint32_t rank_bitmask = 0; // 1 = B, 2 = A, 4 = S +} __packed_ws__(C_SetChallengeRankThreshold_BB_06DF, 0x0C); + +struct C_CreateChallengeModeAwardItem_BB_07DF { + le_uint32_t prize_rank = 0xFFFFFFFF; // 0 = B, 1 = A, 2 = S, anything else = error + le_uint32_t rank_bitmask = 0; // Same as in 06DF + ItemData item; +} __packed_ws__(C_CreateChallengeModeAwardItem_BB_07DF, 0x1C); + +// E0 (S->C): Tournament list (Episode 3) +// The client will send 09 and 10 commands to inspect or enter a tournament. The +// server should respond to an 09 command with an E3 command; the server should +// respond to a 10 command with an E2 command. +// header.flag is the count of filled-in entries. + +struct S_TournamentList_Ep3NTE_E0 { + struct Entry { + le_uint32_t menu_id = 0; + le_uint32_t item_id = 0; + uint8_t unknown_a1 = 0; + uint8_t locked = 0; + uint8_t state = 0; + uint8_t unknown_a2 = 0; + le_uint32_t start_time = 0; // In seconds since Unix epoch + pstring name; + le_uint16_t num_teams = 0; + le_uint16_t max_teams = 0; + } __packed_ws__(Entry, 0x34); + parray entries; +} __packed_ws__(S_TournamentList_Ep3NTE_E0, 0x680); + +struct S_TournamentList_Ep3_E0 { + struct Entry { + le_uint32_t menu_id = 0; + le_uint32_t item_id = 0; + uint8_t unknown_a1 = 0; + uint8_t locked = 0; // If nonzero, the lock icon appears in the menu + // Values for the state field: + // 00 = Preparing + // 01 = 1st Round + // 02 = 2nd Round + // 03 = 3rd Round + // 04 = Semifinals + // 05 = Entries no longer accepted + // 06 = Finals + // 07 = Preparing for Battle + // 08 = Battle in progress + // 09 = Preparing to view Battle + // 0A = Viewing a Battle + // Values beyond 0A don't appear to cause problems, but cause strings to + // appear that are obviously not intended to appear in the tournament list, + // like "View the board" and "Board: Write". (In fact, some of the strings + // listed above may be unintended for this menu as well.) + uint8_t state = 0; + uint8_t unknown_a2 = 0; + le_uint32_t start_time = 0; // In seconds since Unix epoch + pstring name; + le_uint16_t num_teams = 0; + le_uint16_t max_teams = 0; + le_uint16_t unknown_a3 = 0xFFFF; + le_uint16_t unknown_a4 = 0xFFFF; + } __packed_ws__(Entry, 0x38); + parray entries; +} __packed_ws__(S_TournamentList_Ep3_E0, 0x700); + +// E0 (C->S): Request system file (BB) +// No arguments. The server should respond with an E1 or E2 command. + +// E1 (S->C): Game information (Episode 3) +// The header.flag argument determines which fields are valid (and which panes +// should be shown in the information window). The values are the same as for +// the E3 command, but each value only makes sense for one command. That is, 00, +// 01, and 04 should be used with the E1 command, while 02, 03, and 05 should be +// used with the E3 command. See the E3 command for descriptions of what each +// flag value means. + +template +struct S_GameInformationBaseT_Ep3_E1 { + /* 0000 */ pstring game_name; + struct PlayerEntry { + pstring name; // From disp.name + pstring description; // Usually something like "FOmarl CLv30 J" + } __packed_ws__(PlayerEntry, 0x30); + /* 0020 */ parray player_entries; + /* 00E0 */ pstring map_name; + /* 0100 */ RulesT rules; + /* 0114 */ parray spectator_entries; + /* 0294 */ +} __packed__; +using S_GameInformation_Ep3NTE_E1 = S_GameInformationBaseT_Ep3_E1; +using S_GameInformation_Ep3_E1 = S_GameInformationBaseT_Ep3_E1; +check_struct_size(S_GameInformation_Ep3NTE_E1, 0x28C); +check_struct_size(S_GameInformation_Ep3_E1, 0x294); + +// E1 (S->C): System file created (BB) +// This seems to take the place of 00E2 in certain cases. Perhaps it was used +// when a client hadn't logged in before and didn't have a system file, so the +// client should use appropriate defaults. + +struct S_SystemFileCreated_00E1_BB { + // If success is not equal to 1, the client shows a message saying "Forced + // server disconnect (907)" and disconnects. Otherwise, the client proceeeds + // as if it had received an 00E2 command, and sends its first 00E3. + le_uint32_t success = 1; +} __packed_ws__(S_SystemFileCreated_00E1_BB, 4); + +// E2 (C->S): Tournament control (Episode 3) +// No arguments (in any of its forms) except header.flag, which determines ths +// command's meaning. Specifically: +// flag=00: request tournament list (server responds with E0) +// flag=01: check tournament (server responds with E2) +// flag=02: cancel tournament entry (server responds with CC) +// flag=03: create tournament spectator team (server responds with E0) +// flag=04: join tournament spectator team (server responds with E0) +// In case 02, the resulting CC command has header.flag = 0 to indicate the +// player is no longer registered. +// In cases 03 and 04, the client handles follow-ups differently from the 00 +// case. In case 04, the client will send a 10 (to which the server responds +// with an E2), but when a choice is made from that menu, the client sends E6 01 +// instead of 10. In case 03, the flow is similar, but the client sends E7 +// instead of E6 01. +// newserv responds with a standard ship info box (11), but this seems not to be +// intended since it partially overlaps some windows. + +// E2 (S->C): Tournament entry list (Episode 3) +// Client may send 09 commands if the player presses X. It's not clear what the +// server should respond with in this case. +// If the player selects an entry slot, client will respond with a long-form 10 +// command (the Flag03 variant); in this case, unknown_a1 is the team name, and +// password is the team password. The server should respond to that with a CC +// command. + +struct S_TournamentEntryList_Ep3_E2 { + le_uint16_t players_per_team = 0; + le_uint16_t unused = 0; + struct Entry { + le_uint32_t menu_id = 0; + le_uint32_t item_id = 0; + uint8_t unknown_a1 = 0; + // If locked is nonzero, a lock icon appears next to this team and the + // player is prompted for a password if they select this team. + uint8_t locked = 0; + // State values: + // 00 = empty (team_name is ignored; entry is selectable) + // 01 = present, joinable (team_name renders in white) + // 02 = present, finalized (team_name renders in yellow) + // If state is any other value, the entry renders as if its state were 02, + // but cannot be selected at all (the menu cursor simply skips over it). + uint8_t state = 0; + uint8_t unknown_a2 = 0; + pstring name; + } __packed_ws__(Entry, 0x2C); + parray entries; +} __packed_ws__(S_TournamentEntryList_Ep3_E2, 0x584); + +// E2 (S->C): Set system file contents (BB) +// See PSOBBFullSystemFile in SaveFileFormats.hh for format + +// E3 (S->C): Game or tournament info (Episode 3) +// The header.flag argument determines which fields are valid (and which panes +// should be shown in the information window). The values are: +// flag=00: Opponents pane only +// flag=01: Opponents and Rules panes +// flag=02: Rules and bracket panes (the bracket pane uses the tournament's name +// as its title) +// flag=03: Opponents, Rules, and bracket panes +// flag=04: Spectators and Opponents pane +// flag=05: Spectators, Opponents, Rules, and bracket panes +// Sending other values in the header.flag field results in a blank info window +// with unintended strings appearing in the window title. +// Presumably the cases above would be used in different scenarios, probably: +// 00: When inspecting a non-tournament game with no battle in progress +// 01: When inspecting a non-tournament game with a battle in progress +// 02: When inspecting tournaments that have not yet started +// 03: When inspecting a tournament match +// 04: When inspecting a non-tournament spectator team +// 05: When inspecting a tournament spectator team +// The 00, 01, and 04 cases don't really make sense, because the E1 command is +// more appropriate for inspecting non-tournament games. + +template +struct S_TournamentGameDetailsBaseT_Ep3_E3 { + // These fields are used only if the Rules pane is shown + /* 0000 */ pstring name; + /* 0020 */ pstring map_name; + /* 0040 */ RulesT rules; + + // This field is used only if the bracket pane is shown + struct BracketEntry { + le_uint16_t win_count = 0; + le_uint16_t is_active = 0; + pstring team_name; + parray unused; + } __packed_ws__(BracketEntry, 0x24); + /* 0054 */ parray bracket_entries; + + // This field is used only if the Opponents pane is shown. If players_per_team + // is 2, all fields are shown; if player_per_team is 1, team_name and + // players[1] is ignored (only players[0] is shown). + struct PlayerEntry { + pstring name; + pstring description; // Usually something like "RAmarl CLv24 E" + } __packed_ws__(PlayerEntry, 0x30); + struct TeamEntry { + pstring team_name; + parray players; + } __packed_ws__(TeamEntry, 0x70); + /* 04D4 */ parray team_entries; + + /* 05B4 */ le_uint16_t num_bracket_entries = 0; + /* 05B6 */ le_uint16_t players_per_team = 0; + /* 05B8 */ le_uint16_t unknown_a4 = 0; + /* 05BA */ le_uint16_t num_spectators = 0; + /* 05BC */ parray spectator_entries; + /* 073C */ +} __packed__; +using S_TournamentGameDetails_Ep3NTE_E3 = S_TournamentGameDetailsBaseT_Ep3_E3; +using S_TournamentGameDetails_Ep3_E3 = S_TournamentGameDetailsBaseT_Ep3_E3; +check_struct_size(S_TournamentGameDetails_Ep3NTE_E3, 0x734); +check_struct_size(S_TournamentGameDetails_Ep3_E3, 0x73C); + +// E3 (C->S): Player preview request (BB) + +struct C_PlayerPreviewRequest_BB_E3 { + le_int32_t character_index = 0; + le_uint32_t unused = 0; +} __packed_ws__(C_PlayerPreviewRequest_BB_E3, 0x08); + +// E4: CARD lobby battle table state (Episode 3) +// When client sends an E4, server should respond with another E4 (but these +// commands have different formats). +// When the client has received an E4 command in which all entries have state 0 +// or 2, the client will stop the player from moving and show a message saying +// that the game will begin shortly. The server should send a 64 command shortly +// thereafter. + +// header.flag = seated state (1 = present, 0 = leaving) +struct C_CardBattleTableState_Ep3_E4 { + le_uint16_t table_number = 0; + le_uint16_t seat_number = 0; +} __packed_ws__(C_CardBattleTableState_Ep3_E4, 4); + +// header.flag = table number +struct S_CardBattleTableState_Ep3_E4 { + struct Entry { + // State values: + // 0 = no player present + // 1 = player present, not confirmed + // 2 = player present, confirmed + // 3 = player present, declined + le_uint16_t state = 0; + le_uint16_t unknown_a1 = 0; + le_uint32_t guild_card_number = 0; + } __packed_ws__(Entry, 8); + parray entries; +} __packed_ws__(S_CardBattleTableState_Ep3_E4, 0x20); + +// E4 (S->C): Player choice or no player present (BB) + +struct S_ApprovePlayerChoice_BB_00E4 { + le_int32_t character_index = 0; + le_uint32_t result = 0; // 1 = approved +} __packed_ws__(S_ApprovePlayerChoice_BB_00E4, 8); + +struct S_PlayerPreview_NoPlayer_BB_00E4 { + le_int32_t character_index = 0; + le_uint32_t error = 0; // 2 = no player present +} __packed_ws__(S_PlayerPreview_NoPlayer_BB_00E4, 8); + +// E5 (C->S): Confirm CARD lobby battle table choice (Episode 3) +// header.flag specifies whether the client answered "Yes" (1) or "No" (0). + +struct S_CardBattleTableConfirmation_Ep3_E5 { + le_uint16_t table_number = 0; + le_uint16_t seat_number = 0; +} __packed_ws__(S_CardBattleTableConfirmation_Ep3_E5, 4); + +// E5 (S->C): Player preview (BB) +// E5 (C->S): Create character (BB) + +struct SC_PlayerPreview_CreateCharacter_BB_00E5 { + le_int32_t character_index = 0; + PlayerDispDataBBPreview preview; +} __packed_ws__(SC_PlayerPreview_CreateCharacter_BB_00E5, 0x80); + +// E6 (C->S): Spectator team control (Episode 3) + +// With header.flag == 0, this command has no arguments and is used for +// requesting the spectator team list. The server responds with an E6 command. + +// With header.flag == 1, this command is used for joining a tournament +// spectator team. The following arguments are given in this form: + +struct C_JoinSpectatorTeam_Ep3_E6_Flag01 { + le_uint32_t menu_id = 0; + le_uint32_t item_id = 0; +} __packed_ws__(C_JoinSpectatorTeam_Ep3_E6_Flag01, 8); + +// E6 (S->C): Spectator team list (Episode 3) +// Same format as 08 command. + +// E6 (S->C): Set guild card number and update client config (BB) +// This command sets the player's guild card number. During the data server +// phase, it also sets the client config and enabled features (these fields are +// ignored during the game server phase). + +struct S_ClientInit_BB_00E6 { + le_uint32_t error_code = 0; + le_uint32_t player_tag = 0x00010000; + le_uint32_t guild_card_number = 0; + // If security_token is zero, the client scrambles client_config before + // sending it back in a later 93 command. See scramble_bb_security_data in + // ReceiveCommands.cc for details on how this is done. + le_uint32_t security_token = 0; + parray client_config; + uint8_t can_create_team = 1; + uint8_t episode_4_unlocked = 1; + parray unused; +} __packed_ws__(S_ClientInit_BB_00E6, 0x3C); + +// E7 (C->S): Create spectator team (Episode 3) +// This command is used to create speectator teams for both tournaments and +// regular games. The server should be able to tell these cases apart by the +// menu and/or item ID. + +struct C_CreateSpectatorTeam_Ep3_E7 { + le_uint32_t menu_id = 0; + le_uint32_t item_id = 0; + pstring name; + pstring password; + le_uint32_t unused = 0; +} __packed_ws__(C_CreateSpectatorTeam_Ep3_E7, 0x2C); + +// E7 (S->C): Tournament entry list for spectating (Episode 3) +// Same format as E2 command. + +// E7: Sync save files (BB) + +struct SC_SyncSaveFiles_BB_E7 { + /* 0000 */ PSOBBCharacterFile char_file; + /* 2EA4 */ PSOBBFullSystemFile system_file; + /* 3994 */ +} __packed_ws__(SC_SyncSaveFiles_BB_E7, 0x3994); + +// E8 (S->C): Join spectator team (Episode 3) +// header.flag = player count (including spectators) +// The client will crash if leader_id == client_id. Presumably one of the +// primary game's players should be the leader (this is what newserv does). + +struct S_JoinSpectatorTeam_Ep3_E8 { + /* 0000 */ parray variations; // unused + struct PlayerEntry { + /* 0000 */ PlayerLobbyDataDCGC lobby_data; + /* 0020 */ PlayerInventory inventory; + /* 036C */ PlayerDispDataDCPCV3 disp; + /* 043C */ + } __packed_ws__(PlayerEntry, 0x43C); + /* 0080 */ parray players; + /* 1170 */ uint8_t client_id = 0; + /* 1171 */ uint8_t leader_id = 0; + /* 1172 */ uint8_t disable_udp = 1; + /* 1173 */ uint8_t difficulty = 0; + /* 1174 */ uint8_t battle_mode = 0; + /* 1175 */ uint8_t event = 0; + /* 1176 */ uint8_t section_id = 0; + /* 1177 */ uint8_t challenge_mode = 0; + /* 1178 */ le_uint32_t rare_seed = 0; + /* 117C */ uint8_t episode = 0; + /* 117D */ parray unused; + struct SpectatorEntry { + // It seems that at some point Sega intended to show each player's rank in + // spectator teams. The unused1 and unused3 fields are intended for the + // player's encrypted rank text and rank color (according to old Sega logs), + // but the client ignores them. It's not clear what unused4 may have been + // for, but the client also completely ignores it. + /* 00 */ le_uint32_t player_tag = 0; + /* 04 */ le_uint32_t guild_card_number = 0; + /* 08 */ pstring name; + /* 18 */ pstring unused1; + /* 28 */ uint8_t present = 0; + /* 29 */ uint8_t unused2 = 0; + /* 2A */ le_uint16_t level = 0; + /* 2C */ le_uint32_t unused3 = 0xFFFFFFFF; + /* 30 */ le_uint32_t name_color = 0xFFFFFFFF; // ARGB8888 + /* 34 */ parray unused4; + /* 38 */ + } __packed_ws__(SpectatorEntry, 0x38); + // Somewhat misleadingly, this array also includes the players actually in the + // battle - they appear in the first positions. Presumably the first 4 are + // always for battlers, and the last 8 are always for spectators. + /* 1180 */ parray entries; + /* 1420 */ pstring spectator_team_name; + // This field doesn't appear to be actually used by the game, but some servers + // send it anyway (and the game ignores it) + /* 1440 */ parray spectator_players; + /* 3620 */ +} __packed_ws__(S_JoinSpectatorTeam_Ep3_E8, 0x3620); + +// E8 (C->S): Guild card commands (BB) + +// 01E8 (C->S): Check guild card file checksum + +// This struct is for documentation purposes only; newserv ignores the contents +// of this command. +struct C_GuildCardChecksum_01E8 { + le_uint32_t checksum = 0; + le_uint32_t unused = 0; +} __packed_ws__(C_GuildCardChecksum_01E8, 8); + +// 02E8 (S->C): Accept/decline guild card file checksum +// If needs_update is nonzero, the client will request the guild card file by +// sending an 03E8 command. If needs_update is zero, the client will skip +// downloading the guild card file and send a 04EB command (requesting the +// stream file) instead. + +struct S_GuildCardChecksumResponse_BB_02E8 { + le_uint32_t needs_update = 0; + le_uint32_t unused = 0; +} __packed_ws__(S_GuildCardChecksumResponse_BB_02E8, 8); + +// 03E8 (C->S): Request guild card file +// No arguments +// Server should send the guild card file data using DC commands. + +// 04E8 (C->S): Add guild card +// Format is GuildCardBB (see PlayerSubordinates.hh) + +// 05E8 (C->S): Delete guild card + +struct C_DeleteGuildCard_BB_05E8_08E8 { + le_uint32_t guild_card_number = 0; +} __packed_ws__(C_DeleteGuildCard_BB_05E8_08E8, 4); + +// 06E8 (C->S): Update (overwrite) guild card +// Note: This command is also sent when the player writes a comment on their own +// guild card. +// Format is GuildCardBB (see PlayerSubordinates.hh) + +// 07E8 (C->S): Add blocked user +// Format is GuildCardBB (see PlayerSubordinates.hh) + +// 08E8 (C->S): Delete blocked user +// Same format as 05E8. + +// 09E8 (C->S): Write comment + +struct C_WriteGuildCardComment_BB_09E8 { + le_uint32_t guild_card_number = 0; + pstring comment; +} __packed_ws__(C_WriteGuildCardComment_BB_09E8, 0xB4); + +// 0AE8 (C->S): Swap positions of guild cards in list + +struct C_SwapGuildCardPositions_BB_0AE8 { + le_uint32_t guild_card_number1 = 0; + le_uint32_t guild_card_number2 = 0; +} __packed_ws__(C_SwapGuildCardPositions_BB_0AE8, 8); + +// E9 (S->C): Remove player from spectator team (Episode 3) +// Same format as 66/69 commands. Like 69 (and unlike 66), the disable_udp field +// is unused in command E9. When a spectator leaves a spectator team, the +// primary players should receive a 6xB4x52 command to update their spectator +// counts. + +// EA (S->C): Timed message box (Episode 3) +// The message appears in the upper half of the screen; the box is as wide as +// the 1A/D5 box but is vertically shorter. The box cannot be dismissed or +// interacted with by the player in any way; it disappears by itself after the +// given number of frames. +// header.flag appears to be relevant - the handler's behavior is different if +// it's 1 (vs. any other value). There don't seem to be any in-game behavioral +// differences though. + +struct S_TimedMessageBoxHeader_Ep3_EA { + le_uint32_t duration = 0; // In frames; 30 frames = 1 second + // Message data follows here (up to 0x1000 chars) +} __packed_ws__(S_TimedMessageBoxHeader_Ep3_EA, 4); + +// EA: Team control (BB) + +// 01EA (C->S): Create team + +struct C_CreateTeam_BB_01EA { + pstring name; +} __packed_ws__(C_CreateTeam_BB_01EA, 0x20); + +// 02EA (S->C): Create team result +// No arguments except header.flag, which specifies the error code. Values: +// 0 = success +// 1 = generic error +// 2 = name already registered +// 3 = generic error +// 4 = generic error +// 5 = generic error +// 6 = generic error +// Anything else = command is ignored + +// 03EA (C->S): Add team member + +struct C_AddOrRemoveTeamMember_BB_03EA_05EA { + le_uint32_t guild_card_number = 0; +} __packed_ws__(C_AddOrRemoveTeamMember_BB_03EA_05EA, 4); + +// 04EA (S->C): Add team member result +// No arguments except header.flag, which specifies the error code. Values: +// 0 = success +// 5 = team is full +// Anything else = generic error + +// 05EA (C->S): Remove team member +// Same format as 03EA. + +// 06EA (S->C): Remove team member result +// No arguments except header.flag, which specifies the error code. 0 means +// success, but it's not known what any other values mean. The client expects +// the error code to be less than 7. + +// 07EA: Team chat + +struct SC_TeamChat_BB_07EA { + pstring sender_name; + // Text follows here. The message is truncated by the client if it is longer + // than 0x8F wchar_ts. +} __packed_ws__(SC_TeamChat_BB_07EA, 0x20); + +// 08EA (C->S): Get team member list +// No arguments + +// 09EA (S->C): Team member list + +struct S_TeamMemberList_BB_09EA { + le_uint32_t entry_count = 0; + struct Entry { + // This is displayed as "<%04d> %s" % (rank, name) + le_uint32_t rank = 0; + le_uint32_t privilege_level = 0; // 0x10 or 0x20 = green, 0x30 = blue, 0x40 = red, anything else = white + le_uint32_t guild_card_number = 0; + pstring name; + } __packed_ws__(Entry, 0x2C); + // Variable-length field: + // Entry entries[entry_count]; +} __packed_ws__(S_TeamMemberList_BB_09EA, 4); + +// 0CEA (S->C): Unknown +// The client appears to ignore this command. + +struct S_Unknown_BB_0CEA { + parray unknown_a1; + // Text follows here +} __packed_ws__(S_Unknown_BB_0CEA, 0x20); + +// 0DEA (C->S): Get team name +// No arguments + +// 0EEA (S->C): Team name + +struct S_TeamName_BB_0EEA { + parray unused; + pstring team_name; +} __packed_ws__(S_TeamName_BB_0EEA, 0x30); + +// 0FEA (C->S): Set team flag +// The client also accepts this command but completely ignores it. + +struct C_SetTeamFlag_BB_0FEA { + parray flag_data; +} __packed_ws__(C_SetTeamFlag_BB_0FEA, 0x800); + +// 10EA: Delete team (C->S) and result (S->C) +// No arguments (C->S) +// No arguments except header.flag (S->C) + +// 11EA: Change team member privilege level +// The format below is used only when the client sends this command; when the +// server sends it, only header.flag is used. As with various other team +// commands, header.flag specifies the error code in this case. +// header.flag specifies the new privilege level for the specified team member. +// Known values: 0 = normal, 0x30 = leader, 0x40 = master + +struct C_ChangeTeamMemberPrivilegeLevel_BB_11EA { + le_uint32_t guild_card_number = 0; +} __packed_ws__(C_ChangeTeamMemberPrivilegeLevel_BB_11EA, 4); + +// 12EA (S->C): Team membership information +// If the client is not in a team, all fields should be zero. + +struct S_TeamMembershipInformation_BB_12EA { + le_uint32_t unknown_a1 = 0; + le_uint32_t guild_card_number = 0; + le_uint32_t team_id = 0; + le_uint32_t unknown_a4 = 0; + le_uint32_t unknown_a6 = 0; + uint8_t privilege_level = 0; + uint8_t team_member_count = 0; + uint8_t unknown_a8 = 0; + uint8_t unknown_a9 = 0; + pstring team_name; +} __packed_ws__(S_TeamMembershipInformation_BB_12EA, 0x38); + +// 13EA: Team info for lobby players +// header.flag specifies the number of entries. + +struct S_TeamInfoForPlayer_BB_13EA_15EA_Entry { + // The client uses the first four of these to determine if the player is in a + // team or not - if they are all zero, the player is not in a team. + /* 0000 */ le_uint32_t guild_card_number = 0; + /* 0004 */ le_uint32_t team_id = 0; + /* 0008 */ le_uint32_t reward_flags = 0; + /* 000C */ le_uint32_t unknown_a6 = 0; + /* 0010 */ uint8_t privilege_level = 0; + /* 0011 */ uint8_t team_member_count = 0; + /* 0012 */ uint8_t unknown_a8 = 0; + /* 0013 */ uint8_t unknown_a9 = 0; + /* 0014 */ pstring team_name; + /* 0034 */ le_uint32_t guild_card_number2 = 0; + /* 0038 */ le_uint32_t lobby_client_id = 0; + /* 003C */ pstring player_name; + /* 005C */ parray flag_data; + /* 085C */ +} __packed_ws__(S_TeamInfoForPlayer_BB_13EA_15EA_Entry, 0x85C); + +// 14EA (C->S): Get team info for lobby players +// No arguments. Client always sends 1 in the header.flag field. + +// 15EA (S->C): Team info for lobby players +// header.flag specifies the number of entries. The entry format appears to be +// the same as for the 13EA command. + +// 16EA (S->C): Transfer item via Simple Mail result +// No arguments except header.flag, which is 0 if the transfer failed and +// nonzero if it succeeded. + +// 18EA: Intra-team ranking information +// No arguments (C->S) + +struct S_IntraTeamRanking_BB_18EA { + /* 0000 */ le_uint32_t ranking_points = 0; + /* 0004 */ le_uint32_t unknown_a2 = 0; + /* 0008 */ le_uint32_t points_remaining = 0; + /* 000C */ le_uint32_t num_entries = 1; + struct Entry { + /* 00 */ le_uint32_t rank = 0; + /* 04 */ le_uint32_t privilege_level = 0; + /* 08 */ le_uint32_t guild_card_number = 0; + /* 0C */ pstring player_name; + /* 2C */ le_uint32_t points = 0; + /* 30 */ + } __packed_ws__(Entry, 0x30); + // Variable-length field: + /* 0010 */ // Entry entries[num_entries]; +} __packed_ws__(S_IntraTeamRanking_BB_18EA, 0x10); + +// 19EA: Team reward list +// No arguments (C->S) + +struct S_TeamRewardList_BB_19EA_1AEA { + le_uint32_t num_entries; + struct Entry { + /* 0000 */ pstring name; + /* 0080 */ pstring description; + /* 0180 */ le_uint32_t team_points = 0; + /* 0184 */ le_uint32_t reward_id = 0; + /* 0188 */ + } __packed_ws__(Entry, 0x188); + // Variable length field: + // Entry entries[num_entries]; +} __packed_ws__(S_TeamRewardList_BB_19EA_1AEA, 4); + +// 1AEA: Team rewards available for purchase +// Same format as 19EA. + +// 1BEA (C->S): Buy team reward +// No arguments except header.flag, which specifies a reward_id from a preceding +// 1AEA command. + +// 1CEA: Cross-team ranking information +// No arguments when sent by the client. + +struct S_CrossTeamRanking_BB_1CEA { + le_uint32_t num_entries; + struct Entry { + /* 00 */ pstring team_name; + /* 20 */ le_uint32_t team_points = 0; + /* 24 */ le_uint32_t unknown_a1 = 0; + /* 28 */ + } __packed_ws__(Entry, 0x28); + // Variable length field: + // Entry entries[num_entries]; +} __packed_ws__(S_CrossTeamRanking_BB_1CEA, 4); + +// 1DEA (S->C): Update team rewards bitmask +// header.flag specifies the new rewards bitmask. + +// 1EEA (C->S): Rename team +// header.flag is used, but it's unknown what the value means. + +struct C_RenameTeam_BB_1EEA { + pstring new_team_name; +} __packed_ws__(C_RenameTeam_BB_1EEA, 0x20); + +// 1FEA (S->C): Rename team result +// This command behaves like 02EA, but is sent in response to 1EEA instead. + +// 20EA: Unknown +// header.flag is used, but no other arguments. When sent by the server, +// header.flag is an error code, similar to various other result commands in +// this section. + +// EB (S->C): Add player to spectator team (Episode 3) +// Same format and usage as 65 and 68 commands, but sent to spectators in a +// spectator team. +// This command is used to add both primary players and spectators - if the +// client ID in .lobby_data is 0-3, it's a primary player, otherwise it's a +// spectator. (In the case of a primary player joining, the other primary +// players in the game receive a 65 command rather than an EB command to notify +// them of the joining player; in the case of a joining spectator, the primary +// players receive a 6xB4x52 instead.) + +// 01EB (S->C): Send stream file index (BB) + +// Command is a list of these; header.flag is the entry count. +struct S_StreamFileIndexEntry_BB_01EB { + le_uint32_t size = 0; + le_uint32_t checksum = 0; // CRC32 of file data + le_uint32_t offset = 0; // offset in stream (== sum of all previous files' sizes) + pstring filename; +} __packed_ws__(S_StreamFileIndexEntry_BB_01EB, 0x4C); + +// 02EB (S->C): Send stream file chunk (BB) + +struct S_StreamFileChunk_BB_02EB { + le_uint32_t chunk_index = 0; + parray data; +} __packed_ws__(S_StreamFileChunk_BB_02EB, 0x6804); + +// 03EB (C->S): Request a specific stream file chunk +// header.flag is the chunk index. Server should respond with a 02EB command. + +// 04EB (C->S): Request stream file header +// No arguments +// Server should respond with a 01EB command. + +// EC (C->S): Create game (Episode 3) +// Same format as C1; some fields are unused (e.g. episode, difficulty). + +// EC (C->S): Leave character select (BB) + +struct C_LeaveCharacterSelect_BB_00EC { + // Reason codes: + // 0 = canceled + // 1 = recreate character + // 2 = dressing room + le_uint32_t reason = 0; +} __packed_ws__(C_LeaveCharacterSelect_BB_00EC, 4); + +// ED (S->C): Force leave lobby/game (Episode 3) +// No arguments +// This command forces the client out of the game or lobby they're currently in +// and sends them to the lobby. If the client is in a lobby (and not a game), +// the client sends a 98 in response as if they were in a game. Curiously, the +// client also sends a meseta transaction (BA) with a value of zero before +// sending an 84 to be added to a lobby. This is used when a spectator team is +// disbanded because the target game ends. + +// ED (C->S): Update save file data (BB) +// There are several subcommands (noted in the structs below) that each update a +// specific kind of data. +// TODO: Actually define these structures and don't just treat them as raw data + +struct C_UpdateOptionFlags_BB_01ED { + le_uint32_t option_flags = 0; +} __packed_ws__(C_UpdateOptionFlags_BB_01ED, 4); + +struct C_UpdateSymbolChats_BB_02ED { + parray symbol_chats; +} __packed_ws__(C_UpdateSymbolChats_BB_02ED, 0x4E0); + +struct C_UpdateChatShortcuts_BB_03ED { + parray chat_shortcuts; +} __packed_ws__(C_UpdateChatShortcuts_BB_03ED, 0xA40); + +struct C_UpdateKeyConfig_BB_04ED { + parray key_config; +} __packed_ws__(C_UpdateKeyConfig_BB_04ED, 0x16C); + +struct C_UpdatePadConfig_BB_05ED { + parray pad_config; +} __packed_ws__(C_UpdatePadConfig_BB_05ED, 0x38); + +struct C_UpdateTechMenu_BB_06ED { + parray tech_menu; +} __packed_ws__(C_UpdateTechMenu_BB_06ED, 0x28); + +struct C_UpdateCustomizeMenu_BB_07ED { + parray customize; +} __packed_ws__(C_UpdateCustomizeMenu_BB_07ED, 0xE8); + +struct C_UpdateChallengeRecords_BB_08ED { + PlayerRecordsChallengeBB records; +} __packed_ws__(C_UpdateChallengeRecords_BB_08ED, 0x140); + +// EE: Trade cards (Episode 3) +// This command has different forms depending on the header.flag value; the flag +// values match the command numbers from the Episodes 1&2 trade window sequence. +// The sequence of events with the EE command also matches that of the Episodes +// 1&2 trade window; see the description of the D0 command above for details. + +// EE D0 (C->S): Begin trade +struct SC_TradeCards_Ep3_EE_FlagD0_FlagD3 { + le_uint16_t target_client_id = 0; + le_uint16_t entry_count = 0; + struct Entry { + le_uint32_t card_type = 0; + le_uint32_t count = 0; + } __packed_ws__(Entry, 8); + parray entries; +} __packed_ws__(SC_TradeCards_Ep3_EE_FlagD0_FlagD3, 0x24); + +// EE D1 (S->C): Advance trade state +struct S_AdvanceCardTradeState_Ep3_EE_FlagD1 { + le_uint32_t unused = 0; +} __packed_ws__(S_AdvanceCardTradeState_Ep3_EE_FlagD1, 4); + +// EE D2 (C->S): Trade can proceed +// No arguments + +// EE D3 (S->C): Execute trade +// Same format as EE D0 + +// EE D4 (C->S): Trade failed +// EE D4 (S->C): Trade complete + +struct S_CardTradeComplete_Ep3_EE_FlagD4 { + le_uint32_t success = 0; // 0 = failed, 1 = success, anything else = invalid +} __packed_ws__(S_CardTradeComplete_Ep3_EE_FlagD4, 4); + +// EE (S->C): Scrolling message (BB) +// Same format as 01. The message appears at the top of the screen and slowly +// scrolls to the left. The maximum length of the message is 0x400 bytes (0x200 +// UTF-16 characters). + +// EF (C->S): Join card auction (Episode 3) +// When a card auction is ready to begin, the leader sends this command to +// request the card list. The server then sends an EF command to all players +// to start the auction. + +// EF (S->C): Start card auction (Episode 3) + +struct S_StartCardAuction_Ep3_EF { + le_uint16_t points_available = 0; + le_uint16_t unused = 0; + struct Entry { + le_uint16_t card_id = 0xFFFF; // Must be < 0x02F1 + le_uint16_t min_price = 0; // Must be > 0 and < 100 + } __packed_ws__(Entry, 4); + parray entries; +} __packed_ws__(S_StartCardAuction_Ep3_EF, 0x54); + +// EF (S->C): Set or disable shutdown command (BB) +// All variants of EF except 00EF cause the given Windows shell command to be +// run (via ShellExecuteA) just before the game exits normally. There can be at +// most one shutdown command at a time; a later EF command will overwrite the +// previous EF command's effects. The 00EF command deletes the previous shutdown +// command if any was present, causing no command to run when the game closes. +// There is no indication to the player when a shutdown command has been set. + +// This command is likely just a vestigial debugging feature that Sega left in, +// but it presents a fairly obvious security risk. There is no way for the +// server to know whether an EF command it sent has actually executed on the +// client, so newserv's proxy unconditionally blocks this command. + +struct S_SetShutdownCommand_BB_01EF { + pstring command; +} __packed_ws__(S_SetShutdownCommand_BB_01EF, 0x200); + +// F0 (S->C): Force update player lobby data (BB) +// Format is PlayerLobbyDataBB (in PlayerSubordinates.hh). This command +// overwrites the lobby data for the player given by .client_id without +// reloading the game or lobby. + +// This command probably exists to handle cases like the following: +// 1. Player A is in a team and is not the team master. Player A creates a game. +// 2. The master of the team changes to player B during this game. +// 3. Player B then joins the game that A is in. +// Some effects (e.g. Commander Blade) depend on the team master ID in the +// PlayerLobbyDataBB structure, and this is the only way to update that +// structure without reloading the lobby or game. If this command did not exist, +// then player A would not know that B was the master when they join the game, +// so A would not see the bonus from Commander Blade if B uses it. + +// F1: Invalid command +// F2: Invalid command +// F3: Invalid command +// F4: Invalid command +// F5: Invalid command +// F6: Invalid command +// F7: Invalid command +// F8: Invalid command +// F9: Invalid command +// FA: Invalid command +// FB: Invalid command +// FC: Invalid command +// FD: Invalid command +// FE: Invalid command +// FF: Invalid command + +// Removed commands + +// There is evidence that some commands and features were fully removed from +// PSO at some point. + +// There is a command named RcvGamePause in all DC versions of PSO, but its +// handler function is missing. It's likely there was a way to actually pause +// the game during early development, but it was removed, likely because it'd +// be a fairly poor player experience. + +// There are two commands named SndGameStatus and SndGameCondition in the DC +// versions, but their sender functions are missing in all versions. It's not +// clear what exactly they would have sent, or when they would have been +// triggered. + +// Finally, there is a function named SndPsoGetText which was in DCv1 and DCv2, +// but not in DC NTE or the December 2000 prototype. This may have been a way +// for the server to prompt the user to input some text. As with the other +// unused functions, the code was removed, leaving only the function name. + +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +// GAME SUBCOMMANDS //////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +// The game subcommands are used in commands 60, 62, 6C, 6D, C9, and CB. These +// are laid out similarly as above. These structs start with G_ to indicate that +// they are (usually) bidirectional, and are (usually) generated by clients and +// consumed by clients. Generally in newserv source, these commands are referred +// to as (for example) 6x02, etc., referencing the fact that they are almost +// always sent via a command starting with the hex digit 6. + +// All game subcommands have the same header format, which is one of: +// - XX SS ... +// - XX 00 ?? ?? TT TT TT TT ... +// where X is the subcommand number (e.g. in 6xA2, it would be A2), S is the +// size in words of the entire subcommand (that is, overall size in bytes / 4), +// and T is the overall size in bytes. The second form is generally only used +// when the overall size in bytes is 0x400 or longer (so the S field doesn't +// suffice to describe its length), but it may also be used in some cases where +// the subcommand is shorter. +// Multiple subcommands may be sent in the same 6x command. It seems the client +// never sends commands like this, but newserv generates commands containing +// multiple subcommands in some situations (for example, the implementation of +// infinite HP does this). +// If any subcommand or group thereof is longer than 0x400 bytes, the 6C or 6D +// commands must be used. The 60 and 62 commands exhibit undefined behavior if +// this limit is exceeded. + +// Some subcommands are "protected" on V3 and later (not including GC NTE); +// these commands are blocked by the client if they affect the local player. If +// a V3 or later client receives a protected subcommand that would affect its +// own player, it instead ignores the entire subcommand. This means that the +// server or other players cannot send these subcommands to affect other +// players; they can only send these commands to inform other clients about +// changes or actions from their own player. +// The protected subcommands are marked (protected) in the listings below. + +// These common structures are used my many subcommands. +struct G_ClientIDHeader { + uint8_t subcommand = 0; + uint8_t size = 0; + le_uint16_t client_id = 0; // <= 12 +} __packed_ws__(G_ClientIDHeader, 4); +struct G_EnemyIDHeader { + uint8_t subcommand = 0; + uint8_t size = 0; + le_uint16_t enemy_id = 0; // In [0x1000, 0x4000); not the same as enemy_index! +} __packed_ws__(G_EnemyIDHeader, 4); +struct G_ObjectIDHeader { + uint8_t subcommand = 0; + uint8_t size = 0; + le_uint16_t object_id = 0; // >= 0x4000, != 0xFFFF +} __packed_ws__(G_ObjectIDHeader, 4); +struct G_ParameterHeader { + uint8_t subcommand = 0; + uint8_t size = 0; + le_uint16_t param = 0; +} __packed_ws__(G_ParameterHeader, 4); +struct G_UnusedHeader { + uint8_t subcommand = 0; + uint8_t size = 0; + le_uint16_t unused = 0; +} __packed_ws__(G_UnusedHeader, 4); + +template +struct G_ExtendedHeaderT { + HeaderT basic_header; + le_uint32_t size = 0; +} __packed__; + +// 6x00: Invalid subcommand +// 6x01: Invalid subcommand + +// 6x02: Unknown +// This subcommand is completely ignored on V3. +// TODO: It is not ignored on V1 and V2. Figure out what it does and document it. + +// 6x03: Unknown +// This subcommand is completely ignored on V3. +// TODO: It is not ignored on V1 and V2. Figure out what it does and document it. + +// 6x04: Unknown + +struct G_Unknown_6x04 { + G_ClientIDHeader header; + le_uint16_t unknown_a1 = 0; + le_uint16_t unknown_a2 = 0; +} __packed_ws__(G_Unknown_6x04, 8); + +// 6x05: Switch state changed +// Some things that don't look like switches are implemented as switches using +// this subcommand. For example, when all enemies in a room are defeated, this +// subcommand is used to unlock the doors. +// Note: In the client, this is a subclass of 6x04, similar to how 6xA2 is a +// subclass of 6x60. + +struct G_SwitchStateChanged_6x05 { + // Note: header.object_id is 0xFFFF for room clear when all enemies defeated + G_ObjectIDHeader header; + // TODO: Some of these might be big-endian on GC; it only byteswaps + // switch_flag_num. Are the others actually uint16, or are they uint8[2]? + le_uint16_t unknown_a1 = 0; + le_uint16_t unknown_a2 = 0; + le_uint16_t switch_flag_num = 0; + uint8_t switch_flag_floor = 0; + // Only two bits in flags have meanings: + // 01 - set unlock flag (if not set, the flag is cleared instead) + // 02 - play room unlock sound if floor matches client's floor + uint8_t flags = 0; +} __packed_ws__(G_SwitchStateChanged_6x05, 0x0C); + +// 6x06: Send guild card + +struct G_SendGuildCard_DCNTE_6x06 { + G_UnusedHeader header; + GuildCardDCNTE guild_card; + uint8_t unused; +} __packed_ws__(G_SendGuildCard_DCNTE_6x06, 0x80); + +struct G_SendGuildCard_DC_6x06 { + G_UnusedHeader header; + GuildCardDC guild_card; + parray unused; +} __packed_ws__(G_SendGuildCard_DC_6x06, 0x84); + +struct G_SendGuildCard_PC_6x06 { + G_UnusedHeader header; + GuildCardPC guild_card; +} __packed_ws__(G_SendGuildCard_PC_6x06, 0xF4); + +struct G_SendGuildCard_GCNTE_6x06 { + G_UnusedHeader header; + GuildCardGCNTE guild_card; +} __packed_ws__(G_SendGuildCard_GCNTE_6x06, 0xA8); + +struct G_SendGuildCard_GC_6x06 { + G_UnusedHeader header; + GuildCardGC guild_card; +} __packed_ws__(G_SendGuildCard_GC_6x06, 0x94); + +struct G_SendGuildCard_XB_6x06 { + G_UnusedHeader header; + GuildCardXB guild_card; +} __packed_ws__(G_SendGuildCard_XB_6x06, 0x230); + +struct G_SendGuildCard_BB_6x06 { + G_UnusedHeader header; + GuildCardBB guild_card; +} __packed_ws__(G_SendGuildCard_BB_6x06, 0x10C); + +// 6x07: Symbol chat + +struct G_SymbolChat_6x07 { + G_UnusedHeader header; + le_uint32_t client_id = 0; + SymbolChat data; +} __packed_ws__(G_SymbolChat_6x07, 0x44); + +// 6x08: Invalid subcommand + +// 6x09: Unknown + +struct G_Unknown_6x09 { + G_EnemyIDHeader header; +} __packed_ws__(G_Unknown_6x09, 4); + +// 6x0A: Update enemy state + +template +struct G_UpdateEnemyStateT_6x0A { + G_EnemyIDHeader header; + le_uint16_t enemy_index = 0; // [0, 0xB50) + le_uint16_t total_damage = 0; + // Flags: + // 00000400 - should play hit animation + // 00000800 - is dead + typename std::conditional_t flags = 0; +} __packed__; +using G_UpdateEnemyState_GC_6x0A = G_UpdateEnemyStateT_6x0A; +using G_UpdateEnemyState_DC_PC_XB_BB_6x0A = G_UpdateEnemyStateT_6x0A; +check_struct_size(G_UpdateEnemyState_GC_6x0A, 0x0C); +check_struct_size(G_UpdateEnemyState_DC_PC_XB_BB_6x0A, 0x0C); + +// 6x0B: Update object state + +struct G_UpdateObjectState_6x0B { + G_ClientIDHeader header; + le_uint32_t flags = 0; + le_uint32_t object_index = 0; +} __packed_ws__(G_UpdateObjectState_6x0B, 0x0C); + +// 6x0C: Add condition (poison/slow/etc.) (protected on V3/V4) +// 6x0D: Remove condition (poison/slow/etc.) (protected on V3/V4) + +struct G_AddOrRemoveCondition_6x0C_6x0D { + G_ClientIDHeader header; + le_uint32_t unknown_a1 = 0; // Probably condition type + le_uint32_t unknown_a2 = 0; +} __packed_ws__(G_AddOrRemoveCondition_6x0C_6x0D, 0x0C); + +// 6x0E: Unknown (protected on V3/V4) + +struct G_Unknown_6x0E { + G_ClientIDHeader header; +} __packed_ws__(G_Unknown_6x0E, 4); + +// 6x0F: Invalid subcommand + +// 6x10: Unknown (not valid on Episode 3) + +struct G_Unknown_6x10_6x11_6x12_6x14 { + G_EnemyIDHeader header; + le_uint16_t unknown_a2 = 0; + le_uint16_t unknown_a3 = 0; + le_uint32_t unknown_a4 = 0; +} __packed_ws__(G_Unknown_6x10_6x11_6x12_6x14, 0x0C); + +// 6x11: Unknown (not valid on Episode 3) +// Same format as 6x10 + +// 6x12: Dragon boss actions (not valid on Episode 3) + +template +struct G_DragonBossActionsT_6x12 { + using F32T = typename std::conditional::type; + G_EnemyIDHeader header; + le_uint16_t unknown_a2 = 0; + le_uint16_t unknown_a3 = 0; + le_uint32_t unknown_a4 = 0; + F32T x = 0.0f; + F32T z = 0.0f; +} __packed__; +using G_DragonBossActions_DC_PC_XB_BB_6x12 = G_DragonBossActionsT_6x12; +using G_DragonBossActions_GC_6x12 = G_DragonBossActionsT_6x12; +check_struct_size(G_DragonBossActions_DC_PC_XB_BB_6x12, 0x14); +check_struct_size(G_DragonBossActions_GC_6x12, 0x14); + +// 6x13: De Rol Le boss actions (not valid on Episode 3) + +struct G_DeRolLeBossActions_6x13 { + G_EnemyIDHeader header; + le_uint16_t unknown_a2 = 0; + le_uint16_t unknown_a3 = 0; +} __packed_ws__(G_DeRolLeBossActions_6x13, 8); + +// 6x14: De Rol Le boss actions (not valid on Episode 3) +// Same format as 6x10 + +// 6x15: Vol Opt boss actions (not valid on Episode 3) + +struct G_VolOptBossActions_6x15 { + G_EnemyIDHeader header; + le_uint16_t unknown_a2 = 0; + le_uint16_t unknown_a3 = 0; + le_uint16_t unknown_a4 = 0; + le_uint16_t unknown_a5 = 0; +} __packed_ws__(G_VolOptBossActions_6x15, 0x0C); + +// 6x16: Vol Opt boss actions (not valid on Episode 3) + +struct G_VolOptBossActions_6x16 { + G_UnusedHeader header; + parray unknown_a2; + le_uint16_t unknown_a3 = 0; +} __packed_ws__(G_VolOptBossActions_6x16, 0x0C); + +// 6x17: Vol Opt phase 2 boss actions (not valid on Episode 3) + +struct G_VolOpt2BossActions_6x17 { + G_ClientIDHeader header; + le_float unknown_a2 = 0.0f; + le_float unknown_a3 = 0.0f; + le_float unknown_a4 = 0.0f; + le_uint32_t unknown_a5 = 0; +} __packed_ws__(G_VolOpt2BossActions_6x17, 0x14); + +// 6x18: Vol Opt phase 2 boss actions (not valid on Episode 3) + +struct G_VolOpt2BossActions_6x18 { + G_ClientIDHeader header; + parray unknown_a2; +} __packed_ws__(G_VolOpt2BossActions_6x18, 0x0C); + +// 6x19: Dark Falz boss actions (not valid on Episode 3) + +struct G_DarkFalzActions_6x19 { + G_EnemyIDHeader header; + le_uint16_t unknown_a2 = 0; + le_uint16_t unknown_a3 = 0; + le_uint32_t unknown_a4 = 0; + le_uint32_t unused = 0; +} __packed_ws__(G_DarkFalzActions_6x19, 0x10); + +// 6x1A: Invalid subcommand + +// 6x1B: Unknown (not valid on Episode 3) (protected on V3/V4) + +struct G_Unknown_6x1B { + G_ClientIDHeader header; +} __packed_ws__(G_Unknown_6x1B, 4); + +// 6x1C: Destroy NPC (protected on V3/V4) + +struct G_DestroyNPC_6x1C { + G_ClientIDHeader header; +} __packed_ws__(G_DestroyNPC_6x1C, 4); + +// 6x1D: Invalid subcommand +// 6x1E: Invalid subcommand + +// 6x1F: Set player floor and request positions + +struct G_SetPlayerFloor_DCNTE_6x1F { + G_ClientIDHeader header; +} __packed_ws__(G_SetPlayerFloor_DCNTE_6x1F, 4); + +struct G_SetPlayerFloor_6x1F { + G_ClientIDHeader header; + le_int32_t floor = 0; +} __packed_ws__(G_SetPlayerFloor_6x1F, 8); + +// 6x20: Set position (protected on V3/V4) +// Existing clients send this in response to a 6x1F command when a new client +// joins a lobby or game, so the new client knows where to place them. + +struct G_SetPosition_6x20 { + G_ClientIDHeader header; + le_int32_t floor = 0; + le_float x = 0.0f; + le_float y = 0.0f; + le_float z = 0.0f; + le_uint32_t unknown_a1 = 0; +} __packed_ws__(G_SetPosition_6x20, 0x18); + +// 6x21: Inter-level warp (protected on V3/V4) + +struct G_InterLevelWarp_6x21 { + G_ClientIDHeader header; + le_int32_t floor = 0; +} __packed_ws__(G_InterLevelWarp_6x21, 8); + +// 6x22: Set player invisible (protected on V3/V4) +// 6x23: Set player visible (protected on V3/V4) +// These are generally used while a player is in the process of changing floors. + +struct G_SetPlayerVisibility_6x22_6x23 { + G_ClientIDHeader header; +} __packed_ws__(G_SetPlayerVisibility_6x22_6x23, 4); + +// 6x24: Teleport player (protected on V3/V4) + +struct G_TeleportPlayer_6x24 { + G_ClientIDHeader header; + le_uint32_t unknown_a1 = 0; + le_float x = 0.0f; + le_float y = 0.0f; + le_float z = 0.0f; +} __packed_ws__(G_TeleportPlayer_6x24, 0x14); + +// 6x25: Equip item (protected on V3/V4) + +struct G_EquipItem_6x25 { + G_ClientIDHeader header; + le_uint32_t item_id = 0; + // Values here match the EquipSlot enum (in ItemData.hh) + le_uint32_t equip_slot = 0; +} __packed_ws__(G_EquipItem_6x25, 0x0C); + +// 6x26: Unequip item (protected on V3/V4) + +struct G_UnequipItem_6x26 { + G_ClientIDHeader header; + le_uint32_t item_id = 0; + le_uint32_t unused = 0; +} __packed_ws__(G_UnequipItem_6x26, 0x0C); + +// 6x27: Use item (protected on V3/V4) + +struct G_UseItem_6x27 { + G_ClientIDHeader header; + le_uint32_t item_id = 0; +} __packed_ws__(G_UseItem_6x27, 8); + +// 6x28: Feed MAG (protected on V3/V4) + +struct G_FeedMAG_6x28 { + G_ClientIDHeader header; + le_uint32_t mag_item_id = 0; + le_uint32_t fed_item_id = 0; +} __packed_ws__(G_FeedMAG_6x28, 0x0C); + +// 6x29: Delete inventory item (via bank deposit / sale / feeding MAG) (protected on V3 but not V4) +// This subcommand is also used for reducing the size of stacks - if amount is +// less than the stack count, the item is not deleted and its ID remains valid. + +struct G_DeleteInventoryItem_6x29 { + G_ClientIDHeader header; + le_uint32_t item_id = 0; + le_uint32_t amount = 0; +} __packed_ws__(G_DeleteInventoryItem_6x29, 0x0C); + +// 6x2A: Drop item (protected on V3/V4) + +struct G_DropItem_6x2A { + G_ClientIDHeader header; + le_uint16_t unknown_a1 = 0; // Should be 1... maybe amount? + le_uint16_t floor = 0; + le_uint32_t item_id = 0; + le_float x = 0.0f; + le_float y = 0.0f; + le_float z = 0.0f; +} __packed_ws__(G_DropItem_6x2A, 0x18); + +// 6x2B: Create item in inventory (e.g. via tekker or bank withdraw) (protected on V3/V4) +// On BB, the 6xBE command is used instead of 6x2B to create inventory items. + +struct G_CreateInventoryItem_DC_6x2B { + G_ClientIDHeader header; + ItemData item_data; +} __packed_ws__(G_CreateInventoryItem_DC_6x2B, 0x18); + +struct G_CreateInventoryItem_PC_V3_BB_6x2B : G_CreateInventoryItem_DC_6x2B { + uint8_t unused1 = 0; + uint8_t unknown_a2 = 0; + parray unused2 = 0; +} __packed_ws__(G_CreateInventoryItem_PC_V3_BB_6x2B, 0x1C); + +// 6x2C: Talk to NPC (protected on V3/V4) + +struct G_TalkToNPC_6x2C { + G_ClientIDHeader header; + le_uint16_t unknown_a1 = 0; + le_uint16_t unknown_a2 = 0; + le_float unknown_a3 = 0.0f; + le_float unknown_a4 = 0.0f; + le_float unknown_a5 = 0.0f; +} __packed_ws__(G_TalkToNPC_6x2C, 0x14); + +// 6x2D: Done talking to NPC (protected on V3/V4) + +struct G_EndTalkToNPC_6x2D { + G_ClientIDHeader header; +} __packed_ws__(G_EndTalkToNPC_6x2D, 4); + +// 6x2E: Set and/or clear player flags (protected on V3/V4) + +struct G_SetOrClearPlayerFlags_6x2E { + G_ClientIDHeader header; + le_uint32_t and_mask = 0; + le_uint32_t or_mask = 0; +} __packed_ws__(G_SetOrClearPlayerFlags_6x2E, 0x0C); + +// 6x2F: Change player HP + +struct G_ChangePlayerHP_6x2F { + G_UnusedHeader header; + le_uint32_t type = 0; // 0 = set HP, 1 = add/subtract HP, 2 = add/sub fixed HP + le_uint16_t amount = 0; + le_uint16_t client_id = 0; +} __packed_ws__(G_ChangePlayerHP_6x2F, 0x0C); + +// 6x30: Level up + +struct G_LevelUp_DCNTE_6x30 { + G_ClientIDHeader header; +} __packed_ws__(G_LevelUp_DCNTE_6x30, 4); + +struct G_LevelUp_6x30 { + G_ClientIDHeader header; + le_uint16_t atp = 0; + le_uint16_t mst = 0; + le_uint16_t evp = 0; + le_uint16_t hp = 0; + le_uint16_t dfp = 0; + le_uint16_t ata = 0; + le_uint16_t level = 0; + le_uint16_t unknown_a1 = 0; // Must be 0 or 1 +} __packed_ws__(G_LevelUp_6x30, 0x14); + +// 6x31: Resurrect player (protected on V3/V4) + +struct G_UseMedicalCenter_6x31 { + G_ClientIDHeader header; +} __packed_ws__(G_UseMedicalCenter_6x31, 4); + +// 6x32: Resurrect player (Medical Center) + +struct G_Unknown_6x32 { + G_UnusedHeader header; +} __packed_ws__(G_Unknown_6x32, 4); + +// 6x33: Resurrect player (with Moon Atomizer) (protected on V3/V4) + +struct G_RevivePlayer_6x33 { + G_ClientIDHeader header; + le_uint16_t client_id2 = 0; + le_uint16_t unused = 0; +} __packed_ws__(G_RevivePlayer_6x33, 8); + +// 6x34: Unknown +// This subcommand is completely ignored (at least, by PSO GC). + +// 6x35: Invalid subcommand + +// 6x36: Unknown (supported; game only) +// This subcommand is completely ignored (at least, by PSO GC). + +// 6x37: Photon blast (protected on V3/V4) + +struct G_PhotonBlast_6x37 { + G_ClientIDHeader header; + le_uint16_t unknown_a1 = 0; + le_uint16_t unused = 0; +} __packed_ws__(G_PhotonBlast_6x37, 8); + +// 6x38: Donate to photon blast (protected on V3/V4) + +struct G_Unknown_6x38 { + G_ClientIDHeader header; + le_uint16_t unknown_a1 = 0; + le_uint16_t unused = 0; +} __packed_ws__(G_Unknown_6x38, 8); + +// 6x39: Photon blast ready (protected on V3/V4) + +struct G_PhotonBlastReady_6x38 { + G_ClientIDHeader header; +} __packed_ws__(G_PhotonBlastReady_6x38, 4); + +// 6x3A: Unknown (supported; game only) (protected on V3/V4) + +struct G_Unknown_6x3A { + G_ClientIDHeader header; +} __packed_ws__(G_Unknown_6x3A, 4); + +// 6x3B: Unknown (supported; lobby & game) (protected on V3/V4) + +struct G_Unknown_6x3B { + G_ClientIDHeader header; +} __packed_ws__(G_Unknown_6x3B, 4); + +// 6x3C: Unknown (DCv1 and earlier) +// This command has a handler, but it does nothing, even on DC NTE. + +// 6x3D: Invalid subcommand + +// 6x3E: Stop moving (protected on V3/V4) + +struct G_StopAtPosition_6x3E { + G_ClientIDHeader header; + le_uint16_t unknown_a1 = 0; + le_uint16_t angle = 0; + le_int16_t floor = 0; + le_int16_t room = 0; + le_float x = 0.0f; + le_float y = 0.0f; + le_float z = 0.0f; +} __packed_ws__(G_StopAtPosition_6x3E, 0x18); + +// 6x3F: Set position (protected on V3/V4) + +struct G_SetPosition_6x3F { + G_ClientIDHeader header; + le_uint16_t unknown_a1 = 0; + le_uint16_t angle = 0; + le_int16_t floor = 0; + le_int16_t room = 0; + le_float x = 0.0f; + le_float y = 0.0f; + le_float z = 0.0f; +} __packed_ws__(G_SetPosition_6x3F, 0x18); + +// 6x40: Walk (protected on V3/V4) + +struct G_WalkToPosition_6x40 { + G_ClientIDHeader header; + le_float x = 0.0f; + le_float z = 0.0f; + le_uint32_t action = 0; +} __packed_ws__(G_WalkToPosition_6x40, 0x10); + +// 6x41: Unknown +// This subcommand is completely ignored by v2 and later. + +struct G_Unknown_6x41 { + G_ClientIDHeader header; + le_float x = 0.0f; + le_float z = 0.0f; +} __packed_ws__(G_Unknown_6x41, 0x0C); + +// 6x42: Run (protected on V3/V4) + +struct G_RunToPosition_6x42 { + G_ClientIDHeader header; + le_float x = 0.0f; + le_float z = 0.0f; +} __packed_ws__(G_RunToPosition_6x42, 0x0C); + +// 6x43: First attack (protected on V3/V4) +// 6x44: Second attack (protected on V3/V4) +// 6x45: Third attack (protected on V3/V4) + +struct G_Attack_6x43_6x44_6x45 { + G_ClientIDHeader header; + le_uint16_t unknown_a1 = 0; + le_uint16_t unknown_a2 = 0; +} __packed_ws__(G_Attack_6x43_6x44_6x45, 8); + +// 6x46: Attack finished (sent after each of 43, 44, and 45) (protected on V3/V4) +// The number of targets is not bounds-checked during byteswapping on GC +// clients. The client only expects up to 10 entries here, so if the number of +// targets is too large, the client will byteswap the function's return address +// on the stack, and it will crash. + +struct TargetEntry { + le_uint16_t entity_id = 0; + le_uint16_t unknown_a2 = 0; +} __packed_ws__(TargetEntry, 4); + +struct G_AttackFinished_6x46 { + G_ClientIDHeader header; + le_uint32_t count = 0; + // The client may send a shorter command if not all of these are used. + parray targets; +} __packed_ws__(G_AttackFinished_6x46, 0x30); + +// 6x47: Cast technique (protected on V3/V4) +// On GC, this command has the same bounds-check bug as 6x46. + +struct G_CastTechnique_6x47 { + G_ClientIDHeader header; + uint8_t technique_number = 0; + uint8_t unused = 0; // Must not be negative + // Note: The level here isn't the actual tech level that was cast, if the + // actual level is > 15. In that case, a 6x8D is sent first, which contains + // the additional level which is added to this level at cast time. They + // probably did this for legacy reasons when dealing with v1/v2 + // compatibility, and never cleaned it up. + uint8_t level = 0; + uint8_t target_count = 0; // Must be in [0, 10] + // The client may send a shorter command if not all of these are used. + parray targets; +} __packed_ws__(G_CastTechnique_6x47, 0x30); + +// 6x48: Cast technique complete (protected on V3/V4) + +struct G_CastTechniqueComplete_6x48 { + G_ClientIDHeader header; + le_uint16_t technique_number = 0; + // This level matches the level sent in the 6x47 command, even if that level + // was overridden by a preceding 6x8D command. + le_uint16_t level = 0; +} __packed_ws__(G_CastTechniqueComplete_6x48, 8); + +// 6x49: Execute Photon Blast (protected on V3/V4) +// On GC, this command has the same bounds-check bug as 6x46. + +struct G_ExecutePhotonBlast_6x49 { + G_ClientIDHeader header; + uint8_t unknown_a1 = 0; + uint8_t unknown_a2 = 0; + le_uint16_t target_count = 0; + le_uint16_t unknown_a3 = 0; + le_uint16_t unknown_a4 = 0; + // The client may send a shorter command if not all of these are used. + parray targets; +} __packed_ws__(G_ExecutePhotonBlast_6x49, 0x34); + +// 6x4A: Fully shield attack (protected on V3/V4) + +struct G_ShieldAttack_6x4A { + G_ClientIDHeader header; +} __packed_ws__(G_ShieldAttack_6x4A, 4); + +// 6x4B: Hit by enemy (protected on V3/V4) +// 6x4C: Hit by enemy (protected on V3/V4) + +struct G_HitByEnemy_6x4B_6x4C { + G_ClientIDHeader header; + le_uint16_t angle = 0; + le_uint16_t damage = 0; + le_float x_velocity = 0.0f; + le_float z_velocity = 0.0f; +} __packed_ws__(G_HitByEnemy_6x4B_6x4C, 0x10); + +// 6x4D: Player died (protected on V3/V4) + +struct G_PlayerDied_6x4D { + G_ClientIDHeader header; + le_uint32_t unknown_a1 = 0; +} __packed_ws__(G_PlayerDied_6x4D, 8); + +// 6x4E: Player is dead can be revived (protected on V3/V4) + +struct G_PlayerRevivable_6x4E { + G_ClientIDHeader header; +} __packed_ws__(G_PlayerRevivable_6x4E, 4); + +// 6x4F: Player revived (protected on V3/V4) + +struct G_PlayerRevived_6x4F { + G_ClientIDHeader header; +} __packed_ws__(G_PlayerRevived_6x4F, 4); + +// 6x50: Switch interaction (protected on V3/V4) + +struct G_SwitchInteraction_6x50 { + G_ClientIDHeader header; + le_uint32_t unknown_a1 = 0; +} __packed_ws__(G_SwitchInteraction_6x50, 8); + +// 6x51: Invalid subcommand + +// 6x52: Set animation state (protected on V3/V4) + +struct G_SetAnimationState_6x52 { + G_ClientIDHeader header; + le_uint16_t animation = 0; + le_uint16_t unknown_a2 = 0; + le_uint32_t angle = 0; +} __packed_ws__(G_SetAnimationState_6x52, 0x0C); + +// 6x53: Unknown (supported; game only) (protected on V3/V4) + +struct G_Unknown_6x53 { + G_ClientIDHeader header; +} __packed_ws__(G_Unknown_6x53, 4); + +// 6x54: Unknown +// This subcommand is completely ignored (at least, by PSO GC). + +struct G_Unknown_6x54 { + G_ClientIDHeader header; +} __packed_ws__(G_Unknown_6x54, 4); + +// 6x55: Intra-map warp (protected on V3/V4) + +struct G_IntraMapWarp_6x55 { + G_ClientIDHeader header; + le_uint32_t unknown_a1 = 0; + le_float x1 = 0.0f; + le_float y1 = 0.0f; + le_float z1 = 0.0f; + le_float x2 = 0.0f; + le_float y2 = 0.0f; + le_float z2 = 0.0f; +} __packed_ws__(G_IntraMapWarp_6x55, 0x20); + +// 6x56: Unknown (supported; game) (protected on V3/V4) + +struct G_Unknown_6x56 { + G_ClientIDHeader header; + le_uint32_t unknown_a1 = 0; + le_float x = 0.0f; + le_float y = 0.0f; + le_float z = 0.0f; +} __packed_ws__(G_Unknown_6x56, 0x14); + +// 6x57: Unknown (supported; lobby & game) (protected on V3/V4) + +struct G_Unknown_6x57 { + G_ClientIDHeader header; +} __packed_ws__(G_Unknown_6x57, 4); + +// 6x58: Lobby animation (protected on V3/V4) + +struct G_LobbyAnimation_6x58 { + G_ClientIDHeader header; + le_uint16_t animation_number = 0; + le_uint16_t unused = 0; +} __packed_ws__(G_LobbyAnimation_6x58, 8); + +// 6x59: Pick up item + +struct G_PickUpItem_6x59 { + G_ClientIDHeader header; + le_uint16_t client_id2 = 0; + le_uint16_t floor = 0; + le_uint32_t item_id = 0; +} __packed_ws__(G_PickUpItem_6x59, 0x0C); + +// 6x5A: Request to pick up item + +struct G_PickUpItemRequest_6x5A { + G_ClientIDHeader header; + le_uint32_t item_id = 0; + le_uint16_t floor = 0; + le_uint16_t unused = 0; +} __packed_ws__(G_PickUpItemRequest_6x5A, 0x0C); + +// 6x5B: Unknown (DCv1 and earlier) +// This command has a handler, but it does nothing, even on DC NTE. + +// 6x5C: Destroy floor item +// Same format as 6x63. It appears this version should not be used because it +// removes the item from the floor just like 6x63 does, but 6x5C doesn't call +// the item's destructor. + +// 6x5D: Drop meseta or stacked item +// On DC NTE, this command has the same format, but is subcommand 6x4F instead. + +struct G_DropStackedItem_DC_6x5D { + G_ClientIDHeader header; + le_uint16_t floor = 0; + le_uint16_t unknown_a2 = 0; // Corresponds to FloorItem::unknown_a2 + le_float x = 0.0f; + le_float z = 0.0f; + ItemData item_data; +} __packed_ws__(G_DropStackedItem_DC_6x5D, 0x24); + +struct G_DropStackedItem_PC_V3_BB_6x5D : G_DropStackedItem_DC_6x5D { + le_uint32_t unused3 = 0; +} __packed_ws__(G_DropStackedItem_PC_V3_BB_6x5D, 0x28); + +// 6x5E: Buy item at shop + +struct G_BuyShopItem_6x5E { + G_ClientIDHeader header; + ItemData item_data; +} __packed_ws__(G_BuyShopItem_6x5E, 0x18); + +// 6x5F: Drop item from box/enemy + +struct FloorItem { + /* 00 */ uint8_t floor = 0; + /* 01 */ uint8_t from_enemy = 0; + /* 02 */ le_uint16_t entity_id = 0; // < 0x0B50 if from_enemy != 0; otherwise < 0x0BA0 + /* 04 */ le_float x = 0.0f; + /* 08 */ le_float z = 0.0f; + /* 0C */ le_uint16_t unknown_a2 = 0; + // The drop number is scoped to the floor and increments by 1 each time an + // item is dropped. The last item dropped in each floor has drop_number equal + // to total_items_dropped_per_floor[floor - 1] - 1. + /* 0E */ le_uint16_t drop_number = 0; + /* 10 */ ItemData item; + /* 24 */ +} __packed_ws__(FloorItem, 0x24); + +struct G_DropItem_DC_6x5F { + G_UnusedHeader header; + FloorItem item; +} __packed_ws__(G_DropItem_DC_6x5F, 0x28); + +struct G_DropItem_PC_V3_BB_6x5F : G_DropItem_DC_6x5F { + le_uint32_t unused3 = 0; +} __packed_ws__(G_DropItem_PC_V3_BB_6x5F, 0x2C); + +// 6x60: Request for item drop (handled by the server on BB) + +struct G_StandardDropItemRequest_DC_6x60 { + /* 00 */ G_UnusedHeader header; + /* 04 */ uint8_t floor = 0; + /* 05 */ uint8_t rt_index = 0; + /* 06 */ le_uint16_t entity_id = 0; + /* 08 */ le_float x = 0.0f; + /* 0C */ le_float z = 0.0f; + /* 10 */ le_uint16_t section = 0; + /* 12 */ le_uint16_t ignore_def = 0; + /* 14 */ +} __packed_ws__(G_StandardDropItemRequest_DC_6x60, 0x14); + +struct G_StandardDropItemRequest_PC_V3_BB_6x60 : G_StandardDropItemRequest_DC_6x60 { + /* 14 */ uint8_t effective_area = 0; + /* 15 */ parray unused; + /* 18 */ +} __packed_ws__(G_StandardDropItemRequest_PC_V3_BB_6x60, 0x18); + +// 6x61: Activate MAG effect + +struct G_ActivateMagEffect_6x61 { + G_UnusedHeader header; + le_uint32_t mag_item_id = 0; + le_uint32_t effect_number = 0; +} __packed_ws__(G_ActivateMagEffect_6x61, 0x0C); + +// 6x62: Unknown +// This command has a handler, but it does nothing even on DC NTE. + +// 6x63: Destroy floor item (used when too many items have been dropped) + +struct G_DestroyFloorItem_6x5C_6x63 { + G_UnusedHeader header; + le_uint32_t item_id = 0; + le_uint32_t floor = 0; +} __packed_ws__(G_DestroyFloorItem_6x5C_6x63, 0x0C); + +// 6x64: Unknown (not valid on Episode 3) +// This command has a handler, but it does nothing even on DC NTE. + +// 6x65: Unknown (not valid on Episode 3) +// This command has a handler, but it does nothing even on DC NTE. + +// 6x66: Use star atomizer + +struct G_UseStarAtomizer_6x66 { + G_UnusedHeader header; + parray target_client_ids; +} __packed_ws__(G_UseStarAtomizer_6x66, 0x0C); + +// 6x67: Trigger set event + +struct G_TriggerSetEvent_6x67 { + G_UnusedHeader header; + le_uint32_t floor = 0; + le_uint32_t event_id = 0; // NOT event index + le_uint32_t client_id = 0; +} __packed_ws__(G_TriggerSetEvent_6x67, 0x10); + +// 6x68: Set telepipe state + +struct G_SetTelepipeState_6x68 { + G_UnusedHeader header; + le_uint16_t client_id2 = 0; + le_uint16_t floor = 0; + le_uint16_t unknown_b1 = 0; + uint8_t unknown_b2 = 0; + uint8_t unknown_b3 = 0; + le_float x = 0.0f; + le_float y = 0.0f; + le_float z = 0.0f; + le_uint32_t unknown_a3 = 0; +} __packed_ws__(G_SetTelepipeState_6x68, 0x1C); + +// 6x69: NPC control +// Note: NPCs cannot be destroyed with 6x69; 6x1C is used instead for that. + +struct G_NPCControl_6x69 { + G_UnusedHeader header; + le_uint16_t param1; // Commands 0/3: state; command 1: npc_entity_id; command 2: unknown + le_uint16_t param2; // Commands 0/3: npc_entity_id; commands 1/2: unused + le_uint16_t command = 0; // 0 = create follower NPC, 1 = stop acting, 2 = start acting, 3 = create attacker NPC + le_uint16_t param3; // Commands 0/3: npc_template_index; commands 1/2: unused +} __packed_ws__(G_NPCControl_6x69, 0x0C); + +// 6x6A: Use boss warp (not valid on Episode 3) + +struct G_UseBossWarp_6x6A { + G_ClientIDHeader header; + le_uint16_t unknown_a1 = 0; + le_uint16_t unused = 0; +} __packed_ws__(G_UseBossWarp_6x6A, 8); + +// 6x6B: Sync enemy state (used while loading into game) + +struct G_SyncGameStateHeader_DCNTE_6x6B_6x6C_6x6D_6x6E { + G_ExtendedHeaderT header; + le_uint32_t decompressed_size = 0; + // BC0-compressed data follows here (see bc0_decompress) +} __packed_ws__(G_SyncGameStateHeader_DCNTE_6x6B_6x6C_6x6D_6x6E, 0x0C); + +struct G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E { + G_ExtendedHeaderT header; + le_uint32_t decompressed_size = 0; + le_uint32_t compressed_size = 0; // Must be <= subcommand_size - 0x10 + // BC0-compressed data follows here (see bc0_decompress) +} __packed_ws__(G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E, 0x10); + +// Decompressed format is a list of these +struct G_SyncEnemyState_6x6B_Entry_Decompressed { + le_uint32_t flags = 0; // Same as flags in 6x0A + le_uint16_t item_drop_id = 0; + le_uint16_t total_damage = 0; // Same as in 6x0A + uint8_t red_buff_type = 0; + uint8_t red_buff_level = 0; + uint8_t blue_buff_type = 0; + uint8_t blue_buff_level = 0; +} __packed_ws__(G_SyncEnemyState_6x6B_Entry_Decompressed, 0x0C); + +// 6x6C: Sync object state (used while loading into game) +// Compressed format is the same as 6x6B. + +// Decompressed format is a list of these +struct G_SyncObjectState_6x6C_Entry_Decompressed { + le_uint16_t flags = 0; + le_uint16_t item_drop_id = 0; +} __packed_ws__(G_SyncObjectState_6x6C_Entry_Decompressed, 4); + +// 6x6D: Sync item state (used while loading into game) +// Internal name: RcvItemCondition +// Compressed format is the same as 6x6B. + +// There is a bug in the client that can cause desync between players' item IDs +// if the 6x6D command is sent too quickly. Under normal operation, the client +// keeps track of the next item ID to be assigned for items it creates, and uses +// that to assign item IDs to its inventory items when it's done loading (just +// before it sends the 6F command). The loading process triggered by the 64 +// (game join) command resets these next item ID variables to their default +// values, and the 6x6D command sent by the leader resets them again to match +// the corresponding variables in the leader's item state. However, the loading +// process doesn't actually start until the next frame after the 64 command is +// received, so if the 6x6D command is received on the same frame as the 64 +// command, it will set the next item ID variables correctly, and the loading +// process will then clear all of them on the next frame. The client will then +// assign its own inventory item IDs based on the default base item ID, which +// will result in incorrect IDs if another player had previously been in the +// game in the same slot (since the leader's next item ID for the joining player +// will not match the default value). Fortunately, the game processes commands +// in two phases: first, it receives as much data as possible, then it processes +// as many commands as possible. So, to prevent this bug, we delay all commands +// after a 64 is sent until the client responds to a ping command (sent +// immediately after the 64 command), which ensures that the 64 and 6x6D +// commands cannot be processed on the same frame. + +struct G_SyncItemState_6x6D_Decompressed { + // Note: 16 vs. 15 is not a bug here - there really is an extra field in the + // next drop number vs. the floor item count. Despite this, Pioneer 2 or Lab + // (floor 0) isn't included in next_drop_number_per_floor (so Forest 1 is [0] + // in that array) but it is included in floor_item_count_per_floor (so Forest + // 1 is [1] there). + /* 00 */ parray next_drop_number_per_floor; + // Only [0]-[3] in this array are ever actually used in normal gameplay, but + // the client fills in all 12 of these with reasonable values. + /* 20 */ parray next_item_id_per_player; + /* 50 */ parray floor_item_count_per_floor; + // Variable-length field: + /* 8C */ // FloorItem items[sum(floor_item_count_per_floor)]; +} __packed_ws__(G_SyncItemState_6x6D_Decompressed, 0x8C); + +// 6x6E: Sync set flag state (used while loading into game) +// Compressed format is the same as 6x6B. + +struct G_SyncSetFlagState_6x6E_Decompressed { + le_uint16_t total_size = 0; // == sum of the following 3 fields + le_uint16_t entity_set_flags_size = 0; + le_uint16_t event_set_flags_size = 0; + le_uint16_t switch_flags_size = 0; + // Variable-length fields follow here: + // EntitySetFlags entity_set_flags; // Total size is entity_set_flags_size + // le_uint16_t event_set_flags[event_set_flags_size / 2]; // Same order as in map files (NOT sorted by event_id) + // SwitchFlags switch_flags; // 0x200 bytes (0x10 floors) on v1 and earlier; 0x240 bytes (0x12 floors) on v2 and later + + struct EntitySetFlags { + le_uint32_t object_set_flags_offset = 0; + le_uint32_t num_object_sets = 0; + le_uint32_t enemy_set_flags_offset = 0; + le_uint32_t num_enemy_sets = 0; + // Variable-length fields follow here: + // le_uint16_t object_set_flags[num_object_sets]; + // le_uint16_t enemy_set_flags[num_enemy_sets]; + } __packed_ws__(EntitySetFlags, 0x10); +} __packed_ws__(G_SyncSetFlagState_6x6E_Decompressed, 8); + +// 6x6F: Set quest flags (used while loading into game) + +struct G_SetQuestFlags_DCv1_6x6F { + G_UnusedHeader header; + QuestFlagsV1 quest_flags; +} __packed_ws__(G_SetQuestFlags_DCv1_6x6F, 0x184); + +struct G_SetQuestFlags_V2_V3_6x6F { + G_UnusedHeader header; + QuestFlags quest_flags; +} __packed_ws__(G_SetQuestFlags_V2_V3_6x6F, 0x204); + +struct G_SetQuestFlags_BB_6x6F { + G_UnusedHeader header; + QuestFlags quest_flags; + // If use_apply_mask is 1, only the flags set in bb_quest_flag_apply_mask + // (in PlayerSubordinates.cc) are overwritten on the receiving client's end. + // The client always sends this with use_apply_mask = 1. + le_uint32_t use_apply_mask = 1; +} __packed_ws__(G_SetQuestFlags_BB_6x6F, 0x208); + +// 6x70: Sync player disp data and inventory (used while loading into game) +// Annoyingly, they didn't use the same format as the 65/67/68 commands here, +// and instead rearranged a bunch of things. This is presumably because this +// structure also includes transient state (e.g. current HP). + +struct Telepipe6x70 { + /* 00 */ le_uint16_t owner_client_id = 0xFFFF; + /* 02 */ le_uint16_t floor = 0; + /* 04 */ le_uint32_t unknown_a1 = 0; + /* 08 */ le_float x = 0.0f; + /* 0C */ le_float y = 0.0f; + /* 10 */ le_float z = 0.0f; + /* 14 */ le_uint32_t unknown_a3 = 0; + /* 18 */ le_uint32_t unknown_a4 = 0x0000FFFF; +} __packed_ws__(Telepipe6x70, 0x1C); + +struct G_Unknown_6x70_SubA1 { + // This is used in all versions of this command except DCNTE and 11/2000. + /* 00 */ le_uint16_t unknown_a1 = 0; + /* 02 */ le_uint16_t unknown_a2 = 0; + /* 04 */ le_uint32_t unknown_a3 = 0; + /* 08 */ le_float unknown_a4 = 0.0f; + /* 0C */ le_uint32_t unknown_a5 = 0; + /* 10 */ le_uint32_t unknown_a6 = 0; +} __packed_ws__(G_Unknown_6x70_SubA1, 0x14); + +struct G_Unknown_6x70_SubA2 { + // This is used in all versions of this command except DCNTE and 11/2000. + /* 00 */ le_uint32_t unknown_a1 = 0; + /* 04 */ le_float unknown_a2 = 0.0f; + /* 08 */ le_uint32_t unknown_a3 = 0; +} __packed_ws__(G_Unknown_6x70_SubA2, 0x0C); + +struct G_SyncPlayerDispAndInventory_BaseDCNTE { + /* 0000 */ le_uint16_t client_id = 0; + /* 0002 */ le_uint16_t unknown_a1 = 0; + /* 0004 */ le_uint32_t flags1 = 0; + /* 0008 */ le_float x = 0.0f; + /* 000C */ le_float y = 0.0f; + /* 0010 */ le_float z = 0.0f; + /* 0014 */ le_uint32_t angle_x = 0; + /* 0018 */ le_uint32_t angle_y = 0; + /* 001C */ le_uint32_t angle_z = 0; + /* 0020 */ le_uint16_t unknown_a3a = 0; + /* 0022 */ le_uint16_t current_hp = 0; +} __packed_ws__(G_SyncPlayerDispAndInventory_BaseDCNTE, 0x24); + +struct G_SyncPlayerDispAndInventory_DCNTE_6x70 { + // Offsets in this struct are relative to the overall command header + /* 0004 */ G_ExtendedHeaderT header = {{0x60, 0x00, 0x0000}, sizeof(G_SyncPlayerDispAndInventory_DCNTE_6x70)}; + /* 000C */ G_SyncPlayerDispAndInventory_BaseDCNTE base; + // The following two fields appear to contain uninitialized data + /* 0030 */ le_uint32_t unknown_a5 = 0; + /* 0034 */ le_uint32_t unknown_a6 = 0; + /* 0038 */ Telepipe6x70 telepipe; + /* 0054 */ le_uint32_t unknown_a8 = 0; + /* 0058 */ parray unknown_a9; + /* 0068 */ le_uint32_t area = 0; + /* 006C */ le_uint32_t flags2 = 0; + /* 0070 */ PlayerVisualConfig visual; + /* 00C0 */ PlayerStats stats; + /* 00E4 */ le_uint32_t num_items = 0; + /* 00E8 */ parray items; + /* 0430 */ +} __packed_ws__(G_SyncPlayerDispAndInventory_DCNTE_6x70, 0x42C); + +struct G_SyncPlayerDispAndInventory_DC112000_6x70 { + // Offsets in this struct are relative to the overall command header + /* 0004 */ G_ExtendedHeaderT header = {{0x67, 0x00, 0x0000}, sizeof(G_SyncPlayerDispAndInventory_DC112000_6x70)}; + /* 000C */ G_SyncPlayerDispAndInventory_BaseDCNTE base; + /* 0030 */ le_uint16_t bonus_hp_from_materials = 0; + /* 0032 */ le_uint16_t bonus_tp_from_materials = 0; + /* 0034 */ parray unknown_a5; + /* 0044 */ Telepipe6x70 telepipe; + /* 0060 */ le_uint32_t unknown_a8 = 0; + /* 0064 */ parray unknown_a9; + /* 0074 */ le_uint32_t area = 0; + /* 0078 */ le_uint32_t flags2 = 0; + /* 007C */ PlayerVisualConfig visual; + /* 00CC */ PlayerStats stats; + /* 00F0 */ le_uint32_t num_items = 0; + /* 00F4 */ parray items; + /* 043C */ +} __packed_ws__(G_SyncPlayerDispAndInventory_DC112000_6x70, 0x438); + +struct G_SyncPlayerDispAndInventory_BaseV1 { + /* 0000 */ G_SyncPlayerDispAndInventory_BaseDCNTE base; + /* 0024 */ le_uint16_t bonus_hp_from_materials = 0; + /* 0026 */ le_uint16_t bonus_tp_from_materials = 0; + /* 0028 */ parray unknown_a4; + /* 0064 */ le_uint32_t language = 0; + /* 0068 */ le_uint32_t player_tag = 0; + /* 006C */ le_uint32_t guild_card_number = 0; + /* 0070 */ le_uint32_t unknown_a6 = 0; + /* 0074 */ le_uint32_t battle_team_number = 0; + /* 0078 */ Telepipe6x70 telepipe; + /* 0094 */ le_uint32_t unknown_a8 = 0; + /* 0098 */ G_Unknown_6x70_SubA1 unknown_a9; + /* 00AC */ le_uint32_t area = 0; + /* 00B0 */ le_uint32_t flags2 = 0; + /* 00B4 */ parray technique_levels_v1 = 0xFF; // Last byte is uninitialized + /* 00C8 */ PlayerVisualConfig visual; + /* 0118 */ +} __packed_ws__(G_SyncPlayerDispAndInventory_BaseV1, 0x118); + +struct G_SyncPlayerDispAndInventory_DC_PC_6x70 { + // Offsets in this struct are relative to the overall command header + /* 0004 */ G_ExtendedHeaderT header = {{0x70, 0x00, 0x0000}, sizeof(G_SyncPlayerDispAndInventory_DC_PC_6x70)}; + /* 000C */ G_SyncPlayerDispAndInventory_BaseV1 base; + /* 0124 */ PlayerStats stats; + /* 0148 */ le_uint32_t num_items = 0; + /* 014C */ parray items; + /* 0494 */ +} __packed_ws__(G_SyncPlayerDispAndInventory_DC_PC_6x70, 0x490); + +// GC NTE also uses this format. +struct G_SyncPlayerDispAndInventory_GC_6x70 { + // Offsets in this struct are relative to the overall command header + /* 0004 */ G_ExtendedHeaderT header = {{0x70, 0x00, 0x0000}, sizeof(G_SyncPlayerDispAndInventory_GC_6x70)}; + /* 000C */ G_SyncPlayerDispAndInventory_BaseV1 base; + /* 0124 */ PlayerStats stats; + /* 0148 */ le_uint32_t num_items = 0; + /* 014C */ parray items; + /* 0494 */ le_uint32_t floor = 0; + /* 0498 */ +} __packed_ws__(G_SyncPlayerDispAndInventory_GC_6x70, 0x494); + +struct G_SyncPlayerDispAndInventory_XB_6x70 { + // Offsets in this struct are relative to the overall command header + /* 0004 */ G_ExtendedHeaderT header = {{0x70, 0x00, 0x0000}, sizeof(G_SyncPlayerDispAndInventory_XB_6x70)}; + /* 000C */ G_SyncPlayerDispAndInventory_BaseV1 base; + /* 0124 */ PlayerStats stats; + /* 0148 */ le_uint32_t num_items = 0; + /* 014C */ parray items; + /* 0494 */ le_uint32_t floor = 0; + /* 0498 */ le_uint32_t xb_user_id_high = 0; + /* 049C */ le_uint32_t xb_user_id_low = 0; + /* 04A0 */ le_uint32_t unknown_a16 = 0; + /* 04A4 */ +} __packed_ws__(G_SyncPlayerDispAndInventory_XB_6x70, 0x4A0); + +struct G_SyncPlayerDispAndInventory_BB_6x70 { + // Offsets in this struct are relative to the overall command header + /* 0008 */ G_ExtendedHeaderT header = {{0x70, 0x00, 0x0000}, sizeof(G_SyncPlayerDispAndInventory_BB_6x70)}; + /* 0010 */ G_SyncPlayerDispAndInventory_BaseV1 base; + /* 0128 */ pstring name; + /* 0148 */ PlayerStats stats; + /* 016C */ le_uint32_t num_items = 0; + /* 0170 */ parray items; + /* 04B8 */ le_uint32_t floor = 0; + /* 04BC */ le_uint32_t xb_user_id_high = 0; + /* 04C0 */ le_uint32_t xb_user_id_low = 0; + /* 04C4 */ le_uint32_t unknown_a16 = 0; + /* 04C8 */ +} __packed_ws__(G_SyncPlayerDispAndInventory_BB_6x70, 0x4C0); + +// 6x71: Unblock game join (used while loading into game) + +struct G_UnblockGameJoin_6x71 { + G_UnusedHeader header; +} __packed_ws__(G_UnblockGameJoin_6x71, 4); + +// 6x72: Player done loading into game + +struct G_DoneLoadingIntoGame_6x72 { + G_UnusedHeader header; +} __packed_ws__(G_DoneLoadingIntoGame_6x72, 4); + +// 6x73: Exit quest +// This command misbehaves if sent in a lobby or in a game when no quest is +// loaded. + +struct G_ExitQuest_6x73 { + G_UnusedHeader header; +} __packed_ws__(G_ExitQuest_6x73, 4); + +// 6x74: Word select +// There is a bug in PSO GC with regard to this command: the client does not +// byteswap the header, which means the client_id field is big-endian. + +template +struct G_WordSelectT_6x74 { + using U16T = typename std::conditional::type; + uint8_t subcommand = 0; + uint8_t size = 0; + U16T client_id = 0; + WordSelectMessage message; +} __packed__; +using G_WordSelect_6x74 = G_WordSelectT_6x74; +using G_WordSelectBE_6x74 = G_WordSelectT_6x74; +check_struct_size(G_WordSelect_6x74, 0x20); +check_struct_size(G_WordSelectBE_6x74, 0x20); + +// 6x75: Update quest flag + +struct G_UpdateQuestFlag_DC_PC_6x75 { + G_UnusedHeader header; + le_uint16_t flag = 0; // Must be < 0x400 + le_uint16_t action = 0; // 0 = set flag, 1 = clear flag +} __packed_ws__(G_UpdateQuestFlag_DC_PC_6x75, 8); + +struct G_UpdateQuestFlag_V3_BB_6x75 : G_UpdateQuestFlag_DC_PC_6x75 { + le_uint16_t difficulty = 0; + le_uint16_t unused = 0; +} __packed_ws__(G_UpdateQuestFlag_V3_BB_6x75, 0x0C); + +// 6x76: Set entity set flags +// This command can only be used to set set flags, since the game performs a +// bitwise OR operation instead of a simple assignment. + +struct G_SetEntitySetFlags_6x76 { + G_EnemyIDHeader header; // 1000-3FFF = enemy, 4000-FFFF = object + le_uint16_t floor = 0; + le_uint16_t flags = 0; +} __packed_ws__(G_SetEntitySetFlags_6x76, 8); + +// 6x77: Sync quest register +// This is sent by the client when an opcode D9 is executed within a quest. + +struct G_SyncQuestRegister_6x77 { + G_UnusedHeader header; + le_uint16_t register_number = 0; // Must be < 0x100 + le_uint16_t unused = 0; + union { + le_uint32_t as_int; + le_float as_float; + } __packed__ value; +} __packed_ws__(G_SyncQuestRegister_6x77, 0x0C); + +// 6x78: Unknown + +struct G_Unknown_6x78 { + G_UnusedHeader header; + le_uint16_t client_id = 0; // Must be < 12 + le_uint16_t unused1 = 0; + le_uint32_t unused2 = 0; +} __packed_ws__(G_Unknown_6x78, 0x0C); + +// 6x79: Lobby 14/15 gogo ball (soccer game) + +struct G_GogoBall_6x79 { + G_UnusedHeader header; + le_uint32_t unknown_a1 = 0; + le_uint32_t unknown_a2 = 0; + le_float unknown_a3 = 0.0f; + le_float unknown_a4 = 0.0f; + uint8_t unknown_a5 = 0; + parray unused; +} __packed_ws__(G_GogoBall_6x79, 0x18); + +// 6x7A: Unknown (protected on V3/V4) + +struct G_Unknown_6x7A { + G_ClientIDHeader header; +} __packed_ws__(G_Unknown_6x7A, 4); + +// 6x7B: Unknown (protected on V3/V4) + +struct G_Unknown_6x7B { + G_ClientIDHeader header; +} __packed_ws__(G_Unknown_6x7B, 4); + +// 6x7C: Set Challenge records (not valid on Episode 3) + +struct G_SetChallengeRecordsBase_6x7C { + G_UnusedHeader header; + le_uint16_t client_id = 0; + parray unknown_a1; +} __packed_ws__(G_SetChallengeRecordsBase_6x7C, 8); + +struct G_SetChallengeRecords_DC_6x7C : G_SetChallengeRecordsBase_6x7C { + PlayerRecordsChallengeDC records; +} __packed_ws__(G_SetChallengeRecords_DC_6x7C, 0xA8); +struct G_SetChallengeRecords_PC_6x7C : G_SetChallengeRecordsBase_6x7C { + PlayerRecordsChallengePC records; +} __packed_ws__(G_SetChallengeRecords_PC_6x7C, 0xE0); +struct G_SetChallengeRecords_V3_6x7C : G_SetChallengeRecordsBase_6x7C { + PlayerRecordsChallengeV3 records; +} __packed_ws__(G_SetChallengeRecords_V3_6x7C, 0x108); +struct G_SetChallengeRecords_BB_6x7C : G_SetChallengeRecordsBase_6x7C { + PlayerRecordsChallengeBB records; +} __packed_ws__(G_SetChallengeRecords_BB_6x7C, 0x148); + +// 6x7D: Set battle mode data (not valid on Episode 3) + +struct G_SetBattleModeData_6x7D { + G_UnusedHeader header; + // Values for what (0-6; values 7 and above are not valid): + // 0 = Unknown (params[0] and [1] are used) + // 1 = Does nothing + // 2 = Unknown (no params are used) + // 3 = Set player score (params[0] = client ID, [1] = score) + // 4 = Unknown (params[0] = client ID) + // 5 = Unknown (no params are used) + // 6 = Unknown (all params are used) + uint8_t what = 0; + uint8_t unknown_a1 = 0; // Only used when what == 0 + uint8_t unused = 0; + uint8_t is_alive = 0; // Only used when what == 3 + parray params; +} __packed_ws__(G_SetBattleModeData_6x7D, 0x18); + +// 6x7E: Unknown (not valid on Episode 3) +// This subcommand is completely ignored (at least, by PSO GC). + +// 6x7F: Battle scores and places (not valid on Episode 3) + +template +struct G_BattleScoresT_6x7F { + using U16T = typename std::conditional::type; + using U32T = typename std::conditional::type; + struct Entry { + U16T client_id = 0; + U16T place = 0; + U32T score = 0; + } __packed_ws__(Entry, 8); + G_UnusedHeader header; + parray entries; +} __packed__; +using G_BattleScores_6x7F = G_BattleScoresT_6x7F; +using G_BattleScoresBE_6x7F = G_BattleScoresT_6x7F; +check_struct_size(G_BattleScores_6x7F, 0x24); +check_struct_size(G_BattleScoresBE_6x7F, 0x24); + +// 6x80: Trigger trap (not valid on Episode 3) + +struct G_TriggerTrap_6x80 { + G_ClientIDHeader header; + le_uint16_t unknown_a1 = 0; + le_uint16_t unknown_a2 = 0; +} __packed_ws__(G_TriggerTrap_6x80, 8); + +// 6x81: Set drop weapon on death flag (protected on V3/V4) + +struct G_SetDropWeaponOnDeathFlag_6x81 { + G_ClientIDHeader header; +} __packed_ws__(G_SetDropWeaponOnDeathFlag_6x81, 4); + +// 6x82: Clear drop weapon on death flag (protected on V3/V4) + +struct G_ClearDropWeaponOnDeathFlag_6x82 { + G_ClientIDHeader header; +} __packed_ws__(G_ClearDropWeaponOnDeathFlag_6x82, 4); + +// 6x83: Place trap (protected on V3/V4) + +struct G_PlaceTrap_6x83 { + G_ClientIDHeader header; + le_uint16_t trap_type = 0; + le_uint16_t unknown_a2 = 0; +} __packed_ws__(G_PlaceTrap_6x83, 8); + +// 6x84: Vol Opt boss actions (not valid on Episode 3) +// Same format and usage as 6x16, except unknown_a2 is ignored in 6x84. + +struct G_VolOptBossActions_6x84 { + G_UnusedHeader header; + parray unknown_a1; + le_uint16_t unknown_a2 = 0; + le_uint16_t unknown_a3 = 0; + le_uint16_t unused = 0; +} __packed_ws__(G_VolOptBossActions_6x84, 0x10); + +// 6x85: Unknown (supported; game only; not valid on Episode 3) + +struct G_Unknown_6x85 { + G_UnusedHeader header; + le_uint16_t unknown_a1 = 0; // Command is ignored unless this is 0 + parray unknown_a2; // Only the first 3 appear to be used +} __packed_ws__(G_Unknown_6x85, 0x14); + +// 6x86: Hit destructible object (not valid on Episode 3) + +struct G_HitDestructibleObject_6x86 { + G_ObjectIDHeader header; + le_uint32_t unknown_a1 = 0; + le_uint32_t unknown_a2 = 0; + le_uint16_t unknown_a3 = 0; + le_uint16_t unknown_a4 = 0; +} __packed_ws__(G_HitDestructibleObject_6x86, 0x10); + +// 6x87: Shrink player (protected on V3/V4) + +struct G_ShrinkPlayer_6x87 { + G_ClientIDHeader header; + le_float unknown_a1 = 0.0f; +} __packed_ws__(G_ShrinkPlayer_6x87, 8); + +// 6x88: Restore shrunken player (protected on V3/V4) + +struct G_RestoreShrunkenPlayer_6x88 { + G_ClientIDHeader header; +} __packed_ws__(G_RestoreShrunkenPlayer_6x88, 4); + +// 6x89: Player killed by monster (protected on V3/V4) + +struct G_PlayerKilledByMonster_6x89 { + G_ClientIDHeader header; + le_uint16_t unknown_a1 = 0; + le_uint16_t unused = 0; +} __packed_ws__(G_PlayerKilledByMonster_6x89, 8); + +// 6x8A: Show Challenge time records window (not valid on Episode 3) +// The leader sends this command to tell other clients to show, hide, or update +// the window that shows Challenge Mode stage competion and time records during +// Challenge quest selection. + +struct G_ShowChallengeTimeRecordsWindow_6x8A { + G_ClientIDHeader header; + // Values for which (decimal): + // 0 = hide window + // 1 = show Episode 1 completion state per player + // 2 = show Episode 2 completion state per player + // 3-11 = show times for Episode 1 stage (which - 2) + // 12-16 = show times for Episode 2 stage (which - 11) + // Anything else = command is ignored + le_uint32_t which = 0; +} __packed_ws__(G_ShowChallengeTimeRecordsWindow_6x8A, 8); + +// 6x8B: Unknown (not valid on Episode 3) +// This command has a handler, but it does nothing. + +// 6x8C: Unknown (not valid on Episode 3) +// This command has a handler, but it does nothing. + +// 6x8D: Set technique level override (protected on V3/V4) +// This command is sent immediately before 6x47 if the technique level is above +// 15. Presumably this was done for compatibility between v1 and v2. + +struct G_SetTechniqueLevelOverride_6x8D { + G_ClientIDHeader header; + uint8_t level_upgrade = 0; + uint8_t unused1 = 0; + le_uint16_t unused2 = 0; +} __packed_ws__(G_SetTechniqueLevelOverride_6x8D, 8); + +// 6x8E: Unknown (not valid on Episode 3) +// This command has a handler, but it does nothing. + +// 6x8F: Unknown (not valid on Episode 3) + +struct G_Unknown_6x8F { + G_ClientIDHeader header; + le_uint16_t client_id2 = 0; + le_uint16_t unknown_a1 = 0; +} __packed_ws__(G_Unknown_6x8F, 8); + +// 6x90: Unknown (not valid on Episode 3) (protected on V3/V4) + +struct G_Unknown_6x90 { + G_ClientIDHeader header; + le_uint32_t unknown_a1 = 0; +} __packed_ws__(G_Unknown_6x90, 8); + +// 6x91: Unknown (supported; game only) + +struct G_Unknown_6x91 { + G_ObjectIDHeader header; + le_uint32_t unknown_a1 = 0; + le_uint32_t unknown_a2 = 0; + le_uint16_t unknown_a3 = 0; + le_uint16_t unknown_a4 = 0; + le_uint16_t switch_flag_num = 0; + uint8_t should_set = 0; // The switch flag is only set if this is equal to 1; otherwise it's cleared + uint8_t switch_flag_floor = 0; +} __packed_ws__(G_Unknown_6x91, 0x14); + +// 6x92: Unknown (not valid on Episode 3) + +struct G_Unknown_6x92 { + G_UnusedHeader header; + le_uint32_t unknown_a1 = 0; + le_float unknown_a2 = 0.0f; +} __packed_ws__(G_Unknown_6x92, 0x0C); + +// 6x93: Activate timed switch (not valid on Episode 3) + +struct G_ActivateTimedSwitch_6x93 { + G_UnusedHeader header; + le_uint16_t switch_flag_floor = 0; + le_uint16_t switch_flag_num = 0; + uint8_t should_set = 0; // The switch flag is only set if this is equal to 1; otherwise it's cleared + parray unused; +} __packed_ws__(G_ActivateTimedSwitch_6x93, 0x0C); + +// 6x94: Warp (not valid on Episode 3) + +struct G_InterLevelWarp_6x94 { + G_UnusedHeader header; + le_uint16_t floor = 0; + parray unused; +} __packed_ws__(G_InterLevelWarp_6x94, 8); + +// 6x95: Unknown (not valid on Episode 3) + +struct G_Unknown_6x95 { + G_UnusedHeader header; + le_uint32_t client_id = 0; + ChallengeTime challenge_time; + le_uint32_t unused1 = 0; + le_uint32_t unused2 = 0; +} __packed_ws__(G_Unknown_6x95, 0x14); + +// 6x96: Unknown (not valid on Episode 3) +// This command has a handler, but it does nothing. + +// 6x97: Select Challenge Mode failure option (not valid on Episode 3) + +struct G_SelectChallengeModeFailureOption_6x97 { + G_UnusedHeader header; + le_uint32_t unused1 = 0; + le_uint32_t is_retry = 0; + le_uint32_t unused2 = 0; + le_uint32_t unused3 = 0; +} __packed_ws__(G_SelectChallengeModeFailureOption_6x97, 0x14); + +// 6x98: Unknown +// This subcommand is completely ignored (at least, by PSO GC). + +// 6x99: Unknown +// This subcommand is completely ignored (at least, by PSO GC). + +// 6x9A: Update player stat (not valid on Episode 3) + +struct G_UpdatePlayerStat_6x9A { + G_ClientIDHeader header; + le_uint16_t client_id2 = 0; + // Values for what: + // 0 = subtract HP + // 1 = subtract TP + // 2 = subtract Meseta + // 3 = add HP + // 4 = add TP + uint8_t what = 0; + uint8_t amount = 0; +} __packed_ws__(G_UpdatePlayerStat_6x9A, 8); + +// 6x9B: Level up all techniques (protected on V3/V4) +// Used in battle mode if the rules specify that techniques should level up +// upon character death. + +struct G_LevelUpAllTechniques_6x9B { + G_UnusedHeader header; + uint8_t num_levels = 0; + parray unused; +} __packed_ws__(G_LevelUpAllTechniques_6x9B, 8); + +// 6x9C: Unknown (supported; game only; not valid on Episode 3) + +struct G_Unknown_6x9C { + G_EnemyIDHeader header; + le_uint32_t unknown_a1 = 0; +} __packed_ws__(G_Unknown_6x9C, 8); + +// 6x9D: Unknown (not valid on Episode 3) + +struct G_Unknown_6x9D { + G_UnusedHeader header; + le_uint32_t client_id2 = 0; +} __packed_ws__(G_Unknown_6x9D, 8); + +// 6x9E: Play camera shutter sound +// This subcommand is only used on PSO PC and PC NTE. It is not implemented (and +// therefore ignored) by all prior versions, and all later versions have +// handlers for this command, but the handlers do nothing. + +struct G_PlayerCameraShutterSound_6x9E { + G_ClientIDHeader header; +} __packed_ws__(G_PlayerCameraShutterSound_6x9E, 4); + +// 6x9F: Gal Gryphon boss actions (not valid on pre-V3 or Episode 3) + +struct G_GalGryphonBossActions_6x9F { + G_EnemyIDHeader header; + le_uint32_t unknown_a1 = 0; + le_float unknown_a2 = 0.0f; + le_float unknown_a3 = 0.0f; +} __packed_ws__(G_GalGryphonBossActions_6x9F, 0x10); + +// 6xA0: Gal Gryphon boss actions (not valid on pre-V3 or Episode 3) + +struct G_GalGryphonBossActions_6xA0 { + G_EnemyIDHeader header; + le_float x = 0.0f; + le_float y = 0.0f; + le_float z = 0.0f; + le_uint32_t unknown_a1 = 0; + le_uint16_t unknown_a2 = 0; + le_uint16_t unknown_a3 = 0; + parray unknown_a4; +} __packed_ws__(G_GalGryphonBossActions_6xA0, 0x28); + +// 6xA1: Revive player (not valid on pre-V3) (protected on V3/V4) + +struct G_RevivePlayer_V3_BB_6xA1 { + G_ClientIDHeader header; +} __packed_ws__(G_RevivePlayer_V3_BB_6xA1, 4); + +// 6xA2: Specializable item drop request (not valid on pre-V3; handled by +// server on BB) + +struct G_SpecializableItemDropRequest_6xA2 : G_StandardDropItemRequest_PC_V3_BB_6x60 { + /* 18 */ le_float param3 = 0.0f; + /* 1C */ le_uint32_t param4 = 0; + /* 20 */ le_uint32_t param5 = 0; + /* 24 */ le_uint32_t param6 = 0; + /* 28 */ +} __packed_ws__(G_SpecializableItemDropRequest_6xA2, 0x28); + +// 6xA3: Olga Flow boss actions (not valid on pre-V3 or Episode 3) + +struct G_OlgaFlowBossActions_6xA3 { + G_EnemyIDHeader header; + uint8_t unknown_a1 = 0; + uint8_t unknown_a2 = 0; + parray unknown_a3; +} __packed_ws__(G_OlgaFlowBossActions_6xA3, 8); + +// 6xA4: Olga Flow phase 1 boss actions (not valid on pre-V3 or Episode 3) + +struct G_OlgaFlowPhase1BossActions_6xA4 { + G_EnemyIDHeader header; + uint8_t what = 0; + parray unknown_a3; +} __packed_ws__(G_OlgaFlowPhase1BossActions_6xA4, 8); + +// 6xA5: Olga Flow phase 2 boss actions (not valid on pre-V3 or Episode 3) + +struct G_OlgaFlowPhase2BossActions_6xA5 { + G_EnemyIDHeader header; + uint8_t what = 0; + parray unknown_a3; +} __packed_ws__(G_OlgaFlowPhase2BossActions_6xA5, 8); + +// 6xA6: Modify trade proposal (not valid on pre-V3) + +struct G_ModifyTradeProposal_6xA6 { + G_ClientIDHeader header; + uint8_t unknown_a1 = 0; // Must be < 8 + uint8_t unknown_a2 = 0; + parray unknown_a3; + le_uint32_t unknown_a4 = 0; + le_uint32_t unknown_a5 = 0; +} __packed_ws__(G_ModifyTradeProposal_6xA6, 0x10); + +// 6xA7: Unknown (not valid on pre-V3) +// This subcommand is completely ignored. + +// 6xA8: Gol Dragon boss actions (not valid on pre-V3 or Episode 3) + +template +struct G_GolDragonBossActionsT_6xA8 { + using F32T = typename std::conditional::type; + G_EnemyIDHeader header; + le_uint16_t unknown_a2 = 0; + le_uint16_t unknown_a3 = 0; + le_uint32_t unknown_a4 = 0; + F32T x = 0.0f; + F32T z = 0.0f; + uint8_t unknown_a5 = 0; + parray unused; +} __packed__; +using G_GolDragonBossActions_XB_BB_6xA8 = G_GolDragonBossActionsT_6xA8; +using G_GolDragonBossActions_GC_6xA8 = G_GolDragonBossActionsT_6xA8; +check_struct_size(G_GolDragonBossActions_XB_BB_6xA8, 0x18); +check_struct_size(G_GolDragonBossActions_GC_6xA8, 0x18); + +// 6xA9: Barba Ray boss actions (not valid on pre-V3 or Episode 3) + +struct G_BarbaRayBossActions_6xA9 { + G_EnemyIDHeader header; + le_uint16_t unknown_a1 = 0; + le_uint16_t unknown_a2 = 0; +} __packed_ws__(G_BarbaRayBossActions_6xA9, 8); + +// 6xAA: Barba Ray boss actions (not valid on pre-V3 or Episode 3) + +struct G_BarbaRayBossActions_6xAA { + G_EnemyIDHeader header; + le_uint16_t unknown_a1 = 0; + le_uint16_t unknown_a2 = 0; + le_uint32_t unknown_a3 = 0; +} __packed_ws__(G_BarbaRayBossActions_6xAA, 0x0C); + +// 6xAB: Create lobby chair (not valid on pre-V3) (protected on V3/V4) +// This command's appears to be different on GC NTE than on any other version. +// It's not known what it does. + +struct G_Unknown_GCNTE_6xAB { + G_EnemyIDHeader header; + le_uint16_t unknown_a1 = 0; + le_uint16_t unknown_a2 = 0; +} __packed_ws__(G_Unknown_GCNTE_6xAB, 8); + +struct G_CreateLobbyChair_6xAB { + G_ClientIDHeader header; + le_uint16_t unknown_a1 = 0; + le_uint16_t unknown_a2 = 0; +} __packed_ws__(G_CreateLobbyChair_6xAB, 8); + +// 6xAC: Unknown (not valid on pre-V3) (protected on V3/V4) +// This command's appears to be different on GC NTE than on any other version. +// It also seems that no version (other than perhaps GC NTE) ever sends this +// command. + +struct G_Unknown_GCNTE_6xAC { + G_EnemyIDHeader header; + le_uint16_t unknown_a1 = 0; + le_uint16_t unknown_a2 = 0; + le_uint32_t unknown_a3 = 0; +} __packed_ws__(G_Unknown_GCNTE_6xAC, 0x0C); + +struct G_Unknown_6xAC { + G_ClientIDHeader header; + le_uint32_t num_items = 0; + parray item_ids; +} __packed_ws__(G_Unknown_6xAC, 0x80); + +// 6xAD: Olga Flow subordinate boss actions (not valid on pre-V3, Episode 3, or +// GC Trial Edition) + +struct G_OlgaFlowSubordinateBossActions_6xAD { + G_UnusedHeader header; + // The first byte in this array seems to have a special meaning + parray unknown_a1; +} __packed_ws__(G_OlgaFlowSubordinateBossActions_6xAD, 0x44); + +// 6xAE: Set lobby chair state (sent by existing clients at join time) +// This subcommand is not valid on DC, PC, or GC Trial Edition. + +struct G_SetLobbyChairState_6xAE { + G_ClientIDHeader header; + le_uint16_t unknown_a1 = 0; + le_uint16_t unknown_a2 = 0; + le_uint32_t unknown_a3 = 0; + le_uint32_t unknown_a4 = 0; +} __packed_ws__(G_SetLobbyChairState_6xAE, 0x10); + +// 6xAF: Turn lobby chair (not valid on pre-V3 or GC Trial Edition) (protected on V3/V4) + +struct G_TurnLobbyChair_6xAF { + G_ClientIDHeader header; + le_uint32_t angle = 0; // In range [0x0000, 0xFFFF] +} __packed_ws__(G_TurnLobbyChair_6xAF, 8); + +// 6xB0: Move lobby chair (not valid on pre-V3 or GC Trial Edition) (protected on V3/V4) + +struct G_MoveLobbyChair_6xB0 { + G_ClientIDHeader header; + le_uint32_t unknown_a1 = 0; +} __packed_ws__(G_MoveLobbyChair_6xB0, 8); + +// 6xB1: Unknown (not valid on pre-V3 or GC Trial Edition) +// This subcommand is completely ignored. + +// 6xB2: Play sound from player (not valid on pre-V3 or GC Trial Edition) +// This command is sent when a snapshot is taken on PSO GC, but it can be used +// to play any sound, centered on the local player. If localize is FFFF, then +// the sound is not centered on the local player and is just played globally. + +struct G_PlaySoundFromPlayer_6xB2 { + G_UnusedHeader header; + uint8_t floor = 0; + uint8_t unused = 0; + le_uint16_t localize = 0; + le_uint32_t sound_id = 0; // 0x00051720 = camera shutter sound +} __packed_ws__(G_PlaySoundFromPlayer_6xB2, 0x0C); + +// 6xB3: Unknown (Xbox; voice chat) + +struct G_Unknown_XB_6xB3 { + G_ClientIDHeader header; + le_uint32_t num_frames; + // (0x0A * num_frames) bytes of data follows here. +} __packed_ws__(G_Unknown_XB_6xB3, 8); + +// 6xB3: CARD battle server data request (Episode 3) + +// CARD battle subcommands have multiple subsubcommands, which we name 6xBYxZZ, +// where Y = 3, 4, 5, or 6, and ZZ is any byte. The formats of these +// subsubcommands are described at the end of this file. + +// The common format for CARD battle subcommand headers is: +struct G_CardBattleCommandHeader { + uint8_t subcommand = 0x00; + uint8_t size = 0x00; + le_uint16_t unused1 = 0x0000; + uint8_t subsubcommand = 0x00; // See 6xBx subcommand table (after this table) + uint8_t sender_client_id = 0x00; + // If mask_key is nonzero, the remainder of the data (after unused2 in this + // struct) is encrypted using a simple algorithm, which is implemented in + // set_mask_for_ep3_game_command in SendCommands.cc. The Episode 3 client + // never sends commands that have a nonzero value in this field, but it does + // properly handle received commands with nonzero values in this field. + // This only applies to Episode 3 final - the Trial Edition does not support + // masking and may send uninitialized data in this field. + uint8_t mask_key = 0x00; + uint8_t unused2 = 0x00; +} __packed_ws__(G_CardBattleCommandHeader, 8); + +// Unlike all other 6x subcommands, the 6xB3 subcommand is sent to the server in +// a CA command instead of a 6x, C9, or CB command. (For this reason, we +// generally refer to 6xB3xZZ commands as CAxZZ commands instead.) The server is +// expected to reply to CA commands with one or more 6xB4 subcommands instead of +// forwarding them. The logic for doing so is implemented in Episode3/Server.cc +// and the surrounding classes. + +// The 6xB3 subcommand has a longer header than 6xB4 and 6xB5. This header is +// common to all 6xB3x (CAx) subcommands. +struct G_CardServerDataCommandHeader { + /* 00 */ uint8_t subcommand = 0xB3; + /* 01 */ uint8_t size = 0x00; + /* 02 */ le_uint16_t unused1 = 0x0000; + /* 04 */ uint8_t subsubcommand = 0x00; // See 6xBx subcommand table (after this table) + /* 05 */ uint8_t sender_client_id = 0x00; + /* 06 */ uint8_t mask_key = 0x00; // Same meaning as in G_CardBattleCommandHeader + /* 07 */ uint8_t unused2 = 0x00; + /* 08 */ be_uint32_t sequence_num = 0; + /* 0C */ be_uint32_t context_token = 0; + /* 10 */ +} __packed_ws__(G_CardServerDataCommandHeader, 0x10); + +// 6xB4: Unknown (Xbox; voice chat) + +struct G_Unknown_XB_6xB4 { + G_ClientIDHeader header; + le_uint32_t unknown_a1; +} __packed_ws__(G_Unknown_XB_6xB4, 8); + +// 6xB4: CARD battle server response (Episode 3) - see 6xB3 above +// 6xB5: CARD battle client command (Episode 3) - see 6xB3 above + +// 6xB5: BB shop request (handled by the server) + +struct G_ShopContentsRequest_BB_6xB5 { + G_UnusedHeader header; + le_uint32_t shop_type = 0; +} __packed_ws__(G_ShopContentsRequest_BB_6xB5, 8); + +// 6xB6: Episode 3 map list and map contents (server->client only) +// Unlike 6xB3-6xB5, these commands cannot be masked. Also unlike 6xB3-6xB5, +// there are only two subsubcommands, so we list them inline here. +// These subcommands can be rather large, so they should be sent with the 6C +// command instead of the 60 command. (The difference in header format, +// including the extended size field, is likely the reason for 6xB6 being a +// separate subcommand from the other CARD battle subcommands.) + +struct G_MapSubsubcommand_Ep3_6xB6 { + G_ExtendedHeaderT header; + uint8_t subsubcommand = 0; // 0x40 or 0x41 + parray unused; +} __packed_ws__(G_MapSubsubcommand_Ep3_6xB6, 0x0C); + +struct G_MapList_Ep3_6xB6x40 { + G_MapSubsubcommand_Ep3_6xB6 header; + le_uint16_t compressed_data_size = 0; + le_uint16_t unused = 0; + // PRS-compressed map list data follows here. newserv generates this from the + // map index when requested; see the MapList struct in Episode3/DataIndexes.hh + // and Episode3::MapIndex::get_compressed_map_list for details on the format. +} __packed_ws__(G_MapList_Ep3_6xB6x40, 0x10); + +struct G_MapData_Ep3_6xB6x41 { + G_MapSubsubcommand_Ep3_6xB6 header; + le_uint32_t map_number = 0; + le_uint16_t compressed_data_size = 0; + le_uint16_t unused = 0; + // PRS-compressed map data follows here (which decompresses to an + // Episode3::MapDefinition). +} __packed_ws__(G_MapData_Ep3_6xB6x41, 0x14); + +// 6xB6: BB shop contents (server->client only) + +struct G_ShopContents_BB_6xB6 { + G_UnusedHeader header; + uint8_t shop_type = 0; + uint8_t num_items = 0; + le_uint16_t unused = 0; + // Note: data2d of these entries should be the price + parray item_datas; +} __packed_ws__(G_ShopContents_BB_6xB6, 0x198); + +// 6xB7: Alias for 6xB3 (Episode 3 Trial Edition) +// This command behaves exactly the same as 6xB3. This alias exists only in +// Episode 3 Trial Edition; it was removed in the final release. + +// 6xB7: BB buy shop item (handled by the server) + +struct G_BuyShopItem_BB_6xB7 { + G_UnusedHeader header; + le_uint32_t shop_item_id = 0; + uint8_t shop_type = 0; + uint8_t item_index = 0; + uint8_t amount = 0; + uint8_t unknown_a1 = 0; // TODO: Probably actually unused; verify this +} __packed_ws__(G_BuyShopItem_BB_6xB7, 0x0C); + +// 6xB8: Alias for 6xB4 (Episode 3 Trial Edition) +// This command behaves exactly the same as 6xB4. This alias exists only in +// Episode 3 Trial Edition; it was removed in the final release. + +// 6xB8: BB identify item request (via tekker) (handled by the server) + +struct G_IdentifyItemRequest_6xB8 { + G_UnusedHeader header; + le_uint32_t item_id = 0; +} __packed_ws__(G_IdentifyItemRequest_6xB8, 8); + +// 6xB9: Alias for 6xB5 (Episode 3 Trial Edition) +// This command behaves exactly the same as 6xB5. This alias exists only in +// Episode 3 Trial Edition; it was removed in the final release. + +// 6xB9: BB provisional tekker result + +struct G_IdentifyResult_BB_6xB9 { + G_ClientIDHeader header; + ItemData item_data; +} __packed_ws__(G_IdentifyResult_BB_6xB9, 0x18); + +// 6xBA: Sync card trade state (Episode 3) +// This command calls various member functions in TCardTradeServer. + +struct G_SyncCardTradeState_Ep3_6xBA { + G_ClientIDHeader header; + le_uint16_t what = 0; // Low byte must be < 9; this indexes into a handler table + le_uint16_t unknown_a2 = 0; + le_uint32_t unknown_a3 = 0; + le_uint32_t unknown_a4 = 0; +} __packed_ws__(G_SyncCardTradeState_Ep3_6xBA, 0x10); + +// 6xBA: BB accept tekker result (handled by the server) + +struct G_AcceptItemIdentification_BB_6xBA { + G_UnusedHeader header; + le_uint32_t item_id = 0; +} __packed_ws__(G_AcceptItemIdentification_BB_6xBA, 8); + +// 6xBB: Sync card trade state (Episode 3) +// This command calls various member functions in TCardTradeServer. +// TODO: Certain invalid values for slot/args in this command can crash the +// client (what is properly bounds-checked). Find out the actual limits for +// slot/args and make newserv enforce them. + +struct G_SyncCardTradeState_Ep3_6xBB { + G_ClientIDHeader header; + le_uint16_t what = 0; // Must be < 5; this indexes into a jump table + le_uint16_t slot = 0; + parray args; +} __packed_ws__(G_SyncCardTradeState_Ep3_6xBB, 0x18); + +// 6xBB: BB bank request (handled by the server) + +// 6xBC: Card counts (Episode 3) +// This is sent by the client in response to a 6xB5x38 command. +// It's possible that this is an early, now-unused implementation of the CAx49 +// command. When the client receives this command, it copies the data into a +// globally-allocated array, but nothing reads from this array. Curiously, this +// command is smaller than 0x400 bytes, but uses the extended subcommand format +// anyway (and uses the 6D command rather than 62). + +struct G_CardCounts_Ep3_6xBC { + G_UnusedHeader header; + le_uint32_t size = 0; + parray unknown_a1; + // The client sends uninitialized data in this field + parray unused; +} __packed_ws__(G_CardCounts_Ep3_6xBC, 0x2FC); + +// 6xBC: BB bank contents (server->client only) + +struct G_BankContentsHeader_BB_6xBC { + G_ExtendedHeaderT header; + le_uint32_t checksum = 0; // can be random; client won't notice + le_uint32_t num_items = 0; + le_uint32_t meseta = 0; + // Item data follows +} __packed_ws__(G_BankContentsHeader_BB_6xBC, 0x14); + +// 6xBD: Word select during battle (Episode 3; not Trial Edition) + +// Note: This structure does not have a normal header - the client ID field is +// big-endian! +struct G_WordSelectDuringBattle_Ep3_6xBD { + G_ClientIDHeader header; + le_uint16_t unknown_a1 = 0; + le_uint16_t unknown_a2 = 0; + parray entries; + le_uint32_t unknown_a4 = 0; + le_uint32_t unknown_a5 = 0; + // This field has the same meaning as the first byte in an 06 command's + // message when sent during an Episode 3 battle. + uint8_t private_flags = 0; + parray unused; +} __packed_ws__(G_WordSelectDuringBattle_Ep3_6xBD, 0x24); + +// 6xBD: BB bank action (take/deposit meseta/item) (handled by the server) + +struct G_BankAction_BB_6xBD { + G_UnusedHeader header; + le_uint32_t item_id = 0; + le_uint32_t meseta_amount = 0; + uint8_t action = 0; // 0 = deposit, 1 = take, 3 = done (close bank window) + uint8_t item_amount = 0; + le_uint16_t item_index = 0; // 0xFFFF = meseta +} __packed_ws__(G_BankAction_BB_6xBD, 0x10); + +// 6xBE: Sound chat (Episode 3; not Trial Edition) +// This is the only subcommand ever sent with the CB command. + +struct G_SoundChat_Ep3_6xBE { + G_UnusedHeader header; + le_uint32_t sound_id = 0; // Must be < 0x27 + be_uint32_t unused = 0; +} __packed_ws__(G_SoundChat_Ep3_6xBE, 0x0C); + +// 6xBE: BB create inventory item (server->client only) + +struct G_CreateInventoryItem_BB_6xBE { + G_ClientIDHeader header; + ItemData item_data; + le_uint32_t unused = 0; +} __packed_ws__(G_CreateInventoryItem_BB_6xBE, 0x1C); + +// 6xBF: Change lobby music (Episode 3; not Trial Edition) + +struct G_ChangeLobbyMusic_Ep3_6xBF { + G_UnusedHeader header; + le_uint32_t song_number = 0; // Must be < 0x34 +} __packed_ws__(G_ChangeLobbyMusic_Ep3_6xBF, 8); + +// 6xBF: Give EXP (BB) (server->client only) + +struct G_GiveExperience_BB_6xBF { + G_ClientIDHeader header; + le_uint32_t amount = 0; +} __packed_ws__(G_GiveExperience_BB_6xBF, 8); + +// 6xC0: Sell item at shop (BB) (protected on V3/V4) + +struct G_SellItemAtShop_BB_6xC0 { + G_UnusedHeader header; + le_uint32_t item_id = 0; + le_uint32_t amount = 0; +} __packed_ws__(G_SellItemAtShop_BB_6xC0, 0x0C); + +// 6xC1: Invite to team (BB) +// 6xC2: Accept invitation to team (BB) + +struct G_TeamInvitationAction_BB_6xC1_6xC2_6xCD_6xCE { + G_ClientIDHeader header; + le_uint32_t guild_card_number = 0; + le_uint32_t action = 0; // 0 or 1 for 6xC1, 2 (or not 2) for 6xC2 + parray unknown_a1; +} __packed_ws__(G_TeamInvitationAction_BB_6xC1_6xC2_6xCD_6xCE, 0x60); + +// 6xC3: Split stacked item (BB; handled by the server) +// Note: This is not sent if an entire stack is dropped; in that case, a normal +// item drop subcommand is generated instead. + +struct G_SplitStackedItem_BB_6xC3 { + G_ClientIDHeader header; + le_uint16_t floor = 0; + le_uint16_t unused2 = 0; + le_float x = 0.0f; + le_float z = 0.0f; + le_uint32_t item_id = 0; + le_uint32_t amount = 0; +} __packed_ws__(G_SplitStackedItem_BB_6xC3, 0x18); + +// 6xC4: Sort inventory (BB; handled by the server) + +struct G_SortInventory_BB_6xC4 { + G_UnusedHeader header; + parray item_ids; +} __packed_ws__(G_SortInventory_BB_6xC4, 0x7C); + +// 6xC5: Medical center used (BB) + +struct G_MedicalCenterUsed_BB_6xC5 { + G_ClientIDHeader header; +} __packed_ws__(G_MedicalCenterUsed_BB_6xC5, 4); + +// 6xC6: Steal experience (BB) + +struct G_StealEXP_BB_6xC6 { + G_ClientIDHeader header; + le_uint16_t entity_id = 0; + le_uint16_t enemy_index = 0; +} __packed_ws__(G_StealEXP_BB_6xC6, 8); + +// 6xC7: Charge attack (BB) + +struct G_ChargeAttack_BB_6xC7 { + G_ClientIDHeader header; + // Tethealla (at least, the ancient public version of it) treats this as + // signed, and gives the player money in that case. We don't do so. + le_uint32_t meseta_amount = 0; +} __packed_ws__(G_ChargeAttack_BB_6xC7, 8); + +// 6xC8: Enemy EXP request (BB; handled by the server) + +struct G_EnemyEXPRequest_BB_6xC8 { + G_EnemyIDHeader header; + le_uint16_t enemy_index = 0; + le_uint16_t requesting_client_id = 0; + uint8_t is_killer = 0; + parray unused; +} __packed_ws__(G_EnemyEXPRequest_BB_6xC8, 0x0C); + +// 6xC9: Adjust player Meseta (BB; handled by server) + +struct G_AdjustPlayerMeseta_BB_6xC9 { + G_UnusedHeader header; + le_int32_t amount = 0; +} __packed_ws__(G_AdjustPlayerMeseta_BB_6xC9, 8); + +// 6xCA: Request item reward from quest (BB; handled by server) + +struct G_ItemRewardRequest_BB_6xCA { + G_UnusedHeader header; + ItemData item_data; +} __packed_ws__(G_ItemRewardRequest_BB_6xCA, 0x18); + +// 6xCB: Transfer item via mail message (BB) + +struct G_TransferItemViaMailMessage_BB_6xCB { + G_ClientIDHeader header; + le_uint32_t item_id = 0; + le_uint32_t amount = 0; + le_uint32_t target_guild_card_number = 0; +} __packed_ws__(G_TransferItemViaMailMessage_BB_6xCB, 0x10); + +// 6xCC: Exchange item for team points (BB) (protected on V3/V4) + +struct G_ExchangeItemForTeamPoints_BB_6xCC { + G_ClientIDHeader header; + le_uint32_t item_id = 0; + le_uint32_t amount = 0; +} __packed_ws__(G_ExchangeItemForTeamPoints_BB_6xCC, 0x0C); + +// 6xCD: Transfer master (BB) +// 6xCE: Accept master transfer (BB) +// Same format as 6xC1 + +// 6xCF: Start battle (BB) + +struct G_StartBattle_BB_6xCF { + G_UnusedHeader header; + BattleRules rules; +} __packed_ws__(G_StartBattle_BB_6xCF, 0x34); + +// 6xD0: Battle mode level up (BB; handled by server) +// Requests the client to be leveled up by num_levels levels. The server should +// respond with a 6x30 command. + +struct G_BattleModeLevelUp_BB_6xD0 { + G_ClientIDHeader header; + le_uint32_t num_levels = 0; +} __packed_ws__(G_BattleModeLevelUp_BB_6xD0, 8); + +// 6xD1: Request Challenge Mode grave recovery item (BB; handled by server) + +struct G_ChallengeModeGraveRecoveryItemRequest_BB_6xD1 { + G_ClientIDHeader header; + le_uint16_t floor = 0; + le_uint16_t unknown_a1 = 0; + le_float x = 0; + le_float z = 0; + le_uint32_t item_type = 0; // Should be < 6 +} __packed_ws__(G_ChallengeModeGraveRecoveryItemRequest_BB_6xD1, 0x14); + +// 6xD2: Set quest counter (BB) +// Writes 4 bytes to the 32-bit field specified by index. + +struct G_SetQuestCounter_BB_6xD2 { + G_ClientIDHeader header; + le_uint32_t index = 0; // There are 0x10 of them (0x00-0x0F) + le_uint32_t value = 0; +} __packed_ws__(G_SetQuestCounter_BB_6xD2, 0x0C); + +// 6xD3: Invalid subcommand + +// 6xD4: Unknown (BB) + +struct G_Unknown_BB_6xD4 { + G_UnusedHeader header; + le_uint16_t action = 0; // Must be in [0, 5] + uint8_t unknown_a1 = 0; // Must be in [0, 15] + uint8_t unused = 0; +} __packed_ws__(G_Unknown_BB_6xD4, 8); + +// 6xD5: Exchange item in quest (BB; handled by server) +// The client sends this when it executes an F953 quest opcode. + +struct G_ExchangeItemInQuest_BB_6xD5 { + G_ClientIDHeader header; + ItemData find_item; // Only data1[0]-[2] are used + ItemData replace_item; // Only data1[0]-[2] are used + le_uint16_t success_function_id = 0; + le_uint16_t failure_function_id = 0; +} __packed_ws__(G_ExchangeItemInQuest_BB_6xD5, 0x30); + +// 6xD6: Wrap item (BB; handled by server) + +struct G_WrapItem_BB_6xD6 { + G_ClientIDHeader header; + ItemData item; + uint8_t unknown_a1 = 0; + parray unused; +} __packed_ws__(G_WrapItem_BB_6xD6, 0x1C); + +// 6xD7: Paganini Photon Drop exchange (BB; handled by server) +// The client sends this when it executes an F955 quest opcode. + +struct G_PaganiniPhotonDropExchange_BB_6xD7 { + G_ClientIDHeader header; + ItemData new_item; // Only data1[0]-[2] are used + le_uint16_t success_function_id = 0; + le_uint16_t failure_function_id = 0; +} __packed_ws__(G_PaganiniPhotonDropExchange_BB_6xD7, 0x1C); + +// 6xD8: Add S-rank weapon special (BB; handled by server) +// The client sends this when it executes an F956 quest opcode. + +struct G_AddSRankWeaponSpecial_BB_6xD8 { + G_ClientIDHeader header; + ItemData unknown_a1; // Only data1[0]-[2] are used + le_uint32_t item_id = 0; + le_uint32_t special_type = 0; + le_uint16_t success_function_id = 0; + le_uint16_t failure_function_id = 0; +} __packed_ws__(G_AddSRankWeaponSpecial_BB_6xD8, 0x24); + +// 6xD9: Momoka item exchange (BB; handled by server) +// The client sends this when it executes an F95B quest opcode. + +struct G_MomokaItemExchange_BB_6xD9 { + G_ClientIDHeader header; + ItemData find_item; // Only data1[0]-[2] are used + ItemData replace_item; // Only data1[0]-[2] are used + le_uint32_t unknown_a3 = 0; + le_uint32_t unknown_a4 = 0; + le_uint16_t unknown_a5 = 0; + le_uint16_t unknown_a6 = 0; +} __packed_ws__(G_MomokaItemExchange_BB_6xD9, 0x38); + +// 6xDA: Upgrade weapon attribute (BB; handled by server) +// The client sends this when it executes an F957 or F958 quest opcode. + +struct G_UpgradeWeaponAttribute_BB_6xDA { + G_ClientIDHeader header; + ItemData item; // Only data1[0-2] are used (argsA[1-3]) + le_uint32_t item_id = 0; // argsA[0] + le_uint32_t attribute = 0; // argsA[4] + le_uint32_t payment_count = 0; // Number of PD or PS (argsA[5]) + le_uint32_t payment_type = 0; // 0 = Photon Drops, 1 = Photon Spheres + le_uint16_t success_function_id = 0; // argsA[6] + le_uint16_t failure_function_id = 0; // argsA[7] +} __packed_ws__(G_UpgradeWeaponAttribute_BB_6xDA, 0x2C); + +// 6xDB: Exchange item in quest (BB) + +struct G_ExchangeItemInQuest_BB_6xDB { + G_ClientIDHeader header; + le_uint32_t unknown_a1 = 0; + le_uint32_t item_id = 0; + le_uint32_t amount = 0; +} __packed_ws__(G_ExchangeItemInQuest_BB_6xDB, 0x10); + +// 6xDC: Saint-Million boss actions (BB) + +struct G_SaintMillionBossActions_BB_6xDC { + G_UnusedHeader header; + le_uint16_t unknown_a1 = 0; + le_uint16_t unknown_a2 = 0; +} __packed_ws__(G_SaintMillionBossActions_BB_6xDC, 8); + +// 6xDD: Set EXP multiplier (BB) +// header.param specifies the EXP multiplier. It is 1-based, so the value 2 +// means all EXP is doubled, for example. + +struct G_SetEXPMultiplier_BB_6xDD { + G_ParameterHeader header; +} __packed_ws__(G_SetEXPMultiplier_BB_6xDD, 4); + +// 6xDE: Exchange Secret Lottery Ticket (BB; handled by server) +// The client sends this when it executes an F95C quest opcode. + +struct G_ExchangeSecretLotteryTicket_BB_6xDE { + G_ClientIDHeader header; + uint8_t index = 0; + uint8_t function_id1 = 0; + le_uint16_t function_id2 = 0; +} __packed_ws__(G_ExchangeSecretLotteryTicket_BB_6xDE, 8); + +// 6xDF: Exchange Photon Crystals (BB; handled by server) +// The client sends this when it executes an F95D quest opcode. + +struct G_ExchangePhotonCrystals_BB_6xDF { + G_ClientIDHeader header; +} __packed_ws__(G_ExchangePhotonCrystals_BB_6xDF, 4); + +// 6xE0: Request item drop from quest (BB; handled by server) +// The client sends this when it executes an F95E quest opcode. + +struct G_RequestItemDropFromQuest_BB_6xE0 { + G_ClientIDHeader header; + uint8_t floor = 0; + uint8_t type = 0; // argsA[0] + uint8_t unknown_a3 = 0; + uint8_t unused = 0; + le_float x = 0.0f; // argsA[1] + le_float z = 0.0f; // argsA[2] +} __packed_ws__(G_RequestItemDropFromQuest_BB_6xE0, 0x10); + +// 6xE1: Exchange Photon Tickets (BB; handled by server) +// The client sends this when it executes an F95F quest opcode. + +struct G_ExchangePhotonTickets_BB_6xE1 { + G_ClientIDHeader header; + uint8_t unknown_a1 = 0; // argsA[0] + uint8_t unknown_a2 = 0; // argsA[1] + uint8_t result_index = 0; // argsA[2] + uint8_t unused = 0; + le_uint16_t function_id1 = 0; // argsA[3] + le_uint16_t unknown_a5 = 0; // argsA[4] +} __packed_ws__(G_ExchangePhotonTickets_BB_6xE1, 0x0C); + +// 6xE2: Get Meseta slot prize (BB) +// The client sends this when it executes an F960 quest opcode. + +struct G_GetMesetaSlotPrize_BB_6xE2 { + G_ClientIDHeader header; + uint8_t result_tier; // This contains the argument value from the F960 opcode + uint8_t floor; + uint8_t unknown_a2; + uint8_t unused; + le_float x; // TODO: Verify this guess + le_float z; // TODO: Verify this guess +} __packed_ws__(G_GetMesetaSlotPrize_BB_6xE2, 0x10); + +// 6xE3: Set Meseta slot prize result (BB) +// The client only uses this to populate the quest text +// replacement token. + +struct G_SetMesetaSlotPrizeResult_BB_6xE3 { + G_ClientIDHeader header; + ItemData item; +} __packed_ws__(G_SetMesetaSlotPrizeResult_BB_6xE3, 0x18); + +// 6xE4: Invalid subcommand +// 6xE5: Invalid subcommand +// 6xE6: Invalid subcommand +// 6xE7: Invalid subcommand +// 6xE8: Invalid subcommand +// 6xE9: Invalid subcommand +// 6xEA: Invalid subcommand +// 6xEB: Invalid subcommand +// 6xEC: Invalid subcommand +// 6xED: Invalid subcommand +// 6xEE: Invalid subcommand +// 6xEF: Invalid subcommand +// 6xF0: Invalid subcommand +// 6xF1: Invalid subcommand +// 6xF2: Invalid subcommand +// 6xF3: Invalid subcommand +// 6xF4: Invalid subcommand +// 6xF5: Invalid subcommand +// 6xF6: Invalid subcommand +// 6xF7: Invalid subcommand +// 6xF8: Invalid subcommand +// 6xF9: Invalid subcommand +// 6xFA: Invalid subcommand +// 6xFB: Invalid subcommand +// 6xFC: Invalid subcommand +// 6xFD: Invalid subcommand +// 6xFE: Invalid subcommand +// 6xFF: Invalid subcommand + +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +// EPISODE 3 CARD BATTLE SUBSUBCOMMANDS //////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +// The Episode 3 CARD battle subsubcommands are used in commands 6xB3, 6xB4, and +// 6xB5. Note that even though there's no overlap in the subsubcommand number +// space, the various subsubcommands must be used with the correct 6xBx +// subcommand - the client will ignore the command if sent via the wrong 6xBx +// subcommand. (For example, sending a 6xB5x02 command will do nothing because +// subsubcommand 02 is only valid for the 6xB4 subcommand.) This table is known +// to be complete, so invalid commands are not listed. + +// In general, 6xB3 (CAx) commands are sent by the client when it wants to take +// an action that affects game state held on the server side. The server will +// send one or more 6xB4 commands in response to update all clients' views of +// the game state. 6xB5 commands do not affect state held on the server side, +// and are generally only of concern on the client side. + +// 6xB4x02: Update hand and equips + +struct G_UpdateHand_Ep3_6xB4x02 { + /* 00 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateHand_Ep3_6xB4x02) / 4, 0, 0x02, 0, 0, 0}; + /* 08 */ le_uint16_t client_id = 0; + /* 0A */ le_uint16_t unused = 0; + /* 0C */ Episode3::HandAndEquipState state; + /* 60 */ +} __packed_ws__(G_UpdateHand_Ep3_6xB4x02, 0x60); + +// 6xB4x03: Set state flags + +struct G_SetStateFlags_Ep3_6xB4x03 { + /* 00 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetStateFlags_Ep3_6xB4x03) / 4, 0, 0x03, 0, 0, 0}; + /* 08 */ Episode3::StateFlags state; + /* 20 */ +} __packed_ws__(G_SetStateFlags_Ep3_6xB4x03, 0x20); + +// 6xB4x04: Update SC/FC short statuses + +struct G_UpdateShortStatuses_Ep3_6xB4x04 { + /* 0000 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateShortStatuses_Ep3_6xB4x04) / 4, 0, 0x04, 0, 0, 0}; + /* 0008 */ le_uint16_t client_id = 0; + /* 000A */ le_uint16_t unused = 0; + // The slots in this array have heterogeneous meanings. Specifically: + // [0] is the SC card status + // [1] through [6] are hand cards + // [7] through [14] are set FC cards (items/creatures) + // [15] is the set assist card + /* 000C */ parray card_statuses; + /* 010C */ +} __packed_ws__(G_UpdateShortStatuses_Ep3_6xB4x04, 0x10C); + +// 6xB4x05: Update map state +// TODO: This structure is different on Ep3 NTE because the Rules structure is +// shorter. Define an appropriate structure for this. + +struct G_UpdateMap_Ep3NTE_6xB4x05 { + /* 0000 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateMap_Ep3NTE_6xB4x05) / 4, 0, 0x05, 0, 0, 0}; + /* 0008 */ Episode3::MapAndRulesStateTrial state; + /* 0138 */ uint8_t start_battle = 0; + /* 0139 */ parray unused; + /* 013C */ +} __packed_ws__(G_UpdateMap_Ep3NTE_6xB4x05, 0x13C); + +struct G_UpdateMap_Ep3_6xB4x05 { + /* 0000 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateMap_Ep3_6xB4x05) / 4, 0, 0x05, 0, 0, 0}; + /* 0008 */ Episode3::MapAndRulesState state; + /* 0140 */ uint8_t start_battle = 0; + /* 0141 */ parray unused; + /* 0144 */ +} __packed_ws__(G_UpdateMap_Ep3_6xB4x05, 0x144); + +// 6xB4x06: Apply condition effect + +struct G_ApplyConditionEffect_Ep3_6xB4x06 { + /* 00 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_ApplyConditionEffect_Ep3_6xB4x06) / 4, 0, 0x06, 0, 0, 0}; + /* 08 */ Episode3::EffectResult effect; + /* 14 */ +} __packed_ws__(G_ApplyConditionEffect_Ep3_6xB4x06, 0x14); + +// 6xB4x07: Set battle decks + +struct G_UpdateDecks_Ep3_6xB4x07 { + /* 0000 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateDecks_Ep3_6xB4x07) / 4, 0, 0x07, 0, 0, 0}; + /* 0008 */ parray entries_present; + /* 000C */ parray entries; + /* 016C */ +} __packed_ws__(G_UpdateDecks_Ep3_6xB4x07, 0x16C); + +// 6xB4x09: Set action state + +struct G_SetActionState_Ep3_6xB4x09 { + /* 00 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetActionState_Ep3_6xB4x09) / 4, 0, 0x09, 0, 0, 0}; + /* 08 */ le_uint16_t client_id = 0; + /* 0A */ parray unknown_a1; + /* 0C */ Episode3::ActionState state; + /* 70 */ +} __packed_ws__(G_SetActionState_Ep3_6xB4x09, 0x70); + +// 6xB4x0A: Update action chain and metadata +// This command is used by Trial Edition. The final version sends 6xB4x4C, +// 6xB4x4D, and 6xB4x4E instead, but still has a handler for this command. + +struct G_UpdateActionChainAndMetadata_Ep3NTE_6xB4x0A { + /* 0000 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateActionChainAndMetadata_Ep3NTE_6xB4x0A) / 4, 0, 0x0A, 0, 0, 0}; + /* 0008 */ le_uint16_t client_id = 0; + // set_index must be 0xFF, or be in the range [0, 9]. If it's 0xFF, all nine + // chains and metadatas are cleared for the client; otherwise, the provided + // chain and metadata are copied into the slot specified by set_index. + /* 000A */ int8_t index = 0; + /* 000B */ uint8_t unused = 0; + /* 000C */ Episode3::ActionChainWithCondsTrial chain; + /* 010C */ Episode3::ActionMetadata metadata; + /* 0180 */ +} __packed_ws__(G_UpdateActionChainAndMetadata_Ep3NTE_6xB4x0A, 0x180); + +struct G_UpdateActionChainAndMetadata_Ep3_6xB4x0A { + /* 0000 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateActionChainAndMetadata_Ep3_6xB4x0A) / 4, 0, 0x0A, 0, 0, 0}; + /* 0008 */ le_uint16_t client_id = 0; + /* 000A */ int8_t index = 0; + /* 000B */ uint8_t unused = 0; + /* 000C */ Episode3::ActionChainWithConds chain; + /* 010C */ Episode3::ActionMetadata metadata; + /* 0180 */ +} __packed_ws__(G_UpdateActionChainAndMetadata_Ep3_6xB4x0A, 0x180); + +// 6xB3x0B / CAx0B: Redraw initial hand (immediately before battle) + +struct G_RedrawInitialHand_Ep3_CAx0B { + G_CardServerDataCommandHeader header = {0xB3, sizeof(G_RedrawInitialHand_Ep3_CAx0B) / 4, 0, 0x0B, 0, 0, 0, 0, 0}; + le_uint16_t client_id = 0; + parray unused2; +} __packed_ws__(G_RedrawInitialHand_Ep3_CAx0B, 0x14); + +// 6xB3x0C / CAx0C: End initial redraw phase + +struct G_EndInitialRedrawPhase_Ep3_CAx0C { + G_CardServerDataCommandHeader header = {0xB3, sizeof(G_EndInitialRedrawPhase_Ep3_CAx0C) / 4, 0, 0x0C, 0, 0, 0, 0, 0}; + le_uint16_t client_id = 0; + parray unused2; +} __packed_ws__(G_EndInitialRedrawPhase_Ep3_CAx0C, 0x14); + +// 6xB3x0D / CAx0D: End non-action phase +// This command is sent when the client has no more actions to take during the +// current phase. This command isn't used for ending the attack or defense +// phases; for those phases, CAx12 and CAx28 are used instead. + +struct G_EndNonAttackPhase_Ep3_CAx0D { + G_CardServerDataCommandHeader header = {0xB3, sizeof(G_EndNonAttackPhase_Ep3_CAx0D) / 4, 0, 0x0D, 0, 0, 0, 0, 0}; + le_uint16_t client_id = 0; + le_uint16_t battle_phase = 0; // Only used on NTE + parray unused2; +} __packed_ws__(G_EndNonAttackPhase_Ep3_CAx0D, 0x1C); + +// 6xB3x0E / CAx0E: Discard card from hand + +struct G_DiscardCardFromHand_Ep3_CAx0E { + G_CardServerDataCommandHeader header = {0xB3, sizeof(G_DiscardCardFromHand_Ep3_CAx0E) / 4, 0, 0x0E, 0, 0, 0, 0, 0}; + le_uint16_t client_id = 0; + le_uint16_t card_ref = 0xFFFF; +} __packed_ws__(G_DiscardCardFromHand_Ep3_CAx0E, 0x14); + +// 6xB3x0F / CAx0F: Set card from hand + +struct G_SetCardFromHand_Ep3_CAx0F { + G_CardServerDataCommandHeader header = {0xB3, sizeof(G_SetCardFromHand_Ep3_CAx0F) / 4, 0, 0x0F, 0, 0, 0, 0, 0}; + le_uint16_t client_id = 0; + le_uint16_t card_ref = 0xFFFF; + le_uint16_t set_index = 0; + le_uint16_t assist_target_player = 0; + Episode3::Location loc; +} __packed_ws__(G_SetCardFromHand_Ep3_CAx0F, 0x1C); + +// 6xB3x10 / CAx10: Move field character + +struct G_MoveFieldCharacter_Ep3_CAx10 { + G_CardServerDataCommandHeader header = {0xB3, sizeof(G_MoveFieldCharacter_Ep3_CAx10) / 4, 0, 0x10, 0, 0, 0, 0, 0}; + le_uint16_t client_id = 0; + le_uint16_t set_index = 0; + Episode3::Location loc; +} __packed_ws__(G_MoveFieldCharacter_Ep3_CAx10, 0x18); + +// 6xB3x11 / CAx11: Enqueue action (play card(s) during action phase) +// This command is used for playing both attacks (and the associated action +// cards), and for playing defense cards. In the attack case, this command is +// sent once for each attack (even if it includes multiple cards); in the +// defense case, this command is sent once for each defense card. + +struct G_EnqueueAttackOrDefense_Ep3_CAx11 { + G_CardServerDataCommandHeader header = {0xB3, sizeof(G_EnqueueAttackOrDefense_Ep3_CAx11) / 4, 0, 0x11, 0, 0, 0, 0, 0}; + le_uint16_t client_id = 0; + parray unused2; + Episode3::ActionState entry; +} __packed_ws__(G_EnqueueAttackOrDefense_Ep3_CAx11, 0x78); + +// 6xB3x12 / CAx12: End attack list (done playing cards during action phase) +// This command informs the server that the client is done playing attacks in +// the current round. (In the defense phase, CAx28 is used instead.) + +struct G_EndAttackList_Ep3_CAx12 { + G_CardServerDataCommandHeader header = {0xB3, sizeof(G_EndAttackList_Ep3_CAx12) / 4, 0, 0x12, 0, 0, 0, 0, 0}; + le_uint16_t client_id = 0; + parray unused2; +} __packed_ws__(G_EndAttackList_Ep3_CAx12, 0x14); + +// 6xB3x13 / CAx13: Set map state during setup + +struct G_SetMapState_Ep3NTE_CAx13 { + G_CardServerDataCommandHeader header = {0xB3, sizeof(G_SetMapState_Ep3NTE_CAx13) / 4, 0, 0x13, 0, 0, 0, 0, 0}; + Episode3::MapAndRulesStateTrial map_and_rules_state; + Episode3::OverlayState overlay_state; +} __packed_ws__(G_SetMapState_Ep3NTE_CAx13, 0x2B4); + +struct G_SetMapState_Ep3_CAx13 { + G_CardServerDataCommandHeader header = {0xB3, sizeof(G_SetMapState_Ep3_CAx13) / 4, 0, 0x13, 0, 0, 0, 0, 0}; + Episode3::MapAndRulesState map_and_rules_state; + Episode3::OverlayState overlay_state; +} __packed_ws__(G_SetMapState_Ep3_CAx13, 0x2BC); + +// 6xB3x14 / CAx14: Set player deck during setup + +struct G_SetPlayerDeck_Ep3_CAx14 { + /* 00 */ G_CardServerDataCommandHeader header = {0xB3, sizeof(G_SetPlayerDeck_Ep3_CAx14) / 4, 0, 0x14, 0, 0, 0, 0, 0}; + /* 10 */ le_uint16_t client_id = 0; + /* 12 */ uint8_t is_cpu_player = 0; + /* 13 */ uint8_t unused2 = 0; + /* 14 */ Episode3::DeckEntry entry; + /* 6C */ +} __packed_ws__(G_SetPlayerDeck_Ep3_CAx14, 0x6C); + +// 6xB3x15 / CAx15: Hard-reset server state +// This command appears to be completely unused; the client never sends it. + +struct G_HardResetServerState_Ep3_CAx15 { + G_CardServerDataCommandHeader header = {0xB3, sizeof(G_HardResetServerState_Ep3_CAx15) / 4, 0, 0x15, 0, 0, 0, 0, 0}; + // No arguments +} __packed_ws__(G_HardResetServerState_Ep3_CAx15, 0x10); + +// 6xB5x17: Unknown +// TODO: Document this from Episode 3 client/server disassembly + +struct G_Unknown_Ep3_6xB5x17 { + G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_Ep3_6xB5x17) / 4, 0, 0x17, 0, 0, 0}; + // No arguments +} __packed_ws__(G_Unknown_Ep3_6xB5x17, 8); + +// 6xB5x1A: Force disconnect +// This command seems to cause the client to unconditionally disconnect. The +// player is returned to the main menu (the "The line was disconnected" message +// box is skipped). Unlike all other known ways to disconnect, the client does +// not save when it receives this command, and instead returns directly to the +// main menu. + +struct G_ForceDisconnect_Ep3_6xB5x1A { + G_CardBattleCommandHeader header = {0xB5, sizeof(G_ForceDisconnect_Ep3_6xB5x1A) / 4, 0, 0x1A, 0, 0, 0}; + // No arguments +} __packed_ws__(G_ForceDisconnect_Ep3_6xB5x1A, 8); + +// 6xB3x1B / CAx1B: Set player name during setup +// Curiously, this command can be used during a non-setup phase; the server +// should ignore the command's contents but still send a 6xB4x1C in response. + +struct G_SetPlayerName_Ep3_CAx1B { + G_CardServerDataCommandHeader header = {0xB3, sizeof(G_SetPlayerName_Ep3_CAx1B) / 4, 0, 0x1B, 0, 0, 0, 0, 0}; + Episode3::NameEntry entry; +} __packed_ws__(G_SetPlayerName_Ep3_CAx1B, 0x24); + +// 6xB4x1C: Set all player names + +struct G_SetPlayerNames_Ep3_6xB4x1C { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetPlayerNames_Ep3_6xB4x1C) / 4, 0, 0x1C, 0, 0, 0}; + parray entries; +} __packed_ws__(G_SetPlayerNames_Ep3_6xB4x1C, 0x58); + +// 6xB3x1D / CAx1D: Request for battle start +// The battle actually begins when the server sends a state flags update (in +// response to this command) that includes RegistrationPhase::BATTLE_STARTED and +// a SetupPhase value other than REGISTRATION. + +struct G_StartBattle_Ep3_CAx1D { + G_CardServerDataCommandHeader header = {0xB3, sizeof(G_StartBattle_Ep3_CAx1D) / 4, 0, 0x1D, 0, 0, 0, 0, 0}; +} __packed_ws__(G_StartBattle_Ep3_CAx1D, 0x10); + +// 6xB4x1E: Action result + +struct G_ActionResult_Ep3_6xB4x1E { + /* 00 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_ActionResult_Ep3_6xB4x1E) / 4, 0, 0x1E, 0, 0, 0}; + /* 08 */ be_uint32_t sequence_num = 0; + /* 0C */ uint8_t error_code = 0; + /* 0D */ uint8_t response_phase = 0; + /* 0E */ parray unused; + /* 10 */ +} __packed_ws__(G_ActionResult_Ep3_6xB4x1E, 0x10); + +// 6xB4x1F: Set context token +// This token is sent back in the context_token field of all CA commands from +// the client. It seems Sega never used this functionality. + +struct G_SetContextToken_Ep3_6xB4x1F { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetContextToken_Ep3_6xB4x1F) / 4, 0, 0x1F, 0, 0, 0}; + // Note that this field is little-endian, but the corresponding context_token + // field in G_CardServerDataCommandHeader is big-endian! + le_uint32_t context_token = 0; +} __packed_ws__(G_SetContextToken_Ep3_6xB4x1F, 0x0C); + +// 6xB5x20: Unknown +// TODO: Document this from Episode 3 client/server disassembly + +struct G_Unknown_Ep3_6xB5x20 { + G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_Ep3_6xB5x20) / 4, 0, 0x20, 0, 0, 0}; + le_uint32_t player_tag = 0x00010000; + le_uint32_t guild_card_number = 0; + uint8_t client_id = 0; + parray unused; +} __packed_ws__(G_Unknown_Ep3_6xB5x20, 0x14); + +// 6xB3x21 / CAx21: End battle + +struct G_EndBattle_Ep3_CAx21 { + G_CardServerDataCommandHeader header = {0xB3, sizeof(G_EndBattle_Ep3_CAx21) / 4, 0, 0x21, 0, 0, 0, 0, 0}; + le_uint32_t unused2 = 0; +} __packed_ws__(G_EndBattle_Ep3_CAx21, 0x14); + +// 6xB4x22: Unknown +// This command appears to be completely unused. The client's handler for this +// command sets a flag on some data structure if it exists, but it appears that +// that data structure is never allocated. + +struct G_Unknown_Ep3_6xB4x22 { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_Unknown_Ep3_6xB4x22) / 4, 0, 0x22, 0, 0, 0}; + // No arguments +} __packed_ws__(G_Unknown_Ep3_6xB4x22, 8); + +// 6xB4x23: Unknown +// This command was actually sent by Sega's original servers, but it does +// nothing on the client. + +struct G_Unknown_Ep3_6xB4x23 { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_Unknown_Ep3_6xB4x23) / 4, 0, 0x23, 0, 0, 0}; + uint8_t present = 0; // Handler expects this to be equal to 1 + uint8_t client_id = 0; + parray unused; +} __packed_ws__(G_Unknown_Ep3_6xB4x23, 0x0C); + +// 6xB5x27: Unknown +// TODO: Document this from Episode 3 client/server disassembly + +struct G_Unknown_Ep3_6xB5x27 { + G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_Ep3_6xB5x27) / 4, 0, 0x27, 0, 0, 0}; + // Note: This command uses header_b1 as well, which looks like another client + // ID (it must be < 4, though it does not always match unknown_a1 below). + le_uint32_t unknown_a1 = 0; // Probably client ID (must be < 4) + le_uint32_t unknown_a2 = 0; // Must be < 0x10 + le_uint32_t unknown_a3 = 0; + le_uint32_t unused = 0; // Curiously, this usually contains a memory address +} __packed_ws__(G_Unknown_Ep3_6xB5x27, 0x18); + +// 6xB3x28 / CAx28: End defense list +// This command informs the server that the client is done playing defense +// cards. (In the attack phase, CAx12 is used instead.) + +struct G_EndDefenseList_Ep3_CAx28 { + G_CardServerDataCommandHeader header = {0xB3, sizeof(G_EndDefenseList_Ep3_CAx28) / 4, 0, 0x28, 0, 0, 0, 0, 0}; + uint8_t unused1 = 0; + uint8_t client_id = 0; + parray unused2; +} __packed_ws__(G_EndDefenseList_Ep3_CAx28, 0x14); + +// 6xB4x29: Set action state +// TODO: How is this different from 6xB4x09? It looks like the server never +// sends this. + +struct G_SetActionState_Ep3_6xB4x29 { + /* 00 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetActionState_Ep3_6xB4x29) / 4, 0, 0x29, 0, 0, 0}; + /* 08 */ uint8_t unknown_a1 = 0; + /* 09 */ parray unknown_a2; + /* 0C */ Episode3::ActionState state; + /* 70 */ +} __packed_ws__(G_SetActionState_Ep3_6xB4x29, 0x70); + +// 6xB4x2A: Unknown +// TODO: Document this from Episode 3 client/server disassembly + +struct G_Unknown_Ep3_6xB4x2A { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_Unknown_Ep3_6xB4x2A) / 4, 0, 0x2A, 0, 0, 0}; + parray unknown_a1; + le_uint16_t unknown_a2 = 0; + parray unused; +} __packed_ws__(G_Unknown_Ep3_6xB4x2A, 0x10); + +// 6xB3x2B / CAx2B: Legacy set card +// It seems Sega's servers completely ignored this command. The command name is +// based on a debug message found nearby. + +struct G_ExecLegacyCard_Ep3_CAx2B { + G_CardServerDataCommandHeader header = {0xB3, sizeof(G_ExecLegacyCard_Ep3_CAx2B) / 4, 0, 0x2B, 0, 0, 0, 0, 0}; + le_uint16_t unused2 = 0; + parray unused3; +} __packed_ws__(G_ExecLegacyCard_Ep3_CAx2B, 0x14); + +// 6xB4x2C: Unknown + +struct G_Unknown_Ep3_6xB4x2C { + /* 00 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_Unknown_Ep3_6xB4x2C) / 4, 0, 0x2C, 0, 0, 0}; + /* 08 */ uint8_t change_type = 0; + /* 09 */ uint8_t client_id = 0; + /* 0A */ parray card_refs; + /* 10 */ Episode3::Location loc; + /* 14 */ parray unknown_a2; + /* 1C */ +} __packed_ws__(G_Unknown_Ep3_6xB4x2C, 0x1C); + +// 6xB5x2D: Unknown +// TODO: Document this from Episode 3 client/server disassembly + +struct G_Unknown_Ep3_6xB5x2D { + G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_Ep3_6xB5x2D) / 4, 0, 0x2D, 0, 0, 0}; + // This array is indexed by client ID. When a client receives this command, it + // sends a 6x70 command to itself. It's not clear what the function of this is + // intended to be. + // TODO: Figure out if tournament fast loading can be implemented using this + // to fix the stuck-in-wall glitch. + parray unknown_a1; +} __packed_ws__(G_Unknown_Ep3_6xB5x2D, 0x0C); + +// 6xB5x2E: Notify other players that battle is about to end + +struct G_BattleEndNotification_Ep3_6xB5x2E { + G_CardBattleCommandHeader header = {0xB5, sizeof(G_BattleEndNotification_Ep3_6xB5x2E) / 4, 0, 0x2E, 0, 0, 0}; + uint8_t unknown_a1 = 0; // Command ignored unless this is 0 or 1 + parray unused; +} __packed_ws__(G_BattleEndNotification_Ep3_6xB5x2E, 0x0C); + +// 6xB5x2F: Set deck in battle setup menu + +struct G_SetDeckInBattleSetupMenu_Ep3_6xB5x2F { + G_CardBattleCommandHeader header = {0xB5, sizeof(G_SetDeckInBattleSetupMenu_Ep3_6xB5x2F) / 4, 0, 0x2F, 0, 0, 0}; + parray unknown_a1; + parray unknown_a2; + pstring deck_name; + parray unknown_a3; + le_uint16_t unknown_a4 = 0; + parray card_ids; + parray unused; + le_uint32_t unknown_a5 = 0; + le_uint16_t unknown_a6 = 0; + le_uint16_t unknown_a7 = 0; +} __packed_ws__(G_SetDeckInBattleSetupMenu_Ep3_6xB5x2F, 0x8C); + +// 6xB5x30: Unknown +// The client never sends this command, and when the client received this +// command, it does nothing. + +struct G_Unknown_Ep3_6xB5x30 { + G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_Ep3_6xB5x30) / 4, 0, 0x30, 0, 0, 0}; + // No arguments +} __packed_ws__(G_Unknown_Ep3_6xB5x30, 8); + +// 6xB5x31: Confirm deck selection + +struct G_ConfirmDeckSelection_Ep3_6xB5x31 { + G_CardBattleCommandHeader header = {0xB5, sizeof(G_ConfirmDeckSelection_Ep3_6xB5x31) / 4, 0, 0x31, 0, 0, 0}; + // Note: This command uses header_b1 for... something. + uint8_t unknown_a1 = 0; // Must be 0 or 1 + uint8_t unknown_a2 = 0; // Must be < 4 + uint8_t unknown_a3 = 0; // Must be < 4 + uint8_t unknown_a4 = 0; // Must be < 0x14 + uint8_t unknown_a5 = 0; // Used as an array index + parray unused; +} __packed_ws__(G_ConfirmDeckSelection_Ep3_6xB5x31, 0x10); + +// 6xB5x32: Move shared menu cursor + +struct G_MoveSharedMenuCursor_Ep3_6xB5x32 { + G_CardBattleCommandHeader header = {0xB5, sizeof(G_MoveSharedMenuCursor_Ep3_6xB5x32) / 4, 0, 0x32, 0, 0, 0}; + le_uint16_t selected_item_index = 0xFFFF; + le_uint16_t chosen_item_index = 0xFFFF; + uint8_t unknown_a1 = 0; + uint8_t unknown_a2 = 0; + uint8_t unknown_a3 = 0; + uint8_t unknown_a4 = 0; + uint8_t unknown_a5 = 0; + parray unused; +} __packed_ws__(G_MoveSharedMenuCursor_Ep3_6xB5x32, 0x14); + +// 6xB4x33: Subtract ally ATK points (e.g. for photon blast) + +struct G_SubtractAllyATKPoints_Ep3_6xB4x33 { + /* 00 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_SubtractAllyATKPoints_Ep3_6xB4x33) / 4, 0, 0x33, 0, 0, 0}; + /* 08 */ uint8_t client_id = 0; + /* 09 */ uint8_t ally_cost = 0; + /* 0A */ le_uint16_t card_ref = 0xFFFF; + /* 0C */ +} __packed_ws__(G_SubtractAllyATKPoints_Ep3_6xB4x33, 0x0C); + +// 6xB3x34 / CAx34: Photon blast request + +struct G_PhotonBlastRequest_Ep3_CAx34 { + G_CardServerDataCommandHeader header = {0xB3, sizeof(G_PhotonBlastRequest_Ep3_CAx34) / 4, 0, 0x34, 0, 0, 0, 0, 0}; + uint8_t ally_client_id = 0; + uint8_t reason = 0; + le_uint16_t card_ref = 0xFFFF; +} __packed_ws__(G_PhotonBlastRequest_Ep3_CAx34, 0x14); + +// 6xB4x35: Update photon blast status + +struct G_PhotonBlastStatus_Ep3_6xB4x35 { + /* 00 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_PhotonBlastStatus_Ep3_6xB4x35) / 4, 0, 0x35, 0, 0, 0}; + /* 08 */ uint8_t client_id = 0; + /* 09 */ uint8_t accepted = 0; + /* 0A */ le_uint16_t card_ref = 0xFFFF; + /* 0C */ +} __packed_ws__(G_PhotonBlastStatus_Ep3_6xB4x35, 0x0C); + +// 6xB5x36: Recreate player +// Setting client_id to a value 4 or greater while in a game causes the player +// to be temporarily replaced with a default HUmar and placed inside the central +// column in the Morgue, rendering them unable to move. The only ways out of +// this predicament appear to be either to disconnect (e.g. select Quit Game +// from the pause menu) or receive an ED (force leave game) command. + +struct G_RecreatePlayer_Ep3_6xB5x36 { + G_CardBattleCommandHeader header = {0xB5, sizeof(G_RecreatePlayer_Ep3_6xB5x36) / 4, 0, 0x36, 0, 0, 0}; + uint8_t client_id = 0; + parray unused; +} __packed_ws__(G_RecreatePlayer_Ep3_6xB5x36, 0x0C); + +// 6xB3x37 / CAx37: Ready to advance from starting rolls phase + +struct G_AdvanceFromStartingRollsPhase_Ep3_CAx37 { + G_CardServerDataCommandHeader header = {0xB3, sizeof(G_AdvanceFromStartingRollsPhase_Ep3_CAx37) / 4, 0, 0x37, 0, 0, 0, 0, 0}; + uint8_t client_id = 0; + parray unused2; +} __packed_ws__(G_AdvanceFromStartingRollsPhase_Ep3_CAx37, 0x14); + +// 6xB5x38: Card counts request +// This command causes the client identified by requested_client_id to send a +// 6xBC command to the client identified by reply_to_client_id (privately, via +// the 6D command). This appears to be unused; it is likely superseded by the +// CAx49 command. + +struct G_CardCountsRequest_Ep3_6xB5x38 { + G_CardBattleCommandHeader header = {0xB5, sizeof(G_CardCountsRequest_Ep3_6xB5x38) / 4, 0, 0x38, 0, 0, 0}; + uint8_t requested_client_id = 0; + uint8_t reply_to_client_id = 0; + parray unused; +} __packed_ws__(G_CardCountsRequest_Ep3_6xB5x38, 0x0C); + +// 6xB4x39: Update all player statistics + +struct G_UpdateAllPlayerStatistics_Ep3NTE_6xB4x39 { + /* 00 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateAllPlayerStatistics_Ep3NTE_6xB4x39) / 4, 0, 0x39, 0, 0, 0}; + /* 08 */ parray stats; + /* 58 */ +} __packed_ws__(G_UpdateAllPlayerStatistics_Ep3NTE_6xB4x39, 0x58); + +struct G_UpdateAllPlayerStatistics_Ep3_6xB4x39 { + /* 00 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateAllPlayerStatistics_Ep3_6xB4x39) / 4, 0, 0x39, 0, 0, 0}; + /* 08 */ parray stats; + /* A8 */ +} __packed_ws__(G_UpdateAllPlayerStatistics_Ep3_6xB4x39, 0xA8); + +// 6xB3x3A / CAx3A: Overall time limit expired +// It seems Sega's servers completely ignored this command and used server-side +// timing instead. newserv does the same. + +struct G_OverallTimeLimitExpired_Ep3_CAx3A { + G_CardServerDataCommandHeader header = {0xB3, sizeof(G_OverallTimeLimitExpired_Ep3_CAx3A) / 4, 0, 0x3A, 0, 0, 0, 0, 0}; +} __packed_ws__(G_OverallTimeLimitExpired_Ep3_CAx3A, 0x10); + +// 6xB4x3B: Load current environment +// This command is used to send spectators in a spectator team to the main +// battle. A 6xB4x05 and 6xB6x41 command shouldhave been sent before this, to +// set the map state that should appear for the new spectator. + +struct G_LoadCurrentEnvironment_Ep3_6xB4x3B { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_LoadCurrentEnvironment_Ep3_6xB4x3B) / 4, 0, 0x3B, 0, 0, 0}; + parray unused; +} __packed_ws__(G_LoadCurrentEnvironment_Ep3_6xB4x3B, 0x0C); + +// 6xB5x3C: Set player substatus +// This command sets the text that appears under the player's name in the HUD. + +struct G_SetPlayerSubstatus_Ep3_6xB5x3C { + // Note: header.sender_client_id specifies which client's status to update + G_CardBattleCommandHeader header = {0xB5, sizeof(G_SetPlayerSubstatus_Ep3_6xB5x3C) / 4, 0, 0x3C, 0, 0, 0}; + // Status values: + // 00 (or any value not listed below) = (nothing) + // 01 = Editing + // 02 = Trading... + // 03 = At Counter + uint8_t status = 0; + parray unused; +} __packed_ws__(G_SetPlayerSubstatus_Ep3_6xB5x3C, 0x0C); + +// 6xB4x3D: Set tournament player decks +// This is sent before the counter sequence in a tournament game, to reserve the +// player and COM slots and set the map number. + +struct G_SetTournamentPlayerDecks_Ep3_6xB4x3D_Entry { + /* 00 */ uint8_t type = 0; // 0 = no player, 1 = human, 2 = COM + /* 01 */ pstring player_name; + /* 11 */ pstring deck_name; // Only used for COM players + /* 21 */ parray unknown_a1; + /* 26 */ parray card_ids; // Can be blank for human players + /* 64 */ uint8_t client_id = 0; // Unused for COMs + /* 65 */ uint8_t unknown_a4 = 0; + /* 66 */ le_uint16_t unknown_a2 = 0; + /* 68 */ le_uint16_t unknown_a3 = 0; + /* 6A */ +} __packed_ws__(G_SetTournamentPlayerDecks_Ep3_6xB4x3D_Entry, 0x6A); + +struct G_SetTournamentPlayerDecks_Ep3NTE_6xB4x3D { + /* 0000 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetTournamentPlayerDecks_Ep3NTE_6xB4x3D) / 4, 0, 0x3D, 0, 0, 0}; + /* 0008 */ Episode3::RulesTrial rules; + /* 0014 */ parray entries; + /* 01BC */ le_uint32_t map_number = 0; + /* 01C0 */ uint8_t player_slot = 0; // Which deck slot is editable by the client + /* 01C1 */ uint8_t unknown_a3 = 0; + /* 01C2 */ uint8_t unknown_a4 = 0; + /* 01C3 */ uint8_t unknown_a5 = 0; + /* 01C4 */ +} __packed_ws__(G_SetTournamentPlayerDecks_Ep3NTE_6xB4x3D, 0x1C4); + +struct G_SetTournamentPlayerDecks_Ep3_6xB4x3D { + /* 0000 */ G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetTournamentPlayerDecks_Ep3_6xB4x3D) / 4, 0, 0x3D, 0, 0, 0}; + /* 0008 */ Episode3::Rules rules; + /* 001C */ parray entries; + /* 01C4 */ le_uint32_t map_number = 0; + /* 01C8 */ uint8_t player_slot = 0; // Which deck slot is editable by the client + /* 01C9 */ uint8_t unknown_a3 = 0; + /* 01CA */ uint8_t unknown_a4 = 0; + /* 01CB */ uint8_t unknown_a5 = 0; + /* 01CC */ +} __packed_ws__(G_SetTournamentPlayerDecks_Ep3_6xB4x3D, 0x1CC); + +// 6xB5x3E: Make card auction bid + +struct G_MakeCardAuctionBid_Ep3_6xB5x3E { + G_CardBattleCommandHeader header = {0xB5, sizeof(G_MakeCardAuctionBid_Ep3_6xB5x3E) / 4, 0, 0x3E, 0, 0, 0}; + // Note: This command uses header.unknown_a1 for the bidder's client ID. + uint8_t card_index = 0; // Index of card in EF command + uint8_t bid_value = 0; // 1-99 + parray unused; +} __packed_ws__(G_MakeCardAuctionBid_Ep3_6xB5x3E, 0x0C); + +// 6xB5x3F: Open blocking menu +// This command opens a shared menu between all clients in a game. The client +// specified in .client_id is able to control the menu; the other clients see +// that player's actions but cannot control anything. + +struct G_OpenBlockingMenu_Ep3_6xB5x3F { + G_CardBattleCommandHeader header = {0xB5, sizeof(G_OpenBlockingMenu_Ep3_6xB5x3F) / 4, 0, 0x3F, 0, 0, 0}; + // Menu type should be one of these values: + // 0x01/0x02 = battle prep menu + // 0x11 = card auction counter menu (join or cancel) + // 0x12 = go directly to card auction state (client sends EF command) + // Other values will likely crash the client. + int8_t menu_type = 0; // Must be in the range [-1, 0x14] + uint8_t client_id = 0; + parray unused1; + le_uint32_t unknown_a3 = 0; + parray unused2; +} __packed_ws__(G_OpenBlockingMenu_Ep3_6xB5x3F, 0x14); + +// 6xB3x40 / CAx40: Request map list +// The server should respond with a 6xB6x40 command. + +struct G_MapListRequest_Ep3_CAx40 { + G_CardServerDataCommandHeader header = {0xB3, sizeof(G_MapListRequest_Ep3_CAx40) / 4, 0, 0x40, 0, 0, 0, 0, 0}; +} __packed_ws__(G_MapListRequest_Ep3_CAx40, 0x10); + +// 6xB3x41 / CAx41: Request map data +// The server should respond with a 6xB6x41 command containing the definition of +// the specified map. + +struct G_MapDataRequest_Ep3_CAx41 { + G_CardServerDataCommandHeader header = {0xB3, sizeof(G_MapDataRequest_Ep3_CAx41) / 4, 0, 0x41, 0, 0, 0, 0, 0}; + le_uint32_t map_number = 0; +} __packed_ws__(G_MapDataRequest_Ep3_CAx41, 0x14); + +// 6xB5x42: Initiate card auction +// Sending this command to a client has the same effect as sending a 6xB5x3F +// command to tell it to open the auction menu. (This works even if the client +// doesn't have a VIP card or there are fewer than 4 players in the current +// game.) Under normal operation, the server doesn't need to do this - the +// client sends this when all of the following conditions are met: +// 1. The client has a VIP card. (This is stored client-side in seq flag 7000.) +// 2. The client is in a game with 4 players. +// 3. All clients are at the auction counter. + +struct G_InitiateCardAuction_Ep3_6xB5x42 { + G_CardBattleCommandHeader header = {0xB5, sizeof(G_InitiateCardAuction_Ep3_6xB5x42) / 4, 0, 0x42, 0, 0, 0}; + // This command uses header.unknown_a1 (probably for the client's ID). +} __packed_ws__(G_InitiateCardAuction_Ep3_6xB5x42, 8); + +// 6xB5x43: Unknown +// This command stores the card IDs and counts in a global array on the client, +// but this array is never read from. It's likely this is a remnant of an +// unimplemented or removed feature, or an earlier implementation of the card +// trade window. + +struct G_Unknown_Ep3_6xB5x43 { + G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_Ep3_6xB5x43) / 4, 0, 0x43, 0, 0, 0}; + struct Entry { + // Both fields here are masked. To get the actual values used by the game, + // XOR the values here with 0x39AB. + le_uint16_t masked_card_id = 0xFFFF; // Must be < 0x2F1 (when unmasked) + le_uint16_t masked_count = 0; // Must be in [1, 99] (when unmasked) + } __packed_ws__(Entry, 4); + parray entries; +} __packed_ws__(G_Unknown_Ep3_6xB5x43, 0x58); + +// 6xB5x44: Card auction bid summary + +struct G_CardAuctionBidSummary_Ep3_6xB5x44 { + G_CardBattleCommandHeader header = {0xB5, sizeof(G_CardAuctionBidSummary_Ep3_6xB5x44) / 4, 0, 0x44, 0, 0, 0}; + // Note: This command uses header.unknown_a1 for the bidder's client ID. + parray bids; // In same order as cards in the EF command +} __packed_ws__(G_CardAuctionBidSummary_Ep3_6xB5x44, 0x18); + +// 6xB5x45: Card auction results + +struct G_CardAuctionResults_Ep3_6xB5x45 { + G_CardBattleCommandHeader header = {0xB5, sizeof(G_CardAuctionResults_Ep3_6xB5x45) / 4, 0, 0x45, 0, 0, 0}; + // Note: This command uses header.unknown_a1 for the sender's client ID. + // This array is indexed by [card_index][client_id], and contains the final + // bid for each player on each card (or 0 if they did not bid on that card). + parray, 8> bids_by_player; +} __packed_ws__(G_CardAuctionResults_Ep3_6xB5x45, 0x48); + +// 6xB4x46: Server version strings +// This command doesn't seem to be necessary to actually play the game; the +// client just copies the included strings to global buffers and then ignores +// them. Sega's servers sent this twice for each battle, however: once after the +// initial setup phase (before starter rolls) and once when the results screen +// appeared. The second instance of this command appears to be caused by them +// recreating the TCardServer object (implemented in newserv's Episode3::Server) +// in order to support multiple sequential battles in the same team. + +struct G_ServerVersionStrings_Ep3_6xB4x46 { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_ServerVersionStrings_Ep3_6xB4x46) / 4, 0, 0x46, 0, 0, 0}; + // In all of the examples (from Sega's servers) that I've seen of this + // command, these fields have the following values: + // version_signature = "[V1][FINAL2.0] 03/09/13 15:30 by K.Toya" + // date_str1 = "Mar 7 2007 21:42:40" + // In the client, date_str1 is different: + // version_signature = "[V1][FINAL2.0] 03/09/13 15:30 by K.Toya" + // date_str1 = "Jan 21 2004 18:36:47' + // Presumably if any logs exist from before 7 March 2007, they would have a + // different date_str1, but the unchanged version_signature likely means that + // Sega never made any code changes on the server side. + pstring version_signature; + pstring date_str1; // Probably card definitions revision date + // In Sega's implementation, it seems this field is blank when starting a + // battle, and contains the current time (in the format "YYYY/MM/DD hh:mm:ss") + // when ending a battle. This may have been used for identifying debug logs. + pstring date_str2; + // It seems Sega used to send 0 here when starting a battle, and 0x04157580 + // when ending a battle. Since the field is unused by the client, it's not + // clear what that value means, if anything. This behavior may be another + // uninitialized memory bug in the server implementation (of which there are + // many other examples). + le_uint32_t unused = 0; +} __packed_ws__(G_ServerVersionStrings_Ep3_6xB4x46, 0xCC); + +struct G_ServerVersionStrings_Ep3NTE_6xB4x46 { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_ServerVersionStrings_Ep3NTE_6xB4x46) / 4, 0, 0x46, 0, 0, 0}; + // Ep3 NTE uses the following strings: + // "03/05/29 18:00 by K.Toya" + pstring version_signature; + // "Jun 11 2003 05:02:36" + pstring date_str1; +} __packed_ws__(G_ServerVersionStrings_Ep3NTE_6xB4x46, 0x88); + +// 6xB5x47: Set spectator's CARD level +// header.sender_client_id is the spectator's client ID. + +struct G_SetSpectatorCARDLevel_Ep3_6xB5x47 { + G_CardBattleCommandHeader header = {0xB5, sizeof(G_SetSpectatorCARDLevel_Ep3_6xB5x47) / 4, 0, 0x47, 0, 0, 0}; + le_uint32_t clv = 0; +} __packed_ws__(G_SetSpectatorCARDLevel_Ep3_6xB5x47, 0x0C); + +// 6xB3x48 / CAx48: End turn + +struct G_EndTurn_Ep3_CAx48 { + G_CardServerDataCommandHeader header = {0xB3, sizeof(G_EndTurn_Ep3_CAx48) / 4, 0, 0x48, 0, 0, 0, 0, 0}; + uint8_t client_id = 0; + parray unused2; +} __packed_ws__(G_EndTurn_Ep3_CAx48, 0x14); + +// 6xB3x49 / CAx49: Card counts +// This command is sent when a client joins a game, but it is completely ignored +// by the original Episode 3 server. Sega presumably could have used this to +// detect the presence of unreleased cards to ban cheaters, but the effects of +// the non-saveable Have All Cards AR code don't appear in this data, so this +// would have been ineffective. There appears to be a place where Sega's server +// intended to use this data, however - the deck verification function takes a +// pointer to the card counts array, but Sega's implementation always passes +// null there, which skips the owned card count check. newserv uses this data at +// that callsite to implement one of the deck validity checks. +// Episode 3 Trial Edition does not send this command. + +struct G_CardCounts_Ep3_CAx49 { + G_CardServerDataCommandHeader header = {0xB3, sizeof(G_CardCounts_Ep3_CAx49) / 4, 0, 0x49, 0, 0, 0, 0, 0}; + uint8_t basis = 0; + parray unused; + // This is encrypted with the trivial algorithm (see decrypt_trivial_gci_data) + // using the basis in the preceding field + parray card_id_to_count; +} __packed_ws__(G_CardCounts_Ep3_CAx49, 0x304); + +// 6xB4x4A: Add to set card log +// This command is not valid on Episode 3 Trial Edition. +// TODO: Document this from Episode 3 client/server disassembly + +struct G_AddToSetCardlog_Ep3_6xB4x4A { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_AddToSetCardlog_Ep3_6xB4x4A) / 4, 0, 0x4A, 0, 0, 0}; + // Note: entry_count appears not to be bounds-checked; presumably the server + // could send up to 0xFF entries, but those after the 8th would not be + // byteswapped before the client handles them. + uint8_t client_id = 0; + uint8_t entry_count = 0; + le_uint16_t round_num = 0; + parray card_refs; +} __packed_ws__(G_AddToSetCardlog_Ep3_6xB4x4A, 0x1C); + +// 6xB4x4B: Set EX result values +// This command is not valid on Episode 3 Trial Edition. +// This command specifies how much EX the player should get based on the +// difference between their level and the levels of the players they defeated or +// were defeated by. (For multi-player opponent teams, the average of the +// opponents' levels is used.) The game scans the appropriate list for the entry +// whose threshold is less than or equal to than the level difference, and +// returns the corresponding value. For example, if the first two entries in the +// win list are {20, 40} and {10, 30}, and the player defeats an opponent who is +// 15 levels above the player's level, the player will get 30 EX when they win +// the battle. If all thresholds are greater than the level difference, the last +// entry's value is used. Finally, if the opponent team has no humans on it, the +// resulting EX values are divided by 2 (so in the example above, the player +// would only get 15 EX for defeating COMs). + +// If any entry in either list has .value < -100 or > 100, the entire command is +// ignored and the EX thresholds and values are reset to their default values. +// These default values are: +// win_entries = {50, 100}, {30, 80}, {15, 70}, {10, 55}, {7, 45}, {4, 35}, +// {1, 25}, {-1, 20}, {-9, 15}, {0, 10} +// lose_entries = {1, 0}, {-2, 0}, {-3, 0}, {-4, 0}, {-5, 0}, {-6, 0}, {-7, 0}, +// {-10, -10}, {-30, -10}, {0, -15} + +struct G_SetEXResultValues_Ep3_6xB4x4B { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetEXResultValues_Ep3_6xB4x4B) / 4, 0, 0x4B, 0, 0, 0}; + struct Entry { + le_int16_t threshold = 0; + le_int16_t value = 0; + } __packed_ws__(Entry, 4); + parray win_entries; + parray lose_entries; +} __packed_ws__(G_SetEXResultValues_Ep3_6xB4x4B, 0x58); + +// 6xB4x4C: Update action chain +// This command is not valid on Episode 3 Trial Edition. + +struct G_UpdateActionChain_Ep3_6xB4x4C { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateActionChain_Ep3_6xB4x4C) / 4, 0, 0x4C, 0, 0, 0}; + uint8_t client_id = 0; + int8_t index = 0; + parray unused; + Episode3::ActionChain chain; +} __packed_ws__(G_UpdateActionChain_Ep3_6xB4x4C, 0x7C); + +// 6xB4x4D: Update action metadata +// This command is not valid on Episode 3 Trial Edition. + +struct G_UpdateActionMetadata_Ep3_6xB4x4D { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateActionMetadata_Ep3_6xB4x4D) / 4, 0, 0x4D, 0, 0, 0}; + uint8_t client_id = 0; + int8_t index = 0; + parray unused; + Episode3::ActionMetadata metadata; +} __packed_ws__(G_UpdateActionMetadata_Ep3_6xB4x4D, 0x80); + +// 6xB4x4E: Update card conditions +// This command is not valid on Episode 3 Trial Edition. + +struct G_UpdateCardConditions_Ep3_6xB4x4E { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateCardConditions_Ep3_6xB4x4E) / 4, 0, 0x4E, 0, 0, 0}; + uint8_t client_id = 0; + int8_t index = 0; + parray unused; + parray conditions; +} __packed_ws__(G_UpdateCardConditions_Ep3_6xB4x4E, 0x9C); + +// 6xB4x4F: Clear set card conditions +// This command is not valid on Episode 3 Trial Edition. + +struct G_ClearSetCardConditions_Ep3_6xB4x4F { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_ClearSetCardConditions_Ep3_6xB4x4F) / 4, 0, 0x4F, 0, 0, 0}; + uint8_t client_id = 0; + uint8_t unused = 0; + // For each 1 bit in this mask, the conditions of the corresponding card + // should be deleted. The low bit corresponds to the SC card; the next bit + // corresponds to set slot 0, the next bit to set slot 1, etc. (The upper 7 + // bits of this field are unused.) + le_uint16_t clear_mask = 0; +} __packed_ws__(G_ClearSetCardConditions_Ep3_6xB4x4F, 0x0C); + +// 6xB4x50: Set trap tile locations +// This command is not valid on Episode 3 Trial Edition. + +struct G_SetTrapTileLocations_Ep3_6xB4x50 { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetTrapTileLocations_Ep3_6xB4x50) / 4, 0, 0x50, 0, 0, 0}; + // Each entry in this array corresponds to one of the 5 trap types, in order. + // Each entry is an [x, y] pair; if that trap type is not present, its + // location entry is FF FF. + parray, 5> locations; + parray unused; +} __packed_ws__(G_SetTrapTileLocations_Ep3_6xB4x50, 0x14); + +// 6xB4x51: Tournament match result +// This command is not valid on Episode 3 Trial Edition. +// This is sent as soon as the battle result is determined (before the battle +// results screen). If the client is in tournament mode (tournament_flag is 1 in +// the StateFlags struct), then it will use this information to show the +// tournament match result screen before the battle results screen. + +struct G_TournamentMatchResult_Ep3_6xB4x51 { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_TournamentMatchResult_Ep3_6xB4x51) / 4, 0, 0x51, 0, 0, 0}; + pstring match_description; + struct NamesEntry { + pstring team_name; + parray, 2> player_names; + } __packed_ws__(NamesEntry, 0x40); + parray names_entries; + le_uint16_t unused1 = 0; + // If round_num is equal to 6, the "On to the next battle..." text is replaced + // with "Congratulations!" and some flashier graphics. This is used for the + // final match. + le_uint16_t round_num = 0; + le_uint16_t num_players_per_team = 0; + le_uint16_t winner_team_id = 0; + le_uint32_t meseta_amount = 0; + // This field apparently is supposed to contain a %s token (as for printf) + // which is replaced with meseta_amount. The results screen animates this text + // counting up from 0 to meseta_amount. + pstring meseta_reward_text; +} __packed_ws__(G_TournamentMatchResult_Ep3_6xB4x51, 0xF4); + +// 6xB4x52: Set game metadata +// This command is not valid on Episode 3 Trial Edition. +// This is sent to all players in a game and all attached spectator teams when +// any player joins or leaves any spectator team watching the same game. + +struct G_SetGameMetadata_Ep3_6xB4x52 { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetGameMetadata_Ep3_6xB4x52) / 4, 0, 0x52, 0, 0, 0}; + // This field appears before the slash in the spectators' HUD. Presumably this + // is used to indicate how many spectators are in the current spectator team. + // In the primary game (watched lobby), this is presumably unused. + le_uint16_t local_spectators = 0; // Clamped to [0, 999] by the client + // This field appears after the slash in the spectators' HUD. This is used to + // indicate how many spectators there are in all spectator teams attached to + // the same battle. + // This field also controls the icon shown in the primary game. If this field + // is nonzero, an icon appears in the middle of the screen during battle when + // the details view is enabled (by pressing Z). However, the number of people + // visible in the icon doesn't match the value in this field. Specifically: + // 0 = no icon + // 1 = icon with a single spectator (green) + // 2-4 = icon with 3 spectators (blue) + // 5-10 = icon with 5 spectators (yellow) + // 11-29 = icon with 8 spectators (purple) + // 30+ = icon with 12 spectators (red) + le_uint16_t total_spectators = 0; // Clamped to [0, 999] by the client + le_uint16_t unused = 0; + // If text_size is not zero, the text is shown in the top bar instead of the + // usual message ("Viewing Battle", "Time left: XX:XX", and the like). + le_uint16_t text_size = 0; + pstring text; +} __packed_ws__(G_SetGameMetadata_Ep3_6xB4x52, 0x110); + +// 6xB4x53: Reject battle start request +// This command is not valid on Episode 3 Trial Edition. +// This is sent in response to a CAx1D command if setup isn't complete (e.g. if +// some names/decks are missing or invalid). Under normal operation, this should +// never happen. +// Note: It seems the client ignores everything in this structure; the command +// handler just sets a global state flag and returns immediately. + +struct G_RejectBattleStartRequest_Ep3_6xB4x53 { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_RejectBattleStartRequest_Ep3_6xB4x53) / 4, 0, 0x53, 0, 0, 0}; + Episode3::SetupPhase setup_phase; + Episode3::RegistrationPhase registration_phase; + parray unused; + Episode3::MapAndRulesState state; +} __packed_ws__(G_RejectBattleStartRequest_Ep3_6xB4x53, 0x144); + +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +// EXTENDED COMMANDS /////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +// These commands are not part of the official protocol; newserv uses these to +// implement extended functionality. + +// 30 (C->S): Extended player info +// Requested with the GetExtendedPlayerInfo patch. Format depends on version: +// DC v2: PSODCV2CharacterFile +// GC v3: PSOGCCharacterFile::Character +// XB v3: PSOXBCharacterFileCharacter diff --git a/src/Compression.cc b/src/Compression.cc index 795f6efb..6bfc4c37 100644 --- a/src/Compression.cc +++ b/src/Compression.cc @@ -1,1357 +1,1357 @@ -#include "Compression.hh" - -#include -#include -#include -#include -#include - -#include -#include - -#include "Text.hh" - -using namespace std; - -template <> -const char* name_for_enum(CompressPhase v) { - switch (v) { - case CompressPhase::INDEX: - return "INDEX"; - case CompressPhase::CONSTRUCT_PATHS: - return "CONSTRUCT_PATHS"; - case CompressPhase::BACKTRACE_OPTIMAL_PATH: - return "BACKTRACE_OPTIMAL_PATH"; - case CompressPhase::GENERATE_RESULT: - return "GENERATE_RESULT"; - default: - return "__UNKNOWN__"; - } -} - -template -struct WindowIndex { - const uint8_t* data; - size_t size; - size_t offset; - set> index; - - WindowIndex(const void* data, size_t size) - : data(reinterpret_cast(data)), - size(size), - offset(0), - index(bind(&WindowIndex::set_comparator, this, placeholders::_1, placeholders::_2)) {} - - void advance() { - if (this->offset >= WindowLength) { - this->index.erase(this->offset - WindowLength); - } - this->index.emplace(this->offset); - this->offset++; - } - - size_t get_match_length(size_t match_offset) const { - size_t match_iter = match_offset; - size_t offset_iter = this->offset; - while ((match_iter < match_offset + MaxMatchLength) && - (match_iter < this->size) && - (offset_iter < this->size) && - (this->data[match_iter] == this->data[offset_iter])) { - match_iter++; - offset_iter++; - } - return match_iter - match_offset; - }; - - // The data structure we want is a binary-searchable set of all strings - // starting at all possible offsets within the sliding window, and we need - // to be able to search lexicographically but insert and delete by offset. - // A std::map would accomplish this, but would be - // horrendously inefficient: we'd have to copy strings far too much. We can - // solve this by instead storing the offset of each string as keys in a set - // and using a custom comparator to treat them as references to binary - // strings within the data. - bool set_comparator(size_t a, size_t b) const { - size_t max_length = min(MaxMatchLength, this->size - max(a, b)); - size_t end_a = a + max_length; - for (; a < end_a; a++, b++) { - uint8_t data_a = static_cast(this->data[a]); - uint8_t data_b = static_cast(this->data[b]); - if (data_a < data_b) { - return true; // a comes before b lexicographically - } else if (data_a > data_b) { - return false; // a comes after b lexicographically - } - } - return a < b; // Maximum-length match; order them by offset - }; - - pair get_best_match() const { - // Find the best match from the index. It's unlikely that we'll get an - // exact match, so check the entry before the upper_bound result too. - // Note: We use upper_bound rather than lower_bound because in PRS, a - // backreference can be encoded with fewer bits if it's close to the - // decompression offset, and this makes us pick the latest match by - // default. - size_t match_offset = 0; - size_t match_size = 0; - auto it = this->index.upper_bound(this->offset); - if (it != this->index.end()) { - size_t new_match_offset = *it; - size_t new_match_size = this->get_match_length(new_match_offset); - if ((new_match_size > match_size) || (new_match_size == match_size && new_match_offset > match_offset)) { - match_offset = new_match_offset; - match_size = new_match_size; - } - } - if (it != this->index.begin()) { - it--; - size_t new_match_offset = *it; - size_t new_match_size = this->get_match_length(new_match_offset); - if ((new_match_size > match_size) || (new_match_size == match_size && new_match_offset > match_offset)) { - match_offset = new_match_offset; - match_size = new_match_size; - } - } - return make_pair(match_offset, match_size); - } -}; - -struct LZSSInterleavedWriter { - StringWriter w; - size_t buf_offset; - uint8_t next_control_bit; - uint8_t buf[0x19]; - - LZSSInterleavedWriter() - : buf_offset(1), - next_control_bit(1) { - this->buf[0] = 0; - } - - void flush_if_ready() { - if (this->next_control_bit == 0) { - this->w.write(this->buf, this->buf_offset); - this->buf[0] = 0; - this->buf_offset = 1; - this->next_control_bit = 1; - } - } - - std::string&& close() { - if (this->buf_offset > 1 || this->next_control_bit != 1) { - this->w.write(this->buf, this->buf_offset); - } - return std::move(this->w.str()); - } - - void write_control(bool v) { - if (this->next_control_bit == 0) { - throw logic_error("write_control called with no space to write"); - } - if (v) { - this->buf[0] |= this->next_control_bit; - } - this->next_control_bit <<= 1; - } - - void write_data(uint8_t v) { - this->buf[this->buf_offset++] = v; - } - - size_t size() const { - return this->w.size() + this->buf_offset; - } -}; - -class ControlStreamReader { -public: - ControlStreamReader(StringReader& r) - : r(r), - bits(0x0000) {} - - bool read() { - if (!(this->bits & 0x0100)) { - this->bits = 0xFF00 | this->r.get_u8(); - } - bool ret = this->bits & 1; - this->bits >>= 1; - return ret; - } - - uint8_t buffered_bits() const { - uint16_t z = this->bits; - uint8_t ret = 0; - for (; z & 0x0100; z >>= 1, ret++) { - } - return ret; - } - -private: - StringReader& r; - uint16_t bits; -}; - -struct PRSPathNode { - enum class CommandType { - NONE = 0, - LITERAL, - SHORT_COPY, - LONG_COPY, - EXTENDED_COPY, - }; - - int16_t short_copy_offset = 0; - uint8_t max_short_copy_size = 0; - int16_t long_copy_offset = 0; - uint8_t max_long_copy_size = 0; - int16_t extended_copy_offset = 0; - uint16_t max_extended_copy_size = 0; - - // Pathfinding state - size_t from_offset = 0; - CommandType from_command_type = CommandType::NONE; - size_t bits_used = static_cast(-1); - - // Stream generation state - size_t to_offset = 0; -}; - -string prs_compress_optimal(const void* in_data_v, size_t in_size, ProgressCallback progress_fn) { - const uint8_t* in_data = reinterpret_cast(in_data_v); - - vector nodes; - nodes.resize(in_size + 1); - nodes[0].bits_used = 18; // Stop command: 2 control bits and 2 data bytes - - size_t copy_progress_max = 3 * in_size; - atomic copy_progress = 0; - - // Populate all possible short copies - std::thread short_window_thread([&]() -> void { - WindowIndex<0x100, 5> window(in_data_v, in_size); - while (window.offset < in_size) { - if (window.offset && (window.offset & 0xFFF) == 0 && progress_fn) { - size_t progress = copy_progress.fetch_add(0x1000) + 0x1000; - progress_fn(CompressPhase::INDEX, progress, copy_progress_max, 0); - } - auto& node = nodes[window.offset]; - auto match = window.get_best_match(); - if (match.second >= 2) { - node.short_copy_offset = match.first - window.offset; - node.max_short_copy_size = match.second; - } - window.advance(); - } - }); - - // Populate all possible long copies - std::thread long_window_thread([&]() -> void { - WindowIndex<0x1FFF, 9> window(in_data_v, in_size); - while (window.offset < in_size) { - if (window.offset && (window.offset & 0xFFF) == 0 && progress_fn) { - size_t progress = copy_progress.fetch_add(0x1000) + 0x1000; - progress_fn(CompressPhase::INDEX, progress, copy_progress_max, 0); - } - auto& node = nodes[window.offset]; - auto match = window.get_best_match(); - if (match.second >= 3) { - node.long_copy_offset = match.first - window.offset; - node.max_long_copy_size = match.second; - } - window.advance(); - } - }); - - // Populate all possible extended copies - std::thread extended_window_thread([&]() -> void { - WindowIndex<0x1FFF, 0x100> window(in_data_v, in_size); - while (window.offset < in_size) { - if (window.offset && (window.offset & 0xFFF) == 0 && progress_fn) { - size_t progress = copy_progress.fetch_add(0x1000) + 0x1000; - progress_fn(CompressPhase::INDEX, progress, copy_progress_max, 0); - } - auto& node = nodes[window.offset]; - auto match = window.get_best_match(); - if (match.second >= 1) { - node.extended_copy_offset = match.first - window.offset; - node.max_extended_copy_size = match.second; - } - window.advance(); - } - }); - - short_window_thread.join(); - long_window_thread.join(); - extended_window_thread.join(); - - // For each node, populate the literal value, and the best ways to get to the - // following nodes - for (size_t z = 0; z < in_size; z++) { - if ((z & 0xFFF) == 0 && progress_fn) { - progress_fn(CompressPhase::CONSTRUCT_PATHS, z, in_size, 0); - } - - auto& node = nodes[z]; - - // Literal: 1 control bit + 1 data byte - size_t bits_used = node.bits_used + 9; - { - auto& next_node = nodes[z + 1]; - if (next_node.bits_used > bits_used) { - next_node.from_offset = z; - next_node.from_command_type = PRSPathNode::CommandType::LITERAL; - next_node.bits_used = bits_used; - } - } - - // Short copy: 4 control bits + 1 data byte - bits_used = node.bits_used + 12; - for (size_t x = 2; x <= node.max_short_copy_size; x++) { - auto& next_node = nodes[z + x]; - if (next_node.bits_used > bits_used) { - next_node.from_offset = z; - next_node.from_command_type = PRSPathNode::CommandType::SHORT_COPY; - next_node.bits_used = bits_used; - } - } - - // Long copy: 2 control bits + 2 data bytes - bits_used = node.bits_used + 18; - for (size_t x = 3; x <= node.max_long_copy_size; x++) { - auto& next_node = nodes[z + x]; - if (next_node.bits_used > bits_used) { - next_node.from_offset = z; - next_node.from_command_type = PRSPathNode::CommandType::LONG_COPY; - next_node.bits_used = bits_used; - } - } - - // Extended copy: 2 control bits + 3 data bytes - bits_used = node.bits_used + 26; - for (size_t x = 1; x <= node.max_extended_copy_size; x++) { - auto& next_node = nodes[z + x]; - if (next_node.bits_used > bits_used) { - next_node.from_offset = z; - next_node.from_command_type = PRSPathNode::CommandType::EXTENDED_COPY; - next_node.bits_used = bits_used; - } - } - } - - // Find the shortest path from the last node to the first node - size_t last_progress_fn_call = static_cast(-1); - for (size_t z = in_size; z > 0;) { - if ((z & ~0xFFF) != (last_progress_fn_call & ~0xFFF)) { - last_progress_fn_call = z; - if (progress_fn) { - progress_fn(CompressPhase::BACKTRACE_OPTIMAL_PATH, z, in_size, 0); - } - } - size_t from_offset = nodes[z].from_offset; - nodes[from_offset].to_offset = z; - z = from_offset; - } - - // Produce the PRS command stream from the shortest path - LZSSInterleavedWriter w; - last_progress_fn_call = static_cast(-1); - for (size_t offset = 0; offset < in_size;) { - if ((offset & ~0xFFF) != (last_progress_fn_call & ~0xFFF)) { - last_progress_fn_call = offset; - if (progress_fn) { - progress_fn(CompressPhase::GENERATE_RESULT, offset, in_size, w.size()); - } - } - - const auto& node = nodes[offset]; - const auto& next_node = nodes[node.to_offset]; - - size_t copy_size = node.to_offset - offset; - switch (next_node.from_command_type) { - case PRSPathNode::CommandType::LITERAL: - if (copy_size != 1) { - throw logic_error("incorrect size for LITERAL copy type"); - } - w.write_control(true); - w.write_data(in_data[offset]); - break; - case PRSPathNode::CommandType::SHORT_COPY: { - if (copy_size < 2 || copy_size > 5) { - throw logic_error("incorrect size for SHORT_COPY copy type"); - } - uint8_t encoded_size = copy_size - 2; - w.write_control(false); - w.flush_if_ready(); - w.write_control(false); - w.flush_if_ready(); - w.write_control(encoded_size & 2); - w.flush_if_ready(); - w.write_control(encoded_size & 1); - w.write_data(node.short_copy_offset & 0xFF); - break; - } - case PRSPathNode::CommandType::LONG_COPY: { - if (copy_size < 2 || copy_size > 9) { - throw logic_error("incorrect size for LONG_COPY copy type"); - } - w.write_control(false); - w.flush_if_ready(); - w.write_control(true); - uint16_t a = (node.long_copy_offset << 3) | (copy_size - 2); - w.write_data(a & 0xFF); - w.write_data(a >> 8); - break; - } - case PRSPathNode::CommandType::EXTENDED_COPY: { - if (copy_size < 1 || copy_size > 0x100) { - throw logic_error("incorrect size for EXTENDED_COPY copy type"); - } - w.write_control(false); - w.flush_if_ready(); - w.write_control(true); - uint16_t a = (node.extended_copy_offset << 3); - w.write_data(a & 0xFF); - w.write_data(a >> 8); - w.write_data(copy_size - 1); - break; - } - default: - throw logic_error("invalid copy type in shortest path"); - } - w.flush_if_ready(); - - offset = node.to_offset; - } - - // Write stop command - w.write_control(false); - w.flush_if_ready(); - w.write_control(true); - w.write_data(0); - w.write_data(0); - - return std::move(w.close()); -} - -string prs_compress_optimal(const string& data, ProgressCallback progress_fn) { - return prs_compress_optimal(data.data(), data.size(), progress_fn); -} - -string prs_compress_pessimal(const void* vdata, size_t size) { - const uint8_t* in_data = reinterpret_cast(vdata); - - // The worst possible encoding we can do is a literal byte when no byte with - // the same value is within the window, or an extended copy if there is a byte - // with the same value in the window. - WindowIndex<0x1FFF, 1> window(in_data, size); - LZSSInterleavedWriter w; - for (size_t z = 0; z < size; z++) { - auto match = window.get_best_match(); - if (match.second >= 1) { - // Write extended copy - int16_t offset = match.first - window.offset; - w.write_control(false); - w.flush_if_ready(); - w.write_control(true); - uint16_t a = (offset << 3); - w.write_data(a & 0xFF); - w.write_data(a >> 8); - w.write_data(0); - } else { - // Write literal - w.write_control(true); - w.write_data(in_data[z]); - } - w.flush_if_ready(); - window.advance(); - } - - // Write stop command - w.write_control(false); - w.flush_if_ready(); - w.write_control(true); - w.write_data(0); - w.write_data(0); - - return std::move(w.close()); -} - -PRSCompressor::PRSCompressor( - ssize_t compression_level, ProgressCallback progress_fn) - : compression_level(compression_level), - progress_fn(progress_fn), - closed(false), - control_byte_offset(0), - pending_control_bits(0), - input_bytes(0) { - this->output.put_u8(0); -} - -void PRSCompressor::add(const void* data, size_t size) { - if (this->closed) { - throw logic_error("compressor is closed"); - } - - StringReader r(data, size); - while (!r.eof()) { - this->add_byte(r.get_u8()); - } -} - -void PRSCompressor::add(const string& data) { - this->add(data.data(), data.size()); -} - -void PRSCompressor::add_byte(uint8_t v) { - if (this->reverse_log.end_offset() + this->forward_log.data.size() <= this->input_bytes) { - this->advance(); - } - this->forward_log.at(this->input_bytes) = v; - this->input_bytes++; -} - -void PRSCompressor::advance() { - // Search for a match in the decompressed data history - size_t best_match_size = 0; - size_t best_match_offset = 0; - size_t best_match_literals = 0; - for (ssize_t num_literals = 0; num_literals <= this->compression_level; num_literals++) { - for (size_t z = 0; z < static_cast(num_literals); z++) { - this->reverse_log.push_back(this->forward_log.at(this->reverse_log.end_offset())); - } - - size_t compression_offset = reverse_log.end_offset(); - uint8_t first_v = this->forward_log.at(compression_offset); - const auto& start_offsets = this->reverse_log.find(first_v); - - for (auto it = start_offsets.begin(); (it != start_offsets.end()) && (best_match_size < 0x100); it++) { - size_t match_offset = *it; - if (match_offset + 0x2000 <= compression_offset) { - continue; - } - - size_t match_size = 0; - size_t match_loop_bytes = compression_offset - match_offset; - while ((match_size < 0x100) && - (compression_offset + match_size < this->input_bytes) && - (this->reverse_log.at(match_offset + (match_size % match_loop_bytes)) == this->forward_log.at(compression_offset + match_size))) { - match_size++; - } - - // If there are multiple matches of the longest length, use the latest one, - // since it's more likely that it can be expressed as a short copy instead - // of a long copy. - if (match_size >= (best_match_size + best_match_literals)) { - best_match_offset = match_offset; - best_match_size = match_size; - best_match_literals = num_literals; - } - } - for (size_t z = 0; z < static_cast(num_literals); z++) { - this->reverse_log.pop_back(); - } - } - - // If the best match has literals preceding it, write those literals - for (size_t z = 0; z < best_match_literals; z++) { - this->advance_literal(); - } - - // If there is a suitable match, write a backreference; otherwise, write a - // literal. The backreference should be encoded: - // - As a short copy if offset in [-0x100, -1] and size in [2, 5] - // - As a long copy if offset in [-0x1FFF, -1] and size in [3, 9] - // - As an extended copy if offset in [-0x1FFF, -1] and size in [10, 0x100] - // Technically an extended copy can be used for sizes 1-9 as well, but if - // size is 1 or 2, writing literals is better (since it uses fewer data - // bytes and control bits), and a long copy can cover sizes 3-9 (and also - // uses fewer data bytes and control bits). - ssize_t backreference_offset = best_match_offset - this->reverse_log.end_offset(); - if (best_match_size < 2) { - // The match is too small; a literal would use fewer bits - this->advance_literal(); - - } else if ((backreference_offset >= -0x100) && (best_match_size <= 5)) { - this->advance_short_copy(backreference_offset, best_match_size); - - } else if (best_match_size < 3) { - // We can't use a long copy for size 2, and it's not worth it to use an - // extended copy for this either (as noted above), so write a literal - this->advance_literal(); - - } else if ((backreference_offset >= -0x1FFF) && (best_match_size <= 9)) { - this->advance_long_copy(backreference_offset, best_match_size); - - } else if ((backreference_offset >= -0x1FFF) && (best_match_size <= 0x100)) { - this->advance_extended_copy(backreference_offset, best_match_size); - - } else { - throw logic_error("invalid best match"); - } -} - -void PRSCompressor::move_forward_data_to_reverse_log(size_t size) { - for (; size > 0; size--) { - this->reverse_log.push_back(this->forward_log.at(this->reverse_log.end_offset())); - if (this->progress_fn && ((this->reverse_log.end_offset() & 0xFFF) == 0)) { - this->progress_fn(CompressPhase::GENERATE_RESULT, this->reverse_log.end_offset(), this->input_bytes, this->output.size()); - } - } -} - -void PRSCompressor::advance_literal() { - this->write_control(true); - this->output.put_u8(this->forward_log.at(this->reverse_log.end_offset())); - this->move_forward_data_to_reverse_log(1); -} - -void PRSCompressor::advance_short_copy(ssize_t offset, size_t size) { - uint8_t encoded_size = size - 2; - this->write_control(false); - this->write_control(false); - this->write_control(encoded_size & 2); - this->write_control(encoded_size & 1); - this->output.put_u8(offset & 0xFF); - this->move_forward_data_to_reverse_log(size); -} - -void PRSCompressor::advance_long_copy(ssize_t offset, size_t size) { - this->write_control(false); - this->write_control(true); - uint16_t a = (offset << 3) | (size - 2); - this->output.put_u8(a & 0xFF); - this->output.put_u8(a >> 8); - this->move_forward_data_to_reverse_log(size); -} - -void PRSCompressor::advance_extended_copy(ssize_t offset, size_t size) { - this->write_control(false); - this->write_control(true); - uint16_t a = (offset << 3); - this->output.put_u8(a & 0xFF); - this->output.put_u8(a >> 8); - this->output.put_u8(size - 1); - this->move_forward_data_to_reverse_log(size); -} - -string& PRSCompressor::close() { - if (!this->closed) { - // Advance until all input is consumed - while (this->reverse_log.end_offset() < this->input_bytes) { - this->advance(); - } - // Write stop command - this->write_control(false); - this->write_control(true); - this->output.put_u8(0); - this->output.put_u8(0); - // Write remaining control bits - this->flush_control(); - this->closed = true; - } - return this->output.str(); -} - -void PRSCompressor::write_control(bool z) { - if (this->pending_control_bits & 0x0100) { - this->output.pput_u8( - this->control_byte_offset, this->pending_control_bits & 0xFF); - this->control_byte_offset = this->output.size(); - this->output.put_u8(0); - this->pending_control_bits = z ? 0x8080 : 0x8000; - } else { - this->pending_control_bits = - (this->pending_control_bits >> 1) | (z ? 0x8080 : 0x8000); - } -} - -void PRSCompressor::flush_control() { - if (this->pending_control_bits & 0xFF00) { - while (!(this->pending_control_bits & 0x0100)) { - this->pending_control_bits >>= 1; - } - this->output.pput_u8( - this->control_byte_offset, this->pending_control_bits & 0xFF); - } else { - if (this->control_byte_offset != this->output.size() - 1) { - throw logic_error("data written without control bits"); - } - this->output.str().resize(this->output.str().size() - 1); - } -} - -string prs_compress( - const void* vdata, - size_t size, - ssize_t compression_level, - ProgressCallback progress_fn) { - PRSCompressor prs(compression_level, progress_fn); - prs.add(vdata, size); - return std::move(prs.close()); -} - -string prs_compress( - const string& data, - ssize_t compression_level, - ProgressCallback progress_fn) { - return prs_compress(data.data(), data.size(), compression_level, progress_fn); -} - -string prs_compress_indexed( - const void* in_data_v, size_t in_size, ProgressCallback progress_fn) { - const uint8_t* in_data = reinterpret_cast(in_data_v); - - LZSSInterleavedWriter w; - WindowIndex<0x100, 5> w_short(in_data_v, in_size); - WindowIndex<0x1FFF, 9> w_long(in_data_v, in_size); - WindowIndex<0x1FFF, 0x100> w_extended(in_data_v, in_size); - - size_t last_progress_fn_call_offset = 0; - while (w_short.offset < in_size) { - if (progress_fn && ((last_progress_fn_call_offset & ~0xFFF) != (w_short.offset & ~0xFFF))) { - last_progress_fn_call_offset = w_short.offset; - progress_fn(CompressPhase::GENERATE_RESULT, w_short.offset, in_size, w.size()); - } - - auto m_short = w_short.get_best_match(); - auto m_long = w_long.get_best_match(); - auto m_extended = w_extended.get_best_match(); - - // Write the match that achieves the best ratio of output bytes to - // compressed bits used. To do this without floating-point math, we multiply - // the output byte count for each type of command by 468 / (command_bits), - // since 468 is the least common multiple of the number of bits for each - // command type. The command type with the highest score is the one we'll - // use, breaking ties by choosing the shorter command type. Note that the - // size of any copy type can be zero if no match was found; if no matches - // were found at all, then we can always write a literal. - size_t score_literal = 52; - size_t score_short = m_short.second * 39; - size_t score_long = m_long.second * 26; - size_t score_extended = m_extended.second * 18; - PRSPathNode::CommandType command_type = PRSPathNode::CommandType::NONE; - if (score_literal < score_short) { - if (score_short < score_long) { - if (score_long < score_extended) { - command_type = PRSPathNode::CommandType::EXTENDED_COPY; - } else { - command_type = PRSPathNode::CommandType::LONG_COPY; - } - } else { - if (score_short < score_extended) { - command_type = PRSPathNode::CommandType::EXTENDED_COPY; - } else { - command_type = PRSPathNode::CommandType::SHORT_COPY; - } - } - } else { - if (score_literal < score_long) { - if (score_long < score_extended) { - command_type = PRSPathNode::CommandType::EXTENDED_COPY; - } else { - command_type = PRSPathNode::CommandType::LONG_COPY; - } - } else { - if (score_literal < score_extended) { - command_type = PRSPathNode::CommandType::EXTENDED_COPY; - } else { - command_type = PRSPathNode::CommandType::LITERAL; - } - } - } - - size_t bytes_consumed = 0; - switch (command_type) { - case PRSPathNode::CommandType::LITERAL: - w.write_control(true); - w.write_data(in_data[w_short.offset]); - bytes_consumed = 1; - break; - case PRSPathNode::CommandType::SHORT_COPY: { - ssize_t backreference_offset = m_short.first - w_short.offset; - uint8_t encoded_size = m_short.second - 2; - w.write_control(false); - w.flush_if_ready(); - w.write_control(false); - w.flush_if_ready(); - w.write_control(encoded_size & 2); - w.flush_if_ready(); - w.write_control(encoded_size & 1); - w.write_data(backreference_offset & 0xFF); - bytes_consumed = m_short.second; - break; - } - case PRSPathNode::CommandType::LONG_COPY: { - ssize_t backreference_offset = m_long.first - w_long.offset; - w.write_control(false); - w.flush_if_ready(); - w.write_control(true); - uint16_t a = (backreference_offset << 3) | (m_long.second - 2); - w.write_data(a & 0xFF); - w.write_data(a >> 8); - bytes_consumed = m_long.second; - break; - } - case PRSPathNode::CommandType::EXTENDED_COPY: { - ssize_t backreference_offset = m_extended.first - w_extended.offset; - w.write_control(false); - w.flush_if_ready(); - w.write_control(true); - uint16_t a = (backreference_offset << 3); - w.write_data(a & 0xFF); - w.write_data(a >> 8); - w.write_data(m_extended.second - 1); - bytes_consumed = m_extended.second; - break; - } - case PRSPathNode::CommandType::NONE: - default: - throw logic_error("invalid command type"); - } - w.flush_if_ready(); - - if (bytes_consumed == 0) { - throw logic_error("no input data was consumed"); - } - - for (size_t z = 0; z < bytes_consumed; z++) { - w_short.advance(); - w_long.advance(); - w_extended.advance(); - } - } - - // Write stop command - w.write_control(false); - w.flush_if_ready(); - w.write_control(true); - w.write_data(0); - w.write_data(0); - - return std::move(w.close()); -} - -string prs_compress_indexed(const string& data, ProgressCallback progress_fn) { - return prs_compress_indexed(data.data(), data.size(), progress_fn); -} - -PRSDecompressResult prs_decompress_with_meta( - const void* data, size_t size, size_t max_output_size, bool allow_unterminated) { - // PRS is an LZ77-based compression algorithm. Compressed data is split into - // two streams: a control stream and a data stream. The control stream is read - // one bit at a time, and the data stream is read one byte at a time. The - // streams are interleaved such that the decompressor never has to move - // backward in the input stream - when the decompressor needs a control bit - // and there are no unused bits from the previous byte of the control stream, - // it reads a byte from the input and treats it as the next 8 control bits. - - // There are 3 distinct commands in PRS, labeled here with their control bits: - // 1 - Literal byte. The decompressor copies one byte from the input data - // stream to the output. - // 00 - Short backreference. The decompressor reads two control bits and adds - // 2 to this value to determine the number of bytes to copy, then reads - // one byte from the data stream to determine how far back in the output - // to copy from. This byte is treated as an 8-bit negative number - so - // 0xF7, for example, means to start copying data from 9 bytes before the - // end of the output. The range must start before the end of the output, - // but the end of the range may be beyond the end of the output. In this - // case, the bytes between the beginning of the range and original end of - // the output are simply repeated. - // 01 - Long backreference. The decompressor reads two bytes from the data and - // byteswaps the resulting 16-bit value (that is, the low byte is read - // first). The start offset (again, as a negative number) is the top 13 - // bits of this value; the size is the low 3 bits of this value, plus 2. - // If the size bits are all zero, an additional byte is read from the - // data stream and 1 is added to it to determine the backreference size - // (we call this an extended backreference). Therefore, the maximum - // backreference size is 256 bytes. - // Decompression ends when either there are no more input bytes to read, or - // when a long backreference is read with all zeroes in its offset field. The - // original implementation stops decompression successfully when any attempt - // to read from the input encounters the end of the stream, but newserv's - // implementation only allows this at the end of an opcode - if end-of-stream - // is encountered partway through an opcode, we throw instead, because it's - // likely the input has been truncated or is malformed in some way. - - StringWriter w; - StringReader r(data, size); - ControlStreamReader cr(r); - - while (!r.eof()) { - // Control 1 = literal byte - if (cr.read()) { - if (max_output_size && w.size() == max_output_size) { - if (allow_unterminated) { - return {std::move(w.str()), r.where()}; - } else { - throw runtime_error("maximum output size exceeded"); - } - } - w.put_u8(r.get_u8()); - - } else { - ssize_t offset; - size_t count; - - // Control 01 = long backreference - if (cr.read()) { - // The bits stored in the data stream are AAAAABBBCCCCCCCC, which we - // rearrange into offset = CCCCCCCCAAAAA and size = BBB. - uint16_t a = r.get_u8(); - a |= (r.get_u8() << 8); - offset = (a >> 3) | (~0x1FFF); - // If offset is zero, it's a stop opcode - if (offset == ~0x1FFF) { - break; - } - // If the size field is zero, it's an extended backreference (size comes - // from another byte in the data stream) - count = (a & 7) ? ((a & 7) + 2) : (r.get_u8() + 1); - - // Control 00 = short backreference - } else { - // Count comes from 2 bits in the control stream instead of from the - // data stream (and 2 is added). Importantly, the control stream bits - // are read first - this may involve reading another control stream - // byte, which happens before the offset is read from the data stream. - count = cr.read() << 1; - count = (count | cr.read()) + 2; - offset = r.get_u8() | (~0xFF); - } - - // Copy bytes from the referenced location in the output. Importantly, - // copy only one byte at a time, in order to support ranges that cover the - // current end of the output. - size_t read_offset = w.size() + offset; - if (read_offset >= w.size()) { - throw runtime_error("backreference offset beyond beginning of output"); - } - for (size_t z = 0; z < count; z++) { - if (max_output_size && w.size() == max_output_size) { - if (allow_unterminated) { - return {std::move(w.str()), r.where()}; - } else { - throw out_of_range("maximum output size exceeded"); - } - } - w.put_u8(w.str()[read_offset + z]); - } - } - } - - return {std::move(w.str()), r.where()}; -} - -PRSDecompressResult prs_decompress_with_meta(const string& data, size_t max_output_size, bool allow_unterminated) { - return prs_decompress_with_meta(data.data(), data.size(), max_output_size, allow_unterminated); -} - -string prs_decompress(const void* data, size_t size, size_t max_output_size, bool allow_unterminated) { - auto ret = prs_decompress_with_meta(data, size, max_output_size, allow_unterminated); - return std::move(ret.data); -} - -string prs_decompress(const string& data, size_t max_output_size, bool allow_unterminated) { - auto ret = prs_decompress_with_meta(data.data(), data.size(), max_output_size, allow_unterminated); - return std::move(ret.data); -} - -size_t prs_decompress_size(const void* data, size_t size, size_t max_output_size, bool allow_unterminated) { - size_t ret = 0; - StringReader r(data, size); - ControlStreamReader cr(r); - - while (!r.eof()) { - if (cr.read()) { - ret++; - r.get_u8(); - - } else { - ssize_t offset; - size_t count; - - if (cr.read()) { - uint16_t a = r.get_u8(); - a |= (r.get_u8() << 8); - offset = (a >> 3) | (~0x1FFF); - if (offset == ~0x1FFF) { - break; - } - count = (a & 7) ? ((a & 7) + 2) : (r.get_u8() + 1); - - } else { - count = cr.read() << 1; - count = (count | cr.read()) + 2; - offset = r.get_u8() | (~0xFF); - } - - size_t read_offset = ret + offset; - if (read_offset >= ret) { - throw runtime_error("backreference offset beyond beginning of output"); - } - ret += count; - } - - if (max_output_size && ret > max_output_size) { - if (allow_unterminated) { - return max_output_size; - } else { - throw out_of_range("maximum output size exceeded"); - } - } - } - - return ret; -} - -size_t prs_decompress_size(const string& data, size_t max_output_size, bool allow_unterminated) { - return prs_decompress_size(data.data(), data.size(), max_output_size, allow_unterminated); -} - -void prs_disassemble(FILE* stream, const void* data, size_t size) { - size_t output_bytes = 0; - StringReader r(data, size); - ControlStreamReader cr(r); - - while (!r.eof()) { - uint8_t buffered_bits = cr.buffered_bits(); - if (cr.read()) { - uint8_t literal_value = r.get_u8(); - fprintf(stream, "[%zX] %hhu> 1 %02hhX literal %02hhX\n", - output_bytes, buffered_bits, literal_value, literal_value); - output_bytes++; - - } else { - size_t count, read_offset; - if (cr.read()) { - uint8_t a_low = r.get_u8(); - uint8_t a_high = r.get_u8(); - uint16_t a = (a_high << 8) | a_low; - ssize_t offset = (a >> 3) | (~0x1FFF); - if (offset == ~0x1FFF) { - fprintf(stream, "[%zX] end\n", output_bytes); - break; - } - if (a & 7) { - count = (a & 7) + 2; - read_offset = output_bytes + offset; - fprintf(stream, "[%zX] %hhu> 01 %02hhX %02hhX long copy from %zd (offset=%zX) size=%zX\n", - output_bytes, buffered_bits, a_low, a_high, offset, read_offset, count); - } else { - uint8_t count_u8 = r.get_u8(); - count = count_u8 + 1; - read_offset = output_bytes + offset; - fprintf(stream, "[%zX] %hhu> 01 %02hhX %02hhX %02hhX extended copy from %zd (offset=%zX) size=%zX\n", - output_bytes, buffered_bits, a_low, a_high, count_u8, offset, read_offset, count); - } - - } else { - bool first_bit = cr.read(); - bool second_bit = cr.read(); - uint8_t offset_u8 = r.get_u8(); - count = ((first_bit ? 2 : 0) | (second_bit ? 1 : 0)) + 2; - ssize_t offset = offset_u8 | (~0xFF); - read_offset = output_bytes + offset; - fprintf(stream, "[%zX] %hhu> 00%c%c %02hhX short copy from %zd (offset=%zX) size=%zX\n", - output_bytes, buffered_bits, first_bit ? '1' : '0', second_bit ? '1' : '0', offset_u8, offset, read_offset, count); - } - - if (read_offset >= output_bytes) { - throw runtime_error("backreference offset beyond beginning of output"); - } - output_bytes += count; - } - } -} - -void prs_disassemble(FILE* stream, const std::string& data) { - return prs_disassemble(stream, data.data(), data.size()); -} - -// BC0 is a compression algorithm fairly similar to PRS, but with a simpler set -// of commands. Like PRS, there is a control stream, indicating when to copy a -// literal byte from the input and when to copy from a backreference; unlike -// PRS, there is only one type of backreference. Also, there is no stop opcode; -// the decompressor simply stops when there are no more input bytes to read. - -struct BC0PathNode { - uint16_t memo_offset = 0; - uint8_t max_copy_size = 0; - - // Pathfinding state - size_t from_offset = 0; - size_t bits_used = static_cast(-1); - - // Stream generation state - size_t to_offset = 0; -}; - -string bc0_compress_optimal( - const void* in_data_v, size_t in_size, ProgressCallback progress_fn) { - const uint8_t* in_data = reinterpret_cast(in_data_v); - - vector nodes; - nodes.resize(in_size + 1); - nodes[0].bits_used = 0; - - // Populate all possible backreferences - { - WindowIndex<0x1000, 0x12> window(in_data_v, in_size); - while (window.offset < in_size) { - if ((window.offset & 0xFFF) == 0 && progress_fn) { - progress_fn(CompressPhase::INDEX, window.offset, in_size, 0); - } - auto& node = nodes[window.offset]; - auto match = window.get_best_match(); - if (match.second >= 3) { - node.memo_offset = (match.first - 0x12) & 0xFFF; - node.max_copy_size = match.second; - } - window.advance(); - } - } - - // For each node, populate the literal value, and the best ways to get to the - // following nodes - for (size_t z = 0; z < in_size; z++) { - if ((z & 0xFFF) == 0 && progress_fn) { - progress_fn(CompressPhase::CONSTRUCT_PATHS, z, in_size, 0); - } - - auto& node = nodes[z]; - - // Literal: 1 control bit + 1 data byte - size_t bits_used = node.bits_used + 9; - { - auto& next_node = nodes[z + 1]; - if (next_node.bits_used > bits_used) { - next_node.from_offset = z; - next_node.bits_used = bits_used; - } - } - - // Backreference: 1 control bit + 2 data bytes - bits_used = node.bits_used + 17; - for (size_t x = 3; x <= node.max_copy_size; x++) { - auto& next_node = nodes[z + x]; - if (next_node.bits_used > bits_used) { - next_node.from_offset = z; - next_node.bits_used = bits_used; - } - } - } - - // Find the shortest path from the last node to the first node - size_t last_progress_fn_call = static_cast(-1); - for (size_t z = in_size; z > 0;) { - if ((z & ~0xFFF) != (last_progress_fn_call & ~0xFFF)) { - last_progress_fn_call = z; - if (progress_fn) { - progress_fn(CompressPhase::BACKTRACE_OPTIMAL_PATH, z, in_size, 0); - } - } - size_t from_offset = nodes[z].from_offset; - nodes[from_offset].to_offset = z; - z = from_offset; - } - - // Produce the BC0 command stream from the shortest path - LZSSInterleavedWriter w; - last_progress_fn_call = static_cast(-1); - for (size_t offset = 0; offset < in_size;) { - if ((offset & ~0xFFF) != (last_progress_fn_call & ~0xFFF)) { - last_progress_fn_call = offset; - if (progress_fn) { - progress_fn(CompressPhase::GENERATE_RESULT, offset, in_size, w.size()); - } - } - - const auto& node = nodes[offset]; - size_t copy_size = node.to_offset - offset; - if (copy_size >= 3 && copy_size <= 0x12) { - w.write_control(false); - w.write_data(node.memo_offset & 0xFF); - w.write_data(((node.memo_offset >> 4) & 0xF0) | (copy_size - 3)); - } else if (copy_size == 1) { - w.write_control(true); - w.write_data(in_data[offset]); - } - w.flush_if_ready(); - - offset = node.to_offset; - } - - return std::move(w.close()); -} - -string bc0_compress(const string& data, ProgressCallback progress_fn) { - return bc0_compress(data.data(), data.size(), progress_fn); -} - -string bc0_compress(const void* in_data_v, size_t in_size, ProgressCallback progress_fn) { - const uint8_t* in_data = reinterpret_cast(in_data_v); - - LZSSInterleavedWriter w; - WindowIndex<0x1000, 0x12> window(in_data_v, in_size); - - size_t last_progress_fn_call_offset = 0; - while (window.offset < in_size) { - if (progress_fn && ((last_progress_fn_call_offset & ~0xFFF) != (window.offset & ~0xFFF))) { - last_progress_fn_call_offset = window.offset; - progress_fn(CompressPhase::GENERATE_RESULT, window.offset, in_size, w.size()); - } - - auto match = window.get_best_match(); - - // Write a backreference if a match was found; otherwise, write a literal - if (match.second >= 3) { - w.write_control(false); - size_t memo_offset = match.first - 0x12; - w.write_data(memo_offset & 0xFF); - w.write_data(((memo_offset >> 4) & 0xF0) | (match.second - 3)); - } else { - w.write_control(true); - w.write_data(in_data[window.offset]); - match.second = 1; - } - w.flush_if_ready(); - - for (size_t z = 0; z < match.second; z++) { - window.advance(); - } - } - - return std::move(w.close()); -} - -string bc0_encode(const void* in_data_v, size_t in_size) { - const uint8_t* in_data = reinterpret_cast(in_data_v); - - LZSSInterleavedWriter w; - for (size_t z = 0; z < in_size; z++) { - w.write_control(true); - w.write_data(in_data[z]); - w.flush_if_ready(); - } - - return std::move(w.close()); -} - -// The BC0 decompression implementation in PSO GC is vulnerable to overflow -// attacks - there is no bounds checking on the output buffer. It is unlikely -// that this can be usefully exploited (e.g. for RCE) because the output pointer -// is loaded from memory before every byte is written, so we cannot change the -// output pointer to any arbitrary address. - -string bc0_decompress(const string& data) { - return bc0_decompress(data.data(), data.size()); -} - -string bc0_decompress(const void* data, size_t size) { - StringReader r(data, size); - StringWriter w; - - // Unlike PRS, BC0 uses a memo which "rolls over" every 0x1000 bytes. The - // boundaries of these "memo pages" are offset by -0x12 bytes for some reason, - // so the first output byte corresponds to position 0xFEE on the first memo - // page. Backreferences refer to offsets based on the start of memo pages; for - // example, if the current output offset is 0x1234, a backreference with - // offset 0x123 refers to the byte that was written at offset 0x1111 (because - // that byte is at offset 0x111 in the memo, because the memo rolls over every - // 0x1000 bytes and the first memo byte was 0x12 bytes before the beginning of - // the next page). The memo is initially zeroed from 0 to 0xFEE; it seems PSO - // GC doesn't initialize the last 0x12 bytes of the first memo page. - parray memo; - uint16_t memo_offset = 0x0FEE; - - // The low byte of this value contains the control stream data; the high bits - // specify which low bits are valid. When the last 1 is shifted out of the - // high byte, we need to read a new control stream byte to get the next set of - // control bits. - uint16_t control_stream_bits = 0x0000; - - while (!r.eof()) { - // Read control stream bits if needed - control_stream_bits >>= 1; - if ((control_stream_bits & 0x100) == 0) { - control_stream_bits = 0xFF00 | r.get_u8(); - if (r.eof()) { - break; - } - } - - // Control bit 0 means to perform a backreference copy. The offset and - // size are stored in two bytes in the input stream, laid out as follows: - // a1 = 0bBBBBBBBB - // a2 = 0bAAAACCCC - // The offset is the concatenation of bits AAAABBBBBBBB, which refers to a - // position in the memo; the number of bytes to copy is (CCCC + 3). The - // decompressor copies that many bytes from that offset in the memo, and - // writes them to the output and to the current position in the memo. - if ((control_stream_bits & 1) == 0) { - uint8_t a1 = r.get_u8(); - if (r.eof()) { - break; - } - uint8_t a2 = r.get_u8(); - size_t count = (a2 & 0x0F) + 3; - size_t backreference_offset = a1 | ((a2 << 4) & 0xF00); - for (size_t z = 0; z < count; z++) { - uint8_t v = memo[(backreference_offset + z) & 0x0FFF]; - w.put_u8(v); - memo[memo_offset] = v; - memo_offset = (memo_offset + 1) & 0x0FFF; - } - - // Control bit 1 means to write a byte directly from the input to the - // output. As above, the byte is also written to the memo. - } else { - uint8_t v = r.get_u8(); - w.put_u8(v); - memo[memo_offset] = v; - memo_offset = (memo_offset + 1) & 0x0FFF; - } - } - - return std::move(w.str()); -} - -void bc0_disassemble(FILE* stream, const string& data) { - bc0_disassemble(stream, data.data(), data.size()); -} - -void bc0_disassemble(FILE* stream, const void* data, size_t size) { - StringReader r(data, size); - uint16_t control_stream_bits = 0x0000; - - size_t output_bytes = 0; - while (!r.eof()) { - // size_t opcode_offset = r.where(); - - control_stream_bits >>= 1; - if ((control_stream_bits & 0x100) == 0) { - control_stream_bits = 0xFF00 | r.get_u8(); - if (r.eof()) { - break; - } - } - - if ((control_stream_bits & 1) == 0) { - uint8_t a1 = r.get_u8(); - if (r.eof()) { - break; - } - (void)a1; - uint8_t a2 = r.get_u8(); - size_t count = (a2 & 0x0F) + 3; - // size_t backreference_offset = a1 | ((a2 << 4) & 0xF00); - fprintf(stream, "[%zX] backreference %02zX\n", output_bytes, count); - output_bytes += count; - - } else { - fprintf(stream, "[%zX] literal %02hhX\n", output_bytes, r.get_u8()); - output_bytes++; - } - } -} +#include "Compression.hh" + +#include +#include +#include +#include +#include + +#include +#include + +#include "Text.hh" + +using namespace std; + +template <> +const char* name_for_enum(CompressPhase v) { + switch (v) { + case CompressPhase::INDEX: + return "INDEX"; + case CompressPhase::CONSTRUCT_PATHS: + return "CONSTRUCT_PATHS"; + case CompressPhase::BACKTRACE_OPTIMAL_PATH: + return "BACKTRACE_OPTIMAL_PATH"; + case CompressPhase::GENERATE_RESULT: + return "GENERATE_RESULT"; + default: + return "__UNKNOWN__"; + } +} + +template +struct WindowIndex { + const uint8_t* data; + size_t size; + size_t offset; + set> index; + + WindowIndex(const void* data, size_t size) + : data(reinterpret_cast(data)), + size(size), + offset(0), + index(bind(&WindowIndex::set_comparator, this, placeholders::_1, placeholders::_2)) {} + + void advance() { + if (this->offset >= WindowLength) { + this->index.erase(this->offset - WindowLength); + } + this->index.emplace(this->offset); + this->offset++; + } + + size_t get_match_length(size_t match_offset) const { + size_t match_iter = match_offset; + size_t offset_iter = this->offset; + while ((match_iter < match_offset + MaxMatchLength) && + (match_iter < this->size) && + (offset_iter < this->size) && + (this->data[match_iter] == this->data[offset_iter])) { + match_iter++; + offset_iter++; + } + return match_iter - match_offset; + }; + + // The data structure we want is a binary-searchable set of all strings + // starting at all possible offsets within the sliding window, and we need + // to be able to search lexicographically but insert and delete by offset. + // A std::map would accomplish this, but would be + // horrendously inefficient: we'd have to copy strings far too much. We can + // solve this by instead storing the offset of each string as keys in a set + // and using a custom comparator to treat them as references to binary + // strings within the data. + bool set_comparator(size_t a, size_t b) const { + size_t max_length = min(MaxMatchLength, this->size - max(a, b)); + size_t end_a = a + max_length; + for (; a < end_a; a++, b++) { + uint8_t data_a = static_cast(this->data[a]); + uint8_t data_b = static_cast(this->data[b]); + if (data_a < data_b) { + return true; // a comes before b lexicographically + } else if (data_a > data_b) { + return false; // a comes after b lexicographically + } + } + return a < b; // Maximum-length match; order them by offset + }; + + pair get_best_match() const { + // Find the best match from the index. It's unlikely that we'll get an + // exact match, so check the entry before the upper_bound result too. + // Note: We use upper_bound rather than lower_bound because in PRS, a + // backreference can be encoded with fewer bits if it's close to the + // decompression offset, and this makes us pick the latest match by + // default. + size_t match_offset = 0; + size_t match_size = 0; + auto it = this->index.upper_bound(this->offset); + if (it != this->index.end()) { + size_t new_match_offset = *it; + size_t new_match_size = this->get_match_length(new_match_offset); + if ((new_match_size > match_size) || (new_match_size == match_size && new_match_offset > match_offset)) { + match_offset = new_match_offset; + match_size = new_match_size; + } + } + if (it != this->index.begin()) { + it--; + size_t new_match_offset = *it; + size_t new_match_size = this->get_match_length(new_match_offset); + if ((new_match_size > match_size) || (new_match_size == match_size && new_match_offset > match_offset)) { + match_offset = new_match_offset; + match_size = new_match_size; + } + } + return make_pair(match_offset, match_size); + } +}; + +struct LZSSInterleavedWriter { + StringWriter w; + size_t buf_offset; + uint8_t next_control_bit; + uint8_t buf[0x19]; + + LZSSInterleavedWriter() + : buf_offset(1), + next_control_bit(1) { + this->buf[0] = 0; + } + + void flush_if_ready() { + if (this->next_control_bit == 0) { + this->w.write(this->buf, this->buf_offset); + this->buf[0] = 0; + this->buf_offset = 1; + this->next_control_bit = 1; + } + } + + std::string&& close() { + if (this->buf_offset > 1 || this->next_control_bit != 1) { + this->w.write(this->buf, this->buf_offset); + } + return std::move(this->w.str()); + } + + void write_control(bool v) { + if (this->next_control_bit == 0) { + throw logic_error("write_control called with no space to write"); + } + if (v) { + this->buf[0] |= this->next_control_bit; + } + this->next_control_bit <<= 1; + } + + void write_data(uint8_t v) { + this->buf[this->buf_offset++] = v; + } + + size_t size() const { + return this->w.size() + this->buf_offset; + } +}; + +class ControlStreamReader { +public: + ControlStreamReader(StringReader& r) + : r(r), + bits(0x0000) {} + + bool read() { + if (!(this->bits & 0x0100)) { + this->bits = 0xFF00 | this->r.get_u8(); + } + bool ret = this->bits & 1; + this->bits >>= 1; + return ret; + } + + uint8_t buffered_bits() const { + uint16_t z = this->bits; + uint8_t ret = 0; + for (; z & 0x0100; z >>= 1, ret++) { + } + return ret; + } + +private: + StringReader& r; + uint16_t bits; +}; + +struct PRSPathNode { + enum class CommandType { + NONE = 0, + LITERAL, + SHORT_COPY, + LONG_COPY, + EXTENDED_COPY, + }; + + int16_t short_copy_offset = 0; + uint8_t max_short_copy_size = 0; + int16_t long_copy_offset = 0; + uint8_t max_long_copy_size = 0; + int16_t extended_copy_offset = 0; + uint16_t max_extended_copy_size = 0; + + // Pathfinding state + size_t from_offset = 0; + CommandType from_command_type = CommandType::NONE; + size_t bits_used = static_cast(-1); + + // Stream generation state + size_t to_offset = 0; +}; + +string prs_compress_optimal(const void* in_data_v, size_t in_size, ProgressCallback progress_fn) { + const uint8_t* in_data = reinterpret_cast(in_data_v); + + vector nodes; + nodes.resize(in_size + 1); + nodes[0].bits_used = 18; // Stop command: 2 control bits and 2 data bytes + + size_t copy_progress_max = 3 * in_size; + atomic copy_progress = 0; + + // Populate all possible short copies + std::thread short_window_thread([&]() -> void { + WindowIndex<0x100, 5> window(in_data_v, in_size); + while (window.offset < in_size) { + if (window.offset && (window.offset & 0xFFF) == 0 && progress_fn) { + size_t progress = copy_progress.fetch_add(0x1000) + 0x1000; + progress_fn(CompressPhase::INDEX, progress, copy_progress_max, 0); + } + auto& node = nodes[window.offset]; + auto match = window.get_best_match(); + if (match.second >= 2) { + node.short_copy_offset = match.first - window.offset; + node.max_short_copy_size = match.second; + } + window.advance(); + } + }); + + // Populate all possible long copies + std::thread long_window_thread([&]() -> void { + WindowIndex<0x1FFF, 9> window(in_data_v, in_size); + while (window.offset < in_size) { + if (window.offset && (window.offset & 0xFFF) == 0 && progress_fn) { + size_t progress = copy_progress.fetch_add(0x1000) + 0x1000; + progress_fn(CompressPhase::INDEX, progress, copy_progress_max, 0); + } + auto& node = nodes[window.offset]; + auto match = window.get_best_match(); + if (match.second >= 3) { + node.long_copy_offset = match.first - window.offset; + node.max_long_copy_size = match.second; + } + window.advance(); + } + }); + + // Populate all possible extended copies + std::thread extended_window_thread([&]() -> void { + WindowIndex<0x1FFF, 0x100> window(in_data_v, in_size); + while (window.offset < in_size) { + if (window.offset && (window.offset & 0xFFF) == 0 && progress_fn) { + size_t progress = copy_progress.fetch_add(0x1000) + 0x1000; + progress_fn(CompressPhase::INDEX, progress, copy_progress_max, 0); + } + auto& node = nodes[window.offset]; + auto match = window.get_best_match(); + if (match.second >= 1) { + node.extended_copy_offset = match.first - window.offset; + node.max_extended_copy_size = match.second; + } + window.advance(); + } + }); + + short_window_thread.join(); + long_window_thread.join(); + extended_window_thread.join(); + + // For each node, populate the literal value, and the best ways to get to the + // following nodes + for (size_t z = 0; z < in_size; z++) { + if ((z & 0xFFF) == 0 && progress_fn) { + progress_fn(CompressPhase::CONSTRUCT_PATHS, z, in_size, 0); + } + + auto& node = nodes[z]; + + // Literal: 1 control bit + 1 data byte + size_t bits_used = node.bits_used + 9; + { + auto& next_node = nodes[z + 1]; + if (next_node.bits_used > bits_used) { + next_node.from_offset = z; + next_node.from_command_type = PRSPathNode::CommandType::LITERAL; + next_node.bits_used = bits_used; + } + } + + // Short copy: 4 control bits + 1 data byte + bits_used = node.bits_used + 12; + for (size_t x = 2; x <= node.max_short_copy_size; x++) { + auto& next_node = nodes[z + x]; + if (next_node.bits_used > bits_used) { + next_node.from_offset = z; + next_node.from_command_type = PRSPathNode::CommandType::SHORT_COPY; + next_node.bits_used = bits_used; + } + } + + // Long copy: 2 control bits + 2 data bytes + bits_used = node.bits_used + 18; + for (size_t x = 3; x <= node.max_long_copy_size; x++) { + auto& next_node = nodes[z + x]; + if (next_node.bits_used > bits_used) { + next_node.from_offset = z; + next_node.from_command_type = PRSPathNode::CommandType::LONG_COPY; + next_node.bits_used = bits_used; + } + } + + // Extended copy: 2 control bits + 3 data bytes + bits_used = node.bits_used + 26; + for (size_t x = 1; x <= node.max_extended_copy_size; x++) { + auto& next_node = nodes[z + x]; + if (next_node.bits_used > bits_used) { + next_node.from_offset = z; + next_node.from_command_type = PRSPathNode::CommandType::EXTENDED_COPY; + next_node.bits_used = bits_used; + } + } + } + + // Find the shortest path from the last node to the first node + size_t last_progress_fn_call = static_cast(-1); + for (size_t z = in_size; z > 0;) { + if ((z & ~0xFFF) != (last_progress_fn_call & ~0xFFF)) { + last_progress_fn_call = z; + if (progress_fn) { + progress_fn(CompressPhase::BACKTRACE_OPTIMAL_PATH, z, in_size, 0); + } + } + size_t from_offset = nodes[z].from_offset; + nodes[from_offset].to_offset = z; + z = from_offset; + } + + // Produce the PRS command stream from the shortest path + LZSSInterleavedWriter w; + last_progress_fn_call = static_cast(-1); + for (size_t offset = 0; offset < in_size;) { + if ((offset & ~0xFFF) != (last_progress_fn_call & ~0xFFF)) { + last_progress_fn_call = offset; + if (progress_fn) { + progress_fn(CompressPhase::GENERATE_RESULT, offset, in_size, w.size()); + } + } + + const auto& node = nodes[offset]; + const auto& next_node = nodes[node.to_offset]; + + size_t copy_size = node.to_offset - offset; + switch (next_node.from_command_type) { + case PRSPathNode::CommandType::LITERAL: + if (copy_size != 1) { + throw logic_error("incorrect size for LITERAL copy type"); + } + w.write_control(true); + w.write_data(in_data[offset]); + break; + case PRSPathNode::CommandType::SHORT_COPY: { + if (copy_size < 2 || copy_size > 5) { + throw logic_error("incorrect size for SHORT_COPY copy type"); + } + uint8_t encoded_size = copy_size - 2; + w.write_control(false); + w.flush_if_ready(); + w.write_control(false); + w.flush_if_ready(); + w.write_control(encoded_size & 2); + w.flush_if_ready(); + w.write_control(encoded_size & 1); + w.write_data(node.short_copy_offset & 0xFF); + break; + } + case PRSPathNode::CommandType::LONG_COPY: { + if (copy_size < 2 || copy_size > 9) { + throw logic_error("incorrect size for LONG_COPY copy type"); + } + w.write_control(false); + w.flush_if_ready(); + w.write_control(true); + uint16_t a = (node.long_copy_offset << 3) | (copy_size - 2); + w.write_data(a & 0xFF); + w.write_data(a >> 8); + break; + } + case PRSPathNode::CommandType::EXTENDED_COPY: { + if (copy_size < 1 || copy_size > 0x100) { + throw logic_error("incorrect size for EXTENDED_COPY copy type"); + } + w.write_control(false); + w.flush_if_ready(); + w.write_control(true); + uint16_t a = (node.extended_copy_offset << 3); + w.write_data(a & 0xFF); + w.write_data(a >> 8); + w.write_data(copy_size - 1); + break; + } + default: + throw logic_error("invalid copy type in shortest path"); + } + w.flush_if_ready(); + + offset = node.to_offset; + } + + // Write stop command + w.write_control(false); + w.flush_if_ready(); + w.write_control(true); + w.write_data(0); + w.write_data(0); + + return std::move(w.close()); +} + +string prs_compress_optimal(const string& data, ProgressCallback progress_fn) { + return prs_compress_optimal(data.data(), data.size(), progress_fn); +} + +string prs_compress_pessimal(const void* vdata, size_t size) { + const uint8_t* in_data = reinterpret_cast(vdata); + + // The worst possible encoding we can do is a literal byte when no byte with + // the same value is within the window, or an extended copy if there is a byte + // with the same value in the window. + WindowIndex<0x1FFF, 1> window(in_data, size); + LZSSInterleavedWriter w; + for (size_t z = 0; z < size; z++) { + auto match = window.get_best_match(); + if (match.second >= 1) { + // Write extended copy + int16_t offset = match.first - window.offset; + w.write_control(false); + w.flush_if_ready(); + w.write_control(true); + uint16_t a = (offset << 3); + w.write_data(a & 0xFF); + w.write_data(a >> 8); + w.write_data(0); + } else { + // Write literal + w.write_control(true); + w.write_data(in_data[z]); + } + w.flush_if_ready(); + window.advance(); + } + + // Write stop command + w.write_control(false); + w.flush_if_ready(); + w.write_control(true); + w.write_data(0); + w.write_data(0); + + return std::move(w.close()); +} + +PRSCompressor::PRSCompressor( + ssize_t compression_level, ProgressCallback progress_fn) + : compression_level(compression_level), + progress_fn(progress_fn), + closed(false), + control_byte_offset(0), + pending_control_bits(0), + input_bytes(0) { + this->output.put_u8(0); +} + +void PRSCompressor::add(const void* data, size_t size) { + if (this->closed) { + throw logic_error("compressor is closed"); + } + + StringReader r(data, size); + while (!r.eof()) { + this->add_byte(r.get_u8()); + } +} + +void PRSCompressor::add(const string& data) { + this->add(data.data(), data.size()); +} + +void PRSCompressor::add_byte(uint8_t v) { + if (this->reverse_log.end_offset() + this->forward_log.data.size() <= this->input_bytes) { + this->advance(); + } + this->forward_log.at(this->input_bytes) = v; + this->input_bytes++; +} + +void PRSCompressor::advance() { + // Search for a match in the decompressed data history + size_t best_match_size = 0; + size_t best_match_offset = 0; + size_t best_match_literals = 0; + for (ssize_t num_literals = 0; num_literals <= this->compression_level; num_literals++) { + for (size_t z = 0; z < static_cast(num_literals); z++) { + this->reverse_log.push_back(this->forward_log.at(this->reverse_log.end_offset())); + } + + size_t compression_offset = reverse_log.end_offset(); + uint8_t first_v = this->forward_log.at(compression_offset); + const auto& start_offsets = this->reverse_log.find(first_v); + + for (auto it = start_offsets.begin(); (it != start_offsets.end()) && (best_match_size < 0x100); it++) { + size_t match_offset = *it; + if (match_offset + 0x2000 <= compression_offset) { + continue; + } + + size_t match_size = 0; + size_t match_loop_bytes = compression_offset - match_offset; + while ((match_size < 0x100) && + (compression_offset + match_size < this->input_bytes) && + (this->reverse_log.at(match_offset + (match_size % match_loop_bytes)) == this->forward_log.at(compression_offset + match_size))) { + match_size++; + } + + // If there are multiple matches of the longest length, use the latest one, + // since it's more likely that it can be expressed as a short copy instead + // of a long copy. + if (match_size >= (best_match_size + best_match_literals)) { + best_match_offset = match_offset; + best_match_size = match_size; + best_match_literals = num_literals; + } + } + for (size_t z = 0; z < static_cast(num_literals); z++) { + this->reverse_log.pop_back(); + } + } + + // If the best match has literals preceding it, write those literals + for (size_t z = 0; z < best_match_literals; z++) { + this->advance_literal(); + } + + // If there is a suitable match, write a backreference; otherwise, write a + // literal. The backreference should be encoded: + // - As a short copy if offset in [-0x100, -1] and size in [2, 5] + // - As a long copy if offset in [-0x1FFF, -1] and size in [3, 9] + // - As an extended copy if offset in [-0x1FFF, -1] and size in [10, 0x100] + // Technically an extended copy can be used for sizes 1-9 as well, but if + // size is 1 or 2, writing literals is better (since it uses fewer data + // bytes and control bits), and a long copy can cover sizes 3-9 (and also + // uses fewer data bytes and control bits). + ssize_t backreference_offset = best_match_offset - this->reverse_log.end_offset(); + if (best_match_size < 2) { + // The match is too small; a literal would use fewer bits + this->advance_literal(); + + } else if ((backreference_offset >= -0x100) && (best_match_size <= 5)) { + this->advance_short_copy(backreference_offset, best_match_size); + + } else if (best_match_size < 3) { + // We can't use a long copy for size 2, and it's not worth it to use an + // extended copy for this either (as noted above), so write a literal + this->advance_literal(); + + } else if ((backreference_offset >= -0x1FFF) && (best_match_size <= 9)) { + this->advance_long_copy(backreference_offset, best_match_size); + + } else if ((backreference_offset >= -0x1FFF) && (best_match_size <= 0x100)) { + this->advance_extended_copy(backreference_offset, best_match_size); + + } else { + throw logic_error("invalid best match"); + } +} + +void PRSCompressor::move_forward_data_to_reverse_log(size_t size) { + for (; size > 0; size--) { + this->reverse_log.push_back(this->forward_log.at(this->reverse_log.end_offset())); + if (this->progress_fn && ((this->reverse_log.end_offset() & 0xFFF) == 0)) { + this->progress_fn(CompressPhase::GENERATE_RESULT, this->reverse_log.end_offset(), this->input_bytes, this->output.size()); + } + } +} + +void PRSCompressor::advance_literal() { + this->write_control(true); + this->output.put_u8(this->forward_log.at(this->reverse_log.end_offset())); + this->move_forward_data_to_reverse_log(1); +} + +void PRSCompressor::advance_short_copy(ssize_t offset, size_t size) { + uint8_t encoded_size = size - 2; + this->write_control(false); + this->write_control(false); + this->write_control(encoded_size & 2); + this->write_control(encoded_size & 1); + this->output.put_u8(offset & 0xFF); + this->move_forward_data_to_reverse_log(size); +} + +void PRSCompressor::advance_long_copy(ssize_t offset, size_t size) { + this->write_control(false); + this->write_control(true); + uint16_t a = (offset << 3) | (size - 2); + this->output.put_u8(a & 0xFF); + this->output.put_u8(a >> 8); + this->move_forward_data_to_reverse_log(size); +} + +void PRSCompressor::advance_extended_copy(ssize_t offset, size_t size) { + this->write_control(false); + this->write_control(true); + uint16_t a = (offset << 3); + this->output.put_u8(a & 0xFF); + this->output.put_u8(a >> 8); + this->output.put_u8(size - 1); + this->move_forward_data_to_reverse_log(size); +} + +string& PRSCompressor::close() { + if (!this->closed) { + // Advance until all input is consumed + while (this->reverse_log.end_offset() < this->input_bytes) { + this->advance(); + } + // Write stop command + this->write_control(false); + this->write_control(true); + this->output.put_u8(0); + this->output.put_u8(0); + // Write remaining control bits + this->flush_control(); + this->closed = true; + } + return this->output.str(); +} + +void PRSCompressor::write_control(bool z) { + if (this->pending_control_bits & 0x0100) { + this->output.pput_u8( + this->control_byte_offset, this->pending_control_bits & 0xFF); + this->control_byte_offset = this->output.size(); + this->output.put_u8(0); + this->pending_control_bits = z ? 0x8080 : 0x8000; + } else { + this->pending_control_bits = + (this->pending_control_bits >> 1) | (z ? 0x8080 : 0x8000); + } +} + +void PRSCompressor::flush_control() { + if (this->pending_control_bits & 0xFF00) { + while (!(this->pending_control_bits & 0x0100)) { + this->pending_control_bits >>= 1; + } + this->output.pput_u8( + this->control_byte_offset, this->pending_control_bits & 0xFF); + } else { + if (this->control_byte_offset != this->output.size() - 1) { + throw logic_error("data written without control bits"); + } + this->output.str().resize(this->output.str().size() - 1); + } +} + +string prs_compress( + const void* vdata, + size_t size, + ssize_t compression_level, + ProgressCallback progress_fn) { + PRSCompressor prs(compression_level, progress_fn); + prs.add(vdata, size); + return std::move(prs.close()); +} + +string prs_compress( + const string& data, + ssize_t compression_level, + ProgressCallback progress_fn) { + return prs_compress(data.data(), data.size(), compression_level, progress_fn); +} + +string prs_compress_indexed( + const void* in_data_v, size_t in_size, ProgressCallback progress_fn) { + const uint8_t* in_data = reinterpret_cast(in_data_v); + + LZSSInterleavedWriter w; + WindowIndex<0x100, 5> w_short(in_data_v, in_size); + WindowIndex<0x1FFF, 9> w_long(in_data_v, in_size); + WindowIndex<0x1FFF, 0x100> w_extended(in_data_v, in_size); + + size_t last_progress_fn_call_offset = 0; + while (w_short.offset < in_size) { + if (progress_fn && ((last_progress_fn_call_offset & ~0xFFF) != (w_short.offset & ~0xFFF))) { + last_progress_fn_call_offset = w_short.offset; + progress_fn(CompressPhase::GENERATE_RESULT, w_short.offset, in_size, w.size()); + } + + auto m_short = w_short.get_best_match(); + auto m_long = w_long.get_best_match(); + auto m_extended = w_extended.get_best_match(); + + // Write the match that achieves the best ratio of output bytes to + // compressed bits used. To do this without floating-point math, we multiply + // the output byte count for each type of command by 468 / (command_bits), + // since 468 is the least common multiple of the number of bits for each + // command type. The command type with the highest score is the one we'll + // use, breaking ties by choosing the shorter command type. Note that the + // size of any copy type can be zero if no match was found; if no matches + // were found at all, then we can always write a literal. + size_t score_literal = 52; + size_t score_short = m_short.second * 39; + size_t score_long = m_long.second * 26; + size_t score_extended = m_extended.second * 18; + PRSPathNode::CommandType command_type = PRSPathNode::CommandType::NONE; + if (score_literal < score_short) { + if (score_short < score_long) { + if (score_long < score_extended) { + command_type = PRSPathNode::CommandType::EXTENDED_COPY; + } else { + command_type = PRSPathNode::CommandType::LONG_COPY; + } + } else { + if (score_short < score_extended) { + command_type = PRSPathNode::CommandType::EXTENDED_COPY; + } else { + command_type = PRSPathNode::CommandType::SHORT_COPY; + } + } + } else { + if (score_literal < score_long) { + if (score_long < score_extended) { + command_type = PRSPathNode::CommandType::EXTENDED_COPY; + } else { + command_type = PRSPathNode::CommandType::LONG_COPY; + } + } else { + if (score_literal < score_extended) { + command_type = PRSPathNode::CommandType::EXTENDED_COPY; + } else { + command_type = PRSPathNode::CommandType::LITERAL; + } + } + } + + size_t bytes_consumed = 0; + switch (command_type) { + case PRSPathNode::CommandType::LITERAL: + w.write_control(true); + w.write_data(in_data[w_short.offset]); + bytes_consumed = 1; + break; + case PRSPathNode::CommandType::SHORT_COPY: { + ssize_t backreference_offset = m_short.first - w_short.offset; + uint8_t encoded_size = m_short.second - 2; + w.write_control(false); + w.flush_if_ready(); + w.write_control(false); + w.flush_if_ready(); + w.write_control(encoded_size & 2); + w.flush_if_ready(); + w.write_control(encoded_size & 1); + w.write_data(backreference_offset & 0xFF); + bytes_consumed = m_short.second; + break; + } + case PRSPathNode::CommandType::LONG_COPY: { + ssize_t backreference_offset = m_long.first - w_long.offset; + w.write_control(false); + w.flush_if_ready(); + w.write_control(true); + uint16_t a = (backreference_offset << 3) | (m_long.second - 2); + w.write_data(a & 0xFF); + w.write_data(a >> 8); + bytes_consumed = m_long.second; + break; + } + case PRSPathNode::CommandType::EXTENDED_COPY: { + ssize_t backreference_offset = m_extended.first - w_extended.offset; + w.write_control(false); + w.flush_if_ready(); + w.write_control(true); + uint16_t a = (backreference_offset << 3); + w.write_data(a & 0xFF); + w.write_data(a >> 8); + w.write_data(m_extended.second - 1); + bytes_consumed = m_extended.second; + break; + } + case PRSPathNode::CommandType::NONE: + default: + throw logic_error("invalid command type"); + } + w.flush_if_ready(); + + if (bytes_consumed == 0) { + throw logic_error("no input data was consumed"); + } + + for (size_t z = 0; z < bytes_consumed; z++) { + w_short.advance(); + w_long.advance(); + w_extended.advance(); + } + } + + // Write stop command + w.write_control(false); + w.flush_if_ready(); + w.write_control(true); + w.write_data(0); + w.write_data(0); + + return std::move(w.close()); +} + +string prs_compress_indexed(const string& data, ProgressCallback progress_fn) { + return prs_compress_indexed(data.data(), data.size(), progress_fn); +} + +PRSDecompressResult prs_decompress_with_meta( + const void* data, size_t size, size_t max_output_size, bool allow_unterminated) { + // PRS is an LZ77-based compression algorithm. Compressed data is split into + // two streams: a control stream and a data stream. The control stream is read + // one bit at a time, and the data stream is read one byte at a time. The + // streams are interleaved such that the decompressor never has to move + // backward in the input stream - when the decompressor needs a control bit + // and there are no unused bits from the previous byte of the control stream, + // it reads a byte from the input and treats it as the next 8 control bits. + + // There are 3 distinct commands in PRS, labeled here with their control bits: + // 1 - Literal byte. The decompressor copies one byte from the input data + // stream to the output. + // 00 - Short backreference. The decompressor reads two control bits and adds + // 2 to this value to determine the number of bytes to copy, then reads + // one byte from the data stream to determine how far back in the output + // to copy from. This byte is treated as an 8-bit negative number - so + // 0xF7, for example, means to start copying data from 9 bytes before the + // end of the output. The range must start before the end of the output, + // but the end of the range may be beyond the end of the output. In this + // case, the bytes between the beginning of the range and original end of + // the output are simply repeated. + // 01 - Long backreference. The decompressor reads two bytes from the data and + // byteswaps the resulting 16-bit value (that is, the low byte is read + // first). The start offset (again, as a negative number) is the top 13 + // bits of this value; the size is the low 3 bits of this value, plus 2. + // If the size bits are all zero, an additional byte is read from the + // data stream and 1 is added to it to determine the backreference size + // (we call this an extended backreference). Therefore, the maximum + // backreference size is 256 bytes. + // Decompression ends when either there are no more input bytes to read, or + // when a long backreference is read with all zeroes in its offset field. The + // original implementation stops decompression successfully when any attempt + // to read from the input encounters the end of the stream, but newserv's + // implementation only allows this at the end of an opcode - if end-of-stream + // is encountered partway through an opcode, we throw instead, because it's + // likely the input has been truncated or is malformed in some way. + + StringWriter w; + StringReader r(data, size); + ControlStreamReader cr(r); + + while (!r.eof()) { + // Control 1 = literal byte + if (cr.read()) { + if (max_output_size && w.size() == max_output_size) { + if (allow_unterminated) { + return {std::move(w.str()), r.where()}; + } else { + throw runtime_error("maximum output size exceeded"); + } + } + w.put_u8(r.get_u8()); + + } else { + ssize_t offset; + size_t count; + + // Control 01 = long backreference + if (cr.read()) { + // The bits stored in the data stream are AAAAABBBCCCCCCCC, which we + // rearrange into offset = CCCCCCCCAAAAA and size = BBB. + uint16_t a = r.get_u8(); + a |= (r.get_u8() << 8); + offset = (a >> 3) | (~0x1FFF); + // If offset is zero, it's a stop opcode + if (offset == ~0x1FFF) { + break; + } + // If the size field is zero, it's an extended backreference (size comes + // from another byte in the data stream) + count = (a & 7) ? ((a & 7) + 2) : (r.get_u8() + 1); + + // Control 00 = short backreference + } else { + // Count comes from 2 bits in the control stream instead of from the + // data stream (and 2 is added). Importantly, the control stream bits + // are read first - this may involve reading another control stream + // byte, which happens before the offset is read from the data stream. + count = cr.read() << 1; + count = (count | cr.read()) + 2; + offset = r.get_u8() | (~0xFF); + } + + // Copy bytes from the referenced location in the output. Importantly, + // copy only one byte at a time, in order to support ranges that cover the + // current end of the output. + size_t read_offset = w.size() + offset; + if (read_offset >= w.size()) { + throw runtime_error("backreference offset beyond beginning of output"); + } + for (size_t z = 0; z < count; z++) { + if (max_output_size && w.size() == max_output_size) { + if (allow_unterminated) { + return {std::move(w.str()), r.where()}; + } else { + throw out_of_range("maximum output size exceeded"); + } + } + w.put_u8(w.str()[read_offset + z]); + } + } + } + + return {std::move(w.str()), r.where()}; +} + +PRSDecompressResult prs_decompress_with_meta(const string& data, size_t max_output_size, bool allow_unterminated) { + return prs_decompress_with_meta(data.data(), data.size(), max_output_size, allow_unterminated); +} + +string prs_decompress(const void* data, size_t size, size_t max_output_size, bool allow_unterminated) { + auto ret = prs_decompress_with_meta(data, size, max_output_size, allow_unterminated); + return std::move(ret.data); +} + +string prs_decompress(const string& data, size_t max_output_size, bool allow_unterminated) { + auto ret = prs_decompress_with_meta(data.data(), data.size(), max_output_size, allow_unterminated); + return std::move(ret.data); +} + +size_t prs_decompress_size(const void* data, size_t size, size_t max_output_size, bool allow_unterminated) { + size_t ret = 0; + StringReader r(data, size); + ControlStreamReader cr(r); + + while (!r.eof()) { + if (cr.read()) { + ret++; + r.get_u8(); + + } else { + ssize_t offset; + size_t count; + + if (cr.read()) { + uint16_t a = r.get_u8(); + a |= (r.get_u8() << 8); + offset = (a >> 3) | (~0x1FFF); + if (offset == ~0x1FFF) { + break; + } + count = (a & 7) ? ((a & 7) + 2) : (r.get_u8() + 1); + + } else { + count = cr.read() << 1; + count = (count | cr.read()) + 2; + offset = r.get_u8() | (~0xFF); + } + + size_t read_offset = ret + offset; + if (read_offset >= ret) { + throw runtime_error("backreference offset beyond beginning of output"); + } + ret += count; + } + + if (max_output_size && ret > max_output_size) { + if (allow_unterminated) { + return max_output_size; + } else { + throw out_of_range("maximum output size exceeded"); + } + } + } + + return ret; +} + +size_t prs_decompress_size(const string& data, size_t max_output_size, bool allow_unterminated) { + return prs_decompress_size(data.data(), data.size(), max_output_size, allow_unterminated); +} + +void prs_disassemble(FILE* stream, const void* data, size_t size) { + size_t output_bytes = 0; + StringReader r(data, size); + ControlStreamReader cr(r); + + while (!r.eof()) { + uint8_t buffered_bits = cr.buffered_bits(); + if (cr.read()) { + uint8_t literal_value = r.get_u8(); + fprintf(stream, "[%zX] %hhu> 1 %02hhX literal %02hhX\n", + output_bytes, buffered_bits, literal_value, literal_value); + output_bytes++; + + } else { + size_t count, read_offset; + if (cr.read()) { + uint8_t a_low = r.get_u8(); + uint8_t a_high = r.get_u8(); + uint16_t a = (a_high << 8) | a_low; + ssize_t offset = (a >> 3) | (~0x1FFF); + if (offset == ~0x1FFF) { + fprintf(stream, "[%zX] end\n", output_bytes); + break; + } + if (a & 7) { + count = (a & 7) + 2; + read_offset = output_bytes + offset; + fprintf(stream, "[%zX] %hhu> 01 %02hhX %02hhX long copy from %zd (offset=%zX) size=%zX\n", + output_bytes, buffered_bits, a_low, a_high, offset, read_offset, count); + } else { + uint8_t count_u8 = r.get_u8(); + count = count_u8 + 1; + read_offset = output_bytes + offset; + fprintf(stream, "[%zX] %hhu> 01 %02hhX %02hhX %02hhX extended copy from %zd (offset=%zX) size=%zX\n", + output_bytes, buffered_bits, a_low, a_high, count_u8, offset, read_offset, count); + } + + } else { + bool first_bit = cr.read(); + bool second_bit = cr.read(); + uint8_t offset_u8 = r.get_u8(); + count = ((first_bit ? 2 : 0) | (second_bit ? 1 : 0)) + 2; + ssize_t offset = offset_u8 | (~0xFF); + read_offset = output_bytes + offset; + fprintf(stream, "[%zX] %hhu> 00%c%c %02hhX short copy from %zd (offset=%zX) size=%zX\n", + output_bytes, buffered_bits, first_bit ? '1' : '0', second_bit ? '1' : '0', offset_u8, offset, read_offset, count); + } + + if (read_offset >= output_bytes) { + throw runtime_error("backreference offset beyond beginning of output"); + } + output_bytes += count; + } + } +} + +void prs_disassemble(FILE* stream, const std::string& data) { + return prs_disassemble(stream, data.data(), data.size()); +} + +// BC0 is a compression algorithm fairly similar to PRS, but with a simpler set +// of commands. Like PRS, there is a control stream, indicating when to copy a +// literal byte from the input and when to copy from a backreference; unlike +// PRS, there is only one type of backreference. Also, there is no stop opcode; +// the decompressor simply stops when there are no more input bytes to read. + +struct BC0PathNode { + uint16_t memo_offset = 0; + uint8_t max_copy_size = 0; + + // Pathfinding state + size_t from_offset = 0; + size_t bits_used = static_cast(-1); + + // Stream generation state + size_t to_offset = 0; +}; + +string bc0_compress_optimal( + const void* in_data_v, size_t in_size, ProgressCallback progress_fn) { + const uint8_t* in_data = reinterpret_cast(in_data_v); + + vector nodes; + nodes.resize(in_size + 1); + nodes[0].bits_used = 0; + + // Populate all possible backreferences + { + WindowIndex<0x1000, 0x12> window(in_data_v, in_size); + while (window.offset < in_size) { + if ((window.offset & 0xFFF) == 0 && progress_fn) { + progress_fn(CompressPhase::INDEX, window.offset, in_size, 0); + } + auto& node = nodes[window.offset]; + auto match = window.get_best_match(); + if (match.second >= 3) { + node.memo_offset = (match.first - 0x12) & 0xFFF; + node.max_copy_size = match.second; + } + window.advance(); + } + } + + // For each node, populate the literal value, and the best ways to get to the + // following nodes + for (size_t z = 0; z < in_size; z++) { + if ((z & 0xFFF) == 0 && progress_fn) { + progress_fn(CompressPhase::CONSTRUCT_PATHS, z, in_size, 0); + } + + auto& node = nodes[z]; + + // Literal: 1 control bit + 1 data byte + size_t bits_used = node.bits_used + 9; + { + auto& next_node = nodes[z + 1]; + if (next_node.bits_used > bits_used) { + next_node.from_offset = z; + next_node.bits_used = bits_used; + } + } + + // Backreference: 1 control bit + 2 data bytes + bits_used = node.bits_used + 17; + for (size_t x = 3; x <= node.max_copy_size; x++) { + auto& next_node = nodes[z + x]; + if (next_node.bits_used > bits_used) { + next_node.from_offset = z; + next_node.bits_used = bits_used; + } + } + } + + // Find the shortest path from the last node to the first node + size_t last_progress_fn_call = static_cast(-1); + for (size_t z = in_size; z > 0;) { + if ((z & ~0xFFF) != (last_progress_fn_call & ~0xFFF)) { + last_progress_fn_call = z; + if (progress_fn) { + progress_fn(CompressPhase::BACKTRACE_OPTIMAL_PATH, z, in_size, 0); + } + } + size_t from_offset = nodes[z].from_offset; + nodes[from_offset].to_offset = z; + z = from_offset; + } + + // Produce the BC0 command stream from the shortest path + LZSSInterleavedWriter w; + last_progress_fn_call = static_cast(-1); + for (size_t offset = 0; offset < in_size;) { + if ((offset & ~0xFFF) != (last_progress_fn_call & ~0xFFF)) { + last_progress_fn_call = offset; + if (progress_fn) { + progress_fn(CompressPhase::GENERATE_RESULT, offset, in_size, w.size()); + } + } + + const auto& node = nodes[offset]; + size_t copy_size = node.to_offset - offset; + if (copy_size >= 3 && copy_size <= 0x12) { + w.write_control(false); + w.write_data(node.memo_offset & 0xFF); + w.write_data(((node.memo_offset >> 4) & 0xF0) | (copy_size - 3)); + } else if (copy_size == 1) { + w.write_control(true); + w.write_data(in_data[offset]); + } + w.flush_if_ready(); + + offset = node.to_offset; + } + + return std::move(w.close()); +} + +string bc0_compress(const string& data, ProgressCallback progress_fn) { + return bc0_compress(data.data(), data.size(), progress_fn); +} + +string bc0_compress(const void* in_data_v, size_t in_size, ProgressCallback progress_fn) { + const uint8_t* in_data = reinterpret_cast(in_data_v); + + LZSSInterleavedWriter w; + WindowIndex<0x1000, 0x12> window(in_data_v, in_size); + + size_t last_progress_fn_call_offset = 0; + while (window.offset < in_size) { + if (progress_fn && ((last_progress_fn_call_offset & ~0xFFF) != (window.offset & ~0xFFF))) { + last_progress_fn_call_offset = window.offset; + progress_fn(CompressPhase::GENERATE_RESULT, window.offset, in_size, w.size()); + } + + auto match = window.get_best_match(); + + // Write a backreference if a match was found; otherwise, write a literal + if (match.second >= 3) { + w.write_control(false); + size_t memo_offset = match.first - 0x12; + w.write_data(memo_offset & 0xFF); + w.write_data(((memo_offset >> 4) & 0xF0) | (match.second - 3)); + } else { + w.write_control(true); + w.write_data(in_data[window.offset]); + match.second = 1; + } + w.flush_if_ready(); + + for (size_t z = 0; z < match.second; z++) { + window.advance(); + } + } + + return std::move(w.close()); +} + +string bc0_encode(const void* in_data_v, size_t in_size) { + const uint8_t* in_data = reinterpret_cast(in_data_v); + + LZSSInterleavedWriter w; + for (size_t z = 0; z < in_size; z++) { + w.write_control(true); + w.write_data(in_data[z]); + w.flush_if_ready(); + } + + return std::move(w.close()); +} + +// The BC0 decompression implementation in PSO GC is vulnerable to overflow +// attacks - there is no bounds checking on the output buffer. It is unlikely +// that this can be usefully exploited (e.g. for RCE) because the output pointer +// is loaded from memory before every byte is written, so we cannot change the +// output pointer to any arbitrary address. + +string bc0_decompress(const string& data) { + return bc0_decompress(data.data(), data.size()); +} + +string bc0_decompress(const void* data, size_t size) { + StringReader r(data, size); + StringWriter w; + + // Unlike PRS, BC0 uses a memo which "rolls over" every 0x1000 bytes. The + // boundaries of these "memo pages" are offset by -0x12 bytes for some reason, + // so the first output byte corresponds to position 0xFEE on the first memo + // page. Backreferences refer to offsets based on the start of memo pages; for + // example, if the current output offset is 0x1234, a backreference with + // offset 0x123 refers to the byte that was written at offset 0x1111 (because + // that byte is at offset 0x111 in the memo, because the memo rolls over every + // 0x1000 bytes and the first memo byte was 0x12 bytes before the beginning of + // the next page). The memo is initially zeroed from 0 to 0xFEE; it seems PSO + // GC doesn't initialize the last 0x12 bytes of the first memo page. + parray memo; + uint16_t memo_offset = 0x0FEE; + + // The low byte of this value contains the control stream data; the high bits + // specify which low bits are valid. When the last 1 is shifted out of the + // high byte, we need to read a new control stream byte to get the next set of + // control bits. + uint16_t control_stream_bits = 0x0000; + + while (!r.eof()) { + // Read control stream bits if needed + control_stream_bits >>= 1; + if ((control_stream_bits & 0x100) == 0) { + control_stream_bits = 0xFF00 | r.get_u8(); + if (r.eof()) { + break; + } + } + + // Control bit 0 means to perform a backreference copy. The offset and + // size are stored in two bytes in the input stream, laid out as follows: + // a1 = 0bBBBBBBBB + // a2 = 0bAAAACCCC + // The offset is the concatenation of bits AAAABBBBBBBB, which refers to a + // position in the memo; the number of bytes to copy is (CCCC + 3). The + // decompressor copies that many bytes from that offset in the memo, and + // writes them to the output and to the current position in the memo. + if ((control_stream_bits & 1) == 0) { + uint8_t a1 = r.get_u8(); + if (r.eof()) { + break; + } + uint8_t a2 = r.get_u8(); + size_t count = (a2 & 0x0F) + 3; + size_t backreference_offset = a1 | ((a2 << 4) & 0xF00); + for (size_t z = 0; z < count; z++) { + uint8_t v = memo[(backreference_offset + z) & 0x0FFF]; + w.put_u8(v); + memo[memo_offset] = v; + memo_offset = (memo_offset + 1) & 0x0FFF; + } + + // Control bit 1 means to write a byte directly from the input to the + // output. As above, the byte is also written to the memo. + } else { + uint8_t v = r.get_u8(); + w.put_u8(v); + memo[memo_offset] = v; + memo_offset = (memo_offset + 1) & 0x0FFF; + } + } + + return std::move(w.str()); +} + +void bc0_disassemble(FILE* stream, const string& data) { + bc0_disassemble(stream, data.data(), data.size()); +} + +void bc0_disassemble(FILE* stream, const void* data, size_t size) { + StringReader r(data, size); + uint16_t control_stream_bits = 0x0000; + + size_t output_bytes = 0; + while (!r.eof()) { + // size_t opcode_offset = r.where(); + + control_stream_bits >>= 1; + if ((control_stream_bits & 0x100) == 0) { + control_stream_bits = 0xFF00 | r.get_u8(); + if (r.eof()) { + break; + } + } + + if ((control_stream_bits & 1) == 0) { + uint8_t a1 = r.get_u8(); + if (r.eof()) { + break; + } + (void)a1; + uint8_t a2 = r.get_u8(); + size_t count = (a2 & 0x0F) + 3; + // size_t backreference_offset = a1 | ((a2 << 4) & 0xF00); + fprintf(stream, "[%zX] backreference %02zX\n", output_bytes, count); + output_bytes += count; + + } else { + fprintf(stream, "[%zX] literal %02hhX\n", output_bytes, r.get_u8()); + output_bytes++; + } + } +} diff --git a/src/Compression.hh b/src/Compression.hh index efbd46a9..55abe11a 100644 --- a/src/Compression.hh +++ b/src/Compression.hh @@ -1,226 +1,226 @@ -#pragma once - -#include - -#include -#include -#include -#include -#include - -#include "Text.hh" - -enum class CompressPhase { - INDEX = 0, - CONSTRUCT_PATHS, - BACKTRACE_OPTIMAL_PATH, - GENERATE_RESULT, -}; - -template <> -const char* name_for_enum(CompressPhase v); - -typedef std::function ProgressCallback; - -//////////////////////////////////////////////////////////////////////////////// -// PRS compression -//////////////////////////////////////////////////////////////////////////////// - -// Use this class if you need to compress from multiple input buffers, or need -// to compress multiple chunks and don't want to copy their contents -// unnecessarily. (For most common use cases, use prs_compress, below, instead.) -// To use this class, instantiate it, then call .add() one or more times, then -// call .close() and use the returned string as the compressed result. -class PRSCompressor { -public: - // compression_level specifies how aggressively to search for alternate paths: - // -1: Don't perform any compression at all, but produce output that can be - // understood by prs_decompress. The output will be about 9/8 the size - // of the input. - // 0: Greedily search for the longest backreference at every point. Don't - // consider any alternate paths. Generally offers a good balance between - // speed and output size. - // 1: Consider two paths at each point when a backreference is found: using - // the backreference or ignoring it. - // 2+: Consider further chains of paths at each point. Using values 2 or - // greater for compression_level generally yields diminishing returns. - explicit PRSCompressor(ssize_t compression_level = 0, ProgressCallback progress_fn = nullptr); - ~PRSCompressor() = default; - - // Adds more input data to be compressed, which logically comes after all - // previous data provided via add() calls. Cannot be called after close() is - // called. - void add(const void* data, size_t size); - void add(const std::string& data); - - // Ends compression and returns the complete compressed result. It's OK to - // std::move() from the returned string reference. - std::string& close(); - - // Returns the total number of bytes passed to add() calls so far. - inline size_t input_size() const { - return this->input_bytes; - } - -private: - template - struct WrappedLog { - parray data; - - WrappedLog() : data(0) {} - ~WrappedLog() = default; - - inline uint8_t at(size_t offset) const { - return this->data[offset % this->data.size()]; - } - inline uint8_t& at(size_t offset) { - return this->data[offset % this->data.size()]; - } - }; - - template - struct IndexedLog : WrappedLog { - size_t offset; - size_t size; - std::array, 0x100> index; - - IndexedLog() - : WrappedLog(), - offset(0), - size(0) {} - ~IndexedLog() = default; - - inline size_t end_offset() const { - return this->offset + this->size; - } - - void push_back(uint8_t v) { - if (this->size == Size) { - this->pop_front(); - } - size_t write_offset = this->offset + this->size; - this->at(write_offset) = v; - this->index[v].push_back(write_offset); - this->size++; - } - uint8_t pop_back() { - if (!this->size) { - throw std::logic_error("pop_back called on empty IndexedLog"); - } - this->size--; - size_t offset = this->offset + this->size; - uint8_t v = this->at(offset); - this->index[v].pop_back(); - return v; - } - uint8_t pop_front() { - uint8_t v = this->at(this->offset); - this->index[v].pop_front(); - this->offset++; - this->size--; - return v; - } - const std::deque& find(uint8_t v) { - return this->index[v]; - } - }; - - void add_byte(uint8_t v); - void advance(); - void move_forward_data_to_reverse_log(size_t size); - void advance_literal(); - void advance_short_copy(ssize_t offset, size_t size); - void advance_long_copy(ssize_t offset, size_t size); - void advance_extended_copy(ssize_t offset, size_t size); - void write_control(bool z); - void flush_control(); - - ssize_t compression_level; - ProgressCallback progress_fn; - bool closed; - - size_t control_byte_offset; - uint16_t pending_control_bits; - - size_t input_bytes; - WrappedLog<0x101> forward_log; - IndexedLog<0x2000> reverse_log; - - StringWriter output; -}; - -// These functions use PRSCompressor to compress a buffer of data. This is -// essentially a shortcut for constructing a PRSCompressor, calling .add() on -// it once, then calling .close(). -std::string prs_compress( - const void* vdata, - size_t size, - ssize_t compression_level = 0, - ProgressCallback progress_fn = nullptr); -std::string prs_compress( - const std::string& data, - ssize_t compression_level = 0, - ProgressCallback progress_fn = nullptr); - -// A faster form of prs_compress that doesn't have a tunable compression level. -std::string prs_compress_indexed( - const void* vdata, - size_t size, - ProgressCallback progress_fn = nullptr); -std::string prs_compress_indexed( - const std::string& data, - ProgressCallback progress_fn = nullptr); - -// Compresses data using PRS to the smallest possible output size. This function -// is slow, but produces results significantly smaller than even Sega's original -// compressor. -std::string prs_compress_optimal(const void* vdata, size_t size, ProgressCallback progress_fn = nullptr); -std::string prs_compress_optimal(const std::string& data, ProgressCallback progress_fn = nullptr); - -// Compresses data using PRS to the LARGEST possible output size. There is no -// practical use for this function except for amusement. -std::string prs_compress_pessimal(const void* vdata, size_t size); - -// Decompresses PRS-compressed data. -struct PRSDecompressResult { - std::string data; - size_t input_bytes_used; -}; -PRSDecompressResult prs_decompress_with_meta(const void* data, size_t size, size_t max_output_size = 0, bool allow_unterminated = false); -PRSDecompressResult prs_decompress_with_meta(const std::string& data, size_t max_output_size = 0, bool allow_unterminated = false); -std::string prs_decompress(const void* data, size_t size, size_t max_output_size = 0, bool allow_unterminated = false); -std::string prs_decompress(const std::string& data, size_t max_output_size = 0, bool allow_unterminated = false); - -// Returns the decompressed size of PRS-compressed data, without actually -// decompressing it. -size_t prs_decompress_size(const void* data, size_t size, size_t max_output_size = 0, bool allow_unterminated = false); -size_t prs_decompress_size(const std::string& data, size_t max_output_size = 0, bool allow_unterminated = false); - -// Prints the command stream from a PRS-compressed buffer. -void prs_disassemble(FILE* stream, const void* data, size_t size); -void prs_disassemble(FILE* stream, const std::string& data); - -//////////////////////////////////////////////////////////////////////////////// -// BC0 compression -//////////////////////////////////////////////////////////////////////////////// - -// Compresses data using the BC0 algorithm. Like with PRS, the optimal variant -// is slow, but produces the smallest possible output. -std::string bc0_compress_optimal( - const void* in_data_v, - size_t in_size, - ProgressCallback progress_fn = nullptr); -std::string bc0_compress(const std::string& data, ProgressCallback progress_fn = nullptr); -std::string bc0_compress(const void* in_data_v, size_t in_size, ProgressCallback progress_fn = nullptr); - -// Encodes data in a BC0-compatible format without compression (similar to using -// compression_level=-1 with prs_compress). -std::string bc0_encode(const void* in_data_v, size_t in_size); - -// Decompresses BC0-compressed data. -std::string bc0_decompress(const std::string& data); -std::string bc0_decompress(const void* data, size_t size); - -// Prints the command stream from a BC0-compressed buffer. -void bc0_disassemble(FILE* stream, const std::string& data); -void bc0_disassemble(FILE* stream, const void* data, size_t size); +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include "Text.hh" + +enum class CompressPhase { + INDEX = 0, + CONSTRUCT_PATHS, + BACKTRACE_OPTIMAL_PATH, + GENERATE_RESULT, +}; + +template <> +const char* name_for_enum(CompressPhase v); + +typedef std::function ProgressCallback; + +//////////////////////////////////////////////////////////////////////////////// +// PRS compression +//////////////////////////////////////////////////////////////////////////////// + +// Use this class if you need to compress from multiple input buffers, or need +// to compress multiple chunks and don't want to copy their contents +// unnecessarily. (For most common use cases, use prs_compress, below, instead.) +// To use this class, instantiate it, then call .add() one or more times, then +// call .close() and use the returned string as the compressed result. +class PRSCompressor { +public: + // compression_level specifies how aggressively to search for alternate paths: + // -1: Don't perform any compression at all, but produce output that can be + // understood by prs_decompress. The output will be about 9/8 the size + // of the input. + // 0: Greedily search for the longest backreference at every point. Don't + // consider any alternate paths. Generally offers a good balance between + // speed and output size. + // 1: Consider two paths at each point when a backreference is found: using + // the backreference or ignoring it. + // 2+: Consider further chains of paths at each point. Using values 2 or + // greater for compression_level generally yields diminishing returns. + explicit PRSCompressor(ssize_t compression_level = 0, ProgressCallback progress_fn = nullptr); + ~PRSCompressor() = default; + + // Adds more input data to be compressed, which logically comes after all + // previous data provided via add() calls. Cannot be called after close() is + // called. + void add(const void* data, size_t size); + void add(const std::string& data); + + // Ends compression and returns the complete compressed result. It's OK to + // std::move() from the returned string reference. + std::string& close(); + + // Returns the total number of bytes passed to add() calls so far. + inline size_t input_size() const { + return this->input_bytes; + } + +private: + template + struct WrappedLog { + parray data; + + WrappedLog() : data(0) {} + ~WrappedLog() = default; + + inline uint8_t at(size_t offset) const { + return this->data[offset % this->data.size()]; + } + inline uint8_t& at(size_t offset) { + return this->data[offset % this->data.size()]; + } + }; + + template + struct IndexedLog : WrappedLog { + size_t offset; + size_t size; + std::array, 0x100> index; + + IndexedLog() + : WrappedLog(), + offset(0), + size(0) {} + ~IndexedLog() = default; + + inline size_t end_offset() const { + return this->offset + this->size; + } + + void push_back(uint8_t v) { + if (this->size == Size) { + this->pop_front(); + } + size_t write_offset = this->offset + this->size; + this->at(write_offset) = v; + this->index[v].push_back(write_offset); + this->size++; + } + uint8_t pop_back() { + if (!this->size) { + throw std::logic_error("pop_back called on empty IndexedLog"); + } + this->size--; + size_t offset = this->offset + this->size; + uint8_t v = this->at(offset); + this->index[v].pop_back(); + return v; + } + uint8_t pop_front() { + uint8_t v = this->at(this->offset); + this->index[v].pop_front(); + this->offset++; + this->size--; + return v; + } + const std::deque& find(uint8_t v) { + return this->index[v]; + } + }; + + void add_byte(uint8_t v); + void advance(); + void move_forward_data_to_reverse_log(size_t size); + void advance_literal(); + void advance_short_copy(ssize_t offset, size_t size); + void advance_long_copy(ssize_t offset, size_t size); + void advance_extended_copy(ssize_t offset, size_t size); + void write_control(bool z); + void flush_control(); + + ssize_t compression_level; + ProgressCallback progress_fn; + bool closed; + + size_t control_byte_offset; + uint16_t pending_control_bits; + + size_t input_bytes; + WrappedLog<0x101> forward_log; + IndexedLog<0x2000> reverse_log; + + StringWriter output; +}; + +// These functions use PRSCompressor to compress a buffer of data. This is +// essentially a shortcut for constructing a PRSCompressor, calling .add() on +// it once, then calling .close(). +std::string prs_compress( + const void* vdata, + size_t size, + ssize_t compression_level = 0, + ProgressCallback progress_fn = nullptr); +std::string prs_compress( + const std::string& data, + ssize_t compression_level = 0, + ProgressCallback progress_fn = nullptr); + +// A faster form of prs_compress that doesn't have a tunable compression level. +std::string prs_compress_indexed( + const void* vdata, + size_t size, + ProgressCallback progress_fn = nullptr); +std::string prs_compress_indexed( + const std::string& data, + ProgressCallback progress_fn = nullptr); + +// Compresses data using PRS to the smallest possible output size. This function +// is slow, but produces results significantly smaller than even Sega's original +// compressor. +std::string prs_compress_optimal(const void* vdata, size_t size, ProgressCallback progress_fn = nullptr); +std::string prs_compress_optimal(const std::string& data, ProgressCallback progress_fn = nullptr); + +// Compresses data using PRS to the LARGEST possible output size. There is no +// practical use for this function except for amusement. +std::string prs_compress_pessimal(const void* vdata, size_t size); + +// Decompresses PRS-compressed data. +struct PRSDecompressResult { + std::string data; + size_t input_bytes_used; +}; +PRSDecompressResult prs_decompress_with_meta(const void* data, size_t size, size_t max_output_size = 0, bool allow_unterminated = false); +PRSDecompressResult prs_decompress_with_meta(const std::string& data, size_t max_output_size = 0, bool allow_unterminated = false); +std::string prs_decompress(const void* data, size_t size, size_t max_output_size = 0, bool allow_unterminated = false); +std::string prs_decompress(const std::string& data, size_t max_output_size = 0, bool allow_unterminated = false); + +// Returns the decompressed size of PRS-compressed data, without actually +// decompressing it. +size_t prs_decompress_size(const void* data, size_t size, size_t max_output_size = 0, bool allow_unterminated = false); +size_t prs_decompress_size(const std::string& data, size_t max_output_size = 0, bool allow_unterminated = false); + +// Prints the command stream from a PRS-compressed buffer. +void prs_disassemble(FILE* stream, const void* data, size_t size); +void prs_disassemble(FILE* stream, const std::string& data); + +//////////////////////////////////////////////////////////////////////////////// +// BC0 compression +//////////////////////////////////////////////////////////////////////////////// + +// Compresses data using the BC0 algorithm. Like with PRS, the optimal variant +// is slow, but produces the smallest possible output. +std::string bc0_compress_optimal( + const void* in_data_v, + size_t in_size, + ProgressCallback progress_fn = nullptr); +std::string bc0_compress(const std::string& data, ProgressCallback progress_fn = nullptr); +std::string bc0_compress(const void* in_data_v, size_t in_size, ProgressCallback progress_fn = nullptr); + +// Encodes data in a BC0-compatible format without compression (similar to using +// compression_level=-1 with prs_compress). +std::string bc0_encode(const void* in_data_v, size_t in_size); + +// Decompresses BC0-compressed data. +std::string bc0_decompress(const std::string& data); +std::string bc0_decompress(const void* data, size_t size); + +// Prints the command stream from a BC0-compressed buffer. +void bc0_disassemble(FILE* stream, const std::string& data); +void bc0_disassemble(FILE* stream, const void* data, size_t size); diff --git a/src/DNSServer.cc b/src/DNSServer.cc index c50f6bd1..304bbdd0 100644 --- a/src/DNSServer.cc +++ b/src/DNSServer.cc @@ -1,121 +1,121 @@ -#include "DNSServer.hh" - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "Loggers.hh" -#include "NetworkAddresses.hh" - -using namespace std; - -DNSServer::DNSServer( - shared_ptr base, - uint32_t local_connect_address, - uint32_t external_connect_address, - shared_ptr banned_ipv4_ranges) - : base(base), - local_connect_address(local_connect_address), - external_connect_address(external_connect_address), - banned_ipv4_ranges(banned_ipv4_ranges) {} - -DNSServer::~DNSServer() { - for (const auto& it : this->fd_to_receive_event) { - close(it.first); - } -} - -void DNSServer::listen(const std::string& socket_path) { - this->add_socket(::listen(socket_path, 0, 0)); -} - -void DNSServer::listen(const std::string& addr, int port) { - this->add_socket(::listen(addr, port, 0)); -} - -void DNSServer::listen(int port) { - this->add_socket(::listen("", port, 0)); -} - -void DNSServer::add_socket(int fd) { - unique_ptr e( - event_new(this->base.get(), fd, EV_READ | EV_PERSIST, &DNSServer::dispatch_on_receive_message, this), - event_free); - event_add(e.get(), nullptr); - this->fd_to_receive_event.emplace(fd, std::move(e)); -} - -void DNSServer::dispatch_on_receive_message(evutil_socket_t fd, - short events, void* ctx) { - reinterpret_cast(ctx)->on_receive_message(fd, events); -} - -string DNSServer::response_for_query(const void* vdata, size_t size, uint32_t resolved_address) { - if (size < 0x0C) { - throw invalid_argument("query too small"); - } - - const char* data = reinterpret_cast(vdata); - size_t name_len = strlen(&data[12]) + 1; - - be_uint32_t be_resolved_address = resolved_address; - - string response; - response.append(data, 2); - response.append("\x81\x80\x00\x01\x00\x01\x00\x00\x00\x00", 10); - response.append(&data[12], name_len); - response.append("\x00\x01\x00\x01\xC0\x0C\x00\x01\x00\x01\x00\x00\x00\x3C\x00\x04", 16); - response.append(reinterpret_cast(&be_resolved_address), 4); - return response; -} - -string DNSServer::response_for_query( - const string& query, uint32_t resolved_address) { - return DNSServer::response_for_query(query.data(), query.size(), resolved_address); -} - -void DNSServer::on_receive_message(int fd, short) { - for (;;) { - struct sockaddr_storage remote; - socklen_t remote_size = sizeof(sockaddr_in); - memset(&remote, 0, remote_size); - - string input(2048, 0); - ssize_t bytes = recvfrom(fd, const_cast(input.data()), input.size(), - 0, reinterpret_cast(&remote), &remote_size); - - if (bytes < 0) { - if (errno != EAGAIN) { - dns_server_log.error("input error %d", errno); - throw runtime_error("cannot read from udp socket"); - } - break; - - } else if (bytes == 0) { - break; - - } else if (bytes < 0x0C) { - dns_server_log.warning("input query too small"); - print_data(stderr, input.data(), bytes); - - } else if (!this->banned_ipv4_ranges->check(remote)) { - input.resize(bytes); - const sockaddr_in* remote_sin = reinterpret_cast(&remote); - uint32_t remote_address = ntohl(remote_sin->sin_addr.s_addr); - uint32_t connect_address = is_local_address(remote_address) - ? this->local_connect_address - : this->external_connect_address; - string response = this->response_for_query(input, connect_address); - sendto(fd, response.data(), response.size(), 0, - reinterpret_cast(&remote), remote_size); - } - } -} +#include "DNSServer.hh" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "Loggers.hh" +#include "NetworkAddresses.hh" + +using namespace std; + +DNSServer::DNSServer( + shared_ptr base, + uint32_t local_connect_address, + uint32_t external_connect_address, + shared_ptr banned_ipv4_ranges) + : base(base), + local_connect_address(local_connect_address), + external_connect_address(external_connect_address), + banned_ipv4_ranges(banned_ipv4_ranges) {} + +DNSServer::~DNSServer() { + for (const auto& it : this->fd_to_receive_event) { + close(it.first); + } +} + +void DNSServer::listen(const std::string& socket_path) { + this->add_socket(::listen(socket_path, 0, 0)); +} + +void DNSServer::listen(const std::string& addr, int port) { + this->add_socket(::listen(addr, port, 0)); +} + +void DNSServer::listen(int port) { + this->add_socket(::listen("", port, 0)); +} + +void DNSServer::add_socket(int fd) { + unique_ptr e( + event_new(this->base.get(), fd, EV_READ | EV_PERSIST, &DNSServer::dispatch_on_receive_message, this), + event_free); + event_add(e.get(), nullptr); + this->fd_to_receive_event.emplace(fd, std::move(e)); +} + +void DNSServer::dispatch_on_receive_message(evutil_socket_t fd, + short events, void* ctx) { + reinterpret_cast(ctx)->on_receive_message(fd, events); +} + +string DNSServer::response_for_query(const void* vdata, size_t size, uint32_t resolved_address) { + if (size < 0x0C) { + throw invalid_argument("query too small"); + } + + const char* data = reinterpret_cast(vdata); + size_t name_len = strlen(&data[12]) + 1; + + be_uint32_t be_resolved_address = resolved_address; + + string response; + response.append(data, 2); + response.append("\x81\x80\x00\x01\x00\x01\x00\x00\x00\x00", 10); + response.append(&data[12], name_len); + response.append("\x00\x01\x00\x01\xC0\x0C\x00\x01\x00\x01\x00\x00\x00\x3C\x00\x04", 16); + response.append(reinterpret_cast(&be_resolved_address), 4); + return response; +} + +string DNSServer::response_for_query( + const string& query, uint32_t resolved_address) { + return DNSServer::response_for_query(query.data(), query.size(), resolved_address); +} + +void DNSServer::on_receive_message(int fd, short) { + for (;;) { + struct sockaddr_storage remote; + socklen_t remote_size = sizeof(sockaddr_in); + memset(&remote, 0, remote_size); + + string input(2048, 0); + ssize_t bytes = recvfrom(fd, const_cast(input.data()), input.size(), + 0, reinterpret_cast(&remote), &remote_size); + + if (bytes < 0) { + if (errno != EAGAIN) { + dns_server_log.error("input error %d", errno); + throw runtime_error("cannot read from udp socket"); + } + break; + + } else if (bytes == 0) { + break; + + } else if (bytes < 0x0C) { + dns_server_log.warning("input query too small"); + print_data(stderr, input.data(), bytes); + + } else if (!this->banned_ipv4_ranges->check(remote)) { + input.resize(bytes); + const sockaddr_in* remote_sin = reinterpret_cast(&remote); + uint32_t remote_address = ntohl(remote_sin->sin_addr.s_addr); + uint32_t connect_address = is_local_address(remote_address) + ? this->local_connect_address + : this->external_connect_address; + string response = this->response_for_query(input, connect_address); + sendto(fd, response.data(), response.size(), 0, + reinterpret_cast(&remote), remote_size); + } + } +} diff --git a/src/DNSServer.hh b/src/DNSServer.hh index 7b7a0530..8563a3ec 100644 --- a/src/DNSServer.hh +++ b/src/DNSServer.hh @@ -1,44 +1,44 @@ -#pragma once - -#include - -#include -#include -#include -#include - -#include "IPV4RangeSet.hh" - -class DNSServer { -public: - DNSServer( - std::shared_ptr base, - uint32_t local_connect_address, - uint32_t external_connect_address, - std::shared_ptr banned_ipv4_ranges); - DNSServer(const DNSServer&) = delete; - DNSServer(DNSServer&&) = delete; - virtual ~DNSServer(); - - inline void set_banned_ipv4_ranges(std::shared_ptr banned_ipv4_ranges) { - this->banned_ipv4_ranges = banned_ipv4_ranges; - } - - void listen(const std::string& socket_path); - void listen(const std::string& addr, int port); - void listen(int port); - void add_socket(int fd); - - static std::string response_for_query(const void* vdata, size_t size, uint32_t resolved_address); - static std::string response_for_query(const std::string& query, uint32_t resolved_address); - -private: - std::shared_ptr base; - std::unordered_map> fd_to_receive_event; - uint32_t local_connect_address; - uint32_t external_connect_address; - std::shared_ptr banned_ipv4_ranges; - - static void dispatch_on_receive_message(evutil_socket_t fd, short events, void* ctx); - void on_receive_message(int fd, short event); -}; +#pragma once + +#include + +#include +#include +#include +#include + +#include "IPV4RangeSet.hh" + +class DNSServer { +public: + DNSServer( + std::shared_ptr base, + uint32_t local_connect_address, + uint32_t external_connect_address, + std::shared_ptr banned_ipv4_ranges); + DNSServer(const DNSServer&) = delete; + DNSServer(DNSServer&&) = delete; + virtual ~DNSServer(); + + inline void set_banned_ipv4_ranges(std::shared_ptr banned_ipv4_ranges) { + this->banned_ipv4_ranges = banned_ipv4_ranges; + } + + void listen(const std::string& socket_path); + void listen(const std::string& addr, int port); + void listen(int port); + void add_socket(int fd); + + static std::string response_for_query(const void* vdata, size_t size, uint32_t resolved_address); + static std::string response_for_query(const std::string& query, uint32_t resolved_address); + +private: + std::shared_ptr base; + std::unordered_map> fd_to_receive_event; + uint32_t local_connect_address; + uint32_t external_connect_address; + std::shared_ptr banned_ipv4_ranges; + + static void dispatch_on_receive_message(evutil_socket_t fd, short events, void* ctx); + void on_receive_message(int fd, short event); +}; diff --git a/src/EnemyType.cc b/src/EnemyType.cc index e0777678..d73d2c52 100644 --- a/src/EnemyType.cc +++ b/src/EnemyType.cc @@ -1,1120 +1,1120 @@ -#include "BattleParamsIndex.hh" - -#include -#include - -#include "Loggers.hh" -#include "PSOEncryption.hh" -#include "StaticGameData.hh" - -using namespace std; - -template <> -const char* name_for_enum(EnemyType type) { - switch (type) { - case EnemyType::UNKNOWN: - return "UNKNOWN"; - case EnemyType::NONE: - return "NONE"; - case EnemyType::NON_ENEMY_NPC: - return "NON_ENEMY_NPC"; - case EnemyType::AL_RAPPY: - return "AL_RAPPY"; - case EnemyType::ASTARK: - return "ASTARK"; - case EnemyType::BA_BOOTA: - return "BA_BOOTA"; - case EnemyType::BARBA_RAY: - return "BARBA_RAY"; - case EnemyType::BARBAROUS_WOLF: - return "BARBAROUS_WOLF"; - case EnemyType::BEE_L: - return "BEE_L"; - case EnemyType::BEE_R: - return "BEE_R"; - case EnemyType::BOOMA: - return "BOOMA"; - case EnemyType::BOOTA: - return "BOOTA"; - case EnemyType::BULCLAW: - return "BULCLAW"; - case EnemyType::BULK: - return "BULK"; - case EnemyType::CANADINE: - return "CANADINE"; - case EnemyType::CANADINE_GROUP: - return "CANADINE_GROUP"; - case EnemyType::CANANE: - return "CANANE"; - case EnemyType::CHAOS_BRINGER: - return "CHAOS_BRINGER"; - case EnemyType::CHAOS_SORCERER: - return "CHAOS_SORCERER"; - case EnemyType::CLAW: - return "CLAW"; - case EnemyType::DARK_BELRA: - return "DARK_BELRA"; - case EnemyType::DARK_FALZ_1: - return "DARK_FALZ_1"; - case EnemyType::DARK_FALZ_2: - return "DARK_FALZ_2"; - case EnemyType::DARK_FALZ_3: - return "DARK_FALZ_3"; - case EnemyType::DARK_GUNNER: - return "DARK_GUNNER"; - case EnemyType::DARVANT: - return "DARVANT"; - case EnemyType::DARVANT_ULTIMATE: - return "DARVANT_ULTIMATE"; - case EnemyType::DE_ROL_LE: - return "DE_ROL_LE"; - case EnemyType::DE_ROL_LE_BODY: - return "DE_ROL_LE_BODY"; - case EnemyType::DE_ROL_LE_MINE: - return "DE_ROL_LE_MINE"; - case EnemyType::DEATH_GUNNER: - return "DEATH_GUNNER"; - case EnemyType::DEL_LILY: - return "DEL_LILY"; - case EnemyType::DEL_RAPPY: - return "DEL_RAPPY"; - case EnemyType::DEL_RAPPY_ALT: - return "DEL_RAPPY_ALT"; - case EnemyType::DELBITER: - return "DELBITER"; - case EnemyType::DELDEPTH: - return "DELDEPTH"; - case EnemyType::DELSABER: - return "DELSABER"; - case EnemyType::DIMENIAN: - return "DIMENIAN"; - case EnemyType::DOLMDARL: - return "DOLMDARL"; - case EnemyType::DOLMOLM: - return "DOLMOLM"; - case EnemyType::DORPHON: - return "DORPHON"; - case EnemyType::DORPHON_ECLAIR: - return "DORPHON_ECLAIR"; - case EnemyType::DRAGON: - return "DRAGON"; - case EnemyType::DUBCHIC: - return "DUBCHIC"; - case EnemyType::DUBWITCH: - return "DUBWITCH"; - case EnemyType::EGG_RAPPY: - return "EGG_RAPPY"; - case EnemyType::EPSIGUARD: - return "EPSIGUARD"; - case EnemyType::EPSILON: - return "EPSILON"; - case EnemyType::EVIL_SHARK: - return "EVIL_SHARK"; - case EnemyType::GAEL: - return "GAEL"; - case EnemyType::GAL_GRYPHON: - return "GAL_GRYPHON"; - case EnemyType::GARANZ: - return "GARANZ"; - case EnemyType::GEE: - return "GEE"; - case EnemyType::GI_GUE: - return "GI_GUE"; - case EnemyType::GIBBLES: - return "GIBBLES"; - case EnemyType::GIGOBOOMA: - return "GIGOBOOMA"; - case EnemyType::GILLCHIC: - return "GILLCHIC"; - case EnemyType::GIRTABLULU: - return "GIRTABLULU"; - case EnemyType::GOBOOMA: - return "GOBOOMA"; - case EnemyType::GOL_DRAGON: - return "GOL_DRAGON"; - case EnemyType::GORAN: - return "GORAN"; - case EnemyType::GORAN_DETONATOR: - return "GORAN_DETONATOR"; - case EnemyType::GRASS_ASSASSIN: - return "GRASS_ASSASSIN"; - case EnemyType::GUIL_SHARK: - return "GUIL_SHARK"; - case EnemyType::HALLO_RAPPY: - return "HALLO_RAPPY"; - case EnemyType::HIDOOM: - return "HIDOOM"; - case EnemyType::HILDEBEAR: - return "HILDEBEAR"; - case EnemyType::HILDEBLUE: - return "HILDEBLUE"; - case EnemyType::ILL_GILL: - return "ILL_GILL"; - case EnemyType::KONDRIEU: - return "KONDRIEU"; - case EnemyType::LA_DIMENIAN: - return "LA_DIMENIAN"; - case EnemyType::LOVE_RAPPY: - return "LOVE_RAPPY"; - case EnemyType::MERICAROL: - return "MERICAROL"; - case EnemyType::MERICUS: - return "MERICUS"; - case EnemyType::MERIKLE: - return "MERIKLE"; - case EnemyType::MERILLIA: - return "MERILLIA"; - case EnemyType::MERILTAS: - return "MERILTAS"; - case EnemyType::MERISSA_A: - return "MERISSA_A"; - case EnemyType::MERISSA_AA: - return "MERISSA_AA"; - case EnemyType::MIGIUM: - return "MIGIUM"; - case EnemyType::MONEST: - return "MONEST"; - case EnemyType::MORFOS: - return "MORFOS"; - case EnemyType::MOTHMANT: - return "MOTHMANT"; - case EnemyType::NANO_DRAGON: - return "NANO_DRAGON"; - case EnemyType::NAR_LILY: - return "NAR_LILY"; - case EnemyType::OLGA_FLOW_1: - return "OLGA_FLOW_1"; - case EnemyType::OLGA_FLOW_2: - return "OLGA_FLOW_2"; - case EnemyType::PAL_SHARK: - return "PAL_SHARK"; - case EnemyType::PAN_ARMS: - return "PAN_ARMS"; - case EnemyType::PAZUZU: - return "PAZUZU"; - case EnemyType::PAZUZU_ALT: - return "PAZUZU_ALT"; - case EnemyType::PIG_RAY: - return "PIG_RAY"; - case EnemyType::POFUILLY_SLIME: - return "POFUILLY_SLIME"; - case EnemyType::POUILLY_SLIME: - return "POUILLY_SLIME"; - case EnemyType::POISON_LILY: - return "POISON_LILY"; - case EnemyType::PYRO_GORAN: - return "PYRO_GORAN"; - case EnemyType::RAG_RAPPY: - return "RAG_RAPPY"; - case EnemyType::RECOBOX: - return "RECOBOX"; - case EnemyType::RECON: - return "RECON"; - case EnemyType::SAINT_MILLION: - return "SAINT_MILLION"; - case EnemyType::SAINT_RAPPY: - return "SAINT_RAPPY"; - case EnemyType::SAND_RAPPY: - return "SAND_RAPPY"; - case EnemyType::SAND_RAPPY_ALT: - return "SAND_RAPPY_ALT"; - case EnemyType::SATELLITE_LIZARD: - return "SATELLITE_LIZARD"; - case EnemyType::SATELLITE_LIZARD_ALT: - return "SATELLITE_LIZARD_ALT"; - case EnemyType::SAVAGE_WOLF: - return "SAVAGE_WOLF"; - case EnemyType::SHAMBERTIN: - return "SHAMBERTIN"; - case EnemyType::SINOW_BEAT: - return "SINOW_BEAT"; - case EnemyType::SINOW_BERILL: - return "SINOW_BERILL"; - case EnemyType::SINOW_GOLD: - return "SINOW_GOLD"; - case EnemyType::SINOW_SPIGELL: - return "SINOW_SPIGELL"; - case EnemyType::SINOW_ZELE: - return "SINOW_ZELE"; - case EnemyType::SINOW_ZOA: - return "SINOW_ZOA"; - case EnemyType::SO_DIMENIAN: - return "SO_DIMENIAN"; - case EnemyType::UL_GIBBON: - return "UL_GIBBON"; - case EnemyType::VOL_OPT_1: - return "VOL_OPT_1"; - case EnemyType::VOL_OPT_2: - return "VOL_OPT_2"; - case EnemyType::VOL_OPT_AMP: - return "VOL_OPT_AMP"; - case EnemyType::VOL_OPT_CORE: - return "VOL_OPT_CORE"; - case EnemyType::VOL_OPT_MONITOR: - return "VOL_OPT_MONITOR"; - case EnemyType::VOL_OPT_PILLAR: - return "VOL_OPT_PILLAR"; - case EnemyType::YOWIE: - return "YOWIE"; - case EnemyType::YOWIE_ALT: - return "YOWIE_ALT"; - case EnemyType::ZE_BOOTA: - return "ZE_BOOTA"; - case EnemyType::ZOL_GIBBON: - return "ZOL_GIBBON"; - case EnemyType::ZU: - return "ZU"; - case EnemyType::ZU_ALT: - return "ZU_ALT"; - case EnemyType::MAX_ENEMY_TYPE: - return "MAX_ENEMY_TYPE"; - default: - throw logic_error("invalid enemy type"); - } -} - -template <> -EnemyType enum_for_name(const char* name) { - static const unordered_map names({ - {"UNKNOWN", EnemyType::UNKNOWN}, - {"NONE", EnemyType::NONE}, - {"NON_ENEMY_NPC", EnemyType::NON_ENEMY_NPC}, - {"AL_RAPPY", EnemyType::AL_RAPPY}, - {"ASTARK", EnemyType::ASTARK}, - {"BA_BOOTA", EnemyType::BA_BOOTA}, - {"BARBA_RAY", EnemyType::BARBA_RAY}, - {"BARBAROUS_WOLF", EnemyType::BARBAROUS_WOLF}, - {"BEE_L", EnemyType::BEE_L}, - {"BEE_R", EnemyType::BEE_R}, - {"BOOMA", EnemyType::BOOMA}, - {"BOOTA", EnemyType::BOOTA}, - {"BULCLAW", EnemyType::BULCLAW}, - {"BULK", EnemyType::BULK}, - {"CANADINE", EnemyType::CANADINE}, - {"CANADINE_GROUP", EnemyType::CANADINE_GROUP}, - {"CANANE", EnemyType::CANANE}, - {"CHAOS_BRINGER", EnemyType::CHAOS_BRINGER}, - {"CHAOS_SORCERER", EnemyType::CHAOS_SORCERER}, - {"CLAW", EnemyType::CLAW}, - {"DARK_BELRA", EnemyType::DARK_BELRA}, - {"DARK_FALZ_1", EnemyType::DARK_FALZ_1}, - {"DARK_FALZ_2", EnemyType::DARK_FALZ_2}, - {"DARK_FALZ_3", EnemyType::DARK_FALZ_3}, - {"DARK_GUNNER", EnemyType::DARK_GUNNER}, - {"DARVANT", EnemyType::DARVANT}, - {"DARVANT_ULTIMATE", EnemyType::DARVANT_ULTIMATE}, - {"DE_ROL_LE", EnemyType::DE_ROL_LE}, - {"DE_ROL_LE_BODY", EnemyType::DE_ROL_LE_BODY}, - {"DE_ROL_LE_MINE", EnemyType::DE_ROL_LE_MINE}, - {"DEATH_GUNNER", EnemyType::DEATH_GUNNER}, - {"DEL_LILY", EnemyType::DEL_LILY}, - {"DEL_RAPPY", EnemyType::DEL_RAPPY}, - {"DEL_RAPPY_ALT", EnemyType::DEL_RAPPY_ALT}, - {"DELBITER", EnemyType::DELBITER}, - {"DELDEPTH", EnemyType::DELDEPTH}, - {"DELSABER", EnemyType::DELSABER}, - {"DIMENIAN", EnemyType::DIMENIAN}, - {"DOLMDARL", EnemyType::DOLMDARL}, - {"DOLMOLM", EnemyType::DOLMOLM}, - {"DORPHON", EnemyType::DORPHON}, - {"DORPHON_ECLAIR", EnemyType::DORPHON_ECLAIR}, - {"DRAGON", EnemyType::DRAGON}, - {"DUBCHIC", EnemyType::DUBCHIC}, - {"DUBWITCH", EnemyType::DUBWITCH}, - {"EGG_RAPPY", EnemyType::EGG_RAPPY}, - {"EPSIGUARD", EnemyType::EPSIGUARD}, - {"EPSILON", EnemyType::EPSILON}, - {"EVIL_SHARK", EnemyType::EVIL_SHARK}, - {"GAEL", EnemyType::GAEL}, - {"GAL_GRYPHON", EnemyType::GAL_GRYPHON}, - {"GARANZ", EnemyType::GARANZ}, - {"GEE", EnemyType::GEE}, - {"GI_GUE", EnemyType::GI_GUE}, - {"GIBBLES", EnemyType::GIBBLES}, - {"GIGOBOOMA", EnemyType::GIGOBOOMA}, - {"GILLCHIC", EnemyType::GILLCHIC}, - {"GIRTABLULU", EnemyType::GIRTABLULU}, - {"GOBOOMA", EnemyType::GOBOOMA}, - {"GOL_DRAGON", EnemyType::GOL_DRAGON}, - {"GORAN", EnemyType::GORAN}, - {"GORAN_DETONATOR", EnemyType::GORAN_DETONATOR}, - {"GRASS_ASSASSIN", EnemyType::GRASS_ASSASSIN}, - {"GUIL_SHARK", EnemyType::GUIL_SHARK}, - {"HALLO_RAPPY", EnemyType::HALLO_RAPPY}, - {"HIDOOM", EnemyType::HIDOOM}, - {"HILDEBEAR", EnemyType::HILDEBEAR}, - {"HILDEBLUE", EnemyType::HILDEBLUE}, - {"ILL_GILL", EnemyType::ILL_GILL}, - {"KONDRIEU", EnemyType::KONDRIEU}, - {"LA_DIMENIAN", EnemyType::LA_DIMENIAN}, - {"LOVE_RAPPY", EnemyType::LOVE_RAPPY}, - {"MERICAROL", EnemyType::MERICAROL}, - {"MERICUS", EnemyType::MERICUS}, - {"MERIKLE", EnemyType::MERIKLE}, - {"MERILLIA", EnemyType::MERILLIA}, - {"MERILTAS", EnemyType::MERILTAS}, - {"MERISSA_A", EnemyType::MERISSA_A}, - {"MERISSA_AA", EnemyType::MERISSA_AA}, - {"MIGIUM", EnemyType::MIGIUM}, - {"MONEST", EnemyType::MONEST}, - {"MORFOS", EnemyType::MORFOS}, - {"MOTHMANT", EnemyType::MOTHMANT}, - {"NANO_DRAGON", EnemyType::NANO_DRAGON}, - {"NAR_LILY", EnemyType::NAR_LILY}, - {"OLGA_FLOW_1", EnemyType::OLGA_FLOW_1}, - {"OLGA_FLOW_2", EnemyType::OLGA_FLOW_2}, - {"PAL_SHARK", EnemyType::PAL_SHARK}, - {"PAN_ARMS", EnemyType::PAN_ARMS}, - {"PAZUZU", EnemyType::PAZUZU}, - {"PAZUZU_ALT", EnemyType::PAZUZU_ALT}, - {"PIG_RAY", EnemyType::PIG_RAY}, - {"POFUILLY_SLIME", EnemyType::POFUILLY_SLIME}, - {"POUILLY_SLIME", EnemyType::POUILLY_SLIME}, - {"POISON_LILY", EnemyType::POISON_LILY}, - {"PYRO_GORAN", EnemyType::PYRO_GORAN}, - {"RAG_RAPPY", EnemyType::RAG_RAPPY}, - {"RECOBOX", EnemyType::RECOBOX}, - {"RECON", EnemyType::RECON}, - {"SAINT_MILLION", EnemyType::SAINT_MILLION}, - {"SAINT_RAPPY", EnemyType::SAINT_RAPPY}, - {"SAND_RAPPY", EnemyType::SAND_RAPPY}, - {"SAND_RAPPY_ALT", EnemyType::SAND_RAPPY_ALT}, - {"SATELLITE_LIZARD", EnemyType::SATELLITE_LIZARD}, - {"SATELLITE_LIZARD_ALT", EnemyType::SATELLITE_LIZARD_ALT}, - {"SAVAGE_WOLF", EnemyType::SAVAGE_WOLF}, - {"SHAMBERTIN", EnemyType::SHAMBERTIN}, - {"SINOW_BEAT", EnemyType::SINOW_BEAT}, - {"SINOW_BERILL", EnemyType::SINOW_BERILL}, - {"SINOW_GOLD", EnemyType::SINOW_GOLD}, - {"SINOW_SPIGELL", EnemyType::SINOW_SPIGELL}, - {"SINOW_ZELE", EnemyType::SINOW_ZELE}, - {"SINOW_ZOA", EnemyType::SINOW_ZOA}, - {"SO_DIMENIAN", EnemyType::SO_DIMENIAN}, - {"UL_GIBBON", EnemyType::UL_GIBBON}, - {"VOL_OPT_1", EnemyType::VOL_OPT_1}, - {"VOL_OPT_2", EnemyType::VOL_OPT_2}, - {"VOL_OPT_AMP", EnemyType::VOL_OPT_AMP}, - {"VOL_OPT_CORE", EnemyType::VOL_OPT_CORE}, - {"VOL_OPT_MONITOR", EnemyType::VOL_OPT_MONITOR}, - {"VOL_OPT_PILLAR", EnemyType::VOL_OPT_PILLAR}, - {"YOWIE", EnemyType::YOWIE}, - {"YOWIE_ALT", EnemyType::YOWIE_ALT}, - {"ZE_BOOTA", EnemyType::ZE_BOOTA}, - {"ZOL_GIBBON", EnemyType::ZOL_GIBBON}, - {"ZU", EnemyType::ZU}, - {"ZU_ALT", EnemyType::ZU_ALT}, - {"MAX_ENEMY_TYPE", EnemyType::MAX_ENEMY_TYPE}, - }); - return names.at(name); -} - -bool enemy_type_valid_for_episode(Episode episode, EnemyType enemy_type) { - switch (episode) { - case Episode::EP1: - switch (enemy_type) { - case EnemyType::AL_RAPPY: - case EnemyType::BARBAROUS_WOLF: - case EnemyType::BOOMA: - case EnemyType::BULCLAW: - case EnemyType::BULK: - case EnemyType::CANADINE_GROUP: - case EnemyType::CANADINE: - case EnemyType::CANANE: - case EnemyType::CHAOS_BRINGER: - case EnemyType::CHAOS_SORCERER: - case EnemyType::CLAW: - case EnemyType::DARK_BELRA: - case EnemyType::DARK_FALZ_1: - case EnemyType::DARK_FALZ_2: - case EnemyType::DARK_FALZ_3: - case EnemyType::DARK_GUNNER: - case EnemyType::DARVANT_ULTIMATE: - case EnemyType::DARVANT: - case EnemyType::DE_ROL_LE: - case EnemyType::DEATH_GUNNER: - case EnemyType::DELSABER: - case EnemyType::DIMENIAN: - case EnemyType::DRAGON: - case EnemyType::DUBCHIC: - case EnemyType::EVIL_SHARK: - case EnemyType::GARANZ: - case EnemyType::GIGOBOOMA: - case EnemyType::GILLCHIC: - case EnemyType::GOBOOMA: - case EnemyType::GRASS_ASSASSIN: - case EnemyType::GUIL_SHARK: - case EnemyType::HIDOOM: - case EnemyType::HILDEBEAR: - case EnemyType::HILDEBLUE: - case EnemyType::LA_DIMENIAN: - case EnemyType::MIGIUM: - case EnemyType::MONEST: - case EnemyType::MOTHMANT: - case EnemyType::NANO_DRAGON: - case EnemyType::NAR_LILY: - case EnemyType::PAL_SHARK: - case EnemyType::PAN_ARMS: - case EnemyType::POFUILLY_SLIME: - case EnemyType::POISON_LILY: - case EnemyType::POUILLY_SLIME: - case EnemyType::RAG_RAPPY: - case EnemyType::SAVAGE_WOLF: - case EnemyType::SINOW_BEAT: - case EnemyType::SINOW_GOLD: - case EnemyType::SO_DIMENIAN: - case EnemyType::VOL_OPT_2: - return true; - default: - return false; - } - case Episode::EP2: - switch (enemy_type) { - case EnemyType::BARBA_RAY: - case EnemyType::BARBAROUS_WOLF: - case EnemyType::CHAOS_SORCERER: - case EnemyType::DARK_BELRA: - case EnemyType::DEL_LILY: - case EnemyType::DELBITER: - case EnemyType::DELDEPTH: - case EnemyType::DELSABER: - case EnemyType::DIMENIAN: - case EnemyType::DOLMDARL: - case EnemyType::DOLMOLM: - case EnemyType::DUBCHIC: - case EnemyType::EGG_RAPPY: - case EnemyType::EPSILON: - case EnemyType::GAEL: - case EnemyType::GAL_GRYPHON: - case EnemyType::GARANZ: - case EnemyType::GEE: - case EnemyType::GI_GUE: - case EnemyType::GIBBLES: - case EnemyType::GILLCHIC: - case EnemyType::GOL_DRAGON: - case EnemyType::GRASS_ASSASSIN: - case EnemyType::HALLO_RAPPY: - case EnemyType::HIDOOM: - case EnemyType::HILDEBEAR: - case EnemyType::HILDEBLUE: - case EnemyType::ILL_GILL: - case EnemyType::LA_DIMENIAN: - case EnemyType::LOVE_RAPPY: - case EnemyType::MERICAROL: - case EnemyType::MERICUS: - case EnemyType::MERIKLE: - case EnemyType::MERILLIA: - case EnemyType::MERILTAS: - case EnemyType::MIGIUM: - case EnemyType::MONEST: - case EnemyType::MORFOS: - case EnemyType::MOTHMANT: - case EnemyType::NAR_LILY: - case EnemyType::OLGA_FLOW_1: - case EnemyType::OLGA_FLOW_2: - case EnemyType::PAN_ARMS: - case EnemyType::POISON_LILY: - case EnemyType::RAG_RAPPY: - case EnemyType::RECOBOX: - case EnemyType::RECON: - case EnemyType::SAINT_RAPPY: - case EnemyType::SAVAGE_WOLF: - case EnemyType::SINOW_BERILL: - case EnemyType::SINOW_SPIGELL: - case EnemyType::SINOW_ZELE: - case EnemyType::SINOW_ZOA: - case EnemyType::SO_DIMENIAN: - case EnemyType::UL_GIBBON: - case EnemyType::ZOL_GIBBON: - return true; - default: - return false; - } - case Episode::EP4: - switch (enemy_type) { - case EnemyType::ASTARK: - case EnemyType::BA_BOOTA: - case EnemyType::BOOTA: - case EnemyType::DEL_RAPPY_ALT: - case EnemyType::DEL_RAPPY: - case EnemyType::DORPHON_ECLAIR: - case EnemyType::DORPHON: - case EnemyType::GIRTABLULU: - case EnemyType::GORAN_DETONATOR: - case EnemyType::GORAN: - case EnemyType::KONDRIEU: - case EnemyType::MERISSA_A: - case EnemyType::MERISSA_AA: - case EnemyType::PAZUZU_ALT: - case EnemyType::PAZUZU: - case EnemyType::PYRO_GORAN: - case EnemyType::SAINT_MILLION: - case EnemyType::SAND_RAPPY_ALT: - case EnemyType::SAND_RAPPY: - case EnemyType::SATELLITE_LIZARD_ALT: - case EnemyType::SATELLITE_LIZARD: - case EnemyType::SHAMBERTIN: - case EnemyType::YOWIE_ALT: - case EnemyType::YOWIE: - case EnemyType::ZE_BOOTA: - case EnemyType::ZU_ALT: - case EnemyType::ZU: - return true; - default: - return false; - } - default: - return false; - } -} - -uint8_t battle_param_index_for_enemy_type(Episode episode, EnemyType enemy_type) { - switch (episode) { - case Episode::EP1: - switch (enemy_type) { - case EnemyType::MOTHMANT: - return 0x00; - case EnemyType::MONEST: - return 0x01; - case EnemyType::SAVAGE_WOLF: - return 0x02; - case EnemyType::BARBAROUS_WOLF: - return 0x03; - case EnemyType::POISON_LILY: - return 0x04; - case EnemyType::NAR_LILY: - return 0x05; - case EnemyType::SINOW_BEAT: - return 0x06; - case EnemyType::CANADINE: - return 0x07; - case EnemyType::CANADINE_GROUP: - return 0x08; - case EnemyType::CANANE: - return 0x09; - case EnemyType::CHAOS_SORCERER: - return 0x0A; - case EnemyType::CHAOS_BRINGER: - return 0x0D; - case EnemyType::DARK_BELRA: - return 0x0E; - case EnemyType::DE_ROL_LE: - return 0x0F; - case EnemyType::DRAGON: - return 0x12; - case EnemyType::SINOW_GOLD: - return 0x13; - case EnemyType::RAG_RAPPY: - return 0x18; - case EnemyType::AL_RAPPY: - return 0x19; - case EnemyType::NANO_DRAGON: - return 0x1A; - case EnemyType::DUBCHIC: - return 0x1B; - case EnemyType::GILLCHIC: - return 0x1C; - case EnemyType::GARANZ: - return 0x1D; - case EnemyType::DARK_GUNNER: - case EnemyType::DEATH_GUNNER: - return 0x1E; - case EnemyType::BULCLAW: - case EnemyType::BULK: - return 0x1F; - case EnemyType::CLAW: - return 0x20; - case EnemyType::VOL_OPT_2: - return 0x25; - case EnemyType::POFUILLY_SLIME: - return 0x30; - case EnemyType::POUILLY_SLIME: - return 0x2F; - case EnemyType::PAN_ARMS: - return 0x31; - case EnemyType::HIDOOM: - return 0x32; - case EnemyType::MIGIUM: - return 0x33; - case EnemyType::DARVANT: - return 0x35; - case EnemyType::DARVANT_ULTIMATE: - return 0x39; - case EnemyType::DARK_FALZ_1: - return 0x36; - case EnemyType::DARK_FALZ_2: - return 0x37; - case EnemyType::DARK_FALZ_3: - return 0x38; - case EnemyType::HILDEBEAR: - return 0x49; - case EnemyType::HILDEBLUE: - return 0x4A; - case EnemyType::BOOMA: - return 0x4B; - case EnemyType::GOBOOMA: - return 0x4C; - case EnemyType::GIGOBOOMA: - return 0x4D; - case EnemyType::GRASS_ASSASSIN: - return 0x4E; - case EnemyType::EVIL_SHARK: - return 0x4F; - case EnemyType::PAL_SHARK: - return 0x50; - case EnemyType::GUIL_SHARK: - return 0x51; - case EnemyType::DELSABER: - return 0x52; - case EnemyType::DIMENIAN: - return 0x53; - case EnemyType::LA_DIMENIAN: - return 0x54; - case EnemyType::SO_DIMENIAN: - return 0x55; - default: - throw runtime_error(string_printf("%s does not have battle parameters in Episode 1", name_for_enum(enemy_type))); - } - break; - case Episode::EP2: - switch (enemy_type) { - case EnemyType::MOTHMANT: - return 0x00; - case EnemyType::MONEST: - return 0x01; - case EnemyType::SAVAGE_WOLF: - return 0x02; - case EnemyType::BARBAROUS_WOLF: - return 0x03; - case EnemyType::POISON_LILY: - return 0x04; - case EnemyType::NAR_LILY: - return 0x05; - case EnemyType::SINOW_BERILL: - return 0x06; - case EnemyType::GEE: - return 0x07; - case EnemyType::CHAOS_SORCERER: - return 0x0A; - case EnemyType::DELBITER: - return 0x0D; - case EnemyType::DARK_BELRA: - return 0x0E; - case EnemyType::BARBA_RAY: - return 0x0F; - case EnemyType::GOL_DRAGON: - return 0x12; - case EnemyType::SINOW_SPIGELL: - return 0x13; - case EnemyType::RAG_RAPPY: - return 0x18; - case EnemyType::LOVE_RAPPY: - case EnemyType::SAINT_RAPPY: - case EnemyType::EGG_RAPPY: - case EnemyType::HALLO_RAPPY: - return 0x19; - case EnemyType::GI_GUE: - return 0x1A; - case EnemyType::DUBCHIC: - return 0x1B; - case EnemyType::GILLCHIC: - return 0x1C; - case EnemyType::GARANZ: - return 0x1D; - case EnemyType::GAL_GRYPHON: - return 0x1E; - case EnemyType::EPSILON: - return 0x23; - case EnemyType::DEL_LILY: - return 0x25; - case EnemyType::ILL_GILL: - return 0x26; - case EnemyType::OLGA_FLOW_1: - return 0x2B; - case EnemyType::OLGA_FLOW_2: - return 0x2C; - case EnemyType::GAEL: - return 0x2E; - case EnemyType::DELDEPTH: - return 0x30; - case EnemyType::PAN_ARMS: - return 0x31; - case EnemyType::HIDOOM: - return 0x32; - case EnemyType::MIGIUM: - return 0x33; - case EnemyType::MERICAROL: - return 0x3A; - case EnemyType::UL_GIBBON: - return 0x3B; - case EnemyType::ZOL_GIBBON: - return 0x3C; - case EnemyType::GIBBLES: - return 0x3D; - case EnemyType::MORFOS: - return 0x40; - case EnemyType::RECOBOX: - return 0x41; - case EnemyType::RECON: - return 0x42; - case EnemyType::SINOW_ZOA: - return 0x43; - case EnemyType::SINOW_ZELE: - return 0x44; - case EnemyType::MERIKLE: - return 0x45; - case EnemyType::MERICUS: - return 0x46; - case EnemyType::HILDEBEAR: - return 0x49; - case EnemyType::HILDEBLUE: - return 0x4A; - case EnemyType::MERILLIA: - return 0x4B; - case EnemyType::MERILTAS: - return 0x4C; - case EnemyType::GRASS_ASSASSIN: - return 0x4E; - case EnemyType::DOLMOLM: - return 0x4F; - case EnemyType::DOLMDARL: - return 0x50; - case EnemyType::DELSABER: - return 0x52; - case EnemyType::DIMENIAN: - return 0x53; - case EnemyType::LA_DIMENIAN: - return 0x54; - case EnemyType::SO_DIMENIAN: - return 0x55; - default: - throw runtime_error(string_printf("%s does not have battle parameters in Episode 2", name_for_enum(enemy_type))); - } - break; - case Episode::EP4: - switch (enemy_type) { - case EnemyType::BOOTA: - return 0x00; - case EnemyType::ZE_BOOTA: - return 0x01; - case EnemyType::BA_BOOTA: - return 0x03; - case EnemyType::SAND_RAPPY: - return 0x05; - case EnemyType::DEL_RAPPY: - return 0x06; - case EnemyType::ZU: - return 0x07; - case EnemyType::PAZUZU: - return 0x08; - case EnemyType::ASTARK: - return 0x09; - case EnemyType::SATELLITE_LIZARD: - return 0x0D; - case EnemyType::YOWIE: - return 0x0E; - case EnemyType::DORPHON: - return 0x0F; - case EnemyType::DORPHON_ECLAIR: - return 0x10; - case EnemyType::GORAN: - return 0x11; - case EnemyType::PYRO_GORAN: - return 0x12; - case EnemyType::GORAN_DETONATOR: - return 0x13; - case EnemyType::SAND_RAPPY_ALT: - return 0x17; - case EnemyType::DEL_RAPPY_ALT: - return 0x18; - case EnemyType::MERISSA_A: - return 0x19; - case EnemyType::MERISSA_AA: - return 0x1A; - case EnemyType::ZU_ALT: - return 0x1B; - case EnemyType::PAZUZU_ALT: - return 0x1C; - case EnemyType::SATELLITE_LIZARD_ALT: - return 0x1D; - case EnemyType::YOWIE_ALT: - return 0x1E; - case EnemyType::GIRTABLULU: - return 0x1F; - case EnemyType::SAINT_MILLION: - return 0x22; - case EnemyType::SHAMBERTIN: - return 0x26; - case EnemyType::KONDRIEU: - return 0x2A; - default: - throw runtime_error(string_printf("%s does not have battle parameters in Episode 4", name_for_enum(enemy_type))); - } - break; - default: - throw logic_error("incorrect episode in battle param lookup"); - } - throw logic_error("fallthrough case in battle param lookup"); -} - -uint8_t rare_table_index_for_enemy_type(EnemyType enemy_type) { - switch (enemy_type) { - case EnemyType::AL_RAPPY: - return 0x06; - case EnemyType::ASTARK: - return 0x41; - case EnemyType::BA_BOOTA: - return 0x4F; - case EnemyType::BARBA_RAY: - return 0x49; - case EnemyType::BARBAROUS_WOLF: - return 0x08; - case EnemyType::BOOMA: - return 0x09; - case EnemyType::BOOTA: - return 0x4D; - case EnemyType::BULK: - return 0x27; - case EnemyType::BULCLAW: - return 0x28; - case EnemyType::CANADINE: - case EnemyType::CANADINE_GROUP: - return 0x1C; - case EnemyType::CANANE: - return 0x1D; - case EnemyType::CHAOS_BRINGER: - return 0x24; - case EnemyType::CHAOS_SORCERER: - return 0x1F; - case EnemyType::CLAW: - return 0x26; - case EnemyType::DARK_BELRA: - return 0x25; - case EnemyType::DARK_FALZ_2: - return 0x2F; - case EnemyType::DARK_FALZ_3: - return 0x2F; - case EnemyType::DARK_GUNNER: - return 0x22; - case EnemyType::DEATH_GUNNER: - return 0x23; - case EnemyType::DE_ROL_LE: - return 0x2D; - case EnemyType::DEL_LILY: - return 0x53; - case EnemyType::DEL_RAPPY: - return 0x57; - case EnemyType::DEL_RAPPY_ALT: - return 0x58; - case EnemyType::DELBITER: - return 0x48; - case EnemyType::DELDEPTH: - return 0x47; - case EnemyType::DELSABER: - return 0x1E; - case EnemyType::DIMENIAN: - return 0x29; - case EnemyType::DOLMDARL: - return 0x41; - case EnemyType::DOLMOLM: - return 0x40; - case EnemyType::DORPHON: - return 0x50; - case EnemyType::DORPHON_ECLAIR: - return 0x51; - case EnemyType::DRAGON: - return 0x2C; - case EnemyType::DUBCHIC: - return 0x18; - case EnemyType::EGG_RAPPY: - return 0x51; - case EnemyType::EPSILON: - return 0x54; - case EnemyType::EVIL_SHARK: - return 0x10; - case EnemyType::GAL_GRYPHON: - return 0x4D; - case EnemyType::GARANZ: - return 0x19; - case EnemyType::GEE: - return 0x36; - case EnemyType::GI_GUE: - return 0x37; - case EnemyType::GIBBLES: - return 0x3D; - case EnemyType::GIGOBOOMA: - return 0x0B; - case EnemyType::GILLCHIC: - return 0x32; - case EnemyType::GIRTABLULU: - return 0x48; - case EnemyType::GOBOOMA: - return 0x0A; - case EnemyType::GOL_DRAGON: - return 0x4C; - case EnemyType::GORAN: - return 0x52; - case EnemyType::GORAN_DETONATOR: - return 0x53; - case EnemyType::GRASS_ASSASSIN: - return 0x0C; - case EnemyType::GUIL_SHARK: - return 0x12; - case EnemyType::HALLO_RAPPY: - return 0x50; - case EnemyType::HIDOOM: - return 0x17; - case EnemyType::HILDEBEAR: - return 0x01; - case EnemyType::HILDEBLUE: - return 0x02; - case EnemyType::ILL_GILL: - return 0x52; - case EnemyType::KONDRIEU: - return 0x5B; - case EnemyType::LA_DIMENIAN: - return 0x2A; - case EnemyType::LOVE_RAPPY: - return 0x33; - case EnemyType::MERICAROL: - return 0x38; - case EnemyType::MERICUS: - return 0x3A; - case EnemyType::MERIKLE: - return 0x39; - case EnemyType::MERILLIA: - return 0x34; - case EnemyType::MERILTAS: - return 0x35; - case EnemyType::MERISSA_A: - return 0x46; - case EnemyType::MERISSA_AA: - return 0x47; - case EnemyType::MIGIUM: - return 0x16; - case EnemyType::MONEST: - return 0x04; - case EnemyType::MORFOS: - return 0x42; - case EnemyType::MOTHMANT: - return 0x03; - case EnemyType::NANO_DRAGON: - return 0x0F; - case EnemyType::NAR_LILY: - return 0x0E; - case EnemyType::OLGA_FLOW_2: - return 0x4E; - case EnemyType::PAL_SHARK: - return 0x11; - case EnemyType::PAN_ARMS: - return 0x15; - case EnemyType::PAZUZU: - return 0x4B; - case EnemyType::PAZUZU_ALT: - return 0x4C; - case EnemyType::POFUILLY_SLIME: - return 0x13; - case EnemyType::POUILLY_SLIME: - return 0x14; - case EnemyType::POISON_LILY: - return 0x0D; - case EnemyType::PYRO_GORAN: - return 0x54; - case EnemyType::RAG_RAPPY: - return 0x05; - case EnemyType::RECOBOX: - return 0x43; - case EnemyType::RECON: - return 0x44; - case EnemyType::SAINT_RAPPY: - return 0x4F; - case EnemyType::SAINT_MILLION: - return 0x59; - case EnemyType::SAND_RAPPY: - return 0x55; - case EnemyType::SAND_RAPPY_ALT: - return 0x56; - case EnemyType::SATELLITE_LIZARD: - return 0x44; - case EnemyType::SATELLITE_LIZARD_ALT: - return 0x45; - case EnemyType::SAVAGE_WOLF: - return 0x07; - case EnemyType::SHAMBERTIN: - return 0x5A; - case EnemyType::SINOW_BEAT: - return 0x1A; - case EnemyType::SINOW_BERILL: - return 0x3E; - case EnemyType::SINOW_GOLD: - return 0x1B; - case EnemyType::SINOW_SPIGELL: - return 0x3F; - case EnemyType::SINOW_ZELE: - return 0x46; - case EnemyType::SINOW_ZOA: - return 0x45; - case EnemyType::SO_DIMENIAN: - return 0x2B; - case EnemyType::UL_GIBBON: - return 0x3B; - case EnemyType::VOL_OPT_2: - return 0x2E; - case EnemyType::YOWIE: - return 0x42; - case EnemyType::YOWIE_ALT: - return 0x43; - case EnemyType::ZE_BOOTA: - return 0x4E; - case EnemyType::ZOL_GIBBON: - return 0x3C; - case EnemyType::ZU: - return 0x49; - case EnemyType::ZU_ALT: - return 0x4A; - default: - throw runtime_error(string_printf("%s does not have a rare table entry", name_for_enum(enemy_type))); - } -} - -const vector& enemy_types_for_rare_table_index(Episode episode, uint8_t rt_index) { - const auto& generate_table = +[](Episode episode) -> vector> { - vector> ret; - for (size_t z = 0; z < static_cast(EnemyType::MAX_ENEMY_TYPE); z++) { - EnemyType t = static_cast(z); - try { - uint8_t rt_index = rare_table_index_for_enemy_type(t); - if (enemy_type_valid_for_episode(episode, t)) { - if (rt_index >= ret.size()) { - ret.resize(rt_index + 1); - } - ret[rt_index].emplace_back(t); - } - } catch (const exception&) { - } - } - return ret; - }; - - static array>, 5> data; - auto& ret = data.at(static_cast(episode)); - if (ret.empty()) { - ret = generate_table(episode); - } - try { - return ret.at(rt_index); - } catch (const out_of_range&) { - static const vector empty_vec; - return empty_vec; - } -} - -bool enemy_type_is_rare(EnemyType type) { - return ((type == EnemyType::HILDEBLUE) || - (type == EnemyType::AL_RAPPY) || - (type == EnemyType::NAR_LILY) || - (type == EnemyType::POUILLY_SLIME) || - (type == EnemyType::MERISSA_AA) || - (type == EnemyType::PAZUZU_ALT) || - (type == EnemyType::DORPHON_ECLAIR) || - (type == EnemyType::KONDRIEU)); -} +#include "BattleParamsIndex.hh" + +#include +#include + +#include "Loggers.hh" +#include "PSOEncryption.hh" +#include "StaticGameData.hh" + +using namespace std; + +template <> +const char* name_for_enum(EnemyType type) { + switch (type) { + case EnemyType::UNKNOWN: + return "UNKNOWN"; + case EnemyType::NONE: + return "NONE"; + case EnemyType::NON_ENEMY_NPC: + return "NON_ENEMY_NPC"; + case EnemyType::AL_RAPPY: + return "AL_RAPPY"; + case EnemyType::ASTARK: + return "ASTARK"; + case EnemyType::BA_BOOTA: + return "BA_BOOTA"; + case EnemyType::BARBA_RAY: + return "BARBA_RAY"; + case EnemyType::BARBAROUS_WOLF: + return "BARBAROUS_WOLF"; + case EnemyType::BEE_L: + return "BEE_L"; + case EnemyType::BEE_R: + return "BEE_R"; + case EnemyType::BOOMA: + return "BOOMA"; + case EnemyType::BOOTA: + return "BOOTA"; + case EnemyType::BULCLAW: + return "BULCLAW"; + case EnemyType::BULK: + return "BULK"; + case EnemyType::CANADINE: + return "CANADINE"; + case EnemyType::CANADINE_GROUP: + return "CANADINE_GROUP"; + case EnemyType::CANANE: + return "CANANE"; + case EnemyType::CHAOS_BRINGER: + return "CHAOS_BRINGER"; + case EnemyType::CHAOS_SORCERER: + return "CHAOS_SORCERER"; + case EnemyType::CLAW: + return "CLAW"; + case EnemyType::DARK_BELRA: + return "DARK_BELRA"; + case EnemyType::DARK_FALZ_1: + return "DARK_FALZ_1"; + case EnemyType::DARK_FALZ_2: + return "DARK_FALZ_2"; + case EnemyType::DARK_FALZ_3: + return "DARK_FALZ_3"; + case EnemyType::DARK_GUNNER: + return "DARK_GUNNER"; + case EnemyType::DARVANT: + return "DARVANT"; + case EnemyType::DARVANT_ULTIMATE: + return "DARVANT_ULTIMATE"; + case EnemyType::DE_ROL_LE: + return "DE_ROL_LE"; + case EnemyType::DE_ROL_LE_BODY: + return "DE_ROL_LE_BODY"; + case EnemyType::DE_ROL_LE_MINE: + return "DE_ROL_LE_MINE"; + case EnemyType::DEATH_GUNNER: + return "DEATH_GUNNER"; + case EnemyType::DEL_LILY: + return "DEL_LILY"; + case EnemyType::DEL_RAPPY: + return "DEL_RAPPY"; + case EnemyType::DEL_RAPPY_ALT: + return "DEL_RAPPY_ALT"; + case EnemyType::DELBITER: + return "DELBITER"; + case EnemyType::DELDEPTH: + return "DELDEPTH"; + case EnemyType::DELSABER: + return "DELSABER"; + case EnemyType::DIMENIAN: + return "DIMENIAN"; + case EnemyType::DOLMDARL: + return "DOLMDARL"; + case EnemyType::DOLMOLM: + return "DOLMOLM"; + case EnemyType::DORPHON: + return "DORPHON"; + case EnemyType::DORPHON_ECLAIR: + return "DORPHON_ECLAIR"; + case EnemyType::DRAGON: + return "DRAGON"; + case EnemyType::DUBCHIC: + return "DUBCHIC"; + case EnemyType::DUBWITCH: + return "DUBWITCH"; + case EnemyType::EGG_RAPPY: + return "EGG_RAPPY"; + case EnemyType::EPSIGUARD: + return "EPSIGUARD"; + case EnemyType::EPSILON: + return "EPSILON"; + case EnemyType::EVIL_SHARK: + return "EVIL_SHARK"; + case EnemyType::GAEL: + return "GAEL"; + case EnemyType::GAL_GRYPHON: + return "GAL_GRYPHON"; + case EnemyType::GARANZ: + return "GARANZ"; + case EnemyType::GEE: + return "GEE"; + case EnemyType::GI_GUE: + return "GI_GUE"; + case EnemyType::GIBBLES: + return "GIBBLES"; + case EnemyType::GIGOBOOMA: + return "GIGOBOOMA"; + case EnemyType::GILLCHIC: + return "GILLCHIC"; + case EnemyType::GIRTABLULU: + return "GIRTABLULU"; + case EnemyType::GOBOOMA: + return "GOBOOMA"; + case EnemyType::GOL_DRAGON: + return "GOL_DRAGON"; + case EnemyType::GORAN: + return "GORAN"; + case EnemyType::GORAN_DETONATOR: + return "GORAN_DETONATOR"; + case EnemyType::GRASS_ASSASSIN: + return "GRASS_ASSASSIN"; + case EnemyType::GUIL_SHARK: + return "GUIL_SHARK"; + case EnemyType::HALLO_RAPPY: + return "HALLO_RAPPY"; + case EnemyType::HIDOOM: + return "HIDOOM"; + case EnemyType::HILDEBEAR: + return "HILDEBEAR"; + case EnemyType::HILDEBLUE: + return "HILDEBLUE"; + case EnemyType::ILL_GILL: + return "ILL_GILL"; + case EnemyType::KONDRIEU: + return "KONDRIEU"; + case EnemyType::LA_DIMENIAN: + return "LA_DIMENIAN"; + case EnemyType::LOVE_RAPPY: + return "LOVE_RAPPY"; + case EnemyType::MERICAROL: + return "MERICAROL"; + case EnemyType::MERICUS: + return "MERICUS"; + case EnemyType::MERIKLE: + return "MERIKLE"; + case EnemyType::MERILLIA: + return "MERILLIA"; + case EnemyType::MERILTAS: + return "MERILTAS"; + case EnemyType::MERISSA_A: + return "MERISSA_A"; + case EnemyType::MERISSA_AA: + return "MERISSA_AA"; + case EnemyType::MIGIUM: + return "MIGIUM"; + case EnemyType::MONEST: + return "MONEST"; + case EnemyType::MORFOS: + return "MORFOS"; + case EnemyType::MOTHMANT: + return "MOTHMANT"; + case EnemyType::NANO_DRAGON: + return "NANO_DRAGON"; + case EnemyType::NAR_LILY: + return "NAR_LILY"; + case EnemyType::OLGA_FLOW_1: + return "OLGA_FLOW_1"; + case EnemyType::OLGA_FLOW_2: + return "OLGA_FLOW_2"; + case EnemyType::PAL_SHARK: + return "PAL_SHARK"; + case EnemyType::PAN_ARMS: + return "PAN_ARMS"; + case EnemyType::PAZUZU: + return "PAZUZU"; + case EnemyType::PAZUZU_ALT: + return "PAZUZU_ALT"; + case EnemyType::PIG_RAY: + return "PIG_RAY"; + case EnemyType::POFUILLY_SLIME: + return "POFUILLY_SLIME"; + case EnemyType::POUILLY_SLIME: + return "POUILLY_SLIME"; + case EnemyType::POISON_LILY: + return "POISON_LILY"; + case EnemyType::PYRO_GORAN: + return "PYRO_GORAN"; + case EnemyType::RAG_RAPPY: + return "RAG_RAPPY"; + case EnemyType::RECOBOX: + return "RECOBOX"; + case EnemyType::RECON: + return "RECON"; + case EnemyType::SAINT_MILLION: + return "SAINT_MILLION"; + case EnemyType::SAINT_RAPPY: + return "SAINT_RAPPY"; + case EnemyType::SAND_RAPPY: + return "SAND_RAPPY"; + case EnemyType::SAND_RAPPY_ALT: + return "SAND_RAPPY_ALT"; + case EnemyType::SATELLITE_LIZARD: + return "SATELLITE_LIZARD"; + case EnemyType::SATELLITE_LIZARD_ALT: + return "SATELLITE_LIZARD_ALT"; + case EnemyType::SAVAGE_WOLF: + return "SAVAGE_WOLF"; + case EnemyType::SHAMBERTIN: + return "SHAMBERTIN"; + case EnemyType::SINOW_BEAT: + return "SINOW_BEAT"; + case EnemyType::SINOW_BERILL: + return "SINOW_BERILL"; + case EnemyType::SINOW_GOLD: + return "SINOW_GOLD"; + case EnemyType::SINOW_SPIGELL: + return "SINOW_SPIGELL"; + case EnemyType::SINOW_ZELE: + return "SINOW_ZELE"; + case EnemyType::SINOW_ZOA: + return "SINOW_ZOA"; + case EnemyType::SO_DIMENIAN: + return "SO_DIMENIAN"; + case EnemyType::UL_GIBBON: + return "UL_GIBBON"; + case EnemyType::VOL_OPT_1: + return "VOL_OPT_1"; + case EnemyType::VOL_OPT_2: + return "VOL_OPT_2"; + case EnemyType::VOL_OPT_AMP: + return "VOL_OPT_AMP"; + case EnemyType::VOL_OPT_CORE: + return "VOL_OPT_CORE"; + case EnemyType::VOL_OPT_MONITOR: + return "VOL_OPT_MONITOR"; + case EnemyType::VOL_OPT_PILLAR: + return "VOL_OPT_PILLAR"; + case EnemyType::YOWIE: + return "YOWIE"; + case EnemyType::YOWIE_ALT: + return "YOWIE_ALT"; + case EnemyType::ZE_BOOTA: + return "ZE_BOOTA"; + case EnemyType::ZOL_GIBBON: + return "ZOL_GIBBON"; + case EnemyType::ZU: + return "ZU"; + case EnemyType::ZU_ALT: + return "ZU_ALT"; + case EnemyType::MAX_ENEMY_TYPE: + return "MAX_ENEMY_TYPE"; + default: + throw logic_error("invalid enemy type"); + } +} + +template <> +EnemyType enum_for_name(const char* name) { + static const unordered_map names({ + {"UNKNOWN", EnemyType::UNKNOWN}, + {"NONE", EnemyType::NONE}, + {"NON_ENEMY_NPC", EnemyType::NON_ENEMY_NPC}, + {"AL_RAPPY", EnemyType::AL_RAPPY}, + {"ASTARK", EnemyType::ASTARK}, + {"BA_BOOTA", EnemyType::BA_BOOTA}, + {"BARBA_RAY", EnemyType::BARBA_RAY}, + {"BARBAROUS_WOLF", EnemyType::BARBAROUS_WOLF}, + {"BEE_L", EnemyType::BEE_L}, + {"BEE_R", EnemyType::BEE_R}, + {"BOOMA", EnemyType::BOOMA}, + {"BOOTA", EnemyType::BOOTA}, + {"BULCLAW", EnemyType::BULCLAW}, + {"BULK", EnemyType::BULK}, + {"CANADINE", EnemyType::CANADINE}, + {"CANADINE_GROUP", EnemyType::CANADINE_GROUP}, + {"CANANE", EnemyType::CANANE}, + {"CHAOS_BRINGER", EnemyType::CHAOS_BRINGER}, + {"CHAOS_SORCERER", EnemyType::CHAOS_SORCERER}, + {"CLAW", EnemyType::CLAW}, + {"DARK_BELRA", EnemyType::DARK_BELRA}, + {"DARK_FALZ_1", EnemyType::DARK_FALZ_1}, + {"DARK_FALZ_2", EnemyType::DARK_FALZ_2}, + {"DARK_FALZ_3", EnemyType::DARK_FALZ_3}, + {"DARK_GUNNER", EnemyType::DARK_GUNNER}, + {"DARVANT", EnemyType::DARVANT}, + {"DARVANT_ULTIMATE", EnemyType::DARVANT_ULTIMATE}, + {"DE_ROL_LE", EnemyType::DE_ROL_LE}, + {"DE_ROL_LE_BODY", EnemyType::DE_ROL_LE_BODY}, + {"DE_ROL_LE_MINE", EnemyType::DE_ROL_LE_MINE}, + {"DEATH_GUNNER", EnemyType::DEATH_GUNNER}, + {"DEL_LILY", EnemyType::DEL_LILY}, + {"DEL_RAPPY", EnemyType::DEL_RAPPY}, + {"DEL_RAPPY_ALT", EnemyType::DEL_RAPPY_ALT}, + {"DELBITER", EnemyType::DELBITER}, + {"DELDEPTH", EnemyType::DELDEPTH}, + {"DELSABER", EnemyType::DELSABER}, + {"DIMENIAN", EnemyType::DIMENIAN}, + {"DOLMDARL", EnemyType::DOLMDARL}, + {"DOLMOLM", EnemyType::DOLMOLM}, + {"DORPHON", EnemyType::DORPHON}, + {"DORPHON_ECLAIR", EnemyType::DORPHON_ECLAIR}, + {"DRAGON", EnemyType::DRAGON}, + {"DUBCHIC", EnemyType::DUBCHIC}, + {"DUBWITCH", EnemyType::DUBWITCH}, + {"EGG_RAPPY", EnemyType::EGG_RAPPY}, + {"EPSIGUARD", EnemyType::EPSIGUARD}, + {"EPSILON", EnemyType::EPSILON}, + {"EVIL_SHARK", EnemyType::EVIL_SHARK}, + {"GAEL", EnemyType::GAEL}, + {"GAL_GRYPHON", EnemyType::GAL_GRYPHON}, + {"GARANZ", EnemyType::GARANZ}, + {"GEE", EnemyType::GEE}, + {"GI_GUE", EnemyType::GI_GUE}, + {"GIBBLES", EnemyType::GIBBLES}, + {"GIGOBOOMA", EnemyType::GIGOBOOMA}, + {"GILLCHIC", EnemyType::GILLCHIC}, + {"GIRTABLULU", EnemyType::GIRTABLULU}, + {"GOBOOMA", EnemyType::GOBOOMA}, + {"GOL_DRAGON", EnemyType::GOL_DRAGON}, + {"GORAN", EnemyType::GORAN}, + {"GORAN_DETONATOR", EnemyType::GORAN_DETONATOR}, + {"GRASS_ASSASSIN", EnemyType::GRASS_ASSASSIN}, + {"GUIL_SHARK", EnemyType::GUIL_SHARK}, + {"HALLO_RAPPY", EnemyType::HALLO_RAPPY}, + {"HIDOOM", EnemyType::HIDOOM}, + {"HILDEBEAR", EnemyType::HILDEBEAR}, + {"HILDEBLUE", EnemyType::HILDEBLUE}, + {"ILL_GILL", EnemyType::ILL_GILL}, + {"KONDRIEU", EnemyType::KONDRIEU}, + {"LA_DIMENIAN", EnemyType::LA_DIMENIAN}, + {"LOVE_RAPPY", EnemyType::LOVE_RAPPY}, + {"MERICAROL", EnemyType::MERICAROL}, + {"MERICUS", EnemyType::MERICUS}, + {"MERIKLE", EnemyType::MERIKLE}, + {"MERILLIA", EnemyType::MERILLIA}, + {"MERILTAS", EnemyType::MERILTAS}, + {"MERISSA_A", EnemyType::MERISSA_A}, + {"MERISSA_AA", EnemyType::MERISSA_AA}, + {"MIGIUM", EnemyType::MIGIUM}, + {"MONEST", EnemyType::MONEST}, + {"MORFOS", EnemyType::MORFOS}, + {"MOTHMANT", EnemyType::MOTHMANT}, + {"NANO_DRAGON", EnemyType::NANO_DRAGON}, + {"NAR_LILY", EnemyType::NAR_LILY}, + {"OLGA_FLOW_1", EnemyType::OLGA_FLOW_1}, + {"OLGA_FLOW_2", EnemyType::OLGA_FLOW_2}, + {"PAL_SHARK", EnemyType::PAL_SHARK}, + {"PAN_ARMS", EnemyType::PAN_ARMS}, + {"PAZUZU", EnemyType::PAZUZU}, + {"PAZUZU_ALT", EnemyType::PAZUZU_ALT}, + {"PIG_RAY", EnemyType::PIG_RAY}, + {"POFUILLY_SLIME", EnemyType::POFUILLY_SLIME}, + {"POUILLY_SLIME", EnemyType::POUILLY_SLIME}, + {"POISON_LILY", EnemyType::POISON_LILY}, + {"PYRO_GORAN", EnemyType::PYRO_GORAN}, + {"RAG_RAPPY", EnemyType::RAG_RAPPY}, + {"RECOBOX", EnemyType::RECOBOX}, + {"RECON", EnemyType::RECON}, + {"SAINT_MILLION", EnemyType::SAINT_MILLION}, + {"SAINT_RAPPY", EnemyType::SAINT_RAPPY}, + {"SAND_RAPPY", EnemyType::SAND_RAPPY}, + {"SAND_RAPPY_ALT", EnemyType::SAND_RAPPY_ALT}, + {"SATELLITE_LIZARD", EnemyType::SATELLITE_LIZARD}, + {"SATELLITE_LIZARD_ALT", EnemyType::SATELLITE_LIZARD_ALT}, + {"SAVAGE_WOLF", EnemyType::SAVAGE_WOLF}, + {"SHAMBERTIN", EnemyType::SHAMBERTIN}, + {"SINOW_BEAT", EnemyType::SINOW_BEAT}, + {"SINOW_BERILL", EnemyType::SINOW_BERILL}, + {"SINOW_GOLD", EnemyType::SINOW_GOLD}, + {"SINOW_SPIGELL", EnemyType::SINOW_SPIGELL}, + {"SINOW_ZELE", EnemyType::SINOW_ZELE}, + {"SINOW_ZOA", EnemyType::SINOW_ZOA}, + {"SO_DIMENIAN", EnemyType::SO_DIMENIAN}, + {"UL_GIBBON", EnemyType::UL_GIBBON}, + {"VOL_OPT_1", EnemyType::VOL_OPT_1}, + {"VOL_OPT_2", EnemyType::VOL_OPT_2}, + {"VOL_OPT_AMP", EnemyType::VOL_OPT_AMP}, + {"VOL_OPT_CORE", EnemyType::VOL_OPT_CORE}, + {"VOL_OPT_MONITOR", EnemyType::VOL_OPT_MONITOR}, + {"VOL_OPT_PILLAR", EnemyType::VOL_OPT_PILLAR}, + {"YOWIE", EnemyType::YOWIE}, + {"YOWIE_ALT", EnemyType::YOWIE_ALT}, + {"ZE_BOOTA", EnemyType::ZE_BOOTA}, + {"ZOL_GIBBON", EnemyType::ZOL_GIBBON}, + {"ZU", EnemyType::ZU}, + {"ZU_ALT", EnemyType::ZU_ALT}, + {"MAX_ENEMY_TYPE", EnemyType::MAX_ENEMY_TYPE}, + }); + return names.at(name); +} + +bool enemy_type_valid_for_episode(Episode episode, EnemyType enemy_type) { + switch (episode) { + case Episode::EP1: + switch (enemy_type) { + case EnemyType::AL_RAPPY: + case EnemyType::BARBAROUS_WOLF: + case EnemyType::BOOMA: + case EnemyType::BULCLAW: + case EnemyType::BULK: + case EnemyType::CANADINE_GROUP: + case EnemyType::CANADINE: + case EnemyType::CANANE: + case EnemyType::CHAOS_BRINGER: + case EnemyType::CHAOS_SORCERER: + case EnemyType::CLAW: + case EnemyType::DARK_BELRA: + case EnemyType::DARK_FALZ_1: + case EnemyType::DARK_FALZ_2: + case EnemyType::DARK_FALZ_3: + case EnemyType::DARK_GUNNER: + case EnemyType::DARVANT_ULTIMATE: + case EnemyType::DARVANT: + case EnemyType::DE_ROL_LE: + case EnemyType::DEATH_GUNNER: + case EnemyType::DELSABER: + case EnemyType::DIMENIAN: + case EnemyType::DRAGON: + case EnemyType::DUBCHIC: + case EnemyType::EVIL_SHARK: + case EnemyType::GARANZ: + case EnemyType::GIGOBOOMA: + case EnemyType::GILLCHIC: + case EnemyType::GOBOOMA: + case EnemyType::GRASS_ASSASSIN: + case EnemyType::GUIL_SHARK: + case EnemyType::HIDOOM: + case EnemyType::HILDEBEAR: + case EnemyType::HILDEBLUE: + case EnemyType::LA_DIMENIAN: + case EnemyType::MIGIUM: + case EnemyType::MONEST: + case EnemyType::MOTHMANT: + case EnemyType::NANO_DRAGON: + case EnemyType::NAR_LILY: + case EnemyType::PAL_SHARK: + case EnemyType::PAN_ARMS: + case EnemyType::POFUILLY_SLIME: + case EnemyType::POISON_LILY: + case EnemyType::POUILLY_SLIME: + case EnemyType::RAG_RAPPY: + case EnemyType::SAVAGE_WOLF: + case EnemyType::SINOW_BEAT: + case EnemyType::SINOW_GOLD: + case EnemyType::SO_DIMENIAN: + case EnemyType::VOL_OPT_2: + return true; + default: + return false; + } + case Episode::EP2: + switch (enemy_type) { + case EnemyType::BARBA_RAY: + case EnemyType::BARBAROUS_WOLF: + case EnemyType::CHAOS_SORCERER: + case EnemyType::DARK_BELRA: + case EnemyType::DEL_LILY: + case EnemyType::DELBITER: + case EnemyType::DELDEPTH: + case EnemyType::DELSABER: + case EnemyType::DIMENIAN: + case EnemyType::DOLMDARL: + case EnemyType::DOLMOLM: + case EnemyType::DUBCHIC: + case EnemyType::EGG_RAPPY: + case EnemyType::EPSILON: + case EnemyType::GAEL: + case EnemyType::GAL_GRYPHON: + case EnemyType::GARANZ: + case EnemyType::GEE: + case EnemyType::GI_GUE: + case EnemyType::GIBBLES: + case EnemyType::GILLCHIC: + case EnemyType::GOL_DRAGON: + case EnemyType::GRASS_ASSASSIN: + case EnemyType::HALLO_RAPPY: + case EnemyType::HIDOOM: + case EnemyType::HILDEBEAR: + case EnemyType::HILDEBLUE: + case EnemyType::ILL_GILL: + case EnemyType::LA_DIMENIAN: + case EnemyType::LOVE_RAPPY: + case EnemyType::MERICAROL: + case EnemyType::MERICUS: + case EnemyType::MERIKLE: + case EnemyType::MERILLIA: + case EnemyType::MERILTAS: + case EnemyType::MIGIUM: + case EnemyType::MONEST: + case EnemyType::MORFOS: + case EnemyType::MOTHMANT: + case EnemyType::NAR_LILY: + case EnemyType::OLGA_FLOW_1: + case EnemyType::OLGA_FLOW_2: + case EnemyType::PAN_ARMS: + case EnemyType::POISON_LILY: + case EnemyType::RAG_RAPPY: + case EnemyType::RECOBOX: + case EnemyType::RECON: + case EnemyType::SAINT_RAPPY: + case EnemyType::SAVAGE_WOLF: + case EnemyType::SINOW_BERILL: + case EnemyType::SINOW_SPIGELL: + case EnemyType::SINOW_ZELE: + case EnemyType::SINOW_ZOA: + case EnemyType::SO_DIMENIAN: + case EnemyType::UL_GIBBON: + case EnemyType::ZOL_GIBBON: + return true; + default: + return false; + } + case Episode::EP4: + switch (enemy_type) { + case EnemyType::ASTARK: + case EnemyType::BA_BOOTA: + case EnemyType::BOOTA: + case EnemyType::DEL_RAPPY_ALT: + case EnemyType::DEL_RAPPY: + case EnemyType::DORPHON_ECLAIR: + case EnemyType::DORPHON: + case EnemyType::GIRTABLULU: + case EnemyType::GORAN_DETONATOR: + case EnemyType::GORAN: + case EnemyType::KONDRIEU: + case EnemyType::MERISSA_A: + case EnemyType::MERISSA_AA: + case EnemyType::PAZUZU_ALT: + case EnemyType::PAZUZU: + case EnemyType::PYRO_GORAN: + case EnemyType::SAINT_MILLION: + case EnemyType::SAND_RAPPY_ALT: + case EnemyType::SAND_RAPPY: + case EnemyType::SATELLITE_LIZARD_ALT: + case EnemyType::SATELLITE_LIZARD: + case EnemyType::SHAMBERTIN: + case EnemyType::YOWIE_ALT: + case EnemyType::YOWIE: + case EnemyType::ZE_BOOTA: + case EnemyType::ZU_ALT: + case EnemyType::ZU: + return true; + default: + return false; + } + default: + return false; + } +} + +uint8_t battle_param_index_for_enemy_type(Episode episode, EnemyType enemy_type) { + switch (episode) { + case Episode::EP1: + switch (enemy_type) { + case EnemyType::MOTHMANT: + return 0x00; + case EnemyType::MONEST: + return 0x01; + case EnemyType::SAVAGE_WOLF: + return 0x02; + case EnemyType::BARBAROUS_WOLF: + return 0x03; + case EnemyType::POISON_LILY: + return 0x04; + case EnemyType::NAR_LILY: + return 0x05; + case EnemyType::SINOW_BEAT: + return 0x06; + case EnemyType::CANADINE: + return 0x07; + case EnemyType::CANADINE_GROUP: + return 0x08; + case EnemyType::CANANE: + return 0x09; + case EnemyType::CHAOS_SORCERER: + return 0x0A; + case EnemyType::CHAOS_BRINGER: + return 0x0D; + case EnemyType::DARK_BELRA: + return 0x0E; + case EnemyType::DE_ROL_LE: + return 0x0F; + case EnemyType::DRAGON: + return 0x12; + case EnemyType::SINOW_GOLD: + return 0x13; + case EnemyType::RAG_RAPPY: + return 0x18; + case EnemyType::AL_RAPPY: + return 0x19; + case EnemyType::NANO_DRAGON: + return 0x1A; + case EnemyType::DUBCHIC: + return 0x1B; + case EnemyType::GILLCHIC: + return 0x1C; + case EnemyType::GARANZ: + return 0x1D; + case EnemyType::DARK_GUNNER: + case EnemyType::DEATH_GUNNER: + return 0x1E; + case EnemyType::BULCLAW: + case EnemyType::BULK: + return 0x1F; + case EnemyType::CLAW: + return 0x20; + case EnemyType::VOL_OPT_2: + return 0x25; + case EnemyType::POFUILLY_SLIME: + return 0x30; + case EnemyType::POUILLY_SLIME: + return 0x2F; + case EnemyType::PAN_ARMS: + return 0x31; + case EnemyType::HIDOOM: + return 0x32; + case EnemyType::MIGIUM: + return 0x33; + case EnemyType::DARVANT: + return 0x35; + case EnemyType::DARVANT_ULTIMATE: + return 0x39; + case EnemyType::DARK_FALZ_1: + return 0x36; + case EnemyType::DARK_FALZ_2: + return 0x37; + case EnemyType::DARK_FALZ_3: + return 0x38; + case EnemyType::HILDEBEAR: + return 0x49; + case EnemyType::HILDEBLUE: + return 0x4A; + case EnemyType::BOOMA: + return 0x4B; + case EnemyType::GOBOOMA: + return 0x4C; + case EnemyType::GIGOBOOMA: + return 0x4D; + case EnemyType::GRASS_ASSASSIN: + return 0x4E; + case EnemyType::EVIL_SHARK: + return 0x4F; + case EnemyType::PAL_SHARK: + return 0x50; + case EnemyType::GUIL_SHARK: + return 0x51; + case EnemyType::DELSABER: + return 0x52; + case EnemyType::DIMENIAN: + return 0x53; + case EnemyType::LA_DIMENIAN: + return 0x54; + case EnemyType::SO_DIMENIAN: + return 0x55; + default: + throw runtime_error(string_printf("%s does not have battle parameters in Episode 1", name_for_enum(enemy_type))); + } + break; + case Episode::EP2: + switch (enemy_type) { + case EnemyType::MOTHMANT: + return 0x00; + case EnemyType::MONEST: + return 0x01; + case EnemyType::SAVAGE_WOLF: + return 0x02; + case EnemyType::BARBAROUS_WOLF: + return 0x03; + case EnemyType::POISON_LILY: + return 0x04; + case EnemyType::NAR_LILY: + return 0x05; + case EnemyType::SINOW_BERILL: + return 0x06; + case EnemyType::GEE: + return 0x07; + case EnemyType::CHAOS_SORCERER: + return 0x0A; + case EnemyType::DELBITER: + return 0x0D; + case EnemyType::DARK_BELRA: + return 0x0E; + case EnemyType::BARBA_RAY: + return 0x0F; + case EnemyType::GOL_DRAGON: + return 0x12; + case EnemyType::SINOW_SPIGELL: + return 0x13; + case EnemyType::RAG_RAPPY: + return 0x18; + case EnemyType::LOVE_RAPPY: + case EnemyType::SAINT_RAPPY: + case EnemyType::EGG_RAPPY: + case EnemyType::HALLO_RAPPY: + return 0x19; + case EnemyType::GI_GUE: + return 0x1A; + case EnemyType::DUBCHIC: + return 0x1B; + case EnemyType::GILLCHIC: + return 0x1C; + case EnemyType::GARANZ: + return 0x1D; + case EnemyType::GAL_GRYPHON: + return 0x1E; + case EnemyType::EPSILON: + return 0x23; + case EnemyType::DEL_LILY: + return 0x25; + case EnemyType::ILL_GILL: + return 0x26; + case EnemyType::OLGA_FLOW_1: + return 0x2B; + case EnemyType::OLGA_FLOW_2: + return 0x2C; + case EnemyType::GAEL: + return 0x2E; + case EnemyType::DELDEPTH: + return 0x30; + case EnemyType::PAN_ARMS: + return 0x31; + case EnemyType::HIDOOM: + return 0x32; + case EnemyType::MIGIUM: + return 0x33; + case EnemyType::MERICAROL: + return 0x3A; + case EnemyType::UL_GIBBON: + return 0x3B; + case EnemyType::ZOL_GIBBON: + return 0x3C; + case EnemyType::GIBBLES: + return 0x3D; + case EnemyType::MORFOS: + return 0x40; + case EnemyType::RECOBOX: + return 0x41; + case EnemyType::RECON: + return 0x42; + case EnemyType::SINOW_ZOA: + return 0x43; + case EnemyType::SINOW_ZELE: + return 0x44; + case EnemyType::MERIKLE: + return 0x45; + case EnemyType::MERICUS: + return 0x46; + case EnemyType::HILDEBEAR: + return 0x49; + case EnemyType::HILDEBLUE: + return 0x4A; + case EnemyType::MERILLIA: + return 0x4B; + case EnemyType::MERILTAS: + return 0x4C; + case EnemyType::GRASS_ASSASSIN: + return 0x4E; + case EnemyType::DOLMOLM: + return 0x4F; + case EnemyType::DOLMDARL: + return 0x50; + case EnemyType::DELSABER: + return 0x52; + case EnemyType::DIMENIAN: + return 0x53; + case EnemyType::LA_DIMENIAN: + return 0x54; + case EnemyType::SO_DIMENIAN: + return 0x55; + default: + throw runtime_error(string_printf("%s does not have battle parameters in Episode 2", name_for_enum(enemy_type))); + } + break; + case Episode::EP4: + switch (enemy_type) { + case EnemyType::BOOTA: + return 0x00; + case EnemyType::ZE_BOOTA: + return 0x01; + case EnemyType::BA_BOOTA: + return 0x03; + case EnemyType::SAND_RAPPY: + return 0x05; + case EnemyType::DEL_RAPPY: + return 0x06; + case EnemyType::ZU: + return 0x07; + case EnemyType::PAZUZU: + return 0x08; + case EnemyType::ASTARK: + return 0x09; + case EnemyType::SATELLITE_LIZARD: + return 0x0D; + case EnemyType::YOWIE: + return 0x0E; + case EnemyType::DORPHON: + return 0x0F; + case EnemyType::DORPHON_ECLAIR: + return 0x10; + case EnemyType::GORAN: + return 0x11; + case EnemyType::PYRO_GORAN: + return 0x12; + case EnemyType::GORAN_DETONATOR: + return 0x13; + case EnemyType::SAND_RAPPY_ALT: + return 0x17; + case EnemyType::DEL_RAPPY_ALT: + return 0x18; + case EnemyType::MERISSA_A: + return 0x19; + case EnemyType::MERISSA_AA: + return 0x1A; + case EnemyType::ZU_ALT: + return 0x1B; + case EnemyType::PAZUZU_ALT: + return 0x1C; + case EnemyType::SATELLITE_LIZARD_ALT: + return 0x1D; + case EnemyType::YOWIE_ALT: + return 0x1E; + case EnemyType::GIRTABLULU: + return 0x1F; + case EnemyType::SAINT_MILLION: + return 0x22; + case EnemyType::SHAMBERTIN: + return 0x26; + case EnemyType::KONDRIEU: + return 0x2A; + default: + throw runtime_error(string_printf("%s does not have battle parameters in Episode 4", name_for_enum(enemy_type))); + } + break; + default: + throw logic_error("incorrect episode in battle param lookup"); + } + throw logic_error("fallthrough case in battle param lookup"); +} + +uint8_t rare_table_index_for_enemy_type(EnemyType enemy_type) { + switch (enemy_type) { + case EnemyType::AL_RAPPY: + return 0x06; + case EnemyType::ASTARK: + return 0x41; + case EnemyType::BA_BOOTA: + return 0x4F; + case EnemyType::BARBA_RAY: + return 0x49; + case EnemyType::BARBAROUS_WOLF: + return 0x08; + case EnemyType::BOOMA: + return 0x09; + case EnemyType::BOOTA: + return 0x4D; + case EnemyType::BULK: + return 0x27; + case EnemyType::BULCLAW: + return 0x28; + case EnemyType::CANADINE: + case EnemyType::CANADINE_GROUP: + return 0x1C; + case EnemyType::CANANE: + return 0x1D; + case EnemyType::CHAOS_BRINGER: + return 0x24; + case EnemyType::CHAOS_SORCERER: + return 0x1F; + case EnemyType::CLAW: + return 0x26; + case EnemyType::DARK_BELRA: + return 0x25; + case EnemyType::DARK_FALZ_2: + return 0x2F; + case EnemyType::DARK_FALZ_3: + return 0x2F; + case EnemyType::DARK_GUNNER: + return 0x22; + case EnemyType::DEATH_GUNNER: + return 0x23; + case EnemyType::DE_ROL_LE: + return 0x2D; + case EnemyType::DEL_LILY: + return 0x53; + case EnemyType::DEL_RAPPY: + return 0x57; + case EnemyType::DEL_RAPPY_ALT: + return 0x58; + case EnemyType::DELBITER: + return 0x48; + case EnemyType::DELDEPTH: + return 0x47; + case EnemyType::DELSABER: + return 0x1E; + case EnemyType::DIMENIAN: + return 0x29; + case EnemyType::DOLMDARL: + return 0x41; + case EnemyType::DOLMOLM: + return 0x40; + case EnemyType::DORPHON: + return 0x50; + case EnemyType::DORPHON_ECLAIR: + return 0x51; + case EnemyType::DRAGON: + return 0x2C; + case EnemyType::DUBCHIC: + return 0x18; + case EnemyType::EGG_RAPPY: + return 0x51; + case EnemyType::EPSILON: + return 0x54; + case EnemyType::EVIL_SHARK: + return 0x10; + case EnemyType::GAL_GRYPHON: + return 0x4D; + case EnemyType::GARANZ: + return 0x19; + case EnemyType::GEE: + return 0x36; + case EnemyType::GI_GUE: + return 0x37; + case EnemyType::GIBBLES: + return 0x3D; + case EnemyType::GIGOBOOMA: + return 0x0B; + case EnemyType::GILLCHIC: + return 0x32; + case EnemyType::GIRTABLULU: + return 0x48; + case EnemyType::GOBOOMA: + return 0x0A; + case EnemyType::GOL_DRAGON: + return 0x4C; + case EnemyType::GORAN: + return 0x52; + case EnemyType::GORAN_DETONATOR: + return 0x53; + case EnemyType::GRASS_ASSASSIN: + return 0x0C; + case EnemyType::GUIL_SHARK: + return 0x12; + case EnemyType::HALLO_RAPPY: + return 0x50; + case EnemyType::HIDOOM: + return 0x17; + case EnemyType::HILDEBEAR: + return 0x01; + case EnemyType::HILDEBLUE: + return 0x02; + case EnemyType::ILL_GILL: + return 0x52; + case EnemyType::KONDRIEU: + return 0x5B; + case EnemyType::LA_DIMENIAN: + return 0x2A; + case EnemyType::LOVE_RAPPY: + return 0x33; + case EnemyType::MERICAROL: + return 0x38; + case EnemyType::MERICUS: + return 0x3A; + case EnemyType::MERIKLE: + return 0x39; + case EnemyType::MERILLIA: + return 0x34; + case EnemyType::MERILTAS: + return 0x35; + case EnemyType::MERISSA_A: + return 0x46; + case EnemyType::MERISSA_AA: + return 0x47; + case EnemyType::MIGIUM: + return 0x16; + case EnemyType::MONEST: + return 0x04; + case EnemyType::MORFOS: + return 0x42; + case EnemyType::MOTHMANT: + return 0x03; + case EnemyType::NANO_DRAGON: + return 0x0F; + case EnemyType::NAR_LILY: + return 0x0E; + case EnemyType::OLGA_FLOW_2: + return 0x4E; + case EnemyType::PAL_SHARK: + return 0x11; + case EnemyType::PAN_ARMS: + return 0x15; + case EnemyType::PAZUZU: + return 0x4B; + case EnemyType::PAZUZU_ALT: + return 0x4C; + case EnemyType::POFUILLY_SLIME: + return 0x13; + case EnemyType::POUILLY_SLIME: + return 0x14; + case EnemyType::POISON_LILY: + return 0x0D; + case EnemyType::PYRO_GORAN: + return 0x54; + case EnemyType::RAG_RAPPY: + return 0x05; + case EnemyType::RECOBOX: + return 0x43; + case EnemyType::RECON: + return 0x44; + case EnemyType::SAINT_RAPPY: + return 0x4F; + case EnemyType::SAINT_MILLION: + return 0x59; + case EnemyType::SAND_RAPPY: + return 0x55; + case EnemyType::SAND_RAPPY_ALT: + return 0x56; + case EnemyType::SATELLITE_LIZARD: + return 0x44; + case EnemyType::SATELLITE_LIZARD_ALT: + return 0x45; + case EnemyType::SAVAGE_WOLF: + return 0x07; + case EnemyType::SHAMBERTIN: + return 0x5A; + case EnemyType::SINOW_BEAT: + return 0x1A; + case EnemyType::SINOW_BERILL: + return 0x3E; + case EnemyType::SINOW_GOLD: + return 0x1B; + case EnemyType::SINOW_SPIGELL: + return 0x3F; + case EnemyType::SINOW_ZELE: + return 0x46; + case EnemyType::SINOW_ZOA: + return 0x45; + case EnemyType::SO_DIMENIAN: + return 0x2B; + case EnemyType::UL_GIBBON: + return 0x3B; + case EnemyType::VOL_OPT_2: + return 0x2E; + case EnemyType::YOWIE: + return 0x42; + case EnemyType::YOWIE_ALT: + return 0x43; + case EnemyType::ZE_BOOTA: + return 0x4E; + case EnemyType::ZOL_GIBBON: + return 0x3C; + case EnemyType::ZU: + return 0x49; + case EnemyType::ZU_ALT: + return 0x4A; + default: + throw runtime_error(string_printf("%s does not have a rare table entry", name_for_enum(enemy_type))); + } +} + +const vector& enemy_types_for_rare_table_index(Episode episode, uint8_t rt_index) { + const auto& generate_table = +[](Episode episode) -> vector> { + vector> ret; + for (size_t z = 0; z < static_cast(EnemyType::MAX_ENEMY_TYPE); z++) { + EnemyType t = static_cast(z); + try { + uint8_t rt_index = rare_table_index_for_enemy_type(t); + if (enemy_type_valid_for_episode(episode, t)) { + if (rt_index >= ret.size()) { + ret.resize(rt_index + 1); + } + ret[rt_index].emplace_back(t); + } + } catch (const exception&) { + } + } + return ret; + }; + + static array>, 5> data; + auto& ret = data.at(static_cast(episode)); + if (ret.empty()) { + ret = generate_table(episode); + } + try { + return ret.at(rt_index); + } catch (const out_of_range&) { + static const vector empty_vec; + return empty_vec; + } +} + +bool enemy_type_is_rare(EnemyType type) { + return ((type == EnemyType::HILDEBLUE) || + (type == EnemyType::AL_RAPPY) || + (type == EnemyType::NAR_LILY) || + (type == EnemyType::POUILLY_SLIME) || + (type == EnemyType::MERISSA_AA) || + (type == EnemyType::PAZUZU_ALT) || + (type == EnemyType::DORPHON_ECLAIR) || + (type == EnemyType::KONDRIEU)); +} diff --git a/src/EnemyType.hh b/src/EnemyType.hh index a02b5e35..49f08d5d 100644 --- a/src/EnemyType.hh +++ b/src/EnemyType.hh @@ -1,149 +1,149 @@ -#pragma once - -#include - -#include - -#include "StaticGameData.hh" - -enum class EnemyType { - UNKNOWN = -1, - NONE = 0, - NON_ENEMY_NPC, - AL_RAPPY, - ASTARK, - BA_BOOTA, - BARBA_RAY, - BARBAROUS_WOLF, - BEE_L, - BEE_R, - BOOMA, - BOOTA, - BULCLAW, - BULK, - CANADINE, - CANADINE_GROUP, - CANANE, - CHAOS_BRINGER, - CHAOS_SORCERER, - CLAW, - DARK_BELRA, - DARK_FALZ_1, - DARK_FALZ_2, - DARK_FALZ_3, - DARK_GUNNER, - DARVANT, - DARVANT_ULTIMATE, - DE_ROL_LE, - DE_ROL_LE_BODY, - DE_ROL_LE_MINE, - DEATH_GUNNER, - DEL_LILY, - DEL_RAPPY, - DEL_RAPPY_ALT, - DELBITER, - DELDEPTH, - DELSABER, - DIMENIAN, - DOLMDARL, - DOLMOLM, - DORPHON, - DORPHON_ECLAIR, - DRAGON, - DUBCHIC, - DUBWITCH, // Has no entry in battle params - EGG_RAPPY, - EPSIGUARD, - EPSILON, - EVIL_SHARK, - GAEL, - GAL_GRYPHON, - GARANZ, - GEE, - GI_GUE, - GIBBLES, - GIGOBOOMA, - GILLCHIC, - GIRTABLULU, - GOBOOMA, - GOL_DRAGON, - GORAN, - GORAN_DETONATOR, - GRASS_ASSASSIN, - GUIL_SHARK, - HALLO_RAPPY, - HIDOOM, - HILDEBEAR, - HILDEBLUE, - ILL_GILL, - KONDRIEU, - LA_DIMENIAN, - LOVE_RAPPY, - MERICAROL, - MERICUS, - MERIKLE, - MERILLIA, - MERILTAS, - MERISSA_A, - MERISSA_AA, - MIGIUM, - MONEST, - MORFOS, - MOTHMANT, - NANO_DRAGON, - NAR_LILY, - OLGA_FLOW_1, - OLGA_FLOW_2, - PAL_SHARK, - PAN_ARMS, - PAZUZU, - PAZUZU_ALT, - PIG_RAY, - POFUILLY_SLIME, - POUILLY_SLIME, - POISON_LILY, - PYRO_GORAN, - RAG_RAPPY, - RECOBOX, - RECON, - SAINT_MILLION, - SAINT_RAPPY, - SAND_RAPPY, - SAND_RAPPY_ALT, - SATELLITE_LIZARD, - SATELLITE_LIZARD_ALT, - SAVAGE_WOLF, - SHAMBERTIN, - SINOW_BEAT, - SINOW_BERILL, - SINOW_GOLD, - SINOW_SPIGELL, - SINOW_ZELE, - SINOW_ZOA, - SO_DIMENIAN, - UL_GIBBON, - VOL_OPT_1, - VOL_OPT_2, - VOL_OPT_AMP, - VOL_OPT_CORE, - VOL_OPT_MONITOR, - VOL_OPT_PILLAR, - YOWIE, - YOWIE_ALT, - ZE_BOOTA, - ZOL_GIBBON, - ZU, - ZU_ALT, - MAX_ENEMY_TYPE, -}; - -template <> -const char* name_for_enum(EnemyType type); -template <> -EnemyType enum_for_name(const char* name); - -bool enemy_type_valid_for_episode(Episode episode, EnemyType enemy_type); -uint8_t battle_param_index_for_enemy_type(Episode episode, EnemyType enemy_type); -uint8_t rare_table_index_for_enemy_type(EnemyType enemy_type); -const std::vector& enemy_types_for_rare_table_index(Episode episode, uint8_t rt_index); -bool enemy_type_is_rare(EnemyType type); +#pragma once + +#include + +#include + +#include "StaticGameData.hh" + +enum class EnemyType { + UNKNOWN = -1, + NONE = 0, + NON_ENEMY_NPC, + AL_RAPPY, + ASTARK, + BA_BOOTA, + BARBA_RAY, + BARBAROUS_WOLF, + BEE_L, + BEE_R, + BOOMA, + BOOTA, + BULCLAW, + BULK, + CANADINE, + CANADINE_GROUP, + CANANE, + CHAOS_BRINGER, + CHAOS_SORCERER, + CLAW, + DARK_BELRA, + DARK_FALZ_1, + DARK_FALZ_2, + DARK_FALZ_3, + DARK_GUNNER, + DARVANT, + DARVANT_ULTIMATE, + DE_ROL_LE, + DE_ROL_LE_BODY, + DE_ROL_LE_MINE, + DEATH_GUNNER, + DEL_LILY, + DEL_RAPPY, + DEL_RAPPY_ALT, + DELBITER, + DELDEPTH, + DELSABER, + DIMENIAN, + DOLMDARL, + DOLMOLM, + DORPHON, + DORPHON_ECLAIR, + DRAGON, + DUBCHIC, + DUBWITCH, // Has no entry in battle params + EGG_RAPPY, + EPSIGUARD, + EPSILON, + EVIL_SHARK, + GAEL, + GAL_GRYPHON, + GARANZ, + GEE, + GI_GUE, + GIBBLES, + GIGOBOOMA, + GILLCHIC, + GIRTABLULU, + GOBOOMA, + GOL_DRAGON, + GORAN, + GORAN_DETONATOR, + GRASS_ASSASSIN, + GUIL_SHARK, + HALLO_RAPPY, + HIDOOM, + HILDEBEAR, + HILDEBLUE, + ILL_GILL, + KONDRIEU, + LA_DIMENIAN, + LOVE_RAPPY, + MERICAROL, + MERICUS, + MERIKLE, + MERILLIA, + MERILTAS, + MERISSA_A, + MERISSA_AA, + MIGIUM, + MONEST, + MORFOS, + MOTHMANT, + NANO_DRAGON, + NAR_LILY, + OLGA_FLOW_1, + OLGA_FLOW_2, + PAL_SHARK, + PAN_ARMS, + PAZUZU, + PAZUZU_ALT, + PIG_RAY, + POFUILLY_SLIME, + POUILLY_SLIME, + POISON_LILY, + PYRO_GORAN, + RAG_RAPPY, + RECOBOX, + RECON, + SAINT_MILLION, + SAINT_RAPPY, + SAND_RAPPY, + SAND_RAPPY_ALT, + SATELLITE_LIZARD, + SATELLITE_LIZARD_ALT, + SAVAGE_WOLF, + SHAMBERTIN, + SINOW_BEAT, + SINOW_BERILL, + SINOW_GOLD, + SINOW_SPIGELL, + SINOW_ZELE, + SINOW_ZOA, + SO_DIMENIAN, + UL_GIBBON, + VOL_OPT_1, + VOL_OPT_2, + VOL_OPT_AMP, + VOL_OPT_CORE, + VOL_OPT_MONITOR, + VOL_OPT_PILLAR, + YOWIE, + YOWIE_ALT, + ZE_BOOTA, + ZOL_GIBBON, + ZU, + ZU_ALT, + MAX_ENEMY_TYPE, +}; + +template <> +const char* name_for_enum(EnemyType type); +template <> +EnemyType enum_for_name(const char* name); + +bool enemy_type_valid_for_episode(Episode episode, EnemyType enemy_type); +uint8_t battle_param_index_for_enemy_type(Episode episode, EnemyType enemy_type); +uint8_t rare_table_index_for_enemy_type(EnemyType enemy_type); +const std::vector& enemy_types_for_rare_table_index(Episode episode, uint8_t rt_index); +bool enemy_type_is_rare(EnemyType type); diff --git a/src/FunctionCompiler.cc b/src/FunctionCompiler.cc index 8bfa2188..cd454977 100644 --- a/src/FunctionCompiler.cc +++ b/src/FunctionCompiler.cc @@ -1,528 +1,528 @@ -#include "FunctionCompiler.hh" - -#include -#include - -#include -#include -#include -#include - -#ifdef HAVE_RESOURCE_FILE -#include -#include -#include -#endif - -#include "CommandFormats.hh" -#include "Compression.hh" -#include "Loggers.hh" - -using namespace std; - -static bool is_function_compiler_available = true; - -bool function_compiler_available() { -#ifndef HAVE_RESOURCE_FILE - return false; -#else - return is_function_compiler_available; -#endif -} - -void set_function_compiler_available(bool is_available) { - is_function_compiler_available = is_available; -} - -const char* name_for_architecture(CompiledFunctionCode::Architecture arch) { - switch (arch) { - case CompiledFunctionCode::Architecture::POWERPC: - return "PowerPC"; - case CompiledFunctionCode::Architecture::X86: - return "x86"; - case CompiledFunctionCode::Architecture::SH4: - return "SH-4"; - default: - throw logic_error("invalid architecture"); - } -} - -template -string CompiledFunctionCode::generate_client_command_t( - const unordered_map& label_writes, - const void* suffix_data, - size_t suffix_size, - uint32_t override_relocations_offset) const { - FooterT footer; - footer.num_relocations = this->relocation_deltas.size(); - footer.unused1.clear(0); - footer.entrypoint_addr_offset = this->entrypoint_offset_offset; - footer.unused2.clear(0); - - StringWriter w; - if (!label_writes.empty()) { - string modified_code = this->code; - for (const auto& it : label_writes) { - size_t offset = this->label_offsets.at(it.first); - if (offset > modified_code.size() - 4) { - throw runtime_error("label out of range"); - } - *reinterpret_cast(modified_code.data() + offset) = it.second; - } - w.write(modified_code); - } else { - w.write(this->code); - } - if (suffix_size) { - w.write(suffix_data, suffix_size); - } - while (w.size() & 3) { - w.put_u8(0); - } - - footer.relocations_offset = w.size(); - - // Always write at least 4 bytes even if there are no relocations - if (this->relocation_deltas.empty()) { - w.put_u32(0); - } - - if (override_relocations_offset) { - footer.relocations_offset = override_relocations_offset; - } else { - for (uint16_t delta : this->relocation_deltas) { - w.put(delta); - } - if (this->relocation_deltas.size() & 1) { - w.put_u16(0); - } - } - - w.put(footer); - return std::move(w.str()); -} - -string CompiledFunctionCode::generate_client_command( - const unordered_map& label_writes, - const void* suffix_data, - size_t suffix_size, - uint32_t override_relocations_offset) const { - if (this->arch == Architecture::POWERPC) { - return this->generate_client_command_t( - label_writes, suffix_data, suffix_size, override_relocations_offset); - } else if ((this->arch == Architecture::X86) || (this->arch == Architecture::SH4)) { - return this->generate_client_command_t( - label_writes, suffix_data, suffix_size, override_relocations_offset); - } else { - throw logic_error("invalid architecture"); - } -} - -bool CompiledFunctionCode::is_big_endian() const { - return this->arch == Architecture::POWERPC; -} - -shared_ptr compile_function_code( - CompiledFunctionCode::Architecture arch, - const string& function_directory, - const string& system_directory, - const string& name, - const string& text) { -#ifndef HAVE_RESOURCE_FILE - (void)arch; - (void)function_directory; - (void)system_directory; - (void)name; - (void)text; - throw runtime_error("function compiler is not available"); - -#else - auto ret = make_shared(); - ret->arch = arch; - ret->short_name = name; - ret->index = 0; - ret->hide_from_patches_menu = false; - - unordered_set get_include_stack; - function get_include = [&](const string& name) -> string { - const char* arch_name_token; - switch (arch) { - case CompiledFunctionCode::Architecture::POWERPC: - arch_name_token = "ppc"; - break; - case CompiledFunctionCode::Architecture::X86: - arch_name_token = "x86"; - break; - case CompiledFunctionCode::Architecture::SH4: - arch_name_token = "sh4"; - break; - default: - throw runtime_error("unknown architecture"); - } - - // Look in the function directory first, then the system directory - string asm_filename = string_printf("%s/%s.%s.inc.s", function_directory.c_str(), name.c_str(), arch_name_token); - if (!isfile(asm_filename)) { - asm_filename = string_printf("%s/%s.%s.inc.s", system_directory.c_str(), name.c_str(), arch_name_token); - } - if (isfile(asm_filename)) { - if (!get_include_stack.emplace(name).second) { - throw runtime_error("mutual recursion between includes: " + name); - } - EmulatorBase::AssembleResult ret; - switch (arch) { - case CompiledFunctionCode::Architecture::POWERPC: - ret = PPC32Emulator::assemble(load_file(asm_filename), get_include); - break; - case CompiledFunctionCode::Architecture::X86: - ret = X86Emulator::assemble(load_file(asm_filename), get_include); - break; - case CompiledFunctionCode::Architecture::SH4: - ret = SH4Emulator::assemble(load_file(asm_filename), get_include); - break; - default: - throw runtime_error("unknown architecture"); - } - get_include_stack.erase(name); - return ret.code; - } - - string bin_filename = function_directory + "/" + name + ".inc.bin"; - if (isfile(bin_filename)) { - return load_file(bin_filename); - } - bin_filename = system_directory + "/" + name + ".inc.bin"; - if (isfile(bin_filename)) { - return load_file(bin_filename); - } - throw runtime_error("data not found for include: " + name + " (from " + asm_filename + " or " + bin_filename + ")"); - }; - - EmulatorBase::AssembleResult assembled; - if (arch == CompiledFunctionCode::Architecture::POWERPC) { - assembled = PPC32Emulator::assemble(text, get_include); - } else if (arch == CompiledFunctionCode::Architecture::X86) { - assembled = X86Emulator::assemble(text, get_include); - } else if (arch == CompiledFunctionCode::Architecture::SH4) { - assembled = SH4Emulator::assemble(text, get_include); - } else { - throw runtime_error("invalid architecture"); - } - ret->code = std::move(assembled.code); - ret->label_offsets = std::move(assembled.label_offsets); - for (const auto& it : assembled.metadata_keys) { - if (it.first == "hide_from_patches_menu") { - ret->hide_from_patches_menu = true; - } else if (it.first == "index") { - if (it.second.size() != 1) { - throw runtime_error("invalid index value in .meta directive"); - } - ret->index = it.second[0]; - } else if (it.first == "name") { - ret->long_name = it.second; - } else if (it.first == "description") { - ret->description = it.second; - } else { - throw runtime_error("unknown metadata key: " + it.first); - } - } - - set reloc_indexes; - for (const auto& it : ret->label_offsets) { - if (starts_with(it.first, "reloc")) { - reloc_indexes.emplace(it.second / 4); - } - } - - try { - ret->entrypoint_offset_offset = ret->label_offsets.at("entry_ptr"); - } catch (const out_of_range&) { - throw runtime_error("code does not contain entry_ptr label"); - } - - uint32_t prev_index = 0; - for (const auto& it : reloc_indexes) { - uint32_t delta = it - prev_index; - if (delta > 0xFFFF) { - throw runtime_error("relocation delta too far away"); - } - ret->relocation_deltas.emplace_back(delta); - prev_index = it; - } - - return ret; -#endif -} - -FunctionCodeIndex::FunctionCodeIndex(const string& directory) { - if (!function_compiler_available()) { - function_compiler_log.info("Function compiler is not available"); - return; - } - - string system_dir_path = ends_with(directory, "/") ? (directory + "System") : (directory + "/System"); - - uint32_t next_menu_item_id = 1; - for (const auto& subdir_name : list_directory_sorted(directory)) { - string subdir_path = ends_with(directory, "/") ? (directory + subdir_name) : (directory + "/" + subdir_name); - if (!isdir(subdir_path)) { - function_compiler_log.warning("Skipping %s (not a directory)", subdir_name.c_str()); - continue; - } - - for (const auto& filename : list_directory_sorted(subdir_path)) { - try { - if (!ends_with(filename, ".s")) { - continue; - } - - string name = filename.substr(0, filename.size() - 2); - if (ends_with(name, ".inc")) { - continue; - } - - bool is_patch = ends_with(name, ".patch"); - if (is_patch) { - name.resize(name.size() - 6); - } - - // Figure out the version or specific_version - CompiledFunctionCode::Architecture arch = CompiledFunctionCode::Architecture::UNKNOWN; - uint32_t specific_version = 0; - string short_name = name; - if (ends_with(name, ".ppc")) { - arch = CompiledFunctionCode::Architecture::POWERPC; - name.resize(name.size() - 4); - short_name = name; - } else if (ends_with(name, ".x86")) { - arch = CompiledFunctionCode::Architecture::X86; - name.resize(name.size() - 4); - short_name = name; - } else if (ends_with(name, ".sh4")) { - arch = CompiledFunctionCode::Architecture::SH4; - name.resize(name.size() - 4); - short_name = name; - } else if (is_patch && (name.size() >= 5) && (name[name.size() - 5] == '.')) { - specific_version = (name[name.size() - 4] << 24) | (name[name.size() - 3] << 16) | (name[name.size() - 2] << 8) | name[name.size() - 1]; - if (specific_version_is_dc(specific_version)) { - arch = CompiledFunctionCode::Architecture::SH4; - } else if (specific_version_is_gc(specific_version)) { - arch = CompiledFunctionCode::Architecture::POWERPC; - } else if (specific_version_is_xb(specific_version) || specific_version_is_bb(specific_version)) { - arch = CompiledFunctionCode::Architecture::X86; - } else { - throw runtime_error("unable to determine architecture from specific_version"); - } - short_name = name.substr(0, name.size() - 5); - } - - if (arch == CompiledFunctionCode::Architecture::UNKNOWN) { - throw runtime_error("unable to determine architecture"); - } - - string path = subdir_path + "/" + filename; - string text = load_file(path); - auto code = compile_function_code(arch, subdir_path, system_dir_path, name, text); - if (code->index != 0) { - if (!this->index_to_function.emplace(code->index, code).second) { - throw runtime_error(string_printf( - "duplicate function index: %08" PRIX32, code->index)); - } - } - code->specific_version = specific_version; - code->source_path = path; - code->short_name = short_name; - this->name_to_function.emplace(name, code); - if (is_patch) { - code->menu_item_id = next_menu_item_id++; - this->menu_item_id_and_specific_version_to_patch_function.emplace( - static_cast(code->menu_item_id) << 32 | specific_version, code); - this->name_and_specific_version_to_patch_function.emplace( - string_printf("%s-%08" PRIX32, short_name.c_str(), specific_version), code); - } - - string index_prefix = code->index ? string_printf("%02X => ", code->index) : ""; - string patch_prefix = is_patch ? string_printf("[%08" PRIX32 "/%08" PRIX32 "] ", code->menu_item_id, code->specific_version) : ""; - function_compiler_log.info("Compiled function %s%s%s (%s)", - index_prefix.c_str(), patch_prefix.c_str(), name.c_str(), name_for_architecture(code->arch)); - - } catch (const exception& e) { - function_compiler_log.warning("Failed to compile function %s: %s", filename.c_str(), e.what()); - } - } - } -} - -shared_ptr FunctionCodeIndex::patch_menu(uint32_t specific_version) const { - auto suffix = string_printf("-%08" PRIX32, specific_version); - - auto ret = make_shared(MenuID::PATCHES, "Patches"); - ret->items.emplace_back(PatchesMenuItemID::GO_BACK, "Go back", "Return to the\nmain menu", 0); - for (const auto& it : this->name_and_specific_version_to_patch_function) { - const auto& fn = it.second; - if (fn->hide_from_patches_menu || !ends_with(it.first, suffix)) { - continue; - } - ret->items.emplace_back( - fn->menu_item_id, - fn->long_name.empty() ? fn->short_name : fn->long_name, - fn->description, - MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL); - } - return ret; -} - -shared_ptr FunctionCodeIndex::patch_switches_menu( - uint32_t specific_version, const std::unordered_set& auto_patches_enabled) const { - auto suffix = string_printf("-%08" PRIX32, specific_version); - - auto ret = make_shared(MenuID::PATCH_SWITCHES, "Patch switches"); - ret->items.emplace_back(PatchesMenuItemID::GO_BACK, "Go back", "Return to the\nmain menu", 0); - for (const auto& it : this->name_and_specific_version_to_patch_function) { - const auto& fn = it.second; - if (fn->hide_from_patches_menu || !ends_with(it.first, suffix)) { - continue; - } - string name; - name.push_back(auto_patches_enabled.count(fn->short_name) ? '*' : '-'); - name += fn->long_name.empty() ? fn->short_name : fn->long_name; - ret->items.emplace_back(fn->menu_item_id, name, fn->description, MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL); - } - return ret; -} - -bool FunctionCodeIndex::patch_menu_empty(uint32_t specific_version) const { - uint32_t mask = specific_version_is_indeterminate(specific_version) ? 0xFF000000 : 0xFFFFFFFF; - for (const auto& it : this->menu_item_id_and_specific_version_to_patch_function) { - if ((it.first & mask) == (specific_version & mask)) { - return false; - } - } - return true; -} - -std::shared_ptr FunctionCodeIndex::get_patch( - const std::string& name, uint32_t specific_version) const { - return this->name_and_specific_version_to_patch_function.at( - string_printf("%s-%08" PRIX32, name.c_str(), specific_version)); -} - -DOLFileIndex::DOLFileIndex(const string& directory) { - if (!function_compiler_available()) { - function_compiler_log.info("Function compiler is not available"); - return; - } - if (!isdir(directory)) { - function_compiler_log.info("DOL file directory is missing"); - return; - } - - auto menu = make_shared(MenuID::PROGRAMS, "Programs"); - this->menu = menu; - menu->items.emplace_back(ProgramsMenuItemID::GO_BACK, "Go back", "Return to the\nmain menu", 0); - - uint32_t next_menu_item_id = 0; - for (const auto& filename : list_directory_sorted(directory)) { - bool is_dol = ends_with(filename, ".dol"); - bool is_compressed_dol = ends_with(filename, ".dol.prs"); - if (!is_dol && !is_compressed_dol) { - continue; - } - string name = filename.substr(0, filename.size() - (is_compressed_dol ? 8 : 4)); - - try { - auto dol = make_shared(); - dol->menu_item_id = next_menu_item_id++; - dol->name = name; - - string path = directory + "/" + filename; - string file_data = load_file(path); - - string description; - if (is_compressed_dol) { - size_t decompressed_size = prs_decompress_size(file_data); - - StringWriter w; - w.put_u32b(file_data.size()); - w.put_u32b(decompressed_size); - w.write(file_data); - while (w.size() & 3) { - w.put_u8(0); - } - dol->data = std::move(w.str()); - - string compressed_size_str = format_size(file_data.size()); - string decompressed_size_str = format_size(decompressed_size); - function_compiler_log.info("Loaded compressed DOL file %s (%s -> %s)", - dol->name.c_str(), compressed_size_str.c_str(), decompressed_size_str.c_str()); - description = string_printf("$C6%s$C7\n%s\n%s (orig)", - dol->name.c_str(), compressed_size_str.c_str(), decompressed_size_str.c_str()); - - } else { - StringWriter w; - w.put_u32b(0); - w.put_u32b(file_data.size()); - w.write(file_data); - while (w.size() & 3) { - w.put_u8(0); - } - dol->data = std::move(w.str()); - - string size_str = format_size(dol->data.size()); - function_compiler_log.info("Loaded DOL file %s (%s)", filename.c_str(), size_str.c_str()); - description = string_printf("$C6%s$C7\n%s", dol->name.c_str(), size_str.c_str()); - } - - this->name_to_file.emplace(dol->name, dol); - this->item_id_to_file.emplace_back(dol); - - menu->items.emplace_back(dol->menu_item_id, dol->name, description, MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL); - - } catch (const exception& e) { - function_compiler_log.warning("Failed to load DOL file %s: %s", filename.c_str(), e.what()); - } - } -} - -uint32_t specific_version_for_gc_header_checksum(uint32_t header_checksum) { - static unordered_map checksum_to_specific_version; - if (checksum_to_specific_version.empty()) { - struct { - char system_code = 'G'; - char game_code1 = 'P'; - char game_code2; - char region_code; - char developer_code1 = '8'; - char developer_code2 = 'P'; - uint8_t disc_number = 0; - uint8_t version_code; - } __packed__ data; - for (const char* game_code2 = "OS"; *game_code2; game_code2++) { - data.game_code2 = *game_code2; - for (const char* region_code = "JEP"; *region_code; region_code++) { - data.region_code = *region_code; - for (uint8_t version_code = 0; version_code < 8; version_code++) { - data.version_code = version_code; - uint32_t checksum = crc32(&data, sizeof(data)); - uint32_t specific_version = 0x33000030 | (*game_code2 << 16) | (*region_code << 8) | version_code; - if (!checksum_to_specific_version.emplace(checksum, specific_version).second) { - throw logic_error("multiple specific_versions have same header checksum"); - } - } - } - { - // Generate entries for Trial Editions - data.region_code = 'J'; - data.system_code = 'D'; - data.version_code = 0; - uint32_t checksum = crc32(&data, sizeof(data)); - uint32_t specific_version = 0x33004A54 | (*game_code2 << 16); - if (!checksum_to_specific_version.emplace(checksum, specific_version).second) { - throw logic_error("multiple specific_versions have same header checksum"); - } - data.system_code = 'G'; - } - } - } - return checksum_to_specific_version.at(header_checksum); -} +#include "FunctionCompiler.hh" + +#include +#include + +#include +#include +#include +#include + +#ifdef HAVE_RESOURCE_FILE +#include +#include +#include +#endif + +#include "CommandFormats.hh" +#include "Compression.hh" +#include "Loggers.hh" + +using namespace std; + +static bool is_function_compiler_available = true; + +bool function_compiler_available() { +#ifndef HAVE_RESOURCE_FILE + return false; +#else + return is_function_compiler_available; +#endif +} + +void set_function_compiler_available(bool is_available) { + is_function_compiler_available = is_available; +} + +const char* name_for_architecture(CompiledFunctionCode::Architecture arch) { + switch (arch) { + case CompiledFunctionCode::Architecture::POWERPC: + return "PowerPC"; + case CompiledFunctionCode::Architecture::X86: + return "x86"; + case CompiledFunctionCode::Architecture::SH4: + return "SH-4"; + default: + throw logic_error("invalid architecture"); + } +} + +template +string CompiledFunctionCode::generate_client_command_t( + const unordered_map& label_writes, + const void* suffix_data, + size_t suffix_size, + uint32_t override_relocations_offset) const { + FooterT footer; + footer.num_relocations = this->relocation_deltas.size(); + footer.unused1.clear(0); + footer.entrypoint_addr_offset = this->entrypoint_offset_offset; + footer.unused2.clear(0); + + StringWriter w; + if (!label_writes.empty()) { + string modified_code = this->code; + for (const auto& it : label_writes) { + size_t offset = this->label_offsets.at(it.first); + if (offset > modified_code.size() - 4) { + throw runtime_error("label out of range"); + } + *reinterpret_cast(modified_code.data() + offset) = it.second; + } + w.write(modified_code); + } else { + w.write(this->code); + } + if (suffix_size) { + w.write(suffix_data, suffix_size); + } + while (w.size() & 3) { + w.put_u8(0); + } + + footer.relocations_offset = w.size(); + + // Always write at least 4 bytes even if there are no relocations + if (this->relocation_deltas.empty()) { + w.put_u32(0); + } + + if (override_relocations_offset) { + footer.relocations_offset = override_relocations_offset; + } else { + for (uint16_t delta : this->relocation_deltas) { + w.put(delta); + } + if (this->relocation_deltas.size() & 1) { + w.put_u16(0); + } + } + + w.put(footer); + return std::move(w.str()); +} + +string CompiledFunctionCode::generate_client_command( + const unordered_map& label_writes, + const void* suffix_data, + size_t suffix_size, + uint32_t override_relocations_offset) const { + if (this->arch == Architecture::POWERPC) { + return this->generate_client_command_t( + label_writes, suffix_data, suffix_size, override_relocations_offset); + } else if ((this->arch == Architecture::X86) || (this->arch == Architecture::SH4)) { + return this->generate_client_command_t( + label_writes, suffix_data, suffix_size, override_relocations_offset); + } else { + throw logic_error("invalid architecture"); + } +} + +bool CompiledFunctionCode::is_big_endian() const { + return this->arch == Architecture::POWERPC; +} + +shared_ptr compile_function_code( + CompiledFunctionCode::Architecture arch, + const string& function_directory, + const string& system_directory, + const string& name, + const string& text) { +#ifndef HAVE_RESOURCE_FILE + (void)arch; + (void)function_directory; + (void)system_directory; + (void)name; + (void)text; + throw runtime_error("function compiler is not available"); + +#else + auto ret = make_shared(); + ret->arch = arch; + ret->short_name = name; + ret->index = 0; + ret->hide_from_patches_menu = false; + + unordered_set get_include_stack; + function get_include = [&](const string& name) -> string { + const char* arch_name_token; + switch (arch) { + case CompiledFunctionCode::Architecture::POWERPC: + arch_name_token = "ppc"; + break; + case CompiledFunctionCode::Architecture::X86: + arch_name_token = "x86"; + break; + case CompiledFunctionCode::Architecture::SH4: + arch_name_token = "sh4"; + break; + default: + throw runtime_error("unknown architecture"); + } + + // Look in the function directory first, then the system directory + string asm_filename = string_printf("%s/%s.%s.inc.s", function_directory.c_str(), name.c_str(), arch_name_token); + if (!isfile(asm_filename)) { + asm_filename = string_printf("%s/%s.%s.inc.s", system_directory.c_str(), name.c_str(), arch_name_token); + } + if (isfile(asm_filename)) { + if (!get_include_stack.emplace(name).second) { + throw runtime_error("mutual recursion between includes: " + name); + } + EmulatorBase::AssembleResult ret; + switch (arch) { + case CompiledFunctionCode::Architecture::POWERPC: + ret = PPC32Emulator::assemble(load_file(asm_filename), get_include); + break; + case CompiledFunctionCode::Architecture::X86: + ret = X86Emulator::assemble(load_file(asm_filename), get_include); + break; + case CompiledFunctionCode::Architecture::SH4: + ret = SH4Emulator::assemble(load_file(asm_filename), get_include); + break; + default: + throw runtime_error("unknown architecture"); + } + get_include_stack.erase(name); + return ret.code; + } + + string bin_filename = function_directory + "/" + name + ".inc.bin"; + if (isfile(bin_filename)) { + return load_file(bin_filename); + } + bin_filename = system_directory + "/" + name + ".inc.bin"; + if (isfile(bin_filename)) { + return load_file(bin_filename); + } + throw runtime_error("data not found for include: " + name + " (from " + asm_filename + " or " + bin_filename + ")"); + }; + + EmulatorBase::AssembleResult assembled; + if (arch == CompiledFunctionCode::Architecture::POWERPC) { + assembled = PPC32Emulator::assemble(text, get_include); + } else if (arch == CompiledFunctionCode::Architecture::X86) { + assembled = X86Emulator::assemble(text, get_include); + } else if (arch == CompiledFunctionCode::Architecture::SH4) { + assembled = SH4Emulator::assemble(text, get_include); + } else { + throw runtime_error("invalid architecture"); + } + ret->code = std::move(assembled.code); + ret->label_offsets = std::move(assembled.label_offsets); + for (const auto& it : assembled.metadata_keys) { + if (it.first == "hide_from_patches_menu") { + ret->hide_from_patches_menu = true; + } else if (it.first == "index") { + if (it.second.size() != 1) { + throw runtime_error("invalid index value in .meta directive"); + } + ret->index = it.second[0]; + } else if (it.first == "name") { + ret->long_name = it.second; + } else if (it.first == "description") { + ret->description = it.second; + } else { + throw runtime_error("unknown metadata key: " + it.first); + } + } + + set reloc_indexes; + for (const auto& it : ret->label_offsets) { + if (starts_with(it.first, "reloc")) { + reloc_indexes.emplace(it.second / 4); + } + } + + try { + ret->entrypoint_offset_offset = ret->label_offsets.at("entry_ptr"); + } catch (const out_of_range&) { + throw runtime_error("code does not contain entry_ptr label"); + } + + uint32_t prev_index = 0; + for (const auto& it : reloc_indexes) { + uint32_t delta = it - prev_index; + if (delta > 0xFFFF) { + throw runtime_error("relocation delta too far away"); + } + ret->relocation_deltas.emplace_back(delta); + prev_index = it; + } + + return ret; +#endif +} + +FunctionCodeIndex::FunctionCodeIndex(const string& directory) { + if (!function_compiler_available()) { + function_compiler_log.info("Function compiler is not available"); + return; + } + + string system_dir_path = ends_with(directory, "/") ? (directory + "System") : (directory + "/System"); + + uint32_t next_menu_item_id = 1; + for (const auto& subdir_name : list_directory_sorted(directory)) { + string subdir_path = ends_with(directory, "/") ? (directory + subdir_name) : (directory + "/" + subdir_name); + if (!isdir(subdir_path)) { + function_compiler_log.warning("Skipping %s (not a directory)", subdir_name.c_str()); + continue; + } + + for (const auto& filename : list_directory_sorted(subdir_path)) { + try { + if (!ends_with(filename, ".s")) { + continue; + } + + string name = filename.substr(0, filename.size() - 2); + if (ends_with(name, ".inc")) { + continue; + } + + bool is_patch = ends_with(name, ".patch"); + if (is_patch) { + name.resize(name.size() - 6); + } + + // Figure out the version or specific_version + CompiledFunctionCode::Architecture arch = CompiledFunctionCode::Architecture::UNKNOWN; + uint32_t specific_version = 0; + string short_name = name; + if (ends_with(name, ".ppc")) { + arch = CompiledFunctionCode::Architecture::POWERPC; + name.resize(name.size() - 4); + short_name = name; + } else if (ends_with(name, ".x86")) { + arch = CompiledFunctionCode::Architecture::X86; + name.resize(name.size() - 4); + short_name = name; + } else if (ends_with(name, ".sh4")) { + arch = CompiledFunctionCode::Architecture::SH4; + name.resize(name.size() - 4); + short_name = name; + } else if (is_patch && (name.size() >= 5) && (name[name.size() - 5] == '.')) { + specific_version = (name[name.size() - 4] << 24) | (name[name.size() - 3] << 16) | (name[name.size() - 2] << 8) | name[name.size() - 1]; + if (specific_version_is_dc(specific_version)) { + arch = CompiledFunctionCode::Architecture::SH4; + } else if (specific_version_is_gc(specific_version)) { + arch = CompiledFunctionCode::Architecture::POWERPC; + } else if (specific_version_is_xb(specific_version) || specific_version_is_bb(specific_version)) { + arch = CompiledFunctionCode::Architecture::X86; + } else { + throw runtime_error("unable to determine architecture from specific_version"); + } + short_name = name.substr(0, name.size() - 5); + } + + if (arch == CompiledFunctionCode::Architecture::UNKNOWN) { + throw runtime_error("unable to determine architecture"); + } + + string path = subdir_path + "/" + filename; + string text = load_file(path); + auto code = compile_function_code(arch, subdir_path, system_dir_path, name, text); + if (code->index != 0) { + if (!this->index_to_function.emplace(code->index, code).second) { + throw runtime_error(string_printf( + "duplicate function index: %08" PRIX32, code->index)); + } + } + code->specific_version = specific_version; + code->source_path = path; + code->short_name = short_name; + this->name_to_function.emplace(name, code); + if (is_patch) { + code->menu_item_id = next_menu_item_id++; + this->menu_item_id_and_specific_version_to_patch_function.emplace( + static_cast(code->menu_item_id) << 32 | specific_version, code); + this->name_and_specific_version_to_patch_function.emplace( + string_printf("%s-%08" PRIX32, short_name.c_str(), specific_version), code); + } + + string index_prefix = code->index ? string_printf("%02X => ", code->index) : ""; + string patch_prefix = is_patch ? string_printf("[%08" PRIX32 "/%08" PRIX32 "] ", code->menu_item_id, code->specific_version) : ""; + function_compiler_log.info("Compiled function %s%s%s (%s)", + index_prefix.c_str(), patch_prefix.c_str(), name.c_str(), name_for_architecture(code->arch)); + + } catch (const exception& e) { + function_compiler_log.warning("Failed to compile function %s: %s", filename.c_str(), e.what()); + } + } + } +} + +shared_ptr FunctionCodeIndex::patch_menu(uint32_t specific_version) const { + auto suffix = string_printf("-%08" PRIX32, specific_version); + + auto ret = make_shared(MenuID::PATCHES, "Patches"); + ret->items.emplace_back(PatchesMenuItemID::GO_BACK, "Go back", "Return to the\nmain menu", 0); + for (const auto& it : this->name_and_specific_version_to_patch_function) { + const auto& fn = it.second; + if (fn->hide_from_patches_menu || !ends_with(it.first, suffix)) { + continue; + } + ret->items.emplace_back( + fn->menu_item_id, + fn->long_name.empty() ? fn->short_name : fn->long_name, + fn->description, + MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL); + } + return ret; +} + +shared_ptr FunctionCodeIndex::patch_switches_menu( + uint32_t specific_version, const std::unordered_set& auto_patches_enabled) const { + auto suffix = string_printf("-%08" PRIX32, specific_version); + + auto ret = make_shared(MenuID::PATCH_SWITCHES, "Patch switches"); + ret->items.emplace_back(PatchesMenuItemID::GO_BACK, "Go back", "Return to the\nmain menu", 0); + for (const auto& it : this->name_and_specific_version_to_patch_function) { + const auto& fn = it.second; + if (fn->hide_from_patches_menu || !ends_with(it.first, suffix)) { + continue; + } + string name; + name.push_back(auto_patches_enabled.count(fn->short_name) ? '*' : '-'); + name += fn->long_name.empty() ? fn->short_name : fn->long_name; + ret->items.emplace_back(fn->menu_item_id, name, fn->description, MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL); + } + return ret; +} + +bool FunctionCodeIndex::patch_menu_empty(uint32_t specific_version) const { + uint32_t mask = specific_version_is_indeterminate(specific_version) ? 0xFF000000 : 0xFFFFFFFF; + for (const auto& it : this->menu_item_id_and_specific_version_to_patch_function) { + if ((it.first & mask) == (specific_version & mask)) { + return false; + } + } + return true; +} + +std::shared_ptr FunctionCodeIndex::get_patch( + const std::string& name, uint32_t specific_version) const { + return this->name_and_specific_version_to_patch_function.at( + string_printf("%s-%08" PRIX32, name.c_str(), specific_version)); +} + +DOLFileIndex::DOLFileIndex(const string& directory) { + if (!function_compiler_available()) { + function_compiler_log.info("Function compiler is not available"); + return; + } + if (!isdir(directory)) { + function_compiler_log.info("DOL file directory is missing"); + return; + } + + auto menu = make_shared(MenuID::PROGRAMS, "Programs"); + this->menu = menu; + menu->items.emplace_back(ProgramsMenuItemID::GO_BACK, "Go back", "Return to the\nmain menu", 0); + + uint32_t next_menu_item_id = 0; + for (const auto& filename : list_directory_sorted(directory)) { + bool is_dol = ends_with(filename, ".dol"); + bool is_compressed_dol = ends_with(filename, ".dol.prs"); + if (!is_dol && !is_compressed_dol) { + continue; + } + string name = filename.substr(0, filename.size() - (is_compressed_dol ? 8 : 4)); + + try { + auto dol = make_shared(); + dol->menu_item_id = next_menu_item_id++; + dol->name = name; + + string path = directory + "/" + filename; + string file_data = load_file(path); + + string description; + if (is_compressed_dol) { + size_t decompressed_size = prs_decompress_size(file_data); + + StringWriter w; + w.put_u32b(file_data.size()); + w.put_u32b(decompressed_size); + w.write(file_data); + while (w.size() & 3) { + w.put_u8(0); + } + dol->data = std::move(w.str()); + + string compressed_size_str = format_size(file_data.size()); + string decompressed_size_str = format_size(decompressed_size); + function_compiler_log.info("Loaded compressed DOL file %s (%s -> %s)", + dol->name.c_str(), compressed_size_str.c_str(), decompressed_size_str.c_str()); + description = string_printf("$C6%s$C7\n%s\n%s (orig)", + dol->name.c_str(), compressed_size_str.c_str(), decompressed_size_str.c_str()); + + } else { + StringWriter w; + w.put_u32b(0); + w.put_u32b(file_data.size()); + w.write(file_data); + while (w.size() & 3) { + w.put_u8(0); + } + dol->data = std::move(w.str()); + + string size_str = format_size(dol->data.size()); + function_compiler_log.info("Loaded DOL file %s (%s)", filename.c_str(), size_str.c_str()); + description = string_printf("$C6%s$C7\n%s", dol->name.c_str(), size_str.c_str()); + } + + this->name_to_file.emplace(dol->name, dol); + this->item_id_to_file.emplace_back(dol); + + menu->items.emplace_back(dol->menu_item_id, dol->name, description, MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL); + + } catch (const exception& e) { + function_compiler_log.warning("Failed to load DOL file %s: %s", filename.c_str(), e.what()); + } + } +} + +uint32_t specific_version_for_gc_header_checksum(uint32_t header_checksum) { + static unordered_map checksum_to_specific_version; + if (checksum_to_specific_version.empty()) { + struct { + char system_code = 'G'; + char game_code1 = 'P'; + char game_code2; + char region_code; + char developer_code1 = '8'; + char developer_code2 = 'P'; + uint8_t disc_number = 0; + uint8_t version_code; + } __packed__ data; + for (const char* game_code2 = "OS"; *game_code2; game_code2++) { + data.game_code2 = *game_code2; + for (const char* region_code = "JEP"; *region_code; region_code++) { + data.region_code = *region_code; + for (uint8_t version_code = 0; version_code < 8; version_code++) { + data.version_code = version_code; + uint32_t checksum = crc32(&data, sizeof(data)); + uint32_t specific_version = 0x33000030 | (*game_code2 << 16) | (*region_code << 8) | version_code; + if (!checksum_to_specific_version.emplace(checksum, specific_version).second) { + throw logic_error("multiple specific_versions have same header checksum"); + } + } + } + { + // Generate entries for Trial Editions + data.region_code = 'J'; + data.system_code = 'D'; + data.version_code = 0; + uint32_t checksum = crc32(&data, sizeof(data)); + uint32_t specific_version = 0x33004A54 | (*game_code2 << 16); + if (!checksum_to_specific_version.emplace(checksum, specific_version).second) { + throw logic_error("multiple specific_versions have same header checksum"); + } + data.system_code = 'G'; + } + } + } + return checksum_to_specific_version.at(header_checksum); +} diff --git a/src/FunctionCompiler.hh b/src/FunctionCompiler.hh index 9ecf46fa..3b17e5ff 100644 --- a/src/FunctionCompiler.hh +++ b/src/FunctionCompiler.hh @@ -1,101 +1,101 @@ -#pragma once - -#include - -#include -#include -#include -#include -#include -#include - -#include "Menu.hh" - -bool function_compiler_available(); -void set_function_compiler_available(bool is_available); - -// TODO: Support x86 and SH4 function calls in the future. Currently we only -// support PPC32 because I haven't written an appropriate x86 assembler yet. - -struct CompiledFunctionCode { - enum class Architecture { - UNKNOWN = 0, - POWERPC, // GC - X86, // PC, XB, BB - SH4, // Dreamcast - }; - Architecture arch; - std::string code; - std::vector relocation_deltas; - std::unordered_map label_offsets; - uint32_t entrypoint_offset_offset; - std::string source_path; // Path to source file from newserv root - std::string short_name; // Based on filename - std::string long_name; // From .meta name directive - std::string description; // From .meta description directive - uint8_t index; // 0 = unused (not registered in index_to_function) - uint32_t menu_item_id; - bool hide_from_patches_menu; - uint32_t specific_version; - - bool is_big_endian() const; - - template - std::string generate_client_command_t( - const std::unordered_map& label_writes, - const void* suffix_data = nullptr, - size_t suffix_size = 0, - uint32_t override_relocations_offset = 0) const; - std::string generate_client_command( - const std::unordered_map& label_writes = {}, - const void* suffix_data = nullptr, - size_t suffix_size = 0, - uint32_t override_relocations_offset = 0) const; -}; - -const char* name_for_architecture(CompiledFunctionCode::Architecture arch); - -std::shared_ptr compile_function_code( - CompiledFunctionCode::Architecture arch, - const std::string& directory, - const std::string& name, - const std::string& text); - -struct FunctionCodeIndex { - FunctionCodeIndex() = default; - explicit FunctionCodeIndex(const std::string& directory); - - std::unordered_map> name_to_function; - std::unordered_map> index_to_function; - std::unordered_map> menu_item_id_and_specific_version_to_patch_function; - // Key here is e.g. "PATCHNAME-SPECIFICVERSION", with the latter in hex - std::map> name_and_specific_version_to_patch_function; - - std::shared_ptr patch_menu(uint32_t specific_version) const; - std::shared_ptr patch_switches_menu(uint32_t specific_version, const std::unordered_set& auto_patches_enabled) const; - bool patch_menu_empty(uint32_t specific_version) const; - - std::shared_ptr get_patch(const std::string& name, uint32_t specific_version) const; -}; - -struct DOLFileIndex { - struct File { - uint32_t menu_item_id; - std::string name; - std::string data; - bool is_compressed; - }; - - std::vector> item_id_to_file; - std::unordered_map> name_to_file; - std::shared_ptr menu; - - DOLFileIndex() = default; - explicit DOLFileIndex(const std::string& directory); - - inline bool empty() const { - return this->name_to_file.empty() && this->item_id_to_file.empty(); - } -}; - -uint32_t specific_version_for_gc_header_checksum(uint32_t header_checksum); +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +#include "Menu.hh" + +bool function_compiler_available(); +void set_function_compiler_available(bool is_available); + +// TODO: Support x86 and SH4 function calls in the future. Currently we only +// support PPC32 because I haven't written an appropriate x86 assembler yet. + +struct CompiledFunctionCode { + enum class Architecture { + UNKNOWN = 0, + POWERPC, // GC + X86, // PC, XB, BB + SH4, // Dreamcast + }; + Architecture arch; + std::string code; + std::vector relocation_deltas; + std::unordered_map label_offsets; + uint32_t entrypoint_offset_offset; + std::string source_path; // Path to source file from newserv root + std::string short_name; // Based on filename + std::string long_name; // From .meta name directive + std::string description; // From .meta description directive + uint8_t index; // 0 = unused (not registered in index_to_function) + uint32_t menu_item_id; + bool hide_from_patches_menu; + uint32_t specific_version; + + bool is_big_endian() const; + + template + std::string generate_client_command_t( + const std::unordered_map& label_writes, + const void* suffix_data = nullptr, + size_t suffix_size = 0, + uint32_t override_relocations_offset = 0) const; + std::string generate_client_command( + const std::unordered_map& label_writes = {}, + const void* suffix_data = nullptr, + size_t suffix_size = 0, + uint32_t override_relocations_offset = 0) const; +}; + +const char* name_for_architecture(CompiledFunctionCode::Architecture arch); + +std::shared_ptr compile_function_code( + CompiledFunctionCode::Architecture arch, + const std::string& directory, + const std::string& name, + const std::string& text); + +struct FunctionCodeIndex { + FunctionCodeIndex() = default; + explicit FunctionCodeIndex(const std::string& directory); + + std::unordered_map> name_to_function; + std::unordered_map> index_to_function; + std::unordered_map> menu_item_id_and_specific_version_to_patch_function; + // Key here is e.g. "PATCHNAME-SPECIFICVERSION", with the latter in hex + std::map> name_and_specific_version_to_patch_function; + + std::shared_ptr patch_menu(uint32_t specific_version) const; + std::shared_ptr patch_switches_menu(uint32_t specific_version, const std::unordered_set& auto_patches_enabled) const; + bool patch_menu_empty(uint32_t specific_version) const; + + std::shared_ptr get_patch(const std::string& name, uint32_t specific_version) const; +}; + +struct DOLFileIndex { + struct File { + uint32_t menu_item_id; + std::string name; + std::string data; + bool is_compressed; + }; + + std::vector> item_id_to_file; + std::unordered_map> name_to_file; + std::shared_ptr menu; + + DOLFileIndex() = default; + explicit DOLFileIndex(const std::string& directory); + + inline bool empty() const { + return this->name_to_file.empty() && this->item_id_to_file.empty(); + } +}; + +uint32_t specific_version_for_gc_header_checksum(uint32_t header_checksum); diff --git a/src/HTTPServer.cc b/src/HTTPServer.cc index f5bfa80c..6baf7d68 100644 --- a/src/HTTPServer.cc +++ b/src/HTTPServer.cc @@ -1,1031 +1,1031 @@ -#include "HTTPServer.hh" - -#include -#include -#include -#include -#include - -#include -#include -#include - -#include "EventUtils.hh" -#include "Loggers.hh" -#include "ProxyServer.hh" -#include "Server.hh" - -using namespace std; - -const unordered_map HTTPServer::explanation_for_response_code({ - {100, "Continue"}, - {101, "Switching Protocols"}, - {102, "Processing"}, - {200, "OK"}, - {201, "Created"}, - {202, "Accepted"}, - {203, "Non-Authoritative Information"}, - {204, "No Content"}, - {205, "Reset Content"}, - {206, "Partial Content"}, - {207, "Multi-Status"}, - {208, "Already Reported"}, - {226, "IM Used"}, - {300, "Multiple Choices"}, - {301, "Moved Permanently"}, - {302, "Found"}, - {303, "See Other"}, - {304, "Not Modified"}, - {305, "Use Proxy"}, - {307, "Temporary Redirect"}, - {308, "Permanent Redirect"}, - {400, "Bad Request"}, - {401, "Unathorized"}, - {402, "Payment Required"}, - {403, "Forbidden"}, - {404, "Not Found"}, - {405, "Method Not Allowed"}, - {406, "Not Acceptable"}, - {407, "Proxy Authentication Required"}, - {408, "Request Timeout"}, - {409, "Conflict"}, - {410, "Gone"}, - {411, "Length Required"}, - {412, "Precondition Failed"}, - {413, "Request Entity Too Large"}, - {414, "Request-URI Too Long"}, - {415, "Unsupported Media Type"}, - {416, "Requested Range Not Satisfiable"}, - {417, "Expectation Failed"}, - {418, "I\'m a Teapot"}, - {420, "Enhance Your Calm"}, - {422, "Unprocessable Entity"}, - {423, "Locked"}, - {424, "Failed Dependency"}, - {426, "Upgrade Required"}, - {428, "Precondition Required"}, - {429, "Too Many Requests"}, - {431, "Request Header Fields Too Large"}, - {444, "No Response"}, - {449, "Retry With"}, - {451, "Unavailable For Legal Reasons"}, - {500, "Internal Server Error"}, - {501, "Not Implemented"}, - {502, "Bad Gateway"}, - {503, "Service Unavailable"}, - {504, "Gateway Timeout"}, - {505, "HTTP Version Not Supported"}, - {506, "Variant Also Negotiates"}, - {507, "Insufficient Storage"}, - {508, "Loop Detected"}, - {509, "Bandwidth Limit Exceeded"}, - {510, "Not Extended"}, - {511, "Network Authentication Required"}, - {598, "Network Read Timeout Error"}, - {599, "Network Connect Timeout Error"}, -}); - -HTTPServer::http_error::http_error(int code, const string& what) - : runtime_error(what), - code(code) {} - -void HTTPServer::send_response(struct evhttp_request* req, int code, const char* content_type, struct evbuffer* b) { - struct evkeyvalq* headers = evhttp_request_get_output_headers(req); - evhttp_add_header(headers, "Content-Type", content_type); - evhttp_add_header(headers, "Server", "newserv"); - evhttp_send_reply(req, code, explanation_for_response_code.at(code), b); -} - -void HTTPServer::send_response(struct evhttp_request* req, int code, const char* content_type, const char* fmt, ...) { - unique_ptr out_buffer(evbuffer_new(), evbuffer_free); - va_list va; - va_start(va, fmt); - evbuffer_add_vprintf(out_buffer.get(), fmt, va); - va_end(va); - HTTPServer::send_response(req, code, content_type, out_buffer.get()); -} - -unordered_multimap HTTPServer::parse_url_params(const string& query) { - unordered_multimap params; - if (query.empty()) { - return params; - } - for (auto it : split(query, '&')) { - size_t first_equals = it.find('='); - if (first_equals != string::npos) { - string value(it, first_equals + 1); - - size_t write_offset = 0, read_offset = 0; - for (; read_offset < value.size(); write_offset++) { - if ((value[read_offset] == '%') && (read_offset < value.size() - 2)) { - value[write_offset] = - static_cast(value_for_hex_char(value[read_offset + 1]) << 4) | - static_cast(value_for_hex_char(value[read_offset + 2])); - read_offset += 3; - } else if (value[write_offset] == '+') { - value[write_offset] = ' '; - read_offset++; - } else { - value[write_offset] = value[read_offset]; - read_offset++; - } - } - value.resize(write_offset); - - params.emplace(piecewise_construct, forward_as_tuple(it, 0, first_equals), - forward_as_tuple(value)); - } else { - params.emplace(it, ""); - } - } - return params; -} - -unordered_map HTTPServer::parse_url_params_unique(const string& query) { - unordered_map ret; - for (const auto& it : HTTPServer::parse_url_params(query)) { - ret.emplace(it.first, std::move(it.second)); - } - return ret; -} - -const string& HTTPServer::get_url_param( - const unordered_multimap& params, const string& key, const string* _default) { - - auto range = params.equal_range(key); - if (range.first == range.second) { - if (!_default) { - throw out_of_range("URL parameter " + key + " not present"); - } - return *_default; - } - - return range.first->second; -} - -HTTPServer::HTTPServer(shared_ptr state) - : state(state), - base(event_base_new(), event_base_free), - http(evhttp_new(this->base.get()), evhttp_free), - th(&HTTPServer::thread_fn, this) { - evhttp_set_gencb(this->http.get(), this->dispatch_handle_request, this); -} - -void HTTPServer::listen(const string& socket_path) { - int fd = ::listen(socket_path, 0, SOMAXCONN); - server_log.info("Listening on Unix socket %s on fd %d (HTTP)", socket_path.c_str(), fd); - this->add_socket(fd); -} - -void HTTPServer::listen(const string& addr, int port) { - if (port == 0) { - this->listen(addr); - } else { - int fd = ::listen(addr, port, SOMAXCONN); - string netloc_str = render_netloc(addr, port); - server_log.info("Listening on TCP interface %s on fd %d (HTTP)", netloc_str.c_str(), fd); - this->add_socket(fd); - } -} - -void HTTPServer::listen(int port) { - this->listen("", port); -} - -void HTTPServer::add_socket(int fd) { - evhttp_accept_socket(this->http.get(), fd); -} - -void HTTPServer::schedule_stop() { - event_base_loopexit(this->base.get(), nullptr); -} - -void HTTPServer::wait_for_stop() { - this->th.join(); -} - -void HTTPServer::dispatch_handle_request(struct evhttp_request* req, void* ctx) { - reinterpret_cast(ctx)->handle_request(req); -} - -JSON HTTPServer::generate_quest_json_st(shared_ptr q) { - if (!q) { - return nullptr; - } - auto battle_rules_json = q->battle_rules ? q->battle_rules->json() : nullptr; - auto challenge_template_index_json = (q->challenge_template_index >= 0) - ? q->challenge_template_index - : JSON(nullptr); - return JSON::dict({ - {"Number", q->quest_number}, - {"Episode", name_for_episode(q->episode)}, - {"Joinable", q->joinable}, - {"LockStatusRegister", (q->lock_status_register >= 0) ? q->lock_status_register : JSON(nullptr)}, - {"Name", q->name}, - {"BattleRules", std::move(battle_rules_json)}, - {"ChallengeTemplateIndex", std::move(challenge_template_index_json)}, - }); -} - -JSON HTTPServer::generate_client_config_json_st(const Client::Config& config) { - const char* drop_notifications_mode = "unknown"; - switch (config.get_drop_notification_mode()) { - case Client::ItemDropNotificationMode::NOTHING: - drop_notifications_mode = "off"; - break; - case Client::ItemDropNotificationMode::RARES_ONLY: - drop_notifications_mode = "rare"; - break; - case Client::ItemDropNotificationMode::ALL_ITEMS: - drop_notifications_mode = "on"; - break; - case Client::ItemDropNotificationMode::ALL_ITEMS_INCLUDING_MESETA: - drop_notifications_mode = "every"; - break; - } - - auto ret = JSON::dict({ - {"SpecificVersion", config.specific_version}, - {"SwitchAssistEnabled", (config.check_flag(Client::Flag::SWITCH_ASSIST_ENABLED) ? true : false)}, - {"InfiniteHPEnabled", (config.check_flag(Client::Flag::INFINITE_HP_ENABLED) ? true : false)}, - {"InfiniteTPEnabled", (config.check_flag(Client::Flag::INFINITE_TP_ENABLED) ? true : false)}, - {"DropNotificationMode", drop_notifications_mode}, - {"DebugEnabled", (config.check_flag(Client::Flag::DEBUG_ENABLED) ? true : false)}, - {"ProxySaveFilesEnabled", (config.check_flag(Client::Flag::PROXY_SAVE_FILES) ? true : false)}, - {"ProxyChatCommandsEnabled", (config.check_flag(Client::Flag::PROXY_CHAT_COMMANDS_ENABLED) ? true : false)}, - {"ProxyPlayerNotificationsEnabled", (config.check_flag(Client::Flag::PROXY_PLAYER_NOTIFICATIONS_ENABLED) ? true : false)}, - {"ProxySuppressClientPings", (config.check_flag(Client::Flag::PROXY_SUPPRESS_CLIENT_PINGS) ? true : false)}, - {"ProxyEp3InfiniteMesetaEnabled", (config.check_flag(Client::Flag::PROXY_EP3_INFINITE_MESETA_ENABLED) ? true : false)}, - {"ProxyEp3InfiniteTimeEnabled", (config.check_flag(Client::Flag::PROXY_EP3_INFINITE_TIME_ENABLED) ? true : false)}, - {"ProxyBlockFunctionCalls", (config.check_flag(Client::Flag::PROXY_BLOCK_FUNCTION_CALLS) ? true : false)}, - {"ProxyEp3UnmaskWhispers", (config.check_flag(Client::Flag::PROXY_EP3_UNMASK_WHISPERS) ? true : false)}, - }); - ret.emplace("OverrideRandomSeed", config.check_flag(Client::Flag::USE_OVERRIDE_RANDOM_SEED) ? config.override_random_seed : JSON(nullptr)); - ret.emplace("OverrideSectionID", (config.override_section_id != 0xFF) ? config.override_section_id : JSON(nullptr)); - ret.emplace("OverrideLobbyEvent", (config.override_lobby_event != 0xFF) ? config.override_lobby_event : JSON(nullptr)); - ret.emplace("OverrideLobbyNumber", (config.override_lobby_number != 0x80) ? config.override_lobby_number : JSON(nullptr)); - return ret; -} - -JSON HTTPServer::generate_account_json_st(shared_ptr a) { - auto dc_nte_licenses_json = JSON::list(); - for (const auto& it : a->dc_nte_licenses) { - dc_nte_licenses_json.emplace_back(it.first); - } - auto dc_licenses_json = JSON::list(); - for (const auto& it : a->dc_licenses) { - dc_licenses_json.emplace_back(it.first); - } - auto pc_licenses_json = JSON::list(); - for (const auto& it : a->pc_licenses) { - pc_licenses_json.emplace_back(it.first); - } - auto gc_licenses_json = JSON::list(); - for (const auto& it : a->gc_licenses) { - gc_licenses_json.emplace_back(it.first); - } - auto xb_licenses_json = JSON::list(); - for (const auto& it : a->xb_licenses) { - xb_licenses_json.emplace_back(it.first); - } - auto bb_licenses_json = JSON::list(); - for (const auto& it : a->bb_licenses) { - bb_licenses_json.emplace_back(it.first); - } - auto auto_patches_json = JSON::list(); - for (const auto& it : a->auto_patches_enabled) { - auto_patches_json.emplace_back(it); - } - return JSON::dict({ - {"AccountID", a->account_id}, - {"Flags", a->flags}, - {"BanEndTime", a->ban_end_time ? a->ban_end_time : JSON(nullptr)}, - {"Ep3CurrentMeseta", a->ep3_current_meseta}, - {"Ep3TotalMesetaEarned", a->ep3_total_meseta_earned}, - {"BBTeamID", a->bb_team_id}, - {"LastPlayerName", a->last_player_name}, - {"AutoReplyMessage", a->auto_reply_message}, - {"IsTemporary", a->is_temporary}, - {"DCNTELicenses", std::move(dc_nte_licenses_json)}, - {"DCLicenses", std::move(dc_licenses_json)}, - {"PCLicenses", std::move(pc_licenses_json)}, - {"GCLicenses", std::move(gc_licenses_json)}, - {"XBLicenses", std::move(xb_licenses_json)}, - {"BBLicenses", std::move(bb_licenses_json)}, - {"AutoPatchesEnabled", std::move(auto_patches_json)}, - }); -}; - -JSON HTTPServer::generate_game_client_json_st(shared_ptr c, shared_ptr item_name_index) { - auto ret = JSON::dict({ - {"ID", c->id}, - {"RemoteAddress", render_sockaddr_storage(c->channel.remote_addr)}, - {"Version", name_for_enum(c->version())}, - {"SubVersion", c->sub_version}, - {"Config", HTTPServer::generate_client_config_json_st(c->config)}, - {"Language", name_for_language_code(c->language())}, - {"LocationX", c->x}, - {"LocationZ", c->z}, - {"LocationFloor", c->floor}, - {"CanChat", c->can_chat}, - }); - ret.emplace("Account", c->login ? HTTPServer::generate_account_json_st(c->login->account) : JSON(nullptr)); - auto l = c->lobby.lock(); - if (l) { - ret.emplace("LobbyID", l->lobby_id); - ret.emplace("LobbyClientID", c->lobby_client_id); - } - if (c->version() == Version::BB_V4) { - ret.emplace("BBCharacterIndex", c->bb_character_index); - } - auto p = c->character(false, false); - if (p) { - if (!is_ep3(c->version())) { - ret.emplace("InventoryItems", p->inventory.num_items); - if (c->version() != Version::DC_NTE) { - ret.emplace("InventoryLanguage", p->inventory.language); - ret.emplace("NumHPMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::HP)); - ret.emplace("NumTPMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::TP)); - if (!is_v1_or_v2(c->version())) { - ret.emplace("NumPowerMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::POWER)); - ret.emplace("NumDefMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::DEF)); - ret.emplace("NumMindMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::MIND)); - ret.emplace("NumEvadeMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::EVADE)); - ret.emplace("NumLuckMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::LUCK)); - } - } - JSON items_json = JSON::list(); - for (size_t z = 0; z < p->inventory.num_items; z++) { - const auto& item = p->inventory.items[z]; - auto item_dict = JSON::dict({ - {"Flags", item.flags.load()}, - {"Data", item.data.hex()}, - {"ItemID", item.data.id.load()}, - }); - if (item_name_index) { - item_dict.emplace("Description", item_name_index->describe_item(item.data, false)); - } - items_json.emplace_back(std::move(item_dict)); - } - ret.emplace("ATP", p->disp.stats.char_stats.atp.load()); - ret.emplace("MST", p->disp.stats.char_stats.mst.load()); - ret.emplace("EVP", p->disp.stats.char_stats.evp.load()); - ret.emplace("HP", p->disp.stats.char_stats.hp.load()); - ret.emplace("DFP", p->disp.stats.char_stats.dfp.load()); - ret.emplace("ATA", p->disp.stats.char_stats.ata.load()); - ret.emplace("LCK", p->disp.stats.char_stats.lck.load()); - ret.emplace("EXP", p->disp.stats.experience.load()); - ret.emplace("Meseta", p->disp.stats.meseta.load()); - auto tech_levels_json = JSON::dict(); - for (size_t z = 0; z < 0x13; z++) { - auto level = p->get_technique_level(z); - tech_levels_json.emplace(name_for_technique(z), (level != 0xFF) ? level : JSON(nullptr)); - } - ret.emplace("TechniqueLevels", std::move(tech_levels_json)); - } - ret.emplace("Height", p->disp.stats.height.load()); - ret.emplace("Level", p->disp.stats.level.load()); - ret.emplace("NameColor", p->disp.visual.name_color.load()); - ret.emplace("ExtraModel", (p->disp.visual.validation_flags & 2) ? p->disp.visual.extra_model : JSON(nullptr)); - ret.emplace("SectionID", name_for_section_id(p->disp.visual.section_id)); - ret.emplace("CharClass", name_for_char_class(p->disp.visual.section_id)); - ret.emplace("Costume", p->disp.visual.costume.load()); - ret.emplace("Skin", p->disp.visual.skin.load()); - ret.emplace("Face", p->disp.visual.face.load()); - ret.emplace("Head", p->disp.visual.head.load()); - ret.emplace("Hair", p->disp.visual.hair.load()); - ret.emplace("HairR", p->disp.visual.hair_r.load()); - ret.emplace("HairG", p->disp.visual.hair_g.load()); - ret.emplace("HairB", p->disp.visual.hair_b.load()); - ret.emplace("ProportionX", p->disp.visual.proportion_x.load()); - ret.emplace("ProportionY", p->disp.visual.proportion_y.load()); - - ret.emplace("Name", p->disp.name.decode(c->language())); - ret.emplace("PlayTimeSeconds", p->play_time_seconds.load()); - - ret.emplace("AutoReply", p->auto_reply.decode(c->language())); - ret.emplace("InfoBoard", p->info_board.decode(c->language())); - auto battle_place_counts = JSON::list({ - p->battle_records.place_counts[0].load(), - p->battle_records.place_counts[1].load(), - p->battle_records.place_counts[2].load(), - p->battle_records.place_counts[3].load(), - }); - ret.emplace("BattlePlaceCounts", std::move(battle_place_counts)); - ret.emplace("BattleDisconnectCount", p->battle_records.disconnect_count.load()); - - if (!is_ep3(c->version())) { - auto json_for_challenge_times = [](const parray& times) -> JSON { - auto times_json = JSON::list(); - for (size_t z = 0; z < times.size(); z++) { - times_json.emplace_back(times[z].decode()); - } - return times_json; - }; - ret.emplace("ChallengeTitleColorXRGB1555", p->challenge_records.title_color.load()); - ret.emplace("ChallengeTimesEp1Online", json_for_challenge_times(p->challenge_records.times_ep1_online)); - ret.emplace("ChallengeTimesEp2Online", json_for_challenge_times(p->challenge_records.times_ep2_online)); - ret.emplace("ChallengeTimesEp1Offline", json_for_challenge_times(p->challenge_records.times_ep1_offline)); - ret.emplace("ChallengeGraveIsEp2", p->challenge_records.grave_is_ep2 ? true : false); - ret.emplace("ChallengeGraveStageNum", p->challenge_records.grave_stage_num); - ret.emplace("ChallengeGraveFloor", p->challenge_records.grave_floor); - ret.emplace("ChallengeGraveDeaths", p->challenge_records.grave_deaths.load()); - { - uint16_t year = 2000 + ((p->challenge_records.grave_time >> 28) & 0x0F); - uint8_t month = (p->challenge_records.grave_time >> 24) & 0x0F; - uint8_t day = (p->challenge_records.grave_time >> 16) & 0xFF; - uint8_t hour = (p->challenge_records.grave_time >> 8) & 0xFF; - uint8_t minute = p->challenge_records.grave_time & 0xFF; - ret.emplace("ChallengeGraveTime", string_printf("%04hu-%02hhu-%02hhu %02hhu:%02hhu:00", year, month, day, hour, minute)); - } - string grave_enemy_types; - if (p->challenge_records.grave_defeated_by_enemy_rt_index) { - for (EnemyType type : enemy_types_for_rare_table_index(p->challenge_records.grave_is_ep2 ? Episode::EP2 : Episode::EP1, p->challenge_records.grave_defeated_by_enemy_rt_index)) { - if (!grave_enemy_types.empty()) { - grave_enemy_types += "/"; - } - grave_enemy_types += name_for_enum(type); - } - } - ret.emplace("ChallengeGraveDefeatedByEnemy", std::move(grave_enemy_types)); - ret.emplace("ChallengeGraveX", p->challenge_records.grave_x.load()); - ret.emplace("ChallengeGraveY", p->challenge_records.grave_y.load()); - ret.emplace("ChallengeGraveZ", p->challenge_records.grave_z.load()); - ret.emplace("ChallengeGraveTeam", p->challenge_records.grave_team.decode()); - ret.emplace("ChallengeGraveMessage", p->challenge_records.grave_message.decode()); - ret.emplace("ChallengeAwardStateEp1OnlineFlags", p->challenge_records.ep1_online_award_state.rank_award_flags.load()); - ret.emplace("ChallengeAwardStateEp1OnlineMaxRank", p->challenge_records.ep1_online_award_state.maximum_rank.decode()); - ret.emplace("ChallengeAwardStateEp2OnlineFlags", p->challenge_records.ep2_online_award_state.rank_award_flags.load()); - ret.emplace("ChallengeAwardStateEp2OnlineMaxRank", p->challenge_records.ep2_online_award_state.maximum_rank.decode()); - ret.emplace("ChallengeAwardStateEp1OfflineFlags", p->challenge_records.ep1_offline_award_state.rank_award_flags.load()); - ret.emplace("ChallengeAwardStateEp1OfflineMaxRank", p->challenge_records.ep1_offline_award_state.maximum_rank.decode()); - ret.emplace("ChallengeRankTitle", p->challenge_records.rank_title.decode()); - } - } - return ret; -} - -JSON HTTPServer::generate_proxy_client_json_st(shared_ptr ses) { - struct LobbyPlayer { - uint32_t guild_card_number = 0; - uint64_t xb_user_id = 0; - std::string name; - uint8_t language = 0; - uint8_t section_id = 0; - uint8_t char_class = 0; - }; - std::vector lobby_players; - - auto lobby_players_json = JSON::list(); - for (size_t z = 0; z < ses->lobby_players.size(); z++) { - const auto& p = ses->lobby_players[z]; - if (p.guild_card_number) { - lobby_players_json.emplace_back(JSON::dict({ - {"GuildCardNumber", p.guild_card_number}, - {"Name", p.name}, - {"Language", name_for_language_code(p.language)}, - {"SectionID", name_for_section_id(p.section_id)}, - {"CharClass", name_for_char_class(p.char_class)}, - })); - lobby_players_json.back().emplace("XBUserID", p.xb_user_id ? p.xb_user_id : JSON(nullptr)); - } else { - lobby_players_json.emplace_back(nullptr); - } - } - - auto ret = JSON::dict({ - {"ID", ses->id}, - {"RemoteClientAddress", render_sockaddr_storage(ses->client_channel.remote_addr)}, - {"RemoteServerAddress", render_sockaddr_storage(ses->server_channel.remote_addr)}, - {"LocalPort", ses->local_port}, - {"NextDestination", render_sockaddr_storage(ses->next_destination)}, - {"Version", name_for_enum(ses->version())}, - {"SubVersion", ses->sub_version}, - {"Name", ses->character_name}, - {"DCHardwareID", ses->hardware_id}, - {"RemoteGuildCardNumber", ses->remote_guild_card_number}, - {"RemoteClientConfigData", format_data_string(&ses->remote_client_config_data[0], ses->remote_client_config_data.size())}, - {"Config", HTTPServer::generate_client_config_json_st(ses->config)}, - {"Language", name_for_language_code(ses->language())}, - {"LobbyClientID", ses->lobby_client_id}, - {"LeaderClientID", ses->leader_client_id}, - {"LocationX", ses->x}, - {"LocationZ", ses->z}, - {"LocationFloor", ses->floor}, - {"IsInGame", ses->is_in_game}, - {"IsInQuest", ses->is_in_quest}, - {"LobbyEvent", ses->lobby_event}, - {"LobbyDifficulty", name_for_difficulty(ses->lobby_difficulty)}, - {"LobbySectionID", name_for_section_id(ses->lobby_section_id)}, - {"LobbyMode", name_for_mode(ses->lobby_mode)}, - {"LobbyEpisode", name_for_episode(ses->lobby_episode)}, - {"LobbyRandomSeed", ses->lobby_random_seed}, - {"LobbyPlayers", std::move(lobby_players_json)}, - }); - switch (ses->drop_mode) { - case ProxyServer::LinkedSession::DropMode::DISABLED: - ret.emplace("DropMode", "none"); - break; - case ProxyServer::LinkedSession::DropMode::PASSTHROUGH: - ret.emplace("DropMode", "default"); - break; - case ProxyServer::LinkedSession::DropMode::INTERCEPT: - ret.emplace("DropMode", "proxy"); - break; - } - ret.emplace("Account", ses->login ? HTTPServer::generate_account_json_st(ses->login->account) : JSON(nullptr)); - return ret; -} - -JSON HTTPServer::generate_lobby_json_st(shared_ptr l, shared_ptr item_name_index) { - std::array, 12> clients; - - auto client_ids_json = JSON::list(); - for (size_t z = 0; z < l->max_clients; z++) { - client_ids_json.emplace_back(l->clients[z] ? l->clients[z]->id : JSON(nullptr)); - } - - auto ret = JSON::dict({ - {"ID", l->lobby_id}, - {"AllowedVersions", l->allowed_versions}, - {"Event", l->event}, - {"LeaderClientID", l->leader_id}, - {"MaxClients", l->max_clients}, - {"IdleTimeoutUsecs", l->idle_timeout_usecs}, - {"ClientIDs", std::move(client_ids_json)}, - {"IsGame", l->is_game()}, - {"IsPersistent", l->check_flag(Lobby::Flag::PERSISTENT)}, - }); - - if (l->is_game()) { - ret.emplace("CheatsEnabled", l->check_flag(Lobby::Flag::CHEATS_ENABLED)); - ret.emplace("MinLevel", l->min_level + 1); - ret.emplace("MaxLevel", l->max_level + 1); - ret.emplace("BaseVersion", l->base_version); - ret.emplace("Episode", name_for_episode(l->episode)); - ret.emplace("HasPassword", !l->password.empty()); - ret.emplace("Name", l->name); - ret.emplace("RandomSeed", l->random_seed); - if (l->episode != Episode::EP3) { - ret.emplace("QuestSelectionInProgress", l->check_flag(Lobby::Flag::QUEST_SELECTION_IN_PROGRESS)); - ret.emplace("QuestInProgress", l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS)); - ret.emplace("JoinableQuestInProgress", l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)); - auto variations_json = JSON::list(); - for (size_t z = 0; z < l->variations.size(); z++) { - variations_json.emplace_back(l->variations[z].load()); - } - ret.emplace("Variations", std::move(variations_json)); - ret.emplace("SectionID", name_for_section_id(l->effective_section_id())); - ret.emplace("Mode", name_for_mode(l->mode)); - ret.emplace("Difficulty", name_for_difficulty(l->difficulty)); - ret.emplace("BaseEXPMultiplier", l->base_exp_multiplier); - ret.emplace("EXPShareMultiplier", l->exp_share_multiplier); - ret.emplace("AllowedDropModes", l->allowed_drop_modes); - switch (l->drop_mode) { - case Lobby::DropMode::DISABLED: - ret.emplace("DropMode", "none"); - break; - case Lobby::DropMode::CLIENT: - ret.emplace("DropMode", "client"); - break; - case Lobby::DropMode::SERVER_SHARED: - ret.emplace("DropMode", "shared"); - break; - case Lobby::DropMode::SERVER_PRIVATE: - ret.emplace("DropMode", "private"); - break; - case Lobby::DropMode::SERVER_DUPLICATE: - ret.emplace("DropMode", "duplicate"); - break; - } - if (l->mode == GameMode::CHALLENGE) { - ret.emplace("ChallengeEXPMultiplier", l->challenge_exp_multiplier); - if (l->challenge_params) { - ret.emplace("ChallengeStageNumber", l->challenge_params->stage_number); - ret.emplace("ChallengeRankColor", l->challenge_params->rank_color); - ret.emplace("ChallengeRankText", l->challenge_params->rank_text); - ret.emplace("ChallengeRank0ThresholdBitmask", l->challenge_params->rank_thresholds[0].bitmask); - ret.emplace("ChallengeRank0ThresholdSeconds", l->challenge_params->rank_thresholds[0].seconds); - ret.emplace("ChallengeRank1ThresholdBitmask", l->challenge_params->rank_thresholds[1].bitmask); - ret.emplace("ChallengeRank1ThresholdSeconds", l->challenge_params->rank_thresholds[1].seconds); - ret.emplace("ChallengeRank2ThresholdBitmask", l->challenge_params->rank_thresholds[2].bitmask); - ret.emplace("ChallengeRank2ThresholdSeconds", l->challenge_params->rank_thresholds[2].seconds); - } - } - - auto floor_items_json = JSON::list(); - for (size_t floor = 0; floor < l->floor_item_managers.size(); floor++) { - for (const auto& it : l->floor_item_managers[floor].items) { - const auto& item = it.second; - auto item_dict = JSON::dict({ - {"LocationFloor", floor}, - {"LocationX", item->x}, - {"LocationZ", item->z}, - {"DropNumber", item->drop_number}, - {"Flags", item->flags}, - {"Data", item->data.hex()}, - {"ItemID", item->data.id.load()}, - }); - if (item_name_index) { - item_dict.emplace("Description", item_name_index->describe_item(item->data, false)); - } - floor_items_json.emplace_back(std::move(item_dict)); - } - } - ret.emplace("FloorItems", std::move(floor_items_json)); - ret.emplace("Quest", HTTPServer::generate_quest_json_st(l->quest)); - - } else { - ret.emplace("BattleInProgress", l->check_flag(Lobby::Flag::BATTLE_IN_PROGRESS)); - ret.emplace("IsSpectatorTeam", l->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM)); - ret.emplace("SpectatorsForbidden", l->check_flag(Lobby::Flag::SPECTATORS_FORBIDDEN)); - - auto ep3s = l->ep3_server; - if (ep3s) { - auto players_json = JSON::list(); - for (size_t z = 0; z < 4; z++) { - if (!ep3s->name_entries[z].present) { - players_json.emplace_back(nullptr); - } else { - auto lc = l->clients[z]; - - auto deck_entry = ep3s->deck_entries[z]; - JSON deck_json = nullptr; - if (deck_entry) { - auto cards_json = JSON::list(); - for (size_t w = 0; w < deck_entry->card_ids.size(); w++) { - try { - const auto& ce = ep3s->options.card_index->definition_for_id(deck_entry->card_ids[w]); - auto name = ce->def.en_name.decode(); - if (name.empty()) { - name = ce->def.en_short_name.decode(); - } - if (name.empty()) { - name = ce->def.jp_name.decode(); - } - if (name.empty()) { - name = ce->def.jp_short_name.decode(); - } - cards_json.emplace_back(name); - } catch (const out_of_range&) { - cards_json.emplace_back(deck_entry->card_ids[w].load()); - } - } - deck_json = JSON::dict({ - {"Name", deck_entry->name.decode(lc ? lc->language() : 1)}, - {"TeamID", deck_entry->team_id.load()}, - {"Cards", std::move(cards_json)}, - {"GodWhimFlag", deck_entry->god_whim_flag}, - {"PlayerLevel", deck_entry->player_level.load()}, - }); - } - - auto player_json = JSON::dict({ - {"PlayerName", ep3s->name_entries[z].name.decode(lc ? lc->language() : 1)}, - {"ClientID", ep3s->name_entries[z].client_id}, - {"IsCOM", !!ep3s->name_entries[z].is_cpu_player}, - {"Deck", std::move(deck_json)}, - }); - players_json.emplace_back(std::move(player_json)); - } - } - auto battle_state_json = JSON::dict({ - {"BehaviorFlags", ep3s->options.behavior_flags}, - {"RandomSeed", ep3s->options.opt_rand_crypt ? ep3s->options.opt_rand_crypt->seed() : JSON(nullptr)}, - {"RandomOffset", ep3s->options.opt_rand_crypt ? ep3s->options.opt_rand_crypt->absolute_offset() : JSON(nullptr)}, - {"Tournament", ep3s->options.tournament ? ep3s->options.tournament->json() : nullptr}, - {"MapNumber", ep3s->last_chosen_map ? ep3s->last_chosen_map->map_number : JSON(nullptr)}, - {"EnvironmentNumber", ep3s->map_and_rules ? ep3s->map_and_rules->environment_number : JSON(nullptr)}, - {"Rules", ep3s->map_and_rules ? ep3s->map_and_rules->rules.json() : nullptr}, - {"Players", std::move(players_json)}, - {"IsBattleFinished", ep3s->battle_finished}, - {"IsBattleInprogress", ep3s->battle_in_progress}, - {"RoundNumber", ep3s->round_num}, - {"FirstTeamTurn", ep3s->first_team_turn}, - {"CurrentTeamTurn", ep3s->current_team_turn1}, - {"BattlePhase", name_for_enum(ep3s->battle_phase)}, - {"SetupPhase", ep3s->setup_phase}, - {"RegistrationPhase", ep3s->registration_phase}, - {"ActionSubphase", ep3s->action_subphase}, - {"BattleStartTimeUsecs", ep3s->battle_start_usecs}, - {"TeamEXP", JSON::list({ep3s->team_exp[0], ep3s->team_exp[1]})}, - {"TeamDiceBonus", JSON::list({ep3s->team_dice_bonus[0], ep3s->team_dice_bonus[1]})}, - }); - // std::shared_ptr state_flags; - // std::array, 4> player_states; - ret.emplace("Episode3BattleState", std::move(battle_state_json)); - } else { - ret.emplace("Episode3BattleState", nullptr); - } - auto watched_lobby = l->watched_lobby.lock(); - if (watched_lobby) { - ret.emplace("WatchedLobbyID", watched_lobby->lobby_id); - } - auto watcher_lobby_ids_json = JSON::list(); - for (const auto& watcher_lobby : l->watcher_lobbies) { - watcher_lobby_ids_json.emplace_back(watcher_lobby->lobby_id); - } - ret.emplace("WatcherLobbyIDs", std::move(watcher_lobby_ids_json)); - ret.emplace("IsReplayLobby", !!l->battle_player); - } - - } else { // Not game - ret.emplace("IsPublic", l->check_flag(Lobby::Flag::PUBLIC)); - ret.emplace("IsDefault", l->check_flag(Lobby::Flag::DEFAULT)); - ret.emplace("IsOverflow", l->check_flag(Lobby::Flag::IS_OVERFLOW)); - ret.emplace("Block", l->block); - } - return ret; -} - -JSON HTTPServer::generate_game_server_clients_json() const { - return call_on_event_thread(this->state->base, [&]() { - auto res = JSON::list(); - for (const auto& it : this->state->channel_to_client) { - res.emplace_back(this->generate_game_client_json_st(it.second, this->state->item_name_index_opt(it.second->version()))); - } - return res; - }); -} - -JSON HTTPServer::generate_proxy_server_clients_json() const { - return call_on_event_thread(this->state->base, [&]() { - JSON res = JSON::list(); - if (this->state->proxy_server) { - for (const auto& it : this->state->proxy_server->all_sessions()) { - res.emplace_back(this->generate_proxy_client_json_st(it.second)); - } - } - return res; - }); -} - -JSON HTTPServer::generate_server_info_json() const { - return call_on_event_thread(this->state->base, [&]() { - size_t game_count = 0; - size_t lobby_count = 0; - for (const auto& it : this->state->id_to_lobby) { - if (it.second->is_game()) { - game_count++; - } else { - lobby_count++; - } - } - uint64_t uptime_usecs = now() - this->state->creation_time; - return JSON::dict({ - {"StartTimeUsecs", this->state->creation_time}, - {"StartTime", format_time(this->state->creation_time)}, - {"UptimeUsecs", uptime_usecs}, - {"Uptime", format_duration(uptime_usecs)}, - {"LobbyCount", lobby_count}, - {"GameCount", game_count}, - {"ClientCount", this->state->channel_to_client.size()}, - {"ProxySessionCount", this->state->proxy_server ? this->state->proxy_server->num_sessions() : 0}, - {"ServerName", this->state->name}, - }); - }); -} - -JSON HTTPServer::generate_lobbies_json() const { - return call_on_event_thread(this->state->base, [&]() { - JSON res = JSON::list(); - for (const auto& it : this->state->id_to_lobby) { - res.emplace_back(this->generate_lobby_json_st(it.second, this->state->item_name_index_opt(it.second->base_version))); - } - return res; - }); -} - -JSON HTTPServer::generate_summary_json() const { - auto ret = call_on_event_thread(this->state->base, [&]() { - auto clients_json = JSON::list(); - for (const auto& it : this->state->channel_to_client) { - auto c = it.second; - auto p = c->character(false, false); - auto l = c->lobby.lock(); - clients_json.emplace_back(JSON::dict({ - {"ID", c->id}, - {"AccountID", c->login ? c->login->account->account_id : JSON(nullptr)}, - {"Name", p ? p->disp.name.decode(it.second->language()) : JSON(nullptr)}, - {"Version", name_for_enum(it.second->version())}, - {"Language", name_for_language_code(it.second->language())}, - {"Level", p ? p->disp.stats.level + 1 : JSON(nullptr)}, - {"Class", p ? name_for_char_class(p->disp.visual.char_class) : JSON(nullptr)}, - {"SectionID", p ? name_for_section_id(p->disp.visual.section_id) : JSON(nullptr)}, - {"LobbyID", l ? l->lobby_id : JSON(nullptr)}, - })); - } - - auto proxy_clients_json = JSON::list(); - if (this->state->proxy_server) { - for (const auto& it : this->state->proxy_server->all_sessions()) { - proxy_clients_json.emplace_back(JSON::dict({ - {"AccountID", it.second->login ? it.second->login->account->account_id : JSON(nullptr)}, - {"Name", it.second->character_name}, - {"Version", name_for_enum(it.second->version())}, - {"Language", name_for_language_code(it.second->language())}, - })); - } - } - - auto games_json = JSON::list(); - for (const auto& it : this->state->id_to_lobby) { - auto l = it.second; - if (l->is_game()) { - auto game_json = JSON::dict({ - {"ID", l->lobby_id}, - {"Name", l->name}, - {"BaseVersion", name_for_enum(l->base_version)}, - {"Players", l->count_clients()}, - {"CheatsEnabled", l->check_flag(Lobby::Flag::CHEATS_ENABLED)}, - {"Episode", name_for_episode(l->episode)}, - {"HasPassword", !l->password.empty()}, - }); - if (l->episode == Episode::EP3) { - auto ep3s = l->ep3_server; - game_json.emplace("BattleInProgress", l->check_flag(Lobby::Flag::BATTLE_IN_PROGRESS)); - game_json.emplace("IsSpectatorTeam", l->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM)); - game_json.emplace("MapNumber", (ep3s && ep3s->last_chosen_map) ? ep3s->last_chosen_map->map_number : JSON(nullptr)); - game_json.emplace("Rules", (ep3s && ep3s->map_and_rules) ? ep3s->map_and_rules->rules.json() : nullptr); - } else { - game_json.emplace("QuestSelectionInProgress", l->check_flag(Lobby::Flag::QUEST_SELECTION_IN_PROGRESS)); - game_json.emplace("QuestInProgress", l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS)); - game_json.emplace("JoinableQuestInProgress", l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)); - game_json.emplace("SectionID", name_for_section_id(l->effective_section_id())); - game_json.emplace("Mode", name_for_mode(l->mode)); - game_json.emplace("Difficulty", name_for_difficulty(l->difficulty)); - game_json.emplace("Quest", this->generate_quest_json_st(l->quest)); - } - games_json.emplace_back(std::move(game_json)); - } - } - - return JSON::dict({ - {"Clients", std::move(clients_json)}, - {"ProxyClients", std::move(proxy_clients_json)}, - {"Games", std::move(games_json)}, - }); - }); - ret.emplace("Server", this->generate_server_info_json()); - return ret; -} - -JSON HTTPServer::generate_all_json() const { - return JSON::dict({ - {"Clients", this->generate_game_server_clients_json()}, - {"ProxyClients", this->generate_proxy_server_clients_json()}, - {"Lobbies", this->generate_lobbies_json()}, - {"Server", this->generate_server_info_json()}, - }); -} - -JSON HTTPServer::generate_ep3_cards_json(bool trial) const { - auto index = call_on_event_thread>(this->state->base, [&]() { - return trial ? this->state->ep3_card_index_trial : this->state->ep3_card_index; - }); - return index->definitions_json(); -} - -JSON HTTPServer::generate_common_tables_json() const { - auto [set_v2, set_v3_v4] = call_on_event_thread, shared_ptr>>(this->state->base, [&]() { - return make_pair(this->state->common_item_set_v2, this->state->common_item_set_v3_v4); - }); - return JSON::dict({{"v1_v2", set_v2->json()}, {"v3_v4", set_v3_v4->json()}}); -} - -JSON HTTPServer::generate_rare_tables_json() const { - auto sets = call_on_event_thread>>(this->state->base, [&]() { - return this->state->rare_item_sets; - }); - JSON ret = JSON::list(); - for (const auto& it : sets) { - ret.emplace_back(it.first); - } - return ret; -} - -JSON HTTPServer::generate_rare_table_json(const std::string& table_name) const { - try { - auto colls = call_on_event_thread, shared_ptr>>(this->state->base, [&]() { - const auto& table = this->state->rare_item_sets.at(table_name); - shared_ptr name_index; - if (ends_with(table_name, "-v1")) { - name_index = this->state->item_name_index_opt(Version::DC_V1); - } else if (ends_with(table_name, "-v2")) { - name_index = this->state->item_name_index_opt(Version::PC_V2); - } else if (ends_with(table_name, "-v3")) { - name_index = this->state->item_name_index_opt(Version::GC_V3); - } else if (ends_with(table_name, "-v4")) { - name_index = this->state->item_name_index_opt(Version::BB_V4); - } - return make_pair(table, name_index); - }); - return colls.first->json(colls.second); - } catch (const out_of_range&) { - throw http_error(404, "table does not exist"); - } -} - -void HTTPServer::handle_request(struct evhttp_request* req) { - shared_ptr ret; - uint32_t serialize_options = 0; - uint64_t start_time = now(); - string uri = evhttp_request_get_uri(req); - - try { - std::unordered_multimap query; - size_t query_pos = uri.find('?'); - if (query_pos != string::npos) { - query = this->parse_url_params(uri.substr(query_pos + 1)); - uri.resize(query_pos); - } - - static const string default_format_option = "false"; - if (this->get_url_param(query, "format", &default_format_option) == "true") { - serialize_options |= JSON::SerializeOption::FORMAT | JSON::SerializeOption::SORT_DICT_KEYS; - } - if (this->get_url_param(query, "hex", &default_format_option) == "true") { - serialize_options |= JSON::SerializeOption::HEX_INTEGERS; - } - - if (uri == "/") { - auto endpoints_json = JSON::list({ - "/y/data/ep3-cards", - "/y/data/ep3-cards-trial", - "/y/data/common-tables", - "/y/data/rare-tables", - "/y/data/rare-tables/", - "/y/data/config", - "/y/clients", - "/y/proxy-clients", - "/y/lobbies", - "/y/server", - "/y/summary", - "/y/all", - }); - ret = make_shared(JSON::dict({{"endpoints", std::move(endpoints_json)}})); - - } else if (uri == "/y/data/ep3-cards") { - ret = make_shared(this->generate_ep3_cards_json(false)); - } else if (uri == "/y/data/ep3-cards-trial") { - ret = make_shared(this->generate_ep3_cards_json(true)); - } else if (uri == "/y/data/common-tables") { - ret = make_shared(this->generate_common_tables_json()); - } else if (uri == "/y/data/rare-tables") { - ret = make_shared(this->generate_rare_tables_json()); - } else if (!strncmp(uri.c_str(), "/y/data/rare-tables/", 20)) { - ret = make_shared(this->generate_rare_table_json(uri.substr(20))); - } else if (uri == "/y/data/config") { - ret = call_on_event_thread>(this->state->base, [this]() { return this->state->config_json; }); - } else if (uri == "/y/clients") { - ret = make_shared(this->generate_game_server_clients_json()); - } else if (uri == "/y/proxy-clients") { - ret = make_shared(this->generate_proxy_server_clients_json()); - } else if (uri == "/y/lobbies") { - ret = make_shared(this->generate_lobbies_json()); - } else if (uri == "/y/server") { - ret = make_shared(this->generate_server_info_json()); - } else if (uri == "/y/summary") { - ret = make_shared(this->generate_summary_json()); - } else if (uri == "/y/all") { - ret = make_shared(this->generate_all_json()); - - } else { - throw http_error(404, "unknown action"); - } - - } catch (const http_error& e) { - unique_ptr out_buffer(evbuffer_new(), evbuffer_free); - evbuffer_add_printf(out_buffer.get(), "%s", e.what()); - this->send_response(req, e.code, "text/plain", out_buffer.get()); - return; - - } catch (const exception& e) { - unique_ptr out_buffer(evbuffer_new(), evbuffer_free); - evbuffer_add_printf(out_buffer.get(), "Error during request: %s", e.what()); - this->send_response(req, 500, "text/plain", out_buffer.get()); - server_log.warning("internal server error during http request: %s", e.what()); - return; - } - - uint64_t handler_end = now(); - unique_ptr out_buffer(evbuffer_new(), evbuffer_free); - string* serialized = new string(ret->serialize(JSON::SerializeOption::ESCAPE_CONTROLS_ONLY | serialize_options)); - size_t size = serialized->size(); - uint64_t serialize_end = now(); - auto cleanup = +[](const void*, size_t, void* s) -> void { - delete reinterpret_cast(s); - }; - evbuffer_add_reference(out_buffer.get(), serialized->data(), serialized->size(), cleanup, serialized); - this->send_response(req, 200, "application/json", out_buffer.get()); - - string handler_time = format_duration(handler_end - start_time); - string serialize_time = format_duration(serialize_end - handler_end); - string size_str = format_size(size); - server_log.info("[HTTPServer] %s in [handler: %s, serialize: %s, size: %s]", - uri.c_str(), handler_time.c_str(), serialize_time.c_str(), size_str.c_str()); -} - -void HTTPServer::thread_fn() { - event_base_loop(this->base.get(), EVLOOP_NO_EXIT_ON_EMPTY); -} +#include "HTTPServer.hh" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "EventUtils.hh" +#include "Loggers.hh" +#include "ProxyServer.hh" +#include "Server.hh" + +using namespace std; + +const unordered_map HTTPServer::explanation_for_response_code({ + {100, "Continue"}, + {101, "Switching Protocols"}, + {102, "Processing"}, + {200, "OK"}, + {201, "Created"}, + {202, "Accepted"}, + {203, "Non-Authoritative Information"}, + {204, "No Content"}, + {205, "Reset Content"}, + {206, "Partial Content"}, + {207, "Multi-Status"}, + {208, "Already Reported"}, + {226, "IM Used"}, + {300, "Multiple Choices"}, + {301, "Moved Permanently"}, + {302, "Found"}, + {303, "See Other"}, + {304, "Not Modified"}, + {305, "Use Proxy"}, + {307, "Temporary Redirect"}, + {308, "Permanent Redirect"}, + {400, "Bad Request"}, + {401, "Unathorized"}, + {402, "Payment Required"}, + {403, "Forbidden"}, + {404, "Not Found"}, + {405, "Method Not Allowed"}, + {406, "Not Acceptable"}, + {407, "Proxy Authentication Required"}, + {408, "Request Timeout"}, + {409, "Conflict"}, + {410, "Gone"}, + {411, "Length Required"}, + {412, "Precondition Failed"}, + {413, "Request Entity Too Large"}, + {414, "Request-URI Too Long"}, + {415, "Unsupported Media Type"}, + {416, "Requested Range Not Satisfiable"}, + {417, "Expectation Failed"}, + {418, "I\'m a Teapot"}, + {420, "Enhance Your Calm"}, + {422, "Unprocessable Entity"}, + {423, "Locked"}, + {424, "Failed Dependency"}, + {426, "Upgrade Required"}, + {428, "Precondition Required"}, + {429, "Too Many Requests"}, + {431, "Request Header Fields Too Large"}, + {444, "No Response"}, + {449, "Retry With"}, + {451, "Unavailable For Legal Reasons"}, + {500, "Internal Server Error"}, + {501, "Not Implemented"}, + {502, "Bad Gateway"}, + {503, "Service Unavailable"}, + {504, "Gateway Timeout"}, + {505, "HTTP Version Not Supported"}, + {506, "Variant Also Negotiates"}, + {507, "Insufficient Storage"}, + {508, "Loop Detected"}, + {509, "Bandwidth Limit Exceeded"}, + {510, "Not Extended"}, + {511, "Network Authentication Required"}, + {598, "Network Read Timeout Error"}, + {599, "Network Connect Timeout Error"}, +}); + +HTTPServer::http_error::http_error(int code, const string& what) + : runtime_error(what), + code(code) {} + +void HTTPServer::send_response(struct evhttp_request* req, int code, const char* content_type, struct evbuffer* b) { + struct evkeyvalq* headers = evhttp_request_get_output_headers(req); + evhttp_add_header(headers, "Content-Type", content_type); + evhttp_add_header(headers, "Server", "newserv"); + evhttp_send_reply(req, code, explanation_for_response_code.at(code), b); +} + +void HTTPServer::send_response(struct evhttp_request* req, int code, const char* content_type, const char* fmt, ...) { + unique_ptr out_buffer(evbuffer_new(), evbuffer_free); + va_list va; + va_start(va, fmt); + evbuffer_add_vprintf(out_buffer.get(), fmt, va); + va_end(va); + HTTPServer::send_response(req, code, content_type, out_buffer.get()); +} + +unordered_multimap HTTPServer::parse_url_params(const string& query) { + unordered_multimap params; + if (query.empty()) { + return params; + } + for (auto it : split(query, '&')) { + size_t first_equals = it.find('='); + if (first_equals != string::npos) { + string value(it, first_equals + 1); + + size_t write_offset = 0, read_offset = 0; + for (; read_offset < value.size(); write_offset++) { + if ((value[read_offset] == '%') && (read_offset < value.size() - 2)) { + value[write_offset] = + static_cast(value_for_hex_char(value[read_offset + 1]) << 4) | + static_cast(value_for_hex_char(value[read_offset + 2])); + read_offset += 3; + } else if (value[write_offset] == '+') { + value[write_offset] = ' '; + read_offset++; + } else { + value[write_offset] = value[read_offset]; + read_offset++; + } + } + value.resize(write_offset); + + params.emplace(piecewise_construct, forward_as_tuple(it, 0, first_equals), + forward_as_tuple(value)); + } else { + params.emplace(it, ""); + } + } + return params; +} + +unordered_map HTTPServer::parse_url_params_unique(const string& query) { + unordered_map ret; + for (const auto& it : HTTPServer::parse_url_params(query)) { + ret.emplace(it.first, std::move(it.second)); + } + return ret; +} + +const string& HTTPServer::get_url_param( + const unordered_multimap& params, const string& key, const string* _default) { + + auto range = params.equal_range(key); + if (range.first == range.second) { + if (!_default) { + throw out_of_range("URL parameter " + key + " not present"); + } + return *_default; + } + + return range.first->second; +} + +HTTPServer::HTTPServer(shared_ptr state) + : state(state), + base(event_base_new(), event_base_free), + http(evhttp_new(this->base.get()), evhttp_free), + th(&HTTPServer::thread_fn, this) { + evhttp_set_gencb(this->http.get(), this->dispatch_handle_request, this); +} + +void HTTPServer::listen(const string& socket_path) { + int fd = ::listen(socket_path, 0, SOMAXCONN); + server_log.info("Listening on Unix socket %s on fd %d (HTTP)", socket_path.c_str(), fd); + this->add_socket(fd); +} + +void HTTPServer::listen(const string& addr, int port) { + if (port == 0) { + this->listen(addr); + } else { + int fd = ::listen(addr, port, SOMAXCONN); + string netloc_str = render_netloc(addr, port); + server_log.info("Listening on TCP interface %s on fd %d (HTTP)", netloc_str.c_str(), fd); + this->add_socket(fd); + } +} + +void HTTPServer::listen(int port) { + this->listen("", port); +} + +void HTTPServer::add_socket(int fd) { + evhttp_accept_socket(this->http.get(), fd); +} + +void HTTPServer::schedule_stop() { + event_base_loopexit(this->base.get(), nullptr); +} + +void HTTPServer::wait_for_stop() { + this->th.join(); +} + +void HTTPServer::dispatch_handle_request(struct evhttp_request* req, void* ctx) { + reinterpret_cast(ctx)->handle_request(req); +} + +JSON HTTPServer::generate_quest_json_st(shared_ptr q) { + if (!q) { + return nullptr; + } + auto battle_rules_json = q->battle_rules ? q->battle_rules->json() : nullptr; + auto challenge_template_index_json = (q->challenge_template_index >= 0) + ? q->challenge_template_index + : JSON(nullptr); + return JSON::dict({ + {"Number", q->quest_number}, + {"Episode", name_for_episode(q->episode)}, + {"Joinable", q->joinable}, + {"LockStatusRegister", (q->lock_status_register >= 0) ? q->lock_status_register : JSON(nullptr)}, + {"Name", q->name}, + {"BattleRules", std::move(battle_rules_json)}, + {"ChallengeTemplateIndex", std::move(challenge_template_index_json)}, + }); +} + +JSON HTTPServer::generate_client_config_json_st(const Client::Config& config) { + const char* drop_notifications_mode = "unknown"; + switch (config.get_drop_notification_mode()) { + case Client::ItemDropNotificationMode::NOTHING: + drop_notifications_mode = "off"; + break; + case Client::ItemDropNotificationMode::RARES_ONLY: + drop_notifications_mode = "rare"; + break; + case Client::ItemDropNotificationMode::ALL_ITEMS: + drop_notifications_mode = "on"; + break; + case Client::ItemDropNotificationMode::ALL_ITEMS_INCLUDING_MESETA: + drop_notifications_mode = "every"; + break; + } + + auto ret = JSON::dict({ + {"SpecificVersion", config.specific_version}, + {"SwitchAssistEnabled", (config.check_flag(Client::Flag::SWITCH_ASSIST_ENABLED) ? true : false)}, + {"InfiniteHPEnabled", (config.check_flag(Client::Flag::INFINITE_HP_ENABLED) ? true : false)}, + {"InfiniteTPEnabled", (config.check_flag(Client::Flag::INFINITE_TP_ENABLED) ? true : false)}, + {"DropNotificationMode", drop_notifications_mode}, + {"DebugEnabled", (config.check_flag(Client::Flag::DEBUG_ENABLED) ? true : false)}, + {"ProxySaveFilesEnabled", (config.check_flag(Client::Flag::PROXY_SAVE_FILES) ? true : false)}, + {"ProxyChatCommandsEnabled", (config.check_flag(Client::Flag::PROXY_CHAT_COMMANDS_ENABLED) ? true : false)}, + {"ProxyPlayerNotificationsEnabled", (config.check_flag(Client::Flag::PROXY_PLAYER_NOTIFICATIONS_ENABLED) ? true : false)}, + {"ProxySuppressClientPings", (config.check_flag(Client::Flag::PROXY_SUPPRESS_CLIENT_PINGS) ? true : false)}, + {"ProxyEp3InfiniteMesetaEnabled", (config.check_flag(Client::Flag::PROXY_EP3_INFINITE_MESETA_ENABLED) ? true : false)}, + {"ProxyEp3InfiniteTimeEnabled", (config.check_flag(Client::Flag::PROXY_EP3_INFINITE_TIME_ENABLED) ? true : false)}, + {"ProxyBlockFunctionCalls", (config.check_flag(Client::Flag::PROXY_BLOCK_FUNCTION_CALLS) ? true : false)}, + {"ProxyEp3UnmaskWhispers", (config.check_flag(Client::Flag::PROXY_EP3_UNMASK_WHISPERS) ? true : false)}, + }); + ret.emplace("OverrideRandomSeed", config.check_flag(Client::Flag::USE_OVERRIDE_RANDOM_SEED) ? config.override_random_seed : JSON(nullptr)); + ret.emplace("OverrideSectionID", (config.override_section_id != 0xFF) ? config.override_section_id : JSON(nullptr)); + ret.emplace("OverrideLobbyEvent", (config.override_lobby_event != 0xFF) ? config.override_lobby_event : JSON(nullptr)); + ret.emplace("OverrideLobbyNumber", (config.override_lobby_number != 0x80) ? config.override_lobby_number : JSON(nullptr)); + return ret; +} + +JSON HTTPServer::generate_account_json_st(shared_ptr a) { + auto dc_nte_licenses_json = JSON::list(); + for (const auto& it : a->dc_nte_licenses) { + dc_nte_licenses_json.emplace_back(it.first); + } + auto dc_licenses_json = JSON::list(); + for (const auto& it : a->dc_licenses) { + dc_licenses_json.emplace_back(it.first); + } + auto pc_licenses_json = JSON::list(); + for (const auto& it : a->pc_licenses) { + pc_licenses_json.emplace_back(it.first); + } + auto gc_licenses_json = JSON::list(); + for (const auto& it : a->gc_licenses) { + gc_licenses_json.emplace_back(it.first); + } + auto xb_licenses_json = JSON::list(); + for (const auto& it : a->xb_licenses) { + xb_licenses_json.emplace_back(it.first); + } + auto bb_licenses_json = JSON::list(); + for (const auto& it : a->bb_licenses) { + bb_licenses_json.emplace_back(it.first); + } + auto auto_patches_json = JSON::list(); + for (const auto& it : a->auto_patches_enabled) { + auto_patches_json.emplace_back(it); + } + return JSON::dict({ + {"AccountID", a->account_id}, + {"Flags", a->flags}, + {"BanEndTime", a->ban_end_time ? a->ban_end_time : JSON(nullptr)}, + {"Ep3CurrentMeseta", a->ep3_current_meseta}, + {"Ep3TotalMesetaEarned", a->ep3_total_meseta_earned}, + {"BBTeamID", a->bb_team_id}, + {"LastPlayerName", a->last_player_name}, + {"AutoReplyMessage", a->auto_reply_message}, + {"IsTemporary", a->is_temporary}, + {"DCNTELicenses", std::move(dc_nte_licenses_json)}, + {"DCLicenses", std::move(dc_licenses_json)}, + {"PCLicenses", std::move(pc_licenses_json)}, + {"GCLicenses", std::move(gc_licenses_json)}, + {"XBLicenses", std::move(xb_licenses_json)}, + {"BBLicenses", std::move(bb_licenses_json)}, + {"AutoPatchesEnabled", std::move(auto_patches_json)}, + }); +}; + +JSON HTTPServer::generate_game_client_json_st(shared_ptr c, shared_ptr item_name_index) { + auto ret = JSON::dict({ + {"ID", c->id}, + {"RemoteAddress", render_sockaddr_storage(c->channel.remote_addr)}, + {"Version", name_for_enum(c->version())}, + {"SubVersion", c->sub_version}, + {"Config", HTTPServer::generate_client_config_json_st(c->config)}, + {"Language", name_for_language_code(c->language())}, + {"LocationX", c->x}, + {"LocationZ", c->z}, + {"LocationFloor", c->floor}, + {"CanChat", c->can_chat}, + }); + ret.emplace("Account", c->login ? HTTPServer::generate_account_json_st(c->login->account) : JSON(nullptr)); + auto l = c->lobby.lock(); + if (l) { + ret.emplace("LobbyID", l->lobby_id); + ret.emplace("LobbyClientID", c->lobby_client_id); + } + if (c->version() == Version::BB_V4) { + ret.emplace("BBCharacterIndex", c->bb_character_index); + } + auto p = c->character(false, false); + if (p) { + if (!is_ep3(c->version())) { + ret.emplace("InventoryItems", p->inventory.num_items); + if (c->version() != Version::DC_NTE) { + ret.emplace("InventoryLanguage", p->inventory.language); + ret.emplace("NumHPMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::HP)); + ret.emplace("NumTPMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::TP)); + if (!is_v1_or_v2(c->version())) { + ret.emplace("NumPowerMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::POWER)); + ret.emplace("NumDefMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::DEF)); + ret.emplace("NumMindMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::MIND)); + ret.emplace("NumEvadeMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::EVADE)); + ret.emplace("NumLuckMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::LUCK)); + } + } + JSON items_json = JSON::list(); + for (size_t z = 0; z < p->inventory.num_items; z++) { + const auto& item = p->inventory.items[z]; + auto item_dict = JSON::dict({ + {"Flags", item.flags.load()}, + {"Data", item.data.hex()}, + {"ItemID", item.data.id.load()}, + }); + if (item_name_index) { + item_dict.emplace("Description", item_name_index->describe_item(item.data, false)); + } + items_json.emplace_back(std::move(item_dict)); + } + ret.emplace("ATP", p->disp.stats.char_stats.atp.load()); + ret.emplace("MST", p->disp.stats.char_stats.mst.load()); + ret.emplace("EVP", p->disp.stats.char_stats.evp.load()); + ret.emplace("HP", p->disp.stats.char_stats.hp.load()); + ret.emplace("DFP", p->disp.stats.char_stats.dfp.load()); + ret.emplace("ATA", p->disp.stats.char_stats.ata.load()); + ret.emplace("LCK", p->disp.stats.char_stats.lck.load()); + ret.emplace("EXP", p->disp.stats.experience.load()); + ret.emplace("Meseta", p->disp.stats.meseta.load()); + auto tech_levels_json = JSON::dict(); + for (size_t z = 0; z < 0x13; z++) { + auto level = p->get_technique_level(z); + tech_levels_json.emplace(name_for_technique(z), (level != 0xFF) ? level : JSON(nullptr)); + } + ret.emplace("TechniqueLevels", std::move(tech_levels_json)); + } + ret.emplace("Height", p->disp.stats.height.load()); + ret.emplace("Level", p->disp.stats.level.load()); + ret.emplace("NameColor", p->disp.visual.name_color.load()); + ret.emplace("ExtraModel", (p->disp.visual.validation_flags & 2) ? p->disp.visual.extra_model : JSON(nullptr)); + ret.emplace("SectionID", name_for_section_id(p->disp.visual.section_id)); + ret.emplace("CharClass", name_for_char_class(p->disp.visual.section_id)); + ret.emplace("Costume", p->disp.visual.costume.load()); + ret.emplace("Skin", p->disp.visual.skin.load()); + ret.emplace("Face", p->disp.visual.face.load()); + ret.emplace("Head", p->disp.visual.head.load()); + ret.emplace("Hair", p->disp.visual.hair.load()); + ret.emplace("HairR", p->disp.visual.hair_r.load()); + ret.emplace("HairG", p->disp.visual.hair_g.load()); + ret.emplace("HairB", p->disp.visual.hair_b.load()); + ret.emplace("ProportionX", p->disp.visual.proportion_x.load()); + ret.emplace("ProportionY", p->disp.visual.proportion_y.load()); + + ret.emplace("Name", p->disp.name.decode(c->language())); + ret.emplace("PlayTimeSeconds", p->play_time_seconds.load()); + + ret.emplace("AutoReply", p->auto_reply.decode(c->language())); + ret.emplace("InfoBoard", p->info_board.decode(c->language())); + auto battle_place_counts = JSON::list({ + p->battle_records.place_counts[0].load(), + p->battle_records.place_counts[1].load(), + p->battle_records.place_counts[2].load(), + p->battle_records.place_counts[3].load(), + }); + ret.emplace("BattlePlaceCounts", std::move(battle_place_counts)); + ret.emplace("BattleDisconnectCount", p->battle_records.disconnect_count.load()); + + if (!is_ep3(c->version())) { + auto json_for_challenge_times = [](const parray& times) -> JSON { + auto times_json = JSON::list(); + for (size_t z = 0; z < times.size(); z++) { + times_json.emplace_back(times[z].decode()); + } + return times_json; + }; + ret.emplace("ChallengeTitleColorXRGB1555", p->challenge_records.title_color.load()); + ret.emplace("ChallengeTimesEp1Online", json_for_challenge_times(p->challenge_records.times_ep1_online)); + ret.emplace("ChallengeTimesEp2Online", json_for_challenge_times(p->challenge_records.times_ep2_online)); + ret.emplace("ChallengeTimesEp1Offline", json_for_challenge_times(p->challenge_records.times_ep1_offline)); + ret.emplace("ChallengeGraveIsEp2", p->challenge_records.grave_is_ep2 ? true : false); + ret.emplace("ChallengeGraveStageNum", p->challenge_records.grave_stage_num); + ret.emplace("ChallengeGraveFloor", p->challenge_records.grave_floor); + ret.emplace("ChallengeGraveDeaths", p->challenge_records.grave_deaths.load()); + { + uint16_t year = 2000 + ((p->challenge_records.grave_time >> 28) & 0x0F); + uint8_t month = (p->challenge_records.grave_time >> 24) & 0x0F; + uint8_t day = (p->challenge_records.grave_time >> 16) & 0xFF; + uint8_t hour = (p->challenge_records.grave_time >> 8) & 0xFF; + uint8_t minute = p->challenge_records.grave_time & 0xFF; + ret.emplace("ChallengeGraveTime", string_printf("%04hu-%02hhu-%02hhu %02hhu:%02hhu:00", year, month, day, hour, minute)); + } + string grave_enemy_types; + if (p->challenge_records.grave_defeated_by_enemy_rt_index) { + for (EnemyType type : enemy_types_for_rare_table_index(p->challenge_records.grave_is_ep2 ? Episode::EP2 : Episode::EP1, p->challenge_records.grave_defeated_by_enemy_rt_index)) { + if (!grave_enemy_types.empty()) { + grave_enemy_types += "/"; + } + grave_enemy_types += name_for_enum(type); + } + } + ret.emplace("ChallengeGraveDefeatedByEnemy", std::move(grave_enemy_types)); + ret.emplace("ChallengeGraveX", p->challenge_records.grave_x.load()); + ret.emplace("ChallengeGraveY", p->challenge_records.grave_y.load()); + ret.emplace("ChallengeGraveZ", p->challenge_records.grave_z.load()); + ret.emplace("ChallengeGraveTeam", p->challenge_records.grave_team.decode()); + ret.emplace("ChallengeGraveMessage", p->challenge_records.grave_message.decode()); + ret.emplace("ChallengeAwardStateEp1OnlineFlags", p->challenge_records.ep1_online_award_state.rank_award_flags.load()); + ret.emplace("ChallengeAwardStateEp1OnlineMaxRank", p->challenge_records.ep1_online_award_state.maximum_rank.decode()); + ret.emplace("ChallengeAwardStateEp2OnlineFlags", p->challenge_records.ep2_online_award_state.rank_award_flags.load()); + ret.emplace("ChallengeAwardStateEp2OnlineMaxRank", p->challenge_records.ep2_online_award_state.maximum_rank.decode()); + ret.emplace("ChallengeAwardStateEp1OfflineFlags", p->challenge_records.ep1_offline_award_state.rank_award_flags.load()); + ret.emplace("ChallengeAwardStateEp1OfflineMaxRank", p->challenge_records.ep1_offline_award_state.maximum_rank.decode()); + ret.emplace("ChallengeRankTitle", p->challenge_records.rank_title.decode()); + } + } + return ret; +} + +JSON HTTPServer::generate_proxy_client_json_st(shared_ptr ses) { + struct LobbyPlayer { + uint32_t guild_card_number = 0; + uint64_t xb_user_id = 0; + std::string name; + uint8_t language = 0; + uint8_t section_id = 0; + uint8_t char_class = 0; + }; + std::vector lobby_players; + + auto lobby_players_json = JSON::list(); + for (size_t z = 0; z < ses->lobby_players.size(); z++) { + const auto& p = ses->lobby_players[z]; + if (p.guild_card_number) { + lobby_players_json.emplace_back(JSON::dict({ + {"GuildCardNumber", p.guild_card_number}, + {"Name", p.name}, + {"Language", name_for_language_code(p.language)}, + {"SectionID", name_for_section_id(p.section_id)}, + {"CharClass", name_for_char_class(p.char_class)}, + })); + lobby_players_json.back().emplace("XBUserID", p.xb_user_id ? p.xb_user_id : JSON(nullptr)); + } else { + lobby_players_json.emplace_back(nullptr); + } + } + + auto ret = JSON::dict({ + {"ID", ses->id}, + {"RemoteClientAddress", render_sockaddr_storage(ses->client_channel.remote_addr)}, + {"RemoteServerAddress", render_sockaddr_storage(ses->server_channel.remote_addr)}, + {"LocalPort", ses->local_port}, + {"NextDestination", render_sockaddr_storage(ses->next_destination)}, + {"Version", name_for_enum(ses->version())}, + {"SubVersion", ses->sub_version}, + {"Name", ses->character_name}, + {"DCHardwareID", ses->hardware_id}, + {"RemoteGuildCardNumber", ses->remote_guild_card_number}, + {"RemoteClientConfigData", format_data_string(&ses->remote_client_config_data[0], ses->remote_client_config_data.size())}, + {"Config", HTTPServer::generate_client_config_json_st(ses->config)}, + {"Language", name_for_language_code(ses->language())}, + {"LobbyClientID", ses->lobby_client_id}, + {"LeaderClientID", ses->leader_client_id}, + {"LocationX", ses->x}, + {"LocationZ", ses->z}, + {"LocationFloor", ses->floor}, + {"IsInGame", ses->is_in_game}, + {"IsInQuest", ses->is_in_quest}, + {"LobbyEvent", ses->lobby_event}, + {"LobbyDifficulty", name_for_difficulty(ses->lobby_difficulty)}, + {"LobbySectionID", name_for_section_id(ses->lobby_section_id)}, + {"LobbyMode", name_for_mode(ses->lobby_mode)}, + {"LobbyEpisode", name_for_episode(ses->lobby_episode)}, + {"LobbyRandomSeed", ses->lobby_random_seed}, + {"LobbyPlayers", std::move(lobby_players_json)}, + }); + switch (ses->drop_mode) { + case ProxyServer::LinkedSession::DropMode::DISABLED: + ret.emplace("DropMode", "none"); + break; + case ProxyServer::LinkedSession::DropMode::PASSTHROUGH: + ret.emplace("DropMode", "default"); + break; + case ProxyServer::LinkedSession::DropMode::INTERCEPT: + ret.emplace("DropMode", "proxy"); + break; + } + ret.emplace("Account", ses->login ? HTTPServer::generate_account_json_st(ses->login->account) : JSON(nullptr)); + return ret; +} + +JSON HTTPServer::generate_lobby_json_st(shared_ptr l, shared_ptr item_name_index) { + std::array, 12> clients; + + auto client_ids_json = JSON::list(); + for (size_t z = 0; z < l->max_clients; z++) { + client_ids_json.emplace_back(l->clients[z] ? l->clients[z]->id : JSON(nullptr)); + } + + auto ret = JSON::dict({ + {"ID", l->lobby_id}, + {"AllowedVersions", l->allowed_versions}, + {"Event", l->event}, + {"LeaderClientID", l->leader_id}, + {"MaxClients", l->max_clients}, + {"IdleTimeoutUsecs", l->idle_timeout_usecs}, + {"ClientIDs", std::move(client_ids_json)}, + {"IsGame", l->is_game()}, + {"IsPersistent", l->check_flag(Lobby::Flag::PERSISTENT)}, + }); + + if (l->is_game()) { + ret.emplace("CheatsEnabled", l->check_flag(Lobby::Flag::CHEATS_ENABLED)); + ret.emplace("MinLevel", l->min_level + 1); + ret.emplace("MaxLevel", l->max_level + 1); + ret.emplace("BaseVersion", l->base_version); + ret.emplace("Episode", name_for_episode(l->episode)); + ret.emplace("HasPassword", !l->password.empty()); + ret.emplace("Name", l->name); + ret.emplace("RandomSeed", l->random_seed); + if (l->episode != Episode::EP3) { + ret.emplace("QuestSelectionInProgress", l->check_flag(Lobby::Flag::QUEST_SELECTION_IN_PROGRESS)); + ret.emplace("QuestInProgress", l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS)); + ret.emplace("JoinableQuestInProgress", l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)); + auto variations_json = JSON::list(); + for (size_t z = 0; z < l->variations.size(); z++) { + variations_json.emplace_back(l->variations[z].load()); + } + ret.emplace("Variations", std::move(variations_json)); + ret.emplace("SectionID", name_for_section_id(l->effective_section_id())); + ret.emplace("Mode", name_for_mode(l->mode)); + ret.emplace("Difficulty", name_for_difficulty(l->difficulty)); + ret.emplace("BaseEXPMultiplier", l->base_exp_multiplier); + ret.emplace("EXPShareMultiplier", l->exp_share_multiplier); + ret.emplace("AllowedDropModes", l->allowed_drop_modes); + switch (l->drop_mode) { + case Lobby::DropMode::DISABLED: + ret.emplace("DropMode", "none"); + break; + case Lobby::DropMode::CLIENT: + ret.emplace("DropMode", "client"); + break; + case Lobby::DropMode::SERVER_SHARED: + ret.emplace("DropMode", "shared"); + break; + case Lobby::DropMode::SERVER_PRIVATE: + ret.emplace("DropMode", "private"); + break; + case Lobby::DropMode::SERVER_DUPLICATE: + ret.emplace("DropMode", "duplicate"); + break; + } + if (l->mode == GameMode::CHALLENGE) { + ret.emplace("ChallengeEXPMultiplier", l->challenge_exp_multiplier); + if (l->challenge_params) { + ret.emplace("ChallengeStageNumber", l->challenge_params->stage_number); + ret.emplace("ChallengeRankColor", l->challenge_params->rank_color); + ret.emplace("ChallengeRankText", l->challenge_params->rank_text); + ret.emplace("ChallengeRank0ThresholdBitmask", l->challenge_params->rank_thresholds[0].bitmask); + ret.emplace("ChallengeRank0ThresholdSeconds", l->challenge_params->rank_thresholds[0].seconds); + ret.emplace("ChallengeRank1ThresholdBitmask", l->challenge_params->rank_thresholds[1].bitmask); + ret.emplace("ChallengeRank1ThresholdSeconds", l->challenge_params->rank_thresholds[1].seconds); + ret.emplace("ChallengeRank2ThresholdBitmask", l->challenge_params->rank_thresholds[2].bitmask); + ret.emplace("ChallengeRank2ThresholdSeconds", l->challenge_params->rank_thresholds[2].seconds); + } + } + + auto floor_items_json = JSON::list(); + for (size_t floor = 0; floor < l->floor_item_managers.size(); floor++) { + for (const auto& it : l->floor_item_managers[floor].items) { + const auto& item = it.second; + auto item_dict = JSON::dict({ + {"LocationFloor", floor}, + {"LocationX", item->x}, + {"LocationZ", item->z}, + {"DropNumber", item->drop_number}, + {"Flags", item->flags}, + {"Data", item->data.hex()}, + {"ItemID", item->data.id.load()}, + }); + if (item_name_index) { + item_dict.emplace("Description", item_name_index->describe_item(item->data, false)); + } + floor_items_json.emplace_back(std::move(item_dict)); + } + } + ret.emplace("FloorItems", std::move(floor_items_json)); + ret.emplace("Quest", HTTPServer::generate_quest_json_st(l->quest)); + + } else { + ret.emplace("BattleInProgress", l->check_flag(Lobby::Flag::BATTLE_IN_PROGRESS)); + ret.emplace("IsSpectatorTeam", l->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM)); + ret.emplace("SpectatorsForbidden", l->check_flag(Lobby::Flag::SPECTATORS_FORBIDDEN)); + + auto ep3s = l->ep3_server; + if (ep3s) { + auto players_json = JSON::list(); + for (size_t z = 0; z < 4; z++) { + if (!ep3s->name_entries[z].present) { + players_json.emplace_back(nullptr); + } else { + auto lc = l->clients[z]; + + auto deck_entry = ep3s->deck_entries[z]; + JSON deck_json = nullptr; + if (deck_entry) { + auto cards_json = JSON::list(); + for (size_t w = 0; w < deck_entry->card_ids.size(); w++) { + try { + const auto& ce = ep3s->options.card_index->definition_for_id(deck_entry->card_ids[w]); + auto name = ce->def.en_name.decode(); + if (name.empty()) { + name = ce->def.en_short_name.decode(); + } + if (name.empty()) { + name = ce->def.jp_name.decode(); + } + if (name.empty()) { + name = ce->def.jp_short_name.decode(); + } + cards_json.emplace_back(name); + } catch (const out_of_range&) { + cards_json.emplace_back(deck_entry->card_ids[w].load()); + } + } + deck_json = JSON::dict({ + {"Name", deck_entry->name.decode(lc ? lc->language() : 1)}, + {"TeamID", deck_entry->team_id.load()}, + {"Cards", std::move(cards_json)}, + {"GodWhimFlag", deck_entry->god_whim_flag}, + {"PlayerLevel", deck_entry->player_level.load()}, + }); + } + + auto player_json = JSON::dict({ + {"PlayerName", ep3s->name_entries[z].name.decode(lc ? lc->language() : 1)}, + {"ClientID", ep3s->name_entries[z].client_id}, + {"IsCOM", !!ep3s->name_entries[z].is_cpu_player}, + {"Deck", std::move(deck_json)}, + }); + players_json.emplace_back(std::move(player_json)); + } + } + auto battle_state_json = JSON::dict({ + {"BehaviorFlags", ep3s->options.behavior_flags}, + {"RandomSeed", ep3s->options.opt_rand_crypt ? ep3s->options.opt_rand_crypt->seed() : JSON(nullptr)}, + {"RandomOffset", ep3s->options.opt_rand_crypt ? ep3s->options.opt_rand_crypt->absolute_offset() : JSON(nullptr)}, + {"Tournament", ep3s->options.tournament ? ep3s->options.tournament->json() : nullptr}, + {"MapNumber", ep3s->last_chosen_map ? ep3s->last_chosen_map->map_number : JSON(nullptr)}, + {"EnvironmentNumber", ep3s->map_and_rules ? ep3s->map_and_rules->environment_number : JSON(nullptr)}, + {"Rules", ep3s->map_and_rules ? ep3s->map_and_rules->rules.json() : nullptr}, + {"Players", std::move(players_json)}, + {"IsBattleFinished", ep3s->battle_finished}, + {"IsBattleInprogress", ep3s->battle_in_progress}, + {"RoundNumber", ep3s->round_num}, + {"FirstTeamTurn", ep3s->first_team_turn}, + {"CurrentTeamTurn", ep3s->current_team_turn1}, + {"BattlePhase", name_for_enum(ep3s->battle_phase)}, + {"SetupPhase", ep3s->setup_phase}, + {"RegistrationPhase", ep3s->registration_phase}, + {"ActionSubphase", ep3s->action_subphase}, + {"BattleStartTimeUsecs", ep3s->battle_start_usecs}, + {"TeamEXP", JSON::list({ep3s->team_exp[0], ep3s->team_exp[1]})}, + {"TeamDiceBonus", JSON::list({ep3s->team_dice_bonus[0], ep3s->team_dice_bonus[1]})}, + }); + // std::shared_ptr state_flags; + // std::array, 4> player_states; + ret.emplace("Episode3BattleState", std::move(battle_state_json)); + } else { + ret.emplace("Episode3BattleState", nullptr); + } + auto watched_lobby = l->watched_lobby.lock(); + if (watched_lobby) { + ret.emplace("WatchedLobbyID", watched_lobby->lobby_id); + } + auto watcher_lobby_ids_json = JSON::list(); + for (const auto& watcher_lobby : l->watcher_lobbies) { + watcher_lobby_ids_json.emplace_back(watcher_lobby->lobby_id); + } + ret.emplace("WatcherLobbyIDs", std::move(watcher_lobby_ids_json)); + ret.emplace("IsReplayLobby", !!l->battle_player); + } + + } else { // Not game + ret.emplace("IsPublic", l->check_flag(Lobby::Flag::PUBLIC)); + ret.emplace("IsDefault", l->check_flag(Lobby::Flag::DEFAULT)); + ret.emplace("IsOverflow", l->check_flag(Lobby::Flag::IS_OVERFLOW)); + ret.emplace("Block", l->block); + } + return ret; +} + +JSON HTTPServer::generate_game_server_clients_json() const { + return call_on_event_thread(this->state->base, [&]() { + auto res = JSON::list(); + for (const auto& it : this->state->channel_to_client) { + res.emplace_back(this->generate_game_client_json_st(it.second, this->state->item_name_index_opt(it.second->version()))); + } + return res; + }); +} + +JSON HTTPServer::generate_proxy_server_clients_json() const { + return call_on_event_thread(this->state->base, [&]() { + JSON res = JSON::list(); + if (this->state->proxy_server) { + for (const auto& it : this->state->proxy_server->all_sessions()) { + res.emplace_back(this->generate_proxy_client_json_st(it.second)); + } + } + return res; + }); +} + +JSON HTTPServer::generate_server_info_json() const { + return call_on_event_thread(this->state->base, [&]() { + size_t game_count = 0; + size_t lobby_count = 0; + for (const auto& it : this->state->id_to_lobby) { + if (it.second->is_game()) { + game_count++; + } else { + lobby_count++; + } + } + uint64_t uptime_usecs = now() - this->state->creation_time; + return JSON::dict({ + {"StartTimeUsecs", this->state->creation_time}, + {"StartTime", format_time(this->state->creation_time)}, + {"UptimeUsecs", uptime_usecs}, + {"Uptime", format_duration(uptime_usecs)}, + {"LobbyCount", lobby_count}, + {"GameCount", game_count}, + {"ClientCount", this->state->channel_to_client.size()}, + {"ProxySessionCount", this->state->proxy_server ? this->state->proxy_server->num_sessions() : 0}, + {"ServerName", this->state->name}, + }); + }); +} + +JSON HTTPServer::generate_lobbies_json() const { + return call_on_event_thread(this->state->base, [&]() { + JSON res = JSON::list(); + for (const auto& it : this->state->id_to_lobby) { + res.emplace_back(this->generate_lobby_json_st(it.second, this->state->item_name_index_opt(it.second->base_version))); + } + return res; + }); +} + +JSON HTTPServer::generate_summary_json() const { + auto ret = call_on_event_thread(this->state->base, [&]() { + auto clients_json = JSON::list(); + for (const auto& it : this->state->channel_to_client) { + auto c = it.second; + auto p = c->character(false, false); + auto l = c->lobby.lock(); + clients_json.emplace_back(JSON::dict({ + {"ID", c->id}, + {"AccountID", c->login ? c->login->account->account_id : JSON(nullptr)}, + {"Name", p ? p->disp.name.decode(it.second->language()) : JSON(nullptr)}, + {"Version", name_for_enum(it.second->version())}, + {"Language", name_for_language_code(it.second->language())}, + {"Level", p ? p->disp.stats.level + 1 : JSON(nullptr)}, + {"Class", p ? name_for_char_class(p->disp.visual.char_class) : JSON(nullptr)}, + {"SectionID", p ? name_for_section_id(p->disp.visual.section_id) : JSON(nullptr)}, + {"LobbyID", l ? l->lobby_id : JSON(nullptr)}, + })); + } + + auto proxy_clients_json = JSON::list(); + if (this->state->proxy_server) { + for (const auto& it : this->state->proxy_server->all_sessions()) { + proxy_clients_json.emplace_back(JSON::dict({ + {"AccountID", it.second->login ? it.second->login->account->account_id : JSON(nullptr)}, + {"Name", it.second->character_name}, + {"Version", name_for_enum(it.second->version())}, + {"Language", name_for_language_code(it.second->language())}, + })); + } + } + + auto games_json = JSON::list(); + for (const auto& it : this->state->id_to_lobby) { + auto l = it.second; + if (l->is_game()) { + auto game_json = JSON::dict({ + {"ID", l->lobby_id}, + {"Name", l->name}, + {"BaseVersion", name_for_enum(l->base_version)}, + {"Players", l->count_clients()}, + {"CheatsEnabled", l->check_flag(Lobby::Flag::CHEATS_ENABLED)}, + {"Episode", name_for_episode(l->episode)}, + {"HasPassword", !l->password.empty()}, + }); + if (l->episode == Episode::EP3) { + auto ep3s = l->ep3_server; + game_json.emplace("BattleInProgress", l->check_flag(Lobby::Flag::BATTLE_IN_PROGRESS)); + game_json.emplace("IsSpectatorTeam", l->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM)); + game_json.emplace("MapNumber", (ep3s && ep3s->last_chosen_map) ? ep3s->last_chosen_map->map_number : JSON(nullptr)); + game_json.emplace("Rules", (ep3s && ep3s->map_and_rules) ? ep3s->map_and_rules->rules.json() : nullptr); + } else { + game_json.emplace("QuestSelectionInProgress", l->check_flag(Lobby::Flag::QUEST_SELECTION_IN_PROGRESS)); + game_json.emplace("QuestInProgress", l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS)); + game_json.emplace("JoinableQuestInProgress", l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)); + game_json.emplace("SectionID", name_for_section_id(l->effective_section_id())); + game_json.emplace("Mode", name_for_mode(l->mode)); + game_json.emplace("Difficulty", name_for_difficulty(l->difficulty)); + game_json.emplace("Quest", this->generate_quest_json_st(l->quest)); + } + games_json.emplace_back(std::move(game_json)); + } + } + + return JSON::dict({ + {"Clients", std::move(clients_json)}, + {"ProxyClients", std::move(proxy_clients_json)}, + {"Games", std::move(games_json)}, + }); + }); + ret.emplace("Server", this->generate_server_info_json()); + return ret; +} + +JSON HTTPServer::generate_all_json() const { + return JSON::dict({ + {"Clients", this->generate_game_server_clients_json()}, + {"ProxyClients", this->generate_proxy_server_clients_json()}, + {"Lobbies", this->generate_lobbies_json()}, + {"Server", this->generate_server_info_json()}, + }); +} + +JSON HTTPServer::generate_ep3_cards_json(bool trial) const { + auto index = call_on_event_thread>(this->state->base, [&]() { + return trial ? this->state->ep3_card_index_trial : this->state->ep3_card_index; + }); + return index->definitions_json(); +} + +JSON HTTPServer::generate_common_tables_json() const { + auto [set_v2, set_v3_v4] = call_on_event_thread, shared_ptr>>(this->state->base, [&]() { + return make_pair(this->state->common_item_set_v2, this->state->common_item_set_v3_v4); + }); + return JSON::dict({{"v1_v2", set_v2->json()}, {"v3_v4", set_v3_v4->json()}}); +} + +JSON HTTPServer::generate_rare_tables_json() const { + auto sets = call_on_event_thread>>(this->state->base, [&]() { + return this->state->rare_item_sets; + }); + JSON ret = JSON::list(); + for (const auto& it : sets) { + ret.emplace_back(it.first); + } + return ret; +} + +JSON HTTPServer::generate_rare_table_json(const std::string& table_name) const { + try { + auto colls = call_on_event_thread, shared_ptr>>(this->state->base, [&]() { + const auto& table = this->state->rare_item_sets.at(table_name); + shared_ptr name_index; + if (ends_with(table_name, "-v1")) { + name_index = this->state->item_name_index_opt(Version::DC_V1); + } else if (ends_with(table_name, "-v2")) { + name_index = this->state->item_name_index_opt(Version::PC_V2); + } else if (ends_with(table_name, "-v3")) { + name_index = this->state->item_name_index_opt(Version::GC_V3); + } else if (ends_with(table_name, "-v4")) { + name_index = this->state->item_name_index_opt(Version::BB_V4); + } + return make_pair(table, name_index); + }); + return colls.first->json(colls.second); + } catch (const out_of_range&) { + throw http_error(404, "table does not exist"); + } +} + +void HTTPServer::handle_request(struct evhttp_request* req) { + shared_ptr ret; + uint32_t serialize_options = 0; + uint64_t start_time = now(); + string uri = evhttp_request_get_uri(req); + + try { + std::unordered_multimap query; + size_t query_pos = uri.find('?'); + if (query_pos != string::npos) { + query = this->parse_url_params(uri.substr(query_pos + 1)); + uri.resize(query_pos); + } + + static const string default_format_option = "false"; + if (this->get_url_param(query, "format", &default_format_option) == "true") { + serialize_options |= JSON::SerializeOption::FORMAT | JSON::SerializeOption::SORT_DICT_KEYS; + } + if (this->get_url_param(query, "hex", &default_format_option) == "true") { + serialize_options |= JSON::SerializeOption::HEX_INTEGERS; + } + + if (uri == "/") { + auto endpoints_json = JSON::list({ + "/y/data/ep3-cards", + "/y/data/ep3-cards-trial", + "/y/data/common-tables", + "/y/data/rare-tables", + "/y/data/rare-tables/", + "/y/data/config", + "/y/clients", + "/y/proxy-clients", + "/y/lobbies", + "/y/server", + "/y/summary", + "/y/all", + }); + ret = make_shared(JSON::dict({{"endpoints", std::move(endpoints_json)}})); + + } else if (uri == "/y/data/ep3-cards") { + ret = make_shared(this->generate_ep3_cards_json(false)); + } else if (uri == "/y/data/ep3-cards-trial") { + ret = make_shared(this->generate_ep3_cards_json(true)); + } else if (uri == "/y/data/common-tables") { + ret = make_shared(this->generate_common_tables_json()); + } else if (uri == "/y/data/rare-tables") { + ret = make_shared(this->generate_rare_tables_json()); + } else if (!strncmp(uri.c_str(), "/y/data/rare-tables/", 20)) { + ret = make_shared(this->generate_rare_table_json(uri.substr(20))); + } else if (uri == "/y/data/config") { + ret = call_on_event_thread>(this->state->base, [this]() { return this->state->config_json; }); + } else if (uri == "/y/clients") { + ret = make_shared(this->generate_game_server_clients_json()); + } else if (uri == "/y/proxy-clients") { + ret = make_shared(this->generate_proxy_server_clients_json()); + } else if (uri == "/y/lobbies") { + ret = make_shared(this->generate_lobbies_json()); + } else if (uri == "/y/server") { + ret = make_shared(this->generate_server_info_json()); + } else if (uri == "/y/summary") { + ret = make_shared(this->generate_summary_json()); + } else if (uri == "/y/all") { + ret = make_shared(this->generate_all_json()); + + } else { + throw http_error(404, "unknown action"); + } + + } catch (const http_error& e) { + unique_ptr out_buffer(evbuffer_new(), evbuffer_free); + evbuffer_add_printf(out_buffer.get(), "%s", e.what()); + this->send_response(req, e.code, "text/plain", out_buffer.get()); + return; + + } catch (const exception& e) { + unique_ptr out_buffer(evbuffer_new(), evbuffer_free); + evbuffer_add_printf(out_buffer.get(), "Error during request: %s", e.what()); + this->send_response(req, 500, "text/plain", out_buffer.get()); + server_log.warning("internal server error during http request: %s", e.what()); + return; + } + + uint64_t handler_end = now(); + unique_ptr out_buffer(evbuffer_new(), evbuffer_free); + string* serialized = new string(ret->serialize(JSON::SerializeOption::ESCAPE_CONTROLS_ONLY | serialize_options)); + size_t size = serialized->size(); + uint64_t serialize_end = now(); + auto cleanup = +[](const void*, size_t, void* s) -> void { + delete reinterpret_cast(s); + }; + evbuffer_add_reference(out_buffer.get(), serialized->data(), serialized->size(), cleanup, serialized); + this->send_response(req, 200, "application/json", out_buffer.get()); + + string handler_time = format_duration(handler_end - start_time); + string serialize_time = format_duration(serialize_end - handler_end); + string size_str = format_size(size); + server_log.info("[HTTPServer] %s in [handler: %s, serialize: %s, size: %s]", + uri.c_str(), handler_time.c_str(), serialize_time.c_str(), size_str.c_str()); +} + +void HTTPServer::thread_fn() { + event_base_loop(this->base.get(), EVLOOP_NO_EXIT_ON_EMPTY); +} diff --git a/src/HTTPServer.hh b/src/HTTPServer.hh index e2cd56cf..164e623e 100644 --- a/src/HTTPServer.hh +++ b/src/HTTPServer.hh @@ -1,76 +1,76 @@ -#pragma once - -#include -#include -#include -#include - -#include -#include - -#include "ProxyServer.hh" -#include "ServerState.hh" - -class HTTPServer { -public: - HTTPServer(std::shared_ptr state); - HTTPServer(const HTTPServer&) = delete; - HTTPServer(HTTPServer&&) = delete; - HTTPServer& operator=(const HTTPServer&) = delete; - HTTPServer& operator=(HTTPServer&&) = delete; - virtual ~HTTPServer() = default; - - void listen(const std::string& socket_path); - void listen(const std::string& addr, int port); - void listen(int port); - void add_socket(int fd); - - void schedule_stop(); - void wait_for_stop(); - -protected: - class http_error : public std::runtime_error { - public: - http_error(int code, const std::string& what); - int code; - }; - - std::shared_ptr state; - std::shared_ptr base; - std::shared_ptr http; - std::thread th; - - void thread_fn(); - - static void dispatch_handle_request(struct evhttp_request* req, void* ctx); - void handle_request(struct evhttp_request* req); - - static const std::unordered_map explanation_for_response_code; - static void send_response(struct evhttp_request* req, int code, const char* content_type, struct evbuffer* b); - static void send_response(struct evhttp_request* req, int code, const char* content_type, const char* fmt, ...); - - static std::unordered_multimap parse_url_params(const std::string& query); - static std::unordered_map parse_url_params_unique(const std::string& query); - static const std::string& get_url_param( - const std::unordered_multimap& params, - const std::string& key, - const std::string* _default = nullptr); - - static JSON generate_quest_json_st(std::shared_ptr q); - static JSON generate_client_config_json_st(const Client::Config& config); - static JSON generate_account_json_st(std::shared_ptr a); - static JSON generate_game_client_json_st(std::shared_ptr c, std::shared_ptr item_name_index); - static JSON generate_proxy_client_json_st(std::shared_ptr ses); - static JSON generate_lobby_json_st(std::shared_ptr l, std::shared_ptr item_name_index); - JSON generate_game_server_clients_json() const; - JSON generate_proxy_server_clients_json() const; - JSON generate_server_info_json() const; - JSON generate_lobbies_json() const; - JSON generate_summary_json() const; - JSON generate_all_json() const; - - JSON generate_ep3_cards_json(bool trial) const; - JSON generate_common_tables_json() const; - JSON generate_rare_tables_json() const; - JSON generate_rare_table_json(const std::string& table_name) const; -}; +#pragma once + +#include +#include +#include +#include + +#include +#include + +#include "ProxyServer.hh" +#include "ServerState.hh" + +class HTTPServer { +public: + HTTPServer(std::shared_ptr state); + HTTPServer(const HTTPServer&) = delete; + HTTPServer(HTTPServer&&) = delete; + HTTPServer& operator=(const HTTPServer&) = delete; + HTTPServer& operator=(HTTPServer&&) = delete; + virtual ~HTTPServer() = default; + + void listen(const std::string& socket_path); + void listen(const std::string& addr, int port); + void listen(int port); + void add_socket(int fd); + + void schedule_stop(); + void wait_for_stop(); + +protected: + class http_error : public std::runtime_error { + public: + http_error(int code, const std::string& what); + int code; + }; + + std::shared_ptr state; + std::shared_ptr base; + std::shared_ptr http; + std::thread th; + + void thread_fn(); + + static void dispatch_handle_request(struct evhttp_request* req, void* ctx); + void handle_request(struct evhttp_request* req); + + static const std::unordered_map explanation_for_response_code; + static void send_response(struct evhttp_request* req, int code, const char* content_type, struct evbuffer* b); + static void send_response(struct evhttp_request* req, int code, const char* content_type, const char* fmt, ...); + + static std::unordered_multimap parse_url_params(const std::string& query); + static std::unordered_map parse_url_params_unique(const std::string& query); + static const std::string& get_url_param( + const std::unordered_multimap& params, + const std::string& key, + const std::string* _default = nullptr); + + static JSON generate_quest_json_st(std::shared_ptr q); + static JSON generate_client_config_json_st(const Client::Config& config); + static JSON generate_account_json_st(std::shared_ptr a); + static JSON generate_game_client_json_st(std::shared_ptr c, std::shared_ptr item_name_index); + static JSON generate_proxy_client_json_st(std::shared_ptr ses); + static JSON generate_lobby_json_st(std::shared_ptr l, std::shared_ptr item_name_index); + JSON generate_game_server_clients_json() const; + JSON generate_proxy_server_clients_json() const; + JSON generate_server_info_json() const; + JSON generate_lobbies_json() const; + JSON generate_summary_json() const; + JSON generate_all_json() const; + + JSON generate_ep3_cards_json(bool trial) const; + JSON generate_common_tables_json() const; + JSON generate_rare_tables_json() const; + JSON generate_rare_table_json(const std::string& table_name) const; +}; diff --git a/src/IPV4RangeSet.cc b/src/IPV4RangeSet.cc index 316935ae..289bbcd5 100644 --- a/src/IPV4RangeSet.cc +++ b/src/IPV4RangeSet.cc @@ -1,73 +1,73 @@ -#include "IPV4RangeSet.hh" - -#include - -using namespace std; - -IPV4RangeSet::IPV4RangeSet(const JSON& json) { - for (const auto& it : json.as_list()) { - // String should be of the form a.b.c.d or a.b.c.d/e - auto tokens = split(it->as_string(), '/'); - - size_t mask_bits; - if (tokens.size() == 1) { - mask_bits = 32; - } else if (tokens.size() == 2) { - mask_bits = stoul(tokens[1], nullptr, 10); - if (mask_bits > 32) { - throw runtime_error("invalid IPv4 address range"); - } - } else { - throw runtime_error("invalid IPv4 address range"); - } - - auto addr_tokens = split(tokens[0], '.'); - if (addr_tokens.size() != 4) { - throw runtime_error("invalid IPv4 address"); - } - uint32_t addr = 0; - for (size_t z = 0; z < 4; z++) { - size_t end_pos = 0; - size_t new_byte = stoul(addr_tokens[z], &end_pos, 10); - if (end_pos != addr_tokens[z].size() || new_byte > 0xFF) { - throw runtime_error("invalid IPv4 address"); - } - addr = (addr << 8) | new_byte; - } - addr &= (0xFFFFFFFF << (32 - mask_bits)); - - this->ranges.emplace(addr, mask_bits); - } -} - -JSON IPV4RangeSet::json() const { - auto ret = JSON::list(); - for (const auto& it : this->ranges) { - uint32_t addr = it.first; - uint8_t mask_bits = it.second; - ret.emplace_back(string_printf("%hhu.%hhu.%hhu.%hhu/%hhu", - static_cast((addr >> 24) & 0xFF), - static_cast((addr >> 16) & 0xFF), - static_cast((addr >> 8) & 0xFF), - static_cast(addr & 0xFF), - mask_bits)); - } - return ret; -} - -bool IPV4RangeSet::check(uint32_t addr) const { - auto it = this->ranges.upper_bound(addr); - if (it == this->ranges.begin()) { - return false; // addr is before any range - } - const auto& range = *(--it); - return (((range.first ^ addr) & (0xFFFFFFFF << (32 - range.second))) == 0); -} - -bool IPV4RangeSet::check(const struct sockaddr_storage& ss) const { - if (ss.ss_family != AF_INET) { - return false; - } - const sockaddr_in* sin = reinterpret_cast(&ss); - return this->check(ntohl(sin->sin_addr.s_addr)); -} +#include "IPV4RangeSet.hh" + +#include + +using namespace std; + +IPV4RangeSet::IPV4RangeSet(const JSON& json) { + for (const auto& it : json.as_list()) { + // String should be of the form a.b.c.d or a.b.c.d/e + auto tokens = split(it->as_string(), '/'); + + size_t mask_bits; + if (tokens.size() == 1) { + mask_bits = 32; + } else if (tokens.size() == 2) { + mask_bits = stoul(tokens[1], nullptr, 10); + if (mask_bits > 32) { + throw runtime_error("invalid IPv4 address range"); + } + } else { + throw runtime_error("invalid IPv4 address range"); + } + + auto addr_tokens = split(tokens[0], '.'); + if (addr_tokens.size() != 4) { + throw runtime_error("invalid IPv4 address"); + } + uint32_t addr = 0; + for (size_t z = 0; z < 4; z++) { + size_t end_pos = 0; + size_t new_byte = stoul(addr_tokens[z], &end_pos, 10); + if (end_pos != addr_tokens[z].size() || new_byte > 0xFF) { + throw runtime_error("invalid IPv4 address"); + } + addr = (addr << 8) | new_byte; + } + addr &= (0xFFFFFFFF << (32 - mask_bits)); + + this->ranges.emplace(addr, mask_bits); + } +} + +JSON IPV4RangeSet::json() const { + auto ret = JSON::list(); + for (const auto& it : this->ranges) { + uint32_t addr = it.first; + uint8_t mask_bits = it.second; + ret.emplace_back(string_printf("%hhu.%hhu.%hhu.%hhu/%hhu", + static_cast((addr >> 24) & 0xFF), + static_cast((addr >> 16) & 0xFF), + static_cast((addr >> 8) & 0xFF), + static_cast(addr & 0xFF), + mask_bits)); + } + return ret; +} + +bool IPV4RangeSet::check(uint32_t addr) const { + auto it = this->ranges.upper_bound(addr); + if (it == this->ranges.begin()) { + return false; // addr is before any range + } + const auto& range = *(--it); + return (((range.first ^ addr) & (0xFFFFFFFF << (32 - range.second))) == 0); +} + +bool IPV4RangeSet::check(const struct sockaddr_storage& ss) const { + if (ss.ss_family != AF_INET) { + return false; + } + const sockaddr_in* sin = reinterpret_cast(&ss); + return this->check(ntohl(sin->sin_addr.s_addr)); +} diff --git a/src/IPV4RangeSet.hh b/src/IPV4RangeSet.hh index 6a67ffa6..b8fce939 100644 --- a/src/IPV4RangeSet.hh +++ b/src/IPV4RangeSet.hh @@ -1,18 +1,18 @@ -#pragma once - -#include -#include - -class IPV4RangeSet { -public: - IPV4RangeSet() = default; - explicit IPV4RangeSet(const JSON& json); - - JSON json() const; - - bool check(uint32_t addr) const; - bool check(const struct sockaddr_storage& ss) const; - -protected: - std::map ranges; // {addr: mask_bits} -}; +#pragma once + +#include +#include + +class IPV4RangeSet { +public: + IPV4RangeSet() = default; + explicit IPV4RangeSet(const JSON& json); + + JSON json() const; + + bool check(uint32_t addr) const; + bool check(const struct sockaddr_storage& ss) const; + +protected: + std::map ranges; // {addr: mask_bits} +}; diff --git a/src/IntegralExpression.cc b/src/IntegralExpression.cc index 8f71cb46..69506509 100644 --- a/src/IntegralExpression.cc +++ b/src/IntegralExpression.cc @@ -1,455 +1,455 @@ -#include "IntegralExpression.hh" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "CommandFormats.hh" -#include "Compression.hh" -#include "Loggers.hh" -#include "PSOEncryption.hh" -#include "QuestScript.hh" -#include "SaveFileFormats.hh" -#include "Text.hh" - -using namespace std; - -IntegralExpression::IntegralExpression(const string& text) - : root(this->parse_expr(text)) {} - -IntegralExpression::BinaryOperatorNode::BinaryOperatorNode( - Type type, unique_ptr&& left, unique_ptr&& right) - : type(type), - left(std::move(left)), - right(std::move(right)) {} - -bool IntegralExpression::BinaryOperatorNode::operator==(const Node& other) const { - try { - const BinaryOperatorNode& other_bin = dynamic_cast(other); - return other_bin.type == this->type && *other_bin.left == *this->left && *other_bin.right == *this->right; - } catch (const bad_cast&) { - return false; - } -} - -int64_t IntegralExpression::BinaryOperatorNode::evaluate(const Env& env) const { - switch (this->type) { - case Type::LOGICAL_OR: - return this->left->evaluate(env) || this->right->evaluate(env); - case Type::LOGICAL_AND: - return this->left->evaluate(env) && this->right->evaluate(env); - case Type::BITWISE_OR: - return this->left->evaluate(env) | this->right->evaluate(env); - case Type::BITWISE_AND: - return this->left->evaluate(env) & this->right->evaluate(env); - case Type::BITWISE_XOR: - return this->left->evaluate(env) ^ this->right->evaluate(env); - case Type::LEFT_SHIFT: - return this->left->evaluate(env) << this->right->evaluate(env); - case Type::RIGHT_SHIFT: - return this->left->evaluate(env) >> this->right->evaluate(env); - case Type::LESS_THAN: - return this->left->evaluate(env) < this->right->evaluate(env); - case Type::GREATER_THAN: - return this->left->evaluate(env) > this->right->evaluate(env); - case Type::LESS_OR_EQUAL: - return this->left->evaluate(env) <= this->right->evaluate(env); - case Type::GREATER_OR_EQUAL: - return this->left->evaluate(env) >= this->right->evaluate(env); - case Type::EQUAL: - return this->left->evaluate(env) == this->right->evaluate(env); - case Type::NOT_EQUAL: - return this->left->evaluate(env) != this->right->evaluate(env); - case Type::ADD: - return this->left->evaluate(env) + this->right->evaluate(env); - case Type::SUBTRACT: - return this->left->evaluate(env) - this->right->evaluate(env); - case Type::MULTIPLY: - return this->left->evaluate(env) * this->right->evaluate(env); - case Type::DIVIDE: - return this->left->evaluate(env) / this->right->evaluate(env); - case Type::MODULUS: - return this->left->evaluate(env) % this->right->evaluate(env); - default: - throw logic_error("invalid binary operator type"); - } -} - -string IntegralExpression::BinaryOperatorNode::str() const { - switch (this->type) { - case Type::LOGICAL_OR: - return "(" + this->left->str() + ") || (" + this->right->str() + ")"; - case Type::LOGICAL_AND: - return "(" + this->left->str() + ") && (" + this->right->str() + ")"; - case Type::BITWISE_OR: - return "(" + this->left->str() + ") | (" + this->right->str() + ")"; - case Type::BITWISE_AND: - return "(" + this->left->str() + ") & (" + this->right->str() + ")"; - case Type::BITWISE_XOR: - return "(" + this->left->str() + ") ^ (" + this->right->str() + ")"; - case Type::LEFT_SHIFT: - return "(" + this->left->str() + ") << (" + this->right->str() + ")"; - case Type::RIGHT_SHIFT: - return "(" + this->left->str() + ") >> (" + this->right->str() + ")"; - case Type::LESS_THAN: - return "(" + this->left->str() + ") < (" + this->right->str() + ")"; - case Type::GREATER_THAN: - return "(" + this->left->str() + ") > (" + this->right->str() + ")"; - case Type::LESS_OR_EQUAL: - return "(" + this->left->str() + ") <= (" + this->right->str() + ")"; - case Type::GREATER_OR_EQUAL: - return "(" + this->left->str() + ") >= (" + this->right->str() + ")"; - case Type::EQUAL: - return "(" + this->left->str() + ") == (" + this->right->str() + ")"; - case Type::NOT_EQUAL: - return "(" + this->left->str() + ") != (" + this->right->str() + ")"; - case Type::ADD: - return "(" + this->left->str() + ") + (" + this->right->str() + ")"; - case Type::SUBTRACT: - return "(" + this->left->str() + ") - (" + this->right->str() + ")"; - case Type::MULTIPLY: - return "(" + this->left->str() + ") * (" + this->right->str() + ")"; - case Type::DIVIDE: - return "(" + this->left->str() + ") / (" + this->right->str() + ")"; - case Type::MODULUS: - return "(" + this->left->str() + ") % (" + this->right->str() + ")"; - default: - throw logic_error("invalid binary operator type"); - } -} - -IntegralExpression::UnaryOperatorNode::UnaryOperatorNode(Type type, unique_ptr&& sub) - : type(type), - sub(std::move(sub)) {} - -bool IntegralExpression::UnaryOperatorNode::operator==(const Node& other) const { - try { - const UnaryOperatorNode& other_un = dynamic_cast(other); - return other_un.type == this->type && *other_un.sub == *this->sub; - } catch (const bad_cast&) { - return false; - } -} - -int64_t IntegralExpression::UnaryOperatorNode::evaluate(const Env& env) const { - switch (this->type) { - case Type::LOGICAL_NOT: - return !this->sub->evaluate(env); - case Type::BITWISE_NOT: - return ~this->sub->evaluate(env); - case Type::NEGATIVE: - return -this->sub->evaluate(env); - default: - throw logic_error("invalid unary operator type"); - } -} - -string IntegralExpression::UnaryOperatorNode::str() const { - switch (this->type) { - case Type::LOGICAL_NOT: - return "!(" + this->sub->str() + ")"; - case Type::BITWISE_NOT: - return "~(" + this->sub->str() + ")"; - case Type::NEGATIVE: - return "-(" + this->sub->str() + ")"; - default: - throw logic_error("invalid unary operator type"); - } -} - -IntegralExpression::FlagLookupNode::FlagLookupNode(uint16_t flag_index) - : flag_index(flag_index) {} - -bool IntegralExpression::FlagLookupNode::operator==(const Node& other) const { - try { - const FlagLookupNode& other_flag = dynamic_cast(other); - return other_flag.flag_index == this->flag_index; - } catch (const bad_cast&) { - return false; - } -} - -int64_t IntegralExpression::FlagLookupNode::evaluate(const Env& env) const { - if (!env.flags) { - throw runtime_error("quest flags not available"); - } - return env.flags->get(this->flag_index) ? 1 : 0; -} - -string IntegralExpression::FlagLookupNode::str() const { - return string_printf("F_%04hX", this->flag_index); -} - -IntegralExpression::ChallengeCompletionLookupNode::ChallengeCompletionLookupNode( - Episode episode, uint8_t stage_index) - : episode(episode), - stage_index(stage_index) {} - -bool IntegralExpression::ChallengeCompletionLookupNode::operator==(const Node& other) const { - try { - const ChallengeCompletionLookupNode& other_cc = dynamic_cast(other); - return other_cc.episode == this->episode && other_cc.stage_index == this->stage_index; - } catch (const bad_cast&) { - return false; - } -} - -int64_t IntegralExpression::ChallengeCompletionLookupNode::evaluate(const Env& env) const { - if (!env.challenge_records) { - throw runtime_error("challenge records not available"); - } - if (this->episode == Episode::EP1) { - return env.challenge_records->times_ep1_online.at(this->stage_index).has_value(); - } else if (this->episode == Episode::EP2) { - return env.challenge_records->times_ep2_online.at(this->stage_index).has_value(); - } - return false; -} - -string IntegralExpression::ChallengeCompletionLookupNode::str() const { - return string_printf("CC_%s_%hhu", abbreviation_for_episode(this->episode), static_cast(this->stage_index + 1)); -} - -IntegralExpression::TeamRewardLookupNode::TeamRewardLookupNode(const string& reward_name) - : reward_name(reward_name) {} - -bool IntegralExpression::TeamRewardLookupNode::operator==(const Node& other) const { - try { - const TeamRewardLookupNode& other_team_reward = dynamic_cast(other); - return other_team_reward.reward_name == this->reward_name; - } catch (const bad_cast&) { - return false; - } -} - -int64_t IntegralExpression::TeamRewardLookupNode::evaluate(const Env& env) const { - return (env.team && env.team->has_reward(this->reward_name)) ? 1 : 0; -} - -string IntegralExpression::TeamRewardLookupNode::str() const { - return "T_" + this->reward_name; -} - -IntegralExpression::NumPlayersLookupNode::NumPlayersLookupNode() {} - -bool IntegralExpression::NumPlayersLookupNode::operator==(const Node& other) const { - return dynamic_cast(&other) != nullptr; -} - -int64_t IntegralExpression::NumPlayersLookupNode::evaluate(const Env& env) const { - return env.num_players; -} - -string IntegralExpression::NumPlayersLookupNode::str() const { - return "V_NumPlayers"; -} - -IntegralExpression::EventLookupNode::EventLookupNode() {} - -bool IntegralExpression::EventLookupNode::operator==(const Node& other) const { - return dynamic_cast(&other) != nullptr; -} - -int64_t IntegralExpression::EventLookupNode::evaluate(const Env& env) const { - return env.event; -} - -string IntegralExpression::EventLookupNode::str() const { - return "V_Event"; -} - -IntegralExpression::V1PresenceLookupNode::V1PresenceLookupNode() {} - -bool IntegralExpression::V1PresenceLookupNode::operator==(const Node& other) const { - return dynamic_cast(&other) != nullptr; -} - -int64_t IntegralExpression::V1PresenceLookupNode::evaluate(const Env& env) const { - return env.v1_present ? 1 : 0; -} - -string IntegralExpression::V1PresenceLookupNode::str() const { - return "V_V1Present"; -} - -IntegralExpression::ConstantNode::ConstantNode(int64_t value) - : value(value) {} - -bool IntegralExpression::ConstantNode::operator==(const Node& other) const { - try { - const ConstantNode& other_const = dynamic_cast(other); - return other_const.value == this->value; - } catch (const bad_cast&) { - return false; - } -} - -int64_t IntegralExpression::ConstantNode::evaluate(const Env&) const { - return this->value; -} - -string IntegralExpression::ConstantNode::str() const { - return string_printf("%" PRId64, this->value); -} - -unique_ptr IntegralExpression::parse_expr(string_view text) { - // Strip off spaces and fully-enclosing parentheses - for (;;) { - size_t starting_size = text.size(); - while (text.at(0) == ' ') { - text = text.substr(1); - } - while (text.at(text.size() - 1) == ' ') { - text = text.substr(0, text.size() - 1); - } - if (text.at(0) == '(' && text.at(text.size() - 1) == ')') { - // It doesn't suffice to just check the first ant last characters, since - // text could be like "(a) && (b)". Instead, we ignore the first and last - // characters, and don't strip anything if the internal parentheses are - // unbalanced. - size_t paren_level = 1; - for (size_t z = 1; z < text.size() - 1; z++) { - if (text[z] == '(') { - paren_level++; - } else if (text[z] == ')') { - paren_level--; - if (paren_level == 0) { - break; - } - } - } - if (paren_level > 0) { - text = text.substr(1, text.size() - 2); - } - } - if (text.size() == starting_size) { - break; - } - } - if (text.empty()) { - throw runtime_error("invalid expression"); - } - - // Check for binary operators at the root level - using BinType = BinaryOperatorNode::Type; - static const vector>> binary_operator_levels = { - {{make_pair("*", BinType::MULTIPLY)}, {make_pair("/", BinType::DIVIDE)}, {make_pair("%", BinType::MODULUS)}}, - {{make_pair("+", BinType::ADD)}, {make_pair("-", BinType::SUBTRACT)}}, - {{make_pair("<<", BinType::LEFT_SHIFT)}, {make_pair(">>", BinType::RIGHT_SHIFT)}}, - {{make_pair("<=", BinType::LESS_OR_EQUAL)}, {make_pair(">=", BinType::GREATER_OR_EQUAL)}, {make_pair("<", BinType::LESS_THAN)}, {make_pair(">", BinType::GREATER_THAN)}}, - {{make_pair("==", BinType::EQUAL)}, {make_pair("!=", BinType::NOT_EQUAL)}}, - {{make_pair("&", BinType::BITWISE_AND)}}, - {{make_pair("^", BinType::BITWISE_XOR)}}, - {{make_pair("|", BinType::BITWISE_OR)}}, - {{make_pair("&&", BinType::LOGICAL_AND)}}, - {{make_pair("||", BinType::LOGICAL_OR)}}, - }; - for (const auto& operators : binary_operator_levels) { - size_t paren_level = 0; - for (size_t z = 0; z < text.size() - 1; z++) { - if (text[z] == '(') { - paren_level++; - continue; - } else if (text[z] == ')') { - paren_level--; - continue; - } - if (!paren_level) { - for (const auto& oper : operators) { - // Awful hack (because I'm too lazy to add a tokenization step): if - // the operator is followed or preceded by another copy of itself, - // don't match it (this prevents us from matching & when the token is - // actually &&) - if ((text.size() > z + oper.first.size()) && - ((z < oper.first.size()) || (text.compare(z - oper.first.size(), oper.first.size(), oper.first) != 0)) && - (text.compare(z, oper.first.size(), oper.first) == 0) && - (text.compare(z + oper.first.size(), oper.first.size(), oper.first) != 0)) { - auto left = IntegralExpression::parse_expr(text.substr(0, z)); - auto right = IntegralExpression::parse_expr(text.substr(z + oper.first.size())); - return make_unique(oper.second, std::move(left), std::move(right)); - } - } - } - } - } - - // Check for unary operators - if (text[0] == '!') { - return make_unique(UnaryOperatorNode::Type::LOGICAL_NOT, - IntegralExpression::parse_expr(text.substr(1))); - } else if (text[0] == '~') { - return make_unique(UnaryOperatorNode::Type::BITWISE_NOT, - IntegralExpression::parse_expr(text.substr(1))); - } else if (text[0] == '-') { - return make_unique(UnaryOperatorNode::Type::NEGATIVE, - IntegralExpression::parse_expr(text.substr(1))); - } - - // Check for env lookups - if (text.starts_with("F_")) { - char* endptr = nullptr; - uint64_t flag = strtoul(text.data() + 2, &endptr, 16); - if (endptr != text.data() + text.size()) { - throw runtime_error("invalid flag lookup token"); - } - if (flag >= 0x400) { - throw runtime_error("invalid flag index"); - } - return make_unique(flag); - } - if (text.starts_with("CC_")) { - Episode episode; - if (text.starts_with("CC_Ep1_")) { - episode = Episode::EP1; - } else if (text.starts_with("CC_Ep2_")) { - episode = Episode::EP2; - } else { - throw runtime_error("invalid challenge episode"); - } - char* endptr = nullptr; - uint64_t stage_index = strtoul(text.data() + 7, &endptr, 0) - 1; - if (endptr != text.data() + text.size()) { - throw runtime_error("invalid challenge completion lookup token"); - } - if ((episode == Episode::EP1 && stage_index > 8) || (episode == Episode::EP2 && stage_index > 4)) { - throw runtime_error("invalid challenge stage index"); - } - return make_unique(episode, stage_index); - } - if (text.starts_with("T_")) { - return make_unique(string(text.substr(2))); - } - if (text == "V_NumPlayers") { - return make_unique(); - } - if (text == "V_Event") { - return make_unique(); - } - if (text == "V_V1Present") { - return make_unique(); - } - - // Check for constants - if (text == "true") { - return make_unique(1); - } - if (text == "false") { - return make_unique(0); - } - try { - size_t endpos; - int64_t v = stoll(string(text), &endpos, 0); - if (endpos == text.size()) { - return make_unique(v); - } - } catch (const exception&) { - } - throw runtime_error("unparseable expression"); -} +#include "IntegralExpression.hh" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "CommandFormats.hh" +#include "Compression.hh" +#include "Loggers.hh" +#include "PSOEncryption.hh" +#include "QuestScript.hh" +#include "SaveFileFormats.hh" +#include "Text.hh" + +using namespace std; + +IntegralExpression::IntegralExpression(const string& text) + : root(this->parse_expr(text)) {} + +IntegralExpression::BinaryOperatorNode::BinaryOperatorNode( + Type type, unique_ptr&& left, unique_ptr&& right) + : type(type), + left(std::move(left)), + right(std::move(right)) {} + +bool IntegralExpression::BinaryOperatorNode::operator==(const Node& other) const { + try { + const BinaryOperatorNode& other_bin = dynamic_cast(other); + return other_bin.type == this->type && *other_bin.left == *this->left && *other_bin.right == *this->right; + } catch (const bad_cast&) { + return false; + } +} + +int64_t IntegralExpression::BinaryOperatorNode::evaluate(const Env& env) const { + switch (this->type) { + case Type::LOGICAL_OR: + return this->left->evaluate(env) || this->right->evaluate(env); + case Type::LOGICAL_AND: + return this->left->evaluate(env) && this->right->evaluate(env); + case Type::BITWISE_OR: + return this->left->evaluate(env) | this->right->evaluate(env); + case Type::BITWISE_AND: + return this->left->evaluate(env) & this->right->evaluate(env); + case Type::BITWISE_XOR: + return this->left->evaluate(env) ^ this->right->evaluate(env); + case Type::LEFT_SHIFT: + return this->left->evaluate(env) << this->right->evaluate(env); + case Type::RIGHT_SHIFT: + return this->left->evaluate(env) >> this->right->evaluate(env); + case Type::LESS_THAN: + return this->left->evaluate(env) < this->right->evaluate(env); + case Type::GREATER_THAN: + return this->left->evaluate(env) > this->right->evaluate(env); + case Type::LESS_OR_EQUAL: + return this->left->evaluate(env) <= this->right->evaluate(env); + case Type::GREATER_OR_EQUAL: + return this->left->evaluate(env) >= this->right->evaluate(env); + case Type::EQUAL: + return this->left->evaluate(env) == this->right->evaluate(env); + case Type::NOT_EQUAL: + return this->left->evaluate(env) != this->right->evaluate(env); + case Type::ADD: + return this->left->evaluate(env) + this->right->evaluate(env); + case Type::SUBTRACT: + return this->left->evaluate(env) - this->right->evaluate(env); + case Type::MULTIPLY: + return this->left->evaluate(env) * this->right->evaluate(env); + case Type::DIVIDE: + return this->left->evaluate(env) / this->right->evaluate(env); + case Type::MODULUS: + return this->left->evaluate(env) % this->right->evaluate(env); + default: + throw logic_error("invalid binary operator type"); + } +} + +string IntegralExpression::BinaryOperatorNode::str() const { + switch (this->type) { + case Type::LOGICAL_OR: + return "(" + this->left->str() + ") || (" + this->right->str() + ")"; + case Type::LOGICAL_AND: + return "(" + this->left->str() + ") && (" + this->right->str() + ")"; + case Type::BITWISE_OR: + return "(" + this->left->str() + ") | (" + this->right->str() + ")"; + case Type::BITWISE_AND: + return "(" + this->left->str() + ") & (" + this->right->str() + ")"; + case Type::BITWISE_XOR: + return "(" + this->left->str() + ") ^ (" + this->right->str() + ")"; + case Type::LEFT_SHIFT: + return "(" + this->left->str() + ") << (" + this->right->str() + ")"; + case Type::RIGHT_SHIFT: + return "(" + this->left->str() + ") >> (" + this->right->str() + ")"; + case Type::LESS_THAN: + return "(" + this->left->str() + ") < (" + this->right->str() + ")"; + case Type::GREATER_THAN: + return "(" + this->left->str() + ") > (" + this->right->str() + ")"; + case Type::LESS_OR_EQUAL: + return "(" + this->left->str() + ") <= (" + this->right->str() + ")"; + case Type::GREATER_OR_EQUAL: + return "(" + this->left->str() + ") >= (" + this->right->str() + ")"; + case Type::EQUAL: + return "(" + this->left->str() + ") == (" + this->right->str() + ")"; + case Type::NOT_EQUAL: + return "(" + this->left->str() + ") != (" + this->right->str() + ")"; + case Type::ADD: + return "(" + this->left->str() + ") + (" + this->right->str() + ")"; + case Type::SUBTRACT: + return "(" + this->left->str() + ") - (" + this->right->str() + ")"; + case Type::MULTIPLY: + return "(" + this->left->str() + ") * (" + this->right->str() + ")"; + case Type::DIVIDE: + return "(" + this->left->str() + ") / (" + this->right->str() + ")"; + case Type::MODULUS: + return "(" + this->left->str() + ") % (" + this->right->str() + ")"; + default: + throw logic_error("invalid binary operator type"); + } +} + +IntegralExpression::UnaryOperatorNode::UnaryOperatorNode(Type type, unique_ptr&& sub) + : type(type), + sub(std::move(sub)) {} + +bool IntegralExpression::UnaryOperatorNode::operator==(const Node& other) const { + try { + const UnaryOperatorNode& other_un = dynamic_cast(other); + return other_un.type == this->type && *other_un.sub == *this->sub; + } catch (const bad_cast&) { + return false; + } +} + +int64_t IntegralExpression::UnaryOperatorNode::evaluate(const Env& env) const { + switch (this->type) { + case Type::LOGICAL_NOT: + return !this->sub->evaluate(env); + case Type::BITWISE_NOT: + return ~this->sub->evaluate(env); + case Type::NEGATIVE: + return -this->sub->evaluate(env); + default: + throw logic_error("invalid unary operator type"); + } +} + +string IntegralExpression::UnaryOperatorNode::str() const { + switch (this->type) { + case Type::LOGICAL_NOT: + return "!(" + this->sub->str() + ")"; + case Type::BITWISE_NOT: + return "~(" + this->sub->str() + ")"; + case Type::NEGATIVE: + return "-(" + this->sub->str() + ")"; + default: + throw logic_error("invalid unary operator type"); + } +} + +IntegralExpression::FlagLookupNode::FlagLookupNode(uint16_t flag_index) + : flag_index(flag_index) {} + +bool IntegralExpression::FlagLookupNode::operator==(const Node& other) const { + try { + const FlagLookupNode& other_flag = dynamic_cast(other); + return other_flag.flag_index == this->flag_index; + } catch (const bad_cast&) { + return false; + } +} + +int64_t IntegralExpression::FlagLookupNode::evaluate(const Env& env) const { + if (!env.flags) { + throw runtime_error("quest flags not available"); + } + return env.flags->get(this->flag_index) ? 1 : 0; +} + +string IntegralExpression::FlagLookupNode::str() const { + return string_printf("F_%04hX", this->flag_index); +} + +IntegralExpression::ChallengeCompletionLookupNode::ChallengeCompletionLookupNode( + Episode episode, uint8_t stage_index) + : episode(episode), + stage_index(stage_index) {} + +bool IntegralExpression::ChallengeCompletionLookupNode::operator==(const Node& other) const { + try { + const ChallengeCompletionLookupNode& other_cc = dynamic_cast(other); + return other_cc.episode == this->episode && other_cc.stage_index == this->stage_index; + } catch (const bad_cast&) { + return false; + } +} + +int64_t IntegralExpression::ChallengeCompletionLookupNode::evaluate(const Env& env) const { + if (!env.challenge_records) { + throw runtime_error("challenge records not available"); + } + if (this->episode == Episode::EP1) { + return env.challenge_records->times_ep1_online.at(this->stage_index).has_value(); + } else if (this->episode == Episode::EP2) { + return env.challenge_records->times_ep2_online.at(this->stage_index).has_value(); + } + return false; +} + +string IntegralExpression::ChallengeCompletionLookupNode::str() const { + return string_printf("CC_%s_%hhu", abbreviation_for_episode(this->episode), static_cast(this->stage_index + 1)); +} + +IntegralExpression::TeamRewardLookupNode::TeamRewardLookupNode(const string& reward_name) + : reward_name(reward_name) {} + +bool IntegralExpression::TeamRewardLookupNode::operator==(const Node& other) const { + try { + const TeamRewardLookupNode& other_team_reward = dynamic_cast(other); + return other_team_reward.reward_name == this->reward_name; + } catch (const bad_cast&) { + return false; + } +} + +int64_t IntegralExpression::TeamRewardLookupNode::evaluate(const Env& env) const { + return (env.team && env.team->has_reward(this->reward_name)) ? 1 : 0; +} + +string IntegralExpression::TeamRewardLookupNode::str() const { + return "T_" + this->reward_name; +} + +IntegralExpression::NumPlayersLookupNode::NumPlayersLookupNode() {} + +bool IntegralExpression::NumPlayersLookupNode::operator==(const Node& other) const { + return dynamic_cast(&other) != nullptr; +} + +int64_t IntegralExpression::NumPlayersLookupNode::evaluate(const Env& env) const { + return env.num_players; +} + +string IntegralExpression::NumPlayersLookupNode::str() const { + return "V_NumPlayers"; +} + +IntegralExpression::EventLookupNode::EventLookupNode() {} + +bool IntegralExpression::EventLookupNode::operator==(const Node& other) const { + return dynamic_cast(&other) != nullptr; +} + +int64_t IntegralExpression::EventLookupNode::evaluate(const Env& env) const { + return env.event; +} + +string IntegralExpression::EventLookupNode::str() const { + return "V_Event"; +} + +IntegralExpression::V1PresenceLookupNode::V1PresenceLookupNode() {} + +bool IntegralExpression::V1PresenceLookupNode::operator==(const Node& other) const { + return dynamic_cast(&other) != nullptr; +} + +int64_t IntegralExpression::V1PresenceLookupNode::evaluate(const Env& env) const { + return env.v1_present ? 1 : 0; +} + +string IntegralExpression::V1PresenceLookupNode::str() const { + return "V_V1Present"; +} + +IntegralExpression::ConstantNode::ConstantNode(int64_t value) + : value(value) {} + +bool IntegralExpression::ConstantNode::operator==(const Node& other) const { + try { + const ConstantNode& other_const = dynamic_cast(other); + return other_const.value == this->value; + } catch (const bad_cast&) { + return false; + } +} + +int64_t IntegralExpression::ConstantNode::evaluate(const Env&) const { + return this->value; +} + +string IntegralExpression::ConstantNode::str() const { + return string_printf("%" PRId64, this->value); +} + +unique_ptr IntegralExpression::parse_expr(string_view text) { + // Strip off spaces and fully-enclosing parentheses + for (;;) { + size_t starting_size = text.size(); + while (text.at(0) == ' ') { + text = text.substr(1); + } + while (text.at(text.size() - 1) == ' ') { + text = text.substr(0, text.size() - 1); + } + if (text.at(0) == '(' && text.at(text.size() - 1) == ')') { + // It doesn't suffice to just check the first ant last characters, since + // text could be like "(a) && (b)". Instead, we ignore the first and last + // characters, and don't strip anything if the internal parentheses are + // unbalanced. + size_t paren_level = 1; + for (size_t z = 1; z < text.size() - 1; z++) { + if (text[z] == '(') { + paren_level++; + } else if (text[z] == ')') { + paren_level--; + if (paren_level == 0) { + break; + } + } + } + if (paren_level > 0) { + text = text.substr(1, text.size() - 2); + } + } + if (text.size() == starting_size) { + break; + } + } + if (text.empty()) { + throw runtime_error("invalid expression"); + } + + // Check for binary operators at the root level + using BinType = BinaryOperatorNode::Type; + static const vector>> binary_operator_levels = { + {{make_pair("*", BinType::MULTIPLY)}, {make_pair("/", BinType::DIVIDE)}, {make_pair("%", BinType::MODULUS)}}, + {{make_pair("+", BinType::ADD)}, {make_pair("-", BinType::SUBTRACT)}}, + {{make_pair("<<", BinType::LEFT_SHIFT)}, {make_pair(">>", BinType::RIGHT_SHIFT)}}, + {{make_pair("<=", BinType::LESS_OR_EQUAL)}, {make_pair(">=", BinType::GREATER_OR_EQUAL)}, {make_pair("<", BinType::LESS_THAN)}, {make_pair(">", BinType::GREATER_THAN)}}, + {{make_pair("==", BinType::EQUAL)}, {make_pair("!=", BinType::NOT_EQUAL)}}, + {{make_pair("&", BinType::BITWISE_AND)}}, + {{make_pair("^", BinType::BITWISE_XOR)}}, + {{make_pair("|", BinType::BITWISE_OR)}}, + {{make_pair("&&", BinType::LOGICAL_AND)}}, + {{make_pair("||", BinType::LOGICAL_OR)}}, + }; + for (const auto& operators : binary_operator_levels) { + size_t paren_level = 0; + for (size_t z = 0; z < text.size() - 1; z++) { + if (text[z] == '(') { + paren_level++; + continue; + } else if (text[z] == ')') { + paren_level--; + continue; + } + if (!paren_level) { + for (const auto& oper : operators) { + // Awful hack (because I'm too lazy to add a tokenization step): if + // the operator is followed or preceded by another copy of itself, + // don't match it (this prevents us from matching & when the token is + // actually &&) + if ((text.size() > z + oper.first.size()) && + ((z < oper.first.size()) || (text.compare(z - oper.first.size(), oper.first.size(), oper.first) != 0)) && + (text.compare(z, oper.first.size(), oper.first) == 0) && + (text.compare(z + oper.first.size(), oper.first.size(), oper.first) != 0)) { + auto left = IntegralExpression::parse_expr(text.substr(0, z)); + auto right = IntegralExpression::parse_expr(text.substr(z + oper.first.size())); + return make_unique(oper.second, std::move(left), std::move(right)); + } + } + } + } + } + + // Check for unary operators + if (text[0] == '!') { + return make_unique(UnaryOperatorNode::Type::LOGICAL_NOT, + IntegralExpression::parse_expr(text.substr(1))); + } else if (text[0] == '~') { + return make_unique(UnaryOperatorNode::Type::BITWISE_NOT, + IntegralExpression::parse_expr(text.substr(1))); + } else if (text[0] == '-') { + return make_unique(UnaryOperatorNode::Type::NEGATIVE, + IntegralExpression::parse_expr(text.substr(1))); + } + + // Check for env lookups + if (text.starts_with("F_")) { + char* endptr = nullptr; + uint64_t flag = strtoul(text.data() + 2, &endptr, 16); + if (endptr != text.data() + text.size()) { + throw runtime_error("invalid flag lookup token"); + } + if (flag >= 0x400) { + throw runtime_error("invalid flag index"); + } + return make_unique(flag); + } + if (text.starts_with("CC_")) { + Episode episode; + if (text.starts_with("CC_Ep1_")) { + episode = Episode::EP1; + } else if (text.starts_with("CC_Ep2_")) { + episode = Episode::EP2; + } else { + throw runtime_error("invalid challenge episode"); + } + char* endptr = nullptr; + uint64_t stage_index = strtoul(text.data() + 7, &endptr, 0) - 1; + if (endptr != text.data() + text.size()) { + throw runtime_error("invalid challenge completion lookup token"); + } + if ((episode == Episode::EP1 && stage_index > 8) || (episode == Episode::EP2 && stage_index > 4)) { + throw runtime_error("invalid challenge stage index"); + } + return make_unique(episode, stage_index); + } + if (text.starts_with("T_")) { + return make_unique(string(text.substr(2))); + } + if (text == "V_NumPlayers") { + return make_unique(); + } + if (text == "V_Event") { + return make_unique(); + } + if (text == "V_V1Present") { + return make_unique(); + } + + // Check for constants + if (text == "true") { + return make_unique(1); + } + if (text == "false") { + return make_unique(0); + } + try { + size_t endpos; + int64_t v = stoll(string(text), &endpos, 0); + if (endpos == text.size()) { + return make_unique(v); + } + } catch (const exception&) { + } + throw runtime_error("unparseable expression"); +} diff --git a/src/IntegralExpression.hh b/src/IntegralExpression.hh index fcc69db0..a4779a9d 100644 --- a/src/IntegralExpression.hh +++ b/src/IntegralExpression.hh @@ -1,188 +1,188 @@ -#pragma once - -#include - -#include -#include -#include -#include -#include - -#include "PlayerSubordinates.hh" -#include "QuestScript.hh" -#include "StaticGameData.hh" -#include "TeamIndex.hh" - -class IntegralExpression { -public: - struct Env { - const QuestFlagsForDifficulty* flags; - const PlayerRecordsChallengeBB* challenge_records; - std::shared_ptr team; - size_t num_players; - uint8_t event; - bool v1_present; - }; - - IntegralExpression(const std::string& text); - ~IntegralExpression() = default; - inline bool operator==(const IntegralExpression& other) const { - return this->root->operator==(*other.root); - } - inline bool operator!=(const IntegralExpression& other) const { - return !this->operator==(other); - } - inline int64_t evaluate(const Env& env) const { - return this->root->evaluate(env); - } - inline std::string str() const { - return this->root->str(); - } - -protected: - class Node { - public: - virtual ~Node() = default; - virtual bool operator==(const Node& other) const = 0; - inline bool operator!=(const Node& other) const { - return !this->operator==(other); - } - virtual int64_t evaluate(const Env& env) const = 0; - virtual std::string str() const = 0; - - protected: - Node() = default; - }; - - class BinaryOperatorNode : public Node { - public: - enum class Type { - LOGICAL_OR = 0, - LOGICAL_AND, - BITWISE_OR, - BITWISE_AND, - BITWISE_XOR, - LEFT_SHIFT, - RIGHT_SHIFT, - LESS_THAN, - GREATER_THAN, - LESS_OR_EQUAL, - GREATER_OR_EQUAL, - EQUAL, - NOT_EQUAL, - ADD, - SUBTRACT, - MULTIPLY, - DIVIDE, - MODULUS, - }; - BinaryOperatorNode(Type type, std::unique_ptr&& left, std::unique_ptr&& right); - virtual ~BinaryOperatorNode() = default; - virtual bool operator==(const Node& other) const; - virtual int64_t evaluate(const Env& env) const; - virtual std::string str() const; - - protected: - Type type; - std::unique_ptr left; - std::unique_ptr right; - }; - - class UnaryOperatorNode : public Node { - public: - enum class Type { - LOGICAL_NOT = 0, - BITWISE_NOT, - NEGATIVE, - }; - UnaryOperatorNode(Type type, std::unique_ptr&& sub); - virtual ~UnaryOperatorNode() = default; - virtual bool operator==(const Node& other) const; - virtual int64_t evaluate(const Env& env) const; - virtual std::string str() const; - - protected: - Type type; - std::unique_ptr sub; - }; - - class FlagLookupNode : public Node { - public: - FlagLookupNode(uint16_t flag_index); - virtual ~FlagLookupNode() = default; - virtual bool operator==(const Node& other) const; - virtual int64_t evaluate(const Env& env) const; - virtual std::string str() const; - - protected: - uint16_t flag_index; - }; - - class ChallengeCompletionLookupNode : public Node { - public: - ChallengeCompletionLookupNode(Episode episode, uint8_t stage_index); - virtual ~ChallengeCompletionLookupNode() = default; - virtual bool operator==(const Node& other) const; - virtual int64_t evaluate(const Env& env) const; - virtual std::string str() const; - - protected: - Episode episode; - uint8_t stage_index; - }; - - class TeamRewardLookupNode : public Node { - public: - TeamRewardLookupNode(const std::string& reward_name); - virtual ~TeamRewardLookupNode() = default; - virtual bool operator==(const Node& other) const; - virtual int64_t evaluate(const Env& env) const; - virtual std::string str() const; - - protected: - std::string reward_name; - }; - - class NumPlayersLookupNode : public Node { - public: - NumPlayersLookupNode(); - virtual ~NumPlayersLookupNode() = default; - virtual bool operator==(const Node& other) const; - virtual int64_t evaluate(const Env& env) const; - virtual std::string str() const; - }; - - class EventLookupNode : public Node { - public: - EventLookupNode(); - virtual ~EventLookupNode() = default; - virtual bool operator==(const Node& other) const; - virtual int64_t evaluate(const Env& env) const; - virtual std::string str() const; - }; - - class V1PresenceLookupNode : public Node { - public: - V1PresenceLookupNode(); - virtual ~V1PresenceLookupNode() = default; - virtual bool operator==(const Node& other) const; - virtual int64_t evaluate(const Env& env) const; - virtual std::string str() const; - }; - - class ConstantNode : public Node { - public: - ConstantNode(int64_t value); - virtual ~ConstantNode() = default; - virtual bool operator==(const Node& other) const; - virtual int64_t evaluate(const Env& env) const; - virtual std::string str() const; - - protected: - int64_t value; - }; - - std::unique_ptr parse_expr(std::string_view text); - - std::unique_ptr root; -}; +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include "PlayerSubordinates.hh" +#include "QuestScript.hh" +#include "StaticGameData.hh" +#include "TeamIndex.hh" + +class IntegralExpression { +public: + struct Env { + const QuestFlagsForDifficulty* flags; + const PlayerRecordsChallengeBB* challenge_records; + std::shared_ptr team; + size_t num_players; + uint8_t event; + bool v1_present; + }; + + IntegralExpression(const std::string& text); + ~IntegralExpression() = default; + inline bool operator==(const IntegralExpression& other) const { + return this->root->operator==(*other.root); + } + inline bool operator!=(const IntegralExpression& other) const { + return !this->operator==(other); + } + inline int64_t evaluate(const Env& env) const { + return this->root->evaluate(env); + } + inline std::string str() const { + return this->root->str(); + } + +protected: + class Node { + public: + virtual ~Node() = default; + virtual bool operator==(const Node& other) const = 0; + inline bool operator!=(const Node& other) const { + return !this->operator==(other); + } + virtual int64_t evaluate(const Env& env) const = 0; + virtual std::string str() const = 0; + + protected: + Node() = default; + }; + + class BinaryOperatorNode : public Node { + public: + enum class Type { + LOGICAL_OR = 0, + LOGICAL_AND, + BITWISE_OR, + BITWISE_AND, + BITWISE_XOR, + LEFT_SHIFT, + RIGHT_SHIFT, + LESS_THAN, + GREATER_THAN, + LESS_OR_EQUAL, + GREATER_OR_EQUAL, + EQUAL, + NOT_EQUAL, + ADD, + SUBTRACT, + MULTIPLY, + DIVIDE, + MODULUS, + }; + BinaryOperatorNode(Type type, std::unique_ptr&& left, std::unique_ptr&& right); + virtual ~BinaryOperatorNode() = default; + virtual bool operator==(const Node& other) const; + virtual int64_t evaluate(const Env& env) const; + virtual std::string str() const; + + protected: + Type type; + std::unique_ptr left; + std::unique_ptr right; + }; + + class UnaryOperatorNode : public Node { + public: + enum class Type { + LOGICAL_NOT = 0, + BITWISE_NOT, + NEGATIVE, + }; + UnaryOperatorNode(Type type, std::unique_ptr&& sub); + virtual ~UnaryOperatorNode() = default; + virtual bool operator==(const Node& other) const; + virtual int64_t evaluate(const Env& env) const; + virtual std::string str() const; + + protected: + Type type; + std::unique_ptr sub; + }; + + class FlagLookupNode : public Node { + public: + FlagLookupNode(uint16_t flag_index); + virtual ~FlagLookupNode() = default; + virtual bool operator==(const Node& other) const; + virtual int64_t evaluate(const Env& env) const; + virtual std::string str() const; + + protected: + uint16_t flag_index; + }; + + class ChallengeCompletionLookupNode : public Node { + public: + ChallengeCompletionLookupNode(Episode episode, uint8_t stage_index); + virtual ~ChallengeCompletionLookupNode() = default; + virtual bool operator==(const Node& other) const; + virtual int64_t evaluate(const Env& env) const; + virtual std::string str() const; + + protected: + Episode episode; + uint8_t stage_index; + }; + + class TeamRewardLookupNode : public Node { + public: + TeamRewardLookupNode(const std::string& reward_name); + virtual ~TeamRewardLookupNode() = default; + virtual bool operator==(const Node& other) const; + virtual int64_t evaluate(const Env& env) const; + virtual std::string str() const; + + protected: + std::string reward_name; + }; + + class NumPlayersLookupNode : public Node { + public: + NumPlayersLookupNode(); + virtual ~NumPlayersLookupNode() = default; + virtual bool operator==(const Node& other) const; + virtual int64_t evaluate(const Env& env) const; + virtual std::string str() const; + }; + + class EventLookupNode : public Node { + public: + EventLookupNode(); + virtual ~EventLookupNode() = default; + virtual bool operator==(const Node& other) const; + virtual int64_t evaluate(const Env& env) const; + virtual std::string str() const; + }; + + class V1PresenceLookupNode : public Node { + public: + V1PresenceLookupNode(); + virtual ~V1PresenceLookupNode() = default; + virtual bool operator==(const Node& other) const; + virtual int64_t evaluate(const Env& env) const; + virtual std::string str() const; + }; + + class ConstantNode : public Node { + public: + ConstantNode(int64_t value); + virtual ~ConstantNode() = default; + virtual bool operator==(const Node& other) const; + virtual int64_t evaluate(const Env& env) const; + virtual std::string str() const; + + protected: + int64_t value; + }; + + std::unique_ptr parse_expr(std::string_view text); + + std::unique_ptr root; +}; diff --git a/src/Items.cc b/src/Items.cc index c88f6937..230737e1 100644 --- a/src/Items.cc +++ b/src/Items.cc @@ -1,513 +1,513 @@ -#include "Items.hh" - -#include - -#include "SendCommands.hh" - -using namespace std; - -void player_use_item(shared_ptr c, size_t item_index, shared_ptr opt_rand_crypt) { - auto s = c->require_server_state(); - - // On PC (and presumably DC), the client sends a 6x29 after this to delete the - // used item. On GC and later versions, this does not happen, so we should - // delete the item here. - bool is_v3_or_later = is_v3(c->version()) || is_v4(c->version()); - bool should_delete_item = is_v3_or_later; - - auto player = c->character(); - auto& item = player->inventory.items[item_index]; - uint32_t primary_identifier = item.data.primary_identifier(); - - if (item.data.is_common_consumable()) { // Monomate, etc. - // Nothing to do (it should be deleted) - - } else if ((primary_identifier & 0xFFFF0000) == 0x03020000) { // Technique disk - auto item_parameter_table = s->item_parameter_table(c->version()); - uint8_t max_level = item_parameter_table->get_max_tech_level(player->disp.visual.char_class, item.data.data1[4]); - if (item.data.data1[2] > max_level) { - throw runtime_error("technique level too high"); - } - player->set_technique_level(item.data.data1[4], item.data.data1[2]); - - } else if ((primary_identifier & 0xFFFF0000) == 0x030A0000) { // Grinder - if (item.data.data1[2] > 2) { - throw runtime_error("incorrect grinder value"); - } - - auto& weapon = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::WEAPON)]; - // Don't enforce the weapon's grind limit on V1 and V2. This is necessary - // because the V2 client replaces its inventory items on the fly with items - // compatible with V1 when sending the 61 and 98 commands. There appears to - // be no way to disable this behavior, so there's no way for the server to - // get an accurate picture of what's actually in the player's inventory, so - // there's no way to know if we would be enforcing the correct grind limit. - if (is_v3_or_later) { - auto item_parameter_table = s->item_parameter_table(c->version()); - auto weapon_def = item_parameter_table->get_weapon(weapon.data.data1[1], weapon.data.data1[2]); - if (weapon.data.data1[3] >= weapon_def.max_grind) { - throw runtime_error("weapon already at maximum grind"); - } - } - weapon.data.data1[3] += (item.data.data1[2] + 1); - - } else if ((primary_identifier & 0xFFFF0000) == 0x030B0000) { // Material - auto p = c->character(); - - using Type = PSOBBCharacterFile::MaterialType; - Type type; - switch (item.data.data1[2]) { - case 0: // Power Material - type = Type::POWER; - p->disp.stats.char_stats.atp += 2; - break; - case 1: // Mind Material - type = Type::MIND; - p->disp.stats.char_stats.mst += 2; - break; - case 2: // Evade Material - type = Type::EVADE; - p->disp.stats.char_stats.evp += 2; - break; - case 3: // HP Material - type = Type::HP; - break; - case 4: // TP Material - type = Type::TP; - break; - case 5: // Def Material - type = Type::DEF; - p->disp.stats.char_stats.dfp += 2; - break; - case 6: // Hit Material (v1/v2) or Luck Material (v3/v4) - type = Type::LUCK; - if (!is_v3_or_later && (c->version() != Version::GC_NTE)) { - // Hit material doesn't exist on v3/v4, but we'll ignore type anyway - // in this case because track_non_hp_tp_materials is false - p->disp.stats.char_stats.ata += 2; - } else { - p->disp.stats.char_stats.lck += 2; - } - break; - case 7: // Luck Material (v1/v2) - type = Type::LUCK; - if (!is_v3_or_later && (c->version() != Version::GC_NTE)) { - p->disp.stats.char_stats.lck += 2; - } else { - throw runtime_error("unknown material used"); - } - break; - default: - throw runtime_error("unknown material used"); - } - if (is_v3_or_later || (type == Type::HP) || (type == Type::TP)) { - p->set_material_usage(type, p->get_material_usage(type) + 1); - } - - } else if ((primary_identifier & 0xFFFF0000) == 0x030F0000) { // AddSlot - auto& armor = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::ARMOR)]; - if (armor.data.data1[5] >= 4) { - throw runtime_error("armor already at maximum slot count"); - } - armor.data.data1[5]++; - - } else if (item.data.is_wrapped(*s->item_stack_limits(c->version()))) { - // Unwrap present - item.data.unwrap(*s->item_stack_limits(c->version())); - should_delete_item = false; - - } else if (primary_identifier == 0x00330000) { - // Unseal Sealed J-Sword => Tsumikiri J-Sword - item.data.data1[1] = 0x32; - item.flags &= (~8); // Unequip it - should_delete_item = false; - - } else if (primary_identifier == 0x00AB0000) { - // Unseal Lame d'Argent => Excalibur - item.data.data1[1] = 0xAC; - item.flags &= (~8); // Unequip it - should_delete_item = false; - - } else if (primary_identifier == 0x01034D00) { - // Unseal Limiter => Adept - item.data.data1[2] = 0x4E; - item.flags &= (~8); // Unequip it - should_delete_item = false; - - } else if (primary_identifier == 0x01034F00) { - // Unseal Swordsman Lore => Proof of Sword-Saint - item.data.data1[2] = 0x50; - item.flags &= (~8); // Unequip it - should_delete_item = false; - - } else if (primary_identifier == 0x030C0000) { - // Cell of MAG 502 - auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)]; - mag.data.data1[1] = (player->disp.visual.section_id & 1) ? 0x1D : 0x21; - - } else if (primary_identifier == 0x030C0100) { - // Cell of MAG 213 - auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)]; - mag.data.data1[1] = (player->disp.visual.section_id & 1) ? 0x27 : 0x22; - - } else if (primary_identifier == 0x030C0200) { - // Parts of RoboChao - auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)]; - mag.data.data1[1] = 0x28; - - } else if (primary_identifier == 0x030C0300) { - // Heart of Opa Opa - auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)]; - mag.data.data1[1] = 0x29; - - } else if (primary_identifier == 0x030C0400) { - // Heart of Pian - auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)]; - mag.data.data1[1] = 0x2A; - - } else if (primary_identifier == 0x030C0500) { - // Heart of Chao - auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)]; - mag.data.data1[1] = 0x2B; - - } else if ((primary_identifier & 0xFFFF0000) == 0x03150000) { - // Christmas Present, etc. - use unwrap_table + probabilities therein - auto item_parameter_table = s->item_parameter_table(c->version()); - auto table = item_parameter_table->get_event_items(item.data.data1[2]); - size_t sum = 0; - for (size_t z = 0; z < table.second; z++) { - sum += table.first[z].probability; - } - if (sum == 0) { - throw runtime_error("no unwrap results available for event"); - } - size_t det = random_from_optional_crypt(opt_rand_crypt) % sum; - for (size_t z = 0; z < table.second; z++) { - const auto& entry = table.first[z]; - if (det > entry.probability) { - det -= entry.probability; - } else { - item.data.data2d = 0; - item.data.data1[0] = entry.item[0]; - item.data.data1[1] = entry.item[1]; - item.data.data1[2] = entry.item[2]; - item.data.data1.clear_after(3); - should_delete_item = false; - - auto l = c->require_lobby(); - if (l->base_version == Version::BB_V4) { - send_create_inventory_item_to_lobby(c, c->lobby_client_id, item.data); - } - break; - } - } - - } else { - // Use item combinations table from ItemPMT - bool combo_applied = false; - for (size_t z = 0; z < player->inventory.num_items; z++) { - auto& inv_item = player->inventory.items[z]; - if (!(inv_item.flags & 0x00000008)) { - continue; - } - try { - auto item_parameter_table = s->item_parameter_table(c->version()); - const auto& combo = item_parameter_table->get_item_combination(item.data, inv_item.data); - if (combo.char_class != 0xFF && combo.char_class != player->disp.visual.char_class) { - throw runtime_error("item combination requires specific char_class"); - } - if (combo.mag_level != 0xFF) { - if (inv_item.data.data1[0] != 2) { - throw runtime_error("item combination applies with mag level requirement, but equipped item is not a mag"); - } - if (inv_item.data.compute_mag_level() < combo.mag_level) { - throw runtime_error("item combination applies with mag level requirement, but equipped mag level is too low"); - } - } - if (combo.grind != 0xFF) { - if (inv_item.data.data1[0] != 0) { - throw runtime_error("item combination applies with grind requirement, but equipped item is not a weapon"); - } - if (inv_item.data.data1[3] < combo.grind) { - throw runtime_error("item combination applies with grind requirement, but equipped weapon grind is too low"); - } - } - if (combo.level != 0xFF && player->disp.stats.level + 1 < combo.level) { - throw runtime_error("item combination applies with level requirement, but player level is too low"); - } - // If we get here, then the combo applies - if (combo_applied) { - throw runtime_error("multiple combinations apply"); - } - combo_applied = true; - - inv_item.data.data1[0] = combo.result_item[0]; - inv_item.data.data1[1] = combo.result_item[1]; - // For mags, don't reset level + PBs + stats - if (inv_item.data.data1[1] != 0x02) { - inv_item.data.data1[2] = combo.result_item[2]; - inv_item.data.data1[3] = 0; // Grind - inv_item.data.data1[4] = 0; // Flags + special - } - inv_item.flags &= (~8); // Unequip it - } catch (const out_of_range&) { - } - } - } - - if (should_delete_item) { - // Allow overdrafting meseta if the client is not BB, since the server isn't - // informed when meseta is added or removed from the bank. - player->remove_item(item.data.id, 1, *s->item_stack_limits(c->version())); - } -} - -void apply_mag_feed_result( - ItemData& mag_item, - const ItemData& fed_item, - shared_ptr item_parameter_table, - shared_ptr mag_evolution_table, - uint8_t char_class, - uint8_t section_id, - bool version_has_rare_mags) { - - static const unordered_map result_index_for_fed_item({ - {0x03000000, 0}, // Monomate - {0x03000100, 1}, // Dimate - {0x03000200, 2}, // Trimate - {0x03010000, 3}, // Monofluid - {0x03010100, 4}, // Difluid - {0x03010200, 5}, // Trifluid - {0x03060000, 6}, // Antidote - {0x03060100, 7}, // Antiparalysis - {0x03030000, 8}, // Sol Atomizer - {0x03040000, 9}, // Moon Atomizer - {0x03050000, 10}, // Star Atomizer - }); - - size_t result_index = result_index_for_fed_item.at(fed_item.primary_identifier()); - const auto& mag_def = item_parameter_table->get_mag(mag_item.data1[1]); - const auto& feed_result = item_parameter_table->get_mag_feed_result(mag_def.feed_table, result_index); - - auto update_stat = +[](ItemData& data, size_t which, int8_t delta) -> void { - uint16_t existing_stat = data.data1w[which] % 100; - if ((delta > 0) || ((delta < 0) && (-delta < existing_stat))) { - uint16_t level = data.compute_mag_level(); - if (level > 200) { - throw runtime_error("mag level is too high"); - } - if ((level == 200) && ((99 - existing_stat) < delta)) { - delta = 99 - existing_stat; - } - data.data1w[which] += delta; - } - }; - - update_stat(mag_item, 2, feed_result.def); - update_stat(mag_item, 3, feed_result.pow); - update_stat(mag_item, 4, feed_result.dex); - update_stat(mag_item, 5, feed_result.mind); - mag_item.data2[0] = clamp(static_cast(mag_item.data2[0]) + feed_result.synchro, 0, 120); - mag_item.data2[1] = clamp(static_cast(mag_item.data2[1]) + feed_result.iq, 0, 200); - - uint8_t mag_level = mag_item.compute_mag_level(); - mag_item.data1[2] = mag_level; - uint8_t evolution_number = mag_evolution_table->get_evolution_number(mag_item.data1[1]); - uint8_t mag_number = mag_item.data1[1]; - - // Note: Sega really did just hardcode all these rules into the client. There - // is no data file describing these evolutions, unfortunately. - - if (mag_level < 10) { - // Nothing to do - - } else if (mag_level < 35) { // Level 10 evolution - if (evolution_number < 1) { - switch (char_class) { - case 0: // HUmar - case 1: // HUnewearl - case 2: // HUcast - case 9: // HUcaseal - mag_item.data1[1] = 0x01; // Varuna - break; - case 3: // RAmar - case 11: // RAmarl - case 4: // RAcast - case 5: // RAcaseal - mag_item.data1[1] = 0x0D; // Kalki - break; - case 10: // FOmar - case 6: // FOmarl - case 7: // FOnewm - case 8: // FOnewearl - mag_item.data1[1] = 0x19; // Vritra - break; - default: - throw runtime_error("invalid character class"); - } - } - - } else if (mag_level < 50) { // Level 35 evolution - if (evolution_number < 2) { - uint16_t flags = mag_item.compute_mag_strength_flags(); - if (mag_number == 0x0D) { - if (flags & 0x110) { - mag_item.data1[1] = 0x02; - } else if (flags & 8) { - mag_item.data1[1] = 0x03; - } else if (flags & 0x20) { - mag_item.data1[1] = 0x0B; - } - } else if (mag_number == 1) { - if (flags & 0x108) { - mag_item.data1[1] = 0x0E; - } else if (flags & 0x10) { - mag_item.data1[1] = 0x0F; - } else if (flags & 0x20) { - mag_item.data1[1] = 0x04; - } - } else if (mag_number == 0x19) { - if (flags & 0x120) { - mag_item.data1[1] = 0x1A; - } else if (flags & 8) { - mag_item.data1[1] = 0x1B; - } else if (flags & 0x10) { - mag_item.data1[1] = 0x14; - } - } - } - - } else if ((mag_level % 5) == 0) { // Level 50 (and beyond) evolutions - if (evolution_number < 4) { - - if ((mag_level >= 100) && version_has_rare_mags) { - uint8_t section_id_group = section_id % 3; - uint16_t def = mag_item.data1w[2] / 100; - uint16_t pow = mag_item.data1w[3] / 100; - uint16_t dex = mag_item.data1w[4] / 100; - uint16_t mind = mag_item.data1w[5] / 100; - bool is_male = char_class_is_male(char_class); - size_t table_index = (is_male ? 0 : 1) + section_id_group * 2; - - bool is_hunter = char_class_is_hunter(char_class); - bool is_ranger = char_class_is_ranger(char_class); - bool is_force = char_class_is_force(char_class); - if (is_force) { - table_index += 12; - } else if (is_ranger) { - table_index += 6; - } else if (!is_hunter) { - throw logic_error("char class is not any of the top-level classes"); - } - - // Note: The original code checks the class (hunter/ranger/force) again - // here, and goes into 3 branches that each do these same checks. - // However, the result of all 3 branches is exactly the same! - if (((section_id_group == 0) && (pow + mind == def + dex)) || - ((section_id_group == 1) && (pow + dex == mind + def)) || - ((section_id_group == 2) && (pow + def == mind + dex))) { - // clang-format off - static const uint8_t result_table[] = { - // M0 F0 M1 F1 M2 F2 - 0x39, 0x3B, 0x3A, 0x3B, 0x3A, 0x3B, // Hunter - 0x3D, 0x3C, 0x3D, 0x3C, 0x3D, 0x3E, // Ranger - 0x41, 0x3F, 0x41, 0x40, 0x41, 0x40, // Force - }; - // clang-format on - mag_item.data1[1] = result_table[table_index]; - } - } - - // If a special evolution did not occur, do a normal level 50 evolution - if (mag_number == mag_item.data1[1]) { - uint16_t flags = mag_item.compute_mag_strength_flags(); - uint16_t def = mag_item.data1w[2] / 100; - uint16_t pow = mag_item.data1w[3] / 100; - uint16_t dex = mag_item.data1w[4] / 100; - uint16_t mind = mag_item.data1w[5] / 100; - - bool is_hunter = char_class_is_hunter(char_class); - bool is_ranger = char_class_is_ranger(char_class); - bool is_force = char_class_is_force(char_class); - if (is_hunter + is_ranger + is_force != 1) { - throw logic_error("char class is not exactly one of the top-level classes"); - } - - if (is_hunter) { - if (flags & 0x108) { - mag_item.data1[1] = (section_id & 1) - ? ((dex < mind) ? 0x08 : 0x06) - : ((dex < mind) ? 0x0C : 0x05); - } else if (flags & 0x010) { - mag_item.data1[1] = (section_id & 1) - ? ((mind < pow) ? 0x12 : 0x10) - : ((mind < pow) ? 0x17 : 0x13); - } else if (flags & 0x020) { - mag_item.data1[1] = (section_id & 1) - ? ((pow < dex) ? 0x16 : 0x24) - : ((pow < dex) ? 0x07 : 0x1E); - } - } else if (is_ranger) { - if (flags & 0x110) { - mag_item.data1[1] = (section_id & 1) - ? ((mind < pow) ? 0x0A : 0x05) - : ((mind < pow) ? 0x0C : 0x06); - } else if (flags & 0x008) { - mag_item.data1[1] = (section_id & 1) - ? ((dex < mind) ? 0x0A : 0x26) - : ((dex < mind) ? 0x0C : 0x06); - } else if (flags & 0x020) { - mag_item.data1[1] = (section_id & 1) - ? ((pow < dex) ? 0x18 : 0x1E) - : ((pow < dex) ? 0x08 : 0x05); - } - } else if (is_force) { - if (flags & 0x120) { - if (def < 45) { - mag_item.data1[1] = (section_id & 1) - ? ((pow < dex) ? 0x17 : 0x09) - : ((pow < dex) ? 0x1E : 0x1C); - } else { - mag_item.data1[1] = 0x24; - } - } else if (flags & 0x008) { - if (def < 45) { - mag_item.data1[1] = (section_id & 1) - ? ((dex < mind) ? 0x1C : 0x20) - : ((dex < mind) ? 0x1F : 0x25); - } else { - mag_item.data1[1] = 0x23; - } - } else if (flags & 0x010) { - if (def < 45) { - mag_item.data1[1] = (section_id & 1) - ? ((mind < pow) ? 0x12 : 0x0C) - : ((mind < pow) ? 0x15 : 0x11); - } else { - mag_item.data1[1] = 0x24; - } - } - } - } - } - } - - // If the mag has evolved, add its new photon blast - if (mag_number != mag_item.data1[1]) { - const auto& new_mag_def = item_parameter_table->get_mag(mag_item.data1[1]); - mag_item.add_mag_photon_blast(new_mag_def.photon_blast); - } -} - -void player_feed_mag(std::shared_ptr c, size_t mag_item_index, size_t fed_item_index) { - auto s = c->require_server_state(); - auto player = c->character(); - apply_mag_feed_result( - player->inventory.items[mag_item_index].data, - player->inventory.items[fed_item_index].data, - s->item_parameter_table(c->version()), - s->mag_evolution_table, - player->disp.visual.char_class, - player->disp.visual.section_id, - !is_v1_or_v2(c->version())); -} +#include "Items.hh" + +#include + +#include "SendCommands.hh" + +using namespace std; + +void player_use_item(shared_ptr c, size_t item_index, shared_ptr opt_rand_crypt) { + auto s = c->require_server_state(); + + // On PC (and presumably DC), the client sends a 6x29 after this to delete the + // used item. On GC and later versions, this does not happen, so we should + // delete the item here. + bool is_v3_or_later = is_v3(c->version()) || is_v4(c->version()); + bool should_delete_item = is_v3_or_later; + + auto player = c->character(); + auto& item = player->inventory.items[item_index]; + uint32_t primary_identifier = item.data.primary_identifier(); + + if (item.data.is_common_consumable()) { // Monomate, etc. + // Nothing to do (it should be deleted) + + } else if ((primary_identifier & 0xFFFF0000) == 0x03020000) { // Technique disk + auto item_parameter_table = s->item_parameter_table(c->version()); + uint8_t max_level = item_parameter_table->get_max_tech_level(player->disp.visual.char_class, item.data.data1[4]); + if (item.data.data1[2] > max_level) { + throw runtime_error("technique level too high"); + } + player->set_technique_level(item.data.data1[4], item.data.data1[2]); + + } else if ((primary_identifier & 0xFFFF0000) == 0x030A0000) { // Grinder + if (item.data.data1[2] > 2) { + throw runtime_error("incorrect grinder value"); + } + + auto& weapon = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::WEAPON)]; + // Don't enforce the weapon's grind limit on V1 and V2. This is necessary + // because the V2 client replaces its inventory items on the fly with items + // compatible with V1 when sending the 61 and 98 commands. There appears to + // be no way to disable this behavior, so there's no way for the server to + // get an accurate picture of what's actually in the player's inventory, so + // there's no way to know if we would be enforcing the correct grind limit. + if (is_v3_or_later) { + auto item_parameter_table = s->item_parameter_table(c->version()); + auto weapon_def = item_parameter_table->get_weapon(weapon.data.data1[1], weapon.data.data1[2]); + if (weapon.data.data1[3] >= weapon_def.max_grind) { + throw runtime_error("weapon already at maximum grind"); + } + } + weapon.data.data1[3] += (item.data.data1[2] + 1); + + } else if ((primary_identifier & 0xFFFF0000) == 0x030B0000) { // Material + auto p = c->character(); + + using Type = PSOBBCharacterFile::MaterialType; + Type type; + switch (item.data.data1[2]) { + case 0: // Power Material + type = Type::POWER; + p->disp.stats.char_stats.atp += 2; + break; + case 1: // Mind Material + type = Type::MIND; + p->disp.stats.char_stats.mst += 2; + break; + case 2: // Evade Material + type = Type::EVADE; + p->disp.stats.char_stats.evp += 2; + break; + case 3: // HP Material + type = Type::HP; + break; + case 4: // TP Material + type = Type::TP; + break; + case 5: // Def Material + type = Type::DEF; + p->disp.stats.char_stats.dfp += 2; + break; + case 6: // Hit Material (v1/v2) or Luck Material (v3/v4) + type = Type::LUCK; + if (!is_v3_or_later && (c->version() != Version::GC_NTE)) { + // Hit material doesn't exist on v3/v4, but we'll ignore type anyway + // in this case because track_non_hp_tp_materials is false + p->disp.stats.char_stats.ata += 2; + } else { + p->disp.stats.char_stats.lck += 2; + } + break; + case 7: // Luck Material (v1/v2) + type = Type::LUCK; + if (!is_v3_or_later && (c->version() != Version::GC_NTE)) { + p->disp.stats.char_stats.lck += 2; + } else { + throw runtime_error("unknown material used"); + } + break; + default: + throw runtime_error("unknown material used"); + } + if (is_v3_or_later || (type == Type::HP) || (type == Type::TP)) { + p->set_material_usage(type, p->get_material_usage(type) + 1); + } + + } else if ((primary_identifier & 0xFFFF0000) == 0x030F0000) { // AddSlot + auto& armor = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::ARMOR)]; + if (armor.data.data1[5] >= 4) { + throw runtime_error("armor already at maximum slot count"); + } + armor.data.data1[5]++; + + } else if (item.data.is_wrapped(*s->item_stack_limits(c->version()))) { + // Unwrap present + item.data.unwrap(*s->item_stack_limits(c->version())); + should_delete_item = false; + + } else if (primary_identifier == 0x00330000) { + // Unseal Sealed J-Sword => Tsumikiri J-Sword + item.data.data1[1] = 0x32; + item.flags &= (~8); // Unequip it + should_delete_item = false; + + } else if (primary_identifier == 0x00AB0000) { + // Unseal Lame d'Argent => Excalibur + item.data.data1[1] = 0xAC; + item.flags &= (~8); // Unequip it + should_delete_item = false; + + } else if (primary_identifier == 0x01034D00) { + // Unseal Limiter => Adept + item.data.data1[2] = 0x4E; + item.flags &= (~8); // Unequip it + should_delete_item = false; + + } else if (primary_identifier == 0x01034F00) { + // Unseal Swordsman Lore => Proof of Sword-Saint + item.data.data1[2] = 0x50; + item.flags &= (~8); // Unequip it + should_delete_item = false; + + } else if (primary_identifier == 0x030C0000) { + // Cell of MAG 502 + auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)]; + mag.data.data1[1] = (player->disp.visual.section_id & 1) ? 0x1D : 0x21; + + } else if (primary_identifier == 0x030C0100) { + // Cell of MAG 213 + auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)]; + mag.data.data1[1] = (player->disp.visual.section_id & 1) ? 0x27 : 0x22; + + } else if (primary_identifier == 0x030C0200) { + // Parts of RoboChao + auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)]; + mag.data.data1[1] = 0x28; + + } else if (primary_identifier == 0x030C0300) { + // Heart of Opa Opa + auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)]; + mag.data.data1[1] = 0x29; + + } else if (primary_identifier == 0x030C0400) { + // Heart of Pian + auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)]; + mag.data.data1[1] = 0x2A; + + } else if (primary_identifier == 0x030C0500) { + // Heart of Chao + auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)]; + mag.data.data1[1] = 0x2B; + + } else if ((primary_identifier & 0xFFFF0000) == 0x03150000) { + // Christmas Present, etc. - use unwrap_table + probabilities therein + auto item_parameter_table = s->item_parameter_table(c->version()); + auto table = item_parameter_table->get_event_items(item.data.data1[2]); + size_t sum = 0; + for (size_t z = 0; z < table.second; z++) { + sum += table.first[z].probability; + } + if (sum == 0) { + throw runtime_error("no unwrap results available for event"); + } + size_t det = random_from_optional_crypt(opt_rand_crypt) % sum; + for (size_t z = 0; z < table.second; z++) { + const auto& entry = table.first[z]; + if (det > entry.probability) { + det -= entry.probability; + } else { + item.data.data2d = 0; + item.data.data1[0] = entry.item[0]; + item.data.data1[1] = entry.item[1]; + item.data.data1[2] = entry.item[2]; + item.data.data1.clear_after(3); + should_delete_item = false; + + auto l = c->require_lobby(); + if (l->base_version == Version::BB_V4) { + send_create_inventory_item_to_lobby(c, c->lobby_client_id, item.data); + } + break; + } + } + + } else { + // Use item combinations table from ItemPMT + bool combo_applied = false; + for (size_t z = 0; z < player->inventory.num_items; z++) { + auto& inv_item = player->inventory.items[z]; + if (!(inv_item.flags & 0x00000008)) { + continue; + } + try { + auto item_parameter_table = s->item_parameter_table(c->version()); + const auto& combo = item_parameter_table->get_item_combination(item.data, inv_item.data); + if (combo.char_class != 0xFF && combo.char_class != player->disp.visual.char_class) { + throw runtime_error("item combination requires specific char_class"); + } + if (combo.mag_level != 0xFF) { + if (inv_item.data.data1[0] != 2) { + throw runtime_error("item combination applies with mag level requirement, but equipped item is not a mag"); + } + if (inv_item.data.compute_mag_level() < combo.mag_level) { + throw runtime_error("item combination applies with mag level requirement, but equipped mag level is too low"); + } + } + if (combo.grind != 0xFF) { + if (inv_item.data.data1[0] != 0) { + throw runtime_error("item combination applies with grind requirement, but equipped item is not a weapon"); + } + if (inv_item.data.data1[3] < combo.grind) { + throw runtime_error("item combination applies with grind requirement, but equipped weapon grind is too low"); + } + } + if (combo.level != 0xFF && player->disp.stats.level + 1 < combo.level) { + throw runtime_error("item combination applies with level requirement, but player level is too low"); + } + // If we get here, then the combo applies + if (combo_applied) { + throw runtime_error("multiple combinations apply"); + } + combo_applied = true; + + inv_item.data.data1[0] = combo.result_item[0]; + inv_item.data.data1[1] = combo.result_item[1]; + // For mags, don't reset level + PBs + stats + if (inv_item.data.data1[1] != 0x02) { + inv_item.data.data1[2] = combo.result_item[2]; + inv_item.data.data1[3] = 0; // Grind + inv_item.data.data1[4] = 0; // Flags + special + } + inv_item.flags &= (~8); // Unequip it + } catch (const out_of_range&) { + } + } + } + + if (should_delete_item) { + // Allow overdrafting meseta if the client is not BB, since the server isn't + // informed when meseta is added or removed from the bank. + player->remove_item(item.data.id, 1, *s->item_stack_limits(c->version())); + } +} + +void apply_mag_feed_result( + ItemData& mag_item, + const ItemData& fed_item, + shared_ptr item_parameter_table, + shared_ptr mag_evolution_table, + uint8_t char_class, + uint8_t section_id, + bool version_has_rare_mags) { + + static const unordered_map result_index_for_fed_item({ + {0x03000000, 0}, // Monomate + {0x03000100, 1}, // Dimate + {0x03000200, 2}, // Trimate + {0x03010000, 3}, // Monofluid + {0x03010100, 4}, // Difluid + {0x03010200, 5}, // Trifluid + {0x03060000, 6}, // Antidote + {0x03060100, 7}, // Antiparalysis + {0x03030000, 8}, // Sol Atomizer + {0x03040000, 9}, // Moon Atomizer + {0x03050000, 10}, // Star Atomizer + }); + + size_t result_index = result_index_for_fed_item.at(fed_item.primary_identifier()); + const auto& mag_def = item_parameter_table->get_mag(mag_item.data1[1]); + const auto& feed_result = item_parameter_table->get_mag_feed_result(mag_def.feed_table, result_index); + + auto update_stat = +[](ItemData& data, size_t which, int8_t delta) -> void { + uint16_t existing_stat = data.data1w[which] % 100; + if ((delta > 0) || ((delta < 0) && (-delta < existing_stat))) { + uint16_t level = data.compute_mag_level(); + if (level > 200) { + throw runtime_error("mag level is too high"); + } + if ((level == 200) && ((99 - existing_stat) < delta)) { + delta = 99 - existing_stat; + } + data.data1w[which] += delta; + } + }; + + update_stat(mag_item, 2, feed_result.def); + update_stat(mag_item, 3, feed_result.pow); + update_stat(mag_item, 4, feed_result.dex); + update_stat(mag_item, 5, feed_result.mind); + mag_item.data2[0] = clamp(static_cast(mag_item.data2[0]) + feed_result.synchro, 0, 120); + mag_item.data2[1] = clamp(static_cast(mag_item.data2[1]) + feed_result.iq, 0, 200); + + uint8_t mag_level = mag_item.compute_mag_level(); + mag_item.data1[2] = mag_level; + uint8_t evolution_number = mag_evolution_table->get_evolution_number(mag_item.data1[1]); + uint8_t mag_number = mag_item.data1[1]; + + // Note: Sega really did just hardcode all these rules into the client. There + // is no data file describing these evolutions, unfortunately. + + if (mag_level < 10) { + // Nothing to do + + } else if (mag_level < 35) { // Level 10 evolution + if (evolution_number < 1) { + switch (char_class) { + case 0: // HUmar + case 1: // HUnewearl + case 2: // HUcast + case 9: // HUcaseal + mag_item.data1[1] = 0x01; // Varuna + break; + case 3: // RAmar + case 11: // RAmarl + case 4: // RAcast + case 5: // RAcaseal + mag_item.data1[1] = 0x0D; // Kalki + break; + case 10: // FOmar + case 6: // FOmarl + case 7: // FOnewm + case 8: // FOnewearl + mag_item.data1[1] = 0x19; // Vritra + break; + default: + throw runtime_error("invalid character class"); + } + } + + } else if (mag_level < 50) { // Level 35 evolution + if (evolution_number < 2) { + uint16_t flags = mag_item.compute_mag_strength_flags(); + if (mag_number == 0x0D) { + if (flags & 0x110) { + mag_item.data1[1] = 0x02; + } else if (flags & 8) { + mag_item.data1[1] = 0x03; + } else if (flags & 0x20) { + mag_item.data1[1] = 0x0B; + } + } else if (mag_number == 1) { + if (flags & 0x108) { + mag_item.data1[1] = 0x0E; + } else if (flags & 0x10) { + mag_item.data1[1] = 0x0F; + } else if (flags & 0x20) { + mag_item.data1[1] = 0x04; + } + } else if (mag_number == 0x19) { + if (flags & 0x120) { + mag_item.data1[1] = 0x1A; + } else if (flags & 8) { + mag_item.data1[1] = 0x1B; + } else if (flags & 0x10) { + mag_item.data1[1] = 0x14; + } + } + } + + } else if ((mag_level % 5) == 0) { // Level 50 (and beyond) evolutions + if (evolution_number < 4) { + + if ((mag_level >= 100) && version_has_rare_mags) { + uint8_t section_id_group = section_id % 3; + uint16_t def = mag_item.data1w[2] / 100; + uint16_t pow = mag_item.data1w[3] / 100; + uint16_t dex = mag_item.data1w[4] / 100; + uint16_t mind = mag_item.data1w[5] / 100; + bool is_male = char_class_is_male(char_class); + size_t table_index = (is_male ? 0 : 1) + section_id_group * 2; + + bool is_hunter = char_class_is_hunter(char_class); + bool is_ranger = char_class_is_ranger(char_class); + bool is_force = char_class_is_force(char_class); + if (is_force) { + table_index += 12; + } else if (is_ranger) { + table_index += 6; + } else if (!is_hunter) { + throw logic_error("char class is not any of the top-level classes"); + } + + // Note: The original code checks the class (hunter/ranger/force) again + // here, and goes into 3 branches that each do these same checks. + // However, the result of all 3 branches is exactly the same! + if (((section_id_group == 0) && (pow + mind == def + dex)) || + ((section_id_group == 1) && (pow + dex == mind + def)) || + ((section_id_group == 2) && (pow + def == mind + dex))) { + // clang-format off + static const uint8_t result_table[] = { + // M0 F0 M1 F1 M2 F2 + 0x39, 0x3B, 0x3A, 0x3B, 0x3A, 0x3B, // Hunter + 0x3D, 0x3C, 0x3D, 0x3C, 0x3D, 0x3E, // Ranger + 0x41, 0x3F, 0x41, 0x40, 0x41, 0x40, // Force + }; + // clang-format on + mag_item.data1[1] = result_table[table_index]; + } + } + + // If a special evolution did not occur, do a normal level 50 evolution + if (mag_number == mag_item.data1[1]) { + uint16_t flags = mag_item.compute_mag_strength_flags(); + uint16_t def = mag_item.data1w[2] / 100; + uint16_t pow = mag_item.data1w[3] / 100; + uint16_t dex = mag_item.data1w[4] / 100; + uint16_t mind = mag_item.data1w[5] / 100; + + bool is_hunter = char_class_is_hunter(char_class); + bool is_ranger = char_class_is_ranger(char_class); + bool is_force = char_class_is_force(char_class); + if (is_hunter + is_ranger + is_force != 1) { + throw logic_error("char class is not exactly one of the top-level classes"); + } + + if (is_hunter) { + if (flags & 0x108) { + mag_item.data1[1] = (section_id & 1) + ? ((dex < mind) ? 0x08 : 0x06) + : ((dex < mind) ? 0x0C : 0x05); + } else if (flags & 0x010) { + mag_item.data1[1] = (section_id & 1) + ? ((mind < pow) ? 0x12 : 0x10) + : ((mind < pow) ? 0x17 : 0x13); + } else if (flags & 0x020) { + mag_item.data1[1] = (section_id & 1) + ? ((pow < dex) ? 0x16 : 0x24) + : ((pow < dex) ? 0x07 : 0x1E); + } + } else if (is_ranger) { + if (flags & 0x110) { + mag_item.data1[1] = (section_id & 1) + ? ((mind < pow) ? 0x0A : 0x05) + : ((mind < pow) ? 0x0C : 0x06); + } else if (flags & 0x008) { + mag_item.data1[1] = (section_id & 1) + ? ((dex < mind) ? 0x0A : 0x26) + : ((dex < mind) ? 0x0C : 0x06); + } else if (flags & 0x020) { + mag_item.data1[1] = (section_id & 1) + ? ((pow < dex) ? 0x18 : 0x1E) + : ((pow < dex) ? 0x08 : 0x05); + } + } else if (is_force) { + if (flags & 0x120) { + if (def < 45) { + mag_item.data1[1] = (section_id & 1) + ? ((pow < dex) ? 0x17 : 0x09) + : ((pow < dex) ? 0x1E : 0x1C); + } else { + mag_item.data1[1] = 0x24; + } + } else if (flags & 0x008) { + if (def < 45) { + mag_item.data1[1] = (section_id & 1) + ? ((dex < mind) ? 0x1C : 0x20) + : ((dex < mind) ? 0x1F : 0x25); + } else { + mag_item.data1[1] = 0x23; + } + } else if (flags & 0x010) { + if (def < 45) { + mag_item.data1[1] = (section_id & 1) + ? ((mind < pow) ? 0x12 : 0x0C) + : ((mind < pow) ? 0x15 : 0x11); + } else { + mag_item.data1[1] = 0x24; + } + } + } + } + } + } + + // If the mag has evolved, add its new photon blast + if (mag_number != mag_item.data1[1]) { + const auto& new_mag_def = item_parameter_table->get_mag(mag_item.data1[1]); + mag_item.add_mag_photon_blast(new_mag_def.photon_blast); + } +} + +void player_feed_mag(std::shared_ptr c, size_t mag_item_index, size_t fed_item_index) { + auto s = c->require_server_state(); + auto player = c->character(); + apply_mag_feed_result( + player->inventory.items[mag_item_index].data, + player->inventory.items[fed_item_index].data, + s->item_parameter_table(c->version()), + s->mag_evolution_table, + player->disp.visual.char_class, + player->disp.visual.section_id, + !is_v1_or_v2(c->version())); +} diff --git a/src/Items.hh b/src/Items.hh index 2924d762..20f48b0c 100644 --- a/src/Items.hh +++ b/src/Items.hh @@ -1,25 +1,25 @@ -#pragma once - -#include - -#include -#include - -#include "Client.hh" -#include "ItemData.hh" -#include "ItemParameterTable.hh" -#include "PSOEncryption.hh" -#include "ServerState.hh" -#include "StaticGameData.hh" - -void player_use_item(std::shared_ptr c, size_t item_index, std::shared_ptr opt_rand_crypt); -void player_feed_mag(std::shared_ptr c, size_t mag_item_index, size_t fed_item_index); - -void apply_mag_feed_result( - ItemData& mag_item, - const ItemData& fed_item, - std::shared_ptr item_parameter_table, - std::shared_ptr mag_evolution_table, - uint8_t char_class, - uint8_t section_id, - bool version_has_rare_mags); +#pragma once + +#include + +#include +#include + +#include "Client.hh" +#include "ItemData.hh" +#include "ItemParameterTable.hh" +#include "PSOEncryption.hh" +#include "ServerState.hh" +#include "StaticGameData.hh" + +void player_use_item(std::shared_ptr c, size_t item_index, std::shared_ptr opt_rand_crypt); +void player_feed_mag(std::shared_ptr c, size_t mag_item_index, size_t fed_item_index); + +void apply_mag_feed_result( + ItemData& mag_item, + const ItemData& fed_item, + std::shared_ptr item_parameter_table, + std::shared_ptr mag_evolution_table, + uint8_t char_class, + uint8_t section_id, + bool version_has_rare_mags); diff --git a/src/LevelTable.cc b/src/LevelTable.cc index 500af596..12d88a38 100644 --- a/src/LevelTable.cc +++ b/src/LevelTable.cc @@ -1,190 +1,190 @@ -#include "LevelTable.hh" - -#include - -#include - -#include "Compression.hh" -#include "PSOEncryption.hh" - -using namespace std; - -void LevelTable::reset_to_base(PlayerStats& stats, uint8_t char_class) const { - stats.level = 0; - stats.experience = 0; - stats.char_stats = this->base_stats_for_class(char_class); -} - -void LevelTable::advance_to_level(PlayerStats& stats, uint32_t level, uint8_t char_class) const { - for (; stats.level < level; stats.level++) { - const auto& level_stats = this->stats_delta_for_level(char_class, stats.level + 1); - // The original code clamps the resulting stat values to [0, max_stat]; we - // don't have max_stat handy so we just allow them to be unbounded - stats.char_stats.atp += level_stats.atp; - stats.char_stats.mst += level_stats.mst; - stats.char_stats.evp += level_stats.evp; - stats.char_stats.hp += level_stats.hp; - stats.char_stats.dfp += level_stats.dfp; - stats.char_stats.ata += level_stats.ata; - // Note: It is not a bug that lck is ignored here; the original code - // ignores it too. - stats.experience = level_stats.experience; - } -} - -LevelTableV2::LevelTableV2(const string& data, bool compressed) { - struct Offsets { - // TODO: The overall format of this file on V2 has much more data than we - // actually use. What's known of the structure so far: - le_uint32_t level_deltas; // -> u32[9] -> LevelStatsDelta[200] - le_uint32_t unknown_a1; // -> float[6] - le_uint32_t max_stats; // -> PlayerStats[9] - le_uint32_t level_100_stats; // -> Level100Entry[9] - le_uint32_t base_stats; // -> u32[9] -> CharacterStats - le_uint32_t unknown_a2; // -> (0x120 zero bytes) - le_uint32_t attack_data; // -> AttackData[9] - le_uint32_t unknown_a4; // -> (0x14-byte struct)[9] - le_uint32_t unknown_a5; // -> float[9] - le_uint32_t unknown_a6; // -> (0x30 bytes) - le_uint32_t unknown_a7; // -> (0x2D bytes) - le_uint32_t unknown_a8; // -> u32[3] -> float[0x2D] - le_uint32_t unknown_a9; // -> (0x90 bytes) - le_uint32_t unknown_a10; // -> u32[3] -> (0x10-byte struct)[0x0C] - le_uint32_t unknown_a11; // -> u32[3] -> (0x30-bytes) - le_uint32_t unknown_a12; // -> u32[3] -> (0x14-byte struct)[0x0F] - } __packed_ws__(Offsets, 0x40); - - StringReader r; - string decompressed_data; - if (compressed) { - decompressed_data = prs_decompress(data); - r = StringReader(decompressed_data); - } else { - r = StringReader(data); - } - - const auto& offsets = r.pget(r.pget_u32l(r.size() - 0x10)); - const auto& level_deltas_offsets = r.pget>(offsets.level_deltas); - const auto& base_stats_offsets = r.pget>(offsets.base_stats); - for (size_t char_class = 0; char_class < 9; char_class++) { - const auto& src_level_deltas = r.pget>(level_deltas_offsets[char_class]); - for (size_t level = 0; level < 200; level++) { - this->level_deltas[char_class][level] = src_level_deltas[level]; - } - this->max_stats[char_class] = r.pget(offsets.max_stats + char_class * sizeof(PlayerStats)); - this->level_100_stats[char_class] = r.pget(offsets.level_100_stats + char_class * sizeof(Level100Entry)); - this->base_stats[char_class] = r.pget(base_stats_offsets[char_class]); - } -} - -const CharacterStats& LevelTableV2::base_stats_for_class(uint8_t char_class) const { - return this->base_stats.at(char_class); -} - -const LevelTableV2::Level100Entry& LevelTableV2::level_100_stats_for_class(uint8_t char_class) const { - return this->level_100_stats.at(char_class); -} - -const PlayerStats& LevelTableV2::max_stats_for_class(uint8_t char_class) const { - return this->max_stats.at(char_class); -} - -const LevelStatsDelta& LevelTableV2::stats_delta_for_level(uint8_t char_class, uint8_t level) const { - return this->level_deltas.at(char_class).at(level); -} - -LevelTableV3BE::LevelTableV3BE(const string& data, bool encrypted) { - StringReader r; - string decompressed_data; - if (encrypted) { - auto decrypted = decrypt_pr2_data(data); - decompressed_data = prs_decompress(decrypted.compressed_data); - if (decompressed_data.size() != decrypted.decompressed_size) { - throw runtime_error("decompressed data size does not match expected size"); - } - r = StringReader(decompressed_data); - } else { - r = StringReader(data); - } - - // The GC format is very simple (but everything is big-endian): - // root: - // u32 offset: - // u32[12] offsets: - // LevelStatsDeltaBE[200] level_deltas - const auto& offsets = r.pget>(r.pget_u32b(r.pget_u32b(r.size() - 0x10))); - for (size_t char_class = 0; char_class < 12; char_class++) { - const auto& src_deltas = r.pget>(offsets[char_class]); - for (size_t level = 0; level < 200; level++) { - const auto& src_delta = src_deltas[level]; - auto& dest_delta = this->level_deltas[char_class][level]; - dest_delta.atp = src_delta.atp; - dest_delta.mst = src_delta.mst; - dest_delta.evp = src_delta.evp; - dest_delta.hp = src_delta.hp; - dest_delta.dfp = src_delta.dfp; - dest_delta.ata = src_delta.ata; - dest_delta.lck = src_delta.lck; - dest_delta.tp = src_delta.tp; - dest_delta.experience = src_delta.experience.load(); - } - } -} - -const CharacterStats& LevelTableV3BE::base_stats_for_class(uint8_t char_class) const { - static const array data = { - // ATP MST EVP HP DFP ATA LCK - CharacterStats{0x0023, 0x001D, 0x002D, 0x0014, 0x0011, 0x001E, 0x000A}, - CharacterStats{0x001E, 0x0028, 0x003C, 0x0013, 0x0016, 0x0019, 0x000A}, - CharacterStats{0x0023, 0x0000, 0x0023, 0x0016, 0x0012, 0x0023, 0x000A}, - CharacterStats{0x0012, 0x0014, 0x0024, 0x0010, 0x000D, 0x0028, 0x000A}, - CharacterStats{0x0019, 0x0000, 0x001F, 0x0012, 0x0012, 0x002D, 0x000A}, - CharacterStats{0x0014, 0x0000, 0x001F, 0x0011, 0x0017, 0x002D, 0x000A}, - CharacterStats{0x000D, 0x0035, 0x0023, 0x0014, 0x000A, 0x000F, 0x000A}, - CharacterStats{0x000D, 0x003C, 0x0032, 0x0013, 0x0007, 0x000C, 0x000A}, - CharacterStats{0x000A, 0x003A, 0x0035, 0x0013, 0x000D, 0x000A, 0x000A}, - CharacterStats{0x0023, 0x0000, 0x0023, 0x0016, 0x0012, 0x0023, 0x000A}, - CharacterStats{0x000D, 0x0035, 0x0023, 0x0014, 0x000A, 0x000F, 0x000A}, - CharacterStats{0x0012, 0x0014, 0x0024, 0x0010, 0x000D, 0x0028, 0x000A}, - }; - return data.at(char_class); -} - -const LevelStatsDelta& LevelTableV3BE::stats_delta_for_level(uint8_t char_class, uint8_t level) const { - return this->level_deltas.at(char_class).at(level); -} - -LevelTableV4::LevelTableV4(const string& data, bool compressed) { - struct Offsets { - le_uint32_t base_stats; // -> u32[12] -> CharacterStats - le_uint32_t level_deltas; // -> u32[12] -> LevelStatsDelta[200] - } __packed_ws__(Offsets, 8); - - StringReader r; - string decompressed_data; - if (compressed) { - decompressed_data = prs_decompress(data); - r = StringReader(decompressed_data); - } else { - r = StringReader(data); - } - - const auto& offsets = r.pget(r.pget_u32l(r.size() - 0x10)); - const auto& level_deltas_offsets = r.pget>(offsets.level_deltas); - const auto& base_stats_offsets = r.pget>(offsets.base_stats); - for (size_t char_class = 0; char_class < 12; char_class++) { - const auto& src_level_deltas = r.pget>(level_deltas_offsets[char_class]); - for (size_t level = 0; level < 200; level++) { - this->level_deltas[char_class][level] = src_level_deltas[level]; - } - this->base_stats[char_class] = r.pget(base_stats_offsets[char_class]); - } -} - -const CharacterStats& LevelTableV4::base_stats_for_class(uint8_t char_class) const { - return this->base_stats.at(char_class); -} - -const LevelStatsDelta& LevelTableV4::stats_delta_for_level(uint8_t char_class, uint8_t level) const { - return this->level_deltas.at(char_class).at(level); -} +#include "LevelTable.hh" + +#include + +#include + +#include "Compression.hh" +#include "PSOEncryption.hh" + +using namespace std; + +void LevelTable::reset_to_base(PlayerStats& stats, uint8_t char_class) const { + stats.level = 0; + stats.experience = 0; + stats.char_stats = this->base_stats_for_class(char_class); +} + +void LevelTable::advance_to_level(PlayerStats& stats, uint32_t level, uint8_t char_class) const { + for (; stats.level < level; stats.level++) { + const auto& level_stats = this->stats_delta_for_level(char_class, stats.level + 1); + // The original code clamps the resulting stat values to [0, max_stat]; we + // don't have max_stat handy so we just allow them to be unbounded + stats.char_stats.atp += level_stats.atp; + stats.char_stats.mst += level_stats.mst; + stats.char_stats.evp += level_stats.evp; + stats.char_stats.hp += level_stats.hp; + stats.char_stats.dfp += level_stats.dfp; + stats.char_stats.ata += level_stats.ata; + // Note: It is not a bug that lck is ignored here; the original code + // ignores it too. + stats.experience = level_stats.experience; + } +} + +LevelTableV2::LevelTableV2(const string& data, bool compressed) { + struct Offsets { + // TODO: The overall format of this file on V2 has much more data than we + // actually use. What's known of the structure so far: + le_uint32_t level_deltas; // -> u32[9] -> LevelStatsDelta[200] + le_uint32_t unknown_a1; // -> float[6] + le_uint32_t max_stats; // -> PlayerStats[9] + le_uint32_t level_100_stats; // -> Level100Entry[9] + le_uint32_t base_stats; // -> u32[9] -> CharacterStats + le_uint32_t unknown_a2; // -> (0x120 zero bytes) + le_uint32_t attack_data; // -> AttackData[9] + le_uint32_t unknown_a4; // -> (0x14-byte struct)[9] + le_uint32_t unknown_a5; // -> float[9] + le_uint32_t unknown_a6; // -> (0x30 bytes) + le_uint32_t unknown_a7; // -> (0x2D bytes) + le_uint32_t unknown_a8; // -> u32[3] -> float[0x2D] + le_uint32_t unknown_a9; // -> (0x90 bytes) + le_uint32_t unknown_a10; // -> u32[3] -> (0x10-byte struct)[0x0C] + le_uint32_t unknown_a11; // -> u32[3] -> (0x30-bytes) + le_uint32_t unknown_a12; // -> u32[3] -> (0x14-byte struct)[0x0F] + } __packed_ws__(Offsets, 0x40); + + StringReader r; + string decompressed_data; + if (compressed) { + decompressed_data = prs_decompress(data); + r = StringReader(decompressed_data); + } else { + r = StringReader(data); + } + + const auto& offsets = r.pget(r.pget_u32l(r.size() - 0x10)); + const auto& level_deltas_offsets = r.pget>(offsets.level_deltas); + const auto& base_stats_offsets = r.pget>(offsets.base_stats); + for (size_t char_class = 0; char_class < 9; char_class++) { + const auto& src_level_deltas = r.pget>(level_deltas_offsets[char_class]); + for (size_t level = 0; level < 200; level++) { + this->level_deltas[char_class][level] = src_level_deltas[level]; + } + this->max_stats[char_class] = r.pget(offsets.max_stats + char_class * sizeof(PlayerStats)); + this->level_100_stats[char_class] = r.pget(offsets.level_100_stats + char_class * sizeof(Level100Entry)); + this->base_stats[char_class] = r.pget(base_stats_offsets[char_class]); + } +} + +const CharacterStats& LevelTableV2::base_stats_for_class(uint8_t char_class) const { + return this->base_stats.at(char_class); +} + +const LevelTableV2::Level100Entry& LevelTableV2::level_100_stats_for_class(uint8_t char_class) const { + return this->level_100_stats.at(char_class); +} + +const PlayerStats& LevelTableV2::max_stats_for_class(uint8_t char_class) const { + return this->max_stats.at(char_class); +} + +const LevelStatsDelta& LevelTableV2::stats_delta_for_level(uint8_t char_class, uint8_t level) const { + return this->level_deltas.at(char_class).at(level); +} + +LevelTableV3BE::LevelTableV3BE(const string& data, bool encrypted) { + StringReader r; + string decompressed_data; + if (encrypted) { + auto decrypted = decrypt_pr2_data(data); + decompressed_data = prs_decompress(decrypted.compressed_data); + if (decompressed_data.size() != decrypted.decompressed_size) { + throw runtime_error("decompressed data size does not match expected size"); + } + r = StringReader(decompressed_data); + } else { + r = StringReader(data); + } + + // The GC format is very simple (but everything is big-endian): + // root: + // u32 offset: + // u32[12] offsets: + // LevelStatsDeltaBE[200] level_deltas + const auto& offsets = r.pget>(r.pget_u32b(r.pget_u32b(r.size() - 0x10))); + for (size_t char_class = 0; char_class < 12; char_class++) { + const auto& src_deltas = r.pget>(offsets[char_class]); + for (size_t level = 0; level < 200; level++) { + const auto& src_delta = src_deltas[level]; + auto& dest_delta = this->level_deltas[char_class][level]; + dest_delta.atp = src_delta.atp; + dest_delta.mst = src_delta.mst; + dest_delta.evp = src_delta.evp; + dest_delta.hp = src_delta.hp; + dest_delta.dfp = src_delta.dfp; + dest_delta.ata = src_delta.ata; + dest_delta.lck = src_delta.lck; + dest_delta.tp = src_delta.tp; + dest_delta.experience = src_delta.experience.load(); + } + } +} + +const CharacterStats& LevelTableV3BE::base_stats_for_class(uint8_t char_class) const { + static const array data = { + // ATP MST EVP HP DFP ATA LCK + CharacterStats{0x0023, 0x001D, 0x002D, 0x0014, 0x0011, 0x001E, 0x000A}, + CharacterStats{0x001E, 0x0028, 0x003C, 0x0013, 0x0016, 0x0019, 0x000A}, + CharacterStats{0x0023, 0x0000, 0x0023, 0x0016, 0x0012, 0x0023, 0x000A}, + CharacterStats{0x0012, 0x0014, 0x0024, 0x0010, 0x000D, 0x0028, 0x000A}, + CharacterStats{0x0019, 0x0000, 0x001F, 0x0012, 0x0012, 0x002D, 0x000A}, + CharacterStats{0x0014, 0x0000, 0x001F, 0x0011, 0x0017, 0x002D, 0x000A}, + CharacterStats{0x000D, 0x0035, 0x0023, 0x0014, 0x000A, 0x000F, 0x000A}, + CharacterStats{0x000D, 0x003C, 0x0032, 0x0013, 0x0007, 0x000C, 0x000A}, + CharacterStats{0x000A, 0x003A, 0x0035, 0x0013, 0x000D, 0x000A, 0x000A}, + CharacterStats{0x0023, 0x0000, 0x0023, 0x0016, 0x0012, 0x0023, 0x000A}, + CharacterStats{0x000D, 0x0035, 0x0023, 0x0014, 0x000A, 0x000F, 0x000A}, + CharacterStats{0x0012, 0x0014, 0x0024, 0x0010, 0x000D, 0x0028, 0x000A}, + }; + return data.at(char_class); +} + +const LevelStatsDelta& LevelTableV3BE::stats_delta_for_level(uint8_t char_class, uint8_t level) const { + return this->level_deltas.at(char_class).at(level); +} + +LevelTableV4::LevelTableV4(const string& data, bool compressed) { + struct Offsets { + le_uint32_t base_stats; // -> u32[12] -> CharacterStats + le_uint32_t level_deltas; // -> u32[12] -> LevelStatsDelta[200] + } __packed_ws__(Offsets, 8); + + StringReader r; + string decompressed_data; + if (compressed) { + decompressed_data = prs_decompress(data); + r = StringReader(decompressed_data); + } else { + r = StringReader(data); + } + + const auto& offsets = r.pget(r.pget_u32l(r.size() - 0x10)); + const auto& level_deltas_offsets = r.pget>(offsets.level_deltas); + const auto& base_stats_offsets = r.pget>(offsets.base_stats); + for (size_t char_class = 0; char_class < 12; char_class++) { + const auto& src_level_deltas = r.pget>(level_deltas_offsets[char_class]); + for (size_t level = 0; level < 200; level++) { + this->level_deltas[char_class][level] = src_level_deltas[level]; + } + this->base_stats[char_class] = r.pget(base_stats_offsets[char_class]); + } +} + +const CharacterStats& LevelTableV4::base_stats_for_class(uint8_t char_class) const { + return this->base_stats.at(char_class); +} + +const LevelStatsDelta& LevelTableV4::stats_delta_for_level(uint8_t char_class, uint8_t level) const { + return this->level_deltas.at(char_class).at(level); +} diff --git a/src/LevelTable.hh b/src/LevelTable.hh index c9286853..2d87c2ab 100644 --- a/src/LevelTable.hh +++ b/src/LevelTable.hh @@ -1,173 +1,173 @@ -#pragma once - -#include - -#include -#include -#include -#include - -#include "Text.hh" - -class LevelTable; - -template -struct CharacterStatsT { - using U16T = typename std::conditional::type; - - /* 00 */ U16T atp = 0; - /* 02 */ U16T mst = 0; - /* 04 */ U16T evp = 0; - /* 06 */ U16T hp = 0; - /* 08 */ U16T dfp = 0; - /* 0A */ U16T ata = 0; - /* 0C */ U16T lck = 0; - /* 0E */ - - operator CharacterStatsT() const { - CharacterStatsT ret; - ret.atp = this->atp.load(); - ret.mst = this->mst.load(); - ret.evp = this->evp.load(); - ret.hp = this->hp.load(); - ret.dfp = this->dfp.load(); - ret.ata = this->ata.load(); - ret.lck = this->lck.load(); - return ret; - } -} __packed__; -using CharacterStats = CharacterStatsT; -using CharacterStatsBE = CharacterStatsT; -check_struct_size(CharacterStats, 0x0E); -check_struct_size(CharacterStatsBE, 0x0E); - -template -struct PlayerStatsT { - using U16T = typename std::conditional::type; - using U32T = typename std::conditional::type; - using F32T = typename std::conditional::type; - - /* 00 */ CharacterStatsT char_stats; - /* 0E */ U16T esp = 0; - /* 10 */ F32T height = 0.0; - /* 14 */ F32T unknown_a3 = 0.0; - /* 18 */ U32T level = 0; - /* 1C */ U32T experience = 0; - /* 20 */ U32T meseta = 0; - /* 24 */ - - operator PlayerStatsT() const { - PlayerStatsT ret; - ret.char_stats = this->char_stats; - ret.esp = this->esp.load(); - ret.height = this->height.load(); - ret.unknown_a3 = this->unknown_a3.load(); - ret.level = this->level.load(); - ret.experience = this->experience.load(); - ret.meseta = this->meseta.load(); - return ret; - } -} __packed__; -using PlayerStats = PlayerStatsT; -using PlayerStatsBE = PlayerStatsT; -check_struct_size(PlayerStats, 0x24); -check_struct_size(PlayerStatsBE, 0x24); - -template -struct LevelStatsDeltaT { - using U32T = typename std::conditional::type; - - /* 00 */ uint8_t atp; - /* 01 */ uint8_t mst; - /* 02 */ uint8_t evp; - /* 03 */ uint8_t hp; - /* 04 */ uint8_t dfp; - /* 05 */ uint8_t ata; - /* 06 */ uint8_t lck; - /* 07 */ uint8_t tp; - /* 08 */ U32T experience; - /* 0C */ - - void apply(CharacterStats& ps) const { - ps.ata += this->ata; - ps.atp += this->atp; - ps.dfp += this->dfp; - ps.evp += this->evp; - ps.hp += this->hp; - ps.mst += this->mst; - ps.lck += this->lck; - } -} __packed__; -using LevelStatsDelta = LevelStatsDeltaT; -using LevelStatsDeltaBE = LevelStatsDeltaT; -check_struct_size(LevelStatsDelta, 0x0C); -check_struct_size(LevelStatsDeltaBE, 0x0C); - -class LevelTable { - // This is the base class for all the LevelTable implementations. The public - // interface here only defines functions that the server needs to handle - // requests, but some subclasses implement more functionality. See the - // comments and Offsets structures inside the subclasses' constructor - // implementations for more details on the file formats. -public: - virtual ~LevelTable() = default; - virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const = 0; - virtual const LevelStatsDelta& stats_delta_for_level(uint8_t char_class, uint8_t level) const = 0; - - void reset_to_base(PlayerStats& stats, uint8_t char_class) const; - void advance_to_level(PlayerStats& stats, uint32_t level, uint8_t char_class) const; - -protected: - LevelTable() = default; -}; - -class LevelTableV2 : public LevelTable { // from PlayerTable.prs (PC) -public: - struct Level100Entry { - /* 00 */ CharacterStats char_stats; - /* 0E */ le_uint16_t unknown_a1 = 0; - /* 10 */ le_float height = 0.0; - /* 14 */ le_float unknown_a3 = 0.0; - /* 18 */ le_uint32_t level = 0; - /* 1C */ - } __packed_ws__(Level100Entry, 0x1C); - - LevelTableV2(const std::string& data, bool compressed); - virtual ~LevelTableV2() = default; - - virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const; - const Level100Entry& level_100_stats_for_class(uint8_t char_class) const; - const PlayerStats& max_stats_for_class(uint8_t char_class) const; - virtual const LevelStatsDelta& stats_delta_for_level(uint8_t char_class, uint8_t level) const; - -private: - std::array base_stats; - std::array level_100_stats; - std::array max_stats; - std::array, 9> level_deltas; -}; - -class LevelTableV3BE : public LevelTable { // from PlyLevelTbl.cpt (GC) -public: - LevelTableV3BE(const std::string& data, bool encrypted); - virtual ~LevelTableV3BE() = default; - - virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const; - virtual const LevelStatsDelta& stats_delta_for_level(uint8_t char_class, uint8_t level) const; - -private: - std::array, 12> level_deltas; -}; - -class LevelTableV4 : public LevelTable { // from PlyLevelTbl.prs (BB) -public: - LevelTableV4(const std::string& data, bool compressed); - virtual ~LevelTableV4() = default; - - virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const; - virtual const LevelStatsDelta& stats_delta_for_level(uint8_t char_class, uint8_t level) const; - -private: - std::array base_stats; - std::array, 12> level_deltas; -}; +#pragma once + +#include + +#include +#include +#include +#include + +#include "Text.hh" + +class LevelTable; + +template +struct CharacterStatsT { + using U16T = typename std::conditional::type; + + /* 00 */ U16T atp = 0; + /* 02 */ U16T mst = 0; + /* 04 */ U16T evp = 0; + /* 06 */ U16T hp = 0; + /* 08 */ U16T dfp = 0; + /* 0A */ U16T ata = 0; + /* 0C */ U16T lck = 0; + /* 0E */ + + operator CharacterStatsT() const { + CharacterStatsT ret; + ret.atp = this->atp.load(); + ret.mst = this->mst.load(); + ret.evp = this->evp.load(); + ret.hp = this->hp.load(); + ret.dfp = this->dfp.load(); + ret.ata = this->ata.load(); + ret.lck = this->lck.load(); + return ret; + } +} __packed__; +using CharacterStats = CharacterStatsT; +using CharacterStatsBE = CharacterStatsT; +check_struct_size(CharacterStats, 0x0E); +check_struct_size(CharacterStatsBE, 0x0E); + +template +struct PlayerStatsT { + using U16T = typename std::conditional::type; + using U32T = typename std::conditional::type; + using F32T = typename std::conditional::type; + + /* 00 */ CharacterStatsT char_stats; + /* 0E */ U16T esp = 0; + /* 10 */ F32T height = 0.0; + /* 14 */ F32T unknown_a3 = 0.0; + /* 18 */ U32T level = 0; + /* 1C */ U32T experience = 0; + /* 20 */ U32T meseta = 0; + /* 24 */ + + operator PlayerStatsT() const { + PlayerStatsT ret; + ret.char_stats = this->char_stats; + ret.esp = this->esp.load(); + ret.height = this->height.load(); + ret.unknown_a3 = this->unknown_a3.load(); + ret.level = this->level.load(); + ret.experience = this->experience.load(); + ret.meseta = this->meseta.load(); + return ret; + } +} __packed__; +using PlayerStats = PlayerStatsT; +using PlayerStatsBE = PlayerStatsT; +check_struct_size(PlayerStats, 0x24); +check_struct_size(PlayerStatsBE, 0x24); + +template +struct LevelStatsDeltaT { + using U32T = typename std::conditional::type; + + /* 00 */ uint8_t atp; + /* 01 */ uint8_t mst; + /* 02 */ uint8_t evp; + /* 03 */ uint8_t hp; + /* 04 */ uint8_t dfp; + /* 05 */ uint8_t ata; + /* 06 */ uint8_t lck; + /* 07 */ uint8_t tp; + /* 08 */ U32T experience; + /* 0C */ + + void apply(CharacterStats& ps) const { + ps.ata += this->ata; + ps.atp += this->atp; + ps.dfp += this->dfp; + ps.evp += this->evp; + ps.hp += this->hp; + ps.mst += this->mst; + ps.lck += this->lck; + } +} __packed__; +using LevelStatsDelta = LevelStatsDeltaT; +using LevelStatsDeltaBE = LevelStatsDeltaT; +check_struct_size(LevelStatsDelta, 0x0C); +check_struct_size(LevelStatsDeltaBE, 0x0C); + +class LevelTable { + // This is the base class for all the LevelTable implementations. The public + // interface here only defines functions that the server needs to handle + // requests, but some subclasses implement more functionality. See the + // comments and Offsets structures inside the subclasses' constructor + // implementations for more details on the file formats. +public: + virtual ~LevelTable() = default; + virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const = 0; + virtual const LevelStatsDelta& stats_delta_for_level(uint8_t char_class, uint8_t level) const = 0; + + void reset_to_base(PlayerStats& stats, uint8_t char_class) const; + void advance_to_level(PlayerStats& stats, uint32_t level, uint8_t char_class) const; + +protected: + LevelTable() = default; +}; + +class LevelTableV2 : public LevelTable { // from PlayerTable.prs (PC) +public: + struct Level100Entry { + /* 00 */ CharacterStats char_stats; + /* 0E */ le_uint16_t unknown_a1 = 0; + /* 10 */ le_float height = 0.0; + /* 14 */ le_float unknown_a3 = 0.0; + /* 18 */ le_uint32_t level = 0; + /* 1C */ + } __packed_ws__(Level100Entry, 0x1C); + + LevelTableV2(const std::string& data, bool compressed); + virtual ~LevelTableV2() = default; + + virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const; + const Level100Entry& level_100_stats_for_class(uint8_t char_class) const; + const PlayerStats& max_stats_for_class(uint8_t char_class) const; + virtual const LevelStatsDelta& stats_delta_for_level(uint8_t char_class, uint8_t level) const; + +private: + std::array base_stats; + std::array level_100_stats; + std::array max_stats; + std::array, 9> level_deltas; +}; + +class LevelTableV3BE : public LevelTable { // from PlyLevelTbl.cpt (GC) +public: + LevelTableV3BE(const std::string& data, bool encrypted); + virtual ~LevelTableV3BE() = default; + + virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const; + virtual const LevelStatsDelta& stats_delta_for_level(uint8_t char_class, uint8_t level) const; + +private: + std::array, 12> level_deltas; +}; + +class LevelTableV4 : public LevelTable { // from PlyLevelTbl.prs (BB) +public: + LevelTableV4(const std::string& data, bool compressed); + virtual ~LevelTableV4() = default; + + virtual const CharacterStats& base_stats_for_class(uint8_t char_class) const; + virtual const LevelStatsDelta& stats_delta_for_level(uint8_t char_class, uint8_t level) const; + +private: + std::array base_stats; + std::array, 12> level_deltas; +}; diff --git a/src/Lobby.cc b/src/Lobby.cc index 1d0c053b..ca3cc0cd 100644 --- a/src/Lobby.cc +++ b/src/Lobby.cc @@ -1,1066 +1,1066 @@ -#include "Lobby.hh" - -#include - -#include - -#include "Compression.hh" -#include "Loggers.hh" -#include "SendCommands.hh" -#include "Text.hh" - -using namespace std; - -bool Lobby::FloorItem::visible_to_client(uint8_t client_id) const { - return this->flags & (1 << client_id); -} - -Lobby::FloorItemManager::FloorItemManager(uint32_t lobby_id, uint8_t floor) - : log(string_printf("[Lobby:%08" PRIX32 ":FloorItems:%02hhX] ", lobby_id, floor), lobby_log.min_level), - next_drop_number(0) {} - -bool Lobby::FloorItemManager::exists(uint32_t item_id) const { - return this->items.count(item_id); -} - -shared_ptr Lobby::FloorItemManager::find(uint32_t item_id) const { - return this->items.at(item_id); -} - -void Lobby::FloorItemManager::add(const ItemData& item, float x, float z, uint16_t flags) { - auto fi = make_shared(); - fi->data = item; - fi->x = x; - fi->z = z; - fi->drop_number = this->next_drop_number++; - fi->flags = flags; - this->add(fi); -} - -void Lobby::FloorItemManager::add(shared_ptr fi) { - if (fi->flags == 0) { - throw logic_error("floor item is not visible to any player"); - } - - auto emplace_ret = this->items.emplace(fi->data.id, fi); - if (!emplace_ret.second) { - throw runtime_error("floor item already exists with the same ID"); - } - for (size_t z = 0; z < 12; z++) { - if (fi->visible_to_client(z)) { - this->queue_for_client[z].emplace(fi->drop_number, fi); - } - } - this->log.info("Added floor item %08" PRIX32 " at %g, %g with drop number %" PRIu64 " with flags %03hX", - fi->data.id.load(), fi->x, fi->z, fi->drop_number, fi->flags); -} - -std::shared_ptr Lobby::FloorItemManager::remove(uint32_t item_id, uint8_t client_id) { - auto item_it = this->items.find(item_id); - if (item_it == this->items.end()) { - throw out_of_range("item not present"); - } - auto fi = item_it->second; - if ((client_id != 0xFF) && !fi->visible_to_client(client_id)) { - throw runtime_error("client does not have access to item"); - } - for (size_t z = 0; z < 12; z++) { - if (fi->visible_to_client(z) && !this->queue_for_client[z].erase(fi->drop_number)) { - throw logic_error("item queue for client is inconsistent"); - } - } - this->items.erase(item_it); - this->log.info("Removed floor item %08" PRIX32 " at %g, %g with drop number %" PRIu64 " with flags %03hX", - fi->data.id.load(), fi->x, fi->z, fi->drop_number, fi->flags); - return fi; -} - -std::unordered_set> Lobby::FloorItemManager::evict() { - unordered_set> ret; - for (size_t z = 0; z < 12; z++) { - while (this->queue_for_client[z].size() > 48) { - ret.emplace(this->remove(this->queue_for_client[z].begin()->second->data.id, 0xFF)); - } - } - this->log.info("Evicted %zu items", ret.size()); - return ret; -} - -void Lobby::FloorItemManager::clear_inaccessible(uint16_t remaining_clients_mask) { - unordered_set item_ids_to_delete; - for (const auto& it : this->items) { - if ((it.second->flags & remaining_clients_mask) == 0) { - item_ids_to_delete.emplace(it.first); - } - } - for (uint32_t item_id : item_ids_to_delete) { - this->remove(item_id, 0xFF); - } - this->log.info("Deleted %zu inaccessible items", item_ids_to_delete.size()); -} - -void Lobby::FloorItemManager::clear_private() { - unordered_set item_ids_to_delete; - for (const auto& it : this->items) { - if ((it.second->flags & 0x00F) != 0x00F) { - item_ids_to_delete.emplace(it.first); - } - } - for (uint32_t item_id : item_ids_to_delete) { - this->remove(item_id, 0xFF); - } - this->log.info("Deleted %zu private items", item_ids_to_delete.size()); -} - -void Lobby::FloorItemManager::clear() { - size_t num_items = this->items.size(); - this->items.clear(); - for (auto& queue : this->queue_for_client) { - queue.clear(); - } - this->next_drop_number = 0; - this->log.info("Deleted %zu items", num_items); -} - -uint32_t Lobby::FloorItemManager::reassign_all_item_ids(uint32_t next_item_id) { - ::map> old_items; - old_items.swap(this->items); - for (auto& queue : this->queue_for_client) { - queue.clear(); - } - for (auto& it : old_items) { - it.second->data.id = next_item_id++; - this->add(it.second); - } - return next_item_id; -} - -Lobby::Lobby(shared_ptr s, uint32_t id, bool is_game) - : server_state(s), - log(string_printf("[%s:%" PRIX32 "] ", is_game ? "Game" : "Lobby", id), lobby_log.min_level), - lobby_id(id), - min_level(0), - max_level(0xFFFFFFFF), - next_game_item_id(0xCC000000), - base_version(Version::GC_V3), - allowed_versions(0x0000), - override_section_id(0xFF), - episode(Episode::NONE), - mode(GameMode::NORMAL), - difficulty(0), - base_exp_multiplier(1), - exp_share_multiplier(0.5), - challenge_exp_multiplier(1.0f), - random_seed(random_object()), - drop_mode(DropMode::CLIENT), - event(0), - block(0), - leader_id(0), - max_clients(12), - enabled_flags(0), - idle_timeout_usecs(0), - idle_timeout_event( - event_new(s->base.get(), -1, EV_TIMEOUT | EV_PERSIST, &Lobby::dispatch_on_idle_timeout, this), - event_free) { - this->log.info("Created"); - if (is_game) { - this->set_flag(Flag::GAME); - } - this->reset_next_item_ids(); -} - -Lobby::~Lobby() { - this->log.info("Deleted"); -} - -void Lobby::reset_next_item_ids() { - uint32_t base_item_id = this->is_game() ? 0x00010000 : 0x10010000; - for (size_t x = 0; x < 12; x++) { - this->next_item_id_for_client[x] = base_item_id + 0x00200000 * x; - } - this->next_game_item_id = 0xCC000000; -} - -shared_ptr Lobby::require_server_state() const { - auto s = this->server_state.lock(); - if (!s) { - throw logic_error("server is deleted"); - } - return s; -} - -shared_ptr Lobby::require_challenge_params() const { - if (!this->challenge_params) { - throw runtime_error("challenge params are missing"); - } - return this->challenge_params; -} - -void Lobby::set_drop_mode(DropMode new_mode) { - this->drop_mode = new_mode; - - bool should_have_item_creator = (this->base_version == Version::BB_V4) || - ((new_mode != DropMode::DISABLED) && (new_mode != DropMode::CLIENT)); - if (should_have_item_creator && !this->item_creator) { - this->create_item_creator(); - } else if (!should_have_item_creator && this->item_creator) { - this->item_creator.reset(); - } -} - -void Lobby::create_item_creator() { - auto s = this->require_server_state(); - - shared_ptr rare_item_set; - shared_ptr common_item_set; - switch (this->base_version) { - case Version::PC_PATCH: - case Version::BB_PATCH: - case Version::GC_EP3_NTE: - case Version::GC_EP3: - throw runtime_error("cannot create item creator for this base version"); - case Version::DC_NTE: - case Version::DC_V1_11_2000_PROTOTYPE: - case Version::DC_V1: - // TODO: We should probably have a v1 common item set at some point too - common_item_set = s->common_item_set_v2; - rare_item_set = s->rare_item_sets.at("rare-table-v1"); - break; - case Version::DC_V2: - case Version::PC_NTE: - case Version::PC_V2: - common_item_set = s->common_item_set_v2; - rare_item_set = s->rare_item_sets.at("rare-table-v2"); - break; - case Version::GC_NTE: - case Version::GC_V3: - case Version::XB_V3: - common_item_set = s->common_item_set_v3_v4; - rare_item_set = s->rare_item_sets.at("rare-table-v3"); - break; - case Version::BB_V4: - common_item_set = s->common_item_set_v3_v4; - rare_item_set = s->rare_item_sets.at("rare-table-v4"); - break; - default: - throw logic_error("invalid lobby base version"); - } - this->item_creator = make_shared( - common_item_set, - rare_item_set, - s->armor_random_set, - s->tool_random_set, - s->weapon_random_sets.at(this->difficulty), - s->tekker_adjustment_set, - s->item_parameter_table(this->base_version), - s->item_stack_limits(this->base_version), - this->episode, - (this->mode == GameMode::SOLO) ? GameMode::NORMAL : this->mode, - this->difficulty, - this->effective_section_id(), - this->opt_rand_crypt, - this->quest ? this->quest->battle_rules : nullptr); -} - -void Lobby::change_section_id() { - if (this->item_creator) { - uint8_t new_section_id = this->effective_section_id(); - if (this->item_creator->get_section_id() != new_section_id) { - this->log.info("Changing section ID to %s", name_for_section_id(new_section_id)); - this->item_creator->set_section_id(new_section_id); - for (const auto& c : this->clients) { - if (c && c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { - send_text_message_printf(c, "$C5Section ID changed\nto %s (%hhu)", name_for_section_id(new_section_id), new_section_id); - } - } - } - } -} - -uint8_t Lobby::effective_section_id() const { - if (this->override_section_id != 0xFF) { - return this->override_section_id; - } - if (this->check_flag(Lobby::Flag::USE_CREATOR_SECTION_ID)) { - return this->creator_section_id; - } - auto leader = this->clients.at(this->leader_id); - if (leader) { - return leader->character()->disp.visual.section_id; - } - return 0; -} - -shared_ptr Lobby::load_maps( - Version version, - Episode episode, - uint8_t difficulty, - uint8_t event, - uint32_t lobby_id, - shared_ptr rare_rates, - uint32_t random_seed, - shared_ptr opt_rand_crypt, - shared_ptr quest_dat_contents_decompressed) { - auto map = make_shared(version, lobby_id, random_seed, opt_rand_crypt); - map->add_entities_from_quest_data( - episode, - difficulty, - event, - quest_dat_contents_decompressed->data(), - quest_dat_contents_decompressed->size(), - rare_rates); - return map; -} - -shared_ptr Lobby::load_maps( - Version version, - Episode episode, - GameMode mode, - uint8_t difficulty, - uint8_t event, - uint32_t lobby_id, - shared_ptr sdt, - function(Version, const string&)> get_file_data, - shared_ptr rare_rates, - uint32_t random_seed, - shared_ptr opt_rand_crypt, - const parray& variations, - const PrefixedLogger* log) { - auto enemy_filenames = sdt->map_filenames_for_variations(variations, episode, mode, SetDataTable::FilenameType::ENEMIES); - auto object_filenames = sdt->map_filenames_for_variations(variations, episode, mode, SetDataTable::FilenameType::OBJECTS); - auto event_filenames = sdt->map_filenames_for_variations(variations, episode, mode, SetDataTable::FilenameType::EVENTS); - return Lobby::load_maps( - enemy_filenames, - object_filenames, - event_filenames, - version, - episode, - mode, - difficulty, - event, - lobby_id, - get_file_data, - rare_rates, - random_seed, - opt_rand_crypt, - log); -} - -shared_ptr Lobby::load_maps( - const vector& enemy_filenames, - const vector& object_filenames, - const vector& event_filenames, - Version version, - Episode episode, - GameMode mode, - uint8_t difficulty, - uint8_t event, - uint32_t lobby_id, - function(Version, const string&)> get_file_data, - shared_ptr rare_rates, - uint32_t rare_seed, - shared_ptr opt_rand_crypt, - const PrefixedLogger* log) { - auto map = make_shared(version, lobby_id, rare_seed, opt_rand_crypt); - - // Don't load free-roam maps in Challenge mode, since players can't go to - // Ragol without a quest loaded - if (mode == GameMode::CHALLENGE) { - return map; - } - - for (size_t floor = 0; floor < 0x12; floor++) { - const auto& floor_enemy_filename = enemy_filenames.at(floor); - if (!floor_enemy_filename.empty()) { - auto map_data = get_file_data(version, floor_enemy_filename); - if (map_data) { - map->add_enemies_from_map_data( - episode, - difficulty, - event, - floor, - map_data->data(), - map_data->size(), - rare_rates); - if (log) { - log->info("Loaded enemies map %s for floor %02zX", floor_enemy_filename.c_str(), floor); - } - } else if (log) { - log->info("Enemies map %s for floor %02zX cannot be used; skipping", floor_enemy_filename.c_str(), floor); - } - } else if (log) { - log->info("No enemies to load for floor %02zX", floor); - } - - const auto& floor_object_filename = object_filenames.at(floor); - if (!floor_object_filename.empty()) { - auto map_data = get_file_data(version, floor_object_filename); - if (map_data) { - map->add_objects_from_map_data(floor, map_data->data(), map_data->size()); - if (log) { - log->info("Loaded objects map %s for floor %02zX", floor_object_filename.c_str(), floor); - } - } else if (log) { - log->info("Objects map %s for floor %02zX cannot be used; skipping", floor_object_filename.c_str(), floor); - } - } else if (log) { - log->info("No objects to load for floor %02zX", floor); - } - - const auto& floor_event_filename = event_filenames.at(floor); - if (!floor_event_filename.empty()) { - auto map_data = get_file_data(version, floor_event_filename); - if (map_data) { - map->add_events_from_map_data(floor, map_data->data(), map_data->size()); - if (log) { - log->info("Loaded events map %s for floor %02zX", floor_event_filename.c_str(), floor); - } - } else if (log) { - log->info("Events map %s for floor %02zX cannot be used; skipping", floor_event_filename.c_str(), floor); - } - } else if (log) { - log->info("No events to load for floor %02zX", floor); - } - } - - return map; -} - -void Lobby::load_maps() { - auto rare_rates = ((this->base_version == Version::BB_V4) && this->rare_enemy_rates) - ? this->rare_enemy_rates - : Map::DEFAULT_RARE_ENEMIES; - - if (this->quest) { - auto leader_c = this->clients.at(this->leader_id); - if (!leader_c) { - throw logic_error("lobby leader is missing"); - } - - auto vq = this->quest->version(this->base_version, leader_c->language()); - if (!vq->dat_contents_decompressed) { - throw runtime_error("quest does not have DAT data"); - } - this->map = this->load_maps( - this->base_version, - this->episode, - this->difficulty, - this->event, - this->lobby_id, - rare_rates, - this->random_seed, - this->opt_rand_crypt, - vq->dat_contents_decompressed); - - } else if (this->mode != GameMode::CHALLENGE) { - auto s = this->require_server_state(); - this->map = this->load_maps( - this->base_version, - this->episode, - this->mode, - this->difficulty, - this->event, - this->lobby_id, - s->set_data_table(this->base_version, this->episode, this->mode, this->difficulty), - bind(&ServerState::load_map_file, s.get(), placeholders::_1, placeholders::_2), - rare_rates, - this->random_seed, - this->opt_rand_crypt, - this->variations, - &this->log); - - } else { - this->map = make_shared(this->base_version, this->lobby_id, this->random_seed, this->opt_rand_crypt); - } - - this->log.info("Generated objects list (%zu entries):", this->map->objects.size()); - for (size_t z = 0; z < this->map->objects.size(); z++) { - string o_str = this->map->objects[z].str(); - this->log.info("(K-%zX) %s", z, o_str.c_str()); - } - this->log.info("Generated enemies list (%zu entries):", this->map->enemies.size()); - for (size_t z = 0; z < this->map->enemies.size(); z++) { - string e_str = this->map->enemies[z].str(); - this->log.info("(E-%zX) %s", z, e_str.c_str()); - } - this->log.info("Generated events list (%zu entries):", this->map->events.size()); - for (size_t z = 0; z < this->map->events.size(); z++) { - string e_str = this->map->events[z].str(); - this->log.info("%s", e_str.c_str()); - } - this->log.info("Loaded maps contain %zu object entries and %zu enemy entries overall (%zu as rares)", - this->map->objects.size(), this->map->enemies.size(), this->map->rare_enemy_indexes.size()); -} - -void Lobby::create_ep3_server() { - auto s = this->require_server_state(); - if (!this->ep3_server) { - this->log.info("Creating Episode 3 server state"); - } else { - this->log.info("Recreating Episode 3 server state"); - } - auto tourn = this->tournament_match ? this->tournament_match->tournament.lock() : nullptr; - bool is_nte = this->base_version == Version::GC_EP3_NTE; - Episode3::Server::Options options = { - .card_index = is_nte ? s->ep3_card_index_trial : s->ep3_card_index, - .map_index = s->ep3_map_index, - .behavior_flags = s->ep3_behavior_flags, - .opt_rand_stream = nullptr, - .opt_rand_crypt = this->opt_rand_crypt, - .tournament = tourn, - .trap_card_ids = s->ep3_trap_card_ids, - }; - if (this->base_version == Version::GC_EP3_NTE) { - options.behavior_flags |= Episode3::BehaviorFlag::IS_TRIAL_EDITION; - } else { - options.behavior_flags &= (~Episode3::BehaviorFlag::IS_TRIAL_EDITION); - } - this->ep3_server = make_shared(this->shared_from_this(), std::move(options)); - this->ep3_server->init(); -} - -void Lobby::reassign_leader_on_client_departure(size_t leaving_client_index) { - for (size_t x = 0; x < this->max_clients; x++) { - if (x == leaving_client_index) { - continue; - } - if (this->clients[x]) { - this->leader_id = x; - this->change_section_id(); - return; - } - } - this->leader_id = 0; -} - -bool Lobby::any_client_loading() const { - for (size_t x = 0; x < this->max_clients; x++) { - auto lc = this->clients[x]; - if (!lc.get()) { - continue; - } - if (lc->config.check_flag(Client::Flag::LOADING) || - lc->config.check_flag(Client::Flag::LOADING_QUEST) || - lc->config.check_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST)) { - return true; - } - } - return false; -} - -size_t Lobby::count_clients() const { - size_t ret = 0; - for (size_t x = 0; x < this->max_clients; x++) { - if (this->clients[x]) { - ret++; - } - } - return ret; -} - -bool Lobby::any_v1_clients_present() const { - for (size_t x = 0; x < this->max_clients; x++) { - if (this->clients[x] && is_v1(this->clients[x]->version())) { - return true; - } - } - return false; -} - -void Lobby::add_client(shared_ptr c, ssize_t required_client_id) { - if (!c->login) { - throw runtime_error("client is not logged in"); - } - - ssize_t index; - ssize_t min_client_id = this->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM) ? 4 : 0; - - if (required_client_id >= 0) { - if (this->clients.at(required_client_id).get()) { - throw out_of_range("required slot is in use"); - } - this->clients[required_client_id] = c; - index = required_client_id; - - } else if (c->config.check_flag(Client::Flag::DEBUG_ENABLED) && (this->mode != GameMode::SOLO)) { - for (index = this->max_clients - 1; index >= min_client_id; index--) { - if (!this->clients[index].get()) { - this->clients[index] = c; - break; - } - } - if (index < min_client_id) { - throw out_of_range("no space left in lobby"); - } - } else { - for (index = min_client_id; index < this->max_clients; index++) { - if (!this->clients[index].get()) { - this->clients[index] = c; - break; - } - } - if (index >= this->max_clients) { - throw out_of_range("no space left in lobby"); - } - } - - c->lobby_client_id = index; - c->lobby = this->weak_from_this(); - c->lobby_arrow_color = 0; - - // If there's no one else in the lobby, set the leader id as well - size_t leader_index; - for (leader_index = 0; leader_index < this->max_clients; leader_index++) { - if (this->clients[leader_index] && (this->clients[leader_index] != c)) { - break; - } - } - if (leader_index >= this->max_clients) { - this->leader_id = c->lobby_client_id; - this->change_section_id(); - } - - // If this is a lobby or no one was here before this, reassign all the floor - // item IDs and reset the next item IDs - if (!this->is_game() || (leader_index >= this->max_clients)) { - this->reset_next_item_ids(); - for (auto& m : this->floor_item_managers) { - this->next_game_item_id = m.reassign_all_item_ids(this->next_game_item_id); - } - } - - // If this is not a game or the joining client is the leader, they will assign - // their item IDs BEFORE they process any inbound commands (therefore a 6x6D - // command, which we will send during loading, should reflect the item state - // AFTER their IDs are assigned). If the joining client is not the leader, - // they will not assign their item IDs until they receive a 6x71 command, - // which is sent AFTER the 6x6D command, so the 6x6D should reflect the item - // state BEFORE their IDs are assigned. (In the latter case, we'll assign the - // IDs for real when they send a 6F command, or 6x1F equivalent in the case of - // DC NTE and 11/2000.) - this->assign_inventory_and_bank_item_ids(c, (!this->is_game() || (c->lobby_client_id == this->leader_id))); - - // On BB, we send artificial flag state to fix an Episode 2 bug where the - // CCA door lock state is overwritten by quests. - if (this->is_game() && (c->version() == Version::BB_V4)) { - c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_FLAG_STATE); - } - - // If the lobby is recording a battle record, add the player join event - if (this->battle_record) { - auto p = c->character(); - PlayerLobbyDataDCGC lobby_data; - lobby_data.player_tag = 0x00010000; - lobby_data.guild_card_number = c->login->account->account_id; - lobby_data.name.encode(p->disp.name.decode(c->language()), c->language()); - this->battle_record->add_player( - lobby_data, - p->inventory, - p->disp.to_dcpcv3(c->language(), c->language()), - c->ep3_config ? (c->ep3_config->online_clv_exp / 100) : 0); - } - - // Send spectator count notifications if needed - if (this->is_game() && this->is_ep3()) { - if (this->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM)) { - auto watched_l = this->watched_lobby.lock(); - if (watched_l) { - send_ep3_update_game_metadata(watched_l); - } - } else { - send_ep3_update_game_metadata(this->shared_from_this()); - } - } - - // There is a player in the lobby, so it is no longer idle - if (event_pending(this->idle_timeout_event.get(), EV_TIMEOUT, nullptr)) { - event_del(this->idle_timeout_event.get()); - this->log.info("Idle timeout cancelled"); - } -} - -void Lobby::remove_client(shared_ptr c) { - if (this->clients.at(c->lobby_client_id) != c) { - auto other_c = this->clients[c->lobby_client_id].get(); - throw logic_error(string_printf( - "client\'s lobby client id (%hhu) does not match client list (%u)", - c->lobby_client_id, - static_cast(other_c ? other_c->lobby_client_id : 0xFF))); - } - this->clients[c->lobby_client_id] = nullptr; - - // Unassign the client's lobby if it matches the current lobby (it may not - // match if the client was already added to another lobby - this can happen - // during the lobby change procedure) - { - auto c_lobby = c->lobby.lock(); - if (c_lobby.get() == this) { - c->lobby.reset(); - } - } - - this->reassign_leader_on_client_departure(c->lobby_client_id); - - // If the lobby is recording a battle record, add the player leave event - if (this->battle_record) { - this->battle_record->delete_player(c->lobby_client_id); - } - - // If the lobby is Episode 3, update the appropriate spectator counts - if (this->is_game() && this->is_ep3()) { - if (this->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM)) { - auto watched_l = this->watched_lobby.lock(); - if (watched_l) { - send_ep3_update_game_metadata(watched_l); - } - } else { - send_ep3_update_game_metadata(this->shared_from_this()); - } - } - - // If there are still players left in the lobby, delete all items that only - // the leaving player could see. Don't do this if no one is left in the lobby, - // since that would mean items could not persist in empty lobbies. - uint16_t remaining_clients_mask = 0; - for (size_t z = 0; z < 12; z++) { - if (this->clients[z]) { - remaining_clients_mask |= (1 << z); - } - } - if (remaining_clients_mask) { - for (auto& m : this->floor_item_managers) { - m.clear_inaccessible(remaining_clients_mask); - } - } else { - for (auto& m : this->floor_item_managers) { - m.clear_private(); - } - } - - if (!remaining_clients_mask && - this->check_flag(Flag::PERSISTENT) && - !this->check_flag(Flag::DEFAULT) && - (this->idle_timeout_usecs > 0)) { - // If the lobby is persistent but has an idle timeout, make it expire after - // the specified time - auto tv = usecs_to_timeval(this->idle_timeout_usecs); - event_add(this->idle_timeout_event.get(), &tv); - this->log.info("Idle timeout scheduled"); - } -} - -void Lobby::move_client_to_lobby( - shared_ptr dest_lobby, - shared_ptr c, - ssize_t required_client_id) { - if (dest_lobby.get() == this) { - return; - } - - if (required_client_id >= 0) { - if (dest_lobby->clients.at(required_client_id)) { - throw out_of_range("required slot is in use"); - } - } else { - ssize_t min_client_id = this->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM) ? 4 : 0; - size_t available_slots = dest_lobby->max_clients - min_client_id; - if (dest_lobby->count_clients() >= available_slots) { - throw out_of_range("no space left in lobby"); - } - } - - this->remove_client(c); - dest_lobby->add_client(c, required_client_id); -} - -shared_ptr Lobby::find_client(const string* identifier, uint64_t account_id) { - for (size_t x = 0; x < this->max_clients; x++) { - auto lc = this->clients[x]; - if (!lc) { - continue; - } - if (account_id && lc->login && (lc->login->account->account_id == account_id)) { - return lc; - } - if (identifier && (lc->character()->disp.name.eq(*identifier, lc->language()))) { - return lc; - } - } - - throw out_of_range("client not found"); -} - -Lobby::JoinError Lobby::join_error_for_client(std::shared_ptr c, const std::string* password) const { - if (this->count_clients() >= this->max_clients) { - return JoinError::FULL; - } - bool debug_enabled = c->config.check_flag(Client::Flag::DEBUG_ENABLED); - if (!this->version_is_allowed(c->version()) && !debug_enabled) { - return JoinError::VERSION_CONFLICT; - } - if (this->is_game()) { - if (this->check_flag(Flag::QUEST_SELECTION_IN_PROGRESS)) { - return JoinError::QUEST_SELECTION_IN_PROGRESS; - } - if (this->check_flag(Flag::QUEST_IN_PROGRESS)) { - return JoinError::QUEST_IN_PROGRESS; - } - if (this->check_flag(Flag::BATTLE_IN_PROGRESS)) { - return JoinError::BATTLE_IN_PROGRESS; - } - if (this->mode == GameMode::SOLO) { - return JoinError::SOLO; - } - if (!debug_enabled && - (this->check_flag(Flag::IS_CLIENT_CUSTOMIZATION) != c->config.check_flag(Client::Flag::IS_CLIENT_CUSTOMIZATION))) { - return JoinError::VERSION_CONFLICT; - } - if (!c->login->account->check_flag(Account::Flag::FREE_JOIN_GAMES)) { - if (password && !this->password.empty() && (*password != this->password)) { - return JoinError::INCORRECT_PASSWORD; - } - auto p = c->character(); - if (p->disp.stats.level < this->min_level) { - return JoinError::LEVEL_TOO_LOW; - } - if (p->disp.stats.level > this->max_level) { - return JoinError::LEVEL_TOO_HIGH; - } - if (this->quest) { - size_t num_clients = this->count_clients() + 1; - bool v1_present = is_v1(c->version()) || this->any_v1_clients_present(); - auto this_sh = this->shared_from_this(); - if (!c->can_see_quest(this->quest, this_sh, this->event, this->difficulty, num_clients, v1_present) || - !c->can_play_quest(this->quest, this_sh, this->event, this->difficulty, num_clients, v1_present)) { - return JoinError::NO_ACCESS_TO_QUEST; - } - } - } - // Only prevent joining during loading if the client is actually trying to - // join (not just loading the game list) - if (password && this->any_client_loading()) { - return JoinError::LOADING; - } - } - return JoinError::ALLOWED; -} - -bool Lobby::item_exists(uint8_t floor, uint32_t item_id) const { - if (floor >= this->floor_item_managers.size()) { - return false; - } - return this->floor_item_managers.at(floor).exists(item_id); -} - -shared_ptr Lobby::find_item(uint8_t floor, uint32_t item_id) const { - return this->floor_item_managers.at(floor).find(item_id); -} - -void Lobby::add_item(uint8_t floor, const ItemData& data, float x, float z, uint16_t flags) { - auto& m = this->floor_item_managers.at(floor); - m.add(data, x, z, flags); - this->evict_items_from_floor(floor); -} - -void Lobby::add_item(uint8_t floor, shared_ptr fi) { - auto& m = this->floor_item_managers.at(floor); - m.add(fi); - this->evict_items_from_floor(floor); -} - -void Lobby::evict_items_from_floor(uint8_t floor) { - auto& m = this->floor_item_managers.at(floor); - auto evicted = m.evict(); - if (!evicted.empty()) { - auto l = this->shared_from_this(); - for (const auto& fi : evicted) { - for (size_t z = 0; z < 12; z++) { - auto lc = this->clients[z]; - if (lc && fi->visible_to_client(z)) { - send_destroy_floor_item_to_client(lc, fi->data.id, floor); - } - } - } - } -} - -shared_ptr Lobby::remove_item(uint8_t floor, uint32_t item_id, uint8_t requesting_client_id) { - return this->floor_item_managers.at(floor).remove(item_id, requesting_client_id); -} - -uint32_t Lobby::generate_item_id(uint8_t client_id) { - if (client_id < this->max_clients) { - return this->next_item_id_for_client[client_id]++; - } - return this->next_game_item_id++; -} - -void Lobby::on_item_id_generated_externally(uint32_t item_id) { - // Note: The client checks for the range (0x00010000, 0x02010000) here, but - // server-side item drop logic uses 0x00810000 as its base ID, so we restrict - // the range further here. - if ((item_id > 0x00010000) && (item_id < 0x00810000)) { - uint16_t item_client_id = (item_id >> 21) & 0x7FF; - uint32_t& next_item_id = this->next_item_id_for_client.at(item_client_id); - next_item_id = std::max(next_item_id, item_id + 1); - } -} - -void Lobby::assign_inventory_and_bank_item_ids(shared_ptr c, bool consume_ids) { - auto p = c->character(); - uint32_t orig_next_item_id = this->next_item_id_for_client.at(c->lobby_client_id); - for (size_t z = 0; z < p->inventory.num_items; z++) { - p->inventory.items[z].data.id = this->generate_item_id(c->lobby_client_id); - } - if (!consume_ids) { - this->next_item_id_for_client[c->lobby_client_id] = orig_next_item_id; - } - - if (c->log.info("Assigned inventory item IDs%s", consume_ids ? "" : " but did not mark IDs as used")) { - c->print_inventory(stderr); - if (p->bank.num_items) { - p->bank.assign_ids(0x99000000 + (c->lobby_client_id << 20)); - c->log.info("Assigned bank item IDs"); - c->print_bank(stderr); - } else { - c->log.info("Bank is empty"); - } - } -} - -unordered_map> Lobby::clients_by_account_id() const { - unordered_map> ret; - for (auto c : this->clients) { - if (c && c->login) { - ret.emplace(c->login->account->account_id, c); - } - } - return ret; -} - -QuestIndex::IncludeCondition Lobby::quest_include_condition() const { - size_t num_players = this->count_clients(); - bool v1_present = this->any_v1_clients_present(); - return [this, num_players, v1_present](shared_ptr q) -> QuestIndex::IncludeState { - bool is_enabled = true; - for (const auto& lc : this->clients) { - auto this_sh = this->shared_from_this(); - if (lc && !lc->can_see_quest(q, this_sh, this->event, this->difficulty, num_players, v1_present)) { - return QuestIndex::IncludeState::HIDDEN; - } - if (lc && !lc->can_play_quest(q, this_sh, this->event, this->difficulty, num_players, v1_present)) { - is_enabled = false; - } - } - return is_enabled ? QuestIndex::IncludeState::AVAILABLE : QuestIndex::IncludeState::DISABLED; - }; -} - -void Lobby::dispatch_on_idle_timeout(evutil_socket_t, short, void* ctx) { - auto l = reinterpret_cast(ctx)->shared_from_this(); - if (l->count_clients() == 0) { - l->log.info("Idle timeout expired"); - auto s = l->require_server_state(); - s->remove_lobby(l); - } else { - l->log.error("Idle timeout occurred, but clients are present in lobby"); - event_del(l->idle_timeout_event.get()); - } -} - -bool Lobby::compare_shared(const shared_ptr& a, const shared_ptr& b) { - // Sort keys: - // 1. Priority class: has free space < empty (persistent) < full < non-joinable (in quest/battle) - // 2. Password: public < locked - // 3. Game mode: Normal < Battle < Challenge < Solo - // 4. Episode: 1 < 2 < 4 - // 5. Difficulty: Normal < Hard < Very Hard < Ultimate - // 6. Game name - static auto get_priority = +[](const shared_ptr& l) -> size_t { - if (l->check_flag(Lobby::Flag::QUEST_SELECTION_IN_PROGRESS) || - l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS) || - l->check_flag(Lobby::Flag::BATTLE_IN_PROGRESS)) { - return 4; - } - size_t num_clients = l->count_clients(); - if (num_clients == l->max_clients) { - return 3; - } - if (num_clients == 0) { - return 2; - } - return 1; - }; - size_t a_priority = get_priority(a); - size_t b_priority = get_priority(b); - if (a_priority < b_priority) { - return true; - } else if (a_priority > b_priority) { - return false; - } - - if (a->password.empty() && !b->password.empty()) { - return true; - } else if (!a->password.empty() && b->password.empty()) { - return false; - } - - size_t a_mode = static_cast(a->mode); - size_t b_mode = static_cast(b->mode); - if (a_mode < b_mode) { - return true; - } else if (a_mode > b_mode) { - return false; - } - - size_t a_episode = static_cast(a->episode); - size_t b_episode = static_cast(b->episode); - if (a_episode < b_episode) { - return true; - } else if (a_episode > b_episode) { - return false; - } - - if (a->difficulty < b->difficulty) { - return true; - } else if (a->difficulty > b->difficulty) { - return false; - } - - return a->name < b->name; -} - -template <> -Lobby::DropMode enum_for_name(const char* name) { - if (!strcmp(name, "DISABLED")) { - return Lobby::DropMode::DISABLED; - } else if (!strcmp(name, "CLIENT")) { - return Lobby::DropMode::CLIENT; - } else if (!strcmp(name, "SERVER_SHARED")) { - return Lobby::DropMode::SERVER_SHARED; - } else if (!strcmp(name, "SERVER_PRIVATE")) { - return Lobby::DropMode::SERVER_PRIVATE; - } else if (!strcmp(name, "SERVER_DUPLICATE")) { - return Lobby::DropMode::SERVER_DUPLICATE; - } else { - throw runtime_error("invalid drop mode"); - } -} - -template <> -const char* name_for_enum(Lobby::DropMode value) { - switch (value) { - case Lobby::DropMode::DISABLED: - return "DISABLED"; - case Lobby::DropMode::CLIENT: - return "CLIENT"; - case Lobby::DropMode::SERVER_SHARED: - return "SERVER_SHARED"; - case Lobby::DropMode::SERVER_PRIVATE: - return "SERVER_PRIVATE"; - case Lobby::DropMode::SERVER_DUPLICATE: - return "SERVER_DUPLICATE"; - default: - throw runtime_error("invalid drop mode"); - } -} +#include "Lobby.hh" + +#include + +#include + +#include "Compression.hh" +#include "Loggers.hh" +#include "SendCommands.hh" +#include "Text.hh" + +using namespace std; + +bool Lobby::FloorItem::visible_to_client(uint8_t client_id) const { + return this->flags & (1 << client_id); +} + +Lobby::FloorItemManager::FloorItemManager(uint32_t lobby_id, uint8_t floor) + : log(string_printf("[Lobby:%08" PRIX32 ":FloorItems:%02hhX] ", lobby_id, floor), lobby_log.min_level), + next_drop_number(0) {} + +bool Lobby::FloorItemManager::exists(uint32_t item_id) const { + return this->items.count(item_id); +} + +shared_ptr Lobby::FloorItemManager::find(uint32_t item_id) const { + return this->items.at(item_id); +} + +void Lobby::FloorItemManager::add(const ItemData& item, float x, float z, uint16_t flags) { + auto fi = make_shared(); + fi->data = item; + fi->x = x; + fi->z = z; + fi->drop_number = this->next_drop_number++; + fi->flags = flags; + this->add(fi); +} + +void Lobby::FloorItemManager::add(shared_ptr fi) { + if (fi->flags == 0) { + throw logic_error("floor item is not visible to any player"); + } + + auto emplace_ret = this->items.emplace(fi->data.id, fi); + if (!emplace_ret.second) { + throw runtime_error("floor item already exists with the same ID"); + } + for (size_t z = 0; z < 12; z++) { + if (fi->visible_to_client(z)) { + this->queue_for_client[z].emplace(fi->drop_number, fi); + } + } + this->log.info("Added floor item %08" PRIX32 " at %g, %g with drop number %" PRIu64 " with flags %03hX", + fi->data.id.load(), fi->x, fi->z, fi->drop_number, fi->flags); +} + +std::shared_ptr Lobby::FloorItemManager::remove(uint32_t item_id, uint8_t client_id) { + auto item_it = this->items.find(item_id); + if (item_it == this->items.end()) { + throw out_of_range("item not present"); + } + auto fi = item_it->second; + if ((client_id != 0xFF) && !fi->visible_to_client(client_id)) { + throw runtime_error("client does not have access to item"); + } + for (size_t z = 0; z < 12; z++) { + if (fi->visible_to_client(z) && !this->queue_for_client[z].erase(fi->drop_number)) { + throw logic_error("item queue for client is inconsistent"); + } + } + this->items.erase(item_it); + this->log.info("Removed floor item %08" PRIX32 " at %g, %g with drop number %" PRIu64 " with flags %03hX", + fi->data.id.load(), fi->x, fi->z, fi->drop_number, fi->flags); + return fi; +} + +std::unordered_set> Lobby::FloorItemManager::evict() { + unordered_set> ret; + for (size_t z = 0; z < 12; z++) { + while (this->queue_for_client[z].size() > 48) { + ret.emplace(this->remove(this->queue_for_client[z].begin()->second->data.id, 0xFF)); + } + } + this->log.info("Evicted %zu items", ret.size()); + return ret; +} + +void Lobby::FloorItemManager::clear_inaccessible(uint16_t remaining_clients_mask) { + unordered_set item_ids_to_delete; + for (const auto& it : this->items) { + if ((it.second->flags & remaining_clients_mask) == 0) { + item_ids_to_delete.emplace(it.first); + } + } + for (uint32_t item_id : item_ids_to_delete) { + this->remove(item_id, 0xFF); + } + this->log.info("Deleted %zu inaccessible items", item_ids_to_delete.size()); +} + +void Lobby::FloorItemManager::clear_private() { + unordered_set item_ids_to_delete; + for (const auto& it : this->items) { + if ((it.second->flags & 0x00F) != 0x00F) { + item_ids_to_delete.emplace(it.first); + } + } + for (uint32_t item_id : item_ids_to_delete) { + this->remove(item_id, 0xFF); + } + this->log.info("Deleted %zu private items", item_ids_to_delete.size()); +} + +void Lobby::FloorItemManager::clear() { + size_t num_items = this->items.size(); + this->items.clear(); + for (auto& queue : this->queue_for_client) { + queue.clear(); + } + this->next_drop_number = 0; + this->log.info("Deleted %zu items", num_items); +} + +uint32_t Lobby::FloorItemManager::reassign_all_item_ids(uint32_t next_item_id) { + ::map> old_items; + old_items.swap(this->items); + for (auto& queue : this->queue_for_client) { + queue.clear(); + } + for (auto& it : old_items) { + it.second->data.id = next_item_id++; + this->add(it.second); + } + return next_item_id; +} + +Lobby::Lobby(shared_ptr s, uint32_t id, bool is_game) + : server_state(s), + log(string_printf("[%s:%" PRIX32 "] ", is_game ? "Game" : "Lobby", id), lobby_log.min_level), + lobby_id(id), + min_level(0), + max_level(0xFFFFFFFF), + next_game_item_id(0xCC000000), + base_version(Version::GC_V3), + allowed_versions(0x0000), + override_section_id(0xFF), + episode(Episode::NONE), + mode(GameMode::NORMAL), + difficulty(0), + base_exp_multiplier(1), + exp_share_multiplier(0.5), + challenge_exp_multiplier(1.0f), + random_seed(random_object()), + drop_mode(DropMode::CLIENT), + event(0), + block(0), + leader_id(0), + max_clients(12), + enabled_flags(0), + idle_timeout_usecs(0), + idle_timeout_event( + event_new(s->base.get(), -1, EV_TIMEOUT | EV_PERSIST, &Lobby::dispatch_on_idle_timeout, this), + event_free) { + this->log.info("Created"); + if (is_game) { + this->set_flag(Flag::GAME); + } + this->reset_next_item_ids(); +} + +Lobby::~Lobby() { + this->log.info("Deleted"); +} + +void Lobby::reset_next_item_ids() { + uint32_t base_item_id = this->is_game() ? 0x00010000 : 0x10010000; + for (size_t x = 0; x < 12; x++) { + this->next_item_id_for_client[x] = base_item_id + 0x00200000 * x; + } + this->next_game_item_id = 0xCC000000; +} + +shared_ptr Lobby::require_server_state() const { + auto s = this->server_state.lock(); + if (!s) { + throw logic_error("server is deleted"); + } + return s; +} + +shared_ptr Lobby::require_challenge_params() const { + if (!this->challenge_params) { + throw runtime_error("challenge params are missing"); + } + return this->challenge_params; +} + +void Lobby::set_drop_mode(DropMode new_mode) { + this->drop_mode = new_mode; + + bool should_have_item_creator = (this->base_version == Version::BB_V4) || + ((new_mode != DropMode::DISABLED) && (new_mode != DropMode::CLIENT)); + if (should_have_item_creator && !this->item_creator) { + this->create_item_creator(); + } else if (!should_have_item_creator && this->item_creator) { + this->item_creator.reset(); + } +} + +void Lobby::create_item_creator() { + auto s = this->require_server_state(); + + shared_ptr rare_item_set; + shared_ptr common_item_set; + switch (this->base_version) { + case Version::PC_PATCH: + case Version::BB_PATCH: + case Version::GC_EP3_NTE: + case Version::GC_EP3: + throw runtime_error("cannot create item creator for this base version"); + case Version::DC_NTE: + case Version::DC_V1_11_2000_PROTOTYPE: + case Version::DC_V1: + // TODO: We should probably have a v1 common item set at some point too + common_item_set = s->common_item_set_v2; + rare_item_set = s->rare_item_sets.at("rare-table-v1"); + break; + case Version::DC_V2: + case Version::PC_NTE: + case Version::PC_V2: + common_item_set = s->common_item_set_v2; + rare_item_set = s->rare_item_sets.at("rare-table-v2"); + break; + case Version::GC_NTE: + case Version::GC_V3: + case Version::XB_V3: + common_item_set = s->common_item_set_v3_v4; + rare_item_set = s->rare_item_sets.at("rare-table-v3"); + break; + case Version::BB_V4: + common_item_set = s->common_item_set_v3_v4; + rare_item_set = s->rare_item_sets.at("rare-table-v4"); + break; + default: + throw logic_error("invalid lobby base version"); + } + this->item_creator = make_shared( + common_item_set, + rare_item_set, + s->armor_random_set, + s->tool_random_set, + s->weapon_random_sets.at(this->difficulty), + s->tekker_adjustment_set, + s->item_parameter_table(this->base_version), + s->item_stack_limits(this->base_version), + this->episode, + (this->mode == GameMode::SOLO) ? GameMode::NORMAL : this->mode, + this->difficulty, + this->effective_section_id(), + this->opt_rand_crypt, + this->quest ? this->quest->battle_rules : nullptr); +} + +void Lobby::change_section_id() { + if (this->item_creator) { + uint8_t new_section_id = this->effective_section_id(); + if (this->item_creator->get_section_id() != new_section_id) { + this->log.info("Changing section ID to %s", name_for_section_id(new_section_id)); + this->item_creator->set_section_id(new_section_id); + for (const auto& c : this->clients) { + if (c && c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { + send_text_message_printf(c, "$C5Section ID changed\nto %s (%hhu)", name_for_section_id(new_section_id), new_section_id); + } + } + } + } +} + +uint8_t Lobby::effective_section_id() const { + if (this->override_section_id != 0xFF) { + return this->override_section_id; + } + if (this->check_flag(Lobby::Flag::USE_CREATOR_SECTION_ID)) { + return this->creator_section_id; + } + auto leader = this->clients.at(this->leader_id); + if (leader) { + return leader->character()->disp.visual.section_id; + } + return 0; +} + +shared_ptr Lobby::load_maps( + Version version, + Episode episode, + uint8_t difficulty, + uint8_t event, + uint32_t lobby_id, + shared_ptr rare_rates, + uint32_t random_seed, + shared_ptr opt_rand_crypt, + shared_ptr quest_dat_contents_decompressed) { + auto map = make_shared(version, lobby_id, random_seed, opt_rand_crypt); + map->add_entities_from_quest_data( + episode, + difficulty, + event, + quest_dat_contents_decompressed->data(), + quest_dat_contents_decompressed->size(), + rare_rates); + return map; +} + +shared_ptr Lobby::load_maps( + Version version, + Episode episode, + GameMode mode, + uint8_t difficulty, + uint8_t event, + uint32_t lobby_id, + shared_ptr sdt, + function(Version, const string&)> get_file_data, + shared_ptr rare_rates, + uint32_t random_seed, + shared_ptr opt_rand_crypt, + const parray& variations, + const PrefixedLogger* log) { + auto enemy_filenames = sdt->map_filenames_for_variations(variations, episode, mode, SetDataTable::FilenameType::ENEMIES); + auto object_filenames = sdt->map_filenames_for_variations(variations, episode, mode, SetDataTable::FilenameType::OBJECTS); + auto event_filenames = sdt->map_filenames_for_variations(variations, episode, mode, SetDataTable::FilenameType::EVENTS); + return Lobby::load_maps( + enemy_filenames, + object_filenames, + event_filenames, + version, + episode, + mode, + difficulty, + event, + lobby_id, + get_file_data, + rare_rates, + random_seed, + opt_rand_crypt, + log); +} + +shared_ptr Lobby::load_maps( + const vector& enemy_filenames, + const vector& object_filenames, + const vector& event_filenames, + Version version, + Episode episode, + GameMode mode, + uint8_t difficulty, + uint8_t event, + uint32_t lobby_id, + function(Version, const string&)> get_file_data, + shared_ptr rare_rates, + uint32_t rare_seed, + shared_ptr opt_rand_crypt, + const PrefixedLogger* log) { + auto map = make_shared(version, lobby_id, rare_seed, opt_rand_crypt); + + // Don't load free-roam maps in Challenge mode, since players can't go to + // Ragol without a quest loaded + if (mode == GameMode::CHALLENGE) { + return map; + } + + for (size_t floor = 0; floor < 0x12; floor++) { + const auto& floor_enemy_filename = enemy_filenames.at(floor); + if (!floor_enemy_filename.empty()) { + auto map_data = get_file_data(version, floor_enemy_filename); + if (map_data) { + map->add_enemies_from_map_data( + episode, + difficulty, + event, + floor, + map_data->data(), + map_data->size(), + rare_rates); + if (log) { + log->info("Loaded enemies map %s for floor %02zX", floor_enemy_filename.c_str(), floor); + } + } else if (log) { + log->info("Enemies map %s for floor %02zX cannot be used; skipping", floor_enemy_filename.c_str(), floor); + } + } else if (log) { + log->info("No enemies to load for floor %02zX", floor); + } + + const auto& floor_object_filename = object_filenames.at(floor); + if (!floor_object_filename.empty()) { + auto map_data = get_file_data(version, floor_object_filename); + if (map_data) { + map->add_objects_from_map_data(floor, map_data->data(), map_data->size()); + if (log) { + log->info("Loaded objects map %s for floor %02zX", floor_object_filename.c_str(), floor); + } + } else if (log) { + log->info("Objects map %s for floor %02zX cannot be used; skipping", floor_object_filename.c_str(), floor); + } + } else if (log) { + log->info("No objects to load for floor %02zX", floor); + } + + const auto& floor_event_filename = event_filenames.at(floor); + if (!floor_event_filename.empty()) { + auto map_data = get_file_data(version, floor_event_filename); + if (map_data) { + map->add_events_from_map_data(floor, map_data->data(), map_data->size()); + if (log) { + log->info("Loaded events map %s for floor %02zX", floor_event_filename.c_str(), floor); + } + } else if (log) { + log->info("Events map %s for floor %02zX cannot be used; skipping", floor_event_filename.c_str(), floor); + } + } else if (log) { + log->info("No events to load for floor %02zX", floor); + } + } + + return map; +} + +void Lobby::load_maps() { + auto rare_rates = ((this->base_version == Version::BB_V4) && this->rare_enemy_rates) + ? this->rare_enemy_rates + : Map::DEFAULT_RARE_ENEMIES; + + if (this->quest) { + auto leader_c = this->clients.at(this->leader_id); + if (!leader_c) { + throw logic_error("lobby leader is missing"); + } + + auto vq = this->quest->version(this->base_version, leader_c->language()); + if (!vq->dat_contents_decompressed) { + throw runtime_error("quest does not have DAT data"); + } + this->map = this->load_maps( + this->base_version, + this->episode, + this->difficulty, + this->event, + this->lobby_id, + rare_rates, + this->random_seed, + this->opt_rand_crypt, + vq->dat_contents_decompressed); + + } else if (this->mode != GameMode::CHALLENGE) { + auto s = this->require_server_state(); + this->map = this->load_maps( + this->base_version, + this->episode, + this->mode, + this->difficulty, + this->event, + this->lobby_id, + s->set_data_table(this->base_version, this->episode, this->mode, this->difficulty), + bind(&ServerState::load_map_file, s.get(), placeholders::_1, placeholders::_2), + rare_rates, + this->random_seed, + this->opt_rand_crypt, + this->variations, + &this->log); + + } else { + this->map = make_shared(this->base_version, this->lobby_id, this->random_seed, this->opt_rand_crypt); + } + + this->log.info("Generated objects list (%zu entries):", this->map->objects.size()); + for (size_t z = 0; z < this->map->objects.size(); z++) { + string o_str = this->map->objects[z].str(); + this->log.info("(K-%zX) %s", z, o_str.c_str()); + } + this->log.info("Generated enemies list (%zu entries):", this->map->enemies.size()); + for (size_t z = 0; z < this->map->enemies.size(); z++) { + string e_str = this->map->enemies[z].str(); + this->log.info("(E-%zX) %s", z, e_str.c_str()); + } + this->log.info("Generated events list (%zu entries):", this->map->events.size()); + for (size_t z = 0; z < this->map->events.size(); z++) { + string e_str = this->map->events[z].str(); + this->log.info("%s", e_str.c_str()); + } + this->log.info("Loaded maps contain %zu object entries and %zu enemy entries overall (%zu as rares)", + this->map->objects.size(), this->map->enemies.size(), this->map->rare_enemy_indexes.size()); +} + +void Lobby::create_ep3_server() { + auto s = this->require_server_state(); + if (!this->ep3_server) { + this->log.info("Creating Episode 3 server state"); + } else { + this->log.info("Recreating Episode 3 server state"); + } + auto tourn = this->tournament_match ? this->tournament_match->tournament.lock() : nullptr; + bool is_nte = this->base_version == Version::GC_EP3_NTE; + Episode3::Server::Options options = { + .card_index = is_nte ? s->ep3_card_index_trial : s->ep3_card_index, + .map_index = s->ep3_map_index, + .behavior_flags = s->ep3_behavior_flags, + .opt_rand_stream = nullptr, + .opt_rand_crypt = this->opt_rand_crypt, + .tournament = tourn, + .trap_card_ids = s->ep3_trap_card_ids, + }; + if (this->base_version == Version::GC_EP3_NTE) { + options.behavior_flags |= Episode3::BehaviorFlag::IS_TRIAL_EDITION; + } else { + options.behavior_flags &= (~Episode3::BehaviorFlag::IS_TRIAL_EDITION); + } + this->ep3_server = make_shared(this->shared_from_this(), std::move(options)); + this->ep3_server->init(); +} + +void Lobby::reassign_leader_on_client_departure(size_t leaving_client_index) { + for (size_t x = 0; x < this->max_clients; x++) { + if (x == leaving_client_index) { + continue; + } + if (this->clients[x]) { + this->leader_id = x; + this->change_section_id(); + return; + } + } + this->leader_id = 0; +} + +bool Lobby::any_client_loading() const { + for (size_t x = 0; x < this->max_clients; x++) { + auto lc = this->clients[x]; + if (!lc.get()) { + continue; + } + if (lc->config.check_flag(Client::Flag::LOADING) || + lc->config.check_flag(Client::Flag::LOADING_QUEST) || + lc->config.check_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST)) { + return true; + } + } + return false; +} + +size_t Lobby::count_clients() const { + size_t ret = 0; + for (size_t x = 0; x < this->max_clients; x++) { + if (this->clients[x]) { + ret++; + } + } + return ret; +} + +bool Lobby::any_v1_clients_present() const { + for (size_t x = 0; x < this->max_clients; x++) { + if (this->clients[x] && is_v1(this->clients[x]->version())) { + return true; + } + } + return false; +} + +void Lobby::add_client(shared_ptr c, ssize_t required_client_id) { + if (!c->login) { + throw runtime_error("client is not logged in"); + } + + ssize_t index; + ssize_t min_client_id = this->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM) ? 4 : 0; + + if (required_client_id >= 0) { + if (this->clients.at(required_client_id).get()) { + throw out_of_range("required slot is in use"); + } + this->clients[required_client_id] = c; + index = required_client_id; + + } else if (c->config.check_flag(Client::Flag::DEBUG_ENABLED) && (this->mode != GameMode::SOLO)) { + for (index = this->max_clients - 1; index >= min_client_id; index--) { + if (!this->clients[index].get()) { + this->clients[index] = c; + break; + } + } + if (index < min_client_id) { + throw out_of_range("no space left in lobby"); + } + } else { + for (index = min_client_id; index < this->max_clients; index++) { + if (!this->clients[index].get()) { + this->clients[index] = c; + break; + } + } + if (index >= this->max_clients) { + throw out_of_range("no space left in lobby"); + } + } + + c->lobby_client_id = index; + c->lobby = this->weak_from_this(); + c->lobby_arrow_color = 0; + + // If there's no one else in the lobby, set the leader id as well + size_t leader_index; + for (leader_index = 0; leader_index < this->max_clients; leader_index++) { + if (this->clients[leader_index] && (this->clients[leader_index] != c)) { + break; + } + } + if (leader_index >= this->max_clients) { + this->leader_id = c->lobby_client_id; + this->change_section_id(); + } + + // If this is a lobby or no one was here before this, reassign all the floor + // item IDs and reset the next item IDs + if (!this->is_game() || (leader_index >= this->max_clients)) { + this->reset_next_item_ids(); + for (auto& m : this->floor_item_managers) { + this->next_game_item_id = m.reassign_all_item_ids(this->next_game_item_id); + } + } + + // If this is not a game or the joining client is the leader, they will assign + // their item IDs BEFORE they process any inbound commands (therefore a 6x6D + // command, which we will send during loading, should reflect the item state + // AFTER their IDs are assigned). If the joining client is not the leader, + // they will not assign their item IDs until they receive a 6x71 command, + // which is sent AFTER the 6x6D command, so the 6x6D should reflect the item + // state BEFORE their IDs are assigned. (In the latter case, we'll assign the + // IDs for real when they send a 6F command, or 6x1F equivalent in the case of + // DC NTE and 11/2000.) + this->assign_inventory_and_bank_item_ids(c, (!this->is_game() || (c->lobby_client_id == this->leader_id))); + + // On BB, we send artificial flag state to fix an Episode 2 bug where the + // CCA door lock state is overwritten by quests. + if (this->is_game() && (c->version() == Version::BB_V4)) { + c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_FLAG_STATE); + } + + // If the lobby is recording a battle record, add the player join event + if (this->battle_record) { + auto p = c->character(); + PlayerLobbyDataDCGC lobby_data; + lobby_data.player_tag = 0x00010000; + lobby_data.guild_card_number = c->login->account->account_id; + lobby_data.name.encode(p->disp.name.decode(c->language()), c->language()); + this->battle_record->add_player( + lobby_data, + p->inventory, + p->disp.to_dcpcv3(c->language(), c->language()), + c->ep3_config ? (c->ep3_config->online_clv_exp / 100) : 0); + } + + // Send spectator count notifications if needed + if (this->is_game() && this->is_ep3()) { + if (this->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM)) { + auto watched_l = this->watched_lobby.lock(); + if (watched_l) { + send_ep3_update_game_metadata(watched_l); + } + } else { + send_ep3_update_game_metadata(this->shared_from_this()); + } + } + + // There is a player in the lobby, so it is no longer idle + if (event_pending(this->idle_timeout_event.get(), EV_TIMEOUT, nullptr)) { + event_del(this->idle_timeout_event.get()); + this->log.info("Idle timeout cancelled"); + } +} + +void Lobby::remove_client(shared_ptr c) { + if (this->clients.at(c->lobby_client_id) != c) { + auto other_c = this->clients[c->lobby_client_id].get(); + throw logic_error(string_printf( + "client\'s lobby client id (%hhu) does not match client list (%u)", + c->lobby_client_id, + static_cast(other_c ? other_c->lobby_client_id : 0xFF))); + } + this->clients[c->lobby_client_id] = nullptr; + + // Unassign the client's lobby if it matches the current lobby (it may not + // match if the client was already added to another lobby - this can happen + // during the lobby change procedure) + { + auto c_lobby = c->lobby.lock(); + if (c_lobby.get() == this) { + c->lobby.reset(); + } + } + + this->reassign_leader_on_client_departure(c->lobby_client_id); + + // If the lobby is recording a battle record, add the player leave event + if (this->battle_record) { + this->battle_record->delete_player(c->lobby_client_id); + } + + // If the lobby is Episode 3, update the appropriate spectator counts + if (this->is_game() && this->is_ep3()) { + if (this->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM)) { + auto watched_l = this->watched_lobby.lock(); + if (watched_l) { + send_ep3_update_game_metadata(watched_l); + } + } else { + send_ep3_update_game_metadata(this->shared_from_this()); + } + } + + // If there are still players left in the lobby, delete all items that only + // the leaving player could see. Don't do this if no one is left in the lobby, + // since that would mean items could not persist in empty lobbies. + uint16_t remaining_clients_mask = 0; + for (size_t z = 0; z < 12; z++) { + if (this->clients[z]) { + remaining_clients_mask |= (1 << z); + } + } + if (remaining_clients_mask) { + for (auto& m : this->floor_item_managers) { + m.clear_inaccessible(remaining_clients_mask); + } + } else { + for (auto& m : this->floor_item_managers) { + m.clear_private(); + } + } + + if (!remaining_clients_mask && + this->check_flag(Flag::PERSISTENT) && + !this->check_flag(Flag::DEFAULT) && + (this->idle_timeout_usecs > 0)) { + // If the lobby is persistent but has an idle timeout, make it expire after + // the specified time + auto tv = usecs_to_timeval(this->idle_timeout_usecs); + event_add(this->idle_timeout_event.get(), &tv); + this->log.info("Idle timeout scheduled"); + } +} + +void Lobby::move_client_to_lobby( + shared_ptr dest_lobby, + shared_ptr c, + ssize_t required_client_id) { + if (dest_lobby.get() == this) { + return; + } + + if (required_client_id >= 0) { + if (dest_lobby->clients.at(required_client_id)) { + throw out_of_range("required slot is in use"); + } + } else { + ssize_t min_client_id = this->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM) ? 4 : 0; + size_t available_slots = dest_lobby->max_clients - min_client_id; + if (dest_lobby->count_clients() >= available_slots) { + throw out_of_range("no space left in lobby"); + } + } + + this->remove_client(c); + dest_lobby->add_client(c, required_client_id); +} + +shared_ptr Lobby::find_client(const string* identifier, uint64_t account_id) { + for (size_t x = 0; x < this->max_clients; x++) { + auto lc = this->clients[x]; + if (!lc) { + continue; + } + if (account_id && lc->login && (lc->login->account->account_id == account_id)) { + return lc; + } + if (identifier && (lc->character()->disp.name.eq(*identifier, lc->language()))) { + return lc; + } + } + + throw out_of_range("client not found"); +} + +Lobby::JoinError Lobby::join_error_for_client(std::shared_ptr c, const std::string* password) const { + if (this->count_clients() >= this->max_clients) { + return JoinError::FULL; + } + bool debug_enabled = c->config.check_flag(Client::Flag::DEBUG_ENABLED); + if (!this->version_is_allowed(c->version()) && !debug_enabled) { + return JoinError::VERSION_CONFLICT; + } + if (this->is_game()) { + if (this->check_flag(Flag::QUEST_SELECTION_IN_PROGRESS)) { + return JoinError::QUEST_SELECTION_IN_PROGRESS; + } + if (this->check_flag(Flag::QUEST_IN_PROGRESS)) { + return JoinError::QUEST_IN_PROGRESS; + } + if (this->check_flag(Flag::BATTLE_IN_PROGRESS)) { + return JoinError::BATTLE_IN_PROGRESS; + } + if (this->mode == GameMode::SOLO) { + return JoinError::SOLO; + } + if (!debug_enabled && + (this->check_flag(Flag::IS_CLIENT_CUSTOMIZATION) != c->config.check_flag(Client::Flag::IS_CLIENT_CUSTOMIZATION))) { + return JoinError::VERSION_CONFLICT; + } + if (!c->login->account->check_flag(Account::Flag::FREE_JOIN_GAMES)) { + if (password && !this->password.empty() && (*password != this->password)) { + return JoinError::INCORRECT_PASSWORD; + } + auto p = c->character(); + if (p->disp.stats.level < this->min_level) { + return JoinError::LEVEL_TOO_LOW; + } + if (p->disp.stats.level > this->max_level) { + return JoinError::LEVEL_TOO_HIGH; + } + if (this->quest) { + size_t num_clients = this->count_clients() + 1; + bool v1_present = is_v1(c->version()) || this->any_v1_clients_present(); + auto this_sh = this->shared_from_this(); + if (!c->can_see_quest(this->quest, this_sh, this->event, this->difficulty, num_clients, v1_present) || + !c->can_play_quest(this->quest, this_sh, this->event, this->difficulty, num_clients, v1_present)) { + return JoinError::NO_ACCESS_TO_QUEST; + } + } + } + // Only prevent joining during loading if the client is actually trying to + // join (not just loading the game list) + if (password && this->any_client_loading()) { + return JoinError::LOADING; + } + } + return JoinError::ALLOWED; +} + +bool Lobby::item_exists(uint8_t floor, uint32_t item_id) const { + if (floor >= this->floor_item_managers.size()) { + return false; + } + return this->floor_item_managers.at(floor).exists(item_id); +} + +shared_ptr Lobby::find_item(uint8_t floor, uint32_t item_id) const { + return this->floor_item_managers.at(floor).find(item_id); +} + +void Lobby::add_item(uint8_t floor, const ItemData& data, float x, float z, uint16_t flags) { + auto& m = this->floor_item_managers.at(floor); + m.add(data, x, z, flags); + this->evict_items_from_floor(floor); +} + +void Lobby::add_item(uint8_t floor, shared_ptr fi) { + auto& m = this->floor_item_managers.at(floor); + m.add(fi); + this->evict_items_from_floor(floor); +} + +void Lobby::evict_items_from_floor(uint8_t floor) { + auto& m = this->floor_item_managers.at(floor); + auto evicted = m.evict(); + if (!evicted.empty()) { + auto l = this->shared_from_this(); + for (const auto& fi : evicted) { + for (size_t z = 0; z < 12; z++) { + auto lc = this->clients[z]; + if (lc && fi->visible_to_client(z)) { + send_destroy_floor_item_to_client(lc, fi->data.id, floor); + } + } + } + } +} + +shared_ptr Lobby::remove_item(uint8_t floor, uint32_t item_id, uint8_t requesting_client_id) { + return this->floor_item_managers.at(floor).remove(item_id, requesting_client_id); +} + +uint32_t Lobby::generate_item_id(uint8_t client_id) { + if (client_id < this->max_clients) { + return this->next_item_id_for_client[client_id]++; + } + return this->next_game_item_id++; +} + +void Lobby::on_item_id_generated_externally(uint32_t item_id) { + // Note: The client checks for the range (0x00010000, 0x02010000) here, but + // server-side item drop logic uses 0x00810000 as its base ID, so we restrict + // the range further here. + if ((item_id > 0x00010000) && (item_id < 0x00810000)) { + uint16_t item_client_id = (item_id >> 21) & 0x7FF; + uint32_t& next_item_id = this->next_item_id_for_client.at(item_client_id); + next_item_id = std::max(next_item_id, item_id + 1); + } +} + +void Lobby::assign_inventory_and_bank_item_ids(shared_ptr c, bool consume_ids) { + auto p = c->character(); + uint32_t orig_next_item_id = this->next_item_id_for_client.at(c->lobby_client_id); + for (size_t z = 0; z < p->inventory.num_items; z++) { + p->inventory.items[z].data.id = this->generate_item_id(c->lobby_client_id); + } + if (!consume_ids) { + this->next_item_id_for_client[c->lobby_client_id] = orig_next_item_id; + } + + if (c->log.info("Assigned inventory item IDs%s", consume_ids ? "" : " but did not mark IDs as used")) { + c->print_inventory(stderr); + if (p->bank.num_items) { + p->bank.assign_ids(0x99000000 + (c->lobby_client_id << 20)); + c->log.info("Assigned bank item IDs"); + c->print_bank(stderr); + } else { + c->log.info("Bank is empty"); + } + } +} + +unordered_map> Lobby::clients_by_account_id() const { + unordered_map> ret; + for (auto c : this->clients) { + if (c && c->login) { + ret.emplace(c->login->account->account_id, c); + } + } + return ret; +} + +QuestIndex::IncludeCondition Lobby::quest_include_condition() const { + size_t num_players = this->count_clients(); + bool v1_present = this->any_v1_clients_present(); + return [this, num_players, v1_present](shared_ptr q) -> QuestIndex::IncludeState { + bool is_enabled = true; + for (const auto& lc : this->clients) { + auto this_sh = this->shared_from_this(); + if (lc && !lc->can_see_quest(q, this_sh, this->event, this->difficulty, num_players, v1_present)) { + return QuestIndex::IncludeState::HIDDEN; + } + if (lc && !lc->can_play_quest(q, this_sh, this->event, this->difficulty, num_players, v1_present)) { + is_enabled = false; + } + } + return is_enabled ? QuestIndex::IncludeState::AVAILABLE : QuestIndex::IncludeState::DISABLED; + }; +} + +void Lobby::dispatch_on_idle_timeout(evutil_socket_t, short, void* ctx) { + auto l = reinterpret_cast(ctx)->shared_from_this(); + if (l->count_clients() == 0) { + l->log.info("Idle timeout expired"); + auto s = l->require_server_state(); + s->remove_lobby(l); + } else { + l->log.error("Idle timeout occurred, but clients are present in lobby"); + event_del(l->idle_timeout_event.get()); + } +} + +bool Lobby::compare_shared(const shared_ptr& a, const shared_ptr& b) { + // Sort keys: + // 1. Priority class: has free space < empty (persistent) < full < non-joinable (in quest/battle) + // 2. Password: public < locked + // 3. Game mode: Normal < Battle < Challenge < Solo + // 4. Episode: 1 < 2 < 4 + // 5. Difficulty: Normal < Hard < Very Hard < Ultimate + // 6. Game name + static auto get_priority = +[](const shared_ptr& l) -> size_t { + if (l->check_flag(Lobby::Flag::QUEST_SELECTION_IN_PROGRESS) || + l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS) || + l->check_flag(Lobby::Flag::BATTLE_IN_PROGRESS)) { + return 4; + } + size_t num_clients = l->count_clients(); + if (num_clients == l->max_clients) { + return 3; + } + if (num_clients == 0) { + return 2; + } + return 1; + }; + size_t a_priority = get_priority(a); + size_t b_priority = get_priority(b); + if (a_priority < b_priority) { + return true; + } else if (a_priority > b_priority) { + return false; + } + + if (a->password.empty() && !b->password.empty()) { + return true; + } else if (!a->password.empty() && b->password.empty()) { + return false; + } + + size_t a_mode = static_cast(a->mode); + size_t b_mode = static_cast(b->mode); + if (a_mode < b_mode) { + return true; + } else if (a_mode > b_mode) { + return false; + } + + size_t a_episode = static_cast(a->episode); + size_t b_episode = static_cast(b->episode); + if (a_episode < b_episode) { + return true; + } else if (a_episode > b_episode) { + return false; + } + + if (a->difficulty < b->difficulty) { + return true; + } else if (a->difficulty > b->difficulty) { + return false; + } + + return a->name < b->name; +} + +template <> +Lobby::DropMode enum_for_name(const char* name) { + if (!strcmp(name, "DISABLED")) { + return Lobby::DropMode::DISABLED; + } else if (!strcmp(name, "CLIENT")) { + return Lobby::DropMode::CLIENT; + } else if (!strcmp(name, "SERVER_SHARED")) { + return Lobby::DropMode::SERVER_SHARED; + } else if (!strcmp(name, "SERVER_PRIVATE")) { + return Lobby::DropMode::SERVER_PRIVATE; + } else if (!strcmp(name, "SERVER_DUPLICATE")) { + return Lobby::DropMode::SERVER_DUPLICATE; + } else { + throw runtime_error("invalid drop mode"); + } +} + +template <> +const char* name_for_enum(Lobby::DropMode value) { + switch (value) { + case Lobby::DropMode::DISABLED: + return "DISABLED"; + case Lobby::DropMode::CLIENT: + return "CLIENT"; + case Lobby::DropMode::SERVER_SHARED: + return "SERVER_SHARED"; + case Lobby::DropMode::SERVER_PRIVATE: + return "SERVER_PRIVATE"; + case Lobby::DropMode::SERVER_DUPLICATE: + return "SERVER_DUPLICATE"; + default: + throw runtime_error("invalid drop mode"); + } +} diff --git a/src/Lobby.hh b/src/Lobby.hh index c7f3dc02..2d3c0e76 100644 --- a/src/Lobby.hh +++ b/src/Lobby.hh @@ -1,322 +1,322 @@ -#pragma once - -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "Client.hh" -#include "CommandFormats.hh" -#include "Episode3/BattleRecord.hh" -#include "Episode3/Server.hh" -#include "ItemCreator.hh" -#include "Map.hh" -#include "Quest.hh" -#include "StaticGameData.hh" -#include "Text.hh" - -struct ServerState; - -struct Lobby : public std::enable_shared_from_this { - struct FloorItem { - ItemData data; - float x; - float z; - uint64_t drop_number; - // The low 12 bits of flags are visibility flags, specifying which clients - // can see the item. (In practice, only the lowest 4 of these bits are used, - // but the game has fields for 12 players so we do too.) - // The 13th bit (0x1000) specifies whether a rare item notification should - // be sent to all players when the item is picked up. This has no effect for - // non-rare items. - uint16_t flags; - - bool visible_to_client(uint8_t client_id) const; - }; - struct FloorItemManager { - PrefixedLogger log; - uint64_t next_drop_number; - // It's important that this is a map and not an unordered_map. See the - // comment in send_game_item_state for more details. - std::map> items; // Keyed on item_id - std::array>, 12> queue_for_client; - - FloorItemManager(uint32_t lobby_id, uint8_t floor); - ~FloorItemManager() = default; - - bool exists(uint32_t item_id) const; - std::shared_ptr find(uint32_t item_id) const; - void add(const ItemData& item, float x, float z, uint16_t flags); - void add(std::shared_ptr fi); - std::shared_ptr remove(uint32_t item_id, uint8_t client_id); - std::unordered_set> evict(); - void clear_inaccessible(uint16_t remaining_clients_mask); - void clear_private(); - void clear(); - uint32_t reassign_all_item_ids(uint32_t next_item_id); - }; - enum class Flag { - // clang-format off - GAME = 0x00000001, - PERSISTENT = 0x00000002, - // Flags used only for games - CHEATS_ENABLED = 0x00000100, - QUEST_SELECTION_IN_PROGRESS = 0x00000200, - QUEST_IN_PROGRESS = 0x00000400, - BATTLE_IN_PROGRESS = 0x00000800, - JOINABLE_QUEST_IN_PROGRESS = 0x00001000, - IS_CLIENT_CUSTOMIZATION = 0x00002000, - IS_SPECTATOR_TEAM = 0x00004000, // .episode must be EP3 also - SPECTATORS_FORBIDDEN = 0x00008000, - START_BATTLE_PLAYER_IMMEDIATELY = 0x00010000, - CANNOT_CHANGE_CHEAT_MODE = 0x00020000, - USE_CREATOR_SECTION_ID = 0x00040000, - // Flags used only for lobbies - PUBLIC = 0x01000000, - DEFAULT = 0x02000000, - IS_OVERFLOW = 0x08000000, - // clang-format on - }; - enum class DropMode { - DISABLED = 0, - CLIENT = 1, // Not allowed for BB games - SERVER_SHARED = 2, - SERVER_PRIVATE = 3, - SERVER_DUPLICATE = 4, - }; - - std::weak_ptr - server_state; - PrefixedLogger log; - - uint32_t lobby_id; - - uint32_t min_level; - uint32_t max_level; - - // Game state - std::array next_item_id_for_client; - uint32_t next_game_item_id; - std::vector floor_item_managers; - std::shared_ptr rare_enemy_rates; - std::shared_ptr map; - parray variations; - std::unique_ptr quest_flags_known; // If null, ALL quest flags are known - std::unique_ptr quest_flag_values; - std::unique_ptr switch_flags; - - // Game config - Version base_version; - // Bits in allowed_versions specify who is allowed to join this game. The - // bits are indexed as (1 << version), where version is a value from the - // Version enum. - uint16_t allowed_versions; - uint8_t creator_section_id; - uint8_t override_section_id; - Episode episode; - GameMode mode; - uint8_t difficulty; // 0-3 - uint16_t base_exp_multiplier; - float exp_share_multiplier; - float challenge_exp_multiplier; - std::string password; - std::string name; - // This seed is also sent to the client for rare enemy generation - uint32_t random_seed; - std::shared_ptr opt_rand_crypt; - uint8_t allowed_drop_modes; - DropMode drop_mode; - std::shared_ptr item_creator; - - struct ChallengeParameters { - uint8_t stage_number = 0; - uint32_t rank_color = 0xFFFFFFFF; - std::string rank_text; - struct RankThreshold { - uint32_t bitmask = 0; - uint32_t seconds = 0; - }; - std::array rank_thresholds; - }; - std::shared_ptr challenge_params; - - // Ep3 stuff - // There are three kinds of Episode 3 games. All of these types have episode - // set to EP3; types 2 and 3 additionally have the IS_SPECTATOR_TEAM flag. - // 1. Primary games. These are the lobbies where battles may take place. - // 2. Watcher games. These lobbies receive all the battle and chat commands - // from a primary game. (This the implementation of spectator teams.) - // 3. Replay games. These lobbies replay a sequence of battle commands and - // chat commands from a previous primary game. - // Types 2 and 3 may be distinguished by the presence of the battle_record - // field - in replay games, it will be present; in watcher games it will be - // absent. - std::shared_ptr ep3_server; // Only used in primary games - std::weak_ptr watched_lobby; // Only used in watcher games - std::unordered_set> watcher_lobbies; // Only used in primary games - std::shared_ptr battle_record; // Not used in watcher games - std::shared_ptr battle_player; // Only used in replay games - std::shared_ptr tournament_match; - std::shared_ptr ep3_ex_result_values; - - // Lobby stuff - uint8_t event; - uint8_t block; - uint8_t leader_id; - uint8_t max_clients; - uint32_t enabled_flags; - std::shared_ptr quest; - std::array, 12> clients; - // Keys in this map are client_id - std::unordered_map> clients_to_add; - - // This is only used when the PERSISTENT flag is set and idle_timeout_usecs - // is not zero - uint64_t idle_timeout_usecs; - std::unique_ptr idle_timeout_event; - - Lobby(std::shared_ptr s, uint32_t id, bool is_game); - Lobby(const Lobby&) = delete; - Lobby(Lobby&&) = delete; - ~Lobby(); - Lobby& operator=(const Lobby&) = delete; - Lobby& operator=(Lobby&&) = delete; - - void reset_next_item_ids(); - - [[nodiscard]] inline bool check_flag(Flag flag) const { - return !!(this->enabled_flags & static_cast(flag)); - } - inline void set_flag(Flag flag) { - this->enabled_flags |= static_cast(flag); - } - inline void clear_flag(Flag flag) { - this->enabled_flags &= (~static_cast(flag)); - } - inline void toggle_flag(Flag flag) { - this->enabled_flags ^= static_cast(flag); - } - - std::shared_ptr require_server_state() const; - std::shared_ptr require_challenge_params() const; - void set_drop_mode(DropMode new_mode); - void create_item_creator(); - void change_section_id(); - uint8_t effective_section_id() const; - static std::shared_ptr load_maps( - Version version, - Episode episode, - uint8_t difficulty, - uint8_t event, - uint32_t lobby_id, - std::shared_ptr rare_rates, - uint32_t random_seed, - std::shared_ptr opt_rand_crypt, - std::shared_ptr quest_dat_contents_decompressed); - static std::shared_ptr load_maps( - Version version, - Episode episode, - GameMode mode, - uint8_t difficulty, - uint8_t event, - uint32_t lobby_id, - std::shared_ptr sdt, - std::function(Version, const std::string&)> get_file_data, - std::shared_ptr rare_rates, - uint32_t random_seed, - std::shared_ptr opt_rand_crypt, - const parray& variations, - const PrefixedLogger* log = nullptr); - static std::shared_ptr load_maps( - const std::vector& enemy_filenames, - const std::vector& object_filenames, - const std::vector& event_filenames, - Version version, - Episode episode, - GameMode mode, - uint8_t difficulty, - uint8_t event, - uint32_t lobby_id, - std::function(Version, const std::string&)> get_file_data, - std::shared_ptr rare_rates, - uint32_t random_seed, - std::shared_ptr opt_rand_crypt, - const PrefixedLogger* log = nullptr); - void load_maps(); - void create_ep3_server(); - - [[nodiscard]] inline bool is_game() const { - return this->check_flag(Flag::GAME); - } - [[nodiscard]] inline bool is_ep3() const { - return this->episode == Episode::EP3; - } - - [[nodiscard]] inline bool version_is_allowed(Version v) const { - return this->allowed_versions & (1 << static_cast(v)); - } - inline void allow_version(Version v) { - this->allowed_versions |= (1 << static_cast(v)); - } - - void reassign_leader_on_client_departure(size_t leaving_client_id); - size_t count_clients() const; - bool any_v1_clients_present() const; - bool any_client_loading() const; - - void add_client(std::shared_ptr c, ssize_t required_client_id = -1); - void remove_client(std::shared_ptr c); - - void move_client_to_lobby( - std::shared_ptr dest_lobby, - std::shared_ptr c, - ssize_t required_client_id = -1); - - std::shared_ptr find_client(const std::string* identifier = nullptr, uint64_t account_id = 0); - - enum class JoinError { - ALLOWED = 0, - FULL, - VERSION_CONFLICT, - QUEST_SELECTION_IN_PROGRESS, - QUEST_IN_PROGRESS, - BATTLE_IN_PROGRESS, - LOADING, - SOLO, - INCORRECT_PASSWORD, - LEVEL_TOO_LOW, - LEVEL_TOO_HIGH, - NO_ACCESS_TO_QUEST, - }; - JoinError join_error_for_client(std::shared_ptr c, const std::string* password) const; - - bool item_exists(uint8_t floor, uint32_t item_id) const; - std::shared_ptr find_item(uint8_t floor, uint32_t item_id) const; - void add_item(uint8_t floor, const ItemData& item, float x, float z, uint16_t flags); - void add_item(uint8_t floor, std::shared_ptr); - void evict_items_from_floor(uint8_t floor); - std::shared_ptr remove_item(uint8_t floor, uint32_t item_id, uint8_t requesting_client_id); - - uint32_t generate_item_id(uint8_t client_id); - void on_item_id_generated_externally(uint32_t item_id); - void assign_inventory_and_bank_item_ids(std::shared_ptr c, bool consume_ids); - - QuestIndex::IncludeCondition quest_include_condition() const; - - std::unordered_map> clients_by_account_id() const; - - static void dispatch_on_idle_timeout(evutil_socket_t, short, void* ctx); - - static bool compare_shared(const std::shared_ptr& a, const std::shared_ptr& b); -}; - -template <> -Lobby::DropMode enum_for_name(const char* name); -template <> -const char* name_for_enum(Lobby::DropMode value); +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "Client.hh" +#include "CommandFormats.hh" +#include "Episode3/BattleRecord.hh" +#include "Episode3/Server.hh" +#include "ItemCreator.hh" +#include "Map.hh" +#include "Quest.hh" +#include "StaticGameData.hh" +#include "Text.hh" + +struct ServerState; + +struct Lobby : public std::enable_shared_from_this { + struct FloorItem { + ItemData data; + float x; + float z; + uint64_t drop_number; + // The low 12 bits of flags are visibility flags, specifying which clients + // can see the item. (In practice, only the lowest 4 of these bits are used, + // but the game has fields for 12 players so we do too.) + // The 13th bit (0x1000) specifies whether a rare item notification should + // be sent to all players when the item is picked up. This has no effect for + // non-rare items. + uint16_t flags; + + bool visible_to_client(uint8_t client_id) const; + }; + struct FloorItemManager { + PrefixedLogger log; + uint64_t next_drop_number; + // It's important that this is a map and not an unordered_map. See the + // comment in send_game_item_state for more details. + std::map> items; // Keyed on item_id + std::array>, 12> queue_for_client; + + FloorItemManager(uint32_t lobby_id, uint8_t floor); + ~FloorItemManager() = default; + + bool exists(uint32_t item_id) const; + std::shared_ptr find(uint32_t item_id) const; + void add(const ItemData& item, float x, float z, uint16_t flags); + void add(std::shared_ptr fi); + std::shared_ptr remove(uint32_t item_id, uint8_t client_id); + std::unordered_set> evict(); + void clear_inaccessible(uint16_t remaining_clients_mask); + void clear_private(); + void clear(); + uint32_t reassign_all_item_ids(uint32_t next_item_id); + }; + enum class Flag { + // clang-format off + GAME = 0x00000001, + PERSISTENT = 0x00000002, + // Flags used only for games + CHEATS_ENABLED = 0x00000100, + QUEST_SELECTION_IN_PROGRESS = 0x00000200, + QUEST_IN_PROGRESS = 0x00000400, + BATTLE_IN_PROGRESS = 0x00000800, + JOINABLE_QUEST_IN_PROGRESS = 0x00001000, + IS_CLIENT_CUSTOMIZATION = 0x00002000, + IS_SPECTATOR_TEAM = 0x00004000, // .episode must be EP3 also + SPECTATORS_FORBIDDEN = 0x00008000, + START_BATTLE_PLAYER_IMMEDIATELY = 0x00010000, + CANNOT_CHANGE_CHEAT_MODE = 0x00020000, + USE_CREATOR_SECTION_ID = 0x00040000, + // Flags used only for lobbies + PUBLIC = 0x01000000, + DEFAULT = 0x02000000, + IS_OVERFLOW = 0x08000000, + // clang-format on + }; + enum class DropMode { + DISABLED = 0, + CLIENT = 1, // Not allowed for BB games + SERVER_SHARED = 2, + SERVER_PRIVATE = 3, + SERVER_DUPLICATE = 4, + }; + + std::weak_ptr + server_state; + PrefixedLogger log; + + uint32_t lobby_id; + + uint32_t min_level; + uint32_t max_level; + + // Game state + std::array next_item_id_for_client; + uint32_t next_game_item_id; + std::vector floor_item_managers; + std::shared_ptr rare_enemy_rates; + std::shared_ptr map; + parray variations; + std::unique_ptr quest_flags_known; // If null, ALL quest flags are known + std::unique_ptr quest_flag_values; + std::unique_ptr switch_flags; + + // Game config + Version base_version; + // Bits in allowed_versions specify who is allowed to join this game. The + // bits are indexed as (1 << version), where version is a value from the + // Version enum. + uint16_t allowed_versions; + uint8_t creator_section_id; + uint8_t override_section_id; + Episode episode; + GameMode mode; + uint8_t difficulty; // 0-3 + uint16_t base_exp_multiplier; + float exp_share_multiplier; + float challenge_exp_multiplier; + std::string password; + std::string name; + // This seed is also sent to the client for rare enemy generation + uint32_t random_seed; + std::shared_ptr opt_rand_crypt; + uint8_t allowed_drop_modes; + DropMode drop_mode; + std::shared_ptr item_creator; + + struct ChallengeParameters { + uint8_t stage_number = 0; + uint32_t rank_color = 0xFFFFFFFF; + std::string rank_text; + struct RankThreshold { + uint32_t bitmask = 0; + uint32_t seconds = 0; + }; + std::array rank_thresholds; + }; + std::shared_ptr challenge_params; + + // Ep3 stuff + // There are three kinds of Episode 3 games. All of these types have episode + // set to EP3; types 2 and 3 additionally have the IS_SPECTATOR_TEAM flag. + // 1. Primary games. These are the lobbies where battles may take place. + // 2. Watcher games. These lobbies receive all the battle and chat commands + // from a primary game. (This the implementation of spectator teams.) + // 3. Replay games. These lobbies replay a sequence of battle commands and + // chat commands from a previous primary game. + // Types 2 and 3 may be distinguished by the presence of the battle_record + // field - in replay games, it will be present; in watcher games it will be + // absent. + std::shared_ptr ep3_server; // Only used in primary games + std::weak_ptr watched_lobby; // Only used in watcher games + std::unordered_set> watcher_lobbies; // Only used in primary games + std::shared_ptr battle_record; // Not used in watcher games + std::shared_ptr battle_player; // Only used in replay games + std::shared_ptr tournament_match; + std::shared_ptr ep3_ex_result_values; + + // Lobby stuff + uint8_t event; + uint8_t block; + uint8_t leader_id; + uint8_t max_clients; + uint32_t enabled_flags; + std::shared_ptr quest; + std::array, 12> clients; + // Keys in this map are client_id + std::unordered_map> clients_to_add; + + // This is only used when the PERSISTENT flag is set and idle_timeout_usecs + // is not zero + uint64_t idle_timeout_usecs; + std::unique_ptr idle_timeout_event; + + Lobby(std::shared_ptr s, uint32_t id, bool is_game); + Lobby(const Lobby&) = delete; + Lobby(Lobby&&) = delete; + ~Lobby(); + Lobby& operator=(const Lobby&) = delete; + Lobby& operator=(Lobby&&) = delete; + + void reset_next_item_ids(); + + [[nodiscard]] inline bool check_flag(Flag flag) const { + return !!(this->enabled_flags & static_cast(flag)); + } + inline void set_flag(Flag flag) { + this->enabled_flags |= static_cast(flag); + } + inline void clear_flag(Flag flag) { + this->enabled_flags &= (~static_cast(flag)); + } + inline void toggle_flag(Flag flag) { + this->enabled_flags ^= static_cast(flag); + } + + std::shared_ptr require_server_state() const; + std::shared_ptr require_challenge_params() const; + void set_drop_mode(DropMode new_mode); + void create_item_creator(); + void change_section_id(); + uint8_t effective_section_id() const; + static std::shared_ptr load_maps( + Version version, + Episode episode, + uint8_t difficulty, + uint8_t event, + uint32_t lobby_id, + std::shared_ptr rare_rates, + uint32_t random_seed, + std::shared_ptr opt_rand_crypt, + std::shared_ptr quest_dat_contents_decompressed); + static std::shared_ptr load_maps( + Version version, + Episode episode, + GameMode mode, + uint8_t difficulty, + uint8_t event, + uint32_t lobby_id, + std::shared_ptr sdt, + std::function(Version, const std::string&)> get_file_data, + std::shared_ptr rare_rates, + uint32_t random_seed, + std::shared_ptr opt_rand_crypt, + const parray& variations, + const PrefixedLogger* log = nullptr); + static std::shared_ptr load_maps( + const std::vector& enemy_filenames, + const std::vector& object_filenames, + const std::vector& event_filenames, + Version version, + Episode episode, + GameMode mode, + uint8_t difficulty, + uint8_t event, + uint32_t lobby_id, + std::function(Version, const std::string&)> get_file_data, + std::shared_ptr rare_rates, + uint32_t random_seed, + std::shared_ptr opt_rand_crypt, + const PrefixedLogger* log = nullptr); + void load_maps(); + void create_ep3_server(); + + [[nodiscard]] inline bool is_game() const { + return this->check_flag(Flag::GAME); + } + [[nodiscard]] inline bool is_ep3() const { + return this->episode == Episode::EP3; + } + + [[nodiscard]] inline bool version_is_allowed(Version v) const { + return this->allowed_versions & (1 << static_cast(v)); + } + inline void allow_version(Version v) { + this->allowed_versions |= (1 << static_cast(v)); + } + + void reassign_leader_on_client_departure(size_t leaving_client_id); + size_t count_clients() const; + bool any_v1_clients_present() const; + bool any_client_loading() const; + + void add_client(std::shared_ptr c, ssize_t required_client_id = -1); + void remove_client(std::shared_ptr c); + + void move_client_to_lobby( + std::shared_ptr dest_lobby, + std::shared_ptr c, + ssize_t required_client_id = -1); + + std::shared_ptr find_client(const std::string* identifier = nullptr, uint64_t account_id = 0); + + enum class JoinError { + ALLOWED = 0, + FULL, + VERSION_CONFLICT, + QUEST_SELECTION_IN_PROGRESS, + QUEST_IN_PROGRESS, + BATTLE_IN_PROGRESS, + LOADING, + SOLO, + INCORRECT_PASSWORD, + LEVEL_TOO_LOW, + LEVEL_TOO_HIGH, + NO_ACCESS_TO_QUEST, + }; + JoinError join_error_for_client(std::shared_ptr c, const std::string* password) const; + + bool item_exists(uint8_t floor, uint32_t item_id) const; + std::shared_ptr find_item(uint8_t floor, uint32_t item_id) const; + void add_item(uint8_t floor, const ItemData& item, float x, float z, uint16_t flags); + void add_item(uint8_t floor, std::shared_ptr); + void evict_items_from_floor(uint8_t floor); + std::shared_ptr remove_item(uint8_t floor, uint32_t item_id, uint8_t requesting_client_id); + + uint32_t generate_item_id(uint8_t client_id); + void on_item_id_generated_externally(uint32_t item_id); + void assign_inventory_and_bank_item_ids(std::shared_ptr c, bool consume_ids); + + QuestIndex::IncludeCondition quest_include_condition() const; + + std::unordered_map> clients_by_account_id() const; + + static void dispatch_on_idle_timeout(evutil_socket_t, short, void* ctx); + + static bool compare_shared(const std::shared_ptr& a, const std::shared_ptr& b); +}; + +template <> +Lobby::DropMode enum_for_name(const char* name); +template <> +const char* name_for_enum(Lobby::DropMode value); diff --git a/src/Loggers.cc b/src/Loggers.cc index f4f8fb61..f492f8bc 100644 --- a/src/Loggers.cc +++ b/src/Loggers.cc @@ -1,48 +1,48 @@ -#include "Loggers.hh" - -#include - -using namespace std; - -PrefixedLogger ax_messages_log("[$ax message] ", LogLevel::USE_DEFAULT); -PrefixedLogger channel_exceptions_log("[Channel] ", LogLevel::USE_DEFAULT); -PrefixedLogger client_log("", LogLevel::USE_DEFAULT); -PrefixedLogger command_data_log("[Commands] ", LogLevel::USE_DEFAULT); -PrefixedLogger config_log("[Config] ", LogLevel::USE_DEFAULT); -PrefixedLogger dns_server_log("[DNSServer] ", LogLevel::USE_DEFAULT); -PrefixedLogger function_compiler_log("[FunctionCompiler] ", LogLevel::USE_DEFAULT); -PrefixedLogger ip_stack_simulator_log("[IPStackSimulator] ", LogLevel::USE_DEFAULT); -PrefixedLogger lobby_log("", LogLevel::USE_DEFAULT); -PrefixedLogger patch_index_log("[PatchFileIndex] ", LogLevel::USE_DEFAULT); -PrefixedLogger player_data_log("", LogLevel::USE_DEFAULT); -PrefixedLogger proxy_server_log("[ProxyServer] ", LogLevel::USE_DEFAULT); -PrefixedLogger replay_log("[ReplaySession] ", LogLevel::USE_DEFAULT); -PrefixedLogger server_log("[Server] ", LogLevel::USE_DEFAULT); -PrefixedLogger static_game_data_log("[StaticGameData] ", LogLevel::USE_DEFAULT); - -static void set_log_level_from_json( - PrefixedLogger& log, const JSON& d, const char* json_key) { - try { - string name = toupper(d.at(json_key).as_string()); - log.min_level = enum_for_name(name.c_str()); - } catch (const out_of_range&) { - } -} - -void set_log_levels_from_json(const JSON& json) { - set_log_level_from_json(ax_messages_log, json, "AXMessages"); - set_log_level_from_json(channel_exceptions_log, json, "ChannelExceptions"); - set_log_level_from_json(client_log, json, "Clients"); - set_log_level_from_json(command_data_log, json, "CommandData"); - set_log_level_from_json(config_log, json, "Config"); - set_log_level_from_json(dns_server_log, json, "DNSServer"); - set_log_level_from_json(function_compiler_log, json, "FunctionCompiler"); - set_log_level_from_json(ip_stack_simulator_log, json, "IPStackSimulator"); - set_log_level_from_json(lobby_log, json, "Lobbies"); - set_log_level_from_json(patch_index_log, json, "PatchFileIndex"); - set_log_level_from_json(player_data_log, json, "PlayerData"); - set_log_level_from_json(proxy_server_log, json, "ProxyServer"); - set_log_level_from_json(replay_log, json, "Replay"); - set_log_level_from_json(server_log, json, "GameServer"); - set_log_level_from_json(static_game_data_log, json, "StaticGameData"); -} +#include "Loggers.hh" + +#include + +using namespace std; + +PrefixedLogger ax_messages_log("[$ax message] ", LogLevel::USE_DEFAULT); +PrefixedLogger channel_exceptions_log("[Channel] ", LogLevel::USE_DEFAULT); +PrefixedLogger client_log("", LogLevel::USE_DEFAULT); +PrefixedLogger command_data_log("[Commands] ", LogLevel::USE_DEFAULT); +PrefixedLogger config_log("[Config] ", LogLevel::USE_DEFAULT); +PrefixedLogger dns_server_log("[DNSServer] ", LogLevel::USE_DEFAULT); +PrefixedLogger function_compiler_log("[FunctionCompiler] ", LogLevel::USE_DEFAULT); +PrefixedLogger ip_stack_simulator_log("[IPStackSimulator] ", LogLevel::USE_DEFAULT); +PrefixedLogger lobby_log("", LogLevel::USE_DEFAULT); +PrefixedLogger patch_index_log("[PatchFileIndex] ", LogLevel::USE_DEFAULT); +PrefixedLogger player_data_log("", LogLevel::USE_DEFAULT); +PrefixedLogger proxy_server_log("[ProxyServer] ", LogLevel::USE_DEFAULT); +PrefixedLogger replay_log("[ReplaySession] ", LogLevel::USE_DEFAULT); +PrefixedLogger server_log("[Server] ", LogLevel::USE_DEFAULT); +PrefixedLogger static_game_data_log("[StaticGameData] ", LogLevel::USE_DEFAULT); + +static void set_log_level_from_json( + PrefixedLogger& log, const JSON& d, const char* json_key) { + try { + string name = toupper(d.at(json_key).as_string()); + log.min_level = enum_for_name(name.c_str()); + } catch (const out_of_range&) { + } +} + +void set_log_levels_from_json(const JSON& json) { + set_log_level_from_json(ax_messages_log, json, "AXMessages"); + set_log_level_from_json(channel_exceptions_log, json, "ChannelExceptions"); + set_log_level_from_json(client_log, json, "Clients"); + set_log_level_from_json(command_data_log, json, "CommandData"); + set_log_level_from_json(config_log, json, "Config"); + set_log_level_from_json(dns_server_log, json, "DNSServer"); + set_log_level_from_json(function_compiler_log, json, "FunctionCompiler"); + set_log_level_from_json(ip_stack_simulator_log, json, "IPStackSimulator"); + set_log_level_from_json(lobby_log, json, "Lobbies"); + set_log_level_from_json(patch_index_log, json, "PatchFileIndex"); + set_log_level_from_json(player_data_log, json, "PlayerData"); + set_log_level_from_json(proxy_server_log, json, "ProxyServer"); + set_log_level_from_json(replay_log, json, "Replay"); + set_log_level_from_json(server_log, json, "GameServer"); + set_log_level_from_json(static_game_data_log, json, "StaticGameData"); +} diff --git a/src/Loggers.hh b/src/Loggers.hh index 825542f5..7f8796b3 100644 --- a/src/Loggers.hh +++ b/src/Loggers.hh @@ -1,22 +1,22 @@ -#pragma once - -#include -#include - -extern PrefixedLogger ax_messages_log; -extern PrefixedLogger channel_exceptions_log; -extern PrefixedLogger client_log; -extern PrefixedLogger command_data_log; -extern PrefixedLogger config_log; -extern PrefixedLogger dns_server_log; -extern PrefixedLogger function_compiler_log; -extern PrefixedLogger ip_stack_simulator_log; -extern PrefixedLogger lobby_log; -extern PrefixedLogger patch_index_log; -extern PrefixedLogger player_data_log; -extern PrefixedLogger proxy_server_log; -extern PrefixedLogger replay_log; -extern PrefixedLogger server_log; -extern PrefixedLogger static_game_data_log; - -void set_log_levels_from_json(const JSON& json); +#pragma once + +#include +#include + +extern PrefixedLogger ax_messages_log; +extern PrefixedLogger channel_exceptions_log; +extern PrefixedLogger client_log; +extern PrefixedLogger command_data_log; +extern PrefixedLogger config_log; +extern PrefixedLogger dns_server_log; +extern PrefixedLogger function_compiler_log; +extern PrefixedLogger ip_stack_simulator_log; +extern PrefixedLogger lobby_log; +extern PrefixedLogger patch_index_log; +extern PrefixedLogger player_data_log; +extern PrefixedLogger proxy_server_log; +extern PrefixedLogger replay_log; +extern PrefixedLogger server_log; +extern PrefixedLogger static_game_data_log; + +void set_log_levels_from_json(const JSON& json); diff --git a/src/Main.cc b/src/Main.cc index 57ae8031..ad2e1d1c 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -1,2820 +1,2820 @@ -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef HAVE_RESOURCE_FILE -#include "AddressTranslator.hh" -#else -#include "AddressTranslator-Stub.hh" -#endif -#include "BMLArchive.hh" -#include "CatSession.hh" -#include "Compression.hh" -#include "DCSerialNumbers.hh" -#include "DNSServer.hh" -#include "GSLArchive.hh" -#include "GVMEncoder.hh" -#include "HTTPServer.hh" -#include "IPStackSimulator.hh" -#include "Loggers.hh" -#include "NetworkAddresses.hh" -#include "PSOGCObjectGraph.hh" -#include "PSOProtocol.hh" -#include "PatchServer.hh" -#include "ProxyServer.hh" -#include "Quest.hh" -#include "QuestScript.hh" -#include "ReplaySession.hh" -#include "Revision.hh" -#include "SaveFileFormats.hh" -#include "SendCommands.hh" -#include "Server.hh" -#include "ServerShell.hh" -#include "ServerState.hh" -#include "StaticGameData.hh" -#include "Text.hh" -#include "TextIndex.hh" - -using namespace std; - -bool use_terminal_colors = false; - -void print_version_info(); -void print_usage(); - -std::string get_config_filename(Arguments& args) { - string config_filename = args.get("config"); - return config_filename.empty() ? "system/config.json" : config_filename; -} - -template -vector parse_int_vector(const JSON& o) { - vector ret; - for (const auto& x : o.as_list()) { - ret.emplace_back(x->as_int()); - } - return ret; -} - -void drop_privileges(const string& username) { - if ((getuid() != 0) || (getgid() != 0)) { - throw runtime_error(string_printf( - "newserv was not started as root; can\'t switch to user %s", - username.c_str())); - } - - struct passwd* pw = getpwnam(username.c_str()); - if (!pw) { - string error = string_for_error(errno); - throw runtime_error(string_printf("user %s not found (%s)", - username.c_str(), error.c_str())); - } - - if (setgid(pw->pw_gid) != 0) { - string error = string_for_error(errno); - throw runtime_error(string_printf("can\'t switch to group %d (%s)", - pw->pw_gid, error.c_str())); - } - if (setuid(pw->pw_uid) != 0) { - string error = string_for_error(errno); - throw runtime_error(string_printf("can\'t switch to user %d (%s)", - pw->pw_uid, error.c_str())); - } - config_log.info("Switched to user %s (%d:%d)", username.c_str(), pw->pw_uid, pw->pw_gid); -} - -Version get_cli_version(Arguments& args, Version default_value = Version::UNKNOWN) { - if (args.get("pc-patch")) { - return Version::PC_PATCH; - } else if (args.get("bb-patch")) { - return Version::BB_PATCH; - } else if (args.get("dc-nte")) { - return Version::DC_NTE; - } else if (args.get("dc-proto") || args.get("dc-11-2000")) { - return Version::DC_V1_11_2000_PROTOTYPE; - } else if (args.get("dc-v1")) { - return Version::DC_V1; - } else if (args.get("dc-v2") || args.get("dc")) { - return Version::DC_V2; - } else if (args.get("pc-nte")) { - return Version::PC_NTE; - } else if (args.get("pc") || args.get("pc-v2")) { - return Version::PC_V2; - } else if (args.get("gc-nte")) { - return Version::GC_NTE; - } else if (args.get("gc") || args.get("gc-v3")) { - return Version::GC_V3; - } else if (args.get("xb") || args.get("xb=v3")) { - return Version::XB_V3; - } else if (args.get("ep3-nte") || args.get("gc-ep3-nte")) { - return Version::GC_EP3_NTE; - } else if (args.get("ep3") || args.get("gc-ep3")) { - return Version::GC_EP3; - } else if (args.get("bb") || args.get("bb-v4")) { - return Version::BB_V4; - } else if (default_value != Version::UNKNOWN) { - return default_value; - } else { - throw runtime_error("a version option is required"); - } -} - -Episode get_cli_episode(Arguments& args) { - if (args.get("ep1")) { - return Episode::EP1; - } else if (args.get("ep2")) { - return Episode::EP2; - } else if (args.get("ep3")) { - return Episode::EP3; - } else if (args.get("ep4")) { - return Episode::EP4; - } else { - throw runtime_error("an episode option is required"); - } -} - -GameMode get_cli_game_mode(Arguments& args) { - if (args.get("battle")) { - return GameMode::BATTLE; - } else if (args.get("challenge")) { - return GameMode::CHALLENGE; - } else if (args.get("solo")) { - return GameMode::SOLO; - } else { - return GameMode::NORMAL; - } -} - -uint8_t get_cli_difficulty(Arguments& args) { - if (args.get("hard")) { - return 1; - } else if (args.get("very-hard")) { - return 2; - } else if (args.get("ultimate")) { - return 3; - } else { - return 0; - } -} - -string read_input_data(Arguments& args) { - const string& input_filename = args.get(1, false); - - string data; - if (!input_filename.empty() && (input_filename != "-")) { - data = load_file(input_filename); - } else { - data = read_all(stdin); - } - if (args.get("parse-data")) { - data = parse_data_string(data, nullptr, ParseDataFlags::ALLOW_FILES); - } - return data; -} - -bool is_text_extension(const char* extension) { - return (!strcmp(extension, "txt") || !strcmp(extension, "json")); -} - -void write_output_data(Arguments& args, const void* data, size_t size, const char* extension) { - const string& input_filename = args.get(1, false); - const string& output_filename = args.get(2, false); - - if (!output_filename.empty() && (output_filename != "-")) { - // If the output is to a specified file, write it there - save_file(output_filename, data, size); - - } else if (output_filename.empty() && (output_filename != "-") && !input_filename.empty() && (input_filename != "-")) { - // If no output filename is given and an input filename is given, write to - // . - if (!extension) { - throw runtime_error("an output filename is required"); - } - string filename = input_filename; - filename += "."; - filename += extension; - save_file(filename, data, size); - - } else if (isatty(fileno(stdout)) && (!extension || !is_text_extension(extension))) { - // If stdout is a terminal and the data is not known to be text, use - // print_data to write the result - print_data(stdout, data, size); - fflush(stdout); - - } else { - // If stdout is not a terminal, write the data as-is - fwritex(stdout, data, size); - fflush(stdout); - } -} - -struct Action; -unordered_map all_actions; -vector action_order; - -struct Action { - const char* name; - const char* help_text; // May be null - function run; - - Action( - const char* name, - const char* help_text, - function run) - : name(name), - help_text(help_text), - run(run) { - auto emplace_ret = all_actions.emplace(this->name, this); - if (!emplace_ret.second) { - throw logic_error(string_printf("multiple actions with the same name: %s", this->name)); - } - action_order.emplace_back(this); - } -}; - -Action a_help( - "help", "\ - help\n\ - You\'re reading it now.\n", - +[](Arguments&) -> void { - print_usage(); - }); - -Action a_version( - "version", "\ - version\n\ - Show newserv\'s revision and build date.\n", - +[](Arguments&) -> void { - print_version_info(); - }); - -static void a_compress_decompress_fn(Arguments& args) { - const auto& action = args.get(0); - bool is_prs = ends_with(action, "-prs"); - bool is_bc0 = ends_with(action, "-bc0"); - bool is_pr2 = ends_with(action, "-pr2"); - bool is_prc = ends_with(action, "-prc"); - bool is_decompress = starts_with(action, "decompress-"); - bool is_big_endian = args.get("big-endian"); - bool is_optimal = args.get("optimal"); - bool is_pessimal = args.get("pessimal"); - int8_t compression_level = args.get("compression-level", 0); - size_t bytes = args.get("bytes", 0); - string seed = args.get("seed"); - - string data = read_input_data(args); - - size_t pr2_expected_size = 0; - if (is_decompress && (is_pr2 || is_prc)) { - auto decrypted = is_big_endian ? decrypt_pr2_data(data) : decrypt_pr2_data(data); - pr2_expected_size = decrypted.decompressed_size; - data = std::move(decrypted.compressed_data); - } - - size_t input_bytes = data.size(); - auto progress_fn = [&](auto, size_t input_progress, size_t, size_t output_progress) -> void { - float progress = static_cast(input_progress * 100) / input_bytes; - float size_ratio = static_cast(output_progress * 100) / input_progress; - fprintf(stderr, "... %zu/%zu (%g%%) => %zu (%g%%) \r", - input_progress, input_bytes, progress, output_progress, size_ratio); - }; - auto optimal_progress_fn = [&](auto phase, size_t input_progress, size_t input_bytes, size_t output_progress) -> void { - const char* phase_name = name_for_enum(phase); - float progress = static_cast(input_progress * 100) / input_bytes; - float size_ratio = static_cast(output_progress * 100) / input_progress; - fprintf(stderr, "... [%s] %zu/%zu (%g%%) => %zu (%g%%) \r", - phase_name, input_progress, input_bytes, progress, output_progress, size_ratio); - }; - - uint64_t start = now(); - if (!is_decompress && (is_prs || is_pr2 || is_prc)) { - if (is_optimal) { - data = prs_compress_optimal(data.data(), data.size(), optimal_progress_fn); - } else if (is_pessimal) { - data = prs_compress_pessimal(data.data(), data.size()); - } else { - data = prs_compress(data, compression_level, progress_fn); - } - } else if (is_decompress && (is_prs || is_pr2 || is_prc)) { - data = prs_decompress(data, bytes, (bytes != 0)); - } else if (!is_decompress && is_bc0) { - if (is_optimal) { - data = bc0_compress_optimal(data.data(), data.size(), optimal_progress_fn); - } else if (compression_level < 0) { - data = bc0_encode(data.data(), data.size()); - } else { - data = bc0_compress(data, progress_fn); - } - } else if (is_decompress && is_bc0) { - data = bc0_decompress(data); - } else { - throw logic_error("invalid behavior"); - } - uint64_t end = now(); - string time_str = format_duration(end - start); - - float size_ratio = static_cast(data.size() * 100) / input_bytes; - double bytes_per_sec = input_bytes / (static_cast(end - start) / 1000000.0); - string bytes_per_sec_str = format_size(bytes_per_sec); - log_info("%zu (0x%zX) bytes input => %zu (0x%zX) bytes output (%g%%) in %s (%s / sec)", - input_bytes, input_bytes, data.size(), data.size(), size_ratio, time_str.c_str(), bytes_per_sec_str.c_str()); - - if (is_pr2 || is_prc) { - if (is_decompress && (data.size() != pr2_expected_size)) { - log_warning("Result data size (%zu bytes) does not match expected size from PR2 header (%zu bytes)", data.size(), pr2_expected_size); - } else if (!is_decompress) { - uint32_t pr2_seed = seed.empty() ? random_object() : stoul(seed, nullptr, 16); - data = is_big_endian - ? encrypt_pr2_data(data, input_bytes, pr2_seed) - : encrypt_pr2_data(data, input_bytes, pr2_seed); - } - } - - const char* extension; - if (is_decompress) { - extension = "dec"; - } else if (is_prs) { - extension = "prs"; - } else if (is_bc0) { - extension = "bc0"; - } else if (is_prc) { - extension = "prc"; - } else if (is_pr2) { - extension = "pr2"; - } else { - throw logic_error("unknown action"); - } - write_output_data(args, data.data(), data.size(), extension); -} - -Action a_compress_prs("compress-prs", nullptr, a_compress_decompress_fn); -Action a_compress_bc0("compress-bc0", nullptr, a_compress_decompress_fn); -Action a_compress_pr2("compress-pr2", nullptr, a_compress_decompress_fn); -Action a_compress_prc("compress-prc", "\ - compress-prs [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ - compress-pr2 [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ - compress-prc [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ - compress-bc0 [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ - Compress data using the PRS, PR2, PRC, or BC0 algorithms. By default, the\n\ - heuristic-based compressor is used, which gives a good balance between\n\ - memory usage, CPU usage, and output size. For PRS and PR2, this compressor\n\ - can be tuned with the --compression-level=N option, which specifies how\n\ - aggressive the compressor should be in searching for literal sequences. The\n\ - default level is 0; a higher value generally means slower compression and a\n\ - smaller output size. If the compression level is -1, the input data is\n\ - encoded in a PRS-compatible format but not actually compressed, resulting\n\ - in valid PRS data which is about 9/8 the size of the input.\n\ - There is also a compressor which produces the absolute smallest output\n\ - size, but uses much more memory and CPU time. To use this compressor, use\n\ - the --optimal option.\n", - a_compress_decompress_fn); -Action a_decompress_prs("decompress-prs", nullptr, a_compress_decompress_fn); -Action a_decompress_bc0("decompress-bc0", nullptr, a_compress_decompress_fn); -Action a_decompress_pr2("decompress-pr2", nullptr, a_compress_decompress_fn); -Action a_decompress_prc("decompress-prc", "\ - decompress-prs [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ - decompress-pr2 [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ - decompress-prc [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ - decompress-bc0 [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ - Decompress data compressed using the PRS, PR2, PRC, or BC0 algorithms.\n", - a_compress_decompress_fn); - -Action a_prs_size( - "prs-size", "\ - prs-size [INPUT-FILENAME]\n\ - Compute the decompressed size of the PRS-compressed input data, but don\'t\n\ - write the decompressed data anywhere.\n", - +[](Arguments& args) { - string data = read_input_data(args); - size_t input_bytes = data.size(); - size_t output_bytes = prs_decompress_size(data); - log_info("%zu (0x%zX) bytes input => %zu (0x%zX) bytes output", - input_bytes, input_bytes, output_bytes, output_bytes); - }); - -Action a_disassemble_prs( - "disassemble-prs", nullptr, +[](Arguments& args) { - prs_disassemble(stdout, read_input_data(args)); - }); -Action a_disassemble_bc0( - "disassemble-bc0", "\ - disassemble-prs [INPUT-FILENAME]\n\ - disassemble-bc0 [INPUT-FILENAME]\n\ - Write a textual representation of the commands contained in a PRS or BC0\n\ - command stream. The output is written to stdout. This is mainly useful for\n\ - debugging the compressors and decompressors themselves.\n", - +[](Arguments& args) { - bc0_disassemble(stdout, read_input_data(args)); - }); - -static void a_encrypt_decrypt_fn(Arguments& args) { - bool is_decrypt = (args.get(0) == "decrypt-data"); - string seed = args.get("seed"); - bool is_big_endian = args.get("big-endian"); - auto version = get_cli_version(args); - - shared_ptr crypt; - switch (version) { - case Version::DC_NTE: - case Version::DC_V1_11_2000_PROTOTYPE: - case Version::DC_V1: - case Version::DC_V2: - case Version::PC_NTE: - case Version::PC_V2: - case Version::GC_NTE: - crypt = make_shared(stoul(seed, nullptr, 16)); - break; - case Version::GC_V3: - case Version::XB_V3: - case Version::GC_EP3_NTE: - case Version::GC_EP3: - crypt = make_shared(stoul(seed, nullptr, 16)); - break; - case Version::BB_V4: { - string key_name = args.get("key"); - if (key_name.empty()) { - throw runtime_error("the --key option is required for BB"); - } - seed = parse_data_string(seed, nullptr, ParseDataFlags::ALLOW_FILES); - auto key = load_object_file("system/blueburst/keys/" + key_name + ".nsk"); - crypt = make_shared(key, seed.data(), seed.size()); - break; - } - default: - throw logic_error("invalid game version"); - } - - string data = read_input_data(args); - - size_t original_size = data.size(); - data.resize((data.size() + 7) & (~7), '\0'); - - if (is_big_endian) { - uint32_t* dwords = reinterpret_cast(data.data()); - for (size_t x = 0; x < (data.size() >> 2); x++) { - dwords[x] = bswap32(dwords[x]); - } - } - - if (is_decrypt) { - crypt->decrypt(data.data(), data.size()); - } else { - crypt->encrypt(data.data(), data.size()); - } - - if (is_big_endian) { - uint32_t* dwords = reinterpret_cast(data.data()); - for (size_t x = 0; x < (data.size() >> 2); x++) { - dwords[x] = bswap32(dwords[x]); - } - } - - data.resize(original_size); - - write_output_data(args, data.data(), data.size(), "dec"); -} - -Action a_encrypt_data("encrypt-data", nullptr, a_encrypt_decrypt_fn); -Action a_decrypt_data("decrypt-data", "\ - encrypt-data [INPUT-FILENAME [OUTPUT-FILENAME] [OPTIONS...]]\n\ - decrypt-data [INPUT-FILENAME [OUTPUT-FILENAME] [OPTIONS...]]\n\ - Encrypt or decrypt data using PSO\'s standard network protocol encryption.\n\ - By default, PSO V3 (GameCube/XBOX) encryption is used, but this can be\n\ - overridden with the --pc or --bb options. The --seed=SEED option specifies\n\ - the encryption seed (4 hex bytes for PC or GC, or 48 hex bytes for BB). For\n\ - BB, the --key=KEY-NAME option is required as well, and refers to a .nsk\n\ - file in system/blueburst/keys (without the directory or .nsk extension).\n\ - For non-BB ciphers, the --big-endian option applies the cipher masks as\n\ - big-endian instead of little-endian, which is necessary for some GameCube\n\ - file formats.\n", - a_encrypt_decrypt_fn); - -static void a_encrypt_decrypt_trivial_fn(Arguments& args) { - bool is_decrypt = (args.get(0) == "decrypt-trivial-data"); - string seed = args.get("seed"); - - if (seed.empty() && !is_decrypt) { - throw logic_error("--seed is required when encrypting data"); - } - string data = read_input_data(args); - uint8_t basis; - if (seed.empty()) { - uint8_t best_seed = 0x00; - size_t best_seed_score = 0; - for (size_t z = 0; z < 0x100; z++) { - string decrypted = data; - decrypt_trivial_gci_data(decrypted.data(), decrypted.size(), z); - size_t score = 0; - for (size_t x = 0; x < decrypted.size(); x++) { - if (decrypted[x] == '\0') { - score++; - } - } - if (score > best_seed_score) { - best_seed = z; - best_seed_score = score; - } - } - fprintf(stderr, "Basis appears to be %02hhX (%zu zero bytes in output)\n", - best_seed, best_seed_score); - basis = best_seed; - } else { - basis = stoul(seed, nullptr, 16); - } - decrypt_trivial_gci_data(data.data(), data.size(), basis); - write_output_data(args, data.data(), data.size(), "dec"); -} - -Action a_encrypt_trivial_data("encrypt-trivial-data", nullptr, a_encrypt_decrypt_trivial_fn); -Action a_decrypt_trivial_data("decrypt-trivial-data", "\ - encrypt-trivial-data --seed=BASIS [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ - decrypt-trivial-data [--seed=BASIS] [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ - Encrypt or decrypt data using the Episode 3 trivial algorithm. When\n\ - encrypting, --seed=BASIS is required; BASIS should be a single byte\n\ - specified in hexadecimal. When decrypting, BASIS should be specified the\n\ - same way, but if it is not given, newserv will try all possible basis\n\ - values and return the one that results in the greatest number of zero bytes\n\ - in the output.\n", - a_encrypt_decrypt_trivial_fn); - -Action a_decrypt_registry_value( - "decrypt-registry-value", nullptr, +[](Arguments& args) { - string data = read_input_data(args); - string out_data = decrypt_v2_registry_value(data.data(), data.size()); - write_output_data(args, out_data.data(), out_data.size(), "dec"); - }); - -Action a_encrypt_challenge_data( - "encrypt-challenge-data", nullptr, +[](Arguments& args) { - string data = read_input_data(args); - encrypt_challenge_rank_text_t(data.data(), data.size()); - write_output_data(args, data.data(), data.size(), "dec"); - }); -Action a_decrypt_challenge_data( - "decrypt-challenge-data", "\ - encrypt-challenge-data [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ - decrypt-challenge-data [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ - Encrypt or decrypt data using the challenge mode trivial algorithm.\n", - +[](Arguments& args) { - string data = read_input_data(args); - decrypt_challenge_rank_text_t(data.data(), data.size()); - write_output_data(args, data.data(), data.size(), "dec"); - }); - -static void a_encrypt_decrypt_gci_save_fn(Arguments& args) { - bool is_decrypt = (args.get(0) == "decrypt-gci-save"); - bool skip_checksum = args.get("skip-checksum"); - string seed = args.get("seed"); - string system_filename = args.get("sys"); - int64_t override_round2_seed = args.get("round2-seed", -1, Arguments::IntFormat::HEX); - - uint32_t round1_seed; - if (!system_filename.empty()) { - string system_data = load_file(system_filename); - StringReader r(system_data); - const auto& header = r.get(); - header.check(); - const auto& system = r.get(); - round1_seed = system.creation_timestamp; - } else if (!seed.empty()) { - round1_seed = stoul(seed, nullptr, 16); - } else { - throw runtime_error("either --sys or --seed must be given"); - } - - auto data = read_input_data(args); - StringReader r(data); - const auto& header = r.get(); - header.check(); - - size_t data_start_offset = r.where(); - - auto process_file = [&]() { - if (is_decrypt) { - const void* data_section = r.getv(header.data_size); - auto decrypted = decrypt_fixed_size_data_section_t( - data_section, header.data_size, round1_seed, skip_checksum, override_round2_seed); - *reinterpret_cast(data.data() + data_start_offset) = decrypted; - } else { - const auto& s = r.get(); - auto encrypted = encrypt_fixed_size_data_section_t(s, round1_seed); - if (data_start_offset + encrypted.size() > data.size()) { - throw runtime_error("encrypted result exceeds file size"); - } - memcpy(data.data() + data_start_offset, encrypted.data(), encrypted.size()); - } - }; - - if (header.data_size == sizeof(PSOGCGuildCardFile)) { - process_file.template operator()(); - } else if (header.is_ep12() && (header.data_size == sizeof(PSOGCCharacterFile))) { - process_file.template operator()(); - } else if (header.is_ep3() && (header.data_size == sizeof(PSOGCEp3CharacterFile))) { - auto* charfile = reinterpret_cast(data.data() + data_start_offset); - if (!is_decrypt) { - for (size_t z = 0; z < charfile->characters.size(); z++) { - charfile->characters[z].ep3_config.encrypt(random_object()); - } - } - process_file.template operator()(); - if (is_decrypt) { - for (size_t z = 0; z < charfile->characters.size(); z++) { - charfile->characters[z].ep3_config.decrypt(); - } - } - } else { - throw runtime_error("unrecognized save type"); - } - - write_output_data(args, data.data(), data.size(), is_decrypt ? "gcid" : "gci"); -} - -Action a_decrypt_gci_save("decrypt-gci-save", nullptr, a_encrypt_decrypt_gci_save_fn); -Action a_encrypt_gci_save("encrypt-gci-save", "\ - encrypt-gci-save CRYPT-OPTION INPUT-FILENAME [OUTPUT-FILENAME]\n\ - decrypt-gci-save CRYPT-OPTION INPUT-FILENAME [OUTPUT-FILENAME]\n\ - Encrypt or decrypt a character or Guild Card file in GCI format. If\n\ - encrypting, the checksum is also recomputed and stored in the encrypted\n\ - file. CRYPT-OPTION is required; it can be either --sys=SYSTEM-FILENAME\n\ - (specifying the name of the corresponding PSO_SYSTEM .gci file) or\n\ - --seed=ROUND1-SEED (specified as a 32-bit hexadecimal number).\n", - a_encrypt_decrypt_gci_save_fn); - -static void a_encrypt_decrypt_pc_save_fn(Arguments& args) { - bool is_decrypt = (args.get(0) == "decrypt-pc-save"); - bool skip_checksum = args.get("skip-checksum"); - string seed = args.get("seed"); - int64_t override_round2_seed = args.get("round2-seed", -1, Arguments::IntFormat::HEX); - - if (seed.empty()) { - throw runtime_error("--seed must be given to specify the serial number"); - } - uint32_t round1_seed = stoul(seed, nullptr, 16); - - auto data = read_input_data(args); - if (data.size() == sizeof(PSOPCGuildCardFile)) { - if (is_decrypt) { - data = decrypt_fixed_size_data_section_s( - data.data(), offsetof(PSOPCGuildCardFile, end_padding), round1_seed, skip_checksum, override_round2_seed); - } else { - data = encrypt_fixed_size_data_section_s( - data.data(), offsetof(PSOPCGuildCardFile, end_padding), round1_seed); - } - data.resize((sizeof(PSOPCGuildCardFile) + 0x1FF) & (~0x1FF), '\0'); - } else if (data.size() == sizeof(PSOPCCharacterFile)) { - PSOPCCharacterFile* charfile = reinterpret_cast(data.data()); - if (is_decrypt) { - for (size_t z = 0; z < charfile->entries.size(); z++) { - if (charfile->entries[z].present) { - try { - charfile->entries[z].encrypted = decrypt_fixed_size_data_section_t( - &charfile->entries[z].encrypted, sizeof(charfile->entries[z].encrypted), round1_seed, skip_checksum, override_round2_seed); - } catch (const exception& e) { - fprintf(stderr, "warning: cannot decrypt character %zu: %s\n", z, e.what()); - } - } - } - } else { - for (size_t z = 0; z < charfile->entries.size(); z++) { - if (charfile->entries[z].present) { - string encrypted = encrypt_fixed_size_data_section_t( - charfile->entries[z].encrypted, round1_seed); - if (encrypted.size() != sizeof(PSOPCCharacterFile::CharacterEntry::EncryptedSection)) { - throw logic_error("incorrect encrypted result size"); - } - charfile->entries[z].encrypted = *reinterpret_cast(encrypted.data()); - } - } - } - } else if (data.size() == sizeof(PSOPCCreationTimeFile)) { - throw runtime_error("the PSO______FLS file is not encrypted; it is just random data"); - } else if (data.size() == sizeof(PSOPCSystemFile)) { - throw runtime_error("the PSO______COM file is not encrypted"); - } else { - throw runtime_error("unknown save file type"); - } - - write_output_data(args, data.data(), data.size(), "dec"); -} - -Action a_decrypt_pc_save("decrypt-pc-save", nullptr, a_encrypt_decrypt_pc_save_fn); -Action a_encrypt_pc_save("encrypt-pc-save", "\ - encrypt-pc-save --seed=SEED [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ - decrypt-pc-save --seed=SEED [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ - Encrypt or decrypt a PSO PC character file (PSO______SYS or PSO______SYD)\n\ - or Guild Card file (PSO______GUD). SEED should be the serial number\n\ - associated with the save file, as a 32-bit hexadecimal integer.\n", - a_encrypt_decrypt_pc_save_fn); - -static void a_encrypt_decrypt_save_data_fn(Arguments& args) { - bool is_decrypt = (args.get(0) == "decrypt-save-data"); - bool skip_checksum = args.get("skip-checksum"); - bool is_big_endian = args.get("big-endian"); - string seed = args.get("seed"); - int64_t override_round2_seed = args.get("round2-seed", -1, Arguments::IntFormat::HEX); - size_t bytes = args.get("bytes", 0); - - if (seed.empty()) { - throw runtime_error("--seed must be given to specify the round1 seed"); - } - uint32_t round1_seed = stoul(seed, nullptr, 16); - - auto data = read_input_data(args); - StringReader r(data); - - string output_data; - size_t effective_size = bytes ? min(bytes, data.size()) : data.size(); - if (is_decrypt) { - output_data = is_big_endian - ? decrypt_fixed_size_data_section_s(data.data(), effective_size, round1_seed, skip_checksum, override_round2_seed) - : decrypt_fixed_size_data_section_s(data.data(), effective_size, round1_seed, skip_checksum, override_round2_seed); - } else { - output_data = is_big_endian - ? encrypt_fixed_size_data_section_s(data.data(), effective_size, round1_seed) - : encrypt_fixed_size_data_section_s(data.data(), effective_size, round1_seed); - } - write_output_data(args, output_data.data(), output_data.size(), "dec"); -} - -// TODO: Write usage text for these actions -Action a_decrypt_save_data("decrypt-save-data", nullptr, a_encrypt_decrypt_save_data_fn); -Action a_encrypt_save_data("encrypt-save-data", nullptr, a_encrypt_decrypt_save_data_fn); - -Action a_decrypt_dcv2_executable( - "decrypt-dcv2-executable", "\ - decrypt-dcv2-executable --executable=EXEC --indexes=INDEXES --values=VALUES\n\ - decrypt-dcv2-executable --executable=EXEC --simple [--seed=SEED]\n\ - Decrypt a PSO DC v2 executable file. EXEC should be the path to the\n\ - executable (DP_ADDRESS.JPN), INDEXES should be the path to the index fixup\n\ - table (KATSUO.SEA), and VALUES should be the path to the value fixup table\n\ - (IWASHI.SEA). The output is written to EXEC.dec.\n\ - If --simple is given, uses the simpler encryption method used in some\n\ - community modifications of the game. In this case, --seed is not required;\n\ - if not given, finds the seed automatically, and prints it to stderr so you\n\ - will be able to use it when re-encrypting.\n", - +[](Arguments& args) { - string executable_filename = args.get("executable", true); - string executable_data = load_file(executable_filename); - string decrypted; - if (args.get("simple")) { - string seed_str = args.get("seed"); - int64_t seed = seed_str.empty() ? -1 : stoull(seed_str, nullptr, 16); - decrypted = crypt_dp_address_jpn_simple(executable_data, seed); - } else { - string values_filename = args.get("values", true); - string indexes_filename = args.get("indexes", true); - string values_data = load_file(values_filename); - string indexes_data = load_file(indexes_filename); - decrypted = decrypt_dp_address_jpn(executable_data, values_data, indexes_data); - } - save_file(executable_filename + ".dec", decrypted); - }); -Action a_encrypt_dcv2_executable( - "encrypt-dcv2-executable", "\ - encrypt-dcv2-executable --executable=EXEC --indexes=INDEXES\n\ - encrypt-dcv2-executable --executable=EXEC --simple --seed=SEED\n\ - Encrypt a PSO DC v2 executable file. EXEC should be the path to the\n\ - executable (DP_ADDRESS.JPN) and INDEXES should be the path to the index\n\ - fixup table (KATSUO.SEA). The output is written to EXEC.enc and\n\ - INDEXES.enc.\n\ - If --simple is given, uses the simpler encryption method used in some\n\ - community modifications of the game. In this case, --seed is required.\n", - +[](Arguments& args) { - string executable_filename = args.get("executable", true); - string executable_data = load_file(executable_filename); - string encrypted_executable; - if (args.get("simple")) { - int64_t seed = stoull(args.get("seed", true), nullptr, 16); - encrypted_executable = crypt_dp_address_jpn_simple(executable_data, seed); - } else { - string indexes_filename = args.get("indexes", true); - string indexes_data = load_file(indexes_filename); - auto encrypted = encrypt_dp_address_jpn(executable_data, indexes_data); - save_file(indexes_filename + ".enc", encrypted.indexes); - encrypted_executable = std::move(encrypted.executable); - } - save_file(executable_filename + ".enc", encrypted_executable); - }); - -Action a_decode_gci_snapshot( - "decode-gci-snapshot", "\ - decode-gci-snapshot [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ - Decode a PSO GC snapshot file into a Windows BMP image.\n", - +[](Arguments& args) { - auto data = read_input_data(args); - StringReader r(data); - const auto& header = r.get(); - try { - header.check(); - } catch (const exception& e) { - log_warning("File header failed validation (%s)", e.what()); - } - const auto& file = r.get(); - if (!file.checksum_correct()) { - log_warning("File internal checksum is incorrect"); - } - - auto img = file.decode_image(); - string saved = img.save(Image::Format::WINDOWS_BITMAP); - write_output_data(args, saved.data(), saved.size(), "bmp"); - }); - -Action a_encode_gvm( - "encode-gvm", "\ - encode-gvm [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ - Encode an image in BMP or PPM/PNM format into a GVM texture. The resulting\n\ - GVM file can be used as an Episode 3 lobby banner.\n", - +[](Arguments& args) { - const string& input_filename = args.get(1, false); - Image img; - if (!input_filename.empty() && (input_filename != "-")) { - img = Image(input_filename); - } else { - img = Image(stdin); - } - string encoded = encode_gvm(img, img.get_has_alpha() ? GVRDataFormat::RGB5A3 : GVRDataFormat::RGB565, "image.gvr", 0); - write_output_data(args, encoded.data(), encoded.size(), "gvm"); - }); - -Action a_salvage_gci( - "salvage-gci", "\ - salvage-gci INPUT-FILENAME [--round2] [CRYPT-OPTION] [--bytes=SIZE]\n\ - Attempt to find either the round-1 or round-2 decryption seed for a\n\ - corrupted GCI file. If --round2 is given, then CRYPT-OPTION must be given\n\ - (and should specify either a valid system file or the round1 seed).\n", - +[](Arguments& args) { - bool round2 = args.get("round2"); - string seed = args.get("seed"); - string system_filename = args.get("sys"); - size_t num_threads = args.get("threads", 0); - size_t offset = args.get("offset", 0); - size_t stride = args.get("stride", 1); - size_t bytes = args.get("bytes", 0); - - uint64_t likely_round1_seed = 0xFFFFFFFFFFFFFFFF; - if (!system_filename.empty()) { - try { - string system_data = load_file(system_filename); - StringReader r(system_data); - const auto& header = r.get(); - header.check(); - const auto& system = r.get(); - likely_round1_seed = system.creation_timestamp; - log_info("System file appears to be in order; round1 seed is %08" PRIX64, likely_round1_seed); - } catch (const exception& e) { - log_warning("Cannot parse system file (%s); ignoring it", e.what()); - } - } else if (!seed.empty()) { - likely_round1_seed = stoul(seed, nullptr, 16); - log_info("Specified round1 seed is %08" PRIX64, likely_round1_seed); - } - - if (round2 && likely_round1_seed > 0x100000000) { - throw invalid_argument("cannot find round2 seed without known round1 seed"); - } - - auto data = read_input_data(args); - StringReader r(data); - const auto& header = r.get(); - header.check(); - - const void* data_section = r.getv(header.data_size); - - auto process_file = [&]() { - vector> top_seeds_by_thread( - num_threads ? num_threads : thread::hardware_concurrency()); - parallel_range( - [&](uint64_t seed, size_t thread_num) -> bool { - size_t zero_count; - if (round2) { - string decrypted = decrypt_gci_fixed_size_data_section_for_salvage( - data_section, header.data_size, likely_round1_seed, seed, bytes); - zero_count = count_zeroes( - decrypted.data() + offset, - decrypted.size() - offset, - stride); - } else { - auto decrypted = decrypt_fixed_size_data_section_t( - data_section, - header.data_size, - seed, - true); - zero_count = count_zeroes( - reinterpret_cast(&decrypted) + offset, - sizeof(decrypted) - offset, - stride); - } - auto& top_seeds = top_seeds_by_thread[thread_num]; - if (top_seeds.size() < 10 || (zero_count >= top_seeds.begin()->second)) { - top_seeds.emplace(zero_count, seed); - if (top_seeds.size() > 10) { - top_seeds.erase(top_seeds.begin()); - } - } - return false; - }, - 0, - 0x100000000, - num_threads); - - multimap top_seeds; - for (const auto& thread_top_seeds : top_seeds_by_thread) { - for (const auto& it : thread_top_seeds) { - top_seeds.emplace(it.first, it.second); - } - } - for (const auto& it : top_seeds) { - const char* sys_seed_str = (!round2 && (it.second == likely_round1_seed)) - ? " (this is the seed from the system file)" - : ""; - log_info("Round %c seed %08" PRIX32 " resulted in %zu zero bytes%s", - round2 ? '2' : '1', it.second, it.first, sys_seed_str); - } - }; - - if (header.data_size == sizeof(PSOGCGuildCardFile)) { - process_file.template operator()(); - } else if (header.is_ep12() && (header.data_size == sizeof(PSOGCCharacterFile))) { - process_file.template operator()(); - } else if (header.is_ep3() && (header.data_size == sizeof(PSOGCEp3CharacterFile))) { - process_file.template operator()(); - } else { - throw runtime_error("unrecognized save type"); - } - }); - -Action a_find_decryption_seed( - "find-decryption-seed", "\ - find-decryption-seed OPTIONS...\n\ - Perform a brute-force search for a decryption seed of the given data. The\n\ - ciphertext is specified with the --encrypted=DATA option and the expected\n\ - plaintext is specified with the --decrypted=DATA option. The plaintext may\n\ - include unmatched bytes (specified with the Phosg parse_data_string ?\n\ - operator), but overall it must be the same length as the ciphertext. By\n\ - default, this option uses PSO V3 encryption, but this can be overridden\n\ - with --pc. (BB encryption seeds are too long to be searched for with this\n\ - function.) By default, the number of worker threads is equal to the number\n\ - of CPU cores in the system, but this can be overridden with the\n\ - --threads=NUM-THREADS option.\n", - +[](Arguments& args) { - const auto& plaintexts_ascii = args.get_multi("decrypted"); - const auto& ciphertext_ascii = args.get("encrypted"); - auto version = get_cli_version(args); - if (plaintexts_ascii.empty() || ciphertext_ascii.empty()) { - throw runtime_error("both --encrypted and --decrypted must be specified"); - } - if (uses_v4_encryption(version)) { - throw runtime_error("--find-decryption-seed cannot be used for BB ciphers"); - } - bool skip_little_endian = args.get("skip-little-endian"); - bool skip_big_endian = args.get("skip-big-endian"); - size_t num_threads = args.get("threads", 0); - - size_t max_plaintext_size = 0; - vector> plaintexts; - for (const auto& plaintext_ascii : plaintexts_ascii) { - string mask; - string data = parse_data_string(plaintext_ascii, &mask, ParseDataFlags::ALLOW_FILES); - if (data.size() != mask.size()) { - throw logic_error("plaintext and mask are not the same size"); - } - max_plaintext_size = max(max_plaintext_size, data.size()); - plaintexts.emplace_back(std::move(data), std::move(mask)); - } - string ciphertext = parse_data_string(ciphertext_ascii, nullptr, ParseDataFlags::ALLOW_FILES); - - auto mask_match = +[](const void* a, const void* b, const void* m, size_t size) -> bool { - const uint8_t* a8 = reinterpret_cast(a); - const uint8_t* b8 = reinterpret_cast(b); - const uint8_t* m8 = reinterpret_cast(m); - for (size_t z = 0; z < size; z++) { - if ((a8[z] & m8[z]) != (b8[z] & m8[z])) { - return false; - } - } - return true; - }; - - uint64_t seed = parallel_range([&](uint64_t seed, size_t) -> bool { - string be_decrypt_buf = ciphertext.substr(0, max_plaintext_size); - string le_decrypt_buf = ciphertext.substr(0, max_plaintext_size); - if (uses_v3_encryption(version)) { - PSOV3Encryption(seed).encrypt_both_endian( - le_decrypt_buf.data(), - be_decrypt_buf.data(), - be_decrypt_buf.size()); - } else { - PSOV2Encryption(seed).encrypt_both_endian( - le_decrypt_buf.data(), - be_decrypt_buf.data(), - be_decrypt_buf.size()); - } - - for (const auto& plaintext : plaintexts) { - if (!skip_little_endian) { - if (mask_match(le_decrypt_buf.data(), plaintext.first.data(), plaintext.second.data(), plaintext.second.size())) { - return true; - } - } - if (!skip_big_endian) { - if (mask_match(be_decrypt_buf.data(), plaintext.first.data(), plaintext.second.data(), plaintext.second.size())) { - return true; - } - } - } - return false; - }, - 0, 0x100000000, num_threads); - - if (seed < 0x100000000) { - log_info("Found seed %08" PRIX64, seed); - } else { - log_error("No seed found"); - } - }); - -Action a_decode_gci( - "decode-gci", nullptr, +[](Arguments& args) { - string input_filename = args.get(1, false); - if (input_filename.empty() || (input_filename == "-")) { - throw invalid_argument("an input filename is required"); - } - string seed = args.get("seed"); - size_t num_threads = args.get("threads", 0); - bool skip_checksum = args.get("skip-checksum"); - int64_t dec_seed = seed.empty() ? -1 : stoul(seed, nullptr, 16); - auto decoded = decode_gci_data(read_input_data(args), num_threads, dec_seed, skip_checksum); - save_file(input_filename + ".dec", decoded); - }); -Action a_decode_vms( - "decode-vms", nullptr, +[](Arguments& args) { - string input_filename = args.get(1, false); - if (input_filename.empty() || (input_filename == "-")) { - throw invalid_argument("an input filename is required"); - } - string seed = args.get("seed"); - size_t num_threads = args.get("threads", 0); - bool skip_checksum = args.get("skip-checksum"); - int64_t dec_seed = seed.empty() ? -1 : stoul(seed, nullptr, 16); - auto decoded = decode_vms_data(read_input_data(args), num_threads, dec_seed, skip_checksum); - save_file(input_filename + ".dec", decoded); - }); -Action a_decode_dlq( - "decode-dlq", nullptr, +[](Arguments& args) { - string input_filename = args.get(1, false); - if (input_filename.empty() || (input_filename == "-")) { - throw invalid_argument("an input filename is required"); - } - auto decoded = decode_dlq_data(read_input_data(args)); - save_file(input_filename + ".dec", decoded); - }); -Action a_decode_qst( - "decode-qst", "\ - decode-gci INPUT-FILENAME [OPTIONS...]\n\ - decode-vms INPUT-FILENAME [OPTIONS...]\n\ - decode-dlq INPUT-FILENAME\n\ - decode-qst INPUT-FILENAME\n\ - Decode the input quest file into a compressed, unencrypted .bin or .dat\n\ - file (or in the case of decode-qst, both a .bin and a .dat file).\n\ - INPUT-FILENAME must be specified, but there is no OUTPUT-FILENAME; the\n\ - output is written to INPUT-FILENAME.dec (or .bin, or .dat). If the output\n\ - is a .dec file, you can rename it to .bin or .dat manually. DLQ and QST\n\ - decoding are relatively simple operations, but GCI and VMS decoding can be\n\ - computationally expensive if the file is encrypted and doesn\'t contain an\n\ - embedded seed. If you know the player\'s serial number who generated the\n\ - GCI or VMS file, use the --seed=SEED option and give the serial number (as\n\ - a hex-encoded 32-bit integer). If you don\'t know the serial number,\n\ - newserv will find it via a brute-force search, which will take a long time.\n", - +[](Arguments& args) { - string input_filename = args.get(1, false); - if (input_filename.empty() || (input_filename == "-")) { - throw invalid_argument("an input filename is required"); - } - auto files = decode_qst_data(read_input_data(args)); - for (const auto& it : files) { - save_file(input_filename + "-" + it.first, it.second); - } - }); - -Action a_encode_qst( - "encode-qst", "\ - encode-qst INPUT-FILENAME [OUTPUT-FILENAME] [OPTIONS...]\n\ - Encode the input quest file (in .bin/.dat format) into a .qst file. If\n\ - --download is given, generates a download .qst instead of an online .qst.\n\ - Specify the quest\'s game version with one of the --dc-nte, --dc-v1,\n\ - --dc-v2, --pc, --gc-nte, --gc, --gc-ep3, --xb, or --bb options.\n", - +[](Arguments& args) { - string input_filename = args.get(1, false); - if (input_filename.empty() || (input_filename == "-")) { - throw invalid_argument("an input filename is required"); - } - auto version = get_cli_version(args); - bool download = args.get("download"); - - string bin_filename = input_filename; - string dat_filename = ends_with(bin_filename, ".bin") - ? (bin_filename.substr(0, bin_filename.size() - 3) + "dat") - : (bin_filename + ".dat"); - string pvr_filename = ends_with(bin_filename, ".bin") - ? (bin_filename.substr(0, bin_filename.size() - 3) + "pvr") - : (bin_filename + ".pvr"); - auto bin_data = make_shared(load_file(bin_filename)); - auto dat_data = make_shared(load_file(dat_filename)); - shared_ptr pvr_data; - try { - pvr_data = make_shared(load_file(pvr_filename)); - } catch (const cannot_open_file&) { - } - auto vq = make_shared(0, 0, version, 0, bin_data, dat_data, pvr_data); - if (download) { - vq = vq->create_download_quest(); - } - string qst_data = vq->encode_qst(); - - write_output_data(args, qst_data.data(), qst_data.size(), "qst"); - }); - -Action a_disassemble_quest_script( - "disassemble-quest-script", "\ - disassemble-quest-script [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ - Disassemble the input quest script (.bin file) into a text representation\n\ - of the commands and metadata it contains. Specify the quest\'s game version\n\ - with one of the --dc-nte, --dc-v1, --dc-v2, --pc, --gc-nte, --gc, --gc-ep3,\n\ - --xb, or --bb options. If you intend to edit and reassemble the script, use\n\ - the --reassembly option to add explicit label numbers and remove offsets\n\ - and data in code sections.\n", - +[](Arguments& args) { - string data = read_input_data(args); - auto version = get_cli_version(args); - if (!args.get("decompressed")) { - data = prs_decompress(data); - } - uint8_t override_language = args.get("language", 0xFF); - bool reassembly_mode = args.get("reassembly"); - string result = disassemble_quest_script(data.data(), data.size(), version, override_language, reassembly_mode); - write_output_data(args, result.data(), result.size(), "txt"); - }); -Action a_disassemble_quest_map( - "disassemble-quest-map", "\ - disassemble-quest-map [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ - Disassemble the input quest map (.dat file) into a text representation of\n\ - the data it contains.\n", - +[](Arguments& args) { - string data = read_input_data(args); - if (!args.get("decompressed")) { - data = prs_decompress(data); - } - string result = Map::disassemble_quest_data(data.data(), data.size()); - write_output_data(args, result.data(), result.size(), "txt"); - }); -Action a_disassemble_free_map( - "disassemble-free-map", "\ - disassemble-free-map INPUT-FILENAME [OUTPUT-FILENAME]\n\ - Disassemble the input free-roam map (.dat or .evt file) into a text\n\ - representation of the data it contains. Unlike othe disassembly actions,\n\ - this action expects its input to be already decompressed. If the input is\n\ - compressed, use the --compressed option. Also unlike other options, the\n\ - input must be from a file (that is, INPUT-FILENAME is required and cannot\n\ - be \"-\").\n", - +[](Arguments& args) { - const string& input_filename = args.get(1, true); - bool is_events = ends_with(input_filename, ".evt"); - bool is_enemies = ends_with(input_filename, "e.dat") || ends_with(input_filename, "e_s.dat") || ends_with(input_filename, "e_c1.dat") || ends_with(input_filename, "e_d.dat"); - bool is_objects = ends_with(input_filename, "o.dat") || ends_with(input_filename, "o_s.dat") || ends_with(input_filename, "o_c1.dat") || ends_with(input_filename, "o_d.dat"); - if (!is_objects && !is_enemies && !is_events) { - throw runtime_error("cannot determine input file type"); - } - - string data = read_input_data(args); - if (args.get("compressed")) { - data = prs_decompress(data); - } - - string result; - if (is_objects) { - result = Map::disassemble_objects_data(data.data(), data.size()); - } else if (is_enemies) { - result = Map::disassemble_enemies_data(data.data(), data.size()); - } else if (is_events) { - result = Map::disassemble_wave_events_data(data.data(), data.size()); - } else { - throw logic_error("unhandled input type"); - } - result.push_back('\n'); - - write_output_data(args, result.data(), result.size(), "txt"); - }); -Action a_disassemble_set_data_table( - "disassemble-set-data-table", "\ - disassemble-set-data-table [INPUT-FILENAME]\n\ - Show the contents of a SetDataTable.rel file. A version option is required.\n", - +[](Arguments& args) { - Version version = get_cli_version(args); - SetDataTable sdt(version, read_input_data(args)); - string str = sdt.str(); - write_output_data(args, str.data(), str.size(), "txt"); - }); - -Action a_assemble_quest_script( - "assemble-quest-script", "\ - assemble-quest-script [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ - Assemble the input quest script (.txt file) into a compressed .bin file\n\ - usable as an online quest script. If --decompressed is given, produces an\n\ - uncompressed .bind file instead.\n", - +[](Arguments& args) { - string text = read_input_data(args); - string result = assemble_quest_script(text); - bool compress = !args.get("decompressed"); - if (compress) { - result = prs_compress_optimal(result); - } - write_output_data(args, result.data(), result.size(), compress ? "bin" : "bind"); - }); - -Action a_assemble_all_patches( - "assemble-all-patches", "\ - assemble-all-patches\n\ - Assemble all patches in the system/client-functions directory, and produce\n\ - two compiled .bin files for each patch (one unencrypted, for most PSO\n\ - versions, and one encrypted, for PSO GC JP v1.4, JP Ep3, and Ep3 Trial\n\ - Edition). The output files are saved in system/client-functions.\n", - +[](Arguments&) { - auto fci = make_shared("system/client-functions"); - - auto process_code = +[](shared_ptr code, - uint32_t checksum_addr, - uint32_t checksum_size, - uint32_t override_start_addr) -> void { - for (uint8_t encrypted = 0; encrypted < 2; encrypted++) { - StringWriter w; - string data = prepare_send_function_call_data( - code, {}, nullptr, 0, checksum_addr, checksum_size, override_start_addr, encrypted); - w.put(PSOCommandHeaderDCV3{.command = 0xB2, .flag = code->index, .size = data.size() + 4}); - w.write(data); - string out_path = code->source_path + (encrypted ? ".enc.bin" : ".std.bin"); - save_file(out_path, w.str()); - fprintf(stderr, "... %s\n", out_path.c_str()); - } - }; - - for (const auto& it : fci->name_and_specific_version_to_patch_function) { - process_code(it.second, 0, 0, 0); - } - try { - process_code(fci->name_to_function.at("VersionDetectDC"), 0, 0, 0); - } catch (const out_of_range&) { - } - try { - process_code(fci->name_to_function.at("VersionDetectGC"), 0, 0, 0); - } catch (const out_of_range&) { - } - try { - process_code(fci->name_to_function.at("VersionDetectXB"), 0, 0, 0); - } catch (const out_of_range&) { - } - try { - process_code(fci->name_to_function.at("CacheClearFix-Phase1"), 0x80000000, 8, 0x7F2734EC); - } catch (const out_of_range&) { - } - try { - process_code(fci->name_to_function.at("CacheClearFix-Phase2"), 0, 0, 0); - } catch (const out_of_range&) { - } - }); - -void a_extract_archive_fn(Arguments& args) { - string output_prefix = args.get(2, false); - if (output_prefix == "-") { - throw invalid_argument("output prefix cannot be stdout"); - } else if (output_prefix.empty()) { - output_prefix = args.get(1, false); - if (output_prefix.empty() || (output_prefix == "-")) { - throw invalid_argument("an input filename must be given"); - } - output_prefix += "_"; - } - - string data = read_input_data(args); - auto data_shared = make_shared(std::move(data)); - - if (args.get(0) == "extract-afs") { - AFSArchive arch(data_shared); - const auto& all_entries = arch.all_entries(); - for (size_t z = 0; z < all_entries.size(); z++) { - auto e = arch.get(z); - string out_file = string_printf("%s-%zu", output_prefix.c_str(), z); - save_file(out_file.c_str(), e.first, e.second); - fprintf(stderr, "... %s\n", out_file.c_str()); - } - } else if (args.get(0) == "extract-gsl") { - GSLArchive arch(data_shared, args.get("big-endian")); - for (const auto& entry_it : arch.all_entries()) { - auto e = arch.get(entry_it.first); - string out_file = output_prefix + entry_it.first; - save_file(out_file.c_str(), e.first, e.second); - fprintf(stderr, "... %s\n", out_file.c_str()); - } - } else if (args.get(0) == "extract-bml") { - BMLArchive arch(data_shared, args.get("big-endian")); - for (const auto& entry_it : arch.all_entries()) { - { - auto e = arch.get(entry_it.first); - string data = prs_decompress(e.first, e.second); - string out_file = output_prefix + entry_it.first; - save_file(out_file, data); - fprintf(stderr, "... %s\n", out_file.c_str()); - } - - auto gvm_e = arch.get_gvm(entry_it.first); - if (gvm_e.second) { - string data = prs_decompress(gvm_e.first, gvm_e.second); - string out_file = output_prefix + entry_it.first + ".gvm"; - save_file(out_file, data); - fprintf(stderr, "... %s\n", out_file.c_str()); - } - } - } else { - throw logic_error("unimplemented archive type"); - } -} - -Action a_extract_afs("extract-afs", nullptr, a_extract_archive_fn); -Action a_extract_gsl("extract-gsl", nullptr, a_extract_archive_fn); -Action a_extract_bml("extract-bml", "\ - extract-afs [INPUT-FILENAME] [--big-endian]\n\ - extract-gsl [INPUT-FILENAME] [--big-endian]\n\ - extract-bml [INPUT-FILENAME] [--big-endian]\n\ - Extract all files from an AFS, GSL, or BML archive into the current\n\ - directory. input-filename may be specified. If output-filename is\n\ - specified, then it is treated as a prefix which is prepended to the\n\ - filename of each file contained in the archive. If --big-endian is given,\n\ - the archive header is read in GameCube format; otherwise it is read in\n\ - PC/BB format.\n", - a_extract_archive_fn); - -Action a_encode_sjis( - "encode-sjis", nullptr, +[](Arguments& args) { - string data = read_input_data(args); - string result = tt_utf8_to_sega_sjis(data); - write_output_data(args, result.data(), result.size(), "txt"); - }); -Action a_decode_sjis( - "decode-sjis", nullptr, +[](Arguments& args) { - string data = read_input_data(args); - string result = tt_sega_sjis_to_utf8(data); - write_output_data(args, result.data(), result.size(), "txt"); - }); - -Action a_decode_text_archive( - "decode-text-archive", "\ - decode-text-archive [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ - Decode a text archive to JSON. --collections=NUM_COLLECTIONS is given,\n\ - expects a fixed number of collections in the input. If --has-pr3 is given,\n\ - expects the input not to have a REL footer.\n", - +[](Arguments& args) { - string data = read_input_data(args); - bool is_sjis = args.get("japanese"); - - unique_ptr ts; - size_t collection_count = args.get("collections", 0); - if (collection_count) { - ts = make_unique(data, collection_count, !args.get("has-pr3"), is_sjis); - } else { - ts = make_unique(data, args.get("big-endian"), is_sjis); - } - JSON j = ts->json(); - string out_data = j.serialize(JSON::SerializeOption::FORMAT | JSON::SerializeOption::ESCAPE_CONTROLS_ONLY); - write_output_data(args, out_data.data(), out_data.size(), "json"); - }); -Action a_encode_text_archive( - "encode-text-archive", "\ - encode-text-archive [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ - Encode a text archive. Currently only supports GC and Xbox format.\n", - +[](Arguments& args) { - const string& input_filename = args.get(1, false); - const string& output_filename = args.get(2, false); - bool is_sjis = args.get("japanese"); - - auto json = JSON::parse(read_input_data(args)); - BinaryTextAndKeyboardsSet a(json); - auto result = a.serialize(args.get("big-endian"), is_sjis); - if (output_filename.empty()) { - if (input_filename.empty() || (input_filename == "-")) { - throw runtime_error("encoded text archive cannot be written to stdout"); - } - save_file(string_printf("%s.pr2", input_filename.c_str()), result.first); - save_file(string_printf("%s.pr3", input_filename.c_str()), result.second); - } else if (output_filename == "-") { - throw runtime_error("encoded text archive cannot be written to stdout"); - } else { - string out_filename = output_filename; - if (ends_with(out_filename, ".pr2")) { - save_file(out_filename, result.first); - out_filename[out_filename.size() - 1] = '3'; - save_file(out_filename, result.second); - } else { - save_file(out_filename + ".pr2", result.first); - save_file(out_filename + ".pr3", result.second); - } - } - }); - -Action a_decode_unicode_text_set( - "decode-unicode-text-set", nullptr, +[](Arguments& args) { - UnicodeTextSet uts(read_input_data(args)); - JSON j = uts.json(); - string out_data = j.serialize(JSON::SerializeOption::FORMAT | JSON::SerializeOption::ESCAPE_CONTROLS_ONLY); - write_output_data(args, out_data.data(), out_data.size(), "json"); - }); -Action a_encode_unicode_text_set( - "encode-unicode-text-set", "\ - decode-unicode-text-set [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ - encode-unicode-text-set [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ - Decode a Unicode text set (e.g. unitxt_e.prs) to JSON for easy editing, or\n\ - encode a JSON file to a Unicode text set.\n", - +[](Arguments& args) { - UnicodeTextSet uts(JSON::parse(read_input_data(args))); - string encoded = uts.serialize(); - write_output_data(args, encoded.data(), encoded.size(), "prs"); - }); - -Action a_decode_word_select_set( - "decode-word-select-set", "\ - decode-word-select-set [INPUT-FILENAME]\n\ - Decode a Word Select data file and print all the tokens. A version option\n\ - (e.g. --gc-ep3) is required. If the Word Select set is for PC or BB, the\n\ - --unitxt option is also required, and must point to a unitxt file in prs\n\ - or JSON format. For PC (V2), the unitxt_e.prs file should be used; for BB,\n\ - the unitxt_ws_e.prs file should be used.\n", - +[](Arguments& args) { - auto version = get_cli_version(args); - - string unitxt_filename = args.get("unitxt"); - const vector* unitxt_collection; - if (!unitxt_filename.empty()) { - unique_ptr uts; - if (ends_with(unitxt_filename, ".prs")) { - uts = make_unique(load_file(unitxt_filename)); - } else if (ends_with(unitxt_filename, ".json")) { - uts = make_unique(JSON::parse(load_file(unitxt_filename))); - } else { - throw runtime_error("unitxt filename must end in .prs or .json"); - } - unitxt_collection = &uts->get((version == Version::BB_V4) ? 0 : 35); - } else { - unitxt_collection = nullptr; - } - - WordSelectSet ws(read_input_data(args), version, unitxt_collection, args.get("japanese")); - ws.print(stdout); - }); -Action a_print_word_select_table( - "print-word-select-table", "\ - print-word-select-table\n\ - Print the Word Select token translation table. If a version option is\n\ - given, prints the table sorted by token ID for that version. If no version\n\ - option is given, prints the token table sorted by canonical name.\n", - +[](Arguments& args) { - auto s = make_shared(get_config_filename(args)); - s->load_patch_indexes(false); - s->load_text_index(false); - s->load_word_select_table(false); - Version v; - try { - v = get_cli_version(args); - } catch (const runtime_error&) { - v = Version::UNKNOWN; - } - if (v != Version::UNKNOWN) { - s->word_select_table->print_index(stdout, v); - } else { - s->word_select_table->print(stdout); - } - }); - -Action a_cat_client( - "cat-client", "\ - cat-client ADDR:PORT\n\ - Connect to the given server and simulate a PSO client. newserv will then\n\ - print all the received commands to stdout, and forward any commands typed\n\ - into stdin to the remote server. It is assumed that the input and output\n\ - are terminals, so all commands are hex-encoded. The --patch, --dc, --pc,\n\ - --gc, and --bb options can be used to select the command format and\n\ - encryption. If --bb is used, the --key=KEY-NAME option is also required (as\n\ - in decrypt-data above).\n", - +[](Arguments& args) { - auto version = get_cli_version(args); - shared_ptr key; - if (uses_v4_encryption(version)) { - string key_file_name = args.get("key"); - if (key_file_name.empty()) { - throw runtime_error("a key filename is required for BB client emulation"); - } - key = make_shared( - load_object_file("system/blueburst/keys/" + key_file_name + ".nsk")); - } - shared_ptr base(event_base_new(), event_base_free); - auto cat_client_remote = make_sockaddr_storage(parse_netloc(args.get(1))).first; - CatSession session(base, cat_client_remote, get_cli_version(args), key); - event_base_dispatch(base.get()); - }); - -Action a_convert_rare_item_set( - "convert-rare-item-set", "\ - convert-rare-item-set INPUT-FILENAME [OUTPUT-FILENAME] [OPTIONS]\n\ - If OUTPUT-FILENAME is not given, print the contents of a rare item table in\n\ - a human-readable format. Otherwise, convert the input rare item set to a\n\ - different format and write it to OUTPUT-FILENAME. Both filenames must end\n\ - in one of the following extensions:\n\ - .json (newserv JSON rare item table)\n\ - .gsl (PSO BB little-endian GSL archive)\n\ - .gslb (PSO GC big-endian GSL archive)\n\ - .afs (PSO V2 little-endian AFS archive)\n\ - .rel (Schtserv rare table; cannot be used in output filename)\n\ - If the --multiply=X option is given, multiplies all drop rates by X (given\n\ - as a decimal value).\n", - +[](Arguments& args) { - auto version = get_cli_version(args); - - double rate_factor = args.get("multiply", 1.0); - auto s = make_shared(get_config_filename(args)); - s->load_config_early(); - s->load_patch_indexes(false); - s->load_text_index(false); - s->load_item_definitions(false); - s->load_item_name_indexes(false); - - string input_filename = args.get(1, false); - if (input_filename.empty() || (input_filename == "-")) { - throw runtime_error("input filename must be given"); - } - - auto data = make_shared(read_input_data(args)); - shared_ptr rs; - if (ends_with(input_filename, ".json")) { - rs = make_shared(JSON::parse(*data), s->item_name_index(version)); - } else if (ends_with(input_filename, ".gsl")) { - rs = make_shared(GSLArchive(data, false), false); - } else if (ends_with(input_filename, ".gslb")) { - rs = make_shared(GSLArchive(data, true), true); - } else if (ends_with(input_filename, ".afs")) { - rs = make_shared(AFSArchive(data), is_v1(version)); - } else if (ends_with(input_filename, ".rel")) { - rs = make_shared(*data, true); - } else { - throw runtime_error("cannot determine input format; use a filename ending with .json, .gsl, .gslb, .afs, or .rel"); - } - - if (rate_factor != 1.0) { - rs->multiply_all_rates(rate_factor); - } - - string output_filename = args.get(2, false); - if (output_filename.empty() || (output_filename == "-")) { - rs->print_all_collections(stdout, s->item_name_index(version)); - } else if (ends_with(output_filename, ".json")) { - auto json = rs->json(s->item_name_index(version)); - string data = json.serialize(JSON::SerializeOption::FORMAT | JSON::SerializeOption::HEX_INTEGERS | JSON::SerializeOption::SORT_DICT_KEYS); - write_output_data(args, data.data(), data.size(), nullptr); - } else if (ends_with(output_filename, ".gsl")) { - string data = rs->serialize_gsl(args.get("big-endian")); - write_output_data(args, data.data(), data.size(), nullptr); - } else if (ends_with(output_filename, ".gslb")) { - string data = rs->serialize_gsl(true); - write_output_data(args, data.data(), data.size(), nullptr); - } else if (ends_with(output_filename, ".afs")) { - bool is_v1 = ::is_v1(get_cli_version(args, Version::GC_V3)); - string data = rs->serialize_afs(is_v1); - write_output_data(args, data.data(), data.size(), nullptr); - } else { - throw runtime_error("cannot determine output format; use a filename ending with .json, .gsl, .gslb, or .afs"); - } - }); -Action a_convert_common_item_set( - "convert-common-item-set", "\ - convert-common-item-set INPUT-FILENAME [OUTPUT-FILENAME]\n\ - Convert the input rare item set to a JSON representation and write it to\n\ - OUTPUT-FILENAME or stdout. The input filename must end in one of the\n\ - following extensions:\n\ - .json (newserv JSON common item table)\n\ - .gsl (PSO BB little-endian GSL archive)\n\ - .gslb (PSO GC big-endian GSL archive)\n", - +[](Arguments& args) { - string input_filename = args.get(1, false); - if (input_filename.empty() || (input_filename == "-")) { - throw runtime_error("input filename must be given"); - } - - auto data = make_shared(read_input_data(args)); - shared_ptr cs; - if (ends_with(input_filename, ".json")) { - cs = make_shared(JSON::parse(*data)); - } else if (ends_with(input_filename, ".gsl")) { - cs = make_shared(data, args.get("big-endian")); - } else if (ends_with(input_filename, ".gslb")) { - cs = make_shared(data, true); - } else { - throw runtime_error("cannot determine input format; use a filename ending with .json, .gsl, .gslb, or .afs"); - } - - const string& output_filename = args.get(2, false); - if (output_filename.empty()) { - cs->print(stdout); - } else { - auto json = cs->json(); - string json_data = json.serialize(JSON::SerializeOption::FORMAT | JSON::SerializeOption::HEX_INTEGERS | JSON::SerializeOption::SORT_DICT_KEYS); - write_output_data(args, json_data.data(), json_data.size(), "json"); - } - }); - -Action a_describe_item( - "describe-item", "\ - describe-item DATA-OR-DESCRIPTION\n\ - Describe an item. The argument may be the item\'s raw hex code or a textual\n\ - description of the item. If the description contains spaces, it must be\n\ - quoted, such as \"L&K14 COMBAT +10 0/10/15/0/35\".\n", - +[](Arguments& args) { - string description = args.get(1); - auto version = get_cli_version(args); - - auto s = make_shared(get_config_filename(args)); - s->load_config_early(); - s->load_patch_indexes(false); - s->load_text_index(false); - s->load_item_definitions(false); - s->load_item_name_indexes(false); - auto name_index = s->item_name_index(version); - - ItemData item = name_index->parse_item_description(description); - - if (args.get("decode")) { - item.decode_for_version(version); - } - - string desc = name_index->describe_item(item); - string desc_colored = name_index->describe_item(item, true); - - log_info("Data (decoded): %02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX -------- %02hhX%02hhX%02hhX%02hhX", - item.data1[0], item.data1[1], item.data1[2], item.data1[3], - item.data1[4], item.data1[5], item.data1[6], item.data1[7], - item.data1[8], item.data1[9], item.data1[10], item.data1[11], - item.data2[0], item.data2[1], item.data2[2], item.data2[3]); - - ItemData item_v2 = item; - item_v2.encode_for_version(Version::PC_V2, s->item_parameter_table_for_encode(Version::PC_V2)); - ItemData item_v2_decoded = item_v2; - item_v2_decoded.decode_for_version(Version::PC_V2); - - log_info("Data (V2-encoded): %02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX -------- %02hhX%02hhX%02hhX%02hhX", - item_v2.data1[0], item_v2.data1[1], item_v2.data1[2], item_v2.data1[3], - item_v2.data1[4], item_v2.data1[5], item_v2.data1[6], item_v2.data1[7], - item_v2.data1[8], item_v2.data1[9], item_v2.data1[10], item_v2.data1[11], - item_v2.data2[0], item_v2.data2[1], item_v2.data2[2], item_v2.data2[3]); - if (item_v2_decoded != item) { - log_warning("V2-decoded data does not match original data"); - log_warning("Data (V2-decoded): %02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX -------- %02hhX%02hhX%02hhX%02hhX", - item_v2_decoded.data1[0], item_v2_decoded.data1[1], item_v2_decoded.data1[2], item_v2_decoded.data1[3], - item_v2_decoded.data1[4], item_v2_decoded.data1[5], item_v2_decoded.data1[6], item_v2_decoded.data1[7], - item_v2_decoded.data1[8], item_v2_decoded.data1[9], item_v2_decoded.data1[10], item_v2_decoded.data1[11], - item_v2_decoded.data2[0], item_v2_decoded.data2[1], item_v2_decoded.data2[2], item_v2_decoded.data2[3]); - } - - ItemData item_gc = item; - item_gc.encode_for_version(Version::GC_V3, s->item_parameter_table_for_encode(Version::GC_V3)); - ItemData item_gc_decoded = item_gc; - item_gc_decoded.decode_for_version(Version::GC_V3); - - log_info("Data (GC-encoded): %02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX -------- %02hhX%02hhX%02hhX%02hhX", - item_gc.data1[0], item_gc.data1[1], item_gc.data1[2], item_gc.data1[3], - item_gc.data1[4], item_gc.data1[5], item_gc.data1[6], item_gc.data1[7], - item_gc.data1[8], item_gc.data1[9], item_gc.data1[10], item_gc.data1[11], - item_gc.data2[0], item_gc.data2[1], item_gc.data2[2], item_gc.data2[3]); - if (item_gc_decoded != item) { - log_warning("GC-decoded data does not match original data"); - log_warning("Data (GC-decoded): %02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX -------- %02hhX%02hhX%02hhX%02hhX", - item_gc_decoded.data1[0], item_gc_decoded.data1[1], item_gc_decoded.data1[2], item_gc_decoded.data1[3], - item_gc_decoded.data1[4], item_gc_decoded.data1[5], item_gc_decoded.data1[6], item_gc_decoded.data1[7], - item_gc_decoded.data1[8], item_gc_decoded.data1[9], item_gc_decoded.data1[10], item_gc_decoded.data1[11], - item_gc_decoded.data2[0], item_gc_decoded.data2[1], item_gc_decoded.data2[2], item_gc_decoded.data2[3]); - } - - log_info("Description: %s", desc.c_str()); - log_info("Description (in-game): %s", desc_colored.c_str()); - - size_t purchase_price = s->item_parameter_table(Version::BB_V4)->price_for_item(item); - size_t sale_price = purchase_price >> 3; - log_info("Purchase price: %zu; sale price: %zu", purchase_price, sale_price); - }); - -Action a_name_all_items( - "name-all-items", nullptr, +[](Arguments& args) { - auto s = make_shared(get_config_filename(args)); - s->load_config_early(); - s->load_patch_indexes(false); - s->load_text_index(false); - s->load_item_definitions(false); - s->load_item_name_indexes(false); - s->load_config_early(); - - set all_primary_identifiers; - for (const auto& index : s->item_name_indexes) { - if (index) { - for (const auto& it : index->all_by_primary_identifier()) { - all_primary_identifiers.emplace(it.first); - } - } - } - - fprintf(stderr, "IDENT :"); - for (size_t v_s = 0; v_s < NUM_VERSIONS; v_s++) { - Version version = static_cast(v_s); - const auto& index = s->item_name_indexes.at(v_s); - if (index) { - fprintf(stderr, " %30s ", name_for_enum(version)); - } - } - fputc('\n', stderr); - - for (uint32_t primary_identifier : all_primary_identifiers) { - fprintf(stderr, "%08" PRIX32 ":", primary_identifier); - for (size_t v_s = 0; v_s < NUM_VERSIONS; v_s++) { - const auto& index = s->item_name_indexes.at(v_s); - if (index) { - Version version = static_cast(v_s); - auto pmt = s->item_parameter_table(version); - ItemData item = ItemData::from_primary_identifier(*s->item_stack_limits(version), primary_identifier); - string name = index->describe_item(item); - try { - bool is_rare = pmt->is_item_rare(item); - fprintf(stderr, " %30s%s", name.c_str(), is_rare ? " (*)" : " "); - } catch (const out_of_range&) { - fprintf(stderr, " "); - } - } - } - fputc('\n', stderr); - } - }); - -Action a_print_item_parameter_tables( - "print-item-tables", nullptr, +[](Arguments& args) { - auto s = make_shared(get_config_filename(args)); - s->load_patch_indexes(false); - s->load_text_index(false); - s->load_item_definitions(false); - s->load_item_name_indexes(false); - for (size_t v_s = 0; v_s < NUM_VERSIONS; v_s++) { - const auto& index = s->item_name_indexes.at(v_s); - if (index) { - Version v = static_cast(v_s); - fprintf(stdout, "======== %s\n", name_for_enum(v)); - index->print_table(stdout); - } - } - }); - -Action a_show_ep3_cards( - "show-ep3-cards", "\ - show-ep3-cards\n\ - Print the Episode 3 card definitions from the system/ep3 directory in a\n\ - human-readable format.\n", - +[](Arguments& args) { - bool one_line = args.get("one-line"); - - auto s = make_shared(get_config_filename(args)); - s->load_ep3_cards(false); - - unique_ptr text_english; - try { - JSON json = JSON::parse(load_file("system/ep3/text-english.json")); - text_english = make_unique(json); - } catch (const exception& e) { - } - - auto card_ids = s->ep3_card_index->all_ids(); - log_info("%zu card definitions", card_ids.size()); - for (uint32_t card_id : card_ids) { - auto entry = s->ep3_card_index->definition_for_id(card_id); - string def_str = entry->def.str(one_line, text_english.get()); - if (one_line) { - fprintf(stdout, "%s\n", def_str.c_str()); - } else { - fprintf(stdout, "%s\n", def_str.c_str()); - if (!entry->debug_tags.empty()) { - string tags = join(entry->debug_tags, ", "); - fprintf(stdout, " Tags: %s\n", tags.c_str()); - } - if (!entry->dice_caption.empty()) { - fprintf(stdout, " Dice caption: %s\n", entry->dice_caption.c_str()); - } - if (!entry->dice_caption.empty()) { - fprintf(stdout, " Dice text: %s\n", entry->dice_text.c_str()); - } - if (!entry->text.empty()) { - string text = str_replace_all(entry->text, "\n", "\n "); - strip_trailing_whitespace(text); - fprintf(stdout, " Text:\n %s\n", text.c_str()); - } - fputc('\n', stdout); - } - } - }); - -Action a_generate_ep3_cards_html( - "generate-ep3-cards-html", "\ - generate-ep3-cards-html [--ep3-nte] [--threads=N]\n\ - Generate an HTML file describing all Episode 3 card definitions from the\n\ - system/ep3 directory. If --ep3-nte is given, use the Trial Edition card\n\ - definitions instead.\n", - +[](Arguments& args) { - size_t num_threads = args.get("threads", 0); - - bool is_nte = (get_cli_version(args, Version::GC_EP3) == Version::GC_EP3_NTE); - - auto s = make_shared(get_config_filename(args)); - s->load_patch_indexes(false); - s->load_text_index(false); - s->load_ep3_cards(false); - - shared_ptr text_english; - try { - text_english = s->text_index->get(Version::GC_EP3, 1); - } catch (const out_of_range&) { - } - - struct CardInfo { - shared_ptr ce; - string small_filename; - string medium_filename; - string large_filename; - string small_data_url; - string medium_data_url; - string large_data_url; - - bool is_empty() const { - return (this->ce == nullptr) && this->small_data_url.empty() && this->medium_data_url.empty() && this->large_data_url.empty(); - } - }; - auto card_index = is_nte ? s->ep3_card_index_trial : s->ep3_card_index; - vector infos; - for (uint32_t card_id : card_index->all_ids()) { - if (infos.size() <= card_id) { - infos.resize(card_id + 1); - } - infos[card_id].ce = card_index->definition_for_id(card_id); - } - bool show_large_column = false; - bool show_medium_column = false; - bool show_small_column = false; - for (const auto& filename : list_directory_sorted("system/ep3/cardtex")) { - if ((filename[0] == 'C' || filename[0] == 'M' || filename[0] == 'L') && (filename[1] == '_')) { - size_t card_id = stoull(filename.substr(2, 3), nullptr, 10); - if (infos.size() <= card_id) { - infos.resize(card_id + 1); - } - auto& info = infos[card_id]; - if (filename[0] == 'C') { - info.large_filename = "system/ep3/cardtex/" + filename; - show_large_column = true; - } else if (filename[0] == 'L') { - info.medium_filename = "system/ep3/cardtex/" + filename; - show_medium_column = true; - } else if (filename[0] == 'M') { - info.small_filename = "system/ep3/cardtex/" + filename; - show_small_column = true; - } - } - } - - parallel_range([&](uint32_t index, size_t) -> bool { - auto& info = infos[index]; - if (!info.large_filename.empty()) { - Image img(info.large_filename); - Image cropped(512, 399); - cropped.blit(img, 0, 0, 512, 399, 0, 0); - info.large_data_url = cropped.png_data_url(); - } - if (!info.medium_filename.empty()) { - Image img(info.medium_filename); - Image cropped(184, 144); - cropped.blit(img, 0, 0, 184, 144, 0, 0); - info.medium_data_url = cropped.png_data_url(); - } - if (!info.small_filename.empty()) { - Image img(info.small_filename); - Image cropped(58, 43); - cropped.blit(img, 0, 0, 58, 43, 0, 0); - info.small_data_url = cropped.png_data_url(); - } - return false; - }, - 0, infos.size(), num_threads); - - deque blocks; - blocks.emplace_back("Phantasy Star Online Episode III cards"); - blocks.emplace_back("
Legend:
Card has no definition and is obviously incomplete
Card is unobtainable in random draws but may be a quest or event reward
Card is obtainable in random draws


"); - blocks.emplace_back(""); - if (show_small_column) { - blocks.emplace_back(""); - } - if (show_medium_column) { - blocks.emplace_back(""); - } - if (show_large_column) { - blocks.emplace_back(""); - } - blocks.emplace_back(""); - for (size_t card_id = 0; card_id < infos.size(); card_id++) { - const auto& entry = infos[card_id]; - if (entry.is_empty()) { - continue; - } - - const char* background_color; - if (!entry.ce) { - background_color = "#663333"; - } else if (entry.ce->def.cannot_drop || - ((entry.ce->def.rank == Episode3::CardRank::D1) || (entry.ce->def.rank == Episode3::CardRank::D2) || (entry.ce->def.rank == Episode3::CardRank::D3)) || - ((entry.ce->def.card_class() == Episode3::CardClass::BOSS_ATTACK_ACTION) || (entry.ce->def.card_class() == Episode3::CardClass::BOSS_TECH)) || - ((entry.ce->def.drop_rates[0] == 6) && (entry.ce->def.drop_rates[1] == 6))) { - background_color = "#336633"; - } else { - background_color = "#333333"; - } - - blocks.emplace_back(string_printf("", background_color)); - blocks.emplace_back(string_printf("", card_id)); - if (show_small_column) { - blocks.emplace_back(""); - } - if (show_medium_column) { - blocks.emplace_back(""); - } - if (show_large_column) { - blocks.emplace_back(""); - } - blocks.emplace_back(""); - } - blocks.emplace_back("
IDSmallMediumLargeTextDisassembly
%04zX
"); - if (!entry.small_data_url.empty()) { - blocks.emplace_back(""); - } - blocks.emplace_back(""); - if (!entry.medium_data_url.empty()) { - blocks.emplace_back(""); - } - blocks.emplace_back(""); - if (!entry.large_data_url.empty()) { - blocks.emplace_back(""); - } - blocks.emplace_back(""); - if (entry.ce) { - blocks.emplace_back("
");
-          blocks.emplace_back(entry.ce->text);
-          blocks.emplace_back("
");
-          blocks.emplace_back(entry.ce->def.str(false, text_english.get()));
-          blocks.emplace_back("
"); - } else { - blocks.emplace_back("
Definition is missing
"); - } - blocks.emplace_back("
"); - - save_file("cards.html", join(blocks, "")); - }); - -Action a_show_ep3_maps( - "show-ep3-maps", "\ - show-ep3-maps\n\ - Print the Episode 3 maps from the system/ep3 directory in a (sort of)\n\ - human-readable format.\n", - +[](Arguments& args) { - config_log.info("Collecting Episode 3 data"); - - auto s = make_shared(get_config_filename(args)); - s->load_ep3_cards(false); - s->load_ep3_maps(false); - - auto map_ids = s->ep3_map_index->all_numbers(); - log_info("%zu maps", map_ids.size()); - for (uint32_t map_id : map_ids) { - auto map = s->ep3_map_index->for_number(map_id); - const auto& vms = map->all_versions(); - for (size_t language = 0; language < vms.size(); language++) { - if (!vms[language]) { - continue; - } - string map_s = vms[language]->map->str(s->ep3_card_index.get(), language); - fprintf(stdout, "(%c) %s\n", char_for_language_code(language), map_s.c_str()); - } - } - }); - -Action a_show_battle_params( - "show-battle-params", "\ - show-battle-params\n\ - Print the Blue Burst battle parameters from the system/blueburst directory\n\ - in a human-readable format.\n", - +[](Arguments& args) { - auto s = make_shared(get_config_filename(args)); - s->load_patch_indexes(false); - s->load_battle_params(false); - - fprintf(stdout, "Episode 1 multi\n"); - s->battle_params->get_table(false, Episode::EP1).print(stdout); - fprintf(stdout, "Episode 1 solo\n"); - s->battle_params->get_table(true, Episode::EP1).print(stdout); - fprintf(stdout, "Episode 2 multi\n"); - s->battle_params->get_table(false, Episode::EP2).print(stdout); - fprintf(stdout, "Episode 2 solo\n"); - s->battle_params->get_table(true, Episode::EP2).print(stdout); - fprintf(stdout, "Episode 4 multi\n"); - s->battle_params->get_table(false, Episode::EP4).print(stdout); - fprintf(stdout, "Episode 4 solo\n"); - s->battle_params->get_table(true, Episode::EP4).print(stdout); - }); - -Action a_find_rare_enemy_seeds( - "find-rare-enemy-seeds", "\ - find-rare-enemy-seeds OPTIONS...\n\ - Search all possible rare seeds to find those that produce one or more rare\n\ - enemies in any set of variations. A version option (e.g. --gc) is required;\n\ - an episode option (--ep1, --ep2, or --ep4) is also required. A difficulty\n\ - option (--normal, --hard, --very-hard, or --ultimate) may be given; this\n\ - affects which rare rates from config.json are used if --bb was given.\n\ - Similarly, --battle, --challenge, or --solo may also be given; this affects\n\ - which variations are used on all versions and which rare rates to use for\n\ - BB. --threads=COUNT controls the number of threads to use for the search\n\ - by default, one thread per CPU core is used. --min-count specifies how many\n\ - rare enemies must be found to output the seed. Finally, --quest=NAME may be\n\ - given to use that quest\'s map instead of the free-roam maps.\n", - +[](Arguments& args) { - auto version = get_cli_version(args); - auto episode = get_cli_episode(args); - auto difficulty = get_cli_difficulty(args); - auto mode = get_cli_game_mode(args); - size_t num_threads = args.get("threads", 0); - size_t min_count = args.get("min-count", 1); - string quest_name = args.get("quest", false); - - auto s = make_shared(get_config_filename(args)); - shared_ptr vq; - if (!quest_name.empty()) { - s->load_config_early(); - s->load_quest_index(false); - auto q = s->quest_index(version)->get(quest_name); - if (!q) { - throw runtime_error("quest does not exist"); - } - vq = q->version(version, 1); - if (!vq) { - throw runtime_error("quest version does not exist"); - } - } else if (version == Version::BB_V4) { - s->load_config_early(); - } else if (version == Version::PC_V2) { - s->load_patch_indexes(false); - } else { - s->clear_map_file_caches(); - } - - shared_ptr rare_rates; - if (version != Version::BB_V4) { - rare_rates = Map::DEFAULT_RARE_ENEMIES; - } else if (mode == GameMode::CHALLENGE) { - rare_rates = s->rare_enemy_rates_challenge; - } else { - rare_rates = s->rare_enemy_rates_by_difficulty[difficulty]; - } - - mutex output_lock; - auto thread_fn = [&](uint64_t seed, size_t) -> bool { - auto random_crypt = make_shared(seed); - parray variations; - - shared_ptr map; - if (vq) { - if (!vq->dat_contents_decompressed) { - throw runtime_error("quest does not have DAT data"); - } - map = Lobby::load_maps( - version, episode, difficulty, 0, 0, rare_rates, seed, random_crypt, vq->dat_contents_decompressed); - - } else { - generate_variations_deprecated(variations, random_crypt, version, episode, (mode == GameMode::SOLO)); - map = Lobby::load_maps( - version, - episode, - mode, - difficulty, - 0, - 0, - s->set_data_table(version, episode, mode, difficulty), - bind(&ServerState::load_map_file, s.get(), placeholders::_1, placeholders::_2), - rare_rates, - seed, - random_crypt, - variations); - } - - vector rare_indexes; - for (size_t z = 0; z < map->enemies.size(); z++) { - if (enemy_type_is_rare(map->enemies[z].type)) { - rare_indexes.emplace_back(z); - } - } - - if (rare_indexes.size() >= min_count) { - lock_guard g(output_lock); - fprintf(stdout, "%08" PRIX64 ":", seed); - for (size_t index : rare_indexes) { - fprintf(stdout, " E-%zX:%s", index, name_for_enum(map->enemies[index].type)); - } - fprintf(stdout, "\n"); - } - - return false; - }; - - parallel_range(thread_fn, 0, 0x100000000, num_threads, nullptr); - }); - -Action a_load_maps_test( - "load-maps-test", nullptr, +[](Arguments& args) { - using SDT = SetDataTable; - auto s = make_shared(get_config_filename(args)); - s->load_config_early(); - s->clear_map_file_caches(); - s->load_patch_indexes(false); - s->load_set_data_tables(false); - s->load_quest_index(false); - for (size_t v_s = NUM_PATCH_VERSIONS; v_s < NUM_VERSIONS; v_s++) { - Version v = static_cast(v_s); - if (is_ep3(v)) { - continue; - } - const array episodes = {Episode::EP1, Episode::EP2, Episode::EP4}; - for (Episode episode : episodes) { - if (episode == Episode::EP4 && !is_v4(v)) { - continue; - } - if (episode == Episode::EP2 && is_v1_or_v2(v) && (v != Version::GC_NTE)) { - continue; - } - const array modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO}; - for (GameMode mode : modes) { - if ((mode == GameMode::BATTLE || mode == GameMode::CHALLENGE) && is_v1(v)) { - continue; - } - if (mode == GameMode::SOLO && !is_v4(v)) { - continue; - } - for (uint8_t difficulty = 0; difficulty < 4; difficulty++) { - if (difficulty == 3 && is_v1(v)) { - continue; - } - auto sdt = s->set_data_table(v, episode, mode, difficulty); - for (uint8_t floor = 0; floor < 0x12; floor++) { - auto variation_maxes = sdt->num_free_roam_variations_for_floor(episode, mode == GameMode::SOLO, floor); - for (size_t var1 = 0; var1 < variation_maxes.first; var1++) { - for (size_t var2 = 0; var2 < variation_maxes.second; var2++) { - auto enemies_filename = sdt->map_filename_for_variation( - floor, var1, var2, episode, mode, SDT::FilenameType::ENEMIES); - auto objects_filename = sdt->map_filename_for_variation( - floor, var1, var2, episode, mode, SDT::FilenameType::OBJECTS); - auto events_filename = sdt->map_filename_for_variation( - floor, var1, var2, episode, mode, SDT::FilenameType::EVENTS); - - fprintf(stderr, "... %s %s %s %s %02hhX %zX %zX", - name_for_enum(v), name_for_episode(episode), name_for_mode(mode), name_for_difficulty(difficulty), floor, var1, var2); - auto map = make_shared(v, 0, 0, nullptr); - if (!enemies_filename.empty()) { - fprintf(stderr, " [%s => ", enemies_filename.c_str()); - auto map_data = s->load_map_file(v, enemies_filename); - if (map_data) { - map->add_enemies_from_map_data( - episode, difficulty, 0, 0, map_data->data(), map_data->size(), Map::DEFAULT_RARE_ENEMIES); - fprintf(stderr, "%zu enemies, %zu sets]", map->enemies.size(), map->enemy_set_flags.size()); - } else { - fprintf(stderr, "__MISSING__]"); - } - } - if (!objects_filename.empty()) { - fprintf(stderr, " [%s => ", objects_filename.c_str()); - auto map_data = s->load_map_file(v, objects_filename); - if (map_data) { - map->add_objects_from_map_data(floor, map_data->data(), map_data->size()); - fprintf(stderr, "%zu objects]", map->objects.size()); - } else { - fprintf(stderr, "__MISSING__]"); - } - } - if (!events_filename.empty()) { - fprintf(stderr, " [%s => ", events_filename.c_str()); - auto map_data = s->load_map_file(v, events_filename); - if (map_data) { - map->add_events_from_map_data(floor, map_data->data(), map_data->size()); - fprintf(stderr, "%zu events, %zu action bytes]", map->events.size(), map->event_action_stream.size()); - } else { - fprintf(stderr, "__MISSING__]"); - } - } - fputc('\n', stderr); - } - } - } - } - } - } - } - - for (const auto& q_it : s->default_quest_index->quests_by_number) { - for (const auto& vq_it : q_it.second->versions) { - auto vq = vq_it.second; - shared_ptr map = Lobby::load_maps( - vq->version, - vq->episode, - 0, - 0, - 0, - Map::DEFAULT_RARE_ENEMIES, - 0, - nullptr, - vq->dat_contents_decompressed); - fprintf(stderr, "... %" PRIu32 " (%s) %s %s %s => %zu enemies (%zu sets), %zu objects, %zu events\n", - vq->quest_number, - vq->name.c_str(), - name_for_episode(vq->episode), - name_for_enum(vq->version), - name_for_language_code(vq->language), - map->enemies.size(), - map->enemy_set_flags.size(), - map->objects.size(), - map->events.size()); - } - } - }); - -Action a_parse_object_graph( - "parse-object-graph", nullptr, +[](Arguments& args) { - uint32_t root_object_address = args.get("root", Arguments::IntFormat::HEX); - string data = read_input_data(args); - PSOGCObjectGraph g(data, root_object_address); - g.print(stdout); - }); - -Action a_generate_dc_serial_number( - "generate-dc-serial-number", "\ - generate-dc-serial-number DOMAIN SUBDOMAIN\n\ - Generate a PSO DC serial number. DOMAIN should be 0 for Japanese, 1 for\n\ - USA, or 2 for Europe. SUBDOMAIN should be 0 for v1, or 1 for v2.\n", - +[](Arguments& args) { - uint8_t domain = args.get(1); - uint8_t subdomain = args.get(2); - string serial_number = generate_dc_serial_number(domain, subdomain); - fprintf(stdout, "%s\n", serial_number.c_str()); - }); -Action a_generate_all_dc_serial_numbers( - "generate-all-dc-serial-numbers", "\ - generate-all-dc-serial-numbers\n\ - Generate all possible PSO DC serial numbers.\n", - +[](Arguments& args) { - size_t num_threads = args.get("threads", 0); - - auto serial_numbers = generate_all_dc_serial_numbers(); - fprintf(stdout, "%zu (0x%zX) serial numbers found\n", serial_numbers.size(), serial_numbers.size()); - for (const auto& it : serial_numbers) { - fprintf(stdout, "Valid serial number: %08" PRIX32, it.first); - for (uint8_t where : it.second) { - fprintf(stdout, " (domain=%hhu, subdomain=%hhu)", - static_cast((where >> 2) & 3), - static_cast(where & 3)); - } - fputc('\n', stdout); - } - - atomic num_valid_serial_numbers = 0; - mutex output_lock; - auto thread_fn = [&](uint64_t serial_number, size_t) -> bool { - for (uint8_t domain = 0; domain < 3; domain++) { - for (uint8_t subdomain = 0; subdomain < 3; subdomain++) { - if (dc_serial_number_is_valid_fast(serial_number, domain, subdomain)) { - num_valid_serial_numbers++; - lock_guard g(output_lock); - fprintf(stdout, "Valid serial number: %08" PRIX64 " (domain=%hhu, subdomain=%hhu)\n", serial_number, domain, subdomain); - } - } - } - return false; - }; - auto progress_fn = [&](uint64_t, uint64_t, uint64_t current_value, uint64_t) -> void { - uint64_t num_found = num_valid_serial_numbers.load(); - fprintf(stderr, "... %08" PRIX64 " %" PRId64 " (0x%" PRIX64 ") found\r", - current_value, num_found, num_found); - }; - parallel_range(thread_fn, 0, 0x100000000, num_threads, progress_fn); - }); -Action a_inspect_dc_serial_number( - "inspect-dc-serial-number", "\ - inspect-dc-serial-number SERIAL-NUMBER\n\ - Show which domain and subdomain the serial number belongs to. (As with\n\ - generate-dc-serial-number, described above, this will tell you which PSO\n\ - version it is valid for.)\n", - +[](Arguments& args) { - const string& serial_number_str = args.get(1, false); - if (serial_number_str.empty()) { - throw invalid_argument("no serial number given"); - } - size_t num_valid_subdomains = 0; - for (uint8_t domain = 0; domain < 3; domain++) { - for (uint8_t subdomain = 0; subdomain < 3; subdomain++) { - if (dc_serial_number_is_valid_fast(serial_number_str, domain, subdomain)) { - fprintf(stdout, "%s is valid in domain %hhu subdomain %hhu\n", serial_number_str.c_str(), domain, subdomain); - num_valid_subdomains++; - } - } - } - if (num_valid_subdomains == 0) { - fprintf(stdout, "%s is not valid in any domain\n", serial_number_str.c_str()); - } - }); -Action a_dc_serial_number_speed_test( - "dc-serial-number-speed-test", "\ - dc-serial-number-speed-test\n\ - Run a speed test of the two DC serial number validation functions.\n", - +[](Arguments& args) { - const string& seed = args.get("seed"); - if (seed.empty()) { - dc_serial_number_speed_test(); - } else { - dc_serial_number_speed_test(stoul(seed, nullptr, 16)); - } - }); - -Action a_address_translator( - "address-translator", nullptr, +[](Arguments& args) { - const string& dir = args.get(1, false); - if (dir.empty() || (dir == "-")) { - throw invalid_argument("a directory name is required"); - } - run_address_translator(dir, args.get(2, false), args.get(3, false)); - }); - -Action a_diff_dol_files( - "diff-dol-files", nullptr, +[](Arguments& args) { - const string& a_filename = args.get(1); - const string& b_filename = args.get(2); - auto result = diff_dol_files(a_filename, b_filename); - for (const auto& it : result) { - string data = format_data_string(it.second, nullptr, FormatDataFlags::HEX_ONLY); - fprintf(stdout, "%08" PRIX32 " %s\n", it.first, data.c_str()); - } - }); - -Action a_generate_hangame_creds( - "generate-hangame-creds", nullptr, +[](Arguments& args) { - const string& user_id = args.get(1); - const string& token = args.get(2); - const string& unused = args.get(3, false); - string hex = format_data_string(encode_psobb_hangame_credentials(user_id, token, unused)); - fprintf(stdout, "psobb.exe 1196310600 %s\n", hex.c_str()); - }); - -Action a_format_ep3_battle_record( - "format-ep3-battle-record", nullptr, +[](Arguments& args) { - string data = read_input_data(args); - Episode3::BattleRecord rec(data); - rec.print(stdout); - }); - -Action a_replay_ep3_battle_commands( - "replay-ep3-battle-commands", nullptr, +[](Arguments& args) { - auto s = make_shared(get_config_filename(args)); - s->load_ep3_cards(false); - s->load_ep3_maps(false); - - int64_t base_seed = args.get("seed", -1); - bool is_trial = (get_cli_version(args, Version::GC_EP3) == Version::GC_EP3_NTE); - - auto input = read_input_data(args); - vector commands; - for (const auto& line : split(input, '\n')) { - string data = parse_data_string(line); - if (!data.empty()) { - commands.emplace_back(std::move(data)); - } - } - - auto run_replay = [&](int64_t seed, size_t) { - Episode3::Server::Options options = { - .card_index = s->ep3_card_index, - .map_index = s->ep3_map_index, - .behavior_flags = 0x0092, - .opt_rand_stream = nullptr, - .opt_rand_crypt = (seed >= 0) ? make_shared(seed) : nullptr, - .tournament = nullptr, - .trap_card_ids = {}, - }; - if (is_trial) { - options.behavior_flags |= Episode3::BehaviorFlag::IS_TRIAL_EDITION; - } - if (base_seed >= 0) { - options.behavior_flags |= Episode3::BehaviorFlag::LOG_COMMANDS_IF_LOBBY_MISSING; - } - auto server = make_shared(nullptr, std::move(options)); - server->init(); - for (const auto& command : commands) { - server->on_server_data_input(nullptr, command); - } - return false; - }; - - if (base_seed >= 0) { - run_replay(base_seed, 0); - } else { - size_t num_threads = args.get("threads", 0); - parallel_range(run_replay, 0, 0x100000000, num_threads); - } - }); - -Action a_replay_ep3_battle_record( - "replay-ep3-battle-record", nullptr, +[](Arguments& args) { - auto rec = make_shared(read_input_data(args)); - - auto s = make_shared(get_config_filename(args)); - s->load_ep3_cards(false); - s->load_ep3_maps(false); - - bool is_trial = (get_cli_version(args, Version::GC_EP3) == Version::GC_EP3_NTE); - - Episode3::Server::Options options = { - .card_index = s->ep3_card_index, - .map_index = s->ep3_map_index, - .behavior_flags = (Episode3::BehaviorFlag::IGNORE_CARD_COUNTS | - Episode3::BehaviorFlag::ENABLE_STATUS_MESSAGES | - Episode3::BehaviorFlag::DISABLE_MASKING | - Episode3::BehaviorFlag::LOG_COMMANDS_IF_LOBBY_MISSING), - .opt_rand_stream = make_shared(rec->get_random_stream()), - .opt_rand_crypt = nullptr, - .tournament = nullptr, - .trap_card_ids = {}, - }; - if (is_trial) { - options.behavior_flags |= Episode3::BehaviorFlag::IS_TRIAL_EDITION; - } - options.behavior_flags |= Episode3::BehaviorFlag::LOG_COMMANDS_IF_LOBBY_MISSING; - auto server = make_shared(nullptr, std::move(options)); - server->init(); - for (const auto& command : rec->get_all_server_data_commands()) { - log_info("Server data command"); - print_data(stderr, command, 0, nullptr, PrintDataFlags::PRINT_ASCII | PrintDataFlags::DISABLE_COLOR | PrintDataFlags::OFFSET_16_BITS); - server->on_server_data_input(nullptr, command); - } - }); - -Action a_disassemble_ep3_battle_record( - "disassemble-ep3-battle-record", nullptr, +[](Arguments& args) { - Episode3::BattleRecord(read_input_data(args)).print(stdout); - }); - -Action a_run_server_replay_log( - "", nullptr, +[](Arguments& args) { - { - string build_date = format_time(BUILD_TIMESTAMP); - config_log.info("newserv %s compiled at %s", GIT_REVISION_HASH, build_date.c_str()); - } - - if (evthread_use_pthreads()) { - throw runtime_error("failed to set up libevent threads"); - } - - if (!isdir("system/players")) { - config_log.info("Players directory does not exist; creating it"); - mkdir("system/players", 0755); - } - - const string& replay_log_filename = args.get("replay-log"); - bool is_replay = !replay_log_filename.empty(); - - signal(SIGPIPE, SIG_IGN); - if (isatty(fileno(stderr))) { - use_terminal_colors = true; - } - - if (is_replay) { - set_function_compiler_available(false); - } - - shared_ptr base(event_base_new(), event_base_free); - auto state = make_shared(base, get_config_filename(args), is_replay); - state->load_all(); - - if (state->dns_server_port && !is_replay) { - if (!state->dns_server_addr.empty()) { - config_log.info("Starting DNS server on %s:%hu", state->dns_server_addr.c_str(), state->dns_server_port); - } else { - config_log.info("Starting DNS server on port %hu", state->dns_server_port); - } - state->dns_server = make_shared( - base, state->local_address, state->external_address, state->banned_ipv4_ranges); - state->dns_server->listen(state->dns_server_addr, state->dns_server_port); - } else { - config_log.info("DNS server is disabled"); - } - - shared_ptr shell; - shared_ptr replay_session; - shared_ptr http_server; - if (is_replay) { - config_log.info("Starting proxy server"); - state->proxy_server = make_shared(base, state); - config_log.info("Starting game server"); - state->game_server = make_shared(base, state); - - auto nop_destructor = +[](FILE*) {}; - shared_ptr log_f(stdin, nop_destructor); - if (replay_log_filename != "-") { - log_f = fopen_shared(replay_log_filename, "rt"); - } - - replay_session = make_shared(base, log_f.get(), state, args.get("require-basic-credentials")); - replay_session->start(); - - } else { - config_log.info("Opening sockets"); - for (const auto& it : state->name_to_port_config) { - const auto& pc = it.second; - if (pc->behavior == ServerBehavior::PROXY_SERVER) { - if (!state->proxy_server.get()) { - config_log.info("Starting proxy server"); - state->proxy_server = make_shared(base, state); - } - if (state->proxy_server.get()) { - // For PC and GC, proxy sessions are dynamically created when a client - // picks a destination from the menu. For patch and BB clients, there's - // no way to ask the client which destination they want, so only one - // destination is supported, and we have to manually specify the - // destination netloc here. - if (is_patch(pc->version)) { - auto [ss, size] = make_sockaddr_storage( - state->proxy_destination_patch.first, - state->proxy_destination_patch.second); - state->proxy_server->listen(pc->addr, pc->port, pc->version, &ss); - } else if (is_v4(pc->version)) { - auto [ss, size] = make_sockaddr_storage( - state->proxy_destination_bb.first, - state->proxy_destination_bb.second); - state->proxy_server->listen(pc->addr, pc->port, pc->version, &ss); - } else { - state->proxy_server->listen(pc->addr, pc->port, pc->version); - } - } - - } else if (pc->behavior == ServerBehavior::PATCH_SERVER_PC) { - if (!state->pc_patch_server.get()) { - config_log.info("Starting PC_V2 patch server"); - state->pc_patch_server = make_shared(state->generate_patch_server_config(false)); - } - string spec = string_printf("TU-%hu-%s-patch2", pc->port, pc->name.c_str()); - state->pc_patch_server->listen(spec, pc->addr, pc->port, Version::PC_PATCH); - - } else if (pc->behavior == ServerBehavior::PATCH_SERVER_BB) { - if (!state->bb_patch_server.get()) { - config_log.info("Starting BB_V4 patch server"); - state->bb_patch_server = make_shared(state->generate_patch_server_config(true)); - } - string spec = string_printf("TU-%hu-%s-patch4", pc->port, pc->name.c_str()); - state->bb_patch_server->listen(spec, pc->addr, pc->port, Version::BB_PATCH); - - } else { - if (!state->game_server.get()) { - config_log.info("Starting game server"); - state->game_server = make_shared(base, state); - } - string spec = string_printf("TG-%hu-%s-%s-%s", pc->port, name_for_enum(pc->version), pc->name.c_str(), name_for_enum(pc->behavior)); - state->game_server->listen(spec, pc->addr, pc->port, pc->version, pc->behavior); - } - } - - if (!state->ip_stack_addresses.empty() || !state->ppp_stack_addresses.empty() || !state->ppp_raw_addresses.empty()) { - config_log.info("Starting IP/PPP stack simulator"); - state->ip_stack_simulator = make_shared(base, state); - for (const auto& it : state->ip_stack_addresses) { - auto netloc = parse_netloc(it); - string spec = (netloc.second == 0) ? ("T-IPS-" + netloc.first) : string_printf("T-IPS-%hu", netloc.second); - state->ip_stack_simulator->listen( - spec, netloc.first, netloc.second, IPStackSimulator::Protocol::ETHERNET_TAPSERVER); - } - for (const auto& it : state->ppp_stack_addresses) { - auto netloc = parse_netloc(it); - string spec = (netloc.second == 0) ? ("T-PPPST-" + netloc.first) : string_printf("T-PPPST-%hu", netloc.second); - state->ip_stack_simulator->listen( - spec, netloc.first, netloc.second, IPStackSimulator::Protocol::HDLC_TAPSERVER); - } - for (const auto& it : state->ppp_raw_addresses) { - auto netloc = parse_netloc(it); - string spec = (netloc.second == 0) ? ("T-PPPSR-" + netloc.first) : string_printf("T-PPPSR-%hu", netloc.second); - state->ip_stack_simulator->listen( - spec, netloc.first, netloc.second, IPStackSimulator::Protocol::HDLC_RAW); - if (netloc.second) { - if (state->local_address == state->external_address) { - config_log.info( - "Note: The Devolution phone number for %s is %" PRIu64, - spec.c_str(), devolution_phone_number_for_netloc(state->local_address, netloc.second)); - } else { - config_log.info( - "Note: The Devolution phone numbers for %s are %" PRIu64 " (local) and %" PRIu64 " (external)", - spec.c_str(), - devolution_phone_number_for_netloc(state->local_address, netloc.second), - devolution_phone_number_for_netloc(state->external_address, netloc.second)); - } - } - } - } - - if (!state->http_addresses.empty() || !state->http_addresses.empty()) { - config_log.info("Starting HTTP server"); - http_server = make_shared(state); - for (const auto& it : state->http_addresses) { - auto netloc = parse_netloc(it); - http_server->listen(netloc.first, netloc.second); - } - } - } - - if (!state->username.empty()) { - config_log.info("Switching to user %s", state->username.c_str()); - drop_privileges(state->username); - } - - bool should_run_shell; - if (state->run_shell_behavior == ServerState::RunShellBehavior::DEFAULT) { - should_run_shell = isatty(fileno(stdin)); - } else if (state->run_shell_behavior == ServerState::RunShellBehavior::ALWAYS) { - should_run_shell = true; - } else { - should_run_shell = false; - } - if (should_run_shell) { - should_run_shell = !replay_session.get(); - } - - config_log.info("Ready"); - if (should_run_shell) { - shell = make_shared(state); - } - - event_base_dispatch(base.get()); - if (replay_session) { - // If in a replay session, run the event loop for a bit longer to make - // sure the server doesn't send anything unexpected after the end of - // the session. - auto tv = usecs_to_timeval(500000); - event_base_loopexit(base.get(), &tv); - event_base_dispatch(base.get()); - } - - config_log.info("Normal shutdown"); - if (state->pc_patch_server) { - state->pc_patch_server->schedule_stop(); - } - if (state->bb_patch_server) { - state->bb_patch_server->schedule_stop(); - } - if (http_server) { - http_server->schedule_stop(); - } - if (state->pc_patch_server) { - config_log.info("Waiting for PC_V2 patch server to stop"); - state->pc_patch_server->wait_for_stop(); - } - if (state->bb_patch_server) { - config_log.info("Waiting for BB_V4 patch server to stop"); - state->bb_patch_server->wait_for_stop(); - } - if (http_server) { - config_log.info("Waiting for HTTP server to stop"); - http_server->wait_for_stop(); - } - state->proxy_server.reset(); // Break reference cycle - }); - -void print_version_info() { - string build_date = format_time(BUILD_TIMESTAMP); - fprintf(stderr, "newserv-%s built %s UTC\n", GIT_REVISION_HASH, build_date.c_str()); -} - -void print_usage() { - print_version_info(); - fputs("\n\ -Usage:\n\ - newserv [ACTION] [OPTIONS...]\n\ -\n\ -If ACTION is not specified, newserv runs in server mode. PSO clients can\n\ -connect normally, join lobbies, play games, and use the proxy server. See\n\ -README.md and system/config.json for more information.\n\ -\n\ -When ACTION is given, newserv will do things other than running the server.\n\ -\n\ -Some actions accept input and/or output filenames; see the descriptions below\n\ -for details. If INPUT-FILENAME is missing or is '-', newserv reads from stdin.\n\ -If OUTPUT-FILENAME is missing and the input is not from stdin, newserv writes\n\ -the output to INPUT-FILENAME.dec or a similarly-named file; if OUTPUT-FILENAME\n\ -is '-', newserv writes the output to stdout. If stdout is a terminal and the\n\ -output is not text or JSON, the data written to stdout is formatted in a\n\ -hex/ASCII view; in any other case, the raw output is written to stdout, which\n\ -(for most actions) may include arbitrary binary data.\n\ -\n\ -The actions are:\n", - stderr); - for (const auto& a : action_order) { - if (a->help_text) { - fputs(a->help_text, stderr); - } - } - fputs("\n\ -Most options that take data as input also accept the following option:\n\ - --parse-data\n\ - For modes that take input (from a file or from stdin), parse the input as\n\ - a hex string before encrypting/decoding/etc.\n\ -\n\ -Many versions also accept or require a version option. The version options are:\n\ - --pc-patch: PC patch server\n\ - --bb-patch: BB patch server\n\ - --dc-nte: DC Network Trial Edition\n\ - --dc-proto or --dc-11-2000: DC 11/2000 prototype\n\ - --dc-v1: DC v1\n\ - --dc-v2 or --dc: DC v2\n\ - --pc-nte: PC Network Trial Edition\n\ - --pc: PC v2\n\ - --gc-nte: GC Episodes 1&2 Trial Edition\n\ - --gc: GC Episodes 1&2\n\ - --xb: Xbox Episodes 1&2\n\ - --ep3-nte: GC Episode 3 Trial Edition\n\ - --ep3: GC Episode 3\n\ - --bb: Blue Burst\n\ -\n", - stderr); -} - -int main(int argc, char** argv) { - Arguments args(&argv[1], argc - 1); - if (args.get("help")) { - print_usage(); - return 0; - } - - string action_name = args.get(0, false); - const Action* a; - try { - a = all_actions.at(action_name); - } catch (const out_of_range&) { - log_error("Unknown or invalid action; try --help"); - return 1; - } -#ifdef PHOSG_WINDOWS - // Cygwin just gives a stackdump when an exception falls out of main(), so - // unlike Linux and macOS, we have to manually catch exceptions here just to - // see what the exception message was. - try { - a->run(args); - } catch (const cannot_open_file& e) { - log_error("Top-level exception (cannot_open_file): %s", e.what()); - throw; - } catch (const invalid_argument& e) { - log_error("Top-level exception (invalid_argument): %s", e.what()); - throw; - } catch (const out_of_range& e) { - log_error("Top-level exception (out_of_range): %s", e.what()); - throw; - } catch (const runtime_error& e) { - log_error("Top-level exception (runtime_error): %s", e.what()); - throw; - } catch (const exception& e) { - log_error("Top-level exception: %s", e.what()); - throw; - } -#else - a->run(args); -#endif - return 0; -} +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_RESOURCE_FILE +#include "AddressTranslator.hh" +#else +#include "AddressTranslator-Stub.hh" +#endif +#include "BMLArchive.hh" +#include "CatSession.hh" +#include "Compression.hh" +#include "DCSerialNumbers.hh" +#include "DNSServer.hh" +#include "GSLArchive.hh" +#include "GVMEncoder.hh" +#include "HTTPServer.hh" +#include "IPStackSimulator.hh" +#include "Loggers.hh" +#include "NetworkAddresses.hh" +#include "PSOGCObjectGraph.hh" +#include "PSOProtocol.hh" +#include "PatchServer.hh" +#include "ProxyServer.hh" +#include "Quest.hh" +#include "QuestScript.hh" +#include "ReplaySession.hh" +#include "Revision.hh" +#include "SaveFileFormats.hh" +#include "SendCommands.hh" +#include "Server.hh" +#include "ServerShell.hh" +#include "ServerState.hh" +#include "StaticGameData.hh" +#include "Text.hh" +#include "TextIndex.hh" + +using namespace std; + +bool use_terminal_colors = false; + +void print_version_info(); +void print_usage(); + +std::string get_config_filename(Arguments& args) { + string config_filename = args.get("config"); + return config_filename.empty() ? "system/config.json" : config_filename; +} + +template +vector parse_int_vector(const JSON& o) { + vector ret; + for (const auto& x : o.as_list()) { + ret.emplace_back(x->as_int()); + } + return ret; +} + +void drop_privileges(const string& username) { + if ((getuid() != 0) || (getgid() != 0)) { + throw runtime_error(string_printf( + "newserv was not started as root; can\'t switch to user %s", + username.c_str())); + } + + struct passwd* pw = getpwnam(username.c_str()); + if (!pw) { + string error = string_for_error(errno); + throw runtime_error(string_printf("user %s not found (%s)", + username.c_str(), error.c_str())); + } + + if (setgid(pw->pw_gid) != 0) { + string error = string_for_error(errno); + throw runtime_error(string_printf("can\'t switch to group %d (%s)", + pw->pw_gid, error.c_str())); + } + if (setuid(pw->pw_uid) != 0) { + string error = string_for_error(errno); + throw runtime_error(string_printf("can\'t switch to user %d (%s)", + pw->pw_uid, error.c_str())); + } + config_log.info("Switched to user %s (%d:%d)", username.c_str(), pw->pw_uid, pw->pw_gid); +} + +Version get_cli_version(Arguments& args, Version default_value = Version::UNKNOWN) { + if (args.get("pc-patch")) { + return Version::PC_PATCH; + } else if (args.get("bb-patch")) { + return Version::BB_PATCH; + } else if (args.get("dc-nte")) { + return Version::DC_NTE; + } else if (args.get("dc-proto") || args.get("dc-11-2000")) { + return Version::DC_V1_11_2000_PROTOTYPE; + } else if (args.get("dc-v1")) { + return Version::DC_V1; + } else if (args.get("dc-v2") || args.get("dc")) { + return Version::DC_V2; + } else if (args.get("pc-nte")) { + return Version::PC_NTE; + } else if (args.get("pc") || args.get("pc-v2")) { + return Version::PC_V2; + } else if (args.get("gc-nte")) { + return Version::GC_NTE; + } else if (args.get("gc") || args.get("gc-v3")) { + return Version::GC_V3; + } else if (args.get("xb") || args.get("xb=v3")) { + return Version::XB_V3; + } else if (args.get("ep3-nte") || args.get("gc-ep3-nte")) { + return Version::GC_EP3_NTE; + } else if (args.get("ep3") || args.get("gc-ep3")) { + return Version::GC_EP3; + } else if (args.get("bb") || args.get("bb-v4")) { + return Version::BB_V4; + } else if (default_value != Version::UNKNOWN) { + return default_value; + } else { + throw runtime_error("a version option is required"); + } +} + +Episode get_cli_episode(Arguments& args) { + if (args.get("ep1")) { + return Episode::EP1; + } else if (args.get("ep2")) { + return Episode::EP2; + } else if (args.get("ep3")) { + return Episode::EP3; + } else if (args.get("ep4")) { + return Episode::EP4; + } else { + throw runtime_error("an episode option is required"); + } +} + +GameMode get_cli_game_mode(Arguments& args) { + if (args.get("battle")) { + return GameMode::BATTLE; + } else if (args.get("challenge")) { + return GameMode::CHALLENGE; + } else if (args.get("solo")) { + return GameMode::SOLO; + } else { + return GameMode::NORMAL; + } +} + +uint8_t get_cli_difficulty(Arguments& args) { + if (args.get("hard")) { + return 1; + } else if (args.get("very-hard")) { + return 2; + } else if (args.get("ultimate")) { + return 3; + } else { + return 0; + } +} + +string read_input_data(Arguments& args) { + const string& input_filename = args.get(1, false); + + string data; + if (!input_filename.empty() && (input_filename != "-")) { + data = load_file(input_filename); + } else { + data = read_all(stdin); + } + if (args.get("parse-data")) { + data = parse_data_string(data, nullptr, ParseDataFlags::ALLOW_FILES); + } + return data; +} + +bool is_text_extension(const char* extension) { + return (!strcmp(extension, "txt") || !strcmp(extension, "json")); +} + +void write_output_data(Arguments& args, const void* data, size_t size, const char* extension) { + const string& input_filename = args.get(1, false); + const string& output_filename = args.get(2, false); + + if (!output_filename.empty() && (output_filename != "-")) { + // If the output is to a specified file, write it there + save_file(output_filename, data, size); + + } else if (output_filename.empty() && (output_filename != "-") && !input_filename.empty() && (input_filename != "-")) { + // If no output filename is given and an input filename is given, write to + // . + if (!extension) { + throw runtime_error("an output filename is required"); + } + string filename = input_filename; + filename += "."; + filename += extension; + save_file(filename, data, size); + + } else if (isatty(fileno(stdout)) && (!extension || !is_text_extension(extension))) { + // If stdout is a terminal and the data is not known to be text, use + // print_data to write the result + print_data(stdout, data, size); + fflush(stdout); + + } else { + // If stdout is not a terminal, write the data as-is + fwritex(stdout, data, size); + fflush(stdout); + } +} + +struct Action; +unordered_map all_actions; +vector action_order; + +struct Action { + const char* name; + const char* help_text; // May be null + function run; + + Action( + const char* name, + const char* help_text, + function run) + : name(name), + help_text(help_text), + run(run) { + auto emplace_ret = all_actions.emplace(this->name, this); + if (!emplace_ret.second) { + throw logic_error(string_printf("multiple actions with the same name: %s", this->name)); + } + action_order.emplace_back(this); + } +}; + +Action a_help( + "help", "\ + help\n\ + You\'re reading it now.\n", + +[](Arguments&) -> void { + print_usage(); + }); + +Action a_version( + "version", "\ + version\n\ + Show newserv\'s revision and build date.\n", + +[](Arguments&) -> void { + print_version_info(); + }); + +static void a_compress_decompress_fn(Arguments& args) { + const auto& action = args.get(0); + bool is_prs = ends_with(action, "-prs"); + bool is_bc0 = ends_with(action, "-bc0"); + bool is_pr2 = ends_with(action, "-pr2"); + bool is_prc = ends_with(action, "-prc"); + bool is_decompress = starts_with(action, "decompress-"); + bool is_big_endian = args.get("big-endian"); + bool is_optimal = args.get("optimal"); + bool is_pessimal = args.get("pessimal"); + int8_t compression_level = args.get("compression-level", 0); + size_t bytes = args.get("bytes", 0); + string seed = args.get("seed"); + + string data = read_input_data(args); + + size_t pr2_expected_size = 0; + if (is_decompress && (is_pr2 || is_prc)) { + auto decrypted = is_big_endian ? decrypt_pr2_data(data) : decrypt_pr2_data(data); + pr2_expected_size = decrypted.decompressed_size; + data = std::move(decrypted.compressed_data); + } + + size_t input_bytes = data.size(); + auto progress_fn = [&](auto, size_t input_progress, size_t, size_t output_progress) -> void { + float progress = static_cast(input_progress * 100) / input_bytes; + float size_ratio = static_cast(output_progress * 100) / input_progress; + fprintf(stderr, "... %zu/%zu (%g%%) => %zu (%g%%) \r", + input_progress, input_bytes, progress, output_progress, size_ratio); + }; + auto optimal_progress_fn = [&](auto phase, size_t input_progress, size_t input_bytes, size_t output_progress) -> void { + const char* phase_name = name_for_enum(phase); + float progress = static_cast(input_progress * 100) / input_bytes; + float size_ratio = static_cast(output_progress * 100) / input_progress; + fprintf(stderr, "... [%s] %zu/%zu (%g%%) => %zu (%g%%) \r", + phase_name, input_progress, input_bytes, progress, output_progress, size_ratio); + }; + + uint64_t start = now(); + if (!is_decompress && (is_prs || is_pr2 || is_prc)) { + if (is_optimal) { + data = prs_compress_optimal(data.data(), data.size(), optimal_progress_fn); + } else if (is_pessimal) { + data = prs_compress_pessimal(data.data(), data.size()); + } else { + data = prs_compress(data, compression_level, progress_fn); + } + } else if (is_decompress && (is_prs || is_pr2 || is_prc)) { + data = prs_decompress(data, bytes, (bytes != 0)); + } else if (!is_decompress && is_bc0) { + if (is_optimal) { + data = bc0_compress_optimal(data.data(), data.size(), optimal_progress_fn); + } else if (compression_level < 0) { + data = bc0_encode(data.data(), data.size()); + } else { + data = bc0_compress(data, progress_fn); + } + } else if (is_decompress && is_bc0) { + data = bc0_decompress(data); + } else { + throw logic_error("invalid behavior"); + } + uint64_t end = now(); + string time_str = format_duration(end - start); + + float size_ratio = static_cast(data.size() * 100) / input_bytes; + double bytes_per_sec = input_bytes / (static_cast(end - start) / 1000000.0); + string bytes_per_sec_str = format_size(bytes_per_sec); + log_info("%zu (0x%zX) bytes input => %zu (0x%zX) bytes output (%g%%) in %s (%s / sec)", + input_bytes, input_bytes, data.size(), data.size(), size_ratio, time_str.c_str(), bytes_per_sec_str.c_str()); + + if (is_pr2 || is_prc) { + if (is_decompress && (data.size() != pr2_expected_size)) { + log_warning("Result data size (%zu bytes) does not match expected size from PR2 header (%zu bytes)", data.size(), pr2_expected_size); + } else if (!is_decompress) { + uint32_t pr2_seed = seed.empty() ? random_object() : stoul(seed, nullptr, 16); + data = is_big_endian + ? encrypt_pr2_data(data, input_bytes, pr2_seed) + : encrypt_pr2_data(data, input_bytes, pr2_seed); + } + } + + const char* extension; + if (is_decompress) { + extension = "dec"; + } else if (is_prs) { + extension = "prs"; + } else if (is_bc0) { + extension = "bc0"; + } else if (is_prc) { + extension = "prc"; + } else if (is_pr2) { + extension = "pr2"; + } else { + throw logic_error("unknown action"); + } + write_output_data(args, data.data(), data.size(), extension); +} + +Action a_compress_prs("compress-prs", nullptr, a_compress_decompress_fn); +Action a_compress_bc0("compress-bc0", nullptr, a_compress_decompress_fn); +Action a_compress_pr2("compress-pr2", nullptr, a_compress_decompress_fn); +Action a_compress_prc("compress-prc", "\ + compress-prs [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ + compress-pr2 [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ + compress-prc [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ + compress-bc0 [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ + Compress data using the PRS, PR2, PRC, or BC0 algorithms. By default, the\n\ + heuristic-based compressor is used, which gives a good balance between\n\ + memory usage, CPU usage, and output size. For PRS and PR2, this compressor\n\ + can be tuned with the --compression-level=N option, which specifies how\n\ + aggressive the compressor should be in searching for literal sequences. The\n\ + default level is 0; a higher value generally means slower compression and a\n\ + smaller output size. If the compression level is -1, the input data is\n\ + encoded in a PRS-compatible format but not actually compressed, resulting\n\ + in valid PRS data which is about 9/8 the size of the input.\n\ + There is also a compressor which produces the absolute smallest output\n\ + size, but uses much more memory and CPU time. To use this compressor, use\n\ + the --optimal option.\n", + a_compress_decompress_fn); +Action a_decompress_prs("decompress-prs", nullptr, a_compress_decompress_fn); +Action a_decompress_bc0("decompress-bc0", nullptr, a_compress_decompress_fn); +Action a_decompress_pr2("decompress-pr2", nullptr, a_compress_decompress_fn); +Action a_decompress_prc("decompress-prc", "\ + decompress-prs [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ + decompress-pr2 [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ + decompress-prc [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ + decompress-bc0 [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ + Decompress data compressed using the PRS, PR2, PRC, or BC0 algorithms.\n", + a_compress_decompress_fn); + +Action a_prs_size( + "prs-size", "\ + prs-size [INPUT-FILENAME]\n\ + Compute the decompressed size of the PRS-compressed input data, but don\'t\n\ + write the decompressed data anywhere.\n", + +[](Arguments& args) { + string data = read_input_data(args); + size_t input_bytes = data.size(); + size_t output_bytes = prs_decompress_size(data); + log_info("%zu (0x%zX) bytes input => %zu (0x%zX) bytes output", + input_bytes, input_bytes, output_bytes, output_bytes); + }); + +Action a_disassemble_prs( + "disassemble-prs", nullptr, +[](Arguments& args) { + prs_disassemble(stdout, read_input_data(args)); + }); +Action a_disassemble_bc0( + "disassemble-bc0", "\ + disassemble-prs [INPUT-FILENAME]\n\ + disassemble-bc0 [INPUT-FILENAME]\n\ + Write a textual representation of the commands contained in a PRS or BC0\n\ + command stream. The output is written to stdout. This is mainly useful for\n\ + debugging the compressors and decompressors themselves.\n", + +[](Arguments& args) { + bc0_disassemble(stdout, read_input_data(args)); + }); + +static void a_encrypt_decrypt_fn(Arguments& args) { + bool is_decrypt = (args.get(0) == "decrypt-data"); + string seed = args.get("seed"); + bool is_big_endian = args.get("big-endian"); + auto version = get_cli_version(args); + + shared_ptr crypt; + switch (version) { + case Version::DC_NTE: + case Version::DC_V1_11_2000_PROTOTYPE: + case Version::DC_V1: + case Version::DC_V2: + case Version::PC_NTE: + case Version::PC_V2: + case Version::GC_NTE: + crypt = make_shared(stoul(seed, nullptr, 16)); + break; + case Version::GC_V3: + case Version::XB_V3: + case Version::GC_EP3_NTE: + case Version::GC_EP3: + crypt = make_shared(stoul(seed, nullptr, 16)); + break; + case Version::BB_V4: { + string key_name = args.get("key"); + if (key_name.empty()) { + throw runtime_error("the --key option is required for BB"); + } + seed = parse_data_string(seed, nullptr, ParseDataFlags::ALLOW_FILES); + auto key = load_object_file("system/blueburst/keys/" + key_name + ".nsk"); + crypt = make_shared(key, seed.data(), seed.size()); + break; + } + default: + throw logic_error("invalid game version"); + } + + string data = read_input_data(args); + + size_t original_size = data.size(); + data.resize((data.size() + 7) & (~7), '\0'); + + if (is_big_endian) { + uint32_t* dwords = reinterpret_cast(data.data()); + for (size_t x = 0; x < (data.size() >> 2); x++) { + dwords[x] = bswap32(dwords[x]); + } + } + + if (is_decrypt) { + crypt->decrypt(data.data(), data.size()); + } else { + crypt->encrypt(data.data(), data.size()); + } + + if (is_big_endian) { + uint32_t* dwords = reinterpret_cast(data.data()); + for (size_t x = 0; x < (data.size() >> 2); x++) { + dwords[x] = bswap32(dwords[x]); + } + } + + data.resize(original_size); + + write_output_data(args, data.data(), data.size(), "dec"); +} + +Action a_encrypt_data("encrypt-data", nullptr, a_encrypt_decrypt_fn); +Action a_decrypt_data("decrypt-data", "\ + encrypt-data [INPUT-FILENAME [OUTPUT-FILENAME] [OPTIONS...]]\n\ + decrypt-data [INPUT-FILENAME [OUTPUT-FILENAME] [OPTIONS...]]\n\ + Encrypt or decrypt data using PSO\'s standard network protocol encryption.\n\ + By default, PSO V3 (GameCube/XBOX) encryption is used, but this can be\n\ + overridden with the --pc or --bb options. The --seed=SEED option specifies\n\ + the encryption seed (4 hex bytes for PC or GC, or 48 hex bytes for BB). For\n\ + BB, the --key=KEY-NAME option is required as well, and refers to a .nsk\n\ + file in system/blueburst/keys (without the directory or .nsk extension).\n\ + For non-BB ciphers, the --big-endian option applies the cipher masks as\n\ + big-endian instead of little-endian, which is necessary for some GameCube\n\ + file formats.\n", + a_encrypt_decrypt_fn); + +static void a_encrypt_decrypt_trivial_fn(Arguments& args) { + bool is_decrypt = (args.get(0) == "decrypt-trivial-data"); + string seed = args.get("seed"); + + if (seed.empty() && !is_decrypt) { + throw logic_error("--seed is required when encrypting data"); + } + string data = read_input_data(args); + uint8_t basis; + if (seed.empty()) { + uint8_t best_seed = 0x00; + size_t best_seed_score = 0; + for (size_t z = 0; z < 0x100; z++) { + string decrypted = data; + decrypt_trivial_gci_data(decrypted.data(), decrypted.size(), z); + size_t score = 0; + for (size_t x = 0; x < decrypted.size(); x++) { + if (decrypted[x] == '\0') { + score++; + } + } + if (score > best_seed_score) { + best_seed = z; + best_seed_score = score; + } + } + fprintf(stderr, "Basis appears to be %02hhX (%zu zero bytes in output)\n", + best_seed, best_seed_score); + basis = best_seed; + } else { + basis = stoul(seed, nullptr, 16); + } + decrypt_trivial_gci_data(data.data(), data.size(), basis); + write_output_data(args, data.data(), data.size(), "dec"); +} + +Action a_encrypt_trivial_data("encrypt-trivial-data", nullptr, a_encrypt_decrypt_trivial_fn); +Action a_decrypt_trivial_data("decrypt-trivial-data", "\ + encrypt-trivial-data --seed=BASIS [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ + decrypt-trivial-data [--seed=BASIS] [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ + Encrypt or decrypt data using the Episode 3 trivial algorithm. When\n\ + encrypting, --seed=BASIS is required; BASIS should be a single byte\n\ + specified in hexadecimal. When decrypting, BASIS should be specified the\n\ + same way, but if it is not given, newserv will try all possible basis\n\ + values and return the one that results in the greatest number of zero bytes\n\ + in the output.\n", + a_encrypt_decrypt_trivial_fn); + +Action a_decrypt_registry_value( + "decrypt-registry-value", nullptr, +[](Arguments& args) { + string data = read_input_data(args); + string out_data = decrypt_v2_registry_value(data.data(), data.size()); + write_output_data(args, out_data.data(), out_data.size(), "dec"); + }); + +Action a_encrypt_challenge_data( + "encrypt-challenge-data", nullptr, +[](Arguments& args) { + string data = read_input_data(args); + encrypt_challenge_rank_text_t(data.data(), data.size()); + write_output_data(args, data.data(), data.size(), "dec"); + }); +Action a_decrypt_challenge_data( + "decrypt-challenge-data", "\ + encrypt-challenge-data [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ + decrypt-challenge-data [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ + Encrypt or decrypt data using the challenge mode trivial algorithm.\n", + +[](Arguments& args) { + string data = read_input_data(args); + decrypt_challenge_rank_text_t(data.data(), data.size()); + write_output_data(args, data.data(), data.size(), "dec"); + }); + +static void a_encrypt_decrypt_gci_save_fn(Arguments& args) { + bool is_decrypt = (args.get(0) == "decrypt-gci-save"); + bool skip_checksum = args.get("skip-checksum"); + string seed = args.get("seed"); + string system_filename = args.get("sys"); + int64_t override_round2_seed = args.get("round2-seed", -1, Arguments::IntFormat::HEX); + + uint32_t round1_seed; + if (!system_filename.empty()) { + string system_data = load_file(system_filename); + StringReader r(system_data); + const auto& header = r.get(); + header.check(); + const auto& system = r.get(); + round1_seed = system.creation_timestamp; + } else if (!seed.empty()) { + round1_seed = stoul(seed, nullptr, 16); + } else { + throw runtime_error("either --sys or --seed must be given"); + } + + auto data = read_input_data(args); + StringReader r(data); + const auto& header = r.get(); + header.check(); + + size_t data_start_offset = r.where(); + + auto process_file = [&]() { + if (is_decrypt) { + const void* data_section = r.getv(header.data_size); + auto decrypted = decrypt_fixed_size_data_section_t( + data_section, header.data_size, round1_seed, skip_checksum, override_round2_seed); + *reinterpret_cast(data.data() + data_start_offset) = decrypted; + } else { + const auto& s = r.get(); + auto encrypted = encrypt_fixed_size_data_section_t(s, round1_seed); + if (data_start_offset + encrypted.size() > data.size()) { + throw runtime_error("encrypted result exceeds file size"); + } + memcpy(data.data() + data_start_offset, encrypted.data(), encrypted.size()); + } + }; + + if (header.data_size == sizeof(PSOGCGuildCardFile)) { + process_file.template operator()(); + } else if (header.is_ep12() && (header.data_size == sizeof(PSOGCCharacterFile))) { + process_file.template operator()(); + } else if (header.is_ep3() && (header.data_size == sizeof(PSOGCEp3CharacterFile))) { + auto* charfile = reinterpret_cast(data.data() + data_start_offset); + if (!is_decrypt) { + for (size_t z = 0; z < charfile->characters.size(); z++) { + charfile->characters[z].ep3_config.encrypt(random_object()); + } + } + process_file.template operator()(); + if (is_decrypt) { + for (size_t z = 0; z < charfile->characters.size(); z++) { + charfile->characters[z].ep3_config.decrypt(); + } + } + } else { + throw runtime_error("unrecognized save type"); + } + + write_output_data(args, data.data(), data.size(), is_decrypt ? "gcid" : "gci"); +} + +Action a_decrypt_gci_save("decrypt-gci-save", nullptr, a_encrypt_decrypt_gci_save_fn); +Action a_encrypt_gci_save("encrypt-gci-save", "\ + encrypt-gci-save CRYPT-OPTION INPUT-FILENAME [OUTPUT-FILENAME]\n\ + decrypt-gci-save CRYPT-OPTION INPUT-FILENAME [OUTPUT-FILENAME]\n\ + Encrypt or decrypt a character or Guild Card file in GCI format. If\n\ + encrypting, the checksum is also recomputed and stored in the encrypted\n\ + file. CRYPT-OPTION is required; it can be either --sys=SYSTEM-FILENAME\n\ + (specifying the name of the corresponding PSO_SYSTEM .gci file) or\n\ + --seed=ROUND1-SEED (specified as a 32-bit hexadecimal number).\n", + a_encrypt_decrypt_gci_save_fn); + +static void a_encrypt_decrypt_pc_save_fn(Arguments& args) { + bool is_decrypt = (args.get(0) == "decrypt-pc-save"); + bool skip_checksum = args.get("skip-checksum"); + string seed = args.get("seed"); + int64_t override_round2_seed = args.get("round2-seed", -1, Arguments::IntFormat::HEX); + + if (seed.empty()) { + throw runtime_error("--seed must be given to specify the serial number"); + } + uint32_t round1_seed = stoul(seed, nullptr, 16); + + auto data = read_input_data(args); + if (data.size() == sizeof(PSOPCGuildCardFile)) { + if (is_decrypt) { + data = decrypt_fixed_size_data_section_s( + data.data(), offsetof(PSOPCGuildCardFile, end_padding), round1_seed, skip_checksum, override_round2_seed); + } else { + data = encrypt_fixed_size_data_section_s( + data.data(), offsetof(PSOPCGuildCardFile, end_padding), round1_seed); + } + data.resize((sizeof(PSOPCGuildCardFile) + 0x1FF) & (~0x1FF), '\0'); + } else if (data.size() == sizeof(PSOPCCharacterFile)) { + PSOPCCharacterFile* charfile = reinterpret_cast(data.data()); + if (is_decrypt) { + for (size_t z = 0; z < charfile->entries.size(); z++) { + if (charfile->entries[z].present) { + try { + charfile->entries[z].encrypted = decrypt_fixed_size_data_section_t( + &charfile->entries[z].encrypted, sizeof(charfile->entries[z].encrypted), round1_seed, skip_checksum, override_round2_seed); + } catch (const exception& e) { + fprintf(stderr, "warning: cannot decrypt character %zu: %s\n", z, e.what()); + } + } + } + } else { + for (size_t z = 0; z < charfile->entries.size(); z++) { + if (charfile->entries[z].present) { + string encrypted = encrypt_fixed_size_data_section_t( + charfile->entries[z].encrypted, round1_seed); + if (encrypted.size() != sizeof(PSOPCCharacterFile::CharacterEntry::EncryptedSection)) { + throw logic_error("incorrect encrypted result size"); + } + charfile->entries[z].encrypted = *reinterpret_cast(encrypted.data()); + } + } + } + } else if (data.size() == sizeof(PSOPCCreationTimeFile)) { + throw runtime_error("the PSO______FLS file is not encrypted; it is just random data"); + } else if (data.size() == sizeof(PSOPCSystemFile)) { + throw runtime_error("the PSO______COM file is not encrypted"); + } else { + throw runtime_error("unknown save file type"); + } + + write_output_data(args, data.data(), data.size(), "dec"); +} + +Action a_decrypt_pc_save("decrypt-pc-save", nullptr, a_encrypt_decrypt_pc_save_fn); +Action a_encrypt_pc_save("encrypt-pc-save", "\ + encrypt-pc-save --seed=SEED [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ + decrypt-pc-save --seed=SEED [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ + Encrypt or decrypt a PSO PC character file (PSO______SYS or PSO______SYD)\n\ + or Guild Card file (PSO______GUD). SEED should be the serial number\n\ + associated with the save file, as a 32-bit hexadecimal integer.\n", + a_encrypt_decrypt_pc_save_fn); + +static void a_encrypt_decrypt_save_data_fn(Arguments& args) { + bool is_decrypt = (args.get(0) == "decrypt-save-data"); + bool skip_checksum = args.get("skip-checksum"); + bool is_big_endian = args.get("big-endian"); + string seed = args.get("seed"); + int64_t override_round2_seed = args.get("round2-seed", -1, Arguments::IntFormat::HEX); + size_t bytes = args.get("bytes", 0); + + if (seed.empty()) { + throw runtime_error("--seed must be given to specify the round1 seed"); + } + uint32_t round1_seed = stoul(seed, nullptr, 16); + + auto data = read_input_data(args); + StringReader r(data); + + string output_data; + size_t effective_size = bytes ? min(bytes, data.size()) : data.size(); + if (is_decrypt) { + output_data = is_big_endian + ? decrypt_fixed_size_data_section_s(data.data(), effective_size, round1_seed, skip_checksum, override_round2_seed) + : decrypt_fixed_size_data_section_s(data.data(), effective_size, round1_seed, skip_checksum, override_round2_seed); + } else { + output_data = is_big_endian + ? encrypt_fixed_size_data_section_s(data.data(), effective_size, round1_seed) + : encrypt_fixed_size_data_section_s(data.data(), effective_size, round1_seed); + } + write_output_data(args, output_data.data(), output_data.size(), "dec"); +} + +// TODO: Write usage text for these actions +Action a_decrypt_save_data("decrypt-save-data", nullptr, a_encrypt_decrypt_save_data_fn); +Action a_encrypt_save_data("encrypt-save-data", nullptr, a_encrypt_decrypt_save_data_fn); + +Action a_decrypt_dcv2_executable( + "decrypt-dcv2-executable", "\ + decrypt-dcv2-executable --executable=EXEC --indexes=INDEXES --values=VALUES\n\ + decrypt-dcv2-executable --executable=EXEC --simple [--seed=SEED]\n\ + Decrypt a PSO DC v2 executable file. EXEC should be the path to the\n\ + executable (DP_ADDRESS.JPN), INDEXES should be the path to the index fixup\n\ + table (KATSUO.SEA), and VALUES should be the path to the value fixup table\n\ + (IWASHI.SEA). The output is written to EXEC.dec.\n\ + If --simple is given, uses the simpler encryption method used in some\n\ + community modifications of the game. In this case, --seed is not required;\n\ + if not given, finds the seed automatically, and prints it to stderr so you\n\ + will be able to use it when re-encrypting.\n", + +[](Arguments& args) { + string executable_filename = args.get("executable", true); + string executable_data = load_file(executable_filename); + string decrypted; + if (args.get("simple")) { + string seed_str = args.get("seed"); + int64_t seed = seed_str.empty() ? -1 : stoull(seed_str, nullptr, 16); + decrypted = crypt_dp_address_jpn_simple(executable_data, seed); + } else { + string values_filename = args.get("values", true); + string indexes_filename = args.get("indexes", true); + string values_data = load_file(values_filename); + string indexes_data = load_file(indexes_filename); + decrypted = decrypt_dp_address_jpn(executable_data, values_data, indexes_data); + } + save_file(executable_filename + ".dec", decrypted); + }); +Action a_encrypt_dcv2_executable( + "encrypt-dcv2-executable", "\ + encrypt-dcv2-executable --executable=EXEC --indexes=INDEXES\n\ + encrypt-dcv2-executable --executable=EXEC --simple --seed=SEED\n\ + Encrypt a PSO DC v2 executable file. EXEC should be the path to the\n\ + executable (DP_ADDRESS.JPN) and INDEXES should be the path to the index\n\ + fixup table (KATSUO.SEA). The output is written to EXEC.enc and\n\ + INDEXES.enc.\n\ + If --simple is given, uses the simpler encryption method used in some\n\ + community modifications of the game. In this case, --seed is required.\n", + +[](Arguments& args) { + string executable_filename = args.get("executable", true); + string executable_data = load_file(executable_filename); + string encrypted_executable; + if (args.get("simple")) { + int64_t seed = stoull(args.get("seed", true), nullptr, 16); + encrypted_executable = crypt_dp_address_jpn_simple(executable_data, seed); + } else { + string indexes_filename = args.get("indexes", true); + string indexes_data = load_file(indexes_filename); + auto encrypted = encrypt_dp_address_jpn(executable_data, indexes_data); + save_file(indexes_filename + ".enc", encrypted.indexes); + encrypted_executable = std::move(encrypted.executable); + } + save_file(executable_filename + ".enc", encrypted_executable); + }); + +Action a_decode_gci_snapshot( + "decode-gci-snapshot", "\ + decode-gci-snapshot [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ + Decode a PSO GC snapshot file into a Windows BMP image.\n", + +[](Arguments& args) { + auto data = read_input_data(args); + StringReader r(data); + const auto& header = r.get(); + try { + header.check(); + } catch (const exception& e) { + log_warning("File header failed validation (%s)", e.what()); + } + const auto& file = r.get(); + if (!file.checksum_correct()) { + log_warning("File internal checksum is incorrect"); + } + + auto img = file.decode_image(); + string saved = img.save(Image::Format::WINDOWS_BITMAP); + write_output_data(args, saved.data(), saved.size(), "bmp"); + }); + +Action a_encode_gvm( + "encode-gvm", "\ + encode-gvm [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ + Encode an image in BMP or PPM/PNM format into a GVM texture. The resulting\n\ + GVM file can be used as an Episode 3 lobby banner.\n", + +[](Arguments& args) { + const string& input_filename = args.get(1, false); + Image img; + if (!input_filename.empty() && (input_filename != "-")) { + img = Image(input_filename); + } else { + img = Image(stdin); + } + string encoded = encode_gvm(img, img.get_has_alpha() ? GVRDataFormat::RGB5A3 : GVRDataFormat::RGB565, "image.gvr", 0); + write_output_data(args, encoded.data(), encoded.size(), "gvm"); + }); + +Action a_salvage_gci( + "salvage-gci", "\ + salvage-gci INPUT-FILENAME [--round2] [CRYPT-OPTION] [--bytes=SIZE]\n\ + Attempt to find either the round-1 or round-2 decryption seed for a\n\ + corrupted GCI file. If --round2 is given, then CRYPT-OPTION must be given\n\ + (and should specify either a valid system file or the round1 seed).\n", + +[](Arguments& args) { + bool round2 = args.get("round2"); + string seed = args.get("seed"); + string system_filename = args.get("sys"); + size_t num_threads = args.get("threads", 0); + size_t offset = args.get("offset", 0); + size_t stride = args.get("stride", 1); + size_t bytes = args.get("bytes", 0); + + uint64_t likely_round1_seed = 0xFFFFFFFFFFFFFFFF; + if (!system_filename.empty()) { + try { + string system_data = load_file(system_filename); + StringReader r(system_data); + const auto& header = r.get(); + header.check(); + const auto& system = r.get(); + likely_round1_seed = system.creation_timestamp; + log_info("System file appears to be in order; round1 seed is %08" PRIX64, likely_round1_seed); + } catch (const exception& e) { + log_warning("Cannot parse system file (%s); ignoring it", e.what()); + } + } else if (!seed.empty()) { + likely_round1_seed = stoul(seed, nullptr, 16); + log_info("Specified round1 seed is %08" PRIX64, likely_round1_seed); + } + + if (round2 && likely_round1_seed > 0x100000000) { + throw invalid_argument("cannot find round2 seed without known round1 seed"); + } + + auto data = read_input_data(args); + StringReader r(data); + const auto& header = r.get(); + header.check(); + + const void* data_section = r.getv(header.data_size); + + auto process_file = [&]() { + vector> top_seeds_by_thread( + num_threads ? num_threads : thread::hardware_concurrency()); + parallel_range( + [&](uint64_t seed, size_t thread_num) -> bool { + size_t zero_count; + if (round2) { + string decrypted = decrypt_gci_fixed_size_data_section_for_salvage( + data_section, header.data_size, likely_round1_seed, seed, bytes); + zero_count = count_zeroes( + decrypted.data() + offset, + decrypted.size() - offset, + stride); + } else { + auto decrypted = decrypt_fixed_size_data_section_t( + data_section, + header.data_size, + seed, + true); + zero_count = count_zeroes( + reinterpret_cast(&decrypted) + offset, + sizeof(decrypted) - offset, + stride); + } + auto& top_seeds = top_seeds_by_thread[thread_num]; + if (top_seeds.size() < 10 || (zero_count >= top_seeds.begin()->second)) { + top_seeds.emplace(zero_count, seed); + if (top_seeds.size() > 10) { + top_seeds.erase(top_seeds.begin()); + } + } + return false; + }, + 0, + 0x100000000, + num_threads); + + multimap top_seeds; + for (const auto& thread_top_seeds : top_seeds_by_thread) { + for (const auto& it : thread_top_seeds) { + top_seeds.emplace(it.first, it.second); + } + } + for (const auto& it : top_seeds) { + const char* sys_seed_str = (!round2 && (it.second == likely_round1_seed)) + ? " (this is the seed from the system file)" + : ""; + log_info("Round %c seed %08" PRIX32 " resulted in %zu zero bytes%s", + round2 ? '2' : '1', it.second, it.first, sys_seed_str); + } + }; + + if (header.data_size == sizeof(PSOGCGuildCardFile)) { + process_file.template operator()(); + } else if (header.is_ep12() && (header.data_size == sizeof(PSOGCCharacterFile))) { + process_file.template operator()(); + } else if (header.is_ep3() && (header.data_size == sizeof(PSOGCEp3CharacterFile))) { + process_file.template operator()(); + } else { + throw runtime_error("unrecognized save type"); + } + }); + +Action a_find_decryption_seed( + "find-decryption-seed", "\ + find-decryption-seed OPTIONS...\n\ + Perform a brute-force search for a decryption seed of the given data. The\n\ + ciphertext is specified with the --encrypted=DATA option and the expected\n\ + plaintext is specified with the --decrypted=DATA option. The plaintext may\n\ + include unmatched bytes (specified with the Phosg parse_data_string ?\n\ + operator), but overall it must be the same length as the ciphertext. By\n\ + default, this option uses PSO V3 encryption, but this can be overridden\n\ + with --pc. (BB encryption seeds are too long to be searched for with this\n\ + function.) By default, the number of worker threads is equal to the number\n\ + of CPU cores in the system, but this can be overridden with the\n\ + --threads=NUM-THREADS option.\n", + +[](Arguments& args) { + const auto& plaintexts_ascii = args.get_multi("decrypted"); + const auto& ciphertext_ascii = args.get("encrypted"); + auto version = get_cli_version(args); + if (plaintexts_ascii.empty() || ciphertext_ascii.empty()) { + throw runtime_error("both --encrypted and --decrypted must be specified"); + } + if (uses_v4_encryption(version)) { + throw runtime_error("--find-decryption-seed cannot be used for BB ciphers"); + } + bool skip_little_endian = args.get("skip-little-endian"); + bool skip_big_endian = args.get("skip-big-endian"); + size_t num_threads = args.get("threads", 0); + + size_t max_plaintext_size = 0; + vector> plaintexts; + for (const auto& plaintext_ascii : plaintexts_ascii) { + string mask; + string data = parse_data_string(plaintext_ascii, &mask, ParseDataFlags::ALLOW_FILES); + if (data.size() != mask.size()) { + throw logic_error("plaintext and mask are not the same size"); + } + max_plaintext_size = max(max_plaintext_size, data.size()); + plaintexts.emplace_back(std::move(data), std::move(mask)); + } + string ciphertext = parse_data_string(ciphertext_ascii, nullptr, ParseDataFlags::ALLOW_FILES); + + auto mask_match = +[](const void* a, const void* b, const void* m, size_t size) -> bool { + const uint8_t* a8 = reinterpret_cast(a); + const uint8_t* b8 = reinterpret_cast(b); + const uint8_t* m8 = reinterpret_cast(m); + for (size_t z = 0; z < size; z++) { + if ((a8[z] & m8[z]) != (b8[z] & m8[z])) { + return false; + } + } + return true; + }; + + uint64_t seed = parallel_range([&](uint64_t seed, size_t) -> bool { + string be_decrypt_buf = ciphertext.substr(0, max_plaintext_size); + string le_decrypt_buf = ciphertext.substr(0, max_plaintext_size); + if (uses_v3_encryption(version)) { + PSOV3Encryption(seed).encrypt_both_endian( + le_decrypt_buf.data(), + be_decrypt_buf.data(), + be_decrypt_buf.size()); + } else { + PSOV2Encryption(seed).encrypt_both_endian( + le_decrypt_buf.data(), + be_decrypt_buf.data(), + be_decrypt_buf.size()); + } + + for (const auto& plaintext : plaintexts) { + if (!skip_little_endian) { + if (mask_match(le_decrypt_buf.data(), plaintext.first.data(), plaintext.second.data(), plaintext.second.size())) { + return true; + } + } + if (!skip_big_endian) { + if (mask_match(be_decrypt_buf.data(), plaintext.first.data(), plaintext.second.data(), plaintext.second.size())) { + return true; + } + } + } + return false; + }, + 0, 0x100000000, num_threads); + + if (seed < 0x100000000) { + log_info("Found seed %08" PRIX64, seed); + } else { + log_error("No seed found"); + } + }); + +Action a_decode_gci( + "decode-gci", nullptr, +[](Arguments& args) { + string input_filename = args.get(1, false); + if (input_filename.empty() || (input_filename == "-")) { + throw invalid_argument("an input filename is required"); + } + string seed = args.get("seed"); + size_t num_threads = args.get("threads", 0); + bool skip_checksum = args.get("skip-checksum"); + int64_t dec_seed = seed.empty() ? -1 : stoul(seed, nullptr, 16); + auto decoded = decode_gci_data(read_input_data(args), num_threads, dec_seed, skip_checksum); + save_file(input_filename + ".dec", decoded); + }); +Action a_decode_vms( + "decode-vms", nullptr, +[](Arguments& args) { + string input_filename = args.get(1, false); + if (input_filename.empty() || (input_filename == "-")) { + throw invalid_argument("an input filename is required"); + } + string seed = args.get("seed"); + size_t num_threads = args.get("threads", 0); + bool skip_checksum = args.get("skip-checksum"); + int64_t dec_seed = seed.empty() ? -1 : stoul(seed, nullptr, 16); + auto decoded = decode_vms_data(read_input_data(args), num_threads, dec_seed, skip_checksum); + save_file(input_filename + ".dec", decoded); + }); +Action a_decode_dlq( + "decode-dlq", nullptr, +[](Arguments& args) { + string input_filename = args.get(1, false); + if (input_filename.empty() || (input_filename == "-")) { + throw invalid_argument("an input filename is required"); + } + auto decoded = decode_dlq_data(read_input_data(args)); + save_file(input_filename + ".dec", decoded); + }); +Action a_decode_qst( + "decode-qst", "\ + decode-gci INPUT-FILENAME [OPTIONS...]\n\ + decode-vms INPUT-FILENAME [OPTIONS...]\n\ + decode-dlq INPUT-FILENAME\n\ + decode-qst INPUT-FILENAME\n\ + Decode the input quest file into a compressed, unencrypted .bin or .dat\n\ + file (or in the case of decode-qst, both a .bin and a .dat file).\n\ + INPUT-FILENAME must be specified, but there is no OUTPUT-FILENAME; the\n\ + output is written to INPUT-FILENAME.dec (or .bin, or .dat). If the output\n\ + is a .dec file, you can rename it to .bin or .dat manually. DLQ and QST\n\ + decoding are relatively simple operations, but GCI and VMS decoding can be\n\ + computationally expensive if the file is encrypted and doesn\'t contain an\n\ + embedded seed. If you know the player\'s serial number who generated the\n\ + GCI or VMS file, use the --seed=SEED option and give the serial number (as\n\ + a hex-encoded 32-bit integer). If you don\'t know the serial number,\n\ + newserv will find it via a brute-force search, which will take a long time.\n", + +[](Arguments& args) { + string input_filename = args.get(1, false); + if (input_filename.empty() || (input_filename == "-")) { + throw invalid_argument("an input filename is required"); + } + auto files = decode_qst_data(read_input_data(args)); + for (const auto& it : files) { + save_file(input_filename + "-" + it.first, it.second); + } + }); + +Action a_encode_qst( + "encode-qst", "\ + encode-qst INPUT-FILENAME [OUTPUT-FILENAME] [OPTIONS...]\n\ + Encode the input quest file (in .bin/.dat format) into a .qst file. If\n\ + --download is given, generates a download .qst instead of an online .qst.\n\ + Specify the quest\'s game version with one of the --dc-nte, --dc-v1,\n\ + --dc-v2, --pc, --gc-nte, --gc, --gc-ep3, --xb, or --bb options.\n", + +[](Arguments& args) { + string input_filename = args.get(1, false); + if (input_filename.empty() || (input_filename == "-")) { + throw invalid_argument("an input filename is required"); + } + auto version = get_cli_version(args); + bool download = args.get("download"); + + string bin_filename = input_filename; + string dat_filename = ends_with(bin_filename, ".bin") + ? (bin_filename.substr(0, bin_filename.size() - 3) + "dat") + : (bin_filename + ".dat"); + string pvr_filename = ends_with(bin_filename, ".bin") + ? (bin_filename.substr(0, bin_filename.size() - 3) + "pvr") + : (bin_filename + ".pvr"); + auto bin_data = make_shared(load_file(bin_filename)); + auto dat_data = make_shared(load_file(dat_filename)); + shared_ptr pvr_data; + try { + pvr_data = make_shared(load_file(pvr_filename)); + } catch (const cannot_open_file&) { + } + auto vq = make_shared(0, 0, version, 0, bin_data, dat_data, pvr_data); + if (download) { + vq = vq->create_download_quest(); + } + string qst_data = vq->encode_qst(); + + write_output_data(args, qst_data.data(), qst_data.size(), "qst"); + }); + +Action a_disassemble_quest_script( + "disassemble-quest-script", "\ + disassemble-quest-script [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ + Disassemble the input quest script (.bin file) into a text representation\n\ + of the commands and metadata it contains. Specify the quest\'s game version\n\ + with one of the --dc-nte, --dc-v1, --dc-v2, --pc, --gc-nte, --gc, --gc-ep3,\n\ + --xb, or --bb options. If you intend to edit and reassemble the script, use\n\ + the --reassembly option to add explicit label numbers and remove offsets\n\ + and data in code sections.\n", + +[](Arguments& args) { + string data = read_input_data(args); + auto version = get_cli_version(args); + if (!args.get("decompressed")) { + data = prs_decompress(data); + } + uint8_t override_language = args.get("language", 0xFF); + bool reassembly_mode = args.get("reassembly"); + string result = disassemble_quest_script(data.data(), data.size(), version, override_language, reassembly_mode); + write_output_data(args, result.data(), result.size(), "txt"); + }); +Action a_disassemble_quest_map( + "disassemble-quest-map", "\ + disassemble-quest-map [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ + Disassemble the input quest map (.dat file) into a text representation of\n\ + the data it contains.\n", + +[](Arguments& args) { + string data = read_input_data(args); + if (!args.get("decompressed")) { + data = prs_decompress(data); + } + string result = Map::disassemble_quest_data(data.data(), data.size()); + write_output_data(args, result.data(), result.size(), "txt"); + }); +Action a_disassemble_free_map( + "disassemble-free-map", "\ + disassemble-free-map INPUT-FILENAME [OUTPUT-FILENAME]\n\ + Disassemble the input free-roam map (.dat or .evt file) into a text\n\ + representation of the data it contains. Unlike othe disassembly actions,\n\ + this action expects its input to be already decompressed. If the input is\n\ + compressed, use the --compressed option. Also unlike other options, the\n\ + input must be from a file (that is, INPUT-FILENAME is required and cannot\n\ + be \"-\").\n", + +[](Arguments& args) { + const string& input_filename = args.get(1, true); + bool is_events = ends_with(input_filename, ".evt"); + bool is_enemies = ends_with(input_filename, "e.dat") || ends_with(input_filename, "e_s.dat") || ends_with(input_filename, "e_c1.dat") || ends_with(input_filename, "e_d.dat"); + bool is_objects = ends_with(input_filename, "o.dat") || ends_with(input_filename, "o_s.dat") || ends_with(input_filename, "o_c1.dat") || ends_with(input_filename, "o_d.dat"); + if (!is_objects && !is_enemies && !is_events) { + throw runtime_error("cannot determine input file type"); + } + + string data = read_input_data(args); + if (args.get("compressed")) { + data = prs_decompress(data); + } + + string result; + if (is_objects) { + result = Map::disassemble_objects_data(data.data(), data.size()); + } else if (is_enemies) { + result = Map::disassemble_enemies_data(data.data(), data.size()); + } else if (is_events) { + result = Map::disassemble_wave_events_data(data.data(), data.size()); + } else { + throw logic_error("unhandled input type"); + } + result.push_back('\n'); + + write_output_data(args, result.data(), result.size(), "txt"); + }); +Action a_disassemble_set_data_table( + "disassemble-set-data-table", "\ + disassemble-set-data-table [INPUT-FILENAME]\n\ + Show the contents of a SetDataTable.rel file. A version option is required.\n", + +[](Arguments& args) { + Version version = get_cli_version(args); + SetDataTable sdt(version, read_input_data(args)); + string str = sdt.str(); + write_output_data(args, str.data(), str.size(), "txt"); + }); + +Action a_assemble_quest_script( + "assemble-quest-script", "\ + assemble-quest-script [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ + Assemble the input quest script (.txt file) into a compressed .bin file\n\ + usable as an online quest script. If --decompressed is given, produces an\n\ + uncompressed .bind file instead.\n", + +[](Arguments& args) { + string text = read_input_data(args); + string result = assemble_quest_script(text); + bool compress = !args.get("decompressed"); + if (compress) { + result = prs_compress_optimal(result); + } + write_output_data(args, result.data(), result.size(), compress ? "bin" : "bind"); + }); + +Action a_assemble_all_patches( + "assemble-all-patches", "\ + assemble-all-patches\n\ + Assemble all patches in the system/client-functions directory, and produce\n\ + two compiled .bin files for each patch (one unencrypted, for most PSO\n\ + versions, and one encrypted, for PSO GC JP v1.4, JP Ep3, and Ep3 Trial\n\ + Edition). The output files are saved in system/client-functions.\n", + +[](Arguments&) { + auto fci = make_shared("system/client-functions"); + + auto process_code = +[](shared_ptr code, + uint32_t checksum_addr, + uint32_t checksum_size, + uint32_t override_start_addr) -> void { + for (uint8_t encrypted = 0; encrypted < 2; encrypted++) { + StringWriter w; + string data = prepare_send_function_call_data( + code, {}, nullptr, 0, checksum_addr, checksum_size, override_start_addr, encrypted); + w.put(PSOCommandHeaderDCV3{.command = 0xB2, .flag = code->index, .size = data.size() + 4}); + w.write(data); + string out_path = code->source_path + (encrypted ? ".enc.bin" : ".std.bin"); + save_file(out_path, w.str()); + fprintf(stderr, "... %s\n", out_path.c_str()); + } + }; + + for (const auto& it : fci->name_and_specific_version_to_patch_function) { + process_code(it.second, 0, 0, 0); + } + try { + process_code(fci->name_to_function.at("VersionDetectDC"), 0, 0, 0); + } catch (const out_of_range&) { + } + try { + process_code(fci->name_to_function.at("VersionDetectGC"), 0, 0, 0); + } catch (const out_of_range&) { + } + try { + process_code(fci->name_to_function.at("VersionDetectXB"), 0, 0, 0); + } catch (const out_of_range&) { + } + try { + process_code(fci->name_to_function.at("CacheClearFix-Phase1"), 0x80000000, 8, 0x7F2734EC); + } catch (const out_of_range&) { + } + try { + process_code(fci->name_to_function.at("CacheClearFix-Phase2"), 0, 0, 0); + } catch (const out_of_range&) { + } + }); + +void a_extract_archive_fn(Arguments& args) { + string output_prefix = args.get(2, false); + if (output_prefix == "-") { + throw invalid_argument("output prefix cannot be stdout"); + } else if (output_prefix.empty()) { + output_prefix = args.get(1, false); + if (output_prefix.empty() || (output_prefix == "-")) { + throw invalid_argument("an input filename must be given"); + } + output_prefix += "_"; + } + + string data = read_input_data(args); + auto data_shared = make_shared(std::move(data)); + + if (args.get(0) == "extract-afs") { + AFSArchive arch(data_shared); + const auto& all_entries = arch.all_entries(); + for (size_t z = 0; z < all_entries.size(); z++) { + auto e = arch.get(z); + string out_file = string_printf("%s-%zu", output_prefix.c_str(), z); + save_file(out_file.c_str(), e.first, e.second); + fprintf(stderr, "... %s\n", out_file.c_str()); + } + } else if (args.get(0) == "extract-gsl") { + GSLArchive arch(data_shared, args.get("big-endian")); + for (const auto& entry_it : arch.all_entries()) { + auto e = arch.get(entry_it.first); + string out_file = output_prefix + entry_it.first; + save_file(out_file.c_str(), e.first, e.second); + fprintf(stderr, "... %s\n", out_file.c_str()); + } + } else if (args.get(0) == "extract-bml") { + BMLArchive arch(data_shared, args.get("big-endian")); + for (const auto& entry_it : arch.all_entries()) { + { + auto e = arch.get(entry_it.first); + string data = prs_decompress(e.first, e.second); + string out_file = output_prefix + entry_it.first; + save_file(out_file, data); + fprintf(stderr, "... %s\n", out_file.c_str()); + } + + auto gvm_e = arch.get_gvm(entry_it.first); + if (gvm_e.second) { + string data = prs_decompress(gvm_e.first, gvm_e.second); + string out_file = output_prefix + entry_it.first + ".gvm"; + save_file(out_file, data); + fprintf(stderr, "... %s\n", out_file.c_str()); + } + } + } else { + throw logic_error("unimplemented archive type"); + } +} + +Action a_extract_afs("extract-afs", nullptr, a_extract_archive_fn); +Action a_extract_gsl("extract-gsl", nullptr, a_extract_archive_fn); +Action a_extract_bml("extract-bml", "\ + extract-afs [INPUT-FILENAME] [--big-endian]\n\ + extract-gsl [INPUT-FILENAME] [--big-endian]\n\ + extract-bml [INPUT-FILENAME] [--big-endian]\n\ + Extract all files from an AFS, GSL, or BML archive into the current\n\ + directory. input-filename may be specified. If output-filename is\n\ + specified, then it is treated as a prefix which is prepended to the\n\ + filename of each file contained in the archive. If --big-endian is given,\n\ + the archive header is read in GameCube format; otherwise it is read in\n\ + PC/BB format.\n", + a_extract_archive_fn); + +Action a_encode_sjis( + "encode-sjis", nullptr, +[](Arguments& args) { + string data = read_input_data(args); + string result = tt_utf8_to_sega_sjis(data); + write_output_data(args, result.data(), result.size(), "txt"); + }); +Action a_decode_sjis( + "decode-sjis", nullptr, +[](Arguments& args) { + string data = read_input_data(args); + string result = tt_sega_sjis_to_utf8(data); + write_output_data(args, result.data(), result.size(), "txt"); + }); + +Action a_decode_text_archive( + "decode-text-archive", "\ + decode-text-archive [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ + Decode a text archive to JSON. --collections=NUM_COLLECTIONS is given,\n\ + expects a fixed number of collections in the input. If --has-pr3 is given,\n\ + expects the input not to have a REL footer.\n", + +[](Arguments& args) { + string data = read_input_data(args); + bool is_sjis = args.get("japanese"); + + unique_ptr ts; + size_t collection_count = args.get("collections", 0); + if (collection_count) { + ts = make_unique(data, collection_count, !args.get("has-pr3"), is_sjis); + } else { + ts = make_unique(data, args.get("big-endian"), is_sjis); + } + JSON j = ts->json(); + string out_data = j.serialize(JSON::SerializeOption::FORMAT | JSON::SerializeOption::ESCAPE_CONTROLS_ONLY); + write_output_data(args, out_data.data(), out_data.size(), "json"); + }); +Action a_encode_text_archive( + "encode-text-archive", "\ + encode-text-archive [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ + Encode a text archive. Currently only supports GC and Xbox format.\n", + +[](Arguments& args) { + const string& input_filename = args.get(1, false); + const string& output_filename = args.get(2, false); + bool is_sjis = args.get("japanese"); + + auto json = JSON::parse(read_input_data(args)); + BinaryTextAndKeyboardsSet a(json); + auto result = a.serialize(args.get("big-endian"), is_sjis); + if (output_filename.empty()) { + if (input_filename.empty() || (input_filename == "-")) { + throw runtime_error("encoded text archive cannot be written to stdout"); + } + save_file(string_printf("%s.pr2", input_filename.c_str()), result.first); + save_file(string_printf("%s.pr3", input_filename.c_str()), result.second); + } else if (output_filename == "-") { + throw runtime_error("encoded text archive cannot be written to stdout"); + } else { + string out_filename = output_filename; + if (ends_with(out_filename, ".pr2")) { + save_file(out_filename, result.first); + out_filename[out_filename.size() - 1] = '3'; + save_file(out_filename, result.second); + } else { + save_file(out_filename + ".pr2", result.first); + save_file(out_filename + ".pr3", result.second); + } + } + }); + +Action a_decode_unicode_text_set( + "decode-unicode-text-set", nullptr, +[](Arguments& args) { + UnicodeTextSet uts(read_input_data(args)); + JSON j = uts.json(); + string out_data = j.serialize(JSON::SerializeOption::FORMAT | JSON::SerializeOption::ESCAPE_CONTROLS_ONLY); + write_output_data(args, out_data.data(), out_data.size(), "json"); + }); +Action a_encode_unicode_text_set( + "encode-unicode-text-set", "\ + decode-unicode-text-set [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ + encode-unicode-text-set [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ + Decode a Unicode text set (e.g. unitxt_e.prs) to JSON for easy editing, or\n\ + encode a JSON file to a Unicode text set.\n", + +[](Arguments& args) { + UnicodeTextSet uts(JSON::parse(read_input_data(args))); + string encoded = uts.serialize(); + write_output_data(args, encoded.data(), encoded.size(), "prs"); + }); + +Action a_decode_word_select_set( + "decode-word-select-set", "\ + decode-word-select-set [INPUT-FILENAME]\n\ + Decode a Word Select data file and print all the tokens. A version option\n\ + (e.g. --gc-ep3) is required. If the Word Select set is for PC or BB, the\n\ + --unitxt option is also required, and must point to a unitxt file in prs\n\ + or JSON format. For PC (V2), the unitxt_e.prs file should be used; for BB,\n\ + the unitxt_ws_e.prs file should be used.\n", + +[](Arguments& args) { + auto version = get_cli_version(args); + + string unitxt_filename = args.get("unitxt"); + const vector* unitxt_collection; + if (!unitxt_filename.empty()) { + unique_ptr uts; + if (ends_with(unitxt_filename, ".prs")) { + uts = make_unique(load_file(unitxt_filename)); + } else if (ends_with(unitxt_filename, ".json")) { + uts = make_unique(JSON::parse(load_file(unitxt_filename))); + } else { + throw runtime_error("unitxt filename must end in .prs or .json"); + } + unitxt_collection = &uts->get((version == Version::BB_V4) ? 0 : 35); + } else { + unitxt_collection = nullptr; + } + + WordSelectSet ws(read_input_data(args), version, unitxt_collection, args.get("japanese")); + ws.print(stdout); + }); +Action a_print_word_select_table( + "print-word-select-table", "\ + print-word-select-table\n\ + Print the Word Select token translation table. If a version option is\n\ + given, prints the table sorted by token ID for that version. If no version\n\ + option is given, prints the token table sorted by canonical name.\n", + +[](Arguments& args) { + auto s = make_shared(get_config_filename(args)); + s->load_patch_indexes(false); + s->load_text_index(false); + s->load_word_select_table(false); + Version v; + try { + v = get_cli_version(args); + } catch (const runtime_error&) { + v = Version::UNKNOWN; + } + if (v != Version::UNKNOWN) { + s->word_select_table->print_index(stdout, v); + } else { + s->word_select_table->print(stdout); + } + }); + +Action a_cat_client( + "cat-client", "\ + cat-client ADDR:PORT\n\ + Connect to the given server and simulate a PSO client. newserv will then\n\ + print all the received commands to stdout, and forward any commands typed\n\ + into stdin to the remote server. It is assumed that the input and output\n\ + are terminals, so all commands are hex-encoded. The --patch, --dc, --pc,\n\ + --gc, and --bb options can be used to select the command format and\n\ + encryption. If --bb is used, the --key=KEY-NAME option is also required (as\n\ + in decrypt-data above).\n", + +[](Arguments& args) { + auto version = get_cli_version(args); + shared_ptr key; + if (uses_v4_encryption(version)) { + string key_file_name = args.get("key"); + if (key_file_name.empty()) { + throw runtime_error("a key filename is required for BB client emulation"); + } + key = make_shared( + load_object_file("system/blueburst/keys/" + key_file_name + ".nsk")); + } + shared_ptr base(event_base_new(), event_base_free); + auto cat_client_remote = make_sockaddr_storage(parse_netloc(args.get(1))).first; + CatSession session(base, cat_client_remote, get_cli_version(args), key); + event_base_dispatch(base.get()); + }); + +Action a_convert_rare_item_set( + "convert-rare-item-set", "\ + convert-rare-item-set INPUT-FILENAME [OUTPUT-FILENAME] [OPTIONS]\n\ + If OUTPUT-FILENAME is not given, print the contents of a rare item table in\n\ + a human-readable format. Otherwise, convert the input rare item set to a\n\ + different format and write it to OUTPUT-FILENAME. Both filenames must end\n\ + in one of the following extensions:\n\ + .json (newserv JSON rare item table)\n\ + .gsl (PSO BB little-endian GSL archive)\n\ + .gslb (PSO GC big-endian GSL archive)\n\ + .afs (PSO V2 little-endian AFS archive)\n\ + .rel (Schtserv rare table; cannot be used in output filename)\n\ + If the --multiply=X option is given, multiplies all drop rates by X (given\n\ + as a decimal value).\n", + +[](Arguments& args) { + auto version = get_cli_version(args); + + double rate_factor = args.get("multiply", 1.0); + auto s = make_shared(get_config_filename(args)); + s->load_config_early(); + s->load_patch_indexes(false); + s->load_text_index(false); + s->load_item_definitions(false); + s->load_item_name_indexes(false); + + string input_filename = args.get(1, false); + if (input_filename.empty() || (input_filename == "-")) { + throw runtime_error("input filename must be given"); + } + + auto data = make_shared(read_input_data(args)); + shared_ptr rs; + if (ends_with(input_filename, ".json")) { + rs = make_shared(JSON::parse(*data), s->item_name_index(version)); + } else if (ends_with(input_filename, ".gsl")) { + rs = make_shared(GSLArchive(data, false), false); + } else if (ends_with(input_filename, ".gslb")) { + rs = make_shared(GSLArchive(data, true), true); + } else if (ends_with(input_filename, ".afs")) { + rs = make_shared(AFSArchive(data), is_v1(version)); + } else if (ends_with(input_filename, ".rel")) { + rs = make_shared(*data, true); + } else { + throw runtime_error("cannot determine input format; use a filename ending with .json, .gsl, .gslb, .afs, or .rel"); + } + + if (rate_factor != 1.0) { + rs->multiply_all_rates(rate_factor); + } + + string output_filename = args.get(2, false); + if (output_filename.empty() || (output_filename == "-")) { + rs->print_all_collections(stdout, s->item_name_index(version)); + } else if (ends_with(output_filename, ".json")) { + auto json = rs->json(s->item_name_index(version)); + string data = json.serialize(JSON::SerializeOption::FORMAT | JSON::SerializeOption::HEX_INTEGERS | JSON::SerializeOption::SORT_DICT_KEYS); + write_output_data(args, data.data(), data.size(), nullptr); + } else if (ends_with(output_filename, ".gsl")) { + string data = rs->serialize_gsl(args.get("big-endian")); + write_output_data(args, data.data(), data.size(), nullptr); + } else if (ends_with(output_filename, ".gslb")) { + string data = rs->serialize_gsl(true); + write_output_data(args, data.data(), data.size(), nullptr); + } else if (ends_with(output_filename, ".afs")) { + bool is_v1 = ::is_v1(get_cli_version(args, Version::GC_V3)); + string data = rs->serialize_afs(is_v1); + write_output_data(args, data.data(), data.size(), nullptr); + } else { + throw runtime_error("cannot determine output format; use a filename ending with .json, .gsl, .gslb, or .afs"); + } + }); +Action a_convert_common_item_set( + "convert-common-item-set", "\ + convert-common-item-set INPUT-FILENAME [OUTPUT-FILENAME]\n\ + Convert the input rare item set to a JSON representation and write it to\n\ + OUTPUT-FILENAME or stdout. The input filename must end in one of the\n\ + following extensions:\n\ + .json (newserv JSON common item table)\n\ + .gsl (PSO BB little-endian GSL archive)\n\ + .gslb (PSO GC big-endian GSL archive)\n", + +[](Arguments& args) { + string input_filename = args.get(1, false); + if (input_filename.empty() || (input_filename == "-")) { + throw runtime_error("input filename must be given"); + } + + auto data = make_shared(read_input_data(args)); + shared_ptr cs; + if (ends_with(input_filename, ".json")) { + cs = make_shared(JSON::parse(*data)); + } else if (ends_with(input_filename, ".gsl")) { + cs = make_shared(data, args.get("big-endian")); + } else if (ends_with(input_filename, ".gslb")) { + cs = make_shared(data, true); + } else { + throw runtime_error("cannot determine input format; use a filename ending with .json, .gsl, .gslb, or .afs"); + } + + const string& output_filename = args.get(2, false); + if (output_filename.empty()) { + cs->print(stdout); + } else { + auto json = cs->json(); + string json_data = json.serialize(JSON::SerializeOption::FORMAT | JSON::SerializeOption::HEX_INTEGERS | JSON::SerializeOption::SORT_DICT_KEYS); + write_output_data(args, json_data.data(), json_data.size(), "json"); + } + }); + +Action a_describe_item( + "describe-item", "\ + describe-item DATA-OR-DESCRIPTION\n\ + Describe an item. The argument may be the item\'s raw hex code or a textual\n\ + description of the item. If the description contains spaces, it must be\n\ + quoted, such as \"L&K14 COMBAT +10 0/10/15/0/35\".\n", + +[](Arguments& args) { + string description = args.get(1); + auto version = get_cli_version(args); + + auto s = make_shared(get_config_filename(args)); + s->load_config_early(); + s->load_patch_indexes(false); + s->load_text_index(false); + s->load_item_definitions(false); + s->load_item_name_indexes(false); + auto name_index = s->item_name_index(version); + + ItemData item = name_index->parse_item_description(description); + + if (args.get("decode")) { + item.decode_for_version(version); + } + + string desc = name_index->describe_item(item); + string desc_colored = name_index->describe_item(item, true); + + log_info("Data (decoded): %02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX -------- %02hhX%02hhX%02hhX%02hhX", + item.data1[0], item.data1[1], item.data1[2], item.data1[3], + item.data1[4], item.data1[5], item.data1[6], item.data1[7], + item.data1[8], item.data1[9], item.data1[10], item.data1[11], + item.data2[0], item.data2[1], item.data2[2], item.data2[3]); + + ItemData item_v2 = item; + item_v2.encode_for_version(Version::PC_V2, s->item_parameter_table_for_encode(Version::PC_V2)); + ItemData item_v2_decoded = item_v2; + item_v2_decoded.decode_for_version(Version::PC_V2); + + log_info("Data (V2-encoded): %02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX -------- %02hhX%02hhX%02hhX%02hhX", + item_v2.data1[0], item_v2.data1[1], item_v2.data1[2], item_v2.data1[3], + item_v2.data1[4], item_v2.data1[5], item_v2.data1[6], item_v2.data1[7], + item_v2.data1[8], item_v2.data1[9], item_v2.data1[10], item_v2.data1[11], + item_v2.data2[0], item_v2.data2[1], item_v2.data2[2], item_v2.data2[3]); + if (item_v2_decoded != item) { + log_warning("V2-decoded data does not match original data"); + log_warning("Data (V2-decoded): %02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX -------- %02hhX%02hhX%02hhX%02hhX", + item_v2_decoded.data1[0], item_v2_decoded.data1[1], item_v2_decoded.data1[2], item_v2_decoded.data1[3], + item_v2_decoded.data1[4], item_v2_decoded.data1[5], item_v2_decoded.data1[6], item_v2_decoded.data1[7], + item_v2_decoded.data1[8], item_v2_decoded.data1[9], item_v2_decoded.data1[10], item_v2_decoded.data1[11], + item_v2_decoded.data2[0], item_v2_decoded.data2[1], item_v2_decoded.data2[2], item_v2_decoded.data2[3]); + } + + ItemData item_gc = item; + item_gc.encode_for_version(Version::GC_V3, s->item_parameter_table_for_encode(Version::GC_V3)); + ItemData item_gc_decoded = item_gc; + item_gc_decoded.decode_for_version(Version::GC_V3); + + log_info("Data (GC-encoded): %02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX -------- %02hhX%02hhX%02hhX%02hhX", + item_gc.data1[0], item_gc.data1[1], item_gc.data1[2], item_gc.data1[3], + item_gc.data1[4], item_gc.data1[5], item_gc.data1[6], item_gc.data1[7], + item_gc.data1[8], item_gc.data1[9], item_gc.data1[10], item_gc.data1[11], + item_gc.data2[0], item_gc.data2[1], item_gc.data2[2], item_gc.data2[3]); + if (item_gc_decoded != item) { + log_warning("GC-decoded data does not match original data"); + log_warning("Data (GC-decoded): %02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX %02hhX%02hhX%02hhX%02hhX -------- %02hhX%02hhX%02hhX%02hhX", + item_gc_decoded.data1[0], item_gc_decoded.data1[1], item_gc_decoded.data1[2], item_gc_decoded.data1[3], + item_gc_decoded.data1[4], item_gc_decoded.data1[5], item_gc_decoded.data1[6], item_gc_decoded.data1[7], + item_gc_decoded.data1[8], item_gc_decoded.data1[9], item_gc_decoded.data1[10], item_gc_decoded.data1[11], + item_gc_decoded.data2[0], item_gc_decoded.data2[1], item_gc_decoded.data2[2], item_gc_decoded.data2[3]); + } + + log_info("Description: %s", desc.c_str()); + log_info("Description (in-game): %s", desc_colored.c_str()); + + size_t purchase_price = s->item_parameter_table(Version::BB_V4)->price_for_item(item); + size_t sale_price = purchase_price >> 3; + log_info("Purchase price: %zu; sale price: %zu", purchase_price, sale_price); + }); + +Action a_name_all_items( + "name-all-items", nullptr, +[](Arguments& args) { + auto s = make_shared(get_config_filename(args)); + s->load_config_early(); + s->load_patch_indexes(false); + s->load_text_index(false); + s->load_item_definitions(false); + s->load_item_name_indexes(false); + s->load_config_early(); + + set all_primary_identifiers; + for (const auto& index : s->item_name_indexes) { + if (index) { + for (const auto& it : index->all_by_primary_identifier()) { + all_primary_identifiers.emplace(it.first); + } + } + } + + fprintf(stderr, "IDENT :"); + for (size_t v_s = 0; v_s < NUM_VERSIONS; v_s++) { + Version version = static_cast(v_s); + const auto& index = s->item_name_indexes.at(v_s); + if (index) { + fprintf(stderr, " %30s ", name_for_enum(version)); + } + } + fputc('\n', stderr); + + for (uint32_t primary_identifier : all_primary_identifiers) { + fprintf(stderr, "%08" PRIX32 ":", primary_identifier); + for (size_t v_s = 0; v_s < NUM_VERSIONS; v_s++) { + const auto& index = s->item_name_indexes.at(v_s); + if (index) { + Version version = static_cast(v_s); + auto pmt = s->item_parameter_table(version); + ItemData item = ItemData::from_primary_identifier(*s->item_stack_limits(version), primary_identifier); + string name = index->describe_item(item); + try { + bool is_rare = pmt->is_item_rare(item); + fprintf(stderr, " %30s%s", name.c_str(), is_rare ? " (*)" : " "); + } catch (const out_of_range&) { + fprintf(stderr, " "); + } + } + } + fputc('\n', stderr); + } + }); + +Action a_print_item_parameter_tables( + "print-item-tables", nullptr, +[](Arguments& args) { + auto s = make_shared(get_config_filename(args)); + s->load_patch_indexes(false); + s->load_text_index(false); + s->load_item_definitions(false); + s->load_item_name_indexes(false); + for (size_t v_s = 0; v_s < NUM_VERSIONS; v_s++) { + const auto& index = s->item_name_indexes.at(v_s); + if (index) { + Version v = static_cast(v_s); + fprintf(stdout, "======== %s\n", name_for_enum(v)); + index->print_table(stdout); + } + } + }); + +Action a_show_ep3_cards( + "show-ep3-cards", "\ + show-ep3-cards\n\ + Print the Episode 3 card definitions from the system/ep3 directory in a\n\ + human-readable format.\n", + +[](Arguments& args) { + bool one_line = args.get("one-line"); + + auto s = make_shared(get_config_filename(args)); + s->load_ep3_cards(false); + + unique_ptr text_english; + try { + JSON json = JSON::parse(load_file("system/ep3/text-english.json")); + text_english = make_unique(json); + } catch (const exception& e) { + } + + auto card_ids = s->ep3_card_index->all_ids(); + log_info("%zu card definitions", card_ids.size()); + for (uint32_t card_id : card_ids) { + auto entry = s->ep3_card_index->definition_for_id(card_id); + string def_str = entry->def.str(one_line, text_english.get()); + if (one_line) { + fprintf(stdout, "%s\n", def_str.c_str()); + } else { + fprintf(stdout, "%s\n", def_str.c_str()); + if (!entry->debug_tags.empty()) { + string tags = join(entry->debug_tags, ", "); + fprintf(stdout, " Tags: %s\n", tags.c_str()); + } + if (!entry->dice_caption.empty()) { + fprintf(stdout, " Dice caption: %s\n", entry->dice_caption.c_str()); + } + if (!entry->dice_caption.empty()) { + fprintf(stdout, " Dice text: %s\n", entry->dice_text.c_str()); + } + if (!entry->text.empty()) { + string text = str_replace_all(entry->text, "\n", "\n "); + strip_trailing_whitespace(text); + fprintf(stdout, " Text:\n %s\n", text.c_str()); + } + fputc('\n', stdout); + } + } + }); + +Action a_generate_ep3_cards_html( + "generate-ep3-cards-html", "\ + generate-ep3-cards-html [--ep3-nte] [--threads=N]\n\ + Generate an HTML file describing all Episode 3 card definitions from the\n\ + system/ep3 directory. If --ep3-nte is given, use the Trial Edition card\n\ + definitions instead.\n", + +[](Arguments& args) { + size_t num_threads = args.get("threads", 0); + + bool is_nte = (get_cli_version(args, Version::GC_EP3) == Version::GC_EP3_NTE); + + auto s = make_shared(get_config_filename(args)); + s->load_patch_indexes(false); + s->load_text_index(false); + s->load_ep3_cards(false); + + shared_ptr text_english; + try { + text_english = s->text_index->get(Version::GC_EP3, 1); + } catch (const out_of_range&) { + } + + struct CardInfo { + shared_ptr ce; + string small_filename; + string medium_filename; + string large_filename; + string small_data_url; + string medium_data_url; + string large_data_url; + + bool is_empty() const { + return (this->ce == nullptr) && this->small_data_url.empty() && this->medium_data_url.empty() && this->large_data_url.empty(); + } + }; + auto card_index = is_nte ? s->ep3_card_index_trial : s->ep3_card_index; + vector infos; + for (uint32_t card_id : card_index->all_ids()) { + if (infos.size() <= card_id) { + infos.resize(card_id + 1); + } + infos[card_id].ce = card_index->definition_for_id(card_id); + } + bool show_large_column = false; + bool show_medium_column = false; + bool show_small_column = false; + for (const auto& filename : list_directory_sorted("system/ep3/cardtex")) { + if ((filename[0] == 'C' || filename[0] == 'M' || filename[0] == 'L') && (filename[1] == '_')) { + size_t card_id = stoull(filename.substr(2, 3), nullptr, 10); + if (infos.size() <= card_id) { + infos.resize(card_id + 1); + } + auto& info = infos[card_id]; + if (filename[0] == 'C') { + info.large_filename = "system/ep3/cardtex/" + filename; + show_large_column = true; + } else if (filename[0] == 'L') { + info.medium_filename = "system/ep3/cardtex/" + filename; + show_medium_column = true; + } else if (filename[0] == 'M') { + info.small_filename = "system/ep3/cardtex/" + filename; + show_small_column = true; + } + } + } + + parallel_range([&](uint32_t index, size_t) -> bool { + auto& info = infos[index]; + if (!info.large_filename.empty()) { + Image img(info.large_filename); + Image cropped(512, 399); + cropped.blit(img, 0, 0, 512, 399, 0, 0); + info.large_data_url = cropped.png_data_url(); + } + if (!info.medium_filename.empty()) { + Image img(info.medium_filename); + Image cropped(184, 144); + cropped.blit(img, 0, 0, 184, 144, 0, 0); + info.medium_data_url = cropped.png_data_url(); + } + if (!info.small_filename.empty()) { + Image img(info.small_filename); + Image cropped(58, 43); + cropped.blit(img, 0, 0, 58, 43, 0, 0); + info.small_data_url = cropped.png_data_url(); + } + return false; + }, + 0, infos.size(), num_threads); + + deque blocks; + blocks.emplace_back("Phantasy Star Online Episode III cards"); + blocks.emplace_back("
Legend:
Card has no definition and is obviously incomplete
Card is unobtainable in random draws but may be a quest or event reward
Card is obtainable in random draws


"); + blocks.emplace_back(""); + if (show_small_column) { + blocks.emplace_back(""); + } + if (show_medium_column) { + blocks.emplace_back(""); + } + if (show_large_column) { + blocks.emplace_back(""); + } + blocks.emplace_back(""); + for (size_t card_id = 0; card_id < infos.size(); card_id++) { + const auto& entry = infos[card_id]; + if (entry.is_empty()) { + continue; + } + + const char* background_color; + if (!entry.ce) { + background_color = "#663333"; + } else if (entry.ce->def.cannot_drop || + ((entry.ce->def.rank == Episode3::CardRank::D1) || (entry.ce->def.rank == Episode3::CardRank::D2) || (entry.ce->def.rank == Episode3::CardRank::D3)) || + ((entry.ce->def.card_class() == Episode3::CardClass::BOSS_ATTACK_ACTION) || (entry.ce->def.card_class() == Episode3::CardClass::BOSS_TECH)) || + ((entry.ce->def.drop_rates[0] == 6) && (entry.ce->def.drop_rates[1] == 6))) { + background_color = "#336633"; + } else { + background_color = "#333333"; + } + + blocks.emplace_back(string_printf("", background_color)); + blocks.emplace_back(string_printf("", card_id)); + if (show_small_column) { + blocks.emplace_back(""); + } + if (show_medium_column) { + blocks.emplace_back(""); + } + if (show_large_column) { + blocks.emplace_back(""); + } + blocks.emplace_back(""); + } + blocks.emplace_back("
IDSmallMediumLargeTextDisassembly
%04zX
"); + if (!entry.small_data_url.empty()) { + blocks.emplace_back(""); + } + blocks.emplace_back(""); + if (!entry.medium_data_url.empty()) { + blocks.emplace_back(""); + } + blocks.emplace_back(""); + if (!entry.large_data_url.empty()) { + blocks.emplace_back(""); + } + blocks.emplace_back(""); + if (entry.ce) { + blocks.emplace_back("
");
+          blocks.emplace_back(entry.ce->text);
+          blocks.emplace_back("
");
+          blocks.emplace_back(entry.ce->def.str(false, text_english.get()));
+          blocks.emplace_back("
"); + } else { + blocks.emplace_back("
Definition is missing
"); + } + blocks.emplace_back("
"); + + save_file("cards.html", join(blocks, "")); + }); + +Action a_show_ep3_maps( + "show-ep3-maps", "\ + show-ep3-maps\n\ + Print the Episode 3 maps from the system/ep3 directory in a (sort of)\n\ + human-readable format.\n", + +[](Arguments& args) { + config_log.info("Collecting Episode 3 data"); + + auto s = make_shared(get_config_filename(args)); + s->load_ep3_cards(false); + s->load_ep3_maps(false); + + auto map_ids = s->ep3_map_index->all_numbers(); + log_info("%zu maps", map_ids.size()); + for (uint32_t map_id : map_ids) { + auto map = s->ep3_map_index->for_number(map_id); + const auto& vms = map->all_versions(); + for (size_t language = 0; language < vms.size(); language++) { + if (!vms[language]) { + continue; + } + string map_s = vms[language]->map->str(s->ep3_card_index.get(), language); + fprintf(stdout, "(%c) %s\n", char_for_language_code(language), map_s.c_str()); + } + } + }); + +Action a_show_battle_params( + "show-battle-params", "\ + show-battle-params\n\ + Print the Blue Burst battle parameters from the system/blueburst directory\n\ + in a human-readable format.\n", + +[](Arguments& args) { + auto s = make_shared(get_config_filename(args)); + s->load_patch_indexes(false); + s->load_battle_params(false); + + fprintf(stdout, "Episode 1 multi\n"); + s->battle_params->get_table(false, Episode::EP1).print(stdout); + fprintf(stdout, "Episode 1 solo\n"); + s->battle_params->get_table(true, Episode::EP1).print(stdout); + fprintf(stdout, "Episode 2 multi\n"); + s->battle_params->get_table(false, Episode::EP2).print(stdout); + fprintf(stdout, "Episode 2 solo\n"); + s->battle_params->get_table(true, Episode::EP2).print(stdout); + fprintf(stdout, "Episode 4 multi\n"); + s->battle_params->get_table(false, Episode::EP4).print(stdout); + fprintf(stdout, "Episode 4 solo\n"); + s->battle_params->get_table(true, Episode::EP4).print(stdout); + }); + +Action a_find_rare_enemy_seeds( + "find-rare-enemy-seeds", "\ + find-rare-enemy-seeds OPTIONS...\n\ + Search all possible rare seeds to find those that produce one or more rare\n\ + enemies in any set of variations. A version option (e.g. --gc) is required;\n\ + an episode option (--ep1, --ep2, or --ep4) is also required. A difficulty\n\ + option (--normal, --hard, --very-hard, or --ultimate) may be given; this\n\ + affects which rare rates from config.json are used if --bb was given.\n\ + Similarly, --battle, --challenge, or --solo may also be given; this affects\n\ + which variations are used on all versions and which rare rates to use for\n\ + BB. --threads=COUNT controls the number of threads to use for the search\n\ + by default, one thread per CPU core is used. --min-count specifies how many\n\ + rare enemies must be found to output the seed. Finally, --quest=NAME may be\n\ + given to use that quest\'s map instead of the free-roam maps.\n", + +[](Arguments& args) { + auto version = get_cli_version(args); + auto episode = get_cli_episode(args); + auto difficulty = get_cli_difficulty(args); + auto mode = get_cli_game_mode(args); + size_t num_threads = args.get("threads", 0); + size_t min_count = args.get("min-count", 1); + string quest_name = args.get("quest", false); + + auto s = make_shared(get_config_filename(args)); + shared_ptr vq; + if (!quest_name.empty()) { + s->load_config_early(); + s->load_quest_index(false); + auto q = s->quest_index(version)->get(quest_name); + if (!q) { + throw runtime_error("quest does not exist"); + } + vq = q->version(version, 1); + if (!vq) { + throw runtime_error("quest version does not exist"); + } + } else if (version == Version::BB_V4) { + s->load_config_early(); + } else if (version == Version::PC_V2) { + s->load_patch_indexes(false); + } else { + s->clear_map_file_caches(); + } + + shared_ptr rare_rates; + if (version != Version::BB_V4) { + rare_rates = Map::DEFAULT_RARE_ENEMIES; + } else if (mode == GameMode::CHALLENGE) { + rare_rates = s->rare_enemy_rates_challenge; + } else { + rare_rates = s->rare_enemy_rates_by_difficulty[difficulty]; + } + + mutex output_lock; + auto thread_fn = [&](uint64_t seed, size_t) -> bool { + auto random_crypt = make_shared(seed); + parray variations; + + shared_ptr map; + if (vq) { + if (!vq->dat_contents_decompressed) { + throw runtime_error("quest does not have DAT data"); + } + map = Lobby::load_maps( + version, episode, difficulty, 0, 0, rare_rates, seed, random_crypt, vq->dat_contents_decompressed); + + } else { + generate_variations_deprecated(variations, random_crypt, version, episode, (mode == GameMode::SOLO)); + map = Lobby::load_maps( + version, + episode, + mode, + difficulty, + 0, + 0, + s->set_data_table(version, episode, mode, difficulty), + bind(&ServerState::load_map_file, s.get(), placeholders::_1, placeholders::_2), + rare_rates, + seed, + random_crypt, + variations); + } + + vector rare_indexes; + for (size_t z = 0; z < map->enemies.size(); z++) { + if (enemy_type_is_rare(map->enemies[z].type)) { + rare_indexes.emplace_back(z); + } + } + + if (rare_indexes.size() >= min_count) { + lock_guard g(output_lock); + fprintf(stdout, "%08" PRIX64 ":", seed); + for (size_t index : rare_indexes) { + fprintf(stdout, " E-%zX:%s", index, name_for_enum(map->enemies[index].type)); + } + fprintf(stdout, "\n"); + } + + return false; + }; + + parallel_range(thread_fn, 0, 0x100000000, num_threads, nullptr); + }); + +Action a_load_maps_test( + "load-maps-test", nullptr, +[](Arguments& args) { + using SDT = SetDataTable; + auto s = make_shared(get_config_filename(args)); + s->load_config_early(); + s->clear_map_file_caches(); + s->load_patch_indexes(false); + s->load_set_data_tables(false); + s->load_quest_index(false); + for (size_t v_s = NUM_PATCH_VERSIONS; v_s < NUM_VERSIONS; v_s++) { + Version v = static_cast(v_s); + if (is_ep3(v)) { + continue; + } + const array episodes = {Episode::EP1, Episode::EP2, Episode::EP4}; + for (Episode episode : episodes) { + if (episode == Episode::EP4 && !is_v4(v)) { + continue; + } + if (episode == Episode::EP2 && is_v1_or_v2(v) && (v != Version::GC_NTE)) { + continue; + } + const array modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO}; + for (GameMode mode : modes) { + if ((mode == GameMode::BATTLE || mode == GameMode::CHALLENGE) && is_v1(v)) { + continue; + } + if (mode == GameMode::SOLO && !is_v4(v)) { + continue; + } + for (uint8_t difficulty = 0; difficulty < 4; difficulty++) { + if (difficulty == 3 && is_v1(v)) { + continue; + } + auto sdt = s->set_data_table(v, episode, mode, difficulty); + for (uint8_t floor = 0; floor < 0x12; floor++) { + auto variation_maxes = sdt->num_free_roam_variations_for_floor(episode, mode == GameMode::SOLO, floor); + for (size_t var1 = 0; var1 < variation_maxes.first; var1++) { + for (size_t var2 = 0; var2 < variation_maxes.second; var2++) { + auto enemies_filename = sdt->map_filename_for_variation( + floor, var1, var2, episode, mode, SDT::FilenameType::ENEMIES); + auto objects_filename = sdt->map_filename_for_variation( + floor, var1, var2, episode, mode, SDT::FilenameType::OBJECTS); + auto events_filename = sdt->map_filename_for_variation( + floor, var1, var2, episode, mode, SDT::FilenameType::EVENTS); + + fprintf(stderr, "... %s %s %s %s %02hhX %zX %zX", + name_for_enum(v), name_for_episode(episode), name_for_mode(mode), name_for_difficulty(difficulty), floor, var1, var2); + auto map = make_shared(v, 0, 0, nullptr); + if (!enemies_filename.empty()) { + fprintf(stderr, " [%s => ", enemies_filename.c_str()); + auto map_data = s->load_map_file(v, enemies_filename); + if (map_data) { + map->add_enemies_from_map_data( + episode, difficulty, 0, 0, map_data->data(), map_data->size(), Map::DEFAULT_RARE_ENEMIES); + fprintf(stderr, "%zu enemies, %zu sets]", map->enemies.size(), map->enemy_set_flags.size()); + } else { + fprintf(stderr, "__MISSING__]"); + } + } + if (!objects_filename.empty()) { + fprintf(stderr, " [%s => ", objects_filename.c_str()); + auto map_data = s->load_map_file(v, objects_filename); + if (map_data) { + map->add_objects_from_map_data(floor, map_data->data(), map_data->size()); + fprintf(stderr, "%zu objects]", map->objects.size()); + } else { + fprintf(stderr, "__MISSING__]"); + } + } + if (!events_filename.empty()) { + fprintf(stderr, " [%s => ", events_filename.c_str()); + auto map_data = s->load_map_file(v, events_filename); + if (map_data) { + map->add_events_from_map_data(floor, map_data->data(), map_data->size()); + fprintf(stderr, "%zu events, %zu action bytes]", map->events.size(), map->event_action_stream.size()); + } else { + fprintf(stderr, "__MISSING__]"); + } + } + fputc('\n', stderr); + } + } + } + } + } + } + } + + for (const auto& q_it : s->default_quest_index->quests_by_number) { + for (const auto& vq_it : q_it.second->versions) { + auto vq = vq_it.second; + shared_ptr map = Lobby::load_maps( + vq->version, + vq->episode, + 0, + 0, + 0, + Map::DEFAULT_RARE_ENEMIES, + 0, + nullptr, + vq->dat_contents_decompressed); + fprintf(stderr, "... %" PRIu32 " (%s) %s %s %s => %zu enemies (%zu sets), %zu objects, %zu events\n", + vq->quest_number, + vq->name.c_str(), + name_for_episode(vq->episode), + name_for_enum(vq->version), + name_for_language_code(vq->language), + map->enemies.size(), + map->enemy_set_flags.size(), + map->objects.size(), + map->events.size()); + } + } + }); + +Action a_parse_object_graph( + "parse-object-graph", nullptr, +[](Arguments& args) { + uint32_t root_object_address = args.get("root", Arguments::IntFormat::HEX); + string data = read_input_data(args); + PSOGCObjectGraph g(data, root_object_address); + g.print(stdout); + }); + +Action a_generate_dc_serial_number( + "generate-dc-serial-number", "\ + generate-dc-serial-number DOMAIN SUBDOMAIN\n\ + Generate a PSO DC serial number. DOMAIN should be 0 for Japanese, 1 for\n\ + USA, or 2 for Europe. SUBDOMAIN should be 0 for v1, or 1 for v2.\n", + +[](Arguments& args) { + uint8_t domain = args.get(1); + uint8_t subdomain = args.get(2); + string serial_number = generate_dc_serial_number(domain, subdomain); + fprintf(stdout, "%s\n", serial_number.c_str()); + }); +Action a_generate_all_dc_serial_numbers( + "generate-all-dc-serial-numbers", "\ + generate-all-dc-serial-numbers\n\ + Generate all possible PSO DC serial numbers.\n", + +[](Arguments& args) { + size_t num_threads = args.get("threads", 0); + + auto serial_numbers = generate_all_dc_serial_numbers(); + fprintf(stdout, "%zu (0x%zX) serial numbers found\n", serial_numbers.size(), serial_numbers.size()); + for (const auto& it : serial_numbers) { + fprintf(stdout, "Valid serial number: %08" PRIX32, it.first); + for (uint8_t where : it.second) { + fprintf(stdout, " (domain=%hhu, subdomain=%hhu)", + static_cast((where >> 2) & 3), + static_cast(where & 3)); + } + fputc('\n', stdout); + } + + atomic num_valid_serial_numbers = 0; + mutex output_lock; + auto thread_fn = [&](uint64_t serial_number, size_t) -> bool { + for (uint8_t domain = 0; domain < 3; domain++) { + for (uint8_t subdomain = 0; subdomain < 3; subdomain++) { + if (dc_serial_number_is_valid_fast(serial_number, domain, subdomain)) { + num_valid_serial_numbers++; + lock_guard g(output_lock); + fprintf(stdout, "Valid serial number: %08" PRIX64 " (domain=%hhu, subdomain=%hhu)\n", serial_number, domain, subdomain); + } + } + } + return false; + }; + auto progress_fn = [&](uint64_t, uint64_t, uint64_t current_value, uint64_t) -> void { + uint64_t num_found = num_valid_serial_numbers.load(); + fprintf(stderr, "... %08" PRIX64 " %" PRId64 " (0x%" PRIX64 ") found\r", + current_value, num_found, num_found); + }; + parallel_range(thread_fn, 0, 0x100000000, num_threads, progress_fn); + }); +Action a_inspect_dc_serial_number( + "inspect-dc-serial-number", "\ + inspect-dc-serial-number SERIAL-NUMBER\n\ + Show which domain and subdomain the serial number belongs to. (As with\n\ + generate-dc-serial-number, described above, this will tell you which PSO\n\ + version it is valid for.)\n", + +[](Arguments& args) { + const string& serial_number_str = args.get(1, false); + if (serial_number_str.empty()) { + throw invalid_argument("no serial number given"); + } + size_t num_valid_subdomains = 0; + for (uint8_t domain = 0; domain < 3; domain++) { + for (uint8_t subdomain = 0; subdomain < 3; subdomain++) { + if (dc_serial_number_is_valid_fast(serial_number_str, domain, subdomain)) { + fprintf(stdout, "%s is valid in domain %hhu subdomain %hhu\n", serial_number_str.c_str(), domain, subdomain); + num_valid_subdomains++; + } + } + } + if (num_valid_subdomains == 0) { + fprintf(stdout, "%s is not valid in any domain\n", serial_number_str.c_str()); + } + }); +Action a_dc_serial_number_speed_test( + "dc-serial-number-speed-test", "\ + dc-serial-number-speed-test\n\ + Run a speed test of the two DC serial number validation functions.\n", + +[](Arguments& args) { + const string& seed = args.get("seed"); + if (seed.empty()) { + dc_serial_number_speed_test(); + } else { + dc_serial_number_speed_test(stoul(seed, nullptr, 16)); + } + }); + +Action a_address_translator( + "address-translator", nullptr, +[](Arguments& args) { + const string& dir = args.get(1, false); + if (dir.empty() || (dir == "-")) { + throw invalid_argument("a directory name is required"); + } + run_address_translator(dir, args.get(2, false), args.get(3, false)); + }); + +Action a_diff_dol_files( + "diff-dol-files", nullptr, +[](Arguments& args) { + const string& a_filename = args.get(1); + const string& b_filename = args.get(2); + auto result = diff_dol_files(a_filename, b_filename); + for (const auto& it : result) { + string data = format_data_string(it.second, nullptr, FormatDataFlags::HEX_ONLY); + fprintf(stdout, "%08" PRIX32 " %s\n", it.first, data.c_str()); + } + }); + +Action a_generate_hangame_creds( + "generate-hangame-creds", nullptr, +[](Arguments& args) { + const string& user_id = args.get(1); + const string& token = args.get(2); + const string& unused = args.get(3, false); + string hex = format_data_string(encode_psobb_hangame_credentials(user_id, token, unused)); + fprintf(stdout, "psobb.exe 1196310600 %s\n", hex.c_str()); + }); + +Action a_format_ep3_battle_record( + "format-ep3-battle-record", nullptr, +[](Arguments& args) { + string data = read_input_data(args); + Episode3::BattleRecord rec(data); + rec.print(stdout); + }); + +Action a_replay_ep3_battle_commands( + "replay-ep3-battle-commands", nullptr, +[](Arguments& args) { + auto s = make_shared(get_config_filename(args)); + s->load_ep3_cards(false); + s->load_ep3_maps(false); + + int64_t base_seed = args.get("seed", -1); + bool is_trial = (get_cli_version(args, Version::GC_EP3) == Version::GC_EP3_NTE); + + auto input = read_input_data(args); + vector commands; + for (const auto& line : split(input, '\n')) { + string data = parse_data_string(line); + if (!data.empty()) { + commands.emplace_back(std::move(data)); + } + } + + auto run_replay = [&](int64_t seed, size_t) { + Episode3::Server::Options options = { + .card_index = s->ep3_card_index, + .map_index = s->ep3_map_index, + .behavior_flags = 0x0092, + .opt_rand_stream = nullptr, + .opt_rand_crypt = (seed >= 0) ? make_shared(seed) : nullptr, + .tournament = nullptr, + .trap_card_ids = {}, + }; + if (is_trial) { + options.behavior_flags |= Episode3::BehaviorFlag::IS_TRIAL_EDITION; + } + if (base_seed >= 0) { + options.behavior_flags |= Episode3::BehaviorFlag::LOG_COMMANDS_IF_LOBBY_MISSING; + } + auto server = make_shared(nullptr, std::move(options)); + server->init(); + for (const auto& command : commands) { + server->on_server_data_input(nullptr, command); + } + return false; + }; + + if (base_seed >= 0) { + run_replay(base_seed, 0); + } else { + size_t num_threads = args.get("threads", 0); + parallel_range(run_replay, 0, 0x100000000, num_threads); + } + }); + +Action a_replay_ep3_battle_record( + "replay-ep3-battle-record", nullptr, +[](Arguments& args) { + auto rec = make_shared(read_input_data(args)); + + auto s = make_shared(get_config_filename(args)); + s->load_ep3_cards(false); + s->load_ep3_maps(false); + + bool is_trial = (get_cli_version(args, Version::GC_EP3) == Version::GC_EP3_NTE); + + Episode3::Server::Options options = { + .card_index = s->ep3_card_index, + .map_index = s->ep3_map_index, + .behavior_flags = (Episode3::BehaviorFlag::IGNORE_CARD_COUNTS | + Episode3::BehaviorFlag::ENABLE_STATUS_MESSAGES | + Episode3::BehaviorFlag::DISABLE_MASKING | + Episode3::BehaviorFlag::LOG_COMMANDS_IF_LOBBY_MISSING), + .opt_rand_stream = make_shared(rec->get_random_stream()), + .opt_rand_crypt = nullptr, + .tournament = nullptr, + .trap_card_ids = {}, + }; + if (is_trial) { + options.behavior_flags |= Episode3::BehaviorFlag::IS_TRIAL_EDITION; + } + options.behavior_flags |= Episode3::BehaviorFlag::LOG_COMMANDS_IF_LOBBY_MISSING; + auto server = make_shared(nullptr, std::move(options)); + server->init(); + for (const auto& command : rec->get_all_server_data_commands()) { + log_info("Server data command"); + print_data(stderr, command, 0, nullptr, PrintDataFlags::PRINT_ASCII | PrintDataFlags::DISABLE_COLOR | PrintDataFlags::OFFSET_16_BITS); + server->on_server_data_input(nullptr, command); + } + }); + +Action a_disassemble_ep3_battle_record( + "disassemble-ep3-battle-record", nullptr, +[](Arguments& args) { + Episode3::BattleRecord(read_input_data(args)).print(stdout); + }); + +Action a_run_server_replay_log( + "", nullptr, +[](Arguments& args) { + { + string build_date = format_time(BUILD_TIMESTAMP); + config_log.info("newserv %s compiled at %s", GIT_REVISION_HASH, build_date.c_str()); + } + + if (evthread_use_pthreads()) { + throw runtime_error("failed to set up libevent threads"); + } + + if (!isdir("system/players")) { + config_log.info("Players directory does not exist; creating it"); + mkdir("system/players", 0755); + } + + const string& replay_log_filename = args.get("replay-log"); + bool is_replay = !replay_log_filename.empty(); + + signal(SIGPIPE, SIG_IGN); + if (isatty(fileno(stderr))) { + use_terminal_colors = true; + } + + if (is_replay) { + set_function_compiler_available(false); + } + + shared_ptr base(event_base_new(), event_base_free); + auto state = make_shared(base, get_config_filename(args), is_replay); + state->load_all(); + + if (state->dns_server_port && !is_replay) { + if (!state->dns_server_addr.empty()) { + config_log.info("Starting DNS server on %s:%hu", state->dns_server_addr.c_str(), state->dns_server_port); + } else { + config_log.info("Starting DNS server on port %hu", state->dns_server_port); + } + state->dns_server = make_shared( + base, state->local_address, state->external_address, state->banned_ipv4_ranges); + state->dns_server->listen(state->dns_server_addr, state->dns_server_port); + } else { + config_log.info("DNS server is disabled"); + } + + shared_ptr shell; + shared_ptr replay_session; + shared_ptr http_server; + if (is_replay) { + config_log.info("Starting proxy server"); + state->proxy_server = make_shared(base, state); + config_log.info("Starting game server"); + state->game_server = make_shared(base, state); + + auto nop_destructor = +[](FILE*) {}; + shared_ptr log_f(stdin, nop_destructor); + if (replay_log_filename != "-") { + log_f = fopen_shared(replay_log_filename, "rt"); + } + + replay_session = make_shared(base, log_f.get(), state, args.get("require-basic-credentials")); + replay_session->start(); + + } else { + config_log.info("Opening sockets"); + for (const auto& it : state->name_to_port_config) { + const auto& pc = it.second; + if (pc->behavior == ServerBehavior::PROXY_SERVER) { + if (!state->proxy_server.get()) { + config_log.info("Starting proxy server"); + state->proxy_server = make_shared(base, state); + } + if (state->proxy_server.get()) { + // For PC and GC, proxy sessions are dynamically created when a client + // picks a destination from the menu. For patch and BB clients, there's + // no way to ask the client which destination they want, so only one + // destination is supported, and we have to manually specify the + // destination netloc here. + if (is_patch(pc->version)) { + auto [ss, size] = make_sockaddr_storage( + state->proxy_destination_patch.first, + state->proxy_destination_patch.second); + state->proxy_server->listen(pc->addr, pc->port, pc->version, &ss); + } else if (is_v4(pc->version)) { + auto [ss, size] = make_sockaddr_storage( + state->proxy_destination_bb.first, + state->proxy_destination_bb.second); + state->proxy_server->listen(pc->addr, pc->port, pc->version, &ss); + } else { + state->proxy_server->listen(pc->addr, pc->port, pc->version); + } + } + + } else if (pc->behavior == ServerBehavior::PATCH_SERVER_PC) { + if (!state->pc_patch_server.get()) { + config_log.info("Starting PC_V2 patch server"); + state->pc_patch_server = make_shared(state->generate_patch_server_config(false)); + } + string spec = string_printf("TU-%hu-%s-patch2", pc->port, pc->name.c_str()); + state->pc_patch_server->listen(spec, pc->addr, pc->port, Version::PC_PATCH); + + } else if (pc->behavior == ServerBehavior::PATCH_SERVER_BB) { + if (!state->bb_patch_server.get()) { + config_log.info("Starting BB_V4 patch server"); + state->bb_patch_server = make_shared(state->generate_patch_server_config(true)); + } + string spec = string_printf("TU-%hu-%s-patch4", pc->port, pc->name.c_str()); + state->bb_patch_server->listen(spec, pc->addr, pc->port, Version::BB_PATCH); + + } else { + if (!state->game_server.get()) { + config_log.info("Starting game server"); + state->game_server = make_shared(base, state); + } + string spec = string_printf("TG-%hu-%s-%s-%s", pc->port, name_for_enum(pc->version), pc->name.c_str(), name_for_enum(pc->behavior)); + state->game_server->listen(spec, pc->addr, pc->port, pc->version, pc->behavior); + } + } + + if (!state->ip_stack_addresses.empty() || !state->ppp_stack_addresses.empty() || !state->ppp_raw_addresses.empty()) { + config_log.info("Starting IP/PPP stack simulator"); + state->ip_stack_simulator = make_shared(base, state); + for (const auto& it : state->ip_stack_addresses) { + auto netloc = parse_netloc(it); + string spec = (netloc.second == 0) ? ("T-IPS-" + netloc.first) : string_printf("T-IPS-%hu", netloc.second); + state->ip_stack_simulator->listen( + spec, netloc.first, netloc.second, IPStackSimulator::Protocol::ETHERNET_TAPSERVER); + } + for (const auto& it : state->ppp_stack_addresses) { + auto netloc = parse_netloc(it); + string spec = (netloc.second == 0) ? ("T-PPPST-" + netloc.first) : string_printf("T-PPPST-%hu", netloc.second); + state->ip_stack_simulator->listen( + spec, netloc.first, netloc.second, IPStackSimulator::Protocol::HDLC_TAPSERVER); + } + for (const auto& it : state->ppp_raw_addresses) { + auto netloc = parse_netloc(it); + string spec = (netloc.second == 0) ? ("T-PPPSR-" + netloc.first) : string_printf("T-PPPSR-%hu", netloc.second); + state->ip_stack_simulator->listen( + spec, netloc.first, netloc.second, IPStackSimulator::Protocol::HDLC_RAW); + if (netloc.second) { + if (state->local_address == state->external_address) { + config_log.info( + "Note: The Devolution phone number for %s is %" PRIu64, + spec.c_str(), devolution_phone_number_for_netloc(state->local_address, netloc.second)); + } else { + config_log.info( + "Note: The Devolution phone numbers for %s are %" PRIu64 " (local) and %" PRIu64 " (external)", + spec.c_str(), + devolution_phone_number_for_netloc(state->local_address, netloc.second), + devolution_phone_number_for_netloc(state->external_address, netloc.second)); + } + } + } + } + + if (!state->http_addresses.empty() || !state->http_addresses.empty()) { + config_log.info("Starting HTTP server"); + http_server = make_shared(state); + for (const auto& it : state->http_addresses) { + auto netloc = parse_netloc(it); + http_server->listen(netloc.first, netloc.second); + } + } + } + + if (!state->username.empty()) { + config_log.info("Switching to user %s", state->username.c_str()); + drop_privileges(state->username); + } + + bool should_run_shell; + if (state->run_shell_behavior == ServerState::RunShellBehavior::DEFAULT) { + should_run_shell = isatty(fileno(stdin)); + } else if (state->run_shell_behavior == ServerState::RunShellBehavior::ALWAYS) { + should_run_shell = true; + } else { + should_run_shell = false; + } + if (should_run_shell) { + should_run_shell = !replay_session.get(); + } + + config_log.info("Ready"); + if (should_run_shell) { + shell = make_shared(state); + } + + event_base_dispatch(base.get()); + if (replay_session) { + // If in a replay session, run the event loop for a bit longer to make + // sure the server doesn't send anything unexpected after the end of + // the session. + auto tv = usecs_to_timeval(500000); + event_base_loopexit(base.get(), &tv); + event_base_dispatch(base.get()); + } + + config_log.info("Normal shutdown"); + if (state->pc_patch_server) { + state->pc_patch_server->schedule_stop(); + } + if (state->bb_patch_server) { + state->bb_patch_server->schedule_stop(); + } + if (http_server) { + http_server->schedule_stop(); + } + if (state->pc_patch_server) { + config_log.info("Waiting for PC_V2 patch server to stop"); + state->pc_patch_server->wait_for_stop(); + } + if (state->bb_patch_server) { + config_log.info("Waiting for BB_V4 patch server to stop"); + state->bb_patch_server->wait_for_stop(); + } + if (http_server) { + config_log.info("Waiting for HTTP server to stop"); + http_server->wait_for_stop(); + } + state->proxy_server.reset(); // Break reference cycle + }); + +void print_version_info() { + string build_date = format_time(BUILD_TIMESTAMP); + fprintf(stderr, "newserv-%s built %s UTC\n", GIT_REVISION_HASH, build_date.c_str()); +} + +void print_usage() { + print_version_info(); + fputs("\n\ +Usage:\n\ + newserv [ACTION] [OPTIONS...]\n\ +\n\ +If ACTION is not specified, newserv runs in server mode. PSO clients can\n\ +connect normally, join lobbies, play games, and use the proxy server. See\n\ +README.md and system/config.json for more information.\n\ +\n\ +When ACTION is given, newserv will do things other than running the server.\n\ +\n\ +Some actions accept input and/or output filenames; see the descriptions below\n\ +for details. If INPUT-FILENAME is missing or is '-', newserv reads from stdin.\n\ +If OUTPUT-FILENAME is missing and the input is not from stdin, newserv writes\n\ +the output to INPUT-FILENAME.dec or a similarly-named file; if OUTPUT-FILENAME\n\ +is '-', newserv writes the output to stdout. If stdout is a terminal and the\n\ +output is not text or JSON, the data written to stdout is formatted in a\n\ +hex/ASCII view; in any other case, the raw output is written to stdout, which\n\ +(for most actions) may include arbitrary binary data.\n\ +\n\ +The actions are:\n", + stderr); + for (const auto& a : action_order) { + if (a->help_text) { + fputs(a->help_text, stderr); + } + } + fputs("\n\ +Most options that take data as input also accept the following option:\n\ + --parse-data\n\ + For modes that take input (from a file or from stdin), parse the input as\n\ + a hex string before encrypting/decoding/etc.\n\ +\n\ +Many versions also accept or require a version option. The version options are:\n\ + --pc-patch: PC patch server\n\ + --bb-patch: BB patch server\n\ + --dc-nte: DC Network Trial Edition\n\ + --dc-proto or --dc-11-2000: DC 11/2000 prototype\n\ + --dc-v1: DC v1\n\ + --dc-v2 or --dc: DC v2\n\ + --pc-nte: PC Network Trial Edition\n\ + --pc: PC v2\n\ + --gc-nte: GC Episodes 1&2 Trial Edition\n\ + --gc: GC Episodes 1&2\n\ + --xb: Xbox Episodes 1&2\n\ + --ep3-nte: GC Episode 3 Trial Edition\n\ + --ep3: GC Episode 3\n\ + --bb: Blue Burst\n\ +\n", + stderr); +} + +int main(int argc, char** argv) { + Arguments args(&argv[1], argc - 1); + if (args.get("help")) { + print_usage(); + return 0; + } + + string action_name = args.get(0, false); + const Action* a; + try { + a = all_actions.at(action_name); + } catch (const out_of_range&) { + log_error("Unknown or invalid action; try --help"); + return 1; + } +#ifdef PHOSG_WINDOWS + // Cygwin just gives a stackdump when an exception falls out of main(), so + // unlike Linux and macOS, we have to manually catch exceptions here just to + // see what the exception message was. + try { + a->run(args); + } catch (const cannot_open_file& e) { + log_error("Top-level exception (cannot_open_file): %s", e.what()); + throw; + } catch (const invalid_argument& e) { + log_error("Top-level exception (invalid_argument): %s", e.what()); + throw; + } catch (const out_of_range& e) { + log_error("Top-level exception (out_of_range): %s", e.what()); + throw; + } catch (const runtime_error& e) { + log_error("Top-level exception (runtime_error): %s", e.what()); + throw; + } catch (const exception& e) { + log_error("Top-level exception: %s", e.what()); + throw; + } +#else + a->run(args); +#endif + return 0; +} diff --git a/src/Map.cc b/src/Map.cc index 92e5eccc..66a17b3d 100644 --- a/src/Map.cc +++ b/src/Map.cc @@ -1,2567 +1,2567 @@ -#include "Map.hh" - -#include -#include -#include - -#include "ItemCreator.hh" -#include "Loggers.hh" -#include "PSOEncryption.hh" -#include "Quest.hh" -#include "StaticGameData.hh" - -using namespace std; - -static constexpr float UINT32_MAX_AS_FLOAT = 4294967296.0f; - -static uint64_t section_index_key(uint8_t floor, uint16_t section, uint16_t wave_number) { - return (static_cast(floor) << 32) | (static_cast(section) << 16) | static_cast(wave_number); -} - -const char* Map::name_for_object_type(uint16_t type) { - switch (type) { - case 0x0000: - return "TObjPlayerSet"; - case 0x0001: - return "TObjParticle"; - case 0x0002: - return "TObjAreaWarpForest"; - case 0x0003: - return "TObjMapWarpForest"; - case 0x0004: - return "TObjLight"; - case 0x0006: - return "TObjEnvSound"; - case 0x0007: - return "TObjFogCollision"; - case 0x0008: - return "TObjEvtCollision"; - case 0x0009: - return "TObjCollision"; - case 0x000A: - return "TOMineIcon01"; - case 0x000B: - return "TOMineIcon02"; - case 0x000C: - return "TOMineIcon03"; - case 0x000D: - return "TOMineIcon04"; - case 0x000E: - return "TObjRoomId"; - case 0x000F: - return "TOSensorGeneral01"; - case 0x0011: - return "TEF_LensFlare"; - case 0x0012: - return "TObjQuestCol"; - case 0x0013: - return "TOHealGeneral"; - case 0x0014: - return "TObjMapCsn"; - case 0x0015: - return "TObjQuestColA"; - case 0x0016: - return "TObjItemLight"; - case 0x0017: - return "TObjRaderCol"; - case 0x0018: - return "TObjFogCollisionSwitch"; - case 0x0019: - return "TObjWarpBossMulti(off)/TObjWarpBoss(on)"; - case 0x001A: - return "TObjSinBoard"; - case 0x001B: - return "TObjAreaWarpQuest"; - case 0x001C: - return "TObjAreaWarpEnding"; - case 0x001D: - return "__UNNAMED_001D__"; - case 0x001E: - return "__UNNAMED_001E__"; - case 0x001F: - return "TObjRaderHideCol"; - case 0x0020: - return "TOSwitchItem"; - case 0x0021: - return "TOSymbolchatColli"; - case 0x0022: - return "TOKeyCol"; - case 0x0023: - return "TOAttackableCol"; - case 0x0024: - return "TOSwitchAttack"; - case 0x0025: - return "TOSwitchTimer"; - case 0x0026: - return "TOChatSensor"; - case 0x0027: - return "TObjRaderIcon"; - case 0x0028: - return "TObjEnvSoundEx"; - case 0x0029: - return "TObjEnvSoundGlobal"; - case 0x0040: - return "TShopGenerator"; - case 0x0041: - return "TObjLuker"; - case 0x0042: - return "TObjBgmCol"; - case 0x0043: - return "TObjCityMainWarp"; - case 0x0044: - return "TObjCityAreaWarp"; - case 0x0045: - return "TObjCityMapWarp"; - case 0x0046: - return "TObjCityDoor_Shop"; - case 0x0047: - return "TObjCityDoor_Guild"; - case 0x0048: - return "TObjCityDoor_Warp"; - case 0x0049: - return "TObjCityDoor_Med"; - case 0x004A: - return "__UNNAMED_004A__"; - case 0x004B: - return "TObjCity_Season_EasterEgg"; - case 0x004C: - return "TObjCity_Season_ValentineHeart"; - case 0x004D: - return "TObjCity_Season_XmasTree"; - case 0x004E: - return "TObjCity_Season_XmasWreath"; - case 0x004F: - return "TObjCity_Season_HalloweenPumpkin"; - case 0x0050: - return "TObjCity_Season_21_21"; - case 0x0051: - return "TObjCity_Season_SonicAdv2"; - case 0x0052: - return "TObjCity_Season_Board"; - case 0x0053: - return "TObjCity_Season_FireWorkCtrl"; - case 0x0054: - return "TObjCityDoor_Lobby"; - case 0x0055: - return "TObjCityMainWarpChallenge"; - case 0x0056: - return "TODoorLabo"; - case 0x0057: - return "TObjTradeCollision"; - case 0x0080: - return "TObjDoor"; - case 0x0081: - return "TObjDoorKey"; - case 0x0082: - return "TObjLazerFenceNorm"; - case 0x0083: - return "TObjLazerFence4"; - case 0x0084: - return "TLazerFenceSw"; - case 0x0085: - return "TKomorebi"; - case 0x0086: - return "TButterfly"; - case 0x0087: - return "TMotorcycle"; - case 0x0088: - return "TObjContainerItem"; - case 0x0089: - return "TObjTank"; - case 0x008B: - return "TObjComputer"; - case 0x008C: - return "TObjContainerIdo"; - case 0x008D: - return "TOCapsuleAncient01"; - case 0x008E: - return "TOBarrierEnergy01"; - case 0x008F: - return "TObjHashi"; - case 0x0090: - return "TOKeyGenericSw"; - case 0x0091: - return "TObjContainerEnemy"; - case 0x0092: - return "TObjContainerBase"; - case 0x0093: - return "TObjContainerAbeEnemy"; - case 0x0095: - return "TObjContainerNoItem"; - case 0x0096: - return "TObjLazerFenceExtra"; - case 0x00C0: - return "TOKeyCave01"; - case 0x00C1: - return "TODoorCave01"; - case 0x00C2: - return "TODoorCave02"; - case 0x00C3: - return "TOHangceilingCave01Key/TOHangceilingCave01Normal/TOHangceilingCave01KeyQuick"; - case 0x00C4: - return "TOSignCave01"; - case 0x00C5: - return "TOSignCave02"; - case 0x00C6: - return "TOSignCave03"; - case 0x00C7: - return "TOAirconCave01"; - case 0x00C8: - return "TOAirconCave02"; - case 0x00C9: - return "TORevlightCave01"; - case 0x00CB: - return "TORainbowCave01"; - case 0x00CC: - return "TOKurage"; - case 0x00CD: - return "TODragonflyCave01"; - case 0x00CE: - return "TODoorCave03"; - case 0x00CF: - return "TOBind"; - case 0x00D0: - return "TOCakeshopCave01"; - case 0x00D1: - return "TORockCaveS01"; - case 0x00D2: - return "TORockCaveM01"; - case 0x00D3: - return "TORockCaveL01"; - case 0x00D4: - return "TORockCaveS02"; - case 0x00D5: - return "TORockCaveM02"; - case 0x00D6: - return "TORockCaveL02"; - case 0x00D7: - return "TORockCaveSS02"; - case 0x00D8: - return "TORockCaveSM02"; - case 0x00D9: - return "TORockCaveSL02"; - case 0x00DA: - return "TORockCaveS03"; - case 0x00DB: - return "TORockCaveM03"; - case 0x00DC: - return "TORockCaveL03"; - case 0x00DE: - return "TODummyKeyCave01"; - case 0x00DF: - return "TORockCaveBL01"; - case 0x00E0: - return "TORockCaveBL02"; - case 0x00E1: - return "TORockCaveBL03"; - case 0x0100: - return "TODoorMachine01"; - case 0x0101: - return "TOKeyMachine01"; - case 0x0102: - return "TODoorMachine02"; - case 0x0103: - return "TOCapsuleMachine01"; - case 0x0104: - return "TOComputerMachine01"; - case 0x0105: - return "TOMonitorMachine01"; - case 0x0106: - return "TODragonflyMachine01"; - case 0x0107: - return "TOLightMachine01"; - case 0x0108: - return "TOExplosiveMachine01"; - case 0x0109: - return "TOExplosiveMachine02"; - case 0x010A: - return "TOExplosiveMachine03"; - case 0x010B: - return "TOSparkMachine01"; - case 0x010C: - return "TOHangerMachine01"; - case 0x0130: - return "TODoorVoShip"; - case 0x0140: - return "TObjGoalWarpAncient"; - case 0x0141: - return "TObjMapWarpAncient"; - case 0x0142: - return "TOKeyAncient02"; - case 0x0143: - return "TOKeyAncient03"; - case 0x0144: - return "TODoorAncient01"; - case 0x0145: - return "TODoorAncient03"; - case 0x0146: - return "TODoorAncient04"; - case 0x0147: - return "TODoorAncient05"; - case 0x0148: - return "TODoorAncient06"; - case 0x0149: - return "TODoorAncient07"; - case 0x014A: - return "TODoorAncient08"; - case 0x014B: - return "TODoorAncient09"; - case 0x014C: - return "TOSensorAncient01"; - case 0x014D: - return "TOKeyAncient01"; - case 0x014E: - return "TOFenceAncient01"; - case 0x014F: - return "TOFenceAncient02"; - case 0x0150: - return "TOFenceAncient03"; - case 0x0151: - return "TOFenceAncient04"; - case 0x0152: - return "TContainerAncient01"; - case 0x0153: - return "TOTrapAncient01"; - case 0x0154: - return "TOTrapAncient02"; - case 0x0155: - return "TOMonumentAncient01"; - case 0x0156: - return "TOMonumentAncient02"; - case 0x0159: - return "TOWreckAncient01"; - case 0x015A: - return "TOWreckAncient02"; - case 0x015B: - return "TOWreckAncient03"; - case 0x015C: - return "TOWreckAncient04"; - case 0x015D: - return "TOWreckAncient05"; - case 0x015E: - return "TOWreckAncient06"; - case 0x015F: - return "TOWreckAncient07"; - case 0x0160: - return "TObjFogCollisionPoison/TObjWarpBoss03"; - case 0x0161: - return "TOContainerAncientItemCommon"; - case 0x0162: - return "TOContainerAncientItemRare"; - case 0x0163: - return "TOContainerAncientEnemyCommon"; - case 0x0164: - return "TOContainerAncientEnemyRare"; - case 0x0165: - return "TOContainerAncientItemNone"; - case 0x0166: - return "TOWreckAncientBrakable05"; - case 0x0167: - return "TOTrapAncient02R"; - case 0x0170: - return "TOBoss4Bird"; - case 0x0171: - return "TOBoss4Tower"; - case 0x0172: - return "TOBoss4Rock"; - case 0x0180: - return "TObjInfoCol"; - case 0x0181: - return "TObjWarpLobby"; - case 0x0182: - return "TObjLobbyMain"; - case 0x0183: - return "__TObjPathObj_subclass_0183__"; - case 0x0184: - return "TObjButterflyLobby"; - case 0x0185: - return "TObjRainbowLobby"; - case 0x0186: - return "TObjKabochaLobby"; - case 0x0187: - return "TObjStendGlassLobby"; - case 0x0188: - return "TObjCurtainLobby"; - case 0x0189: - return "TObjWeddingLobby"; - case 0x018A: - return "TObjTreeLobby"; - case 0x018B: - return "TObjSuisouLobby"; - case 0x018C: - return "TObjParticleLobby"; - case 0x0190: - return "TObjCamera"; - case 0x0191: - return "TObjTuitate"; - case 0x0192: - return "TObjDoaEx01"; - case 0x0193: - return "TObjBigTuitate"; - case 0x01A0: - return "TODoorVS2Door01"; - case 0x01A1: - return "TOVS2Wreck01"; - case 0x01A2: - return "TOVS2Wreck02"; - case 0x01A3: - return "TOVS2Wreck03"; - case 0x01A4: - return "TOVS2Wreck04"; - case 0x01A5: - return "TOVS2Wreck05"; - case 0x01A6: - return "TOVS2Wreck06"; - case 0x01A7: - return "TOVS2Wall01"; - case 0x01A8: - return "__UNNAMED_01A8__"; - case 0x01A9: - return "TObjHashiVersus1"; - case 0x01AA: - return "TObjHashiVersus2"; - case 0x01AB: - return "TODoorFourLightRuins"; - case 0x01C0: - return "TODoorFourLightSpace"; - case 0x0200: - return "TObjContainerJung"; - case 0x0201: - return "TObjWarpJung"; - case 0x0202: - return "TObjDoorJung"; - case 0x0203: - return "TObjContainerJungEx"; - case 0x0204: - return "TODoorJungleMain"; - case 0x0205: - return "TOKeyJungleMain"; - case 0x0206: - return "TORockJungleS01"; - case 0x0207: - return "TORockJungleM01"; - case 0x0208: - return "TORockJungleL01"; - case 0x0209: - return "TOGrassJungle"; - case 0x020A: - return "TObjWarpJungMain"; - case 0x020B: - return "TBGLightningCtrl"; - case 0x020C: - return "__TObjPathObj_subclass_020C__"; - case 0x020D: - return "__TObjPathObj_subclass_020D__"; - case 0x020E: - return "TObjContainerJungEnemy"; - case 0x020F: - return "TOTrapChainSawDamage"; - case 0x0210: - return "TOTrapChainSawKey"; - case 0x0211: - return "TOBiwaMushi"; - case 0x0212: - return "__TObjPathObj_subclass_0212__"; - case 0x0213: - return "TOJungleDesign"; - case 0x0220: - return "TObjFish"; - case 0x0221: - return "TODoorFourLightSeabed"; - case 0x0222: - return "TODoorFourLightSeabedU"; - case 0x0223: - return "TObjSeabedSuiso_CH"; - case 0x0224: - return "TObjSeabedSuisoBrakable"; - case 0x0225: - return "TOMekaFish00"; - case 0x0226: - return "TOMekaFish01"; - case 0x0227: - return "__TObjPathObj_subclass_0227__"; - case 0x0228: - return "TOTrapSeabed01"; - case 0x0229: - return "TOCapsuleLabo"; - case 0x0240: - return "TObjParticle"; - case 0x0280: - return "__TObjAreaWarpForest_subclass_0280__"; - case 0x02A0: - return "TObjLiveCamera"; - case 0x02B0: - return "TContainerAncient01R"; - case 0x02B1: - return "TObjLaboDesignBase"; - case 0x02B2: - return "TObjLaboDesignBase"; - case 0x02B3: - return "TObjLaboDesignBase"; - case 0x02B4: - return "TObjLaboDesignBase"; - case 0x02B5: - return "TObjLaboDesignBase"; - case 0x02B6: - return "TObjLaboDesignBase"; - case 0x02B7: - return "TObjGbAdvance"; - case 0x02B8: - return "TObjQuestColALock2"; - case 0x02B9: - return "TObjMapForceWarp"; - case 0x02BA: - return "TObjQuestCol2"; - case 0x02BB: - return "TODoorLaboNormal"; - case 0x02BC: - return "TObjAreaWarpEndingJung"; - case 0x02BD: - return "TObjLaboMapWarp"; - case 0x0300: - return "__UNKNOWN_0300__"; - case 0x0301: - return "__UNKNOWN_0301__"; - case 0x0302: - return "__UNKNOWN_0302__"; - case 0x0303: - return "__UNKNOWN_0303__"; - case 0x0340: - return "__UNKNOWN_0340__"; - case 0x0341: - return "__UNKNOWN_0341__"; - case 0x0380: - return "__UNKNOWN_0380__"; - case 0x0381: - return "__UNKNOWN_0381__"; - case 0x0382: - return "__UNKNOWN_0382__"; - case 0x0383: - return "__UNKNOWN_0383__"; - case 0x0385: - return "__UNKNOWN_0385__"; - case 0x0386: - return "__UNKNOWN_0386__"; - case 0x0387: - return "__UNKNOWN_0387__"; - case 0x0388: - return "__UNKNOWN_0388__"; - case 0x0389: - return "__UNKNOWN_0389__"; - case 0x038A: - return "__UNKNOWN_038A__"; - case 0x038B: - return "__UNKNOWN_038B__"; - case 0x038C: - return "__UNKNOWN_038C__"; - case 0x038D: - return "__UNKNOWN_038D__"; - case 0x038E: - return "__UNKNOWN_038E__"; - case 0x038F: - return "__UNKNOWN_038F__"; - case 0x0390: - return "__UNKNOWN_0390__"; - case 0x0391: - return "__UNKNOWN_0391__"; - case 0x03C0: - return "__UNKNOWN_03C0__"; - case 0x03C1: - return "__UNKNOWN_03C1__"; - default: - return "__UNKNOWN__"; - } -} - -Map::RareEnemyRates::RareEnemyRates(uint32_t enemy_rate, uint32_t boss_rate) - : hildeblue(enemy_rate), - rappy(enemy_rate), - nar_lily(enemy_rate), - pouilly_slime(enemy_rate), - merissa_aa(enemy_rate), - pazuzu(enemy_rate), - dorphon_eclair(enemy_rate), - kondrieu(boss_rate) {} - -Map::RareEnemyRates::RareEnemyRates(const JSON& json) - : hildeblue(json.get_int("Hildeblue")), - rappy(json.get_int("Rappy")), - nar_lily(json.get_int("NarLily")), - pouilly_slime(json.get_int("PouillySlime")), - merissa_aa(json.get_int("MerissaAA")), - pazuzu(json.get_int("Pazuzu")), - dorphon_eclair(json.get_int("DorphonEclair")), - kondrieu(json.get_int("Kondrieu")) {} - -JSON Map::RareEnemyRates::json() const { - return JSON::dict({ - {"Hildeblue", this->hildeblue}, - {"Rappy", this->rappy}, - {"NarLily", this->nar_lily}, - {"PouillySlime", this->pouilly_slime}, - {"MerissaAA", this->merissa_aa}, - {"Pazuzu", this->pazuzu}, - {"DorphonEclair", this->dorphon_eclair}, - {"Kondrieu", this->kondrieu}, - }); -} - -string Map::ObjectEntry::str() const { - string name_str = Map::name_for_object_type(this->base_type); - return string_printf("[ObjectEntry type=%04hX \"%s\" flags=%04hX index=%04hX a2=%04hX entity_id=%04hX group=%04hX section=%04hX a3=%04hX x=%g y=%g z=%g x_angle=%08" PRIX32 " y_angle=%08" PRIX32 " z_angle=%08" PRIX32 " params=[%g %g %g %08" PRIX32 " %08" PRIX32 " %08" PRIX32 "] unused=%08" PRIX32 "]", - this->base_type.load(), - name_str.c_str(), - this->flags.load(), - this->index.load(), - this->unknown_a2.load(), - this->entity_id.load(), - this->group.load(), - this->section.load(), - this->unknown_a3.load(), - this->x.load(), - this->y.load(), - this->z.load(), - this->x_angle.load(), - this->y_angle.load(), - this->z_angle.load(), - this->param1.load(), - this->param2.load(), - this->param3.load(), - this->param4.load(), - this->param5.load(), - this->param6.load(), - this->unused.load()); -} - -string Map::EnemyEntry::str() const { - return string_printf("[EnemyEntry type=%04hX flags=%04hX index=%04hX num_children=%04hX floor=%04hX entity_id=%04hX section=%04hX wave_number=%04hX wave_number2=%04hX a1=%04hX x=%g y=%g z=%g x_angle=%08" PRIX32 " y_angle=%08" PRIX32 " z_angle=%08" PRIX32 " params=[%g %g %g %g %g %04hX %04hX] unused=%08" PRIX32 "]", - this->base_type.load(), - this->flags.load(), - this->index.load(), - this->num_children.load(), - this->floor.load(), - this->entity_id.load(), - this->section.load(), - this->wave_number.load(), - this->wave_number2.load(), - this->unknown_a1.load(), - this->x.load(), - this->y.load(), - this->z.load(), - this->x_angle.load(), - this->y_angle.load(), - this->z_angle.load(), - this->fparam1.load(), - this->fparam2.load(), - this->fparam3.load(), - this->fparam4.load(), - this->fparam5.load(), - this->uparam1.load(), - this->uparam2.load(), - this->unused.load()); -} - -Map::Enemy::Enemy( - uint16_t enemy_id, - size_t source_index, - size_t set_index, - uint8_t floor, - uint16_t section, - uint16_t wave_number, - EnemyType type) - : source_index(source_index), - set_index(set_index), - enemy_id(enemy_id), - total_damage(0), - game_flags(0), - section(section), - wave_number(wave_number), - type(type), - floor(floor), - server_flags(0) {} - -string Map::Enemy::str() const { - return string_printf("[Map::Enemy E-%hX source %zX %s%s floor=%02hhX section=%04hX wave_number=%04hX server_flags=%02hhX]", - this->enemy_id, - this->source_index, - name_for_enum(this->type), - enemy_type_is_rare(this->type) ? " RARE" : "", - this->floor, - this->section, - this->wave_number, - this->server_flags); -} - -string Map::Event::str() const { - return string_printf("[Map::Event W-%02hhX-%" PRIX32 " flags=%04hX floor=%02hhX action_stream_offset=%" PRIX32 "]", - this->floor, - this->event_id, - this->flags, - this->floor, - this->action_stream_offset); -} - -string Map::Object::str() const { - return string_printf("[Map::Object source %zX %04hX(%s) @%04hX p1=%g p456=[%08" PRIX32 " %08" PRIX32 " %08" PRIX32 "] floor=%02hhX item_drop_checked=%s]", - this->source_index, - this->base_type, - Map::name_for_object_type(this->base_type), - this->section, - this->param1, - this->param4, - this->param5, - this->param6, - this->floor, - this->item_drop_checked ? "true" : "false"); -} - -Map::Map(Version version, uint32_t lobby_id, uint32_t rare_seed, std::shared_ptr opt_rand_crypt) - : log(string_printf("[Lobby:%08" PRIX32 ":map] ", lobby_id), lobby_log.min_level), - version(version), - rare_seed(rare_seed), - opt_rand_crypt(opt_rand_crypt) {} - -void Map::clear() { - this->objects.clear(); - this->enemies.clear(); - this->rare_enemy_indexes.clear(); -} - -void Map::add_objects_from_map_data(uint8_t floor, const void* data, size_t size) { - size_t entry_count = size / sizeof(ObjectEntry); - if (size != entry_count * sizeof(ObjectEntry)) { - throw runtime_error("data size is not a multiple of entry size"); - } - - const auto* objects = reinterpret_cast(data); - for (size_t z = 0; z < entry_count; z++) { - uint16_t object_id = this->objects.size(); - this->objects.emplace_back(Object{ - .source_index = z, - .floor = floor, - .object_id = object_id, - .base_type = objects[z].base_type, - .section = objects[z].section, - .group = objects[z].group, - .param1 = objects[z].param1, - .param3 = objects[z].param3, - .param4 = objects[z].param4, - .param5 = objects[z].param5, - .param6 = objects[z].param6, - .game_flags = 0, - .set_flags = 0, - .item_drop_checked = false, - }); - uint64_t k = section_index_key(floor, objects[z].section, objects[z].group); - this->floor_section_and_group_to_object_index.emplace(k, object_id); - } -} - -bool Map::check_and_log_rare_enemy(bool default_is_rare, uint32_t rare_rate) { - if (default_is_rare) { - return true; - } - - // On BB, rare enemy indexes are generated by the server and sent to the - // client, so we can use any method we want to choose rares. On other - // versions, we must match the client's logic, even though it's more - // computationally expensive. - if (this->version == Version::BB_V4) { - if ((this->rare_enemy_indexes.size() < 0x10) && (random_from_optional_crypt(this->opt_rand_crypt) < rare_rate)) { - this->rare_enemy_indexes.emplace_back(this->enemies.size()); - return true; - } - - } else { - // TODO: We only need the first value from this crypt, so it's unfortunate - // that we have to initialize the entire thing. Find a way to make this - // faster. - PSOV2Encryption crypt(this->rare_seed + 0x1000 + this->enemies.size()); - float det = (static_cast((crypt.next() >> 16) & 0xFFFF) / 65536.0f); - // On v1 and v2 (and GC NTE), the rare rate is 0.1% instead of 0.2%. - float threshold = is_v1_or_v2(this->version) ? 0.001f : 0.002f; - if (det < threshold) { - this->rare_enemy_indexes.emplace_back(this->enemies.size()); - return true; - } - } - - return false; -} - -void Map::add_enemy( - Episode episode, - uint8_t difficulty, - uint8_t event, - uint8_t floor, - size_t source_index, - const EnemyEntry& e, - std::shared_ptr rare_rates) { - size_t set_index = this->enemy_set_flags.size(); - this->enemy_set_flags.emplace_back(0); - - auto add = [&](EnemyType type) -> void { - uint16_t enemy_id = this->enemies.size(); - this->enemies.emplace_back(enemy_id, source_index, set_index, floor, e.section, e.wave_number, type); - uint64_t k = section_index_key(floor, e.section, e.wave_number); - this->floor_section_and_wave_number_to_enemy_index.emplace(k, enemy_id); - }; - - EnemyType child_type = EnemyType::UNKNOWN; - ssize_t default_num_children = 0; - switch (e.base_type) { - case 0x0001: // TObjNpcFemaleBase - case 0x0002: // TObjNpcFemaleChild - case 0x0003: // TObjNpcFemaleDwarf - case 0x0004: // TObjNpcFemaleFat - case 0x0005: // TObjNpcFemaleMacho - case 0x0006: // TObjNpcFemaleOld - case 0x0007: // TObjNpcFemaleTall - case 0x0008: // TObjNpcMaleBase - case 0x0009: // TObjNpcMaleChild - case 0x000A: // TObjNpcMaleDwarf - case 0x000B: // TObjNpcMaleFat - case 0x000C: // TObjNpcMaleMacho - case 0x000D: // TObjNpcMaleOld - case 0x000E: // TObjNpcMaleTall - case 0x0019: // TObjNpcSoldierBase - case 0x001A: // TObjNpcSoldierMacho - case 0x001B: // TObjNpcGovernorBase - case 0x001C: // TObjNpcConnoisseur - case 0x001D: // TObjNpcCloakroomBase - case 0x001E: // TObjNpcExpertBase - case 0x001F: // TObjNpcNurseBase - case 0x0020: // TObjNpcSecretaryBase - case 0x0021: // TObjNpcHHM00 - case 0x0022: // TObjNpcNHW00 - case 0x0024: // TObjNpcHRM00 - case 0x0025: // TObjNpcARM00 - case 0x0026: // TObjNpcARW00 - case 0x0027: // TObjNpcHFW00 - case 0x0028: // TObjNpcNFM00 - case 0x0029: // TObjNpcNFW00 - case 0x002B: // TObjNpcNHW01 - case 0x002C: // TObjNpcAHM01 - case 0x002D: // TObjNpcHRM01 - case 0x0030: // TObjNpcHFW01 - case 0x0031: // TObjNpcNFM01 - case 0x0032: // TObjNpcNFW01 - case 0x0033: // TObjNpcEnemy - case 0x0045: // TObjNpcLappy - case 0x0046: // TObjNpcMoja - case 0x00A9: // TObjNpcBringer - case 0x00D0: // TObjNpcKenkyu - case 0x00D1: // TObjNpcSoutokufu - case 0x00D2: // TObjNpcHosa - case 0x00D3: // TObjNpcKenkyuW - case 0x00F0: // TObjNpcHosa2 - case 0x00F1: // TObjNpcKenkyu2 - case 0x00F2: // TObjNpcNgcBase - case 0x00F3: // TObjNpcNgcBase - case 0x00F4: // TObjNpcNgcBase - case 0x00F5: // TObjNpcNgcBase - case 0x00F6: // TObjNpcNgcBase - case 0x00F7: // TObjNpcNgcBase - case 0x00F8: // TObjNpcNgcBase - case 0x00F9: // TObjNpcNgcBase - case 0x00FA: // TObjNpcNgcBase - case 0x00FB: // TObjNpcNgcBase - case 0x00FC: // TObjNpcNgcBase - case 0x00FD: // TObjNpcNgcBase - case 0x00FE: // TObjNpcNgcBase - case 0x00FF: // TObjNpcNgcBase - case 0x0100: // Unknown NPC - // All of these have a default child count of zero - add(EnemyType::NON_ENEMY_NPC); - break; - - case 0x0040: { // TObjEneMoja - bool default_is_rare = (this->version == Version::BB_V4) ? (e.uparam1 & 1) : (e.uparam1 != 0); - add(this->check_and_log_rare_enemy(default_is_rare, rare_rates->hildeblue) - ? EnemyType::HILDEBLUE - : EnemyType::HILDEBEAR); - break; - } - case 0x0041: { // TObjEneLappy - bool default_is_rare = (this->version == Version::BB_V4) ? (e.uparam1 & 1) : (e.uparam1 != 0); - bool is_rare = this->check_and_log_rare_enemy(default_is_rare, rare_rates->rappy); - switch (episode) { - case Episode::EP1: - add(is_rare ? EnemyType::AL_RAPPY : EnemyType::RAG_RAPPY); - break; - case Episode::EP2: - if (is_rare) { - switch (event) { - case 0x01: // rappy_type 1 - add(EnemyType::SAINT_RAPPY); - break; - case 0x04: // rappy_type 2 - add(EnemyType::EGG_RAPPY); - break; - case 0x05: // rappy_type 3 - add(EnemyType::HALLO_RAPPY); - break; - default: - add(EnemyType::LOVE_RAPPY); - } - } else { - add(EnemyType::RAG_RAPPY); - } - break; - case Episode::EP4: - if (e.floor > 0x05) { - add(is_rare ? EnemyType::DEL_RAPPY_ALT : EnemyType::SAND_RAPPY_ALT); - } else { - add(is_rare ? EnemyType::DEL_RAPPY : EnemyType::SAND_RAPPY); - } - break; - default: - throw logic_error("invalid episode"); - } - break; - } - case 0x0042: // TObjEneBm3FlyNest - add(EnemyType::MONEST); - child_type = EnemyType::MOTHMANT; - default_num_children = 30; - break; - case 0x0043: // TObjEneBm5Wolf - add(e.fparam2 ? EnemyType::BARBAROUS_WOLF : EnemyType::SAVAGE_WOLF); - break; - case 0x0044: { // TObjEneBeast - static const EnemyType types[3] = {EnemyType::BOOMA, EnemyType::GOBOOMA, EnemyType::GIGOBOOMA}; - add(types[e.uparam1 % 3]); - break; - } - case 0x0060: // TObjGrass - add(EnemyType::GRASS_ASSASSIN); - break; - case 0x0061: // TObjEneRe2Flower - if ((episode == Episode::EP2) && (e.floor == 0x11)) { - add(EnemyType::DEL_LILY); - } else { - add(this->check_and_log_rare_enemy(false, rare_rates->nar_lily) - ? EnemyType::NAR_LILY - : EnemyType::POISON_LILY); - } - break; - case 0x0062: // TObjEneNanoDrago - add(EnemyType::NANO_DRAGON); - break; - case 0x0063: { // TObjEneShark - static const EnemyType types[3] = {EnemyType::EVIL_SHARK, EnemyType::PAL_SHARK, EnemyType::GUIL_SHARK}; - add(types[e.uparam1 % 3]); - break; - } - case 0x0064: // TObjEneSlime - if ((e.num_children != 0) && (e.num_children != 4)) { - this->log.warning("POFUILLY_SLIME has an unusual num_children (0x%hX)", e.num_children.load()); - } - default_num_children = -1; // Skip adding children (because we do it here) - for (size_t z = 0; z < 5; z++) { - add(this->check_and_log_rare_enemy((this->version == Version::BB_V4) && (e.uparam2 & 1), rare_rates->pouilly_slime) - ? EnemyType::POUILLY_SLIME - : EnemyType::POFUILLY_SLIME); - } - break; - case 0x0065: // TObjEnePanarms - if ((e.num_children != 0) && (e.num_children != 2)) { - this->log.warning("PAN_ARMS has an unusual num_children (0x%hX)", e.num_children.load()); - } - default_num_children = -1; // Skip adding children (because we do it here) - add(EnemyType::PAN_ARMS); - add(EnemyType::HIDOOM); - add(EnemyType::MIGIUM); - break; - case 0x0080: // TObjEneDubchik - add((e.uparam1 & 0x01) ? EnemyType::GILLCHIC : EnemyType::DUBCHIC); - break; - case 0x0081: // TObjEneGyaranzo - add(EnemyType::GARANZ); - break; - case 0x0082: // TObjEneMe3ShinowaReal - add(e.fparam2 ? EnemyType::SINOW_GOLD : EnemyType::SINOW_BEAT); - default_num_children = 4; - break; - case 0x0083: // TObjEneMe1Canadin - add(EnemyType::CANADINE); - break; - case 0x0084: // TObjEneMe1CanadinLeader - add(EnemyType::CANANE); - child_type = EnemyType::CANADINE_GROUP; - default_num_children = 8; - break; - case 0x0085: // TOCtrlDubchik - add(EnemyType::DUBWITCH); - break; - case 0x00A0: // TObjEneSaver - add(EnemyType::DELSABER); - break; - case 0x00A1: // TObjEneRe4Sorcerer - if ((e.num_children != 0) && (e.num_children != 2)) { - this->log.warning("CHAOS_SORCERER has an unusual num_children (0x%hX)", e.num_children.load()); - } - default_num_children = -1; // Skip adding children (because we do it here) - add(EnemyType::CHAOS_SORCERER); - add(EnemyType::BEE_R); - add(EnemyType::BEE_L); - break; - case 0x00A2: // TObjEneDarkGunner - add(EnemyType::DARK_GUNNER); - break; - case 0x00A3: // TObjEneDarkGunCenter - add(EnemyType::DEATH_GUNNER); - break; - case 0x00A4: // TObjEneDf2Bringer - add(EnemyType::CHAOS_BRINGER); - break; - case 0x00A5: // TObjEneRe7Berura - add(EnemyType::DARK_BELRA); - break; - case 0x00A6: { // TObjEneDimedian - static const EnemyType types[3] = {EnemyType::DIMENIAN, EnemyType::LA_DIMENIAN, EnemyType::SO_DIMENIAN}; - add(types[e.uparam1 % 3]); - break; - } - case 0x00A7: // TObjEneBalClawBody - add(EnemyType::BULCLAW); - child_type = EnemyType::CLAW; - default_num_children = 4; - break; - case 0x00A8: // Unnamed subclass of TObjEneBalClawClaw - add(EnemyType::CLAW); - break; - case 0x00C0: // TBoss1Dragon or TBoss5Gryphon - if (episode == Episode::EP1) { - add(EnemyType::DRAGON); - } else if (episode == Episode::EP2) { - add(EnemyType::GAL_GRYPHON); - } else { - throw runtime_error("DRAGON placed outside of Episode 1 or 2"); - } - break; - case 0x00C1: // TBoss2DeRolLe - if ((e.num_children != 0) && (e.num_children != 0x13)) { - this->log.warning("DE_ROL_LE has an unusual num_children (0x%hX)", e.num_children.load()); - } - default_num_children = -1; // Skip adding children (because we do it here) - add(EnemyType::DE_ROL_LE); - for (size_t z = 0; z < 0x0A; z++) { - add(EnemyType::DE_ROL_LE_BODY); - } - for (size_t z = 0; z < 0x09; z++) { - add(EnemyType::DE_ROL_LE_MINE); - } - break; - case 0x00C2: // TBoss3Volopt - if ((e.num_children != 0) && (e.num_children != 0x23)) { - this->log.warning("VOL_OPT has an unusual num_children (0x%hX)", e.num_children.load()); - } - default_num_children = -1; // Skip adding children (because we do it here) - add(EnemyType::VOL_OPT_1); - for (size_t z = 0; z < 0x06; z++) { - add(EnemyType::VOL_OPT_PILLAR); - } - for (size_t z = 0; z < 0x18; z++) { - add(EnemyType::VOL_OPT_MONITOR); - } - for (size_t z = 0; z < 0x02; z++) { - add(EnemyType::NONE); - } - add(EnemyType::VOL_OPT_AMP); - add(EnemyType::VOL_OPT_CORE); - add(EnemyType::NONE); - break; - case 0x00C5: // Unnamed subclass of TObjEnemyCustom - add(EnemyType::VOL_OPT_2); - break; - case 0x00C8: // TBoss4DarkFalz - if ((e.num_children != 0) && (e.num_children != 0x200)) { - this->log.warning("DARK_FALZ has an unusual num_children (0x%hX)", e.num_children.load()); - } - default_num_children = -1; // Skip adding children (because we do it here) - if (difficulty) { - add(EnemyType::DARK_FALZ_3); - } else { - add(EnemyType::DARK_FALZ_2); - } - for (size_t x = 0; x < 0x1FD; x++) { - add(difficulty == 3 ? EnemyType::DARVANT_ULTIMATE : EnemyType::DARVANT); - } - add(EnemyType::DARK_FALZ_3); - add(EnemyType::DARK_FALZ_2); - add(EnemyType::DARK_FALZ_1); - break; - case 0x00CA: // TBoss6PlotFalz - add(EnemyType::OLGA_FLOW_2); - default_num_children = 0x200; - break; - case 0x00CB: // TBoss7DeRolLeC - add(EnemyType::BARBA_RAY); - child_type = EnemyType::PIG_RAY; - default_num_children = 0x2F; - break; - case 0x00CC: // TBoss8Dragon - add(EnemyType::GOL_DRAGON); - default_num_children = 5; - break; - case 0x00D4: // TObjEneMe3StelthReal - add((e.uparam1 & 1) ? EnemyType::SINOW_SPIGELL : EnemyType::SINOW_BERILL); - default_num_children = 4; - break; - case 0x00D5: // TObjEneMerillLia - add((e.uparam1 & 0x01) ? EnemyType::MERILTAS : EnemyType::MERILLIA); - break; - case 0x00D6: // TObjEneBm9Mericarol - if (e.uparam1 == 0) { - add(EnemyType::MERICAROL); - } else { - add(((e.uparam1 % 3) == 2) ? EnemyType::MERICUS : EnemyType::MERIKLE); - } - break; - case 0x00D7: // TObjEneBm5GibonU - add((e.uparam1 & 0x01) ? EnemyType::ZOL_GIBBON : EnemyType::UL_GIBBON); - break; - case 0x00D8: // TObjEneGibbles - add(EnemyType::GIBBLES); - break; - case 0x00D9: // TObjEneMe1Gee - add(EnemyType::GEE); - break; - case 0x00DA: // TObjEneMe1GiGue - add(EnemyType::GI_GUE); - break; - case 0x00DB: // TObjEneDelDepth - add(EnemyType::DELDEPTH); - break; - case 0x00DC: // TObjEneDellBiter - add(EnemyType::DELBITER); - break; - case 0x00DD: // TObjEneDolmOlm - add(e.uparam1 ? EnemyType::DOLMDARL : EnemyType::DOLMOLM); - break; - case 0x00DE: // TObjEneMorfos - add(EnemyType::MORFOS); - break; - case 0x00DF: // TObjEneRecobox - add(EnemyType::RECOBOX); - child_type = EnemyType::RECON; - break; - case 0x00E0: // TObjEneMe3SinowZoaReal or TObjEneEpsilonBody - if ((episode == Episode::EP2) && (e.floor > 0x0F)) { - add(EnemyType::EPSILON); - default_num_children = 4; - child_type = EnemyType::EPSIGUARD; - } else { - add((e.uparam1 & 0x01) ? EnemyType::SINOW_ZELE : EnemyType::SINOW_ZOA); - } - break; - case 0x00E1: // TObjEneIllGill - add(EnemyType::ILL_GILL); - break; - case 0x0110: - add(EnemyType::ASTARK); - break; - case 0x0111: - if (e.floor > 0x05) { - add(e.fparam2 ? EnemyType::YOWIE_ALT : EnemyType::SATELLITE_LIZARD_ALT); - } else { - add(e.fparam2 ? EnemyType::YOWIE : EnemyType::SATELLITE_LIZARD); - } - break; - case 0x0112: - add(this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates->merissa_aa) - ? EnemyType::MERISSA_AA - : EnemyType::MERISSA_A); - break; - case 0x0113: - add(EnemyType::GIRTABLULU); - break; - case 0x0114: { - bool is_rare = this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates->pazuzu); - if (e.floor > 0x05) { - add(is_rare ? EnemyType::PAZUZU_ALT : EnemyType::ZU_ALT); - } else { - add(is_rare ? EnemyType::PAZUZU : EnemyType::ZU); - } - break; - } - case 0x0115: - if (e.uparam1 & 2) { - add(EnemyType::BA_BOOTA); - } else { - add((e.uparam1 & 1) ? EnemyType::ZE_BOOTA : EnemyType::BOOTA); - } - break; - case 0x0116: - add(this->check_and_log_rare_enemy(e.uparam1 & 0x01, rare_rates->dorphon_eclair) - ? EnemyType::DORPHON_ECLAIR - : EnemyType::DORPHON); - break; - case 0x0117: { - static const EnemyType types[3] = {EnemyType::GORAN, EnemyType::PYRO_GORAN, EnemyType::GORAN_DETONATOR}; - add(types[e.uparam1 % 3]); - break; - } - case 0x0119: { - bool is_rare = this->check_and_log_rare_enemy((e.fparam2 != 0.0f), rare_rates->kondrieu); - if (is_rare) { - add(EnemyType::KONDRIEU); - } else { - add((e.uparam1 & 1) ? EnemyType::SHAMBERTIN : EnemyType::SAINT_MILLION); - } - default_num_children = 0x18; - break; - } - - case 0x00C3: // TBoss3VoloptP01 - case 0x00C4: // TBoss3VoloptCore or subclass - case 0x00C6: // TBoss3VoloptMonitor - case 0x00C7: // TBoss3VoloptHiraisin - case 0x0118: - add(EnemyType::UNKNOWN); - break; - - default: - add(EnemyType::UNKNOWN); - this->log.warning( - "(Entry %zu, offset %zX in file) Invalid enemy type %04hX", - source_index, source_index * sizeof(EnemyEntry), e.base_type.load()); - break; - } - - if (default_num_children >= 0) { - size_t num_children = e.num_children ? e.num_children.load() : default_num_children; - if ((child_type == EnemyType::UNKNOWN) && !this->enemies.empty()) { - child_type = this->enemies.back().type; - } - for (size_t x = 0; x < num_children; x++) { - add(child_type); - } - } -} - -void Map::add_enemies_from_map_data( - Episode episode, - uint8_t difficulty, - uint8_t event, - uint8_t floor, - const void* data, - size_t size, - std::shared_ptr rare_rates) { - size_t entry_count = size / sizeof(EnemyEntry); - if (size != entry_count * sizeof(EnemyEntry)) { - throw runtime_error("data size is not a multiple of entry size"); - } - - StringReader r(data, size); - for (size_t y = 0; y < entry_count; y++) { - this->add_enemy(episode, difficulty, event, floor, y, r.get(), rare_rates); - } -} - -Map::DATParserRandomState::DATParserRandomState(uint32_t rare_seed) - : random(rare_seed), - location_table_random(0), - location_indexes_populated(0), - location_indexes_used(0), - location_entries_base_offset(0) { - this->location_index_table.fill(0); -} - -size_t Map::DATParserRandomState::rand_int_biased(size_t min_v, size_t max_v) { - float max_f = static_cast(max_v + 1); - uint32_t crypt_v = this->random.next(); - float det_f = static_cast(crypt_v); - return max(floorf((max_f * det_f) / UINT32_MAX_AS_FLOAT), min_v); -} - -uint32_t Map::DATParserRandomState::next_location_index() { - if (this->location_indexes_used < this->location_indexes_populated) { - return this->location_index_table.at(this->location_indexes_used++); - } - return 0; -} - -void Map::DATParserRandomState::generate_shuffled_location_table( - const Map::RandomEnemyLocationsHeader& header, StringReader r, uint16_t section) { - if (header.num_sections == 0) { - throw runtime_error("no locations defined"); - } - - StringReader sections_r = r.sub(header.section_table_offset, header.num_sections * sizeof(Map::RandomEnemyLocationSection)); - - size_t bs_min = 0; - size_t bs_max = header.num_sections - 1; - do { - size_t bs_mid = (bs_min + bs_max) / 2; - if (sections_r.pget(bs_mid * sizeof(Map::RandomEnemyLocationSection)).section < section) { - bs_min = bs_mid + 1; - } else { - bs_max = bs_mid; - } - } while (bs_min < bs_max); - - const auto& sec = sections_r.pget(bs_min * sizeof(Map::RandomEnemyLocationSection)); - if (section != sec.section) { - return; - } - - this->location_indexes_populated = sec.count; - this->location_indexes_used = 0; - this->location_entries_base_offset = sec.offset; - for (size_t z = 0; z < sec.count; z++) { - this->location_index_table.at(z) = z; - } - - for (size_t z = 0; z < 4; z++) { - for (size_t x = 0; x < sec.count; x++) { - uint32_t crypt_v = this->location_table_random.next(); - size_t choice = floorf((static_cast(sec.count) * static_cast(crypt_v)) / UINT32_MAX_AS_FLOAT); - uint32_t t = this->location_index_table[x]; - this->location_index_table[x] = this->location_index_table[choice]; - this->location_index_table[choice] = t; - } - } -} - -void Map::add_random_enemies_from_map_data( - Episode episode, - uint8_t difficulty, - uint8_t event, - uint8_t floor, - StringReader wave_events_segment_r, - StringReader locations_segment_r, - StringReader definitions_segment_r, - std::shared_ptr random_state, - std::shared_ptr rare_rates) { - - static const array rand_enemy_base_types = { - 0x44, 0x43, 0x41, 0x42, 0x40, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x80, - 0x81, 0x82, 0x83, 0x84, 0x85, 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, - 0xA7, 0xA8, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, - 0xDE, 0xDF, 0xE0, 0xE0, 0xE1}; - - const auto& wave_events_header = wave_events_segment_r.get(); - if (wave_events_header.format != 0x65767432) { // 'evt2' - throw runtime_error("cannot generate random enemies from non-evt2 event stream"); - } - wave_events_segment_r.go(wave_events_header.entries_offset); - - size_t action_stream_base_offset = this->event_action_stream.size(); - this->event_action_stream += wave_events_segment_r.pread( - wave_events_header.action_stream_offset, wave_events_segment_r.size() - wave_events_header.action_stream_offset); - - const auto& locations_header = locations_segment_r.get(); - const auto& definitions_header = definitions_segment_r.get(); - auto definitions_r = definitions_segment_r.sub( - definitions_header.entries_offset, - definitions_header.entry_count * sizeof(RandomEnemyDefinition)); - auto weights_r = definitions_segment_r.sub( - definitions_header.weight_entries_offset, - definitions_header.weight_entry_count * sizeof(RandomEnemyWeight)); - - for (size_t wave_entry_index = 0; wave_entry_index < wave_events_header.entry_count; wave_entry_index++) { - auto entry_log = this->log.sub(string_printf("(Entry %zu/%" PRIu32 ") ", wave_entry_index, wave_events_header.entry_count.load())); - const auto& entry = wave_events_segment_r.get(); - - size_t remaining_waves = random_state->rand_int_biased(1, entry.max_waves); - // Trace: at 0080E125 EAX is wave count - - le_uint32_t wave_next_event_id = entry.event_id; - uint32_t wave_number = entry.wave_number; - while (remaining_waves) { - remaining_waves--; - auto wave_log = entry_log.sub(string_printf("(Wave %zu) ", remaining_waves)); - - size_t remaining_enemies = random_state->rand_int_biased(entry.min_enemies, entry.max_enemies); - // Trace: at 0080E208 EDI is enemy count - - random_state->generate_shuffled_location_table(locations_header, locations_segment_r, entry.section); - // Trace: at 0080EBB0 *(EBP + 4) points to table (0x20 uint32_ts) - - while (remaining_enemies) { - remaining_enemies--; - auto enemy_log = wave_log.sub(string_printf("(Enemy %zu) ", remaining_enemies)); - - // TODO: Factor this sum out of the loops - weights_r.go(0); - size_t weight_total = 0; - while (!weights_r.eof()) { - weight_total += weights_r.get().weight; - } - // Trace: at 0080E2C2 EBX is weight_total - - size_t det = random_state->rand_int_biased(0, weight_total - 1); - // Trace: at 0080E300 EDX is det - - weights_r.go(0); - while (!weights_r.eof()) { - const auto& weight_entry = weights_r.get(); - if (det < weight_entry.weight) { - if ((weight_entry.base_type_index != 0xFF) && (weight_entry.definition_entry_num != 0xFF)) { - EnemyEntry e; - e.base_type = rand_enemy_base_types.at(weight_entry.base_type_index); - e.wave_number = wave_number; - e.section = entry.section; - e.floor = floor; - - size_t bs_min = 0; - size_t bs_max = definitions_header.entry_count - 1; - if (bs_max == 0) { - throw runtime_error("no available random enemy definitions"); - } - do { - size_t bs_mid = (bs_min + bs_max) / 2; - if (definitions_r.pget(bs_mid * sizeof(RandomEnemyDefinition)).entry_num < weight_entry.definition_entry_num) { - bs_min = bs_mid + 1; - } else { - bs_max = bs_mid; - } - } while (bs_min < bs_max); - - const auto& def = definitions_r.pget(bs_min * sizeof(RandomEnemyDefinition)); - if (def.entry_num == weight_entry.definition_entry_num) { - e.fparam1 = def.fparam1; - e.fparam2 = def.fparam2; - e.fparam3 = def.fparam3; - e.fparam4 = def.fparam4; - e.fparam5 = def.fparam5; - e.uparam1 = def.uparam1; - e.uparam2 = def.uparam2; - e.num_children = random_state->rand_int_biased(def.min_children, def.max_children); - } else { - throw runtime_error("random enemy definition not found"); - } - - const auto& loc = locations_segment_r.pget( - locations_header.entries_offset + sizeof(RandomEnemyLocationEntry) * random_state->next_location_index()); - e.x = loc.x; - e.y = loc.y; - e.z = loc.z; - e.x_angle = loc.x_angle; - e.y_angle = loc.y_angle; - e.z_angle = loc.z_angle; - - // Trace: at 0080E6FE CX is base_type - this->add_enemy(episode, difficulty, event, floor, 0, e, rare_rates); - } else { - enemy_log.warning("Cannot create enemy: parameters are missing"); - } - break; - } else { - det -= weight_entry.weight; - } - } - } - if (remaining_waves) { - /* ev.delay = */ random_state->rand_int_biased(entry.min_delay, entry.max_delay); - this->add_event(wave_next_event_id, entry.flags, floor, entry.section, wave_number, this->event_action_stream.size()); - this->event_action_stream.push_back(0x0C); - wave_next_event_id = entry.event_id + wave_number + 10000; - this->event_action_stream.append(reinterpret_cast(&wave_next_event_id), sizeof(wave_next_event_id)); - this->event_action_stream.push_back(0x01); - wave_number++; - } - } - - /* ev.delay = */ random_state->rand_int_biased(entry.min_delay, entry.max_delay); - this->add_event(wave_next_event_id, entry.flags, floor, entry.section, wave_number, action_stream_base_offset + entry.action_stream_offset); - wave_number++; - } -} - -void Map::add_event(uint32_t event_id, uint16_t flags, uint8_t floor, uint16_t section, uint16_t wave_number, uint32_t action_stream_offset) { - size_t index = this->events.size(); - auto& ev = this->events.emplace_back(); - ev.event_id = event_id; - ev.section = section; - ev.wave_number = wave_number; - ev.flags = flags; - ev.floor = floor; - ev.action_stream_offset = action_stream_offset; - - uint64_t k = (static_cast(floor) << 32) | event_id; - this->floor_and_event_id_to_index.emplace(k, index); - k = section_index_key(floor, section, wave_number); - this->floor_section_and_wave_number_to_event_index.emplace(k, index); -} - -vector Map::get_events(uint8_t floor, uint32_t event_id) { - uint64_t k = (static_cast(floor) << 32) | event_id; - vector ret; - for (auto its = this->floor_and_event_id_to_index.equal_range(k); its.first != its.second; its.first++) { - ret.emplace_back(&this->events.at(its.first->second)); - } - return ret; -} - -vector Map::get_events(uint8_t floor, uint32_t event_id) const { - uint64_t k = (static_cast(floor) << 32) | event_id; - vector ret; - for (auto its = this->floor_and_event_id_to_index.equal_range(k); its.first != its.second; its.first++) { - ret.emplace_back(&this->events.at(its.first->second)); - } - return ret; -} - -void Map::add_events_from_map_data(uint8_t floor, const void* data, size_t size) { - StringReader r(data, size); - const auto& header = r.get(); - if (header.format != 0) { - throw runtime_error("events section format is not zero"); - } - - size_t action_stream_base_offset = this->event_action_stream.size(); - this->event_action_stream += r.pread(header.action_stream_offset, r.size() - header.action_stream_offset); - - this->events.reserve(this->events.size() + header.entry_count); - auto events_r = r.sub(header.entries_offset, sizeof(Event1Entry) * header.entry_count); - while (!events_r.eof()) { - const auto& entry = events_r.get(); - this->add_event(entry.event_id, entry.flags, floor, entry.section, entry.wave_number, entry.action_stream_offset + action_stream_base_offset); - } -} - -vector Map::collect_quest_map_data_sections(const void* data, size_t size) { - vector ret; - StringReader r(data, size); - while (!r.eof()) { - size_t header_offset = r.where(); - const auto& header = r.get(); - - if (header.type() == SectionHeader::Type::END && header.section_size == 0) { - break; - } - if (header.section_size < sizeof(header)) { - throw runtime_error(string_printf("quest layout has invalid section header at offset 0x%zX", r.where() - sizeof(header))); - } - - if (header.floor > 0x100) { - throw runtime_error("section floor number too large"); - } - - if (header.floor >= ret.size()) { - ret.resize(header.floor + 1); - } - auto& floor_sections = ret[header.floor]; - switch (header.type()) { - case SectionHeader::Type::OBJECTS: - if (floor_sections.objects != 0xFFFFFFFF) { - throw runtime_error("multiple objects sections for same floor"); - } - floor_sections.objects = header_offset; - break; - case SectionHeader::Type::ENEMIES: - if (floor_sections.enemies != 0xFFFFFFFF) { - throw runtime_error("multiple enemies sections for same floor"); - } - floor_sections.enemies = header_offset; - break; - case SectionHeader::Type::WAVE_EVENTS: - if (floor_sections.wave_events != 0xFFFFFFFF) { - throw runtime_error("multiple wave events sections for same floor"); - } - floor_sections.wave_events = header_offset; - break; - case SectionHeader::Type::RANDOM_ENEMY_LOCATIONS: - if (floor_sections.random_enemy_locations != 0xFFFFFFFF) { - throw runtime_error("multiple random enemy locations sections for same floor"); - } - floor_sections.random_enemy_locations = header_offset; - break; - case SectionHeader::Type::RANDOM_ENEMY_DEFINITIONS: - if (floor_sections.random_enemy_definitions != 0xFFFFFFFF) { - throw runtime_error("multiple random enemy definitions sections for same floor"); - } - floor_sections.random_enemy_definitions = header_offset; - break; - default: - throw runtime_error("invalid section type"); - } - r.skip(header.data_size); - } - return ret; -} - -void Map::add_entities_from_quest_data( - Episode episode, - uint8_t difficulty, - uint8_t event, - const void* data, - size_t size, - std::shared_ptr rare_rates) { - auto all_floor_sections = this->collect_quest_map_data_sections(data, size); - - StringReader r(data, size); - shared_ptr random_state; - for (size_t floor = 0; floor < all_floor_sections.size(); floor++) { - const auto& floor_sections = all_floor_sections[floor]; - - if (floor_sections.objects != 0xFFFFFFFF) { - const auto& header = r.pget(floor_sections.objects); - if (header.data_size % sizeof(ObjectEntry)) { - throw runtime_error("quest layout object section size is not a multiple of object entry size"); - } - this->add_objects_from_map_data(floor, r.pgetv(floor_sections.objects + sizeof(header), header.data_size), header.data_size); - } - - if ((floor_sections.wave_events != 0xFFFFFFFF) && - (floor_sections.random_enemy_locations != 0xFFFFFFFF) && - (floor_sections.random_enemy_definitions != 0xFFFFFFFF)) { - // Challenge Mode random enemy waves - const auto& wave_events_header = r.pget(floor_sections.wave_events); - const auto& random_enemy_locations_header = r.pget(floor_sections.random_enemy_locations); - const auto& random_enemy_definitions_header = r.pget(floor_sections.random_enemy_definitions); - if (!random_state) { - random_state = make_shared(this->rare_seed); - } - this->add_random_enemies_from_map_data( - episode, - difficulty, - event, - floor, - r.sub(floor_sections.wave_events + sizeof(SectionHeader), wave_events_header.data_size), - r.sub(floor_sections.random_enemy_locations + sizeof(SectionHeader), random_enemy_locations_header.data_size), - r.sub(floor_sections.random_enemy_definitions + sizeof(SectionHeader), random_enemy_definitions_header.data_size), - random_state, - rare_rates); - - } else { - // Non-Challenge (standard) enemies - if (floor_sections.enemies != 0xFFFFFFFF) { - const auto& header = r.pget(floor_sections.enemies); - if (header.data_size % sizeof(EnemyEntry)) { - throw runtime_error("quest layout enemy section size is not a multiple of enemy entry size"); - } - this->add_enemies_from_map_data( - episode, - difficulty, - event, - floor, - r.pgetv(floor_sections.enemies + sizeof(header), header.data_size), - header.data_size, - rare_rates); - } - - if (floor_sections.wave_events != 0xFFFFFFFF) { - const auto& wave_events_header = r.pget(floor_sections.wave_events); - const void* data = r.pgetv(floor_sections.wave_events + sizeof(SectionHeader), wave_events_header.data_size); - this->add_events_from_map_data(floor, data, wave_events_header.data_size); - } - } - } -} - -const Map::Enemy& Map::find_enemy(uint8_t floor, EnemyType type) const { - return const_cast(this)->find_enemy(floor, type); -} - -Map::Enemy& Map::find_enemy(uint8_t floor, EnemyType type) { - if (enemies.empty()) { - throw out_of_range("no enemies defined"); - } - // TODO: Linear search is bad here. Do something better, like binary search - // for the floor start and just linear search through the floor enemies. - for (auto& e : this->enemies) { - if (e.floor == floor && e.type == type) { - return e; - } - } - throw out_of_range("enemy not found"); -} - -std::vector Map::get_objects(uint8_t floor, uint16_t section, uint16_t group) { - uint64_t k = section_index_key(floor, section, group); - vector ret; - for (auto its = this->floor_section_and_group_to_object_index.equal_range(k); its.first != its.second; its.first++) { - ret.emplace_back(&this->objects.at(its.first->second)); - } - return ret; -} - -std::vector Map::get_enemies(uint8_t floor, uint16_t section, uint16_t wave_number) { - uint64_t k = section_index_key(floor, section, wave_number); - vector ret; - for (auto its = this->floor_section_and_wave_number_to_enemy_index.equal_range(k); its.first != its.second; its.first++) { - ret.emplace_back(&this->enemies.at(its.first->second)); - } - return ret; -} - -std::vector Map::get_events(uint8_t floor, uint16_t section, uint16_t wave_number) { - uint64_t k = section_index_key(floor, section, wave_number); - vector ret; - for (auto its = this->floor_section_and_wave_number_to_event_index.equal_range(k); its.first != its.second; its.first++) { - ret.emplace_back(&this->events.at(its.first->second)); - } - return ret; -} - -std::vector Map::get_events(uint8_t floor) { - uint64_t k_start = (static_cast(floor) << 32); - uint64_t k_end = (static_cast(floor + 1) << 32); - vector ret; - for (auto it = this->floor_and_event_id_to_index.lower_bound(k_start); - (it != this->floor_and_event_id_to_index.end()) && (it->first < k_end); - it++) { - ret.emplace_back(&this->events.at(it->second)); - } - return ret; -} - -template -static string disassemble_vector_file_t(const void* data, size_t size, size_t* entry_number, char type_ch) { - deque ret; - StringReader r(data, size); - - size_t local_entry_number = 0; - if (!entry_number) { - entry_number = &local_entry_number; - } - - while (r.remaining() >= sizeof(EntryT)) { - string o_str = r.get().str(); - ret.emplace_back(string_printf("/* %c-%zX */ %s", type_ch, (*entry_number)++, o_str.c_str())); - } - if (r.remaining()) { - ret.emplace_back("// Warning: section size is not a multiple of entry size"); - size_t size = r.remaining(); - ret.emplace_back(format_data(r.getv(size), size)); - } - return join(ret, "\n"); -} - -string Map::disassemble_objects_data(const void* data, size_t size, size_t* object_number) { - return disassemble_vector_file_t(data, size, object_number, 'K'); -} - -string Map::disassemble_enemies_data(const void* data, size_t size, size_t* enemy_number) { - return disassemble_vector_file_t(data, size, enemy_number, 'S'); -} - -string Map::disassemble_wave_events_data(const void* data, size_t size, uint8_t floor) { - deque ret; - StringReader r(data, size); - - const auto& evt_header = r.get(); - if (evt_header.format == 0x65767432) { // 'evt2' - ret.emplace_back(".evt2_format"); // TODO - size_t size = r.remaining(); - ret.emplace_back(format_data(r.getv(size), size)); - } else { - auto action_stream_r = r.sub(evt_header.action_stream_offset); - for (size_t z = 0; z < evt_header.entry_count; z++) { - const auto& entry = r.get(); - ret.emplace_back(string_printf("/* W-%02hhX-%" PRIX32 " */ [Event1Entry flags=%04hX type=%04hX section=%04hX wave_number=%04hX delay=%" PRIu32 "]", - floor, - entry.event_id.load(), - entry.flags.load(), - entry.event_type.load(), - entry.section.load(), - entry.wave_number.load(), - entry.delay.load())); - auto ev_actions_r = action_stream_r.sub(entry.action_stream_offset); - bool should_continue = true; - while (!ev_actions_r.eof() && should_continue) { - uint8_t opcode = ev_actions_r.get_u8(); - switch (opcode) { - case 0x00: - ret.emplace_back(string_printf(" 00 nop")); - break; - case 0x01: - ret.emplace_back(string_printf(" 01 stop")); - should_continue = false; - break; - case 0x08: { - uint16_t section = ev_actions_r.get_u16l(); - uint16_t group = ev_actions_r.get_u16l(); - ret.emplace_back(string_printf(" 08 %04hX %04hX construct_objects section=%04hX group=%04hX", - section, group, section, group)); - break; - } - case 0x09: { - uint16_t section = ev_actions_r.get_u16l(); - uint16_t wave_number = ev_actions_r.get_u16l(); - ret.emplace_back(string_printf(" 09 %04hX %04hX construct_enemies section=%04hX wave_number=%04hX", - section, wave_number, section, wave_number)); - break; - } - case 0x0A: { - uint16_t id = ev_actions_r.get_u16l(); - ret.emplace_back(string_printf(" 0A %04hX enable_switch_flag id=%04hX", id, id)); - break; - } - case 0x0B: { - uint16_t id = ev_actions_r.get_u16l(); - ret.emplace_back(string_printf(" 0B %04hX disable_switch_flag id=%04hX", id, id)); - break; - } - case 0x0C: { - uint32_t event_id = ev_actions_r.get_u32l(); - ret.emplace_back(string_printf(" 0C %08" PRIX32 " trigger_event event_id=%08" PRIX32, event_id, event_id)); - break; - } - case 0x0D: { - uint16_t section = ev_actions_r.get_u16l(); - uint16_t wave_number = ev_actions_r.get_u16l(); - ret.emplace_back(string_printf(" 0D %04hX %04hX construct_enemies_stop section=%04hX wave_number=%04hX", - section, wave_number, section, wave_number)); - break; - } - default: - ret.emplace_back(string_printf(" %02hhX .invalid", opcode)); - } - } - } - } - - return join(ret, "\n"); -} - -string Map::disassemble_quest_data(const void* data, size_t size) { - auto all_floor_sections = Map::collect_quest_map_data_sections(data, size); - - deque ret; - StringReader r(data, size); - size_t object_number = 0; - size_t enemy_number = 0; - for (size_t floor = 0; floor < all_floor_sections.size(); floor++) { - const auto& floor_sections = all_floor_sections[floor]; - - if (floor_sections.objects != 0xFFFFFFFF) { - ret.emplace_back(string_printf(".objects %zu", floor)); - const auto& header = r.pget(floor_sections.objects); - size_t offset = floor_sections.objects + sizeof(SectionHeader); - ret.emplace_back(Map::disassemble_objects_data(r.pgetv(offset, header.data_size), header.data_size, &object_number)); - } - if (floor_sections.enemies != 0xFFFFFFFF) { - ret.emplace_back(string_printf(".enemies %zu", floor)); - const auto& header = r.pget(floor_sections.enemies); - size_t offset = floor_sections.enemies + sizeof(SectionHeader); - ret.emplace_back(Map::disassemble_enemies_data(r.pgetv(offset, header.data_size), header.data_size, &enemy_number)); - } - if (floor_sections.wave_events != 0xFFFFFFFF) { - ret.emplace_back(string_printf(".wave_events %zu", floor)); - const auto& header = r.pget(floor_sections.wave_events); - size_t offset = floor_sections.wave_events + sizeof(SectionHeader); - ret.emplace_back(Map::disassemble_wave_events_data(r.pgetv(offset, header.data_size), header.data_size, floor)); - } - if (floor_sections.random_enemy_locations != 0xFFFFFFFF) { - ret.emplace_back(string_printf(".random_enemy_locations %zu", floor)); - const auto& header = r.pget(floor_sections.random_enemy_locations); - size_t offset = floor_sections.random_enemy_locations + sizeof(SectionHeader); - auto sub_r = r.sub(offset, header.data_size); - ret.emplace_back(format_data(sub_r.getv(sub_r.remaining()), header.data_size, offset)); - } - if (floor_sections.random_enemy_definitions != 0xFFFFFFFF) { - ret.emplace_back(string_printf(".random_enemy_definitions %zu", floor)); - const auto& header = r.pget(floor_sections.random_enemy_definitions); - size_t offset = floor_sections.random_enemy_definitions + sizeof(SectionHeader); - auto sub_r = r.sub(offset, header.data_size); - ret.emplace_back(format_data(sub_r.getv(sub_r.remaining()), header.data_size, offset)); - } - } - - return join(ret, "\n") + "\n"; -} - -SetDataTableBase::SetDataTableBase(Version version) : version(version) {} - -parray SetDataTableBase::generate_variations( - Episode episode, bool is_solo, std::shared_ptr opt_rand_crypt) const { - parray ret; - for (size_t floor = 0; floor < 0x10; floor++) { - auto num_vars = this->num_free_roam_variations_for_floor(episode, is_solo, floor); - ret[floor * 2] = (num_vars.first > 1) ? (random_from_optional_crypt(opt_rand_crypt) % num_vars.first) : 0; - ret[floor * 2 + 1] = (num_vars.second > 1) ? (random_from_optional_crypt(opt_rand_crypt) % num_vars.second) : 0; - } - return ret; -} - -vector SetDataTableBase::map_filenames_for_variations( - const parray& variations, Episode episode, GameMode mode, FilenameType type) const { - vector ret; - for (uint8_t floor = 0; floor < 0x10; floor++) { - ret.emplace_back(this->map_filename_for_variation( - floor, variations[floor * 2], variations[floor * 2 + 1], episode, mode, type)); - } - for (uint8_t floor = 0x10; floor < 0x12; floor++) { - ret.emplace_back(this->map_filename_for_variation(floor, 0, 0, episode, mode, type)); - } - return ret; -} - -uint8_t SetDataTableBase::default_area_for_floor(Episode episode, uint8_t floor) const { - // For some inscrutable reason, Pioneer 2's area number in Episode 4 is - // discontiguous with all the rest. Why, Sega?? - static const std::array areas_ep1 = { - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11}; - static const std::array areas_ep2_gc_nte = { - 0x00, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0xFF, 0xFF}; - static const std::array areas_ep2 = { - 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23}; - static const std::array areas_ep4 = { - 0x2D, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2E, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; - switch (episode) { - case Episode::EP1: - return areas_ep1.at(floor); - case Episode::EP2: { - const auto& areas = ((this->version == Version::GC_NTE) ? areas_ep2_gc_nte : areas_ep2); - return areas.at(floor); - } - case Episode::EP4: - return areas_ep4.at(floor); - default: - throw logic_error("incorrect episode"); - } -} - -SetDataTable::SetDataTable(Version version, const string& data) : SetDataTableBase(version) { - if (is_big_endian(this->version)) { - this->load_table_t(data); - } else { - this->load_table_t(data); - } -} - -template -void SetDataTable::load_table_t(const string& data) { - using U32T = typename conditional::type; - - StringReader r(data); - - struct Footer { - U32T table3_offset; - U32T table3_count; // In le_uint16_ts (so *2 for size in bytes) - U32T unknown_a3; // == 1 - U32T unknown_a4; // == 0 - U32T root_table_offset_offset; - U32T unknown_a6; // == 0 - U32T unknown_a7; // == 0 - U32T unknown_a8; // == 0 - } __packed_ws__(Footer, 0x20); - - if (r.size() < sizeof(Footer)) { - throw runtime_error("set data table is too small"); - } - auto& footer = r.pget