Scripts for analyzing Earth 2150 data files
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

154 lines
9.3KB

  1. from enum import Enum
  2. import sys
  3. filename = 'C:/Games/Earth 2150/WDFiles/Parameters/EARTH2150.par'
  4. def read_int(stream):
  5. return int.from_bytes(stream.read(4), byteorder='little')
  6. def read_string(stream):
  7. length = read_int(stream)
  8. namedata = stream.read(length)
  9. try:
  10. return namedata.decode()
  11. except:
  12. print(length, namedata)
  13. raise
  14. def read_field(stream, is_string):
  15. if is_string:
  16. return read_string(stream)
  17. else:
  18. return read_int(stream)
  19. class EntityType(Enum):
  20. Vehicle = 1
  21. Cannon = 2
  22. Missile = 3
  23. Building = 4
  24. Special = 5
  25. Equipment = 6
  26. ShieldGenerator = 7
  27. SoundPack = 8
  28. SpecialUpdatesLinks = 9
  29. Parameters = 10
  30. class EntityClass(Enum):
  31. # Vehicle
  32. MOVEABLE = 0x00c00101
  33. SUPPLYTRANSPORTER = 0x01c00101
  34. BUILDROBOT = 0x02c00101
  35. MININGROBOT = 0x04c00101
  36. SAPPERROBOT = 0x08c00101
  37. # Cannon
  38. CANNON = 0x00000102
  39. # Missile
  40. MISSILE = 0x00010401
  41. # Building
  42. BUILDING = 0x00010101
  43. # Equipment
  44. REPAIRER = 0x00000202
  45. CONTAINERTRANSPORTER = 0x00000402
  46. LOOKROUNDEQUIPMENT = 0x00000802
  47. EQUIPMENT = 0x00000002
  48. def __repr__(self):
  49. return self.name
  50. type_field_map = {
  51. EntityType.Vehicle: ['classID', 'mesh', 'shadowType', 'viewParamsIndex', 'cost', 'timeOfBuild', '$soundPackID', '$smokeID', '$killExplosionID', '$destructedID', 'hp', 'regenerationHP', 'armour', 'calorificCapacity', 'disableResist', 'storeableFlags', 'standType', 'sightRange', '$talkPackID', '$shieldGeneratorID', 'maxShieldUpdate', 'slot1Type', 'slot2Type', 'slot3Type', 'slot4Type', 'soilSpeed', 'roadSpeed', 'sandSpeed', 'bankSpeed', 'waterSpeed', 'deepWaterSpeed', 'airSpeed', 'objectType', '$engineSmokeID', '$dustID', '$billowID', '$standBillowID', '$trackID'],
  52. EntityType.Cannon: ['classID', 'mesh', 'shadowType', 'viewParamsIndex', 'cost', 'timeOfBuild', '$soundPackID', '$smokeID', '$killExplosionID', '$destructedID', 'rangeOfSight', 'plugType', 'slotType', 'maxAlphaPerTick', 'maxBetaPerTick', 'alphaMargin', 'betaMargin', 'barrelBetaType', 'barrelBetaAngle', 'barrelCount', '$ammoID', 'ammoType', 'targetType', 'rangeOfFire', 'plusDamage', 'fireType', 'shootDelay', 'needExternal', 'reloadDelay', 'maxAmmo', '$barrelExplosionID'],
  53. EntityType.Missile: ['classID', 'mesh', 'shadowType', 'viewParamsIndex', 'cost', 'timeOfBuild', '$soundPackID', '$smokeID', '$killExplosionID', '$destructedID', 'hp', 'regenerationHP', 'armour', 'calorificCapacity', 'disableResist', 'resistantFlags', 'standType', 'type', 'rocketType', 'missileSize', '$rocketDummyID', 'IsAntiRocketTarget', 'speed', 'timeOfShoot', 'plusRangeOfFire', 'hitType', 'hitRange', 'typeOfDamage', 'damage', '$explosionID'],
  54. EntityType.Building: ['classID', 'mesh', 'shadowType', 'viewParamsIndex', 'cost', 'timeOfBuild', '$soundPackID', '$smokeID', '$killExplosionID', '$destructedID', 'hp', 'regenerationHP', 'armour', 'calorificCapacity', 'disableResist', 'storeableFlags', 'standType', 'sightRange', '$talkPackID', '$shieldGeneratorID', 'maxShieldUpdate', 'slot1Type', 'slot2Type', 'slot3Type', 'slot4Type', 'buildingType', 'buildingTypeEx', 'buildingTabType', '$initCannonID1', '$initCannonID2', '$initCannonID3', '$initCannonID4', '$copulaID', 'buildingTunnelNumber', '$upgradeCopulaSmallID', '$upgradeCopulaBigID', '$buildLCTransporterID', '$chimneySmokeID', 'needPower', '$slaveBuildingID', 'maxSubBuildingsCount', 'powerLevel', 'powerTransmitterRange', 'connectTransmitterRange', 'fullEnergyPowerInDay', 'resourceInputOutput', 'ticPerContainer', '$containerID', 'containerSmeltingTicks', 'resourcesPerTransport', '$transporterID', '$buildingAmmoID', 'rangeOfBuildingFire', '$shootExplosionID', 'ammoReloadTime', '$buildExplosion', 'copulaAnimationFlags', 'endOfClosingCopulaAnimation', '$laserID', 'spaceStationType'],
  55. EntityType.Equipment: ['classID', 'mesh', 'shadowType', 'viewParamsIndex', 'cost', 'timeOfBuild', '$soundPackID', '$smokeID', '$killExplosionID', '$destructedID', 'rangeOfSight', 'plugType', 'slotType', 'maxAlphaPerTick', 'maxBetaPerTick'],
  56. EntityType.ShieldGenerator: ['shieldCost', 'shieldValue', 'reloadTime', 'shieldMeshName', 'shieldMeshViewIndex'],
  57. EntityType.SoundPack: ['normalWavePack1', 'normalWavePack2', 'normalWavePack3', 'normalWavePack4', 'loopedWavePack1', 'loopedWavePack2', 'loopedWavePack3', 'loopedWavePack4'],
  58. EntityType.SpecialUpdatesLinks: ['$specialUpdateLink']
  59. }
  60. class_field_map = {
  61. EntityClass.SUPPLYTRANSPORTER: ['ammoCapacity', 'animSupplyDownStart', 'animSupplyDownEnd', 'animSupplyUpStart', 'animSupplyUpEnd'],
  62. EntityClass.BUILDROBOT: ['$wallD', '$bridgeID', 'tunnelNumber', 'roadBuildTime', 'flatBuildTime', 'trenchBuildTime', 'tunnelBuildTime', 'buildObjectAnimationAngle', 'digNormalAnimationAngle', 'digLowAnimationAngle', 'animBuildObjectStartStart', 'animBuildObjectStartEnd', 'animBuildObjectWorkStart', 'animBuildObjectWorkEnd', 'animBuildObjectEndStart', 'animBuildObjectEndEnd', 'animDigNormalStartStart', 'animDigNormalStartEnd', 'animDigNormalWorkStart', 'animDigNormalWorkEnd', 'animDigNormalEndStart', 'animDigNormalEndEnd', 'animDigLowStartStart', 'animDigLowStartEnd', 'animDigLowWorkStart', 'animDigLowWorkEnd', 'animDigLowEndStart', 'animDigLowEndEnd', '$digSmokeID'],
  63. EntityClass.MININGROBOT: ['containersCnt', 'ticksPerContainer', 'putResourceAngle', 'animHarvestStartStart', 'animHarvestStartEnd', 'animHarvestWorkStart', 'animHarvestWorkEnd', 'animHarvestEndStart', 'animHarvestEndEnd', '$harvestSmokeID'],
  64. EntityClass.SAPPERROBOT: ['minesLookRange', '$mineID', 'maxMinesCount', 'animDownStart', 'animDownEnd', 'animUpStart', 'animUpEnd', '$putMineSmokeID'],
  65. EntityClass.REPAIRER: ['repairerFlags', 'repairHPPerTick', 'repairElectronicsPerTick', 'ticksPerRepair', 'convertTankTime', 'convertBuildingTime', 'convertHealthyTankTime', 'convertHealthyBuildingTime', 'repaintTankTime', 'repaintBuildingTime', 'upgradeTankTime', 'animRepairStartStart', 'animRepairStartEnd', 'animRepairWorkStart', 'animRepairWorkEnd', 'animRepairEndStart', 'animRepairEndEnd', 'animConvertStartStart', 'animConvertStartEnd', 'animConvertWorkStart', 'animConvertWorkEnd', 'animConvertEndStart', 'animConvertEndEnd', 'animRepaintStartStart', 'animRepaintStartEnd', 'animRepaintWorkStart', 'animRepaintWorkEnd', 'animRepaintEndStart', 'animRepaintEndEnd'],
  66. EntityClass.CONTAINERTRANSPORTER: ['animContainerDownStart', 'animContainerDownEnd', 'animContainerUpStart', 'animContainerUpEnd'],
  67. EntityClass.LOOKROUNDEQUIPMENT: ['lookRoundTypeMask', 'lookRoundRange', 'turnSpeed', 'bannerAddExperienceLevel', 'regenerationHPMultiple', 'shieldReloadAdd']
  68. }
  69. enum_mappings = {
  70. 'classID': EntityClass
  71. }
  72. class Entity:
  73. def __init__(self, stream):
  74. self.name = read_string(stream)
  75. req_research_count = read_int(stream)
  76. self.req_research = [read_int(stream) for _ in range(req_research_count)]
  77. field_count = read_int(stream)
  78. field_types = stream.read(field_count)
  79. self.fields = list()
  80. for (i, is_string) in enumerate(field_types):
  81. if is_string:
  82. self.fields.append(read_string(stream))
  83. else:
  84. val = read_int(stream)
  85. # Skip the -1 after every string
  86. if not (val == 0xffffffff and i > 0 and field_types[i-1]):
  87. self.fields.append(val)
  88. def __repr__(self):
  89. return f'Entity{{name={self.name!r}, req_research={self.req_research}, fields={len(self.fields)}{self.fields}}}'
  90. class EntityGroup:
  91. def __init__(self, stream):
  92. #self.faction = Faction(read_int(parfile))
  93. self.faction = read_int(parfile)
  94. self.entity_type = EntityType(read_int(parfile))
  95. entity_count = read_int(parfile)
  96. self.entities = [Entity(stream) for _ in range(entity_count)]
  97. def __repr__(self):
  98. entities = ''
  99. for entity in self.entities:
  100. entities += f' {entity}\n'
  101. return f'EntityGroup{{faction={self.faction}, entity_type={self.entity_type}, entities=\n{entities}}}'
  102. with open(filename, 'rb') as parfile:
  103. headers = parfile.read(16)
  104. entity_group_count = int.from_bytes(headers[8:12], byteorder='little')
  105. print('Entity group count:', entity_group_count)
  106. print()
  107. ents = dict()
  108. for _ in range(entity_group_count):
  109. ent = EntityGroup(parfile)
  110. #print(ent)
  111. if ent.entity_type in ents:
  112. ents[ent.entity_type].append(ent)
  113. else:
  114. ents[ent.entity_type] = [ent]
  115. for etype in [EntityType.Vehicle, EntityType.Cannon, EntityType.Missile, EntityType.Building, EntityType.Equipment]:
  116. print(etype.name)
  117. field_names = type_field_map.get(etype, [])
  118. for ent in ents.get(etype, []):
  119. for e in ent.entities:
  120. class_id = EntityClass(e.fields[0])
  121. class_field_names = field_names + class_field_map.get(class_id, [])
  122. mapped_fields = dict(zip(class_field_names, e.fields))
  123. mapped_fields['classID'] = class_id
  124. unmapped_fields = e.fields[len(class_field_names):]
  125. if unmapped_fields:
  126. raise ValueError(f'{e.name} {ent.entity_type} {mapped_fields=} {unmapped_fields=}')
  127. print(f'{e.name} {mapped_fields}')
  128. print()
  129. print()