Compare commits

...

6 Commits

Author SHA1 Message Date
Exempt-Medic
a90e43bf47 Update marin.txt 2025-07-10 08:45:03 -04:00
axe-y
b1ff55dd06 DLCQ: Fix/Refactor LFoD Start Inventory (#5176) 2025-07-10 08:33:52 -04:00
Remy Jette
f4b5422f66 Factorio: Fix link to world_gen documentation (#5171) 2025-07-07 22:57:55 +02:00
massimilianodelliubaldini
d4ebace99f [Jak and Daxter] Auto Detect Install Path after Game Launcher Update #5152 2025-07-07 19:15:37 +02:00
NewSoupVi
95e09c8e2a Core: Take Counter back out of RestrictedUnpickler #5169 2025-07-07 16:24:35 +02:00
Fabian Dill
4623d59206 Core: ensure slot_data and er_hint_info are only base data types (#5144)
---------

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2025-07-07 15:51:39 +02:00
8 changed files with 75 additions and 43 deletions

View File

@@ -12,6 +12,7 @@ import worlds
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld
from Fill import FillError, balance_multiworld_progression, distribute_items_restrictive, flood_items, \
parse_planned_blocks, distribute_planned_blocks, resolve_early_locations_for_planned
from NetUtils import convert_to_base_types
from Options import StartInventoryPool
from Utils import __version__, output_path, version_tuple
from settings import get_settings
@@ -334,6 +335,9 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None)
}
AutoWorld.call_all(multiworld, "modify_multidata", multidata)
for key in ("slot_data", "er_hint_data"):
multidata[key] = convert_to_base_types(multidata[key])
multidata = zlib.compress(pickle.dumps(multidata), 9)
with open(os.path.join(temp_dir, f'{outfilebase}.archipelago'), 'wb') as f:

View File

@@ -106,6 +106,27 @@ def _scan_for_TypedTuples(obj: typing.Any) -> typing.Any:
return obj
_base_types = str | int | bool | float | None | tuple["_base_types", ...] | dict["_base_types", "base_types"]
def convert_to_base_types(obj: typing.Any) -> _base_types:
if isinstance(obj, (tuple, list, set, frozenset)):
return tuple(convert_to_base_types(o) for o in obj)
elif isinstance(obj, dict):
return {convert_to_base_types(key): convert_to_base_types(value) for key, value in obj.items()}
elif obj is None or type(obj) in (str, int, float, bool):
return obj
# unwrap simple types to their base, such as StrEnum
elif isinstance(obj, str):
return str(obj)
elif isinstance(obj, int):
return int(obj)
elif isinstance(obj, float):
return float(obj)
else:
raise Exception(f"Cannot handle {type(obj)}")
_encode = JSONEncoder(
ensure_ascii=False,
check_circular=False,

View File

@@ -441,9 +441,6 @@ class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module: str, name: str) -> type:
if module == "builtins" and name in safe_builtins:
return getattr(builtins, name)
# used by OptionCounter
if module == "collections" and name == "Counter":
return collections.Counter
# used by MultiServer -> savegame/multidata
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint",
"SlotType", "NetworkSlot", "HintStatus"}:

View File

@@ -1,7 +1,7 @@
import unittest
from Fill import distribute_items_restrictive
from NetUtils import encode
from NetUtils import convert_to_base_types
from worlds.AutoWorld import AutoWorldRegister, call_all
from worlds import failed_world_loads
from . import setup_solo_multiworld
@@ -47,7 +47,7 @@ class TestImplemented(unittest.TestCase):
call_all(multiworld, "post_fill")
for key, data in multiworld.worlds[1].fill_slot_data().items():
self.assertIsInstance(key, str, "keys in slot data must be a string")
self.assertIsInstance(encode(data), str, f"object {type(data).__name__} not serializable.")
convert_to_base_types(data) # only put base data types into slot data
def test_no_failed_world_loads(self):
if failed_world_loads:

View File

