Files
dockipelago/worlds/evn/locations.py
Jonathan Tinney 7971961166
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
add schedule I, sonic 1/frontiers/heroes, spirit island
2026-04-02 23:46:36 -07:00

240 lines
9.7 KiB
Python

from __future__ import annotations
from typing import TYPE_CHECKING, Dict, Optional, TypedDict
from venv import logger
from BaseClasses import ItemClassification, Location, Region
#from worlds.evn.regions import can_accept_location # Is this an improper import?
from .rezdata import misns
from .apdata.customoutf import cust_outf_table
from .logics import possible_regions, EVNRegionData, story_routes, EVNStoryRoute, misns_to_ignore
from .apdata.offsets import offsets_table as loc_type_offset
# import re
if TYPE_CHECKING:
from .world import EVNWorld
GAME_NAME = "EV Nova"
# Every location must have a unique integer ID associated with it.
# We will have a lookup from location name to ID here that, in world.py, we will import and bind to the world class.
# Even if a location doesn't exist on specific options, it must be present in this lookup.
# I feel like "location" is a misnomer for a "check"
# Possible types:
# mission completes
# purchase items
# first explore of a sys
# _
# FOR TESTING
# LOCATION_NAME_TO_ID = {
# # Location IDs don't need to be sequential, as long as they're unique and greater than 0.
# # "Example_Mission": 10,
# # "Fed Mission 1": 11,
# # "Fed Mission Final": 12,
# "Delivery to Earth; Vellos1-128": 128,
# "Visit Vell-os Homeworld; Vellos2-129": 129,
# "Head to Sol;Tutorial 001-251": 251,
# "Trade between Earth and Port Kane;Tutorial 002-630": 630, # Do *not* lock progression behind this mission
# "United Shipping Intro;United Shipping1-504": 504,
# "Un. Shipping Delivery;United Shipping1a-505": 505,
# "Take Polaris Home;Rebel I22 LAST-354": 354,
# "Take Llyrell to Korell; Vellos31 LAST-417": 417,
# "Take Krane to Earth;Fed43 LAST-474": 474,
# "A Parting Gift;Fed26 (forced) LAST-596": 596,
# "Return to Heraan;Auroran 029 LAST-686": 686,
# "Destroy McGowan;Pirate 011 LAST-712": 712,
# "Return to Ar'Za Iusia;Polaris 46-887": 887,
# }
# to add other checks, such as outfits, give them their own offset and range.
# TODO: Move all these offset dictionaries to an offset file that they will import from.
# starting_id = 128
# loc_type_offset: Dict[str, int] = {
# "misn": 2000 - starting_id, # 2000 - 2999 will be missions. We have 791/1000 misns, so this should be safe.
# }
class EVNLocationData(TypedDict, total=False):
name: str
address: Optional[int]
#parent_region: Optional[Region]
# Each Location instance must correctly report the "game" it belongs to.
# To make this simple, it is common practice to subclass the basic Location class and override the "game" field.
class EVNLocation(Location):
#game = EVNWorld.game
game = GAME_NAME
# player: int
# name: str
# address: Optional[int] # I think this is the location ID
# parent_region: Optional[Region]
# locked: bool = False
# show_in_spoiler: bool = True
# progress_type: LocationProgressType = LocationProgressType.DEFAULT
# always_allow: Callable[[CollectionState, Item], bool] = staticmethod(lambda state, item: False)
# access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
# item_rule: Callable[[Item], bool] = staticmethod(lambda item: True)
# item: Optional[Item] = None
def get_locations() -> Dict[int, EVNLocationData]:
# wild. For some reason this was updating ev_item_bank, but treating item_name_to_id as a local variable and not updating it, even though we declared it as global. So, explicity informing the function that both are globals.
ret_data: Dict[int, EVNLocationData] = {}
# Missions
for mission in misns.misn_table.keys():
temp_mission = misns.misn_table[mission]
#logger.info(f"loc type offset {loc_type_offset['misn']}")
loc_id = loc_type_offset["misn"] + (int)(temp_mission["id"]) # Probably a safer way to test this? Fails if not int somehow probably.
#logger.info(f"creating location for mission {temp_mission['name']} with id {loc_id}. final name: {temp_mission['name'].strip() + '-' + temp_mission['id']}")
ret_data[loc_id] = EVNLocationData(
name=temp_mission["name"].strip() + "-" + temp_mission["id"], # adding ID to name to ensure uniqueness. We could also add the subname if we wanted, but ID is probably safer.
address=loc_id,
)
# Custom outf checks
for coutf in cust_outf_table.keys():
temp_outf = cust_outf_table[coutf]
loc_id = loc_type_offset["outf_cks"] + (int)(temp_outf["id"])
#logger.info(f'adding location (custom outf): {loc_id}, {temp_outf["name"]}')
ret_data[loc_id] = EVNLocationData(
name=temp_outf["name"].strip() + "-" + temp_outf["id"],
address=loc_id
)
return ret_data
#loc_name_to_id = {data.name: loc_id for loc_id, data in ev_location_bank.items()}
# the int key will be our control bit used by the client to identify the item
ev_location_bank = get_locations()
def get_location_ids() -> Dict[str, int]:
global ev_location_bank
return {data["name"]: item_id for item_id, data in ev_location_bank.items()}
loc_name_to_id = get_location_ids()
def get_location_inverted_lookup() -> Dict[int, str]:
global loc_name_to_id
return {v: k for k, v in loc_name_to_id.items()}
loc_id_to_name = get_location_inverted_lookup()
def get_location_names_with_ids(world: EVNWorld, location_names: list[str]) -> Dict[str, int | None]:
# Surely there is a simpler way? This seems inefficient.
#return ev_location_bank[[loc_id for loc_id in ev_location_bank if ev_location_bank[loc_id].name == name][0]]
#return ev_location_bank[loc_name_to_id[name]]
#return {name: loc_name_to_id[name] for name in location_names if name in loc_name_to_id}
ret_dict: Dict[str, int | None] = {}
for name in location_names:
if name in loc_name_to_id:
ret_dict[name] = loc_name_to_id[name]
else:
ret_dict[name] = None
logger.info(f"location id not found for {name}")
return ret_dict
def create_all_locations(world: EVNWorld) -> None:
create_universe_locations(world)
create_regular_locations(world)
#create_events(world)
# Create universe locations - where not exists id in logic regions, add mission to temp obj. return temp obj into universe region list of locations
# Create remaining locations - just the missions listed in the chosen story line's regions. Don't add the rest - they'll be blocked in the plugin.
def create_universe_locations(world: EVNWorld) -> None:
"""
Populates the default universe region with all locations not used by a story string.
This may not technically need to be separate from create_regular_locations, but it is for now.
:param world: the current world object being populated with data
:type world: EVNWorld
"""
# Get our default region, "Universe", as defined in world (Other games may use a different name)
universe = world.get_region("Universe")
misn_offset = loc_type_offset["misn"]
coutf_offset = loc_type_offset["outf_cks"]
chosen_route = world.get_chosen_string()
# Check if location used by any story regions
for key, loc in ev_location_bank.items():
loc_found = False
# cust outf - shortcut it (was added later)
if key > coutf_offset: # misn is 2k but outf_cks is 4k, so we know here this is an okay check
if not chosen_route["use_extended_checks"] and "(ext)" in loc["name"]:
#logger.info(f'skipping cust outf {key}')
continue
universe.add_locations(
get_location_names_with_ids(world, [loc["name"]])
, EVNLocation
)
continue
# misns
offset_key = key - misn_offset
# first, check if it is a link mission and auto-ignore
if offset_key in misns_to_ignore:
continue
# is the mission used by the storyline?
for rid, sreg in possible_regions.items():
if offset_key in sreg["missions"]:
loc_found = True
#logger.info(f"found offset {offset_key} key in {sreg['name']}")
break
# if so, skip it. It is populated in the next function.
if loc_found:
continue
# If it wasn't found, add it to our default region
universe.add_locations(
get_location_names_with_ids(world, [loc["name"]])
, EVNLocation
)
#logger.info(f"added to universe: {key} - {offset_key} - {loc['name']}")
def create_regular_locations(world: EVNWorld) -> None:
# Finally, we need to put the Locations ("checks") into their regions.
# Once again, before we do anything, we can grab our regions we created by using world.get_region()
#universe = world.get_region("Universe")
misn_offset = loc_type_offset["misn"]
chosen_route = world.get_chosen_string()
for key in chosen_route["regions"]:
sregion = possible_regions[key]
world_region = world.get_region(sregion["name"])
for misnid in sregion["missions"]:
loc = ev_location_bank[misnid + misn_offset]
world_region.add_locations(
get_location_names_with_ids(world, [loc["name"]])
, EVNLocation
)
# I don't know how I want to handle the victory conditions yet.
# fed_string.add_event(
# "Fed Final Mission Complete", "Victory", location_type=EVNLocation, item_type=items.EVNItem
# )
# universe.add_event(
# "String Complete", STRING_COMPLETE_BIT, location_type=EVNLocation, item_type=items.EVNItem
# )
# NOTE: I think that event locations and items have null ID because they exist purely for AP logic, and don't actually trade to/from the game.