forked from mirror/Archipelago
Some checks failed
Analyze modified files / flake8 (push) Failing after 2m28s
Build / build-win (push) Has been cancelled
Build / build-ubuntu2204 (push) Has been cancelled
ctest / Test C++ ubuntu-latest (push) Has been cancelled
ctest / Test C++ windows-latest (push) Has been cancelled
Analyze modified files / mypy (push) Has been cancelled
Build and Publish Docker Images / Push Docker image to Docker Hub (push) Successful in 5m4s
Native Code Static Analysis / scan-build (push) Failing after 5m2s
type check / pyright (push) Successful in 1m7s
unittests / Test Python 3.11.2 ubuntu-latest (push) Failing after 16m23s
unittests / Test Python 3.12 ubuntu-latest (push) Failing after 28m19s
unittests / Test Python 3.13 ubuntu-latest (push) Failing after 14m49s
unittests / Test hosting with 3.13 on ubuntu-latest (push) Successful in 5m0s
unittests / Test Python 3.13 macos-latest (push) Has been cancelled
unittests / Test Python 3.11 windows-latest (push) Has been cancelled
unittests / Test Python 3.13 windows-latest (push) Has been cancelled
423 lines
16 KiB
Python
423 lines
16 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
from collections import defaultdict
|
|
from typing import TextIO, Dict, Set
|
|
|
|
from jinja2 import Environment, PackageLoader, select_autoescape
|
|
|
|
from GameData import GameData, ElementType, ItemType, ItemFlags
|
|
|
|
SCRIPT_DIR = os.path.join(os.path.dirname(__file__))
|
|
|
|
|
|
CLASSIFICATION_OVERRIDES: defaultdict[int, str] = defaultdict(lambda: 'filler', {
|
|
# NOTE: using strings here, because importing ItemClassification from BaseClasses will cause a circular import
|
|
65:'progression',# ItemName.Shamans_Rod
|
|
458:'progression',# ItemName.Sea_Gods_Tear
|
|
439:'progression',# ItemName.Right_Prong
|
|
448:'progression',# ItemName.Healing_Fungus
|
|
242:'progression',# ItemName.Black_Crystal
|
|
452:'progression',# ItemName.Pretty_Stone
|
|
453:'progression',# ItemName.Red_Cloth
|
|
454:'progression',# ItemName.Milk
|
|
455:'progression',# ItemName.Lil_Turtle
|
|
456:'progression',# ItemName.Aquarius_Stone
|
|
440:'progression',# ItemName.Left_Prong
|
|
451:'progression',# ItemName.Dancing_Idol
|
|
441:'progression',# ItemName.Center_Prong
|
|
460:'progression',# ItemName.Magma_Ball
|
|
326:'progression',# ItemName.Trident
|
|
244:'progression',# ItemName.Blue_Key
|
|
243:'progression',# ItemName.Red_Key
|
|
222:'progression',# ItemName.Mythril_Bag_Mars
|
|
459:'progression',# ItemName.Ruin_Key
|
|
# 229:'progression_skip_balancing', # ItemName.Lucky_Medal
|
|
443: 'useful',# ItemName.Mysterious_Card
|
|
444: 'useful',# ItemName.Trainers_Whip
|
|
445: 'useful',# ItemName.Tomegathericon
|
|
# 449: 'filler',# ItemName.Laughing_Fungus
|
|
# 333: 'useful',# ItemName.Ixion_Mail
|
|
# 290: 'useful',# ItemName.Hypnos_Sword
|
|
# 394: 'useful',# ItemName.Clarity_Circlet
|
|
# 358: 'useful',# ItemName.Fujin_Shield
|
|
# 279: 'useful',# ItemName.Storm_Brand
|
|
# 231: 'filler',# ItemName.Bone
|
|
|
|
# 425:'useful', # Rusty_Staff_Dracomace
|
|
# 426:'useful', # Rusty_Staff_GlowerStaff
|
|
# 427:'useful', # Rusty_Staff_GoblinsRod
|
|
# 417:'useful', # Rusty_Sword_CorsairsEdge
|
|
# 418:'useful', # Rusty_Sword_RobbersBlade
|
|
# 419:'useful', # Rusty_Sword_PiratesSabre
|
|
# 420:'useful', # Rusty_Sword_SoulBrand
|
|
# 424:'useful', # Rusty_Mace_HagboneMace
|
|
# 423:'useful', # Rusty_Mace_DemonMace
|
|
# 422:'useful', # Rusty_Axe_VikingAxe
|
|
# 421:'useful', # Rusty_Axe_CaptainsAxe
|
|
|
|
# 301:'useful', # Themis_Axe
|
|
# 340:'useful', # Full_Metal_Vest
|
|
# 383:'useful', # Nurses_Cap
|
|
# 287:'useful', # Pirates_Sword
|
|
# 414:'useful', # Guardian_Ring
|
|
# 309:'useful', # Blow_Mace
|
|
# 384:'useful', # Thorn_Crown
|
|
# 266:'useful', # Unicorn_Ring
|
|
# 300:'useful', # Disk_Axe
|
|
# 370:'useful', # Bone_Armlet
|
|
# 259:'useful', # Turtle_Boots
|
|
# 291:'useful', # Mist_Sabre
|
|
# 343:'useful', # Festival_Coat
|
|
# 334:'useful', # Phantasmal_Mail
|
|
# 283:'useful', # Cloud_Brand
|
|
# 351:'useful', # Iris_Robe
|
|
# 7:'useful', # Fire_Brand
|
|
# 371:'useful', # Jesters_Armlet
|
|
# 281:'useful', # Lightning_Sword
|
|
# 349:'useful', # Muni_Robe
|
|
# 311:'useful', # Thanatos_Mace
|
|
# 378:'useful', # Viking_Helm
|
|
# 26:'useful', # Masamune
|
|
# 366:'useful', # Spirit_Gloves
|
|
# 344:'useful', # Erinyes_Tunic
|
|
# 319:'useful', # Meditation_Rod
|
|
# 292:'useful', # Phaetons_Blade
|
|
# 388:'useful', # Alastors_Hood
|
|
# 10:'useful', # Sol_Blade
|
|
# 336:'useful', # Valkyrie_Mail
|
|
|
|
183:'useful', #Potion
|
|
186:'useful', #Psy_Crystal
|
|
190:'useful', #Mist_Potion
|
|
# 191:'useful', #Power_Bread
|
|
# 192:'useful', #Cookie
|
|
# 193:'useful', #Apple
|
|
# 194:'useful', #Hard_Nut
|
|
# 195:'useful', #Mint
|
|
# 196:'useful', #Lucky_Pepper
|
|
429:'useful', #Tear_Stone
|
|
189:'useful', #Water_of_Life
|
|
437:'useful', #Orihalcon
|
|
435:'useful', #Mythril_Silver
|
|
436:'useful', #Dark_Matter
|
|
430:'useful', #Star_Dust
|
|
431:'useful', #Sylph_Feather
|
|
432:'useful', #Dragon_Skin
|
|
434:'useful', #Golem_Core
|
|
433:'useful', #Salamander_Tail
|
|
})
|
|
|
|
|
|
USEFUL_ITEM_GROUPS: Dict[str, Set[int]] = {
|
|
"useful_consumables": {
|
|
183, # Potion
|
|
186, # Psy Crystal
|
|
189, # Water of Life
|
|
190, # Mist Potion
|
|
},
|
|
"forge_materials": {
|
|
x for x in range(429, 438) # Tear Stone through Orihalcon
|
|
},
|
|
"class_change_items": {
|
|
x for x in range(443, 446) # Mysterious Card, Trainer's Whip, Tomagathericon
|
|
}
|
|
}
|
|
|
|
OTHER_ITEM_GROUPS: Dict[str, Set[int]] = {
|
|
"rusty_items": {
|
|
x for x in range(417, 428) # Rusty Sword - Robber's Blade through Goblin's Rod
|
|
},
|
|
"stat_boosters": {
|
|
x for x in range(191, 197) # Power Bread through Lucky Pepper
|
|
},
|
|
}
|
|
|
|
|
|
# Items that should not be part of the item data at all
|
|
EXCLUDED_ITEMS = {
|
|
184, # Hermes' Water
|
|
185, # Empty Bottle
|
|
220, # Venus Star
|
|
221, # Mercury Star
|
|
223, # Mythril Bag (Jupiter)
|
|
224, # Mythril Bag (Empty)
|
|
225, # Small Jewel
|
|
230, # Dragon's Eye
|
|
232, # Anchor Charm
|
|
234, # Cell Key
|
|
235, # Boat Ticket
|
|
237, # Mystic Draught
|
|
245, # Mythril Bag (Mars & Jupiter)
|
|
246, # Jupiter Star
|
|
247, # Mars Star, 222, Mythril Bag (Mars) is actually used in the game
|
|
450, # Signal Whistle
|
|
457, # Large Bread
|
|
}
|
|
|
|
def main():
|
|
env = Environment(
|
|
loader=PackageLoader("gen"),
|
|
autoescape=select_autoescape()
|
|
)
|
|
env.filters['bitwise_and'] = lambda x, y: x & y > 0
|
|
data = GameData()
|
|
generate_location_names(env, data)
|
|
generate_item_names(env, data)
|
|
generate_item_data(env, data)
|
|
generate_location_data(env, data)
|
|
|
|
|
|
def generate_location_names(env: Environment, data: GameData):
|
|
template = env.get_template('LocationNames.py.jinja')
|
|
hidden_items = []
|
|
key_items = []
|
|
summon_tablets = []
|
|
major_items = []
|
|
remainder = []
|
|
earth_djinn = []
|
|
water_djinn = []
|
|
fire_djinn = []
|
|
air_djinn = []
|
|
events = []
|
|
name_list = []
|
|
|
|
for djinn in data.raw_djinn_data:
|
|
# d = {'key': djinn.name, 'value': djinn.name}
|
|
if djinn.element == ElementType.Earth:
|
|
earth_djinn.append(djinn)
|
|
elif djinn.element == ElementType.Water:
|
|
water_djinn.append(djinn)
|
|
elif djinn.element == ElementType.Fire:
|
|
fire_djinn.append(djinn)
|
|
elif djinn.element == ElementType.Air:
|
|
air_djinn.append(djinn)
|
|
name_list.append({'name': djinn.name, 'id': djinn.ap_id})
|
|
for event in data.events.values():
|
|
events.append(data.location_names[event.event_id])
|
|
name_list.append({'name': data.location_names[event.event_id].py_name, 'id': event.event_id})
|
|
for loc_datum in data.raw_location_data:
|
|
loc_name = data.location_names[loc_datum.id]
|
|
name_list.append({'name': loc_name.py_name, 'id': loc_datum.addr[0]})
|
|
if loc_datum.is_summon:
|
|
summon_tablets.append(loc_name)
|
|
elif loc_datum.is_key_item:
|
|
key_items.append(loc_name)
|
|
elif loc_datum.is_major_item:
|
|
major_items.append(loc_name)
|
|
elif loc_datum.is_hidden:
|
|
hidden_items.append(loc_name)
|
|
else:
|
|
remainder.append(loc_name)
|
|
|
|
with open(os.path.join(SCRIPT_DIR, 'gen', 'InternalLocationNames.py'), 'w') as outfile:
|
|
write_warning(outfile)
|
|
outfile.write(template.render(hiddenItems=hidden_items, keyItems=key_items,
|
|
summonTablets=summon_tablets, majorItems=major_items,
|
|
remainder=remainder, earthDjinn=earth_djinn,
|
|
waterDjinn=water_djinn, fireDjinn=fire_djinn,
|
|
airDjinn=air_djinn, events=events,
|
|
name_list=name_list))
|
|
|
|
def generate_item_names(env: Environment, data: GameData):
|
|
template = env.get_template('ItemNames.py.jinja')
|
|
|
|
with open(os.path.join(SCRIPT_DIR, 'gen', 'InternalItemNames.py'), 'w') as outfile:
|
|
write_warning(outfile)
|
|
name_dict = {
|
|
item.id: {
|
|
'item': item,
|
|
'name': data.item_names[item.id]
|
|
} for item in data.raw_item_data if item.id not in EXCLUDED_ITEMS
|
|
}
|
|
name_list = [{'name': data.item_names[x.id].py_name, 'id': x.id}
|
|
for x in data.raw_item_data if x.id not in EXCLUDED_ITEMS]
|
|
summons = [x for x in data.raw_summon_data]
|
|
name_list += [{'name': x.name, 'id': x.id} for x in summons]
|
|
events = [data.item_names[event.event_id] for event in data.events.values()]
|
|
name_list += [{'name': data.item_names[x.id].py_name, 'id': x.id} for x in events]
|
|
name_list += [{'name': d.name, 'id': d.ap_id} for d in data.raw_djinn_data]
|
|
name_list += [{'name': data.item_names[p.id].py_name, 'id': p.id} for p in data.raw_psy_data]
|
|
name_list += [{'name': c.name, 'id': c.id} for c in data.raw_character_data]
|
|
outfile.write(template.render(
|
|
name_list=name_list,
|
|
summons=summons,
|
|
items=name_dict.values(),
|
|
psyenergies=[data.item_names[x.id] for x in data.raw_psy_data],
|
|
djinn=[data.item_names[x.ap_id] for x in data.raw_djinn_data],
|
|
events=events,
|
|
characters=[data.item_names[c.id] for c in data.raw_character_data],
|
|
types=[x for x in ItemType if x < ItemType.Psyenergy or x == ItemType.Mimic]))
|
|
|
|
def generate_item_data(env: Environment, data: GameData):
|
|
|
|
template = env.get_template('ItemData.py.jinja')
|
|
with open(os.path.join(SCRIPT_DIR, 'gen', 'InternalItemData.py'), 'w') as outfile:
|
|
write_warning(outfile)
|
|
names = data.item_names
|
|
|
|
summons = [x for x in data.raw_summon_data]
|
|
psyenergies = [x for x in data.raw_psy_data]
|
|
characters = [c for c in data.raw_character_data]
|
|
djinns = [x for x in data.raw_djinn_data]
|
|
psyitems = []
|
|
mimics = []
|
|
other_prog = []
|
|
other_useful = dict()
|
|
other_items = dict()
|
|
shop_only = []
|
|
forge_only = []
|
|
lucky_only = []
|
|
non_vanilla = []
|
|
vanilla_coins = []
|
|
misc = []
|
|
remainder = []
|
|
vanilla_item_ids = { x.vanilla_contents for x in data.raw_location_data if x.vanilla_name != 'Mimic'}
|
|
vanilla_item_ids.add(228) # Game Ticket
|
|
misc_ids = {
|
|
0, # Empty
|
|
231, # Bone
|
|
449, # Laughing Fungus
|
|
}
|
|
shop_only_ids = set()
|
|
forge_only_ids = set()
|
|
lucky_only_ids = set()
|
|
for id in data.vanilla_shop_contents:
|
|
if id not in vanilla_item_ids:
|
|
shop_only_ids.add(id)
|
|
vanilla_item_ids.add(id)
|
|
for id in data.forgeable_ids:
|
|
if id not in vanilla_item_ids:
|
|
forge_only_ids.add(id)
|
|
vanilla_item_ids.add(id)
|
|
for id in data.lucky_medal_ids:
|
|
if id not in vanilla_item_ids:
|
|
lucky_only_ids.add(id)
|
|
vanilla_item_ids.add(id)
|
|
for id in data.lucky_wheels_ids:
|
|
if id not in vanilla_item_ids:
|
|
lucky_only_ids.add(id)
|
|
vanilla_item_ids.add(id)
|
|
classifications = dict(CLASSIFICATION_OVERRIDES)
|
|
for item in data.raw_item_data:
|
|
datum = {'item': item, 'name': names[item.id]}
|
|
if item.id not in classifications:
|
|
if item.item_type in {ItemType.Shirt, ItemType.Boots, ItemType.Ring}:
|
|
classifications[item.id] = 'useful'
|
|
elif item.item_type in {ItemType.Armor, ItemType.Weapon, ItemType.Helm, ItemType.Shield} and item.flags & ItemFlags.Rare > 0:
|
|
classifications[item.id] = 'useful'
|
|
else:
|
|
classifications[item.id] = 'filler'
|
|
if item.id in EXCLUDED_ITEMS:
|
|
continue
|
|
elif item.is_mimic:
|
|
mimics.append(datum)
|
|
elif item.item_type == ItemType.PsyenergyItem:
|
|
psyitems.append(datum)
|
|
elif CLASSIFICATION_OVERRIDES[item.id] == 'progression':
|
|
other_prog.append(datum)
|
|
elif any(item.id in keys for keys in OTHER_ITEM_GROUPS.values()):
|
|
other_items[item.id] = datum
|
|
elif CLASSIFICATION_OVERRIDES[item.id] == 'useful':
|
|
other_useful[item.id] = datum
|
|
elif item.id in shop_only_ids:
|
|
shop_only.append(datum)
|
|
elif item.id in forge_only_ids:
|
|
forge_only.append(datum)
|
|
elif item.id in lucky_only_ids:
|
|
lucky_only.append(datum)
|
|
elif item.id > 0x8000:
|
|
vanilla_coins.append(datum)
|
|
elif item.id not in vanilla_item_ids:
|
|
non_vanilla.append(datum)
|
|
elif item.id in misc_ids:
|
|
misc.append(datum)
|
|
elif classifications[item.id] == 'useful':
|
|
other_useful[item.id] = datum
|
|
else:
|
|
remainder.append(datum)
|
|
useful_remainder_ids = set()
|
|
for usefuls in USEFUL_ITEM_GROUPS.values():
|
|
useful_remainder_ids |= usefuls
|
|
useful_remainder_ids = {x['item'].id for x in other_useful.values()} - useful_remainder_ids
|
|
outfile.write(template.render(
|
|
summons=summons,
|
|
psyenergies=psyenergies,
|
|
psyitems=psyitems,
|
|
djinns=djinns,
|
|
characters=characters,
|
|
mimics=mimics,
|
|
other_prog=other_prog,
|
|
misc=misc,
|
|
other_useful=other_useful,
|
|
useful_groups=USEFUL_ITEM_GROUPS,
|
|
useful_remainder=useful_remainder_ids,
|
|
other_items=other_items,
|
|
other_groups=OTHER_ITEM_GROUPS,
|
|
non_vanilla=non_vanilla,
|
|
shop_only=shop_only,
|
|
forge_only=forge_only,
|
|
lucky_only=lucky_only,
|
|
vanilla_coins=vanilla_coins,
|
|
vanilla_item_ids=sorted(vanilla_item_ids),
|
|
# unique_items=unique_items,
|
|
# gear=gear,
|
|
remainder=remainder,
|
|
progression=classifications,
|
|
events=data.events.values()
|
|
))
|
|
|
|
def generate_location_data(env: Environment, data: GameData):
|
|
template = env.get_template('LocationData.py.jinja')
|
|
with open(os.path.join(SCRIPT_DIR, 'gen', 'InternalLocationData.py'), 'w') as outfile:
|
|
write_warning(outfile)
|
|
loc_data = data.raw_location_data
|
|
psy_ids = {psy.id for psy in data.raw_psy_data}
|
|
djinn_locs = data.raw_djinn_data
|
|
summons_loc = []
|
|
psy_locs = []
|
|
remainder = []
|
|
for loc in loc_data:
|
|
if loc.is_summon:
|
|
summons_loc.append(loc)
|
|
elif loc.vanilla_contents in psy_ids:
|
|
psy_locs.append(loc)
|
|
else:
|
|
remainder.append(loc)
|
|
loc_type_lookup: defaultdict[int, str] = defaultdict(lambda: 'Item', {
|
|
2722: 'Trade', #Pretty Stone
|
|
2724: 'Trade', #Red Cloth
|
|
2723: 'Trade', #Milk
|
|
2721: 'Trade', #Li'l Turtle
|
|
3328: 'Character', # Contigo Isaac
|
|
3329: 'Character', # Contigo Garet
|
|
3330: 'Character', # Contigo Ivan
|
|
3331: 'Character', # Contigo Mia
|
|
3333: 'Character', # Idejima Jenna
|
|
3334: 'Character', # Idejima Sheba
|
|
3335: 'Character', # Kibombo Piers
|
|
})
|
|
for loc in remainder:
|
|
if loc.is_hidden:
|
|
loc_type_lookup[loc.id] = 'Hidden'
|
|
outfile.write(template.render(
|
|
summon_locations=summons_loc,
|
|
psyenergy_locations=psy_locs,
|
|
djinn_locations=djinn_locs,
|
|
other_locations=remainder,
|
|
events=data.events.values(),
|
|
loc_type_lookup=loc_type_lookup,
|
|
))
|
|
|
|
|
|
def write_warning(fp: TextIO):
|
|
fp.write(
|
|
"""# This file was generated using jinja2 from a template. If this file needs
|
|
# to be changed, either change the template, or the code leveraging the template.
|
|
""")
|
|
|
|
#
|
|
|
|
if __name__ == '__main__':
|
|
main()
|