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.

298 lines
19KB

  1. from enum import Enum, unique
  2. from io import StringIO
  3. from itertools import groupby
  4. import csv
  5. import os
  6. import os.path
  7. import sys
  8. if len(sys.argv) > 1:
  9. filename = sys.argv[1]
  10. else:
  11. filename = 'C:/Games/Earth 2150 - The Moon Project/WDFiles/Parameters/EARTH2150.par'
  12. def read_int(stream):
  13. return int.from_bytes(stream.read(4), byteorder='little')
  14. def read_string(stream):
  15. length = read_int(stream)
  16. namedata = stream.read(length)
  17. try:
  18. return namedata.decode()
  19. except:
  20. print(length, namedata)
  21. raise
  22. def read_field(stream, is_string):
  23. if is_string:
  24. return read_string(stream)
  25. else:
  26. return read_int(stream)
  27. class Faction(Enum):
  28. NEUTRAL = 0
  29. UCS = 1
  30. ED = 2
  31. LC = 3
  32. class EntityType(Enum):
  33. Vehicle = 1
  34. Cannon = 2
  35. Missile = 3
  36. Building = 4
  37. Special = 5
  38. Equipment = 6
  39. ShieldGenerator = 7
  40. SoundPack = 8
  41. SpecialUpdatesLinks = 9
  42. Parameters = 10
  43. class ResearchTab(Enum):
  44. CHASSIS = 0
  45. WEAPON = 1
  46. AMMO = 2
  47. SPECIAL = 3
  48. @unique
  49. class EntityClass(Enum):
  50. # Vehicle
  51. VEHICLE = 0x00c00101
  52. #MOVEABLE = 0x00c00101
  53. SUPPLYTRANSPORTER = 0x01c00101
  54. BUILDROBOT = 0x02c00101
  55. MININGROBOT = 0x04c00101
  56. SAPPERROBOT = 0x08c00101
  57. # Cannon
  58. CANNON = 0x00000102
  59. # Missile
  60. MISSILE = 0x00010401
  61. # Building
  62. BUILDING = 0x00010101
  63. # Special
  64. PASSIVE = 0x00000201
  65. MINE = 0x00000801
  66. MULTIEXPLOSION = 0x00010004
  67. BUILDPASSIVE = 0x00010201
  68. PLATOON = 0x00020101
  69. TRANSIENTPASSIVE = 0x00020201
  70. EXPLOSION = 0x00020401
  71. FLYINGWASTE = 0x00040401
  72. UPGRADECOPULA = 0x00001002
  73. STARTINGPOSITIONMARK = 0x00080101
  74. SMOKE = 0x00080401
  75. ARTEFACT = 0x01020201
  76. EXPLOSIONEX = 0x01020401
  77. BUILDINGTRANSPORTER = 0x01040101
  78. WALLLASER = 0x01100401
  79. RESOURCETRANSPORTER = 0x02040101
  80. BUILDERLINE = 0x02100401
  81. UNITTRANSPORTER = 0x04040101
  82. # Equipment
  83. EQUIPMENT = 0x00000002
  84. REPAIRER = 0x00000202
  85. CONTAINERTRANSPORTER = 0x00000402
  86. LOOKROUNDEQUIPMENT = 0x00000802
  87. TRANSPORTERHOOK = 0x00002002
  88. def __repr__(self):
  89. return self.name
  90. type_field_map = {
  91. 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'],
  92. 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'],
  93. 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'],
  94. 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'],
  95. EntityType.Special: ['classID', 'mesh', 'shadowType', 'viewParamsIndex', 'cost', 'timeOfBuild', '$soundPackID', '$smokeID', '$killExplosionID', '$destructedID'],
  96. EntityType.Equipment: ['classID', 'mesh', 'shadowType', 'viewParamsIndex', 'cost', 'timeOfBuild', '$soundPackID', '$smokeID', '$killExplosionID', '$destructedID', 'rangeOfSight', 'plugType', 'slotType', 'maxAlphaPerTick', 'maxBetaPerTick'],
  97. EntityType.ShieldGenerator: ['shieldCost', 'shieldValue', 'reloadTime', 'shieldMeshName', 'shieldMeshViewIndex'],
  98. EntityType.SpecialUpdatesLinks: ['$specialUpdateLink']
  99. }
  100. class_field_map = {
  101. EntityClass.SUPPLYTRANSPORTER: ['ammoCapacity', 'animSupplyDownStart', 'animSupplyDownEnd', 'animSupplyUpStart', 'animSupplyUpEnd'],
  102. 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'],
  103. EntityClass.MININGROBOT: ['containersCnt', 'ticksPerContainer', 'putResourceAngle', 'animHarvestStartStart', 'animHarvestStartEnd', 'animHarvestWorkStart', 'animHarvestWorkEnd', 'animHarvestEndStart', 'animHarvestEndEnd', '$harvestSmokeID'],
  104. EntityClass.SAPPERROBOT: ['minesLookRange', '$mineID', 'maxMinesCount', 'animDownStart', 'animDownEnd', 'animUpStart', 'animUpEnd', '$putMineSmokeID'],
  105. EntityClass.PASSIVE: ['hp', 'regenerationHP', 'armour', 'calorificCapacity', 'disableResist', 'storeableFlags', 'standType', 'passiveMask', '$wallCopulaID'],
  106. EntityClass.MINE: ['hp', 'regenerationHP', 'armour', 'calorificCapacity', 'disableResist', 'storeableFlags', 'standType', 'mineSize', 'mineTypeOfDamage', 'mineDamage'],
  107. EntityClass.MULTIEXPLOSION: ['useDownBuilding', 'downBuildingStart', 'downBuildingTime', '$subObject1', 'time1', 'angle1', 'dist4X1', '$subObject2', 'time2', 'angle2', 'dist4X2', '$subObject3', 'time3', 'angle3', 'dist4X3', '$subObject4', 'time4', 'angle4', 'dist4X4', '$subObject5', 'time5', 'angle5', 'dist4X5', '$subObject6', 'time6', 'angle6', 'dist4X6', '$subObject7', 'time7', 'angle7', 'dist4X7', '$subObject8', 'time8', 'angle8', 'dist4X8'],
  108. EntityClass.BUILDPASSIVE: ['hp', 'regenerationHP', 'armour', 'calorificCapacity', 'disableResist', 'storeableFlags', 'standType', 'passiveMask', '$wallCopulaID'],
  109. EntityClass.PLATOON: ['hp', 'regenerationHP', 'armour', 'calorificCapacity', 'disableResist', 'storeableFlags', 'standType', 'sightRange', '$talkPackID', '$shieldGeneratorID', 'maxShieldUpdate', 'slot1Type', 'slot2Type', 'slot3Type', 'slot4Type'],
  110. EntityClass.TRANSIENTPASSIVE: ['hp', 'regenerationHP', 'armour', 'calorificCapacity', 'disableResist', 'storeableFlags', 'standType', 'passiveMask', '$wallCopulaID'],
  111. EntityClass.EXPLOSION: ['hp', 'regenerationHP', 'armour', 'calorificCapacity', 'disableResist', 'storeableFlags', 'standType', 'explosionTicks', 'explosionFlags'],
  112. EntityClass.EXPLOSIONEX: ['hp', 'regenerationHP', 'armour', 'calorificCapacity', 'disableResist', 'storeableFlags', 'standType', 'explosionTicks', 'explosionFlags'],
  113. EntityClass.FLYINGWASTE: ['hp', 'regenerationHP', 'armour', 'calorificCapacity', 'disableResist', 'storeableFlags', 'standType', 'wasteSize', '$subWasteID1', 'subWaste1Alpha', '$subWasteID2', 'subWaste2Alpha', '$subWasteID3', 'subWaste3Alpha', '$subWasteID4', 'subWaste4Alpha', 'flightTime', 'wasteSpeed', 'wasteDistanceX4', 'wasteBeta'],
  114. EntityClass.UPGRADECOPULA: ['rangeOfSight', 'plugType', 'slotType', 'maxAlphaPerTick', 'maxBetaPerTick'],
  115. EntityClass.STARTINGPOSITIONMARK: ['hp', 'regenerationHP', 'armour', 'calorificCapacity', 'disableResist', 'storeableFlags', 'standType', 'sightRange', '$talkPackID', '$shieldGeneratorID', 'maxShieldUpdate', 'slot1Type', 'slot2Type', 'slot3Type', 'slot4Type', 'positionType'],
  116. EntityClass.SMOKE: ['hp', 'regenerationHP', 'armour', 'calorificCapacity', 'disableResist', 'storeableFlags', 'standType', 'mesh1', 'mesh2', 'mesh3', 'smokeTime1', 'smokeTime2', 'smokeTime3', 'smokeFrequency', 'startingTime', 'smokingTime', 'endingTime', 'smokeUpSpeed', 'newSmokeDistance'],
  117. EntityClass.ARTEFACT: ['hp', 'regenerationHP', 'armour', 'calorificCapacity', 'disableResist', 'storeableFlags', 'standType', 'passiveMask', '$wallCopulaID', 'artefactMask', 'artefactParam', 'respawnTime'],
  118. EntityClass.BUILDINGTRANSPORTER: ['hp', 'regenerationHP', 'armour', 'calorificCapacity', 'disableResist', 'storeableFlags', 'standType', 'sightRange', '$talkPackID', '$shieldGeneratorID', 'maxShieldUpdate', 'slot1Type', 'slot2Type', 'slot3Type', 'slot4Type', 'vehicleSpeed', 'verticalVehicleAnimationType', '$builderLineID'],
  119. EntityClass.WALLLASER: ['hp', 'regenerationHP', 'armour', 'calorificCapacity', 'disableResist', 'storeableFlags', 'standType'],
  120. EntityClass.RESOURCETRANSPORTER: ['hp', 'regenerationHP', 'armour', 'calorificCapacity', 'disableResist', 'storeableFlags', 'standType', 'sightRange', '$talkPackID', '$shieldGeneratorID', 'maxShieldUpdate', 'slot1Type', 'slot2Type', 'slot3Type', 'slot4Type', 'vehicleSpeed', 'verticalVehicleAnimationType', 'resourceVehicleType', 'animatedTransporterStop', 'showVideoPerTransportersCount', 'totalOrbitalMoney'],
  121. EntityClass.BUILDERLINE: ['hp', 'regenerationHP', 'armour', 'calorificCapacity', 'disableResist', 'storeableFlags', 'standType'],
  122. EntityClass.UNITTRANSPORTER: ['hp', 'regenerationHP', 'armour', 'calorificCapacity', 'disableResist', 'storeableFlags', 'standType', 'sightRange', '$talkPackID', '$shieldGeneratorID', 'maxShieldUpdate', 'slot1Type', 'slot2Type', 'slot3Type', 'slot4Type', 'vehicleSpeed', 'verticalVehicleAnimationType', 'unitsCount', 'dockingHeight', 'animLoadingStartStart', 'animLoadingStartEnd', 'animLoadingEndStart', 'animLoadingEndEnd', 'animUnloadingStartStart', 'animUnloadingStartEnd', 'animUnloadingEndStart', 'animUnloadingEndEnd'],
  123. 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'],
  124. EntityClass.CONTAINERTRANSPORTER: ['animContainerDownStart', 'animContainerDownEnd', 'animContainerUpStart', 'animContainerUpEnd'],
  125. EntityClass.LOOKROUNDEQUIPMENT: ['lookRoundTypeMask', 'lookRoundRange', 'turnSpeed', 'bannerAddExperienceLevel', 'regenerationHPMultiple', 'shieldReloadAdd'],
  126. EntityClass.TRANSPORTERHOOK: ['animTransporterDownStart', 'animTransporterDownEnd', 'animTransporterUpStart', 'animTransporterUpEnd', 'angleToGetPut', 'angleOfGetUnitByLandTransporter', 'takeHeight']
  127. }
  128. enum_mappings = {
  129. 'classID': EntityClass
  130. }
  131. class Research:
  132. def __init__(self, stream):
  133. req_research_count = read_int(stream)
  134. self.previous = [read_int(stream) for _ in range(req_research_count)]
  135. self.id = read_int(stream)
  136. self.faction = Faction(read_int(stream))
  137. self.campaign_cost = read_int(stream)
  138. self.skirmish_cost = read_int(stream)
  139. self.campaign_time = read_int(stream)
  140. self.skirmish_time = read_int(stream)
  141. self.name = read_string(stream)
  142. self.video = read_string(stream)
  143. self.type = ResearchTab(read_int(stream))
  144. self.mesh = read_string(stream)
  145. self.meshParamsIndex = read_int(stream)
  146. def __repr__(self):
  147. items = ("%s=%r" % (k, v) for k, v in self.__dict__.items())
  148. return "%s{%s}" % (self.__class__.__name__, ', '.join(items))
  149. class Entity:
  150. def __init__(self, stream):
  151. self.name = read_string(stream)
  152. req_research_count = read_int(stream)
  153. self.req_research = [read_int(stream) for _ in range(req_research_count)]
  154. field_count = read_int(stream)
  155. field_types = stream.read(field_count)
  156. self.fields = list()
  157. for (i, is_string) in enumerate(field_types):
  158. if is_string:
  159. self.fields.append(read_string(stream))
  160. else:
  161. val = read_int(stream)
  162. # Skip the -1 after every string
  163. if not (val == 0xffffffff and i > 0 and field_types[i-1]):
  164. self.fields.append(val)
  165. def __repr__(self):
  166. return f'Entity{{name={self.name!r}, req_research={self.req_research}, fields={len(self.fields)}{self.fields}}}'
  167. class EntityGroup:
  168. def __init__(self, stream):
  169. self.faction = Faction(read_int(parfile))
  170. self.entity_type = EntityType(read_int(parfile))
  171. entity_count = read_int(parfile)
  172. self.entities = [Entity(stream) for _ in range(entity_count)]
  173. def __repr__(self):
  174. entities = ''
  175. for entity in self.entities:
  176. entities += f' {entity}\n'
  177. return f'EntityGroup{{faction={self.faction}, entity_type={self.entity_type}, entities=\n{entities}}}'
  178. class CsvFileManager:
  179. def __init__(self, directory=os.curdir):
  180. self.directory = directory
  181. self.files = dict()
  182. self.writers = dict()
  183. def __enter__(self):
  184. return self
  185. def __exit__(self, type, value, traceback):
  186. self.close()
  187. def close(self):
  188. for f in self.files.values():
  189. f.close()
  190. def get(self, entity_group):
  191. class_id = None
  192. fields = None
  193. if entity_group.entity_type in [EntityType.Vehicle, EntityType.Cannon, EntityType.Missile, EntityType.Building, EntityType.Special, EntityType.Equipment]:
  194. try:
  195. class_id = entity_group.entities[0].fields[0]
  196. if class_id == EntityClass.EXPLOSIONEX:
  197. class_id = EntityClass.EXPLOSION
  198. elif class_id in [EntityClass.BUILDPASSIVE, EntityClass.TRANSIENTPASSIVE]:
  199. class_id = EntityClass.PASSIVE
  200. name = EntityClass(class_id).name.lower()
  201. except ValueError:
  202. print(f'{class_id:08x}', entity_group)
  203. raise
  204. elif entity_group.entity_type == EntityType.SoundPack:
  205. first_name = entity_group.entities[0].name
  206. if first_name.startswith('TALK_'):
  207. name = 'talkpack'
  208. fields = ['selected', 'move', 'attack', 'command', 'enemy', 'help', 'freeWay']
  209. elif first_name.startswith('PLAYERTALK_'):
  210. name = 'playertalkpack'
  211. fields = ['baseUnderAttack', 'buildingUnderAttack', 'spacePortUnderAttack', 'enemyLandInBase', 'lowMaterials', 'lowMaterialsInBase', 'lowPower', 'lowPowerInBase', 'researchComplete', 'productionStarted', 'productionCompleted', 'productionCanceled', 'platoonLost', 'platoonCreated', 'platoonDisbanded', 'unitLost', 'transporterArrived', 'artefactLocated', 'artefactRecovered', 'newAreaLocationFound', 'enemyMainBaseLocated', 'newSourceFieldLocated', 'sourceFieldExploited', 'buildingLost']
  212. else:
  213. name = 'soundpack'
  214. fields = ['normalWavePack1', 'normalWavePack2', 'normalWavePack3', 'normalWavePack4', 'loopedWavePack1', 'loopedWavePack2', 'loopedWavePack3', 'loopedWavePack4']
  215. elif entity_group.entity_type == EntityType.Parameters:
  216. fields = []
  217. name = 'parameters'
  218. else:
  219. name = entity_group.entity_type.name.lower()
  220. if name not in self.writers:
  221. path = os.path.relpath(name + '.csv', self.directory)
  222. self.files[name] = open(path, 'w', newline='')
  223. self.writers[name] = csv.writer(self.files[name], lineterminator='\n')
  224. if fields is None:
  225. fields = ['name', 'research'] + type_field_map.get(entity_group.entity_type, []) + class_field_map.get(class_id, [])
  226. self.writers[name].writerow(fields) # header
  227. return self.writers[name]
  228. def __repr__(self):
  229. return f'FileManager{{directory={self.directory!r}, files={list(self.files.keys())}}}'
  230. def resnames(research, ids):
  231. return ' '.join(research[id].name for id in ids)
  232. with open(filename, 'rb') as parfile:
  233. headers = parfile.read(16)
  234. entity_group_count = int.from_bytes(headers[8:12], byteorder='little')
  235. entity_groups = [EntityGroup(parfile) for _ in range(entity_group_count)]
  236. research_count = read_int(parfile)
  237. research = [Research(parfile) for _ in range(research_count)]
  238. research_ids = {r.id : r for r in research}
  239. with CsvFileManager() as fm:
  240. for (i, group) in enumerate(entity_groups):
  241. writer = fm.get(group)
  242. writer.writerow([f'Group {i}', group.faction.name])
  243. for e in group.entities:
  244. writer.writerow([e.name, resnames(research, e.req_research)] + e.fields)
  245. writer.writerow([])
  246. with open('research.csv', 'w') as csvfile:
  247. writer = csv.DictWriter(csvfile, ['name', 'faction', 'campaign_cost', 'skirmish_cost', 'campaign_time', 'skirmish_time', 'video', 'type', 'mesh', 'meshParamsIndex', 'previous'], lineterminator='\n')
  248. writer.writeheader()
  249. for r in sorted(research, key=lambda r: r.id):
  250. r = vars(r)
  251. del(r['id'])
  252. r['previous'] = resnames(research, r['previous'])
  253. writer.writerow(r)