@@ -30,7 +30,6 @@ class Group(enum.Enum):
Deprecated = enum.auto()
@dataclass(frozen=True)
class ItemData:
code_without_offset: offset
@@ -98,14 +97,15 @@ def create_trap_items(world, world_options: Options.DLCQuestOptions, trap_needed
return traps
def create_items(world, world_options: Options.DLCQuestOptions, locations_count: int, excluded_items: list[str], random: Random):
def create_items(world, world_options: Options.DLCQuestOptions, locations_count: int, excluded_items: list[str],
random: Random):
created_items = []
if world_options.campaign == Options.Campaign.option_basic or world_options.campaign == Options.Campaign.option_both:
create_items_basic(world_options, created_items, world, excluded_items)
create_items_campaign(world_options, created_items, world, excluded_items, Group.DLCQuest, 825, 250)
if (world_options.campaign == Options.Campaign.option_live_freemium_or_die or
world_options.campaign == Options.Campaign.option_both):
create_items_lfod(world_options, created_items, world, excluded_items)
create_items_campaign(world_options, created_items, world, excluded_items, Group.Freemium, 889, 200)
trap_items = create_trap_items(world, world_options, locations_count - len(created_items), random)
created_items += trap_items
@@ -113,27 +113,8 @@ def create_items(world, world_options: Options.DLCQuestOptions, locations_count:
return created_items
def create_items_lfod(world_options, created_items, world, excluded_items):
for item in items_by_group[Group.Freemium]:
if item.name in excluded_items:
excluded_items.remove(item)
continue
if item.has_any_group(Group.DLC):
created_items.append(world.create_item(item))
if item.has_any_group(Group.Item) and world_options.item_shuffle == Options.ItemShuffle.option_shuffled:
created_items.append(world.create_item(item))
if item.has_any_group(Group.Twice):
created_items.append(world.create_item(item))
if world_options.coinsanity == Options.CoinSanity.option_coin:
if world_options.coinbundlequantity == -1:
create_coin_piece(created_items, world, 889, 200, Group.Freemium)
return
create_coin(world_options, created_items, world, 889, 200, Group.Freemium)
def create_items_basic(world_options, created_items, world, excluded_items):
for item in items_by_group[Group.DLCQuest]:
def create_items_campaign(world_options: Options.DLCQuestOptions, created_items: list[DLCQuestItem], world, excluded_items: list[str], group: Group, total_coins: int, required_coins: int):
for item in items_by_group[group]:
if item.name in excluded_items:
excluded_items.remove(item.name)
continue
@@ -146,14 +127,15 @@ def create_items_basic(world_options, created_items, world, excluded_items):
created_items.append(world.create_item(item))
if world_options.coinsanity == Options.CoinSanity.option_coin:
if world_options.coinbundlequantity == -1:
create_coin_piece(created_items, world, 825, 250, Group.DLCQuest)
create_coin_piece(created_items, world, total_coins, required_coins, group)
return
create_coin(world_options, created_items, world, 825, 250, Group.DLCQuest)
create_coin(world_options, created_items, world, total_coins, required_coins, group)
def create_coin(world_options, created_items, world, total_coins, required_coins, group):
coin_bundle_required = math.ceil(required_coins / world_options.coinbundlequantity)
coin_bundle_useful = math.ceil((total_coins - coin_bundle_required * world_options.coinbundlequantity) / world_options.coinbundlequantity)
coin_bundle_useful = math.ceil(
(total_coins - coin_bundle_required * world_options.coinbundlequantity) / world_options.coinbundlequantity)
for item in items_by_group[group]:
if item.has_any_group(Group.Coin):
for i in range(coin_bundle_required):
@@ -165,7 +147,7 @@ def create_coin(world_options, created_items, world, total_coins, required_coins
def create_coin_piece(created_items, world, total_coins, required_coins, group):
for item in items_by_group[group]:
if item.has_any_group(Group.Piece):
for i in range(required_coins*10):
for i in range(required_coins * 10):
created_items.append(world.create_item(item))
for i in range((total_coins - required_coins) * 10):
created_items.append(world.create_item(item, ItemClassification.useful))

View File

@@ -321,7 +321,7 @@ class InventorySpillTrapCount(TrapCount):
class FactorioWorldGen(OptionDict):
"""World Generation settings. Overview of options at https://wiki.factorio.com/Map_generator,
with in-depth documentation at https://lua-api.factorio.com/latest/Concepts.html#MapGenSettings"""
with in-depth documentation at https://lua-api.factorio.com/latest/concepts/MapGenSettings.html"""
display_name = "World Generation"
# FIXME: do we want default be a rando-optimized default or in-game DS?
value: dict[str, dict[str, typing.Any]]

View File

@@ -367,7 +367,7 @@ def find_root_directory(ctx: JakAndDaxterContext):
f" Close all launchers, games, clients, and console windows, then restart Archipelago.")
if not os.path.exists(settings_path):
msg = (f"{err_title}: the OpenGOAL settings file does not exist.\n"
msg = (f"{err_title}: The OpenGOAL settings file does not exist.\n"
f"{alt_instructions}")
ctx.on_log_error(logger, msg)
return
@@ -375,14 +375,44 @@ def find_root_directory(ctx: JakAndDaxterContext):
with open(settings_path, "r") as f:
load = json.load(f)
jak1_installed = load["games"]["Jak 1"]["isInstalled"]
# This settings file has changed format once before, and may do so again in the future.
# Guard against future incompatibilities by checking the file version first, and use that to determine
# what JSON keys to look for next.
try:
settings_version = load["version"]
logger.debug(f"OpenGOAL settings file version: {settings_version}")
except KeyError:
msg = (f"{err_title}: The OpenGOAL settings file has no version number!\n"
f"{alt_instructions}")
ctx.on_log_error(logger, msg)
return
try:
if settings_version == "2.0":
jak1_installed = load["games"]["Jak 1"]["isInstalled"]
mod_sources = load["games"]["Jak 1"]["modsInstalledVersion"]
elif settings_version == "3.0":
jak1_installed = load["games"]["jak1"]["isInstalled"]
mod_sources = load["games"]["jak1"]["mods"]
else:
msg = (f"{err_title}: The OpenGOAL settings file has an unknown version number ({settings_version}).\n"
f"{alt_instructions}")
ctx.on_log_error(logger, msg)
return
except KeyError as e:
msg = (f"{err_title}: The OpenGOAL settings file does not contain key entry {e}!\n"
f"{alt_instructions}")
ctx.on_log_error(logger, msg)
return
if not jak1_installed:
msg = (f"{err_title}: The OpenGOAL Launcher is missing a normal install of Jak 1!\n"
f"{alt_instructions}")
ctx.on_log_error(logger, msg)
return
mod_sources = load["games"]["Jak 1"]["modsInstalledVersion"]
if mod_sources is None:
msg = (f"{err_title}: No mod sources have been configured in the OpenGOAL Launcher!\n"
f"{alt_instructions}")

View File

@@ -255,7 +255,6 @@ Try Bumper Stickers!
Try Castlevania 64!
Try Celeste 64!
Try ChecksFinder!
Try Clique!
Try Dark Souls III!
Try DLCQuest!
Try Donkey Kong Country 3!
@@ -268,6 +267,7 @@ Try A Hat in Time!
Try Heretic!
Try Hollow Knight!
Try Hylics 2!
Try Jak and Daxter: The Precursor Legacy!
Try Kingdom Hearts 2!
Try Kirby's Dream Land 3!
Try Landstalker - The Treasures of King Nole!
@@ -288,11 +288,10 @@ Try Pokemon Emerald!
Try Pokemon Red and Blue!
Try Raft!
Try Risk of Rain 2!
Try Rogue Legacy!
Try Secret of Evermore!
Try shapez!
Try Shivers!
Try A Short Hike!
Try Slay the Spire!
Try SMZ3!
Try Sonic Adventure 2 Battle!
Try Starcraft 2!
@@ -300,6 +299,7 @@ Try Stardew Valley!
Try Subnautica!
Try Sudoku!
Try Super Mario 64!
Try Super Mario Land 2: 6 Golden Coins!
Try Super Mario World!
Try Super Metroid!
Try Terraria!
@@ -312,7 +312,6 @@ Try The Witness!
Try Yoshi's Island!
Try Yu-Gi-Oh! 2006!
Try Zillion!
Try Zork Grand Inquisitor!
Try Old School Runescape!
Try Kingdom Hearts!
Try Mega Man 2!
@@ -369,7 +368,6 @@ Have they added Among Us to AP yet?
Every copy of LADX is personalized, David.
Looks like you're going on A Short Hike. Bring back feathers please?
Functioning Brain is at...\nWait. This isn't Witness. Wrong game, sorry.
Don't forget to check your Clique!\nIf, y'know, you have one. No pressure...
:3
Sorry ######, but your progression item is in another world.
&newgames\n&oldgames