diff --git a/resources/effect_id.ts b/resources/effect_id.ts index 2f04d2d9c7..ec7b4b3a44 100644 --- a/resources/effect_id.ts +++ b/resources/effect_id.ts @@ -2,6 +2,8 @@ // DO NOT EDIT THIS FILE DIRECTLY const data = { + '2': 'E88', + '4': 'E8A', 'ABitBerserk': '905', 'AMansBestFriend': '16E', 'Abandon': '2DA', @@ -100,6 +102,7 @@ const data = { 'ArmysPaeon': '8AA', 'ArrowHeld': '39A', 'ArtificialBoost': '700', + 'Ascended': 'E02', 'Ashen': '56C', 'AspectedBenefic': '343', 'AspectedHelios': '344', @@ -117,13 +120,17 @@ const data = { 'AstralEssence': '6AE', 'AstralFire': 'AD', 'AstralRealignment': '18E', + 'AstralTilt': 'DF9', 'AstralWarmth': 'C90', + 'AstralbrightSoul': 'DFC', + 'AstralstrongSoul': 'DFE', 'AtDeathsDoor': 'AB2', 'AtTheLimit': '95C', 'AtkDefUp': '3D2', 'AtkDown': '3C3', 'AtkUp': '3C2', 'Atlas': '89E', + 'Atmosfaction': 'E07', 'Atrophy': '287', 'AttackDown': '1A', 'AttackMagicPotencyDown': '22', @@ -139,7 +146,6 @@ const data = { 'Autophysis': 'A3D', 'AvariciousRuin': '9CF', 'BackFromTheBrink': '9D5', - 'BackUnseen': '6AD', 'BackWithThee': '8C1', 'BackwardBearing': 'B14', 'BackwardWhimsy': 'B1A', @@ -177,9 +183,12 @@ const data = { 'BeyondDeath': '566', 'BeyondLimits': '5FA', 'BibliotaphSimulation': '5D4', + 'BigBang': 'EB0', + 'BigBounce': 'EB1', 'BigbulgeBiggerbrain': '3EB', 'BigbulgeGoblixer': '3E6', 'BindResistance': '547', + 'BindingDark': 'EB6', 'BindingSoulSnare': 'DDB', 'Bio': 'B3', 'BioIi': 'BD', @@ -226,6 +235,7 @@ const data = { 'BodilyManipulation': 'CF6', 'Boiling': 'B52', 'BoleHeld': '399', + 'BondsOfDarkness': 'EB7', 'Boosted': '990', 'BootCampMode': '902', 'BorneHeart': '612', @@ -246,6 +256,7 @@ const data = { 'BreathOfMagic': 'E80', 'BreathOfTheGorgon': 'CFF', 'Briar': '1B4', + 'BrightPulse': 'E9C', 'BrightwingedFortitude': 'A64', 'BrightwingedFury': 'A68', 'BrilliantDynamis': 'D76', @@ -255,6 +266,10 @@ const data = { 'Brotherhood': '4A1', 'BrotherlyLove': '627', 'BrushWithDeath': '84F', + 'BubbleGaol': 'EA2', + 'BubbleNet': 'EA0', + 'BubbleWeave': 'E9F', + 'BullsEye': 'E9E', 'Bulwark': '4D', 'BurningBrand': '850', 'BurningCounter': '3B5', @@ -292,6 +307,7 @@ const data = { 'Chelomorph': 'CF3', 'ChelonianGate': '9C0', 'ChilledToTheBone': '794', + 'ChimericSoul': 'DD9', 'Chiromorph': '2FF', 'Chiten': '4D8', 'ChocoBeak': 'EC', @@ -311,6 +327,7 @@ const data = { 'CleanerShot': '359', 'CloakOfDeath': '253', 'Clockwork': '5A3', + 'CloseCaloric': 'E05', 'CloyingCondensation': '9E4', 'CocoonOfThePenitent': '70F', 'CodeMi': 'D77', @@ -364,12 +381,14 @@ const data = { 'Craven': '58D', 'CravenCompanionship': 'B96', 'CreepingPoison': 'B0A', + 'CriticalFactor': 'E0A', 'CriticalPerformanceBug': 'D65', 'CriticalSkill': '41', 'CriticalUp': '4A4', 'CritterVulnerability': '3CE', 'CrownOfTheGorgon': 'D18', 'CrumblingBulwark': '64F', + 'CrystalCourier': 'E78', 'CrystalVeil': '142', 'Cube': '42E', 'CureIiiReady': 'C0B', @@ -388,7 +407,6 @@ const data = { 'DarkForce': '360', 'DarkMind': '2EA', 'DarkResistanceDownIi': 'CFB', - 'DarkWhispers': 'DCF', 'DarkenedFire': 'AC9', 'Darkness': '38A', 'DarksAccord': 'DE2', @@ -454,11 +472,14 @@ const data = { 'DivineCommandmentTurn': '591', 'DivineMight': 'A71', 'DivineSeal': '9F', + 'DivisiveDark': 'EB2', 'Dizzy': 'B9E', + 'DmoniacBonds': 'DDE', 'DotonCorruption': 'C07', 'DotonHeavy': '1F6', 'Double': '295', 'DoubleEdgeL': '675', + 'DoubledDark': 'EB4', 'DownAndOut': 'A47', 'DownTheRabbitHole': '5FB', 'DownpourOfDeath': '83', @@ -480,6 +501,7 @@ const data = { 'DrunkWithPower': 'E0B', 'Duality': '316', 'DuelOrDie': '9F1', + 'DuodmoniacBonds': 'DDF', 'DustPoisoning': '197', 'DutiesAsAssigned': '96F', 'DynamicFluid': '641', @@ -541,7 +563,9 @@ const data = { 'Enshielded': '76D', 'Enstone': 'CF', 'EntangledFlames': 'AC7', + 'Entropifaction': 'E08', 'Entropy': '640', + 'EntropyResistance': 'E21', 'Enwater': 'D1', 'EpPenalty': '946', 'EpicEcho': 'AAE', @@ -655,6 +679,7 @@ const data = { 'FlourishingSymmetry': 'BC9', 'FlourishingWindmill': '718', 'FlyingHigh': '6C2', + 'FoamyFetters': 'ECC', 'Focalization': '818', 'FoolsFigure': '184', 'FoolsTightrope': '181', @@ -664,7 +689,6 @@ const data = { 'ForcedWithdrawal': '3D1', 'ForeMarkOfTheTides': 'AD2', 'Foresight': '53', - 'ForkedLightning': '24B', 'FormlessFist': '9D1', 'ForwardBearing': 'B13', 'ForwardWhimsy': 'B8E', @@ -682,7 +706,6 @@ const data = { 'FreezingCounter': '3B6', 'Frenzied': '278', 'FreshPerspective': '94B', - 'FrontUnseen': 'A54', 'FrontalBlasterCharge': 'D8C', 'FrontlineForte': 'C45', 'FrontlineMarch': 'C43', @@ -726,6 +749,7 @@ const data = { 'Gloam': '1DC', 'GlossalResistanceDown': 'CF7', 'GlovesOff': 'D82', + 'GloweringDark': 'EB5', 'GnashingWolf': 'B93', 'Goad': '1EF', 'GoblixerGrumblygut': '3EA', @@ -780,8 +804,10 @@ const data = { 'HeartOfTheMountain': '148', 'Heartless': '613', 'Heat': 'C4C', + 'HeatWave': 'EAA', 'HeavenlyShield': '6C7', 'Heavensent': 'C68', + 'HeavensflameSoul': 'DFA', 'HeavyFeet': '2C5', 'HeavyResistance': '546', 'HeavySoulSnare': 'DDC', @@ -824,6 +850,8 @@ const data = { 'HpBoost3': '618', 'HpBoost4': '619', 'Hubris': 'A2B', + 'HydrobulletTarget': 'EA4', + 'HydrofallTarget': 'EA3', 'Hypercharge': '2B0', 'HyperchargedCondensation': '951', 'Hypervelocity': 'BE7', @@ -842,6 +870,7 @@ const data = { 'ImmortalConception': 'D10', 'ImmortalSpark': 'D0F', 'Impactful': '557', + 'Impassioned': 'EAF', 'ImperfectionAlpha': 'D02', 'ImperfectionBeta': 'D03', 'ImperfectionGamma': 'D04', @@ -876,6 +905,7 @@ const data = { 'InnerDragon': '132', 'InnerQuiet': 'FB', 'InnerStrength': 'A67', + 'Inscribed': 'E94', 'Intemperate': '8E3', 'IntensifiedWailing': 'DEC', 'InternalRelease': '64', @@ -939,7 +969,6 @@ const data = { 'LeftBlasterCharge': 'D8E', 'LeftEye': '5AE', 'LeftMarkOfTheTides': 'AD4', - 'LeftUnseen': '6AC', 'LeftWithThee': '8C2', 'LeftwardBearing': 'B15', 'LeftwardWhimsy': 'B19', @@ -1126,6 +1155,7 @@ const data = { 'NeapTide': 'D01', 'Necrosis': 'B95', 'Nectar': '39F', + 'NeedleVeil': 'EA7', 'NewMoon': '600', 'Nightmare': '1A7', 'Noctoshield': '1AA', @@ -1221,6 +1251,7 @@ const data = { 'PowderBarrel': 'BE3', 'PowderMark': '993', 'PowerSlash': '55C', + 'Powerful': 'D28', 'Pox': '15B', 'PrayerOfLight': 'BC5', 'PrayersOfHope': 'B0C', @@ -1250,9 +1281,11 @@ const data = { 'PureMuscle': '6FA', 'PushBack': '366', 'Pyramid': '42F', + 'Pyrefaction': 'E06', 'PyreticBooster': '8F6', 'Quadruple': 'AAC', 'Quarantine': '3A4', + 'QuarteredSoul': 'DFF', 'QuickeningDynamis': 'D74', 'QuickerNock': '84', 'Quintuplecast': '948', @@ -1270,6 +1303,7 @@ const data = { 'RapidRecast': '66D', 'RaptorForm': '6C', 'RatAndMouse': 'E19', + 'Rationing': '43C', 'RavenBlight': '1CA', 'RawIntuition': '2DF', 'RayOfFortitude': 'A41', @@ -1320,7 +1354,6 @@ const data = { 'RightBlasterCharge': 'D8F', 'RightEye': '776', 'RightMarkOfTheTides': 'AD5', - 'RightUnseen': '6AB', 'RightWithThee': '8C3', 'RightwardBearing': 'B16', 'RightwardWhimsy': 'B18', @@ -1330,6 +1363,9 @@ const data = { 'RiseOfThePhoenix': '251', 'RisingRhythm': 'A88', 'RiteOfPassage': 'CCD', + 'RiteOfTheGreatWhale': 'E97', + 'RiteOfTheSeaTurtle': 'E96', + 'RiteOfTheSparrow': 'E95', 'RoadToToad': '3F1', 'RoleCall': 'AF2', 'RolePlaying': '5FE', @@ -1386,10 +1422,9 @@ const data = { 'SequenceAs1': '152', 'SeraphFlight': 'C18', 'SeraphicIllumination': '753', + 'SeventhHeaven': 'E9D', 'SevereDamage': '2C9', - 'SewerDweller': 'D28', 'ShackledApart': '978', - 'ShackledTogether': '979', 'Shadewalker': '314', 'ShadowFlare': 'BE', 'ShadowLimb': '47C', @@ -1442,6 +1477,8 @@ const data = { 'SleepingDark': '692', 'SleepingLight': '693', 'SleeveDraw': '786', + 'SlipperyGround': 'E6D', + 'SlipperySlope': 'E77', 'Slipping': 'C9B', 'SlowResistance': '548', 'Smackdown': '814', @@ -1478,6 +1515,7 @@ const data = { 'SpearDrawn': '394', 'SpearHeld': '39B', 'SpearfishersIntuition': 'B4B', + 'Spectator': 'E7C', 'SpellInWaiting': '710', 'SpellInWaitingDarkAeroIii': '99F', 'SpellInWaitingDarkBlizzardIii': '99E', @@ -1493,7 +1531,6 @@ const data = { 'SpicyCirculationI': 'E1D', 'SpicyCirculationXxii': 'E1C', 'SpineshatterDiveTarget': 'AC4', - 'Spinning': 'B9D', 'SpireHeld': '39D', 'SpiritDartL': '676', 'SpiritOfTheAetherweaver': '907', @@ -1527,10 +1564,10 @@ const data = { 'SquadronEngineeringManual': '43A', 'SquadronEnlistmentManual': '43E', 'SquadronGearMaintenanceManual': '43D', - 'SquadronRationingManual': '43C', 'SquadronSpiritbondingManual': '43B', 'SquadronSurvivalManual': '439', 'SquirrellyPrayer': 'E15', + 'StableSystem': 'E22', 'Staggered': '2CB', 'StandardFinish': '839', 'StandingFirm': '8D9', @@ -1562,6 +1599,8 @@ const data = { 'Stunstrikes': '46', 'StygianTendrils': '952', 'SubtleRuin': '9D0', + 'SubtractiveSuppressorAlpha': 'E8C', + 'SubtractiveSuppressorBeta': 'E8D', 'SubversiveStance': '1CD', 'Succor': 'A6', 'SuffocatedWill': '254', @@ -1578,6 +1617,7 @@ const data = { 'Supersplice': 'D13', 'Surecast': 'A0', 'SurfaceSlap': '70B', + 'SurgeVector': 'E8B', 'SurgingTempest': 'A75', 'SurgingWaters': '73A', 'SurpanakhasFury': '852', @@ -1612,6 +1652,7 @@ const data = { 'TenebrousGrasp': 'B10', 'TerminalVelocity': '581', 'Testudo': '883', + 'TetradmoniacBonds': 'E70', 'ThaliaksWard': 'E7', 'ThatWhichBindsUs': '169', 'TheArrow': '75C', @@ -1652,6 +1693,7 @@ const data = { 'ThunderIv': '4BA', 'Thundercloud': 'A4', 'ThunderousEcho': 'CDD', + 'TimeAndTide': 'E9B', 'TimesUp': '5B8', 'Tingling': '9BC', 'Tireless': '277', @@ -1707,6 +1749,9 @@ const data = { 'UmbralFreeze': 'C91', 'UmbralIce': 'B0', 'UmbralRays': 'B5D', + 'UmbralTilt': 'DF8', + 'UmbralbrightSoul': 'DFB', + 'UmbralstrongSoul': 'DFD', 'Unbridled': '30E', 'Unchained': '5C', 'Uncontrollable': '2DE', @@ -1725,6 +1770,7 @@ const data = { 'UnsealedSeitonTenchu': 'C78', 'UnshakableLoyalty': 'BC0', 'Unstable': '8C9', + 'UnstableFactor': 'E09', 'Unveiled': '654', 'UnwaveringWill': '89F', 'UnwillingHost': '3A9', @@ -1823,34 +1869,12 @@ const data = { 'WrathfulRevelation': 'DF2', 'Wyrmclaw': '8D2', 'Wyrmfang': '8D3', + 'XMarkedSoul': 'E00', 'YellowPaint': '5BB', 'YourMove2Squares': '9B0', 'YourMove3Squares': '9B1', 'YourMove4Squares': '9B2', 'Zoe': 'A33', - '_Rsv_3545_1_1_0_0_S74cfc3b0_E74cfc3b0': 'DD9', - '_Rsv_3550_1_1_0_0_S74cfc3b0_E74cfc3b0': 'DDE', - '_Rsv_3551_1_1_0_0_S74cfc3b0_E74cfc3b0': 'DDF', - '_Rsv_3576_1_1_0_0_S74cfc3b0_E74cfc3b0': 'DF8', - '_Rsv_3577_1_1_0_0_S74cfc3b0_E74cfc3b0': 'DF9', - '_Rsv_3578_1_1_0_0_S74cfc3b0_E74cfc3b0': 'DFA', - '_Rsv_3579_1_1_0_0_S74cfc3b0_E74cfc3b0': 'DFB', - '_Rsv_3580_1_1_0_0_S74cfc3b0_E74cfc3b0': 'DFC', - '_Rsv_3581_1_1_0_0_S74cfc3b0_E74cfc3b0': 'DFD', - '_Rsv_3582_1_1_0_0_S74cfc3b0_E74cfc3b0': 'DFE', - '_Rsv_3583_1_1_0_0_S74cfc3b0_E74cfc3b0': 'DFF', - '_Rsv_3584_1_1_0_0_S74cfc3b0_E74cfc3b0': 'E00', - '_Rsv_3586_1_1_0_0_S74cfc3b0_E74cfc3b0': 'E02', - '_Rsv_3588_1_1_0_0_S74cfc3b0_E74cfc3b0': 'E04', - '_Rsv_3589_1_1_0_0_S74cfc3b0_E74cfc3b0': 'E05', - '_Rsv_3590_1_1_0_0_S74cfc3b0_E74cfc3b0': 'E06', - '_Rsv_3591_1_1_0_0_S74cfc3b0_E74cfc3b0': 'E07', - '_Rsv_3592_1_1_0_0_S74cfc3b0_E74cfc3b0': 'E08', - '_Rsv_3593_1_1_0_0_S74cfc3b0_E74cfc3b0': 'E09', - '_Rsv_3594_1_1_0_0_S74cfc3b0_E74cfc3b0': 'E0A', - '_Rsv_3617_1_1_0_0_S74cfc3b0_E74cfc3b0': 'E21', - '_Rsv_3618_1_1_0_0_S74cfc3b0_E74cfc3b0': 'E22', - '_Rsv_3696_1_1_0_0_S74cfc3b0_E74cfc3b0': 'E70', 'ii': 'C2A', } as const; diff --git a/resources/pet_names.ts b/resources/pet_names.ts index b0da127647..3a7c5ae2e4 100644 --- a/resources/pet_names.ts +++ b/resources/pet_names.ts @@ -31,6 +31,7 @@ const data: PetData = { '黄宝石泰坦', '绿宝石迦楼罗', '琥珀宝石兽', + '黑曜宝石兽', ], 'de': [ 'Smaragd-Karfunkel', @@ -155,6 +156,7 @@ const data: PetData = { '타이탄 토파즈', '가루다 에메랄드', '카벙클 앰버', + '카벙클 옵시디언', ], }; diff --git a/resources/world_id.ts b/resources/world_id.ts index 111ad267e9..3e3615a16e 100644 --- a/resources/world_id.ts +++ b/resources/world_id.ts @@ -1,6 +1,8 @@ -// Auto-generated from world_id.ts +// Auto-generated from gen_world_ids.ts // DO NOT EDIT THIS FILE DIRECTLY +// NOTE: This data is filtered to public worlds only (i.e. isPublic: true) + export type DataCenter = { id: number; name: string; @@ -29,70 +31,6 @@ export const worldNameToWorld = (name: string): World | undefined => { }; const data: Worlds = { - '0': { - 'id': 0, - 'internalName': 'crossworld', - 'name': 'Dev', - 'region': 0, - 'userType': 0, - }, - '1': { - 'id': 1, - 'internalName': 'reserved1', - 'name': 'Dev', - 'region': 0, - 'userType': 0, - }, - '2': { - 'id': 2, - 'internalName': 'c-contents', - 'name': 'c-contents', - 'region': 1, - 'userType': 0, - }, - '3': { - 'dataCenter': { - 'id': 1, - 'name': 'Elemental', - }, - 'id': 3, - 'internalName': 'c-whiteae', - 'name': 'c-whiteae', - 'region': 1, - 'userType': 1, - }, - '4': { - 'id': 4, - 'internalName': 'c-baudinii', - 'name': 'c-baudinii', - 'region': 1, - 'userType': 0, - }, - '5': { - 'id': 5, - 'internalName': 'c-contents2', - 'name': 'c-contents2', - 'region': 1, - 'userType': 0, - }, - '6': { - 'dataCenter': { - 'id': 1, - 'name': 'Elemental', - }, - 'id': 6, - 'internalName': 'c-funereus', - 'name': 'c-funereus', - 'region': 1, - 'userType': 1, - }, - '16': { - 'id': 16, - 'internalName': 'konconv', - 'name': 'konconv', - 'region': 1, - 'userType': 0, - }, '21': { 'dataCenter': { 'id': 9, @@ -137,27 +75,6 @@ const data: Worlds = { 'region': 1, 'userType': 1, }, - '25': { - 'id': 25, - 'internalName': 'Chaos', - 'name': 'Chaos', - 'region': 1, - 'userType': 1, - }, - '26': { - 'id': 26, - 'internalName': 'Hecatoncheir', - 'name': 'Hecatoncheir', - 'region': 1, - 'userType': 1, - }, - '27': { - 'id': 27, - 'internalName': 'Moomba', - 'name': 'Moomba', - 'region': 1, - 'userType': 1, - }, '28': { 'dataCenter': { 'id': 3, @@ -770,13 +687,6 @@ const data: Worlds = { 'region': 1, 'userType': 5, }, - '84': { - 'id': 84, - 'internalName': 'Syldra', - 'name': 'Syldra', - 'region': 1, - 'userType': 5, - }, '85': { 'dataCenter': { 'id': 6, @@ -931,139 +841,115 @@ const data: Worlds = { 'region': 1, 'userType': 4, }, - '100': { + '400': { 'dataCenter': { - 'id': 1, - 'name': 'Elemental', + 'id': 6, + 'name': 'Chaos', }, - 'id': 100, - 'internalName': 'dev_test', - 'name': 'dev_test', + 'id': 400, + 'internalName': 'Sagittarius', + 'name': 'Sagittarius', 'region': 1, - 'userType': 0, + 'userType': 5, }, - '101': { + '401': { 'dataCenter': { - 'id': 1, - 'name': 'Elemental', + 'id': 6, + 'name': 'Chaos', }, - 'id': 101, - 'internalName': 'zone_test', - 'name': 'zone_test', + 'id': 401, + 'internalName': 'Phantom', + 'name': 'Phantom', 'region': 1, - 'userType': 0, + 'userType': 5, }, - '102': { + '402': { 'dataCenter': { - 'id': 1, - 'name': 'Elemental', + 'id': 7, + 'name': 'Light', }, - 'id': 102, - 'internalName': 'trs_test', - 'name': 'trs_test', - 'region': 1, - 'userType': 0, - }, - '103': { - 'id': 103, - 'internalName': 'contents_test', - 'name': 'contents_test', + 'id': 402, + 'internalName': 'Alpha', + 'name': 'Alpha', 'region': 1, - 'userType': 0, + 'userType': 5, }, - '110': { + '403': { 'dataCenter': { - 'id': 1, - 'name': 'Elemental', + 'id': 7, + 'name': 'Light', }, - 'id': 110, - 'internalName': 'b-tirica', - 'name': 'b-tirica', + 'id': 403, + 'internalName': 'Raiden', + 'name': 'Raiden', 'region': 1, - 'userType': 1, - }, - '111': { - 'id': 111, - 'internalName': 'b-contents', - 'name': 'b-contents', - 'region': 1, - 'userType': 0, + 'userType': 5, }, - '112': { + '404': { 'dataCenter': { - 'id': 4, - 'name': 'Aether', + 'id': 11, + 'name': 'Dynamis', }, - 'id': 112, - 'internalName': 'b-chiriri', - 'name': 'b-chiriri', + 'id': 404, + 'internalName': 'Marilith', + 'name': 'Marilith', 'region': 1, 'userType': 3, }, - '113': { - 'id': 113, - 'internalName': 'b-contents2', - 'name': 'b-contents2', - 'region': 1, - 'userType': 0, - }, - '114': { + '405': { 'dataCenter': { - 'id': 2, - 'name': 'Gaia', + 'id': 11, + 'name': 'Dynamis', }, - 'id': 114, - 'internalName': 'b-jugularis', - 'name': 'b-jugularis', + 'id': 405, + 'internalName': 'Seraph', + 'name': 'Seraph', 'region': 1, - 'userType': 1, + 'userType': 3, }, - '115': { + '406': { 'dataCenter': { - 'id': 1, - 'name': 'Elemental', + 'id': 11, + 'name': 'Dynamis', }, - 'id': 115, - 'internalName': 'e-regia', - 'name': 'e-regia', + 'id': 406, + 'internalName': 'Halicarnassus', + 'name': 'Halicarnassus', 'region': 1, - 'userType': 1, + 'userType': 3, }, - '116': { + '407': { 'dataCenter': { - 'id': 4, - 'name': 'Aether', + 'id': 11, + 'name': 'Dynamis', }, - 'id': 116, - 'internalName': 'e-pialii', - 'name': 'e-pialii', + 'id': 407, + 'internalName': 'Maduin', + 'name': 'Maduin', 'region': 1, 'userType': 3, }, - '117': { - 'id': 117, - 'internalName': 'e-contents', - 'name': 'e-contents', - 'region': 1, - 'userType': 0, - }, - '118': { - 'id': 118, - 'internalName': 'e-contents2', - 'name': 'e-contents2', + '3000': { + 'dataCenter': { + 'id': 12, + 'name': 'NA Cloud DC (Beta)', + }, + 'id': 3000, + 'internalName': 'Cloudtest01', + 'name': 'Cloudtest01', 'region': 1, - 'userType': 0, + 'userType': 9, }, - '119': { + '3001': { 'dataCenter': { - 'id': 1, - 'name': 'Elemental', + 'id': 12, + 'name': 'NA Cloud DC (Beta)', }, - 'id': 119, - 'internalName': 'e-coloria', - 'name': 'e-coloria', + 'id': 3001, + 'internalName': 'Cloudtest02', + 'name': 'Cloudtest02', 'region': 1, - 'userType': 1, + 'userType': 9, }, } as const; diff --git a/util/coinach.ts b/util/coinach.ts deleted file mode 100644 index 6fa6a219b3..0000000000 --- a/util/coinach.ts +++ /dev/null @@ -1,271 +0,0 @@ -// Helper for automating SaintCoinach -- https://github.com/ufx/SaintCoinach - -import { exec } from 'child_process'; -import fs from 'fs'; -import path from 'path'; -import { promisify } from 'util'; - -import eslint from 'eslint'; - -const coinachExe = 'SaintCoinach.Cmd.exe'; -const defaultCoinachPaths = ['C:\\SaintCoinach\\', 'D:\\SaintCoinach\\']; - -if ( - process.env['CACTBOT_DEFAULT_COINACH_PATH'] !== undefined && - process.env['CACTBOT_DEFAULT_COINACH_PATH'].length > 0 -) - defaultCoinachPaths.push(process.env['CACTBOT_DEFAULT_COINACH_PATH']); - -const ffxivExe = path.join('game', 'ffxiv_dx11.exe'); - -const defaultFfxivPaths = [ - 'C:\\Program Files (x86)\\SquareEnix\\FINAL FANTASY XIV - A Realm Reborn', - 'D:\\Program Files (x86)\\SquareEnix\\FINAL FANTASY XIV - A Realm Reborn', -]; - -if ( - process.env['CACTBOT_DEFAULT_FFXIV_PATH'] !== undefined && - process.env['CACTBOT_DEFAULT_FFXIV_PATH'].length > 0 -) - defaultFfxivPaths.push(process.env['CACTBOT_DEFAULT_FFXIV_PATH']); - -const isObject = (obj: unknown): obj is { [key: string]: unknown } => - obj !== null && typeof obj === 'object' && !Array.isArray(obj); - -class CoinachError extends Error { - constructor(message?: string, cmd?: string, output?: string) { - const errorMessage = `message: ${message ?? ''} -cmd: ${cmd ?? ''} -output: ${output ?? ''} -`; - super(errorMessage); - } -} - -export class CoinachReader { - coinachPath: string; - ffxivPath: string; - verbose: boolean; - constructor(coinachPath: string | null, ffxivPath: string | null, verbose = false) { - this.verbose = verbose; - - if (coinachPath === null) { - for (const p of defaultCoinachPaths) { - if (fs.existsSync(path.join(p, coinachExe))) { - coinachPath = p; - break; - } - } - } - - if (ffxivPath === null) { - for (const p of defaultFfxivPaths) { - if (fs.existsSync(path.join(p, ffxivExe))) { - ffxivPath = p; - break; - } - } - } - - if (coinachPath === null) - throw new CoinachError('coinach path not found'); - if (ffxivPath === null) - throw new CoinachError('ffxiv path not found'); - this.coinachPath = coinachPath; - this.ffxivPath = ffxivPath; - - if (!fs.existsSync(path.join(this.coinachPath, coinachExe))) - throw new CoinachError(`invalid coinach path: ${this.coinachPath}`); - if (!fs.existsSync(path.join(this.ffxivPath, ffxivExe))) - throw new CoinachError(`invalid ffxiv path: ${this.ffxivPath}`); - } - - async exd(table: string, lang: string): Promise { - return await this._coinachCmd('exd', table, lang); - } - - async rawexd(table: string, lang: string): Promise { - return await this._coinachCmd('rawexd', table, lang); - } - - async _coinachCmd(coinachCmd: string, table: string, lang: string): Promise { - const cmdList = [path.join(this.coinachPath, coinachExe)]; - const args = [this.ffxivPath, `lang ${lang}`, `${coinachCmd} ${table}`]; - - const cmd = [...cmdList, ...args].map((x) => `"${x}"`).join(' '); - - if (this.verbose) { - console.log(`coinach_path: ${this.coinachPath}`); - console.log(`ffxiv_path: ${this.ffxivPath}`); - console.log(`cmd: ${cmd}`); - } - // # This will throw an exception if stuff is VERY wrong - // # however, return code is still 0 even if all exports fail. - // # Also, it seems to need to be run from the SaintCoinach directory. - const output = String(promisify(exec)(cmd)); - // # Manually check output for errors. - let m = /^([0-9])* files exported, ([0-9])* failed/m.exec(output); - if (!m) - throw new CoinachError('Unknown output', cmd, output); - if (m[1] === '0') - throw new CoinachError('Zero successes', cmd, output); - if (m[2] !== '0') - throw new CoinachError('Non-zero failures', cmd, output); - - // # Find directory that this export was written to. - // # There's no way to control this. - m = /^Definition version: ([0-9.]*)/m.exec(output); - if (!m) - throw new CoinachError('Unknown output', cmd, output); - - const csvFilename = path.join( - this.coinachPath, - m[1] ?? '', - coinachCmd, - `${table}.csv`, - ); - - if (this.verbose) { - console.log(`output: \noutput`); - console.log(`output csv: ${csvFilename}`); - } - // # Read the whole file immediately, - // # as future commands with different langs will overwrite. - const lines = String( - await promisify(fs.readFile)(csvFilename, { - flag: 'r', - encoding: 'utf-8', - }), - ).split(/\r?\n/); - - if (this.verbose) - console.log(`csv lines: ${lines.length}`); - - return lines; - } -} - -export class CoinachWriter { - cactbotPath: string; - verbose: boolean; - - constructor(cactbotPath: string | null, verbose: boolean) { - this.verbose = verbose; - - if (cactbotPath === null) - cactbotPath = this._findCactbotPath(); - this.cactbotPath = cactbotPath; - if (!fs.existsSync(this.cactbotPath)) - throw new Error(`Invalid cactboth path: ${this.cactbotPath}`); - } - - _findCactbotPath(): string { - return '.'; - } - - sortObjByKeys(obj: unknown): unknown { - if (!isObject(obj) || Array.isArray(obj)) - return obj; - - const out = Object - .keys(obj) - .sort() - .reduce((acc: typeof obj, key) => { - const nested = obj[key]; - if (isObject(nested)) - acc[key] = this.sortObjByKeys(nested); - else - acc[key] = nested; - - return acc; - }, {}); - return out; - } - - async write( - filename: string, - scriptname: string, - _variable: string | null, - d: unknown[], - ): Promise { - const fullPath = path.join(this.cactbotPath, filename); - - let str = JSON.stringify(this.sortObjByKeys(d), null, 2); - - // # make keys integers, remove leading zeroes. - str = str.replace(/\'0*([0-9]+)\': {/, '$1: {'); - const f = `// Auto-generated from ${scriptname} -// DO NOT EDIT THIS FILE DIRECTLY - - -export default ${str}`; - - const linter = new eslint.ESLint({ fix: true }); - const results = await linter.lintText(f, { filePath: fullPath }); - - // There's only one result from lintText, as per documentation. - const lintResult = results[0]; - if (!lintResult || lintResult.errorCount > 0 || lintResult.warningCount > 0) { - console.error('Lint ran with errors, aborting.'); - process.exit(2); - } - - // Overwrite the file, if it already exists. - const flags = 'w'; - const writer = fs.createWriteStream(fullPath, { flags: flags }); - writer.on('error', (err) => { - console.error(err); - process.exit(-1); - }); - - writer.write(lintResult.output); - - if (this.verbose) - console.log(`wrote: ${filename}`); - } - - async writeTypeScript( - filename: string, - scriptname: string, - header: string | null, - type: string | null, - asConst: boolean | null, - data: { [s: string]: unknown }, - ): Promise { - const fullPath = path.join(this.cactbotPath, filename); - - let str = JSON.stringify(this.sortObjByKeys(data), null, 2); - - // # make keys integers, remove leading zeroes. - str = str.replace(/\'0*([0-9]+)\': {/, '$1: {'); - const f = `// Auto-generated from ${scriptname} -// DO NOT EDIT THIS FILE DIRECTLY -${header !== null ? `\n${header}` : ''} -const data${type !== null ? `:${type}` : ' '} = ${str}${asConst ? ' as const' : ''}; - -export default data;`; - - const linter = new eslint.ESLint({ fix: true }); - const results = await linter.lintText(f, { filePath: fullPath }); - - // There's only one result from lintText, as per documentation. - const lintResult = results[0]; - if (!lintResult || lintResult.errorCount > 0 || lintResult.warningCount > 0) { - console.error('Lint ran with errors, aborting.'); - process.exit(2); - } - - // Overwrite the file, if it already exists. - const flags = 'w'; - const writer = fs.createWriteStream(fullPath, { flags: flags }); - writer.on('error', (err) => { - console.error(err); - process.exit(-1); - }); - - writer.write(lintResult.output); - - if (this.verbose) - console.log(`wrote: ${filename}`); - } -} diff --git a/util/gen_effect_id.ts b/util/gen_effect_id.ts index 15cb56e1d9..b863df2e1f 100644 --- a/util/gen_effect_id.ts +++ b/util/gen_effect_id.ts @@ -1,11 +1,38 @@ import path from 'path'; -import { CoinachWriter } from './coinach'; -import { cleanName, getIntlTable, Table } from './csv_util'; +import { cleanName } from './csv_util'; +import { OutputFileAttributes, XivApi } from './xivapi'; + +const _EFFECT_ID: OutputFileAttributes = { + // Maybe this should be called Status like the table, but everything else + // says gain/lose effects. + outputFile: 'resources/effect_id.ts', + type: '', + header: '', + asConst: true, +}; + +const _ENDPOINT = 'Status'; + +const _COLUMNS = [ + 'ID', + 'Name', +]; + +type ResultStatus = { + ID: number; + Name: string | null; +}; + +type XivApiStatus = ResultStatus[]; -// Maybe this should be called Status like the table, but everything else -// says gain/lose effects. -const effectsOutputFile = 'effect_id.ts'; +type MappingTable = { + [name: string]: number; +}; + +type OutputEffectId = { + [name: string]: string; // the id is converted to hex, so use string +}; // TODO: add renaming? // Almagest: 563 @@ -14,89 +41,93 @@ const effectsOutputFile = 'effect_id.ts'; // to differentiate other than manually. There's also older effects that // did different things that are still around. // -// This is a map of id to skill name (for smoke testing/documentation). -const knownMapping = { - 'Thundercloud': '164', - 'Battle Litany': '786', - 'Right Eye': '1910', - 'Left Eye': '1454', - 'Meditative Brotherhood': '1182', - 'Brotherhood': '1185', - 'Embolden': '1297', - 'Technical Finish': '1822', - 'Sheltron': '1856', - 'Lord of Crowns': '1876', - 'Lady of Crowns': '1877', - 'Divination': '1878', - 'Further Ruin': '2701', - 'The Balance': '1882', - 'The Bole': '1883', - 'The Arrow': '1884', - 'The Spear': '1885', - 'The Ewer': '1886', - 'The Spire': '1887', - 'Sword Oath': '1902', - 'Tactician': '1951', +// This is a map of skill name ro effect id (for smoke testing/documentation). +// +const knownMapping: Readonly = { + 'Thundercloud': 164, + 'Battle Litany': 786, + 'Right Eye': 1910, + 'Left Eye': 1454, + 'Meditative Brotherhood': 1182, + 'Brotherhood': 1185, + 'Embolden': 1297, + 'Technical Finish': 1822, + 'Sheltron': 1856, + 'Lord of Crowns': 1876, + 'Lady of Crowns': 1877, + 'Divination': 1878, + 'Further Ruin': 2701, + 'The Balance': 1882, + 'The Bole': 1883, + 'The Arrow': 1884, + 'The Spear': 1885, + 'The Ewer': 1886, + 'The Spire': 1887, + 'Sword Oath': 1902, + 'Tactician': 1951, // This is for others, 1821 is for self. - 'Standard Finish': '2105', - 'The Wanderer\'s Minuet': '2216', - 'Mage\'s Ballad': '2217', - 'Army\'s Paeon': '2218', - 'Stormbite': '1201', - 'Caustic Bite': '1200', - 'Windbite': '129', - 'Venomous Bite': '124', - 'Higanbana': '1228', - 'Wildfire': '861', - 'Chain Stratagem': '1221', - 'Vulnerability Up': '638', - 'Eukrasian Dosis III': '2616', - 'Radiant Finale': '2964', - 'Requiescat': '1368', - 'Overheated': '2688', -} as const; + 'Standard Finish': 2105, + 'The Wanderer\'s Minuet': 2216, + 'Mage\'s Ballad': 2217, + 'Army\'s Paeon': 2218, + 'Stormbite': 1201, + 'Caustic Bite': 1200, + 'Windbite': 129, + 'Venomous Bite': 124, + 'Higanbana': 1228, + 'Wildfire': 861, + 'Chain Stratagem': 1221, + 'Vulnerability Up': 638, + 'Eukrasian Dosis III': 2616, + 'Radiant Finale': 2964, + 'Requiescat': 1368, + 'Overheated': 2688, +}; // These custom name of effect will not be checked, but you'd better make it clean. // Use this only when you need to handle different effects with a same name. -const customMapping = { - 'EmboldenSelf': '1239', -} as const; +const customMapping: Readonly = { + 'EmboldenSelf': 1239, +}; const printError = ( header: string, what: string, - map: Record, - key: string, -) => console.error(`${header} ${what}: ${JSON.stringify(map[key])}`); +) => console.error(`${header} ${what}`); -const makeEffectMap = (table: Table<'#', 'Name'>) => { +const assembleData = (apiData: XivApiStatus): OutputEffectId => { + const formattedData: OutputEffectId = {}; const foundNames = new Set(); + const map = new Map(); - const map = new Map(); - for (const [id, effect] of Object.entries(table)) { - const rawName = effect['Name']; - if (rawName === undefined) + for (const effect of apiData) { + const id = effect.ID; + const rawName = effect.Name; + if (rawName === null || id === null) continue; const name = cleanName(rawName); // Skip empty strings. if (!name) continue; + // TODO: The below printError() calls generate a ton of noise. That's to be expected, + // but we might want to add a flag to suppress these entirely, or filter out + // existing/known conflicts so we can just see what's changing each patch. if (rawName in knownMapping) { - if (id !== knownMapping[rawName as keyof typeof knownMapping]) { - printError('skipping', rawName, table, id); + if (id !== knownMapping[rawName]) { + printError('skipping', rawName); continue; } } if (map.has(name)) { - printError('collision', name, table, id); - printError('collision', name, table, map.get(name) ?? ''); + printError('collision', name); + printError('collision', name); map.delete(name); continue; } if (foundNames.has(name)) { - printError('collision', name, table, id); + printError('collision', name); continue; } @@ -108,7 +139,7 @@ const makeEffectMap = (table: Table<'#', 'Name'>) => { for (const rawName of Object.keys(knownMapping)) { const name = cleanName(rawName); if (name && !foundNames.has(name)) - printError('missing', name, knownMapping, rawName); + printError('missing known name', rawName); } // Add custom effect name for necessary duplicates. @@ -116,21 +147,24 @@ const makeEffectMap = (table: Table<'#', 'Name'>) => { map.set(name, id); // Store ids as hex. - map.forEach((id, name) => map.set(name, parseInt(id).toString(16).toUpperCase())); + map.forEach((id, name) => formattedData[name] = id.toString(16).toUpperCase()); - return Object.fromEntries(map); + return formattedData; }; export default async (): Promise => { - const table = await getIntlTable('Status', ['#', 'Name', 'Icon', 'PartyListPriority']); - - const writer = new CoinachWriter(null, true); - void writer.writeTypeScript( - path.join('resources', effectsOutputFile), - 'gen_effect_id.ts', - null, - null, - true, - makeEffectMap(table), + const api = new XivApi(null, true); + + const apiData = await api.queryApi( + _ENDPOINT, + _COLUMNS, + ) as XivApiStatus; + + const outputData = assembleData(apiData); + + await api.writeFile( + path.basename(import.meta.url), + _EFFECT_ID, + outputData, ); }; diff --git a/util/gen_hunt_data.ts b/util/gen_hunt_data.ts index e804bdbc64..594c59e5e7 100644 --- a/util/gen_hunt_data.ts +++ b/util/gen_hunt_data.ts @@ -61,10 +61,10 @@ type Rank = 'S' | 'SS+' | 'SS-' | 'A' | 'B'; type ResultMonsterBNpcName = { ID: string | number; - Name_de: string; - Name_en: string; - Name_fr: string; - Name_ja: string; + Name_de: string | null; + Name_en: string | null; + Name_fr: string | null; + Name_ja: string | null; }; type ResultMonsterBNpcBase = { @@ -73,14 +73,12 @@ type ResultMonsterBNpcBase = { type ResultMonster = { ID: string | number; - Rank: string | number; + Rank: string | number | null; BNpcBase: ResultMonsterBNpcBase; BNpcName: ResultMonsterBNpcName; }; -type XivApiNotoriousMonster = { - [key: number]: ResultMonster; -}; +type XivApiNotoriousMonster = ResultMonster[]; type OutputHuntMap = { [name: string]: { @@ -129,7 +127,7 @@ const assembleData = async (apiData: XivApiNotoriousMonster): Promise; - }; +const _ENDPOINT = 'Pet'; + +const _COLUMNS = [ + 'ID', + 'Name_de', + 'Name_en', + 'Name_fr', + 'Name_ja', +]; + +const _LOCALE_TABLE = 'Pet'; + +const _LOCALE_COLUMNS = ['Name']; + +type ResultPet = { + Name_de: string | null; + Name_en: string | null; + Name_fr: string | null; + Name_ja: string | null; }; -const fetchXivapi = async () => { - const url = `https://xivapi.com/Pet?columns=${localeKeys.join(',')}`; - const response = await fetch(url); - const json = (await response.json()) as XivapiResult; - return json.Results; +type XivApiPet = ResultPet[]; + +type OutputPetNames = { + cn: string[]; + de: string[]; + en: string[]; + fr: string[]; + ja: string[]; + ko: string[]; }; -const normalize = ( - content: XivapiResult['Results'], -): Record<'en' | 'de' | 'fr' | 'ja', string[]> => { - const result: Record<'en' | 'de' | 'fr' | 'ja', string[]> = { - // cactbot-ignore-missing-translations - en: [], +const fetchLocaleCsvTables = async () => { + const cnPet = await getCnTable(_LOCALE_TABLE, _LOCALE_COLUMNS); + const koPet = await getKoTable(_LOCALE_TABLE, _LOCALE_COLUMNS); + return { + cn: cnPet, + ko: koPet, + }; +}; + +const assembleData = async (apiData: XivApiPet): Promise => { + // This isn't really a locale object, and ordering is alpha in the current file, so: + // eslint-disable-next-line rulesdir/cactbot-locale-order + const formattedData: OutputPetNames = { + cn: [], de: [], + en: [], fr: [], ja: [], + ko: [], }; - for (const [, data] of Object.entries(content)) { - // skip empty names and duplicates - if (data.Name === '' || result.en.includes(data.Name)) + + for (const pet of apiData) { + // If no en name (or if duplicate), skip processing + if (pet.Name_en === null || pet.Name_en === '' || formattedData.en.includes(pet.Name_en)) continue; + formattedData.en.push(pet.Name_en); - result.en.push(data.Name); - result.de.push(data.Name_de); - result.fr.push(data.Name_fr); - result.ja.push(data.Name_ja); + if (pet.Name_de !== null) + formattedData.de.push(pet.Name_de); + if (pet.Name_fr !== null) + formattedData.fr.push(pet.Name_fr); + if (pet.Name_ja !== null) + formattedData.ja.push(pet.Name_ja); + } + + const localeCsvTables = await fetchLocaleCsvTables(); + for (const name of Object.keys(localeCsvTables.cn).filter((k) => k !== '')) { + formattedData.cn.push(name); + } + for (const name of Object.keys(localeCsvTables.ko).filter((k) => k !== '')) { + formattedData.ko.push(name); } - return result; + + return formattedData; }; export default async (): Promise => { - const tables = { - ...normalize(await fetchXivapi()), - cn: Object.keys(await getCnTable('Pet', keys)).filter((k) => k !== ''), - ko: Object.keys(await getKoTable('Pet', keys)).filter((k) => k !== ''), - }; + const api = new XivApi(null, true); - const writer = new CoinachWriter(null, true); - const header = `import { Lang } from './languages'; + const apiData = await api.queryApi( + _ENDPOINT, + _COLUMNS, + ) as XivApiPet; -type PetData = { - [name in Lang]: readonly string[]; -}; -`; - await writer.writeTypeScript( - path.join('resources', _OUTPUT_FILE), - 'gen_pet_names.ts', - header, - 'PetData', - false, - tables, + const outputData = await assembleData(apiData); + + await api.writeFile( + path.basename(import.meta.url), + _PET_NAMES, + outputData, ); }; diff --git a/util/gen_weather_rate.ts b/util/gen_weather_rate.ts index e6b561ebaa..921abb29a5 100644 --- a/util/gen_weather_rate.ts +++ b/util/gen_weather_rate.ts @@ -69,12 +69,10 @@ type ResultWeatherRate = [K in WeatherField]: ResultWeatherName; } & { - [K in RateField]: number; + [K in RateField]: string | number | null; }; -type XivApiWeatherRate = { - [key: number]: ResultWeatherRate; -}; +type XivApiWeatherRate = ResultWeatherRate[]; type OutputWeatherRate = { [id: number]: { @@ -86,7 +84,7 @@ type OutputWeatherRate = { const assembleData = (apiData: XivApiWeatherRate): OutputWeatherRate => { const formattedData: OutputWeatherRate = {}; - for (const [, record] of Object.entries(apiData)) { + for (const record of apiData) { const id = typeof record.ID !== 'number' ? parseInt(record.ID) : record.ID; const rates: number[] = []; const weathers: string[] = []; @@ -96,7 +94,12 @@ const assembleData = (apiData: XivApiWeatherRate): OutputWeatherRate => { const rateField = `Rate${v}` as RateField; const weatherField = `Weather${v}` as WeatherField; - sumRate += record[rateField]; + let rate = record[rateField]; + if (rate !== null) { + rate = typeof rate === 'number' ? rate : parseInt(rate); + sumRate += rate; + } + const weatherName = record[weatherField].Name; // stop processing for this ID on the first empty/null weather string diff --git a/util/gen_world_ids.ts b/util/gen_world_ids.ts index 87807bdcfe..5d84f6935a 100644 --- a/util/gen_world_ids.ts +++ b/util/gen_world_ids.ts @@ -1,121 +1,163 @@ import path from 'path'; -import fetch from 'node-fetch'; +import { OutputFileAttributes, XivApi } from './xivapi'; + +const _WORLD_ID: OutputFileAttributes = { + outputFile: 'resources/world_id.ts', + type: 'Worlds', + header: `// NOTE: This data is filtered to public worlds only (i.e. isPublic: true) + + export type DataCenter = { + id: number; + name: string; + }; -import { CoinachWriter } from './coinach'; +export type World = { + id: number; + internalName: string; + name: string; + region: number; + userType: number; + dataCenter?: DataCenter; + isPublic?: boolean; +}; -const _OUTPUT_FILE = 'world_id.ts'; +export type Worlds = { + [id: string]: World +}; -const worldFieldMap = { - 'ID': 'id', - 'InternalName': 'internalName', - 'Name': 'name', - 'Region': 'region', - 'UserType': 'userType', - 'DataCenter': 'dataCenter', - 'IsPublic': 'isPublic', -} as const; +export const worldNameToWorld = (name: string): World | undefined => { + return Object.values(data).find((world: World) => { + if (world.name === name) { + return true; + } + }); +}; +`, + asConst: true, +}; + +const _ENDPOINT = 'World'; -const dataCenterFieldMap = { - 'ID': 'id', - 'Name': 'name', -} as const; +const _COLUMNS = [ + 'ID', + 'InternalName', + 'Name', + 'Region', + 'UserType', + 'DataCenter.ID', + 'DataCenter.Name', + 'IsPublic', +]; type ResultDataCenter = { - ID: number; - Name: string; + ID: string | number | null; + Name: string | null; }; type ResultWorld = { ID: number; - InternalName: string; - Name: string; - Region: number; - UserType: number; - DataCenter: null | ResultDataCenter; + InternalName: string | null; + Name: string | null; + Region: number | null; + UserType: number | null; + DataCenter: ResultDataCenter; + IsPublic: string | number | null; }; -type XivapiResult = { - Results: { - [key: number]: ResultWorld; - }; +type XivApiWorld = ResultWorld[]; + +type OutputDataCenter = { + id: number; + name: string; +}; + +type OutputWorld = { + id: number; + internalName: string; + name: string; + region: number; + userType: number; + dataCenter?: OutputDataCenter; + isPublic?: boolean; }; -type DataCenter = { - [key in keyof ResultDataCenter as (typeof dataCenterFieldMap)[key]]: ResultDataCenter[key]; +type OutputWorldIds = { + [id: string]: OutputWorld; }; -type World = { - [key in keyof ResultWorld as (typeof worldFieldMap)[key]]: key extends 'DataCenter' - ? (DataCenter | undefined) - : ResultWorld[key]; +const scrubDataCenter = (dc: ResultDataCenter): OutputDataCenter | undefined => { + if (dc.ID === null || dc.ID === '') + return; + if (dc.Name === null || dc.Name === '') + return; + const idNum = typeof dc.ID === 'string' ? parseInt(dc.ID) : dc.ID; + return { + id: idNum, + name: dc.Name, + }; }; -const fetchXivapi = async () => { - const url = `https://xivapi.com/World?columns=${Object.keys(worldFieldMap).join(',')}`; - const response = await fetch(url); - const json = (await response.json()) as XivapiResult; - return json.Results; +const scrubIsPublic = (pub: string | number | null): boolean | undefined => { + if (pub === null || pub === '') + return; + const pubNum = typeof pub === 'string' ? parseInt(pub) : pub; + if (pubNum === 0) + return false; + if (pubNum === 1) + return true; + return; }; -const remapResults = ( - content: XivapiResult['Results'], -): Record => { - const result: Record = {}; - for (const [, data] of Object.entries(content)) { - const dc = data.DataCenter === null ? undefined : { - id: data.DataCenter.ID, - name: data.DataCenter.Name, - }; - result[data.ID] = { +const assembleData = (apiData: XivApiWorld): OutputWorldIds => { + const formattedData: OutputWorldIds = {}; + + for (const data of apiData) { + const dc = scrubDataCenter(data.DataCenter); + const isPublic = scrubIsPublic(data.IsPublic); + + // there are many hundreds of dev/test/whatever entries in + // the World table that substantially clutter the data + // for our use cases, we only care about public worlds + if (!isPublic) + continue; + + if ( + data.InternalName === null || + data.Name === null || + data.Name === '' || // filter out empty strings or we get a ton of trash + data.Region === null || + data.UserType === null + ) + continue; + + formattedData[data.ID.toString()] = { id: data.ID, internalName: data.InternalName, name: data.Name, region: data.Region, userType: data.UserType, dataCenter: dc, + // isPublic: isPublic, // value is always implicitly 'true' given the filter above }; } - return result; + return formattedData; }; export default async (): Promise => { - const table = remapResults(await fetchXivapi()); + const api = new XivApi(null, true); - const writer = new CoinachWriter(null, true); - const header = `export type DataCenter = { - id: number; - name: string; - }; + const apiData = await api.queryApi( + _ENDPOINT, + _COLUMNS, + ) as XivApiWorld; -export type World = { - id: number; - internalName: string; - name: string; - region: number; - userType: number; - dataCenter?: DataCenter; - isPublic?: boolean; -}; - -export type Worlds = { - [id: string]: World -}; + const outputData = assembleData(apiData); -export const worldNameToWorld = (name: string): World | undefined => { - return Object.values(data).find((world: World) => { - if (world.name === name) { - return true; - } - }); -}; -`; - await writer.writeTypeScript( - path.join('resources', _OUTPUT_FILE), - 'world_id.ts', - header, - 'Worlds', - true, - table, + await api.writeFile( + path.basename(import.meta.url), + _WORLD_ID, + outputData, + true, // require keys to be returned as strings (because that's the existing format) ); }; diff --git a/util/gen_zone_id_and_info.ts b/util/gen_zone_id_and_info.ts index 18261f6666..4fe58e6fb8 100644 --- a/util/gen_zone_id_and_info.ts +++ b/util/gen_zone_id_and_info.ts @@ -154,6 +154,10 @@ type ResultContentType = { Name: string | null; }; +// XivApiTerritoryType & XivAPIContentFinderCondition are indexed objects +// based on the 'ID' field of their respective XIVAPI data sets. +// The api library returns these as arrays; this script uses index functions +// to restructure the input so it's easier to work with. type XivApiTerritoryType = { [key: number]: ResultTerritoryType; }; @@ -162,9 +166,7 @@ type XivApiContentFinderCondition = { [key: number]: ResultContentFinderCondition; }; -type XivApiContentType = { - [key: number]: ResultContentType; -}; +type XivApiContentType = ResultContentType[]; type ZoneIDOutput = { [zone: string]: number | null; @@ -208,30 +210,22 @@ const printError = ( console.error(`${msg} ${zoneName}: ${JSON.stringify(ttIdToData[ttId])}`); }; -const reindexTtData = (data: XivApiTerritoryType): XivApiTerritoryType => { +const indexTtData = (data: ResultTerritoryType[]): XivApiTerritoryType => { const ttData: XivApiTerritoryType = {}; - for (const [, row] of Object.entries(data)) { + for (const row of data) { ttData[row.ID] = row; } return ttData; }; -const reindexCfcData = (data: XivApiContentFinderCondition): XivApiContentFinderCondition => { +const indexCfcData = (data: ResultContentFinderCondition[]): XivApiContentFinderCondition => { const cfcData: XivApiContentFinderCondition = {}; - for (const [, row] of Object.entries(data)) { + for (const row of data) { cfcData[row.ID] = row; } return cfcData; }; -const reindexCtData = (data: XivApiContentType): XivApiContentType => { - const ctData: XivApiContentType = {}; - for (const [, row] of Object.entries(data)) { - ctData[row.ID] = row; - } - return ctData; -}; - const fetchLocaleCsvTables = async () => { const cnPlaceName = await getCnTable('PlaceName', ['#', 'Name'], ['placeId', 'placeName']); const koPlaceName = await getKoTable('PlaceName', ['#', 'Name'], ['placeId', 'placeName']); @@ -647,7 +641,7 @@ const generateContentTypeMap = ( ): ContentTypeOutput => { const contentTypeMap: ContentTypeOutput = {}; - for (const [, ct] of Object.entries(ctData)) { + for (const ct of ctData) { if (ct.ID === null || ct.Name === null || ct.Name === '') continue; contentTypeMap[cleanName(ct.Name)] = ct.ID; @@ -662,16 +656,12 @@ const generateContentTypeMap = ( }; const assembleData = async ( - ttRawData: XivApiTerritoryType, - cfcRawData: XivApiContentFinderCondition, - ctRawData: XivApiContentType, + ttRawData: ResultTerritoryType[], + cfcRawData: ResultContentFinderCondition[], + ctData: XivApiContentType, ): Promise => { - // re-index api data based on data keys, not xivapi/json indices - // we don't need new types, since the data will still be id-indexed - // separate functions, though, to maintain typing. - const ttData = reindexTtData(ttRawData); - const cfcData = reindexCfcData(cfcRawData); - const ctData = reindexCtData(ctRawData); + const ttData = indexTtData(ttRawData); + const cfcData = indexCfcData(cfcRawData); const zoneIdData = generateZoneIdMap(ttData, cfcData); @@ -689,12 +679,12 @@ const api = new XivApi(null, true); const ttRawData = await api.queryApi( _TT_ENDPOINT, _TT_COLUMNS, -) as XivApiTerritoryType; +) as ResultTerritoryType[]; const cfcRawData = await api.queryApi( _CFC_ENDPOINT, _CFC_COLUMNS, -) as XivApiContentFinderCondition; +) as ResultContentFinderCondition[]; const ctRawData = await api.queryApi( _CT_ENDPOINT, diff --git a/util/xivapi.ts b/util/xivapi.ts index 6ce40eaac5..bb68d0a16c 100644 --- a/util/xivapi.ts +++ b/util/xivapi.ts @@ -17,13 +17,22 @@ const _XIVAPI_RESULTS_LIMIT = 3000; // We're using some generic typing because the data format // will depend on the endpoint used by each script. + +type XivApiRecord = { + [column: string]: unknown; +}; + +type XivApiOutput = XivApiRecord[]; + type XivApiResultData = { - [key: number]: { - [column: string]: unknown; - }; + [key: number]: XivApiRecord; }; type XivApiResult = { + Pagination: { + Page: string | number; + PageTotal: string | number; + }; Results: XivApiResultData; }; @@ -67,18 +76,33 @@ export class XivApi { if (columns.length === 0) exitError(`Cannot query API endpoint ${endpoint}: No columns specified.`); - const url = `${_XIVAPI_URL}${endpoint}?limit=${_XIVAPI_RESULTS_LIMIT}&columns=${ - columns.join(',') - }`; - - // TODO: Add some error detection & handling - const response = await fetch(url); - const json = (await response.json()) as XivApiResult; + let currentPage = 0; + let maxPage = 1; + const output: XivApiOutput = []; + while (currentPage < maxPage) { + currentPage++; + let url = `${_XIVAPI_URL}${endpoint}?limit=${_XIVAPI_RESULTS_LIMIT}&columns=${ + columns.join(',') + }`; + + if (currentPage !== 1) + url += `&page=${currentPage}`; + // TODO: add better error handling? + const response = await fetch(url); + const jsonResult = (await response.json()) as XivApiResult; + + if (currentPage === 1) + maxPage = typeof jsonResult.Pagination.PageTotal === 'string' + ? parseInt(jsonResult.Pagination.PageTotal) + : jsonResult.Pagination.PageTotal; + + output.push(...Object.values(jsonResult.Results)); + } if (this.verbose) console.log(`Xivapi query successful for endpoint: ${endpoint}`); - return json.Results; + return output; } sortObjByKeys(obj: unknown): unknown { @@ -104,13 +128,15 @@ export class XivApi { scriptName: string, file: OutputFileAttributes, data: { [s: string]: unknown }, + keysAsStrings?: boolean, ): Promise { const fullPath = path.join(this.cactbotPath, file.outputFile); let str = JSON.stringify(this.sortObjByKeys(data), null, 2); - // # make keys integers, remove leading zeroes. - str = str.replace(/['"]0*([0-9]+)['"]: {/g, '$1: {'); + // make keys integers, remove leading zeroes. + if (keysAsStrings === undefined || !keysAsStrings) + str = str.replace(/['"]0*([0-9]+)['"]: {/g, '$1: {'); const fileOutput = `// Auto-generated from ${scriptName} // DO NOT EDIT THIS FILE DIRECTLY