Compare commits
113 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0668f94461 | ||
|
|
953ccc55d9 | ||
|
|
fbaa8226c4 | ||
|
|
8abfd14569 | ||
|
|
f2f4d6a133 | ||
|
|
3ed7092af5 | ||
|
|
9d6fa855d8 | ||
|
|
8c7404edf9 | ||
|
|
3f6a9e5dc7 | ||
|
|
9e1748bf67 | ||
|
|
527a9b49e2 | ||
|
|
b187223162 | ||
|
|
2c5e99efed | ||
|
|
fa8531022d | ||
|
|
8d4be10fd7 | ||
|
|
285b9e12eb | ||
|
|
53fcb86174 | ||
|
|
a532ceeb0a | ||
|
|
9ec0680ce5 | ||
|
|
299036ecca | ||
|
|
4bfeb77a3a | ||
|
|
ab7a5b07eb | ||
|
|
50ad661796 | ||
|
|
d3e71ecb9b | ||
|
|
ba3bb201cd | ||
|
|
01d88c362a | ||
|
|
95350a1fa9 | ||
|
|
cc458ca5b1 | ||
|
|
f19878fcb8 | ||
|
|
eb8e8691e9 | ||
|
|
0423c22d7f | ||
|
|
3441c390bd | ||
|
|
a0fb9bc4ab | ||
|
|
a7bb6f6a95 | ||
|
|
f1bef73757 | ||
|
|
4598dd1a0f | ||
|
|
0241d6f443 | ||
|
|
72acb5509a | ||
|
|
b43e99fa20 | ||
|
|
b5083ddb1b | ||
|
|
f62e8b7be9 | ||
|
|
f655dc0dbc | ||
|
|
95e0fa2672 | ||
|
|
4b7c8f757e | ||
|
|
381e9c744a | ||
|
|
9aa4bb3f4b | ||
|
|
63617edfef | ||
|
|
72de0450e0 | ||
|
|
306bdd322f | ||
|
|
231613cb3b | ||
|
|
2af5739592 | ||
|
|
b38f7c8f2a | ||
|
|
e3a81c1bed | ||
|
|
cd8452d839 | ||
|
|
4b38cb4c2e | ||
|
|
eda8c6f263 | ||
|
|
a8cf67c94d | ||
|
|
928b341fb3 | ||
|
|
6e51b1d50c | ||
|
|
78aaa65b45 | ||
|
|
3627d8f1ae | ||
|
|
1e64b817f6 | ||
|
|
37e999652d | ||
|
|
9408557f03 | ||
|
|
16701249b4 | ||
|
|
3c1ac134f2 | ||
|
|
230d9d993e | ||
|
|
d1c83ffc09 | ||
|
|
a52f991543 | ||
|
|
dfc56a3272 | ||
|
|
41037ce599 | ||
|
|
a3924ed40a | ||
|
|
361bd4e5f6 | ||
|
|
8cc245ac11 | ||
|
|
2d8a6e84c1 | ||
|
|
d2add54cd6 | ||
|
|
40044ac5a6 | ||
|
|
bb15d0636e | ||
|
|
2cc7d8395b | ||
|
|
2f2e039356 | ||
|
|
0cd388ca66 | ||
|
|
7ef1fe81f6 | ||
|
|
774610de7b | ||
|
|
f6c85e17d5 | ||
|
|
8142306562 | ||
|
|
2d84245103 | ||
|
|
1d954b192c | ||
|
|
db0604f585 | ||
|
|
08beb5fbe6 | ||
|
|
7df06b87a5 | ||
|
|
abf4e82737 | ||
|
|
7f8617d639 | ||
|
|
f5c62a82ac | ||
|
|
66514ec607 | ||
|
|
096e682b18 | ||
|
|
e098b3c504 | ||
|
|
4dde466364 | ||
|
|
6d6fc52481 | ||
|
|
eaae4af832 | ||
|
|
7f5afddb38 | ||
|
|
36a981aaa2 | ||
|
|
fdcf093be0 | ||
|
|
1bd55b4572 | ||
|
|
eb0e5b7438 | ||
|
|
884dece54c | ||
|
|
3759f4c644 | ||
|
|
f232f74246 | ||
|
|
a9ecab35d8 | ||
|
|
e1e25d0eae | ||
|
|
f45c042351 | ||
|
|
f15bb9dbd7 | ||
|
|
610871c61b | ||
|
|
35b9e4768a |
2
.github/workflows/unittests.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install flake8 pytest
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
python ModuleUpdate.py --yes --force
|
||||
- name: Unittests
|
||||
run: |
|
||||
pytest test
|
||||
|
||||
1
.gitignore
vendored
@@ -16,7 +16,6 @@
|
||||
*.apsave
|
||||
|
||||
build
|
||||
/build_factorio/
|
||||
bundle/components.wxs
|
||||
dist
|
||||
README.html
|
||||
|
||||
118
BaseClasses.py
@@ -13,7 +13,7 @@ import random
|
||||
|
||||
class MultiWorld():
|
||||
debug_types = False
|
||||
player_names: Dict[int, List[str]]
|
||||
player_name: Dict[int, str]
|
||||
_region_cache: Dict[int, Dict[str, Region]]
|
||||
difficulty_requirements: dict
|
||||
required_medallions: dict
|
||||
@@ -36,7 +36,6 @@ class MultiWorld():
|
||||
def __init__(self, players: int):
|
||||
self.random = random.Random() # world-local random state is saved for multiple generations running concurrently
|
||||
self.players = players
|
||||
self.teams = 1
|
||||
self.glitch_triforce = False
|
||||
self.algorithm = 'balanced'
|
||||
self.dungeons = []
|
||||
@@ -83,11 +82,9 @@ class MultiWorld():
|
||||
set_player_attr('item_functionality', 'normal')
|
||||
set_player_attr('timer', False)
|
||||
set_player_attr('goal', 'ganon')
|
||||
set_player_attr('progressive', 'on')
|
||||
set_player_attr('accessibility', 'items')
|
||||
set_player_attr('retro', False)
|
||||
set_player_attr('hints', True)
|
||||
set_player_attr('player_names', [])
|
||||
set_player_attr('required_medallions', ['Ether', 'Quake'])
|
||||
set_player_attr('swamp_patch_required', False)
|
||||
set_player_attr('powder_patch_required', False)
|
||||
@@ -162,10 +159,10 @@ class MultiWorld():
|
||||
return tuple(player for player in self.player_ids if self.game[player] == game_name)
|
||||
|
||||
def get_name_string_for_object(self, obj) -> str:
|
||||
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})'
|
||||
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_name(obj.player)})'
|
||||
|
||||
def get_player_names(self, player: int) -> str:
|
||||
return ", ".join(self.player_names[player])
|
||||
def get_player_name(self, player: int) -> str:
|
||||
return self.player_name[player]
|
||||
|
||||
def initialize_regions(self, regions=None):
|
||||
for region in regions if regions else self.regions:
|
||||
@@ -174,7 +171,7 @@ class MultiWorld():
|
||||
|
||||
@functools.cached_property
|
||||
def world_name_lookup(self):
|
||||
return {self.player_names[player_id][0]: player_id for player_id in self.player_ids}
|
||||
return {self.player_name[player_id]: player_id for player_id in self.player_ids}
|
||||
|
||||
def _recache(self):
|
||||
"""Rebuild world cache"""
|
||||
@@ -197,7 +194,6 @@ class MultiWorld():
|
||||
self._recache()
|
||||
return self._region_cache[player][regionname]
|
||||
|
||||
|
||||
def get_entrance(self, entrance: str, player: int) -> Entrance:
|
||||
try:
|
||||
return self._entrance_cache[entrance, player]
|
||||
@@ -205,7 +201,6 @@ class MultiWorld():
|
||||
self._recache()
|
||||
return self._entrance_cache[entrance, player]
|
||||
|
||||
|
||||
def get_location(self, location: str, player: int) -> Location:
|
||||
try:
|
||||
return self._location_cache[location, player]
|
||||
@@ -213,15 +208,18 @@ class MultiWorld():
|
||||
self._recache()
|
||||
return self._location_cache[location, player]
|
||||
|
||||
|
||||
def get_dungeon(self, dungeonname: str, player: int) -> Dungeon:
|
||||
for dungeon in self.dungeons:
|
||||
if dungeon.name == dungeonname and dungeon.player == player:
|
||||
return dungeon
|
||||
raise KeyError('No such dungeon %s for player %d' % (dungeonname, player))
|
||||
|
||||
|
||||
def get_all_state(self, keys=False) -> CollectionState:
|
||||
key = f"_all_state_{keys}"
|
||||
cached = getattr(self, key, None)
|
||||
if cached:
|
||||
return cached.copy()
|
||||
|
||||
ret = CollectionState(self)
|
||||
|
||||
for item in self.itempool:
|
||||
@@ -246,6 +244,7 @@ class MultiWorld():
|
||||
p):
|
||||
world.collect(ret, item)
|
||||
ret.sweep_for_events()
|
||||
setattr(self, key, ret)
|
||||
return ret
|
||||
|
||||
def get_items(self) -> list:
|
||||
@@ -264,8 +263,6 @@ class MultiWorld():
|
||||
|
||||
def push_precollected(self, item: Item):
|
||||
item.world = self
|
||||
if (item.smallkey and self.keyshuffle[item.player]) or (item.bigkey and self.bigkeyshuffle[item.player]):
|
||||
item.advancement = True
|
||||
self.precollected_items.append(item)
|
||||
self.state.collect(item, True)
|
||||
|
||||
@@ -759,53 +756,12 @@ class CollectionState(object):
|
||||
return changed
|
||||
|
||||
def remove(self, item):
|
||||
if item.advancement:
|
||||
to_remove = item.name
|
||||
if item.game == "A Link to the Past" and to_remove.startswith('Progressive '):
|
||||
if 'Sword' in to_remove:
|
||||
if self.has('Golden Sword', item.player):
|
||||
to_remove = 'Golden Sword'
|
||||
elif self.has('Tempered Sword', item.player):
|
||||
to_remove = 'Tempered Sword'
|
||||
elif self.has('Master Sword', item.player):
|
||||
to_remove = 'Master Sword'
|
||||
elif self.has('Fighter Sword', item.player):
|
||||
to_remove = 'Fighter Sword'
|
||||
else:
|
||||
to_remove = None
|
||||
elif 'Glove' in item.name:
|
||||
if self.has('Titans Mitts', item.player):
|
||||
to_remove = 'Titans Mitts'
|
||||
elif self.has('Power Glove', item.player):
|
||||
to_remove = 'Power Glove'
|
||||
else:
|
||||
to_remove = None
|
||||
elif 'Shield' in item.name:
|
||||
if self.has('Mirror Shield', item.player):
|
||||
to_remove = 'Mirror Shield'
|
||||
elif self.has('Red Shield', item.player):
|
||||
to_remove = 'Red Shield'
|
||||
elif self.has('Blue Shield', item.player):
|
||||
to_remove = 'Blue Shield'
|
||||
else:
|
||||
to_remove = None
|
||||
elif 'Bow' in item.name:
|
||||
if self.has('Silver Bow', item.player):
|
||||
to_remove = 'Silver Bow'
|
||||
elif self.has('Bow', item.player):
|
||||
to_remove = 'Bow'
|
||||
else:
|
||||
to_remove = None
|
||||
|
||||
if to_remove:
|
||||
|
||||
self.prog_items[to_remove, item.player] -= 1
|
||||
if self.prog_items[to_remove, item.player] < 1:
|
||||
del (self.prog_items[to_remove, item.player])
|
||||
# invalidate caches, nothing can be trusted anymore now
|
||||
self.reachable_regions[item.player] = set()
|
||||
self.blocked_connections[item.player] = set()
|
||||
self.stale[item.player] = True
|
||||
changed = self.world.worlds[item.player].remove(self, item)
|
||||
if changed:
|
||||
# invalidate caches, nothing can be trusted anymore now
|
||||
self.reachable_regions[item.player] = set()
|
||||
self.blocked_connections[item.player] = set()
|
||||
self.stale[item.player] = True
|
||||
|
||||
@unique
|
||||
class RegionType(int, Enum):
|
||||
@@ -853,9 +809,8 @@ class Region(object):
|
||||
|
||||
def can_fill(self, item: Item):
|
||||
inside_dungeon_item = item.locked_dungeon_item
|
||||
sewer_hack = self.world.mode[item.player] == 'standard' and item.name == 'Small Key (Hyrule Castle)'
|
||||
if sewer_hack or inside_dungeon_item:
|
||||
return self.dungeon and self.dungeon.is_dungeon_item(item) and item.player == self.player
|
||||
if inside_dungeon_item:
|
||||
return self.dungeon.is_dungeon_item(item) and item.player == self.player
|
||||
|
||||
return True
|
||||
|
||||
@@ -1104,7 +1059,7 @@ class Item():
|
||||
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
|
||||
|
||||
|
||||
class Spoiler(object):
|
||||
class Spoiler():
|
||||
world: MultiWorld
|
||||
|
||||
def __init__(self, world):
|
||||
@@ -1130,8 +1085,8 @@ class Spoiler(object):
|
||||
def parse_data(self):
|
||||
self.medallions = OrderedDict()
|
||||
for player in self.world.get_game_players("A Link to the Past"):
|
||||
self.medallions[f'Misery Mire ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][0]
|
||||
self.medallions[f'Turtle Rock ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][1]
|
||||
self.medallions[f'Misery Mire ({self.world.get_player_name(player)})'] = self.world.required_medallions[player][0]
|
||||
self.medallions[f'Turtle Rock ({self.world.get_player_name(player)})'] = self.world.required_medallions[player][1]
|
||||
|
||||
self.startinventory = list(map(str, self.world.precollected_items))
|
||||
|
||||
@@ -1236,10 +1191,8 @@ class Spoiler(object):
|
||||
'tile_shuffle': self.world.tile_shuffle,
|
||||
'bush_shuffle': self.world.bush_shuffle,
|
||||
'beemizer': self.world.beemizer,
|
||||
'progressive': self.world.progressive,
|
||||
'shufflepots': self.world.shufflepots,
|
||||
'players': self.world.players,
|
||||
'teams': self.world.teams,
|
||||
'progression_balancing': self.world.progression_balancing,
|
||||
'triforce_pieces_available': self.world.triforce_pieces_available,
|
||||
'triforce_pieces_required': self.world.triforce_pieces_required,
|
||||
@@ -1259,7 +1212,7 @@ class Spoiler(object):
|
||||
out['Starting Inventory'] = self.startinventory
|
||||
out['Special'] = self.medallions
|
||||
if self.hashes:
|
||||
out['Hashes'] = {f"{self.world.player_names[player][team]} (Team {team+1})": hash for (player, team), hash in self.hashes.items()}
|
||||
out['Hashes'] = self.hashes
|
||||
if self.shops:
|
||||
out['Shops'] = self.shops
|
||||
out['playthrough'] = self.playthrough
|
||||
@@ -1270,7 +1223,6 @@ class Spoiler(object):
|
||||
return json.dumps(out)
|
||||
|
||||
def to_file(self, filename):
|
||||
import Options
|
||||
self.parse_data()
|
||||
|
||||
def bool_to_text(variable: Union[bool, str]) -> str:
|
||||
@@ -1284,27 +1236,24 @@ class Spoiler(object):
|
||||
self.metadata['version'], self.world.seed))
|
||||
outfile.write('Filling Algorithm: %s\n' % self.world.algorithm)
|
||||
outfile.write('Players: %d\n' % self.world.players)
|
||||
outfile.write('Teams: %d\n' % self.world.teams)
|
||||
|
||||
for player in range(1, self.world.players + 1):
|
||||
if self.world.players > 1:
|
||||
outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_names(player)))
|
||||
outfile.write('Game: %s\n' % self.metadata['game'][player])
|
||||
outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_name(player)))
|
||||
outfile.write('Game: %s\n' % self.world.game[player])
|
||||
if self.world.players > 1:
|
||||
outfile.write('Progression Balanced: %s\n' % (
|
||||
'Yes' if self.metadata['progression_balancing'][player] else 'No'))
|
||||
outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player])
|
||||
options = self.world.worlds[player].options
|
||||
if options:
|
||||
for f_option in options:
|
||||
for f_option, option in options.items():
|
||||
res = getattr(self.world, f_option)[player]
|
||||
outfile.write(f'{f_option+":":33}{bool_to_text(res) if type(res) == Options.Toggle else res.get_option_name()}\n')
|
||||
displayname = getattr(option, "displayname", f_option)
|
||||
outfile.write(f'{displayname + ":":33}{res.get_current_option_name()}\n')
|
||||
|
||||
if player in self.world.get_game_players("A Link to the Past"):
|
||||
for team in range(self.world.teams):
|
||||
outfile.write('%s%s\n' % (
|
||||
f"Hash - {self.world.player_names[player][team]} (Team {team + 1}): " if
|
||||
(player in self.world.get_game_players("A Link to the Past") and self.world.teams > 1) else 'Hash: ',
|
||||
self.hashes[player, team]))
|
||||
outfile.write('%s%s\n' % ('Hash: ', self.hashes[player]))
|
||||
|
||||
outfile.write('Logic: %s\n' % self.metadata['logic'][player])
|
||||
outfile.write('Dark Room Logic: %s\n' % self.metadata['dark_room_logic'][player])
|
||||
@@ -1323,7 +1272,6 @@ class Spoiler(object):
|
||||
self.metadata["triforce_pieces_required"][player])
|
||||
outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player])
|
||||
outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player])
|
||||
outfile.write('Item Progression: %s\n' % self.metadata['progressive'][player])
|
||||
outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player])
|
||||
if self.metadata['shuffle'][player] != "vanilla":
|
||||
outfile.write('Entrance Shuffle Seed %s\n' % self.metadata['er_seeds'][player])
|
||||
@@ -1366,7 +1314,7 @@ class Spoiler(object):
|
||||
self.metadata['shuffle_prizes'][player])
|
||||
if self.entrances:
|
||||
outfile.write('\n\nEntrances:\n\n')
|
||||
outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: '
|
||||
outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_name(entry["player"])}: '
|
||||
if self.world.players > 1 else '', entry['entrance'],
|
||||
'<=>' if entry['direction'] == 'both' else
|
||||
'<=' if entry['direction'] == 'exit' else '=>',
|
||||
@@ -1380,7 +1328,7 @@ class Spoiler(object):
|
||||
if factorio_players:
|
||||
outfile.write('\n\nRecipes:\n')
|
||||
for player in factorio_players:
|
||||
name = self.world.get_player_names(player)
|
||||
name = self.world.get_player_name(player)
|
||||
for recipe in self.world.worlds[player].custom_recipes.values():
|
||||
outfile.write(f"\n{recipe.name} ({name}): {recipe.ingredients} -> {recipe.products}")
|
||||
|
||||
@@ -1398,7 +1346,7 @@ class Spoiler(object):
|
||||
for player in self.world.get_game_players("A Link to the Past"):
|
||||
if self.world.boss_shuffle[player] != 'none':
|
||||
bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses
|
||||
outfile.write(f'\n\nBosses{(f" ({self.world.get_player_names(player)})" if self.world.players > 1 else "")}:\n')
|
||||
outfile.write(f'\n\nBosses{(f" ({self.world.get_player_name(player)})" if self.world.players > 1 else "")}:\n')
|
||||
outfile.write(' '+'\n '.join([f'{x}: {y}' for x, y in bossmap.items()]))
|
||||
outfile.write('\n\nPlaythrough:\n\n')
|
||||
outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join([' %s: %s' % (location, item) for (location, item) in sphere.items()] if sphere_nr != '0' else [f' {item}' for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()]))
|
||||
|
||||
@@ -14,6 +14,7 @@ from worlds import network_data_package, AutoWorldRegister
|
||||
|
||||
logger = logging.getLogger("Client")
|
||||
|
||||
|
||||
class ClientCommandProcessor(CommandProcessor):
|
||||
def __init__(self, ctx: CommonContext):
|
||||
self.ctx = ctx
|
||||
@@ -49,7 +50,7 @@ class ClientCommandProcessor(CommandProcessor):
|
||||
"""List all missing location checks, from your local game state"""
|
||||
count = 0
|
||||
checked_count = 0
|
||||
for location, location_id in AutoWorldRegister.world_types[self.ctx.game].item_name_to_id.items():
|
||||
for location, location_id in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id.items():
|
||||
if location_id < 0:
|
||||
continue
|
||||
if location_id not in self.ctx.locations_checked:
|
||||
@@ -68,8 +69,6 @@ class ClientCommandProcessor(CommandProcessor):
|
||||
self.output("No missing location checks found.")
|
||||
return True
|
||||
|
||||
|
||||
|
||||
def _cmd_ready(self):
|
||||
self.ctx.ready = not self.ctx.ready
|
||||
if self.ctx.ready:
|
||||
@@ -83,11 +82,13 @@ class ClientCommandProcessor(CommandProcessor):
|
||||
def default(self, raw: str):
|
||||
asyncio.create_task(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]))
|
||||
|
||||
|
||||
class CommonContext():
|
||||
starting_reconnect_delay = 5
|
||||
current_reconnect_delay = starting_reconnect_delay
|
||||
command_processor = ClientCommandProcessor
|
||||
game: None
|
||||
ui: None
|
||||
|
||||
def __init__(self, server_address, password):
|
||||
# server state
|
||||
@@ -140,16 +141,17 @@ class CommonContext():
|
||||
if local_package and local_package["version"] > network_data_package["version"]:
|
||||
data_package: dict = local_package
|
||||
elif network: # check if data from server is newer
|
||||
for key, value in data_package.items():
|
||||
if type(value) == dict: # convert to int keys
|
||||
data_package[key] = \
|
||||
{int(subkey) if subkey.isdigit() else subkey: subvalue for subkey, subvalue in value.items()}
|
||||
|
||||
if data_package["version"] > network_data_package["version"]:
|
||||
Utils.persistent_store("datapackage", "latest", network_data_package)
|
||||
|
||||
item_lookup: dict = data_package["lookup_any_item_id_to_name"]
|
||||
locations_lookup: dict = data_package["lookup_any_location_id_to_name"]
|
||||
item_lookup: dict = {}
|
||||
locations_lookup: dict = {}
|
||||
for game, gamedata in data_package["games"].items():
|
||||
for item_name, item_id in gamedata["item_name_to_id"].items():
|
||||
item_lookup[item_id] = item_name
|
||||
for location_name, location_id in gamedata["location_name_to_id"].items():
|
||||
locations_lookup[location_id] = location_name
|
||||
|
||||
def get_item_name_from_id(code: int):
|
||||
return item_lookup.get(code, f'Unknown item (ID:{code})')
|
||||
@@ -196,7 +198,7 @@ class CommonContext():
|
||||
self.input_requests += 1
|
||||
return await self.input_queue.get()
|
||||
|
||||
async def connect(self, address= None):
|
||||
async def connect(self, address=None):
|
||||
await self.disconnect()
|
||||
self.server_task = asyncio.create_task(server_loop(self, address))
|
||||
|
||||
@@ -204,7 +206,15 @@ class CommonContext():
|
||||
logger.info(args["text"])
|
||||
|
||||
def on_print_json(self, args: dict):
|
||||
logger.info(self.jsontotextparser(args["data"]))
|
||||
if self.ui:
|
||||
self.ui.print_json(args["data"])
|
||||
else:
|
||||
text = self.jsontotextparser(args["data"])
|
||||
logger.info(text)
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
"""For custom package handling in subclasses."""
|
||||
pass
|
||||
|
||||
|
||||
async def server_loop(ctx: CommonContext, address=None):
|
||||
@@ -284,8 +294,10 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
logger.info('Password required')
|
||||
logger.info(f"Forfeit setting: {args['forfeit_mode']}")
|
||||
logger.info(f"Remaining setting: {args['remaining_mode']}")
|
||||
logger.info(f"A !hint costs {args['hint_cost']}% of checks points and you get {args['location_check_points']}"
|
||||
f" for each location checked. Use !hint for more information.")
|
||||
logger.info(
|
||||
f"A !hint costs {args['hint_cost']}% of your total location count as points"
|
||||
f" and you get {args['location_check_points']}"
|
||||
f" for each location checked. Use !hint for more information.")
|
||||
ctx.hint_cost = int(args['hint_cost'])
|
||||
ctx.check_points = int(args['location_check_points'])
|
||||
ctx.forfeit_mode = args['forfeit_mode']
|
||||
@@ -388,9 +400,14 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
elif cmd == 'InvalidPacket':
|
||||
logger.warning(f"Invalid Packet of {args['type']}: {args['text']}")
|
||||
|
||||
elif cmd == "Bounced":
|
||||
pass
|
||||
|
||||
else:
|
||||
logger.debug(f"unknown command {cmd}")
|
||||
|
||||
ctx.on_package(cmd, args)
|
||||
|
||||
|
||||
async def console_loop(ctx: CommonContext):
|
||||
import sys
|
||||
@@ -410,4 +427,4 @@ async def console_loop(ctx: CommonContext):
|
||||
if input_text:
|
||||
commandprocessor(input_text)
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
logger.exception(e)
|
||||
|
||||
@@ -16,121 +16,22 @@ from MultiServer import mark_raw
|
||||
|
||||
import Utils
|
||||
import random
|
||||
from NetUtils import RawJSONtoTextParser, NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePart
|
||||
from NetUtils import NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePart
|
||||
|
||||
from worlds.factorio.Technologies import lookup_id_to_name
|
||||
from worlds.factorio import Factorio
|
||||
|
||||
os.makedirs("logs", exist_ok=True)
|
||||
|
||||
# Log to file in gui case
|
||||
if getattr(sys, "frozen", False) and not "--nogui" in sys.argv:
|
||||
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO,
|
||||
filename=os.path.join("logs", "FactorioClient.txt"), filemode="w")
|
||||
filename=os.path.join("logs", "FactorioClient.txt"), filemode="w", force=True)
|
||||
else:
|
||||
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO)
|
||||
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO, force=True)
|
||||
logging.getLogger().addHandler(logging.FileHandler(os.path.join("logs", "FactorioClient.txt"), "w"))
|
||||
|
||||
gui_enabled = Utils.is_frozen() or "--nogui" not in sys.argv
|
||||
|
||||
def get_kivy_app():
|
||||
os.environ["KIVY_NO_CONSOLELOG"] = "1"
|
||||
os.environ["KIVY_NO_FILELOG"] = "1"
|
||||
os.environ["KIVY_NO_ARGS"] = "1"
|
||||
from kivy.app import App
|
||||
from kivy.base import ExceptionHandler, ExceptionManager, Config
|
||||
from kivy.uix.gridlayout import GridLayout
|
||||
from kivy.uix.textinput import TextInput
|
||||
from kivy.uix.recycleview import RecycleView
|
||||
from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem
|
||||
from kivy.lang import Builder
|
||||
|
||||
|
||||
class FactorioManager(App):
|
||||
def __init__(self, ctx):
|
||||
super(FactorioManager, self).__init__()
|
||||
self.ctx = ctx
|
||||
self.commandprocessor = ctx.command_processor(ctx)
|
||||
self.icon = r"data/icon.png"
|
||||
|
||||
def build(self):
|
||||
self.grid = GridLayout()
|
||||
self.grid.cols = 1
|
||||
|
||||
self.tabs = TabbedPanel()
|
||||
self.tabs.default_tab_text = "All"
|
||||
self.title = "Archipelago Factorio Client"
|
||||
pairs = [
|
||||
("Client", "Archipelago"),
|
||||
("FactorioServer", "Factorio Server Log"),
|
||||
("FactorioWatcher", "Bridge Data Log"),
|
||||
]
|
||||
self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name) for logger_name, name in pairs))
|
||||
for logger_name, display_name in pairs:
|
||||
bridge_logger = logging.getLogger(logger_name)
|
||||
panel = TabbedPanelItem(text=display_name)
|
||||
panel.content = UILog(bridge_logger)
|
||||
self.tabs.add_widget(panel)
|
||||
|
||||
self.grid.add_widget(self.tabs)
|
||||
textinput = TextInput(size_hint_y=None, height=30, multiline=False)
|
||||
textinput.bind(on_text_validate=self.on_message)
|
||||
self.grid.add_widget(textinput)
|
||||
self.commandprocessor("/help")
|
||||
return self.grid
|
||||
|
||||
def on_stop(self):
|
||||
self.ctx.exit_event.set()
|
||||
|
||||
def on_message(self, textinput: TextInput):
|
||||
try:
|
||||
input_text = textinput.text.strip()
|
||||
textinput.text = ""
|
||||
|
||||
if self.ctx.input_requests > 0:
|
||||
self.ctx.input_requests -= 1
|
||||
self.ctx.input_queue.put_nowait(input_text)
|
||||
elif input_text:
|
||||
self.commandprocessor(input_text)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
def on_address(self, text: str):
|
||||
print(text)
|
||||
|
||||
|
||||
class LogtoUI(logging.Handler):
|
||||
def __init__(self, on_log):
|
||||
super(LogtoUI, self).__init__(logging.DEBUG)
|
||||
self.on_log = on_log
|
||||
|
||||
def handle(self, record: logging.LogRecord) -> None:
|
||||
self.on_log(record)
|
||||
|
||||
|
||||
class UILog(RecycleView):
|
||||
cols = 1
|
||||
|
||||
def __init__(self, *loggers_to_handle, **kwargs):
|
||||
super(UILog, self).__init__(**kwargs)
|
||||
self.data = []
|
||||
for logger in loggers_to_handle:
|
||||
logger.addHandler(LogtoUI(self.on_log))
|
||||
|
||||
def on_log(self, record: logging.LogRecord) -> None:
|
||||
self.data.append({"text": record.getMessage()})
|
||||
|
||||
|
||||
class E(ExceptionHandler):
|
||||
def handle_exception(self, inst):
|
||||
logger.exception(inst)
|
||||
return ExceptionManager.RAISE
|
||||
|
||||
|
||||
ExceptionManager.add_handler(E())
|
||||
|
||||
Config.set("input", "mouse", "mouse,disable_multitouch")
|
||||
Builder.load_file(Utils.local_path("data", "client.kv"))
|
||||
return FactorioManager
|
||||
|
||||
class FactorioCommandProcessor(ClientCommandProcessor):
|
||||
ctx: FactorioContext
|
||||
@@ -139,38 +40,44 @@ class FactorioCommandProcessor(ClientCommandProcessor):
|
||||
def _cmd_factorio(self, text: str) -> bool:
|
||||
"""Send the following command to the bound Factorio Server."""
|
||||
if self.ctx.rcon_client:
|
||||
# TODO: Print the command non-silently only for race seeds, or otherwise block anything but /factorio /save in race seeds.
|
||||
self.ctx.print_to_game(f"/factorio {text}")
|
||||
result = self.ctx.rcon_client.send_command(text)
|
||||
if result:
|
||||
self.output(result)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _cmd_connect(self, address: str = "") -> bool:
|
||||
"""Connect to a MultiWorld Server"""
|
||||
if not self.ctx.auth:
|
||||
if self.ctx.rcon_client:
|
||||
get_info(self.ctx, self.ctx.rcon_client) # retrieve current auth code
|
||||
else:
|
||||
self.output("Cannot connect to a server with unknown own identity, bridge to Factorio first.")
|
||||
return super(FactorioCommandProcessor, self)._cmd_connect(address)
|
||||
def _cmd_resync(self):
|
||||
"""Manually trigger a resync."""
|
||||
self.ctx.awaiting_bridge = True
|
||||
|
||||
|
||||
class FactorioContext(CommonContext):
|
||||
command_processor = FactorioCommandProcessor
|
||||
game = "Factorio"
|
||||
|
||||
# updated by spinup server
|
||||
mod_version: Utils.Version = Utils.Version(0, 0, 0)
|
||||
|
||||
def __init__(self, server_address, password):
|
||||
super(FactorioContext, self).__init__(server_address, password)
|
||||
self.send_index = 0
|
||||
self.rcon_client = None
|
||||
self.awaiting_bridge = False
|
||||
self.raw_json_text_parser = RawJSONtoTextParser(self)
|
||||
self.factorio_json_text_parser = FactorioJSONtoTextParser(self)
|
||||
|
||||
async def server_auth(self, password_requested):
|
||||
if password_requested and not self.password:
|
||||
await super(FactorioContext, self).server_auth(password_requested)
|
||||
|
||||
if not self.auth:
|
||||
if self.rcon_client:
|
||||
get_info(self, self.rcon_client) # retrieve current auth code
|
||||
else:
|
||||
raise Exception("Cannot connect to a server with unknown own identity, "
|
||||
"bridge to Factorio first.")
|
||||
|
||||
await self.send_msgs([{"cmd": 'Connect',
|
||||
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
|
||||
'tags': ['AP'],
|
||||
@@ -180,23 +87,34 @@ class FactorioContext(CommonContext):
|
||||
def on_print(self, args: dict):
|
||||
logger.info(args["text"])
|
||||
if self.rcon_client:
|
||||
cleaned_text = args['text'].replace('"', '')
|
||||
self.rcon_client.send_command(f"/sc game.print(\"[font=default-large-bold]Archipelago:[/font] "
|
||||
f"{cleaned_text}\")")
|
||||
self.print_to_game(args['text'])
|
||||
|
||||
def on_print_json(self, args: dict):
|
||||
text = self.raw_json_text_parser(copy.deepcopy(args["data"]))
|
||||
logger.info(text)
|
||||
if self.rcon_client:
|
||||
text = self.factorio_json_text_parser(args["data"])
|
||||
cleaned_text = text.replace('"', '')
|
||||
self.rcon_client.send_command(f"/sc game.print(\"[font=default-large-bold]Archipelago:[/font] "
|
||||
f"{cleaned_text}\")")
|
||||
text = self.factorio_json_text_parser(copy.deepcopy(args["data"]))
|
||||
self.print_to_game(text)
|
||||
super(FactorioContext, self).on_print_json(args)
|
||||
|
||||
@property
|
||||
def savegame_name(self) -> str:
|
||||
return f"AP_{self.seed_name}_{self.auth}.zip"
|
||||
|
||||
def print_to_game(self, text):
|
||||
# TODO: remove around version 0.2
|
||||
if self.mod_version < Utils.Version(0, 1, 6):
|
||||
text = text.replace('"', '')
|
||||
self.rcon_client.send_command(f"/sc game.print(\"[font=default-large-bold]Archipelago:[/font] "
|
||||
f"{text}\")")
|
||||
else:
|
||||
self.rcon_client.send_command(f"/ap-print [font=default-large-bold]Archipelago:[/font] "
|
||||
f"{text}")
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd == "Connected":
|
||||
# catch up sync anything that is already cleared.
|
||||
for tech in args["checked_locations"]:
|
||||
item_name = f"ap-{tech}-"
|
||||
self.rcon_client.send_command(f'/ap-get-technology {item_name}\t-1')
|
||||
|
||||
async def game_watcher(ctx: FactorioContext):
|
||||
bridge_logger = logging.getLogger("FactorioWatcher")
|
||||
@@ -207,9 +125,9 @@ async def game_watcher(ctx: FactorioContext):
|
||||
ctx.awaiting_bridge = False
|
||||
data = json.loads(ctx.rcon_client.send_command("/ap-sync"))
|
||||
if data["slot_name"] != ctx.auth:
|
||||
logger.warning(f"Connected World is not the expected one {data['slot_name']} != {ctx.auth}")
|
||||
bridge_logger.warning(f"Connected World is not the expected one {data['slot_name']} != {ctx.auth}")
|
||||
elif data["seed_name"] != ctx.seed_name:
|
||||
logger.warning(
|
||||
bridge_logger.warning(
|
||||
f"Connected Multiworld is not the expected one {data['seed_name']} != {ctx.seed_name}")
|
||||
else:
|
||||
data = data["info"]
|
||||
@@ -276,20 +194,23 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
||||
factorio_server_logger.info(msg)
|
||||
if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg:
|
||||
ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
|
||||
# trigger lua interface confirmation
|
||||
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
|
||||
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
|
||||
# TODO: remove around version 0.2
|
||||
if ctx.mod_version < Utils.Version(0, 1, 6):
|
||||
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
|
||||
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
|
||||
|
||||
if not ctx.awaiting_bridge and "Archipelago Bridge Data available for game tick " in msg:
|
||||
ctx.awaiting_bridge = True
|
||||
|
||||
if ctx.rcon_client:
|
||||
while ctx.send_index < len(ctx.items_received):
|
||||
transfer_item: NetworkItem = ctx.items_received[ctx.send_index]
|
||||
item_id = transfer_item.item
|
||||
player_name = ctx.player_names[transfer_item.player]
|
||||
if item_id not in lookup_id_to_name:
|
||||
logging.error(f"Cannot send unknown item ID: {item_id}")
|
||||
if item_id not in Factorio.item_id_to_name:
|
||||
factorio_server_logger.error(f"Cannot send unknown item ID: {item_id}")
|
||||
else:
|
||||
item_name = lookup_id_to_name[item_id]
|
||||
item_name = Factorio.item_id_to_name[item_id]
|
||||
factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.")
|
||||
ctx.rcon_client.send_command(f'/ap-get-technology {item_name}\t{ctx.send_index}\t{player_name}')
|
||||
ctx.send_index += 1
|
||||
@@ -335,19 +256,24 @@ async def factorio_spinup_server(ctx: FactorioContext):
|
||||
while not factorio_queue.empty():
|
||||
msg = factorio_queue.get()
|
||||
factorio_server_logger.info(msg)
|
||||
if "Loading mod AP-" in msg and msg.endswith("(data.lua)"):
|
||||
parts = msg.split()
|
||||
ctx.mod_version = Utils.Version(*(int(number) for number in parts[-2].split(".")))
|
||||
if not rcon_client and "Starting RCON interface at IP ADDR:" in msg:
|
||||
rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
|
||||
get_info(ctx, rcon_client)
|
||||
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
if ctx.mod_version == ctx.__class__.mod_version:
|
||||
raise Exception("No Archipelago mod was loaded. Aborting.")
|
||||
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
logging.error("Aborted Factorio Server Bridge")
|
||||
ctx.exit_event.set()
|
||||
|
||||
else:
|
||||
logger.info(f"Got World Information from AP Mod for seed {ctx.seed_name} in slot {ctx.auth}")
|
||||
logger.info(f"Got World Information from AP Mod {tuple(ctx.mod_version)} for seed {ctx.seed_name} in slot {ctx.auth}")
|
||||
|
||||
finally:
|
||||
factorio_process.terminate()
|
||||
@@ -359,8 +285,9 @@ async def main(args):
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||
if gui_enabled:
|
||||
input_task = None
|
||||
ui_app = get_kivy_app()(ctx)
|
||||
ui_task = asyncio.create_task(ui_app.async_run(), name="UI")
|
||||
from kvui import FactorioManager
|
||||
ctx.ui = FactorioManager(ctx)
|
||||
ui_task = asyncio.create_task(ctx.ui.async_run(), name="UI")
|
||||
else:
|
||||
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
||||
ui_task = None
|
||||
@@ -378,7 +305,7 @@ async def main(args):
|
||||
|
||||
if ctx.server and not ctx.server.socket.closed:
|
||||
await ctx.server.socket.close()
|
||||
if ctx.server_task is not None:
|
||||
if ctx.server_task:
|
||||
await ctx.server_task
|
||||
|
||||
while ctx.input_requests > 0:
|
||||
@@ -411,7 +338,7 @@ if __name__ == '__main__':
|
||||
|
||||
parser = argparse.ArgumentParser(description="Optional arguments to FactorioClient follow. "
|
||||
"Remaining arguments get passed into bound Factorio instance."
|
||||
"Refer to factorio --help for those.")
|
||||
"Refer to Factorio --help for those.")
|
||||
parser.add_argument('--rcon-port', default='24242', type=int, help='Port to use to communicate with Factorio')
|
||||
parser.add_argument('--rcon-password', help='Password to authenticate with RCON.')
|
||||
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
|
||||
@@ -431,7 +358,7 @@ if __name__ == '__main__':
|
||||
if not os.path.exists(bin_dir):
|
||||
raise FileNotFoundError(f"Path {bin_dir} does not exist or could not be accessed.")
|
||||
if not os.path.isdir(bin_dir):
|
||||
raise FileNotFoundError(f"Path {bin_dir} is not a directory.")
|
||||
raise NotADirectoryError(f"Path {bin_dir} is not a directory.")
|
||||
if not os.path.exists(executable):
|
||||
if os.path.exists(executable + ".exe"):
|
||||
executable = executable + ".exe"
|
||||
|
||||
59
Fill.py
@@ -7,6 +7,7 @@ from BaseClasses import CollectionState, Location, MultiWorld
|
||||
from worlds.alttp.Items import ItemFactory
|
||||
from worlds.alttp.Regions import key_drop_data
|
||||
from worlds.generic import PlandoItem
|
||||
from worlds.AutoWorld import call_all
|
||||
|
||||
|
||||
class FillError(RuntimeError):
|
||||
@@ -69,7 +70,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations,
|
||||
itempool.extend(unplaced_items)
|
||||
|
||||
|
||||
def distribute_items_restrictive(world: MultiWorld, gftower_trash=False, fill_locations=None):
|
||||
def distribute_items_restrictive(world: MultiWorld, fill_locations=None):
|
||||
# If not passed in, then get a shuffled list of locations to fill in
|
||||
if not fill_locations:
|
||||
fill_locations = world.get_unfilled_locations()
|
||||
@@ -92,52 +93,9 @@ def distribute_items_restrictive(world: MultiWorld, gftower_trash=False, fill_lo
|
||||
else:
|
||||
restitempool.append(item)
|
||||
|
||||
standard_keyshuffle_players = set()
|
||||
|
||||
# fill in gtower locations with trash first
|
||||
for player in world.get_game_players("A Link to the Past"):
|
||||
if not gftower_trash or not world.ganonstower_vanilla[player] or \
|
||||
world.logic[player] in {'owglitches', 'hybridglitches', "nologic"}:
|
||||
gtower_trash_count = 0
|
||||
elif 'triforcehunt' in world.goal[player] and ('local' in world.goal[player] or world.players == 1):
|
||||
gtower_trash_count = world.random.randint(world.crystals_needed_for_gt[player] * 2,
|
||||
world.crystals_needed_for_gt[player] * 4)
|
||||
else:
|
||||
gtower_trash_count = world.random.randint(0, world.crystals_needed_for_gt[player] * 2)
|
||||
|
||||
if gtower_trash_count:
|
||||
gtower_locations = [location for location in fill_locations if
|
||||
'Ganons Tower' in location.name and location.player == player]
|
||||
world.random.shuffle(gtower_locations)
|
||||
trashcnt = 0
|
||||
localrest = localrestitempool[player]
|
||||
if localrest:
|
||||
gt_item_pool = restitempool + localrest
|
||||
world.random.shuffle(gt_item_pool)
|
||||
else:
|
||||
gt_item_pool = restitempool.copy()
|
||||
|
||||
while gtower_locations and gt_item_pool and trashcnt < gtower_trash_count:
|
||||
spot_to_fill = gtower_locations.pop()
|
||||
item_to_place = gt_item_pool.pop()
|
||||
if item_to_place in localrest:
|
||||
localrest.remove(item_to_place)
|
||||
else:
|
||||
restitempool.remove(item_to_place)
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
fill_locations.remove(spot_to_fill)
|
||||
trashcnt += 1
|
||||
if world.mode[player] == 'standard' and world.keyshuffle[player] is True:
|
||||
standard_keyshuffle_players.add(player)
|
||||
|
||||
|
||||
# Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots
|
||||
if standard_keyshuffle_players:
|
||||
progitempool.sort(
|
||||
key=lambda item: 1 if item.name == 'Small Key (Hyrule Castle)' and
|
||||
item.player in standard_keyshuffle_players else 0)
|
||||
|
||||
world.random.shuffle(fill_locations)
|
||||
call_all(world, "fill_hook", progitempool, nonexcludeditempool, localrestitempool, restitempool, fill_locations)
|
||||
|
||||
fill_restrictive(world, world.state, fill_locations, progitempool)
|
||||
|
||||
if nonexcludeditempool:
|
||||
@@ -168,11 +126,8 @@ def distribute_items_restrictive(world: MultiWorld, gftower_trash=False, fill_lo
|
||||
unplaced = [item for item in progitempool + restitempool]
|
||||
unfilled = [location.name for location in fill_locations]
|
||||
|
||||
for location in fill_locations:
|
||||
world.push_item(location, ItemFactory('Nothing', location.player), False)
|
||||
|
||||
if unplaced or unfilled:
|
||||
logging.warning(f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}')
|
||||
raise FillError(f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}')
|
||||
|
||||
|
||||
def fast_fill(world: MultiWorld, item_pool: typing.List, fill_locations: typing.List) -> typing.Tuple[typing.List, typing.List]:
|
||||
@@ -232,7 +187,7 @@ def flood_items(world: MultiWorld):
|
||||
location_list = world.get_reachable_locations()
|
||||
world.random.shuffle(location_list)
|
||||
for location in location_list:
|
||||
if location.item is not None and not location.item.advancement and not location.item.smallkey and not location.item.bigkey:
|
||||
if location.item is not None and not location.item.advancement:
|
||||
# safe to replace
|
||||
replace_item = location.item
|
||||
replace_item.location = None
|
||||
@@ -444,4 +399,4 @@ def distribute_planned(world: MultiWorld):
|
||||
except ValueError:
|
||||
placement.warn(f"Could not remove {item} from pool as it's already missing from it.")
|
||||
except Exception as e:
|
||||
raise Exception(f"Error running plando for player {player} ({world.player_names[player]})") from e
|
||||
raise Exception(f"Error running plando for player {player} ({world.player_name[player]})") from e
|
||||
|
||||
31
Generate.py
@@ -40,7 +40,6 @@ def mystery_argparse():
|
||||
help="Input directory for player files.")
|
||||
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
|
||||
parser.add_argument('--multi', default=defaults["players"], type=lambda value: min(max(int(value), 1), 255))
|
||||
parser.add_argument('--teams', default=1, type=lambda value: max(int(value), 1))
|
||||
parser.add_argument('--spoiler', type=int, default=defaults["spoiler"])
|
||||
parser.add_argument('--rom', default=options["lttp_options"]["rom_file"], help="Path to the 1.0 JP LttP Baserom.")
|
||||
parser.add_argument('--enemizercli', default=defaults["enemizer_path"])
|
||||
@@ -128,7 +127,6 @@ def main(args=None, callback=ERmain):
|
||||
erargs.skip_playthrough = args.spoiler < 2
|
||||
erargs.outputname = seed_name
|
||||
erargs.outputpath = args.outputpath
|
||||
erargs.teams = args.teams
|
||||
|
||||
# set up logger
|
||||
if args.log_level:
|
||||
@@ -179,6 +177,8 @@ def main(args=None, callback=ERmain):
|
||||
getattr(erargs, k)[player] = v
|
||||
except AttributeError:
|
||||
setattr(erargs, k, {player: v})
|
||||
except Exception as e:
|
||||
raise Exception(f"Error setting {k} to {v} for player {player}") from e
|
||||
except Exception as e:
|
||||
raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e
|
||||
else:
|
||||
@@ -189,8 +189,6 @@ def main(args=None, callback=ERmain):
|
||||
erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
|
||||
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
|
||||
|
||||
erargs.names = ",".join(erargs.name[i] for i in range(1, args.multi + 1))
|
||||
del (erargs.name)
|
||||
if args.yaml_output:
|
||||
import yaml
|
||||
important = {}
|
||||
@@ -267,7 +265,7 @@ def handle_name(name: str, player: int, name_counter: Counter):
|
||||
name] > 1 else ''),
|
||||
player=player,
|
||||
PLAYER=(player if player > 1 else '')))
|
||||
new_name = new_name.strip().replace(' ', '_')[:16]
|
||||
new_name = new_name.strip()[:16]
|
||||
if new_name == "Archipelago":
|
||||
raise Exception(f"You cannot name yourself \"{new_name}\"")
|
||||
return new_name
|
||||
@@ -610,7 +608,8 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
ret.enemy_damage = {None: 'default',
|
||||
'default': 'default',
|
||||
'shuffled': 'shuffled',
|
||||
'random': 'chaos'
|
||||
'random': 'chaos', # to be removed
|
||||
'chaos': 'chaos',
|
||||
}[get_choice('enemy_damage', weights)]
|
||||
|
||||
ret.enemy_health = get_choice('enemy_health', weights)
|
||||
@@ -635,8 +634,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
|
||||
ret.dungeon_counters = get_choice('dungeon_counters', weights, 'default')
|
||||
|
||||
ret.progressive = convert_to_on_off(get_choice('progressive', weights, 'on'))
|
||||
|
||||
ret.shuffle_prizes = get_choice('shuffle_prizes', weights, "g")
|
||||
|
||||
ret.required_medallions = [get_choice("misery_mire_medallion", weights, "random"),
|
||||
@@ -737,20 +734,10 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
else:
|
||||
ret.sprite_pool += [key] * int(value)
|
||||
|
||||
ret.disablemusic = get_choice('disablemusic', weights, False)
|
||||
ret.triforcehud = get_choice('triforcehud', weights, 'hide_goal')
|
||||
ret.quickswap = get_choice('quickswap', weights, True)
|
||||
ret.fastmenu = get_choice('menuspeed', weights, "normal")
|
||||
ret.reduceflashing = get_choice('reduceflashing', weights, False)
|
||||
ret.heartcolor = get_choice('heartcolor', weights, "red")
|
||||
ret.heartbeep = convert_to_on_off(get_choice('heartbeep', weights, "normal"))
|
||||
ret.ow_palettes = get_choice('ow_palettes', weights, "default")
|
||||
ret.uw_palettes = get_choice('uw_palettes', weights, "default")
|
||||
ret.hud_palettes = get_choice('hud_palettes', weights, "default")
|
||||
ret.sword_palettes = get_choice('sword_palettes', weights, "default")
|
||||
ret.shield_palettes = get_choice('shield_palettes', weights, "default")
|
||||
ret.link_palettes = get_choice('link_palettes', weights, "default")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import atexit
|
||||
confirmation = atexit.register(input, "Press enter to close.")
|
||||
main()
|
||||
# in case of error-free exit should not need confirmation
|
||||
atexit.unregister(confirmation)
|
||||
@@ -19,7 +19,7 @@ from tkinter import Tk, Frame, Label, StringVar, Entry, filedialog, messagebox,
|
||||
from urllib.parse import urlparse
|
||||
from urllib.request import urlopen
|
||||
|
||||
from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings
|
||||
from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings, get_base_rom_bytes
|
||||
from Utils import output_path, local_path, open_file
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ def main():
|
||||
help='Path to an ALttP JAP(1.0) rom to use as a base.')
|
||||
parser.add_argument('--loglevel', default='info', const='info', nargs='?',
|
||||
choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
|
||||
parser.add_argument('--fastmenu', default='normal', const='normal', nargs='?',
|
||||
parser.add_argument('--menuspeed', default='normal', const='normal', nargs='?',
|
||||
choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
|
||||
help='''\
|
||||
Select the rate at which the menu opens and closes.
|
||||
@@ -100,6 +100,7 @@ def main():
|
||||
parser.add_argument('--names', default='', type=str)
|
||||
parser.add_argument('--update_sprites', action='store_true', help='Update Sprite Database, then exit.')
|
||||
args = parser.parse_args()
|
||||
args.music = not args.disablemusic
|
||||
if args.update_sprites:
|
||||
run_sprite_update()
|
||||
sys.exit()
|
||||
@@ -150,7 +151,7 @@ def adjust(args):
|
||||
if hasattr(args, "world"):
|
||||
world = getattr(args, "world")
|
||||
|
||||
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic,
|
||||
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.menuspeed, args.music,
|
||||
args.sprite, palettes_options, reduceflashing=args.reduceflashing or racerom, world=world)
|
||||
path = output_path(f'{os.path.basename(args.rom)[:-4]}_adjusted.sfc')
|
||||
rom.write_to_file(path)
|
||||
@@ -195,14 +196,14 @@ def adjustGUI():
|
||||
guiargs = Namespace()
|
||||
guiargs.heartbeep = rom_vars.heartbeepVar.get()
|
||||
guiargs.heartcolor = rom_vars.heartcolorVar.get()
|
||||
guiargs.fastmenu = rom_vars.fastMenuVar.get()
|
||||
guiargs.menuspeed = rom_vars.menuspeedVar.get()
|
||||
guiargs.ow_palettes = rom_vars.owPalettesVar.get()
|
||||
guiargs.uw_palettes = rom_vars.uwPalettesVar.get()
|
||||
guiargs.hud_palettes = rom_vars.hudPalettesVar.get()
|
||||
guiargs.sword_palettes = rom_vars.swordPalettesVar.get()
|
||||
guiargs.shield_palettes = rom_vars.shieldPalettesVar.get()
|
||||
guiargs.quickswap = bool(rom_vars.quickSwapVar.get())
|
||||
guiargs.disablemusic = bool(rom_vars.disableMusicVar.get())
|
||||
guiargs.music = bool(rom_vars.MusicVar.get())
|
||||
guiargs.reduceflashing = bool(rom_vars.disableFlashingVar.get())
|
||||
guiargs.rom = romVar2.get()
|
||||
guiargs.baserom = romVar.get()
|
||||
@@ -221,7 +222,6 @@ def adjustGUI():
|
||||
else:
|
||||
messagebox.showinfo(title="Success", message=f"Rom patched successfully to {path}")
|
||||
from Utils import persistent_store
|
||||
from worlds.alttp.Rom import Sprite
|
||||
if isinstance(guiargs.sprite, Sprite):
|
||||
guiargs.sprite = guiargs.sprite.name
|
||||
persistent_store("adjuster", "last_settings_3", guiargs)
|
||||
@@ -411,9 +411,8 @@ def get_rom_frame(parent=None):
|
||||
|
||||
def RomSelect():
|
||||
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")])
|
||||
import Patch
|
||||
try:
|
||||
Patch.get_base_rom_bytes(rom) # throws error on checksum fail
|
||||
get_base_rom_bytes(rom) # throws error on checksum fail
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
messagebox.showerror(title="Error while reading ROM", message=str(e))
|
||||
@@ -439,9 +438,10 @@ def get_rom_options_frame(parent=None):
|
||||
romOptionsFrame.rowconfigure(i, weight=1)
|
||||
vars = Namespace()
|
||||
|
||||
vars.disableMusicVar = IntVar()
|
||||
disableMusicCheckbutton = Checkbutton(romOptionsFrame, text="Disable music", variable=vars.disableMusicVar)
|
||||
disableMusicCheckbutton.grid(row=0, column=0, sticky=E)
|
||||
vars.MusicVar = IntVar()
|
||||
vars.MusicVar.set(1)
|
||||
MusicCheckbutton = Checkbutton(romOptionsFrame, text="Music", variable=vars.MusicVar)
|
||||
MusicCheckbutton.grid(row=0, column=0, sticky=E)
|
||||
|
||||
vars.disableFlashingVar = IntVar(value=1)
|
||||
disableFlashingCheckbutton = Checkbutton(romOptionsFrame, text="Disable flashing (anti-epilepsy)", variable=vars.disableFlashingVar)
|
||||
@@ -485,14 +485,14 @@ def get_rom_options_frame(parent=None):
|
||||
quickSwapCheckbutton = Checkbutton(romOptionsFrame, text="L/R Quickswapping", variable=vars.quickSwapVar)
|
||||
quickSwapCheckbutton.grid(row=1, column=0, sticky=E)
|
||||
|
||||
fastMenuFrame = Frame(romOptionsFrame)
|
||||
fastMenuFrame.grid(row=1, column=1, sticky=E)
|
||||
fastMenuLabel = Label(fastMenuFrame, text='Menu speed')
|
||||
fastMenuLabel.pack(side=LEFT)
|
||||
vars.fastMenuVar = StringVar()
|
||||
vars.fastMenuVar.set('normal')
|
||||
fastMenuOptionMenu = OptionMenu(fastMenuFrame, vars.fastMenuVar, 'normal', 'instant', 'double', 'triple', 'quadruple', 'half')
|
||||
fastMenuOptionMenu.pack(side=LEFT)
|
||||
menuspeedFrame = Frame(romOptionsFrame)
|
||||
menuspeedFrame.grid(row=1, column=1, sticky=E)
|
||||
menuspeedLabel = Label(menuspeedFrame, text='Menu speed')
|
||||
menuspeedLabel.pack(side=LEFT)
|
||||
vars.menuspeedVar = StringVar()
|
||||
vars.menuspeedVar.set('normal')
|
||||
menuspeedOptionMenu = OptionMenu(menuspeedFrame, vars.menuspeedVar, 'normal', 'instant', 'double', 'triple', 'quadruple', 'half')
|
||||
menuspeedOptionMenu.pack(side=LEFT)
|
||||
|
||||
heartcolorFrame = Frame(romOptionsFrame)
|
||||
heartcolorFrame.grid(row=2, column=0, sticky=E)
|
||||
@@ -518,7 +518,7 @@ def get_rom_options_frame(parent=None):
|
||||
owPalettesLabel.pack(side=LEFT)
|
||||
vars.owPalettesVar = StringVar()
|
||||
vars.owPalettesVar.set('default')
|
||||
owPalettesOptionMenu = OptionMenu(owPalettesFrame, vars.owPalettesVar, 'default', 'random', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||
owPalettesOptionMenu = OptionMenu(owPalettesFrame, vars.owPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||
owPalettesOptionMenu.pack(side=LEFT)
|
||||
|
||||
uwPalettesFrame = Frame(romOptionsFrame)
|
||||
@@ -527,7 +527,7 @@ def get_rom_options_frame(parent=None):
|
||||
uwPalettesLabel.pack(side=LEFT)
|
||||
vars.uwPalettesVar = StringVar()
|
||||
vars.uwPalettesVar.set('default')
|
||||
uwPalettesOptionMenu = OptionMenu(uwPalettesFrame, vars.uwPalettesVar, 'default', 'random', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||
uwPalettesOptionMenu = OptionMenu(uwPalettesFrame, vars.uwPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||
uwPalettesOptionMenu.pack(side=LEFT)
|
||||
|
||||
hudPalettesFrame = Frame(romOptionsFrame)
|
||||
@@ -536,7 +536,7 @@ def get_rom_options_frame(parent=None):
|
||||
hudPalettesLabel.pack(side=LEFT)
|
||||
vars.hudPalettesVar = StringVar()
|
||||
vars.hudPalettesVar.set('default')
|
||||
hudPalettesOptionMenu = OptionMenu(hudPalettesFrame, vars.hudPalettesVar, 'default', 'random', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||
hudPalettesOptionMenu = OptionMenu(hudPalettesFrame, vars.hudPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||
hudPalettesOptionMenu.pack(side=LEFT)
|
||||
|
||||
swordPalettesFrame = Frame(romOptionsFrame)
|
||||
@@ -545,7 +545,7 @@ def get_rom_options_frame(parent=None):
|
||||
swordPalettesLabel.pack(side=LEFT)
|
||||
vars.swordPalettesVar = StringVar()
|
||||
vars.swordPalettesVar.set('default')
|
||||
swordPalettesOptionMenu = OptionMenu(swordPalettesFrame, vars.swordPalettesVar, 'default', 'random', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||
swordPalettesOptionMenu = OptionMenu(swordPalettesFrame, vars.swordPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||
swordPalettesOptionMenu.pack(side=LEFT)
|
||||
|
||||
shieldPalettesFrame = Frame(romOptionsFrame)
|
||||
@@ -554,7 +554,7 @@ def get_rom_options_frame(parent=None):
|
||||
shieldPalettesLabel.pack(side=LEFT)
|
||||
vars.shieldPalettesVar = StringVar()
|
||||
vars.shieldPalettesVar.set('default')
|
||||
shieldPalettesOptionMenu = OptionMenu(shieldPalettesFrame, vars.shieldPalettesVar, 'default', 'random', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||
shieldPalettesOptionMenu = OptionMenu(shieldPalettesFrame, vars.shieldPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
|
||||
shieldPalettesOptionMenu.pack(side=LEFT)
|
||||
|
||||
spritePoolFrame = Frame(romOptionsFrame)
|
||||
|
||||
103
LttPClient.py
@@ -1,6 +1,9 @@
|
||||
import argparse
|
||||
import atexit
|
||||
exit_func = atexit.register(input, "Press enter to close.")
|
||||
import threading
|
||||
import time
|
||||
import sys
|
||||
import multiprocessing
|
||||
import os
|
||||
import subprocess
|
||||
@@ -10,7 +13,6 @@ from json import loads, dumps
|
||||
|
||||
from Utils import get_item_name_from_id
|
||||
|
||||
exit_func = atexit.register(input, "Press enter to close.")
|
||||
|
||||
import ModuleUpdate
|
||||
|
||||
@@ -23,11 +25,22 @@ from NetUtils import *
|
||||
from worlds.alttp import Regions, Shops
|
||||
from worlds.alttp import Items
|
||||
import Utils
|
||||
from CommonClient import CommonContext, server_loop, logger, console_loop, ClientCommandProcessor
|
||||
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor
|
||||
|
||||
snes_logger = logging.getLogger("SNES")
|
||||
|
||||
from MultiServer import mark_raw
|
||||
|
||||
os.makedirs("logs", exist_ok=True)
|
||||
|
||||
# Log to file in gui case
|
||||
if getattr(sys, "frozen", False) and not "--nogui" in sys.argv:
|
||||
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO,
|
||||
filename=os.path.join("logs", "LttPClient.txt"), filemode="w", force=True)
|
||||
else:
|
||||
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO, force=True)
|
||||
logging.getLogger().addHandler(logging.FileHandler(os.path.join("logs", "LttPClient.txt"), "w"))
|
||||
|
||||
|
||||
class LttPCommandProcessor(ClientCommandProcessor):
|
||||
def _cmd_slow_mode(self, toggle: str = ""):
|
||||
@@ -41,7 +54,8 @@ class LttPCommandProcessor(ClientCommandProcessor):
|
||||
|
||||
@mark_raw
|
||||
def _cmd_snes(self, snes_address: str = "") -> bool:
|
||||
"""Connect to a snes. Optionally include network address of a snes to connect to, otherwise show available devices"""
|
||||
"""Connect to a snes.
|
||||
Optionally include network address of a snes to connect to, otherwise show available devices"""
|
||||
self.ctx.snes_reconnect_address = None
|
||||
asyncio.create_task(snes_connect(self.ctx, snes_address if snes_address else self.ctx.snes_address))
|
||||
return True
|
||||
@@ -71,6 +85,7 @@ class Context(CommonContext):
|
||||
self.snes_recv_queue = asyncio.Queue()
|
||||
self.snes_request_lock = asyncio.Lock()
|
||||
self.snes_write_buffer = []
|
||||
self.snes_connector_lock = threading.Lock()
|
||||
|
||||
self.awaiting_rom = False
|
||||
self.rom = None
|
||||
@@ -91,7 +106,7 @@ class Context(CommonContext):
|
||||
await super(Context, self).server_auth(password_requested)
|
||||
if self.rom is None:
|
||||
self.awaiting_rom = True
|
||||
logger.info(
|
||||
snes_logger.info(
|
||||
'No ROM detected, awaiting snes connection to authenticate to the multiworld server (/snes)')
|
||||
return
|
||||
self.awaiting_rom = False
|
||||
@@ -420,19 +435,21 @@ def launch_sni(ctx: Context):
|
||||
sni_path = os.path.join(sni_path, file)
|
||||
|
||||
if os.path.isfile(sni_path):
|
||||
logger.info(f"Attempting to start {sni_path}")
|
||||
snes_logger.info(f"Attempting to start {sni_path}")
|
||||
import subprocess
|
||||
subprocess.Popen(sni_path, cwd=os.path.dirname(sni_path), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
if Utils.is_frozen(): # if it spawns a visible console, may as well populate it
|
||||
subprocess.Popen(sni_path, cwd=os.path.dirname(sni_path))
|
||||
else:
|
||||
subprocess.Popen(sni_path, cwd=os.path.dirname(sni_path), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
else:
|
||||
logger.info(
|
||||
snes_logger.info(
|
||||
f"Attempt to start SNI was aborted as path {sni_path} was not found, "
|
||||
f"please start it yourself if it is not running")
|
||||
|
||||
|
||||
async def _snes_connect(ctx: Context, address: str):
|
||||
address = f"ws://{address}" if "://" not in address else address
|
||||
|
||||
logger.info("Connecting to SNI at %s ..." % address)
|
||||
snes_logger.info("Connecting to SNI at %s ..." % address)
|
||||
seen_problems = set()
|
||||
succesful = False
|
||||
while not succesful:
|
||||
@@ -444,7 +461,7 @@ async def _snes_connect(ctx: Context, address: str):
|
||||
# only tell the user about new problems, otherwise silently lay in wait for a working connection
|
||||
if problem not in seen_problems:
|
||||
seen_problems.add(problem)
|
||||
logger.error(f"Error connecting to SNI ({problem})")
|
||||
snes_logger.error(f"Error connecting to SNI ({problem})")
|
||||
|
||||
if len(seen_problems) == 1:
|
||||
# this is the first problem. Let's try launching SNI if it isn't already running
|
||||
@@ -467,7 +484,7 @@ async def get_snes_devices(ctx: Context):
|
||||
devices = reply['Results'] if 'Results' in reply and len(reply['Results']) > 0 else None
|
||||
|
||||
if not devices:
|
||||
logger.info('No SNES device found. Please connect a SNES device to SNI.')
|
||||
snes_logger.info('No SNES device found. Please connect a SNES device to SNI.')
|
||||
while not devices:
|
||||
await asyncio.sleep(1)
|
||||
await socket.send(dumps(DeviceList_Request))
|
||||
@@ -482,7 +499,10 @@ async def get_snes_devices(ctx: Context):
|
||||
async def snes_connect(ctx: Context, address):
|
||||
global SNES_RECONNECT_DELAY
|
||||
if ctx.snes_socket is not None and ctx.snes_state == SNESState.SNES_CONNECTED:
|
||||
logger.error('Already connected to snes')
|
||||
if ctx.rom:
|
||||
snes_logger.error('Already connected to SNES, with rom loaded.')
|
||||
else:
|
||||
snes_logger.error('Already connected to SNI, likely awaiting a device.')
|
||||
return
|
||||
|
||||
recv_task = None
|
||||
@@ -505,7 +525,7 @@ async def snes_connect(ctx: Context, address):
|
||||
await snes_disconnect(ctx)
|
||||
return
|
||||
|
||||
logger.info("Attaching to " + device)
|
||||
snes_logger.info("Attaching to " + device)
|
||||
|
||||
Attach_Request = {
|
||||
"Opcode": "Attach",
|
||||
@@ -518,6 +538,7 @@ async def snes_connect(ctx: Context, address):
|
||||
ctx.snes_reconnect_address = address
|
||||
recv_task = asyncio.create_task(snes_recv_loop(ctx))
|
||||
SNES_RECONNECT_DELAY = ctx.starting_reconnect_delay
|
||||
snes_logger.info(f"Attached to {device}")
|
||||
|
||||
except Exception as e:
|
||||
if recv_task is not None:
|
||||
@@ -530,9 +551,9 @@ async def snes_connect(ctx: Context, address):
|
||||
ctx.snes_socket = None
|
||||
ctx.snes_state = SNESState.SNES_DISCONNECTED
|
||||
if not ctx.snes_reconnect_address:
|
||||
logger.error("Error connecting to snes (%s)" % e)
|
||||
snes_logger.error("Error connecting to snes (%s)" % e)
|
||||
else:
|
||||
logger.error(f"Error connecting to snes, attempt again in {SNES_RECONNECT_DELAY}s")
|
||||
snes_logger.error(f"Error connecting to snes, attempt again in {SNES_RECONNECT_DELAY}s")
|
||||
asyncio.create_task(snes_autoreconnect(ctx))
|
||||
SNES_RECONNECT_DELAY *= 2
|
||||
|
||||
@@ -559,11 +580,11 @@ async def snes_recv_loop(ctx: Context):
|
||||
try:
|
||||
async for msg in ctx.snes_socket:
|
||||
ctx.snes_recv_queue.put_nowait(msg)
|
||||
logger.warning("Snes disconnected")
|
||||
snes_logger.warning("Snes disconnected")
|
||||
except Exception as e:
|
||||
if not isinstance(e, websockets.WebSocketException):
|
||||
logger.exception(e)
|
||||
logger.error("Lost connection to the snes, type /snes to reconnect")
|
||||
snes_logger.exception(e)
|
||||
snes_logger.error("Lost connection to the snes, type /snes to reconnect")
|
||||
finally:
|
||||
socket, ctx.snes_socket = ctx.snes_socket, None
|
||||
if socket is not None and not socket.closed:
|
||||
@@ -576,7 +597,7 @@ async def snes_recv_loop(ctx: Context):
|
||||
ctx.rom = None
|
||||
|
||||
if ctx.snes_reconnect_address:
|
||||
logger.info(f"...reconnecting in {SNES_RECONNECT_DELAY}s")
|
||||
snes_logger.info(f"...reconnecting in {SNES_RECONNECT_DELAY}s")
|
||||
asyncio.create_task(snes_autoreconnect(ctx))
|
||||
|
||||
|
||||
@@ -605,10 +626,10 @@ async def snes_read(ctx: Context, address, size):
|
||||
break
|
||||
|
||||
if len(data) != size:
|
||||
logger.error('Error reading %s, requested %d bytes, received %d' % (hex(address), size, len(data)))
|
||||
snes_logger.error('Error reading %s, requested %d bytes, received %d' % (hex(address), size, len(data)))
|
||||
if len(data):
|
||||
logger.error(str(data))
|
||||
logger.warning('Communication Failure with SNI')
|
||||
snes_logger.error(str(data))
|
||||
snes_logger.warning('Communication Failure with SNI')
|
||||
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
|
||||
await ctx.snes_socket.close()
|
||||
return None
|
||||
@@ -634,7 +655,7 @@ async def snes_write(ctx: Context, write_list):
|
||||
await ctx.snes_socket.send(dumps(PutAddress_Request))
|
||||
await ctx.snes_socket.send(data)
|
||||
else:
|
||||
logger.warning(f"Could not send data to SNES: {data}")
|
||||
snes_logger.warning(f"Could not send data to SNES: {data}")
|
||||
except websockets.ConnectionClosed:
|
||||
return False
|
||||
|
||||
@@ -673,7 +694,7 @@ async def track_locations(ctx: Context, roomid, roomdata):
|
||||
new_locations.append(location_id)
|
||||
ctx.locations_checked.add(location_id)
|
||||
location = ctx.location_name_getter(location_id)
|
||||
logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
||||
snes_logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
||||
|
||||
try:
|
||||
if roomid in location_shop_ids:
|
||||
@@ -682,7 +703,7 @@ async def track_locations(ctx: Context, roomid, roomdata):
|
||||
if int(b) and (Shops.SHOP_ID_START + cnt) not in ctx.locations_checked:
|
||||
new_check(Shops.SHOP_ID_START + cnt)
|
||||
except Exception as e:
|
||||
logger.info(f"Exception: {e}")
|
||||
snes_logger.info(f"Exception: {e}")
|
||||
|
||||
for location_id, (loc_roomid, loc_mask) in location_table_uw_id.items():
|
||||
try:
|
||||
@@ -691,7 +712,7 @@ async def track_locations(ctx: Context, roomid, roomdata):
|
||||
roomdata << 4) & loc_mask != 0:
|
||||
new_check(location_id)
|
||||
except Exception as e:
|
||||
logger.exception(f"Exception: {e}")
|
||||
snes_logger.exception(f"Exception: {e}")
|
||||
|
||||
uw_begin = 0x129
|
||||
ow_end = uw_end = 0
|
||||
@@ -774,7 +795,7 @@ async def game_watcher(ctx: Context):
|
||||
await ctx.server_auth(False)
|
||||
|
||||
if ctx.auth and ctx.auth != ctx.rom:
|
||||
logger.warning("ROM change detected, please reconnect to the multiworld server")
|
||||
snes_logger.warning("ROM change detected, please reconnect to the multiworld server")
|
||||
await ctx.disconnect()
|
||||
|
||||
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
|
||||
@@ -857,6 +878,8 @@ async def main():
|
||||
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
|
||||
parser.add_argument('--founditems', default=False, action='store_true',
|
||||
help='Show items found by other players for themselves.')
|
||||
if not Utils.is_frozen(): # Frozen state has no cmd window in the first place
|
||||
parser.add_argument('--nogui', default=False, action='store_true', help="Turns off Client GUI.")
|
||||
args = parser.parse_args()
|
||||
logging.basicConfig(format='%(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
|
||||
if args.diff_file:
|
||||
@@ -875,22 +898,32 @@ async def main():
|
||||
asyncio.create_task(run_game(adjustedromfile if adjusted else romfile))
|
||||
|
||||
ctx = Context(args.snes, args.connect, args.password)
|
||||
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
||||
|
||||
if ctx.server_task is None:
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||
asyncio.create_task(snes_connect(ctx, ctx.snes_address))
|
||||
|
||||
if Utils.is_frozen() or "--nogui" not in sys.argv:
|
||||
input_task = None
|
||||
from kvui import LttPManager
|
||||
ctx.ui = LttPManager(ctx)
|
||||
ui_task = asyncio.create_task(ctx.ui.async_run(), name="UI")
|
||||
else:
|
||||
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
||||
ui_task = None
|
||||
|
||||
snes_connect_task = asyncio.create_task(snes_connect(ctx, ctx.snes_address))
|
||||
watcher_task = asyncio.create_task(game_watcher(ctx), name="GameWatcher")
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
if snes_connect_task:
|
||||
snes_connect_task.cancel()
|
||||
ctx.server_address = None
|
||||
ctx.snes_reconnect_address = None
|
||||
|
||||
await watcher_task
|
||||
|
||||
if ctx.server is not None and not ctx.server.socket.closed:
|
||||
if ctx.server and not ctx.server.socket.closed:
|
||||
await ctx.server.socket.close()
|
||||
if ctx.server_task is not None:
|
||||
if ctx.server_task:
|
||||
await ctx.server_task
|
||||
|
||||
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
|
||||
@@ -900,7 +933,11 @@ async def main():
|
||||
ctx.input_queue.put_nowait(None)
|
||||
ctx.input_requests -= 1
|
||||
|
||||
await input_task
|
||||
if ui_task:
|
||||
await ui_task
|
||||
|
||||
if input_task:
|
||||
input_task.cancel()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
195
Main.py
@@ -10,18 +10,15 @@ import tempfile
|
||||
import zipfile
|
||||
from typing import Dict, Tuple
|
||||
|
||||
from BaseClasses import MultiWorld, CollectionState, Region, Item
|
||||
from BaseClasses import MultiWorld, CollectionState, Region, RegionType
|
||||
from worlds.alttp.Items import item_name_groups
|
||||
from worlds.alttp.Regions import lookup_vanilla_location_to_entrance
|
||||
from worlds.alttp.Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, get_hash_string
|
||||
from worlds.alttp.Dungeons import fill_dungeons, fill_dungeons_restrictive
|
||||
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned
|
||||
from worlds.alttp.Shops import ShopSlotFill, SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
|
||||
from worlds.alttp.ItemPool import difficulties, fill_prizes
|
||||
from Utils import output_path, parse_player_names, get_options, __version__, version_tuple
|
||||
from worlds.alttp.ItemPool import difficulties
|
||||
from Utils import output_path, get_options, __version__, version_tuple
|
||||
from worlds.generic.Rules import locality_rules, exclusion_rules
|
||||
from worlds import AutoWorld
|
||||
import Patch
|
||||
|
||||
seeddigits = 20
|
||||
|
||||
@@ -67,7 +64,6 @@ def main(args, seed=None):
|
||||
world.difficulty = args.difficulty.copy()
|
||||
world.item_functionality = args.item_functionality.copy()
|
||||
world.timer = args.timer.copy()
|
||||
world.progressive = args.progressive.copy()
|
||||
world.goal = args.goal.copy()
|
||||
world.local_items = args.local_items.copy()
|
||||
if hasattr(args, "algorithm"): # current GUI options
|
||||
@@ -100,7 +96,6 @@ def main(args, seed=None):
|
||||
world.blue_clock_time = args.blue_clock_time.copy()
|
||||
world.green_clock_time = args.green_clock_time.copy()
|
||||
world.shufflepots = args.shufflepots.copy()
|
||||
world.progressive = args.progressive.copy()
|
||||
world.dungeon_counters = args.dungeon_counters.copy()
|
||||
world.glitch_boots = args.glitch_boots.copy()
|
||||
world.triforce_pieces_available = args.triforce_pieces_available.copy()
|
||||
@@ -118,6 +113,10 @@ def main(args, seed=None):
|
||||
world.required_medallions = args.required_medallions.copy()
|
||||
world.game = args.game.copy()
|
||||
world.set_options(args)
|
||||
world.player_name = args.name.copy()
|
||||
world.alttp_rom = args.rom
|
||||
world.enemizer = args.enemizercli
|
||||
world.sprite = args.sprite.copy()
|
||||
world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
|
||||
|
||||
world.slot_seeds = {player: random.Random(world.random.getrandbits(64)) for player in
|
||||
@@ -149,13 +148,6 @@ def main(args, seed=None):
|
||||
for name, cls in AutoWorld.AutoWorldRegister.world_types.items():
|
||||
logger.info(f" {name:{longest_name}}: {len(cls.item_names):3} Items | {len(cls.location_names):3} Locations")
|
||||
|
||||
parsed_names = parse_player_names(args.names, world.players, args.teams)
|
||||
world.teams = len(parsed_names)
|
||||
for i, team in enumerate(parsed_names, 1):
|
||||
if world.players > 1:
|
||||
logger.info('%s%s', 'Team%d: ' % i if world.teams > 1 else 'Players: ', ', '.join(team))
|
||||
for player, name in enumerate(team, 1):
|
||||
world.player_names[player].append(name)
|
||||
|
||||
logger.info('')
|
||||
for player in world.get_game_players("A Link to the Past"):
|
||||
@@ -218,29 +210,16 @@ def main(args, seed=None):
|
||||
|
||||
distribute_planned(world)
|
||||
|
||||
logger.info('Placing Dungeon Prizes.')
|
||||
logger.info('Running Pre Main Fill.')
|
||||
|
||||
fill_prizes(world)
|
||||
|
||||
logger.info('Placing Dungeon Items.')
|
||||
|
||||
if world.algorithm in ['balanced', 'vt26'] or any(
|
||||
list(args.mapshuffle.values()) + list(args.compassshuffle.values()) +
|
||||
list(args.keyshuffle.values()) + list(args.bigkeyshuffle.values())):
|
||||
fill_dungeons_restrictive(world)
|
||||
else:
|
||||
fill_dungeons(world)
|
||||
AutoWorld.call_all(world, "pre_fill")
|
||||
|
||||
logger.info('Fill the world.')
|
||||
|
||||
if world.algorithm == 'flood':
|
||||
flood_items(world) # different algo, biased towards early game progress items
|
||||
elif world.algorithm == 'vt25':
|
||||
distribute_items_restrictive(world, False)
|
||||
elif world.algorithm == 'vt26':
|
||||
distribute_items_restrictive(world, True)
|
||||
elif world.algorithm == 'balanced':
|
||||
distribute_items_restrictive(world, True)
|
||||
distribute_items_restrictive(world)
|
||||
|
||||
logger.info("Filling Shop Slots")
|
||||
|
||||
@@ -251,115 +230,18 @@ def main(args, seed=None):
|
||||
|
||||
logger.info('Generating output files.')
|
||||
outfilebase = 'AP_' + world.seed_name
|
||||
rom_names = []
|
||||
|
||||
def _gen_rom(team: int, player: int, output_directory:str):
|
||||
use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player]
|
||||
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
|
||||
or world.shufflepots[player] or world.bush_shuffle[player]
|
||||
or world.killable_thieves[player])
|
||||
|
||||
rom = LocalRom(args.rom)
|
||||
|
||||
patch_rom(world, rom, player, team, use_enemizer)
|
||||
|
||||
if use_enemizer:
|
||||
patch_enemizer(world, team, player, rom, args.enemizercli, output_directory)
|
||||
|
||||
if args.race:
|
||||
patch_race_rom(rom, world, player)
|
||||
|
||||
world.spoiler.hashes[(player, team)] = get_hash_string(rom.hash)
|
||||
|
||||
palettes_options = {}
|
||||
palettes_options['dungeon'] = args.uw_palettes[player]
|
||||
palettes_options['overworld'] = args.ow_palettes[player]
|
||||
palettes_options['hud'] = args.hud_palettes[player]
|
||||
palettes_options['sword'] = args.sword_palettes[player]
|
||||
palettes_options['shield'] = args.shield_palettes[player]
|
||||
palettes_options['link'] = args.link_palettes[player]
|
||||
|
||||
apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player],
|
||||
args.fastmenu[player], args.disablemusic[player], args.sprite[player],
|
||||
palettes_options, world, player, True,
|
||||
reduceflashing=args.reduceflashing[player] or args.race,
|
||||
triforcehud=args.triforcehud[player])
|
||||
|
||||
mcsb_name = ''
|
||||
if all([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
|
||||
world.bigkeyshuffle[player]]):
|
||||
mcsb_name = '-keysanity'
|
||||
elif [world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
|
||||
world.bigkeyshuffle[player]].count(True) == 1:
|
||||
mcsb_name = '-mapshuffle' if world.mapshuffle[player] else \
|
||||
'-compassshuffle' if world.compassshuffle[player] else \
|
||||
'-universal_keys' if world.keyshuffle[player] == "universal" else \
|
||||
'-keyshuffle' if world.keyshuffle[player] else '-bigkeyshuffle'
|
||||
elif any([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
|
||||
world.bigkeyshuffle[player]]):
|
||||
mcsb_name = '-%s%s%s%sshuffle' % (
|
||||
'M' if world.mapshuffle[player] else '', 'C' if world.compassshuffle[player] else '',
|
||||
'U' if world.keyshuffle[player] == "universal" else 'S' if world.keyshuffle[player] else '',
|
||||
'B' if world.bigkeyshuffle[player] else '')
|
||||
|
||||
outfilepname = f'_P{player}'
|
||||
outfilepname += f"_{world.player_names[player][team].replace(' ', '_')}" \
|
||||
if world.player_names[player][team] != 'Player%d' % player else ''
|
||||
outfilestuffs = {
|
||||
"logic": world.logic[player], # 0
|
||||
"difficulty": world.difficulty[player], # 1
|
||||
"item_functionality": world.item_functionality[player], # 2
|
||||
"mode": world.mode[player], # 3
|
||||
"goal": world.goal[player], # 4
|
||||
"timer": str(world.timer[player]), # 5
|
||||
"shuffle": world.shuffle[player], # 6
|
||||
"algorithm": world.algorithm, # 7
|
||||
"mscb": mcsb_name, # 8
|
||||
"retro": world.retro[player], # 9
|
||||
"progressive": world.progressive, # A
|
||||
"hints": 'True' if world.hints[player] else 'False' # B
|
||||
}
|
||||
# 0 1 2 3 4 5 6 7 8 9 A B
|
||||
outfilesuffix = ('_%s_%s-%s-%s-%s%s_%s-%s%s%s%s%s' % (
|
||||
# 0 1 2 3 4 5 6 7 8 9 A B C
|
||||
# _noglitches_normal-normal-open-ganon-ohko_simple-balanced-keysanity-retro-prog_random-nohints
|
||||
# _noglitches_normal-normal-open-ganon _simple-balanced-keysanity-retro
|
||||
# _noglitches_normal-normal-open-ganon _simple-balanced-keysanity -prog_random
|
||||
# _noglitches_normal-normal-open-ganon _simple-balanced-keysanity -nohints
|
||||
outfilestuffs["logic"], # 0
|
||||
|
||||
outfilestuffs["difficulty"], # 1
|
||||
outfilestuffs["item_functionality"], # 2
|
||||
outfilestuffs["mode"], # 3
|
||||
outfilestuffs["goal"], # 4
|
||||
"" if outfilestuffs["timer"] in ['False', 'none', 'display'] else "-" + outfilestuffs["timer"], # 5
|
||||
|
||||
outfilestuffs["shuffle"], # 6
|
||||
outfilestuffs["algorithm"], # 7
|
||||
outfilestuffs["mscb"], # 8
|
||||
|
||||
"-retro" if outfilestuffs["retro"] == "True" else "", # 9
|
||||
"-prog_" + outfilestuffs["progressive"] if outfilestuffs["progressive"] in ['off', 'random'] else "", # A
|
||||
"-nohints" if not outfilestuffs["hints"] == "True" else "") # B
|
||||
) if not args.outputname else ''
|
||||
rompath = os.path.join(output_directory, f'{outfilebase}{outfilepname}{outfilesuffix}.sfc')
|
||||
rom.write_to_file(rompath, hide_enemizer=True)
|
||||
Patch.create_patch_file(rompath, player=player, player_name=world.player_names[player][team])
|
||||
os.unlink(rompath)
|
||||
return player, team, bytes(rom.name)
|
||||
|
||||
pool = concurrent.futures.ThreadPoolExecutor()
|
||||
|
||||
output = tempfile.TemporaryDirectory()
|
||||
with output as temp_dir:
|
||||
check_accessibility_task = pool.submit(world.fulfills_accessibility)
|
||||
rom_futures = []
|
||||
|
||||
output_file_futures = []
|
||||
for team in range(world.teams):
|
||||
for player in world.get_game_players("A Link to the Past"):
|
||||
rom_futures.append(pool.submit(_gen_rom, team, player, temp_dir))
|
||||
|
||||
for player in world.player_ids:
|
||||
output_file_futures.append(pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
|
||||
output_file_futures.append(pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir))
|
||||
|
||||
def get_entrance_to_region(region: Region):
|
||||
for entrance in region.entrances:
|
||||
@@ -371,7 +253,7 @@ def main(args, seed=None):
|
||||
# collect ER hint info
|
||||
er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if
|
||||
world.shuffle[player] != "vanilla" or world.retro[player]}
|
||||
from worlds.alttp.Regions import RegionType
|
||||
|
||||
for region in world.regions:
|
||||
if region.player in er_hint_data and region.locations:
|
||||
main_entrance = get_entrance_to_region(region)
|
||||
@@ -427,12 +309,8 @@ def main(args, seed=None):
|
||||
|
||||
FillDisabledShopSlots(world)
|
||||
|
||||
def write_multidata(roms, outputs):
|
||||
import base64
|
||||
def write_multidata():
|
||||
import NetUtils
|
||||
for future in roms:
|
||||
rom_name = future.result()
|
||||
rom_names.append(rom_name)
|
||||
slot_data = {}
|
||||
client_versions = {}
|
||||
minimum_versions = {"server": (0, 1, 1), "clients": client_versions}
|
||||
@@ -440,8 +318,6 @@ def main(args, seed=None):
|
||||
for slot in world.player_ids:
|
||||
client_versions[slot] = world.worlds[slot].get_required_client_version()
|
||||
games[slot] = world.game[slot]
|
||||
connect_names = {base64.b64encode(rom_name).decode(): (team, slot) for
|
||||
slot, team, rom_name in rom_names}
|
||||
precollected_items = {player: [] for player in range(1, world.players + 1)}
|
||||
for item in world.precollected_items:
|
||||
precollected_items[item.player].append(item.code)
|
||||
@@ -452,11 +328,6 @@ def main(args, seed=None):
|
||||
if world.tech_tree_information[player].value == 2:
|
||||
sending_visible_players.add(player)
|
||||
|
||||
for i, team in enumerate(parsed_names):
|
||||
for player, name in enumerate(team, 1):
|
||||
if player not in world.get_game_players("A Link to the Past"):
|
||||
connect_names[name] = (i, player)
|
||||
|
||||
for slot in world.player_ids:
|
||||
slot_data[slot] = world.worlds[slot].fill_slot_data()
|
||||
|
||||
@@ -476,11 +347,11 @@ def main(args, seed=None):
|
||||
precollected_hints[location.player].add(hint)
|
||||
precollected_hints[location.item.player].add(hint)
|
||||
|
||||
multidata = zlib.compress(pickle.dumps({
|
||||
multidata = {
|
||||
"slot_data": slot_data,
|
||||
"games": games,
|
||||
"names": parsed_names,
|
||||
"connect_names": connect_names,
|
||||
"names": [[name for player, name in sorted(world.player_name.items())]],
|
||||
"connect_names": {name: (0, player) for player, name in world.player_name.items()},
|
||||
"remote_items": {player for player in world.player_ids if
|
||||
world.worlds[player].remote_items},
|
||||
"locations": locations_data,
|
||||
@@ -493,15 +364,17 @@ def main(args, seed=None):
|
||||
"tags": ["AP"],
|
||||
"minimum_versions": minimum_versions,
|
||||
"seed_name": world.seed_name
|
||||
}), 9)
|
||||
}
|
||||
AutoWorld.call_all(world, "modify_multidata", multidata)
|
||||
|
||||
with open(os.path.join(temp_dir, '%s.archipelago' % outfilebase), 'wb') as f:
|
||||
multidata = zlib.compress(pickle.dumps(multidata), 9)
|
||||
|
||||
with open(os.path.join(temp_dir, f'{outfilebase}.archipelago'), 'wb') as f:
|
||||
f.write(bytes([1])) # version of format
|
||||
f.write(multidata)
|
||||
for future in outputs:
|
||||
future.result() # collect errors if they occured
|
||||
|
||||
multidata_task = pool.submit(write_multidata, rom_futures, output_file_futures)
|
||||
|
||||
multidata_task = pool.submit(write_multidata)
|
||||
if not check_accessibility_task.result():
|
||||
if not world.can_beat_game():
|
||||
raise Exception("Game appears as unbeatable. Aborting.")
|
||||
@@ -513,8 +386,10 @@ def main(args, seed=None):
|
||||
if not args.skip_playthrough:
|
||||
logger.info('Calculating playthrough.')
|
||||
create_playthrough(world)
|
||||
if args.create_spoiler: # needs spoiler.hashes to be filled, that depend on rom_futures being done
|
||||
if args.create_spoiler:
|
||||
world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
|
||||
for future in output_file_futures:
|
||||
future.result()
|
||||
zipfilename = output_path(f"AP_{world.seed_name}.zip")
|
||||
logger.info(f'Creating final archive at {zipfilename}.')
|
||||
with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED,
|
||||
@@ -642,17 +517,13 @@ def create_playthrough(world):
|
||||
sphere if location.player == player})
|
||||
if player in world.get_game_players("A Link to the Past"):
|
||||
for path in dict(world.spoiler.paths).values():
|
||||
if any(exit == 'Pyramid Fairy' for (_, exit) in path):
|
||||
if any(exit_path == 'Pyramid Fairy' for (_, exit_path) in path):
|
||||
if world.mode[player] != 'inverted':
|
||||
world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = get_path(state,
|
||||
world.get_region(
|
||||
'Big Bomb Shop',
|
||||
player))
|
||||
world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = \
|
||||
get_path(state,world.get_region('Big Bomb Shop', player))
|
||||
else:
|
||||
world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = get_path(state,
|
||||
world.get_region(
|
||||
'Inverted Big Bomb Shop',
|
||||
player))
|
||||
world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = \
|
||||
get_path(state,world.get_region('Inverted Big Bomb Shop', player))
|
||||
|
||||
# we can finally output our playthrough
|
||||
world.spoiler.playthrough = {"0": sorted([str(item) for item in world.precollected_items if item.advancement])}
|
||||
|
||||
180
MinecraftClient.py
Normal file
@@ -0,0 +1,180 @@
|
||||
import argparse
|
||||
import os, sys
|
||||
import re
|
||||
import atexit
|
||||
from subprocess import Popen
|
||||
from shutil import copyfile
|
||||
from base64 import b64decode
|
||||
from time import strftime
|
||||
|
||||
import requests
|
||||
|
||||
import Utils
|
||||
|
||||
atexit.register(input, "Press enter to exit.")
|
||||
|
||||
# 1 or more digits followed by m or g, then optional b
|
||||
max_heap_re = re.compile(r"^\d+[mMgG][bB]?$")
|
||||
|
||||
|
||||
def prompt_yes_no(prompt):
|
||||
yes_inputs = {'yes', 'ye', 'y'}
|
||||
no_inputs = {'no', 'n'}
|
||||
while True:
|
||||
choice = input(prompt + " [y/n] ").lower()
|
||||
if choice in yes_inputs:
|
||||
return True
|
||||
elif choice in no_inputs:
|
||||
return False
|
||||
else:
|
||||
print('Please respond with "y" or "n".')
|
||||
|
||||
|
||||
# Find Forge jar file; raise error if not found
|
||||
def find_forge_jar(forge_dir):
|
||||
for entry in os.scandir(forge_dir):
|
||||
if ".jar" in entry.name and "forge" in entry.name:
|
||||
print(f"Found forge .jar: {entry.name}")
|
||||
return entry.name
|
||||
raise FileNotFoundError(f"Could not find forge .jar in {forge_dir}.")
|
||||
|
||||
|
||||
# Create mods folder if needed; find AP randomizer jar; return None if not found.
|
||||
def find_ap_randomizer_jar(forge_dir):
|
||||
mods_dir = os.path.join(forge_dir, 'mods')
|
||||
if os.path.isdir(mods_dir):
|
||||
ap_mod_re = re.compile(r"^aprandomizer-[\d\.]+\.jar$")
|
||||
for entry in os.scandir(mods_dir):
|
||||
match = ap_mod_re.match(entry.name)
|
||||
if match:
|
||||
print(f"Found AP randomizer mod: {match.group()}")
|
||||
return match.group()
|
||||
return None
|
||||
else:
|
||||
os.mkdir(mods_dir)
|
||||
print(f"Created mods folder in {forge_dir}")
|
||||
return None
|
||||
|
||||
|
||||
# Create APData folder if needed; clean .apmc files from APData; copy given .apmc into directory.
|
||||
def replace_apmc_files(forge_dir, apmc_file):
|
||||
if apmc_file is None:
|
||||
return
|
||||
apdata_dir = os.path.join(forge_dir, 'APData')
|
||||
if not os.path.isdir(apdata_dir):
|
||||
os.mkdir(apdata_dir)
|
||||
print(f"Created APData folder in {forge_dir}")
|
||||
for entry in os.scandir(apdata_dir):
|
||||
if ".apmc" in entry.name and entry.is_file():
|
||||
os.remove(entry.path)
|
||||
print(f"Removed {entry.name} in {apdata_dir}")
|
||||
copyfile(apmc_file, os.path.join(apdata_dir, os.path.basename(apmc_file)))
|
||||
print(f"Copied {os.path.basename(apmc_file)} to {apdata_dir}")
|
||||
|
||||
|
||||
# Check mod version, download new mod from GitHub releases page if needed.
|
||||
def update_mod(forge_dir):
|
||||
ap_randomizer = find_ap_randomizer_jar(forge_dir)
|
||||
|
||||
client_releases_endpoint = "https://api.github.com/repos/KonoTyran/Minecraft_AP_Randomizer/releases"
|
||||
resp = requests.get(client_releases_endpoint)
|
||||
if resp.status_code == 200: # OK
|
||||
latest_release = resp.json()[0]
|
||||
if ap_randomizer != latest_release['assets'][0]['name']:
|
||||
print(f"A new release of the Minecraft AP randomizer mod was found: {latest_release['assets'][0]['name']}")
|
||||
if ap_randomizer is not None:
|
||||
print(f"Your current mod is {ap_randomizer}.")
|
||||
else:
|
||||
print(f"You do not have the AP randomizer mod installed.")
|
||||
if prompt_yes_no("Would you like to update?"):
|
||||
old_ap_mod = os.path.join(forge_dir, 'mods', ap_randomizer) if ap_randomizer is not None else None
|
||||
new_ap_mod = os.path.join(forge_dir, 'mods', latest_release['assets'][0]['name'])
|
||||
print("Downloading AP randomizer mod. This may take a moment...")
|
||||
apmod_resp = requests.get(latest_release['assets'][0]['browser_download_url'])
|
||||
if apmod_resp.status_code == 200:
|
||||
with open(new_ap_mod, 'wb') as f:
|
||||
f.write(apmod_resp.content)
|
||||
print(f"Wrote new mod file to {new_ap_mod}")
|
||||
if old_ap_mod is not None:
|
||||
os.remove(old_ap_mod)
|
||||
print(f"Removed old mod file from {old_ap_mod}")
|
||||
else:
|
||||
print(f"Error retrieving the randomizer mod (status code {apmod_resp.status_code}).")
|
||||
print(f"Please report this issue on the Archipelago Discord server.")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f"Error checking for randomizer mod updates (status code {resp.status_code}).")
|
||||
print(f"If this was not expected, please report this issue on the Archipelago Discord server.")
|
||||
if not prompt_yes_no("Continue anyways?"):
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
# Check if the EULA is agreed to, and prompt the user to read and agree if necessary.
|
||||
def check_eula(forge_dir):
|
||||
eula_path = os.path.join(forge_dir, "eula.txt")
|
||||
if not os.path.isfile(eula_path):
|
||||
# Create eula.txt
|
||||
with open(eula_path, 'w') as f:
|
||||
f.write("#By changing the setting below to TRUE you are indicating your agreement to our EULA (https://account.mojang.com/documents/minecraft_eula).\n")
|
||||
f.write(f"#{strftime('%a %b %d %X %Z %Y')}\n")
|
||||
f.write("eula=false\n")
|
||||
with open(eula_path, 'r+') as f:
|
||||
text = f.read()
|
||||
if 'false' in text:
|
||||
# Prompt user to agree to the EULA
|
||||
print("You need to agree to the Minecraft EULA in order to run the server.")
|
||||
print("The EULA can be found at https://account.mojang.com/documents/minecraft_eula")
|
||||
if prompt_yes_no("Do you agree to the EULA?"):
|
||||
f.seek(0)
|
||||
f.write(text.replace('false', 'true'))
|
||||
f.truncate()
|
||||
print(f"Set {eula_path} to true")
|
||||
else:
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
# Run the Forge server. Return process object
|
||||
def run_forge_server(forge_dir, heap_arg):
|
||||
forge_server = find_forge_jar(forge_dir)
|
||||
|
||||
java_exe = os.path.abspath(os.path.join('jre8', 'bin', 'java.exe'))
|
||||
if not os.path.isfile(java_exe):
|
||||
java_exe = "java" # try to fall back on java in the PATH
|
||||
|
||||
heap_arg = max_heap_re.match(max_heap).group()
|
||||
if heap_arg[-1] in ['b', 'B']:
|
||||
heap_arg = heap_arg[:-1]
|
||||
heap_arg = "-Xmx" + heap_arg
|
||||
|
||||
argstring = ' '.join([java_exe, heap_arg, "-jar", forge_server, "-nogui"])
|
||||
print(f"Running Forge server: {argstring}")
|
||||
os.chdir(forge_dir)
|
||||
return Popen(argstring)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("apmc_file", default=None, nargs='?', help="Path to an Archipelago Minecraft data file (.apmc)")
|
||||
|
||||
args = parser.parse_args()
|
||||
options = Utils.get_options()
|
||||
|
||||
apmc_file = os.path.abspath(args.apmc_file)
|
||||
forge_dir = options["minecraft_options"]["forge_directory"]
|
||||
max_heap = options["minecraft_options"]["max_heap_size"]
|
||||
|
||||
# Change to executable's working directory
|
||||
os.chdir(os.path.abspath(os.path.dirname(sys.argv[0])))
|
||||
|
||||
if apmc_file is not None and not os.path.isfile(apmc_file):
|
||||
raise FileNotFoundError(f"Path {apmc_file} does not exist or could not be accessed.")
|
||||
if not os.path.isdir(forge_dir):
|
||||
raise NotADirectoryError(f"Path {forge_dir} does not exist or could not be accessed.")
|
||||
if not max_heap_re.match(max_heap):
|
||||
raise Exception(f"Max heap size {max_heap} in incorrect format. Use a number followed by M or G, e.g. 512M or 2G.")
|
||||
|
||||
update_mod(forge_dir)
|
||||
replace_apmc_files(forge_dir, apmc_file)
|
||||
check_eula(forge_dir)
|
||||
server_process = run_forge_server(forge_dir, max_heap)
|
||||
server_process.wait()
|
||||
@@ -23,10 +23,13 @@ def update_command():
|
||||
subprocess.call([sys.executable, '-m', 'pip', 'install', '-r', file, '--upgrade'])
|
||||
|
||||
|
||||
def update():
|
||||
def update(yes = False, force = False):
|
||||
global update_ran
|
||||
if not update_ran:
|
||||
update_ran = True
|
||||
if force:
|
||||
update_command()
|
||||
return
|
||||
for req_file in requirements_files:
|
||||
path = os.path.join(os.path.dirname(sys.argv[0]), req_file)
|
||||
if not os.path.exists(path):
|
||||
@@ -38,12 +41,19 @@ def update():
|
||||
try:
|
||||
pkg_resources.require(requirement)
|
||||
except pkg_resources.ResolutionError:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
input(f'Requirement {requirement} is not satisfied, press enter to install it')
|
||||
if not yes:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
input(f'Requirement {requirement} is not satisfied, press enter to install it')
|
||||
update_command()
|
||||
return
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
update()
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description='Install archipelago requirements')
|
||||
parser.add_argument('-y', '--yes', dest='yes', action='store_true', help='answer "yes" to all questions')
|
||||
parser.add_argument('-f', '--force', dest='force', action='store_true', help='force update')
|
||||
args = parser.parse_args()
|
||||
|
||||
update(args.yes, args.force)
|
||||
|
||||
@@ -111,6 +111,7 @@ class Context(Node):
|
||||
self.games: typing.Dict[int, str] = {}
|
||||
self.minimum_client_versions: typing.Dict[int, Utils.Version] = {}
|
||||
self.seed_name = ""
|
||||
self.random = random.Random()
|
||||
|
||||
def get_hint_cost(self, slot):
|
||||
if self.hint_cost:
|
||||
@@ -118,10 +119,20 @@ class Context(Node):
|
||||
return 0
|
||||
|
||||
def load(self, multidatapath: str, use_embedded_server_options: bool = False):
|
||||
with open(multidatapath, 'rb') as f:
|
||||
data = f.read()
|
||||
if multidatapath.lower().endswith(".zip"):
|
||||
import zipfile
|
||||
with zipfile.ZipFile(multidatapath) as zf:
|
||||
for file in zf.namelist():
|
||||
if file.endswith(".archipelago"):
|
||||
data = zf.read(file)
|
||||
break
|
||||
else:
|
||||
raise Exception("No .archipelago found in archive.")
|
||||
else:
|
||||
with open(multidatapath, 'rb') as f:
|
||||
data = f.read()
|
||||
|
||||
self._load(self._decompress(data), use_embedded_server_options)
|
||||
self._load(self._decompress(data), use_embedded_server_options)
|
||||
self.data_filename = multidatapath
|
||||
|
||||
@staticmethod
|
||||
@@ -147,6 +158,7 @@ class Context(Node):
|
||||
self.player_names[team, player] = name
|
||||
self.player_name_lookup[name] = team, player
|
||||
self.seed_name = decoded_obj["seed_name"]
|
||||
self.random.seed(self.seed_name)
|
||||
self.connect_names = decoded_obj['connect_names']
|
||||
self.remote_items = decoded_obj['remote_items']
|
||||
self.locations = decoded_obj['locations']
|
||||
@@ -213,8 +225,10 @@ class Context(Node):
|
||||
self.saving = enabled
|
||||
if self.saving:
|
||||
if not self.save_filename:
|
||||
self.save_filename = (self.data_filename[:-11] if self.data_filename.endswith('.archipelago') else (
|
||||
self.data_filename + '_')) + 'apsave'
|
||||
import os
|
||||
name, ext = os.path.splitext(self.data_filename)
|
||||
self.save_filename = name + '.apsave' if ext.lower() in ('.archipelago','.zip') \
|
||||
else self.data_filename + '_' + 'apsave'
|
||||
try:
|
||||
with open(self.save_filename, 'rb') as f:
|
||||
save_data = restricted_loads(zlib.decompress(f.read()))
|
||||
@@ -263,6 +277,7 @@ class Context(Node):
|
||||
(key, value.timestamp()) for key, value in self.client_activity_timers.items()),
|
||||
"client_connection_timers": tuple(
|
||||
(key, value.timestamp()) for key, value in self.client_connection_timers.items()),
|
||||
"random_state": self.random.getstate()
|
||||
}
|
||||
|
||||
return d
|
||||
@@ -283,7 +298,8 @@ class Context(Node):
|
||||
{tuple(key): datetime.datetime.fromtimestamp(value, datetime.timezone.utc) for key, value
|
||||
in savedata["client_activity_timers"]})
|
||||
self.location_checks.update(savedata["location_checks"])
|
||||
|
||||
if "random_state" in savedata:
|
||||
self.random.setstate(savedata["random_state"])
|
||||
logging.info(f'Loaded save file with {sum([len(p) for p in self.received_items.values()])} received items '
|
||||
f'for {len(self.received_items)} players')
|
||||
|
||||
@@ -409,7 +425,8 @@ async def on_client_joined(ctx: Context, client: Client):
|
||||
update_client_status(ctx, client, ClientStatus.CLIENT_CONNECTED)
|
||||
version_str = '.'.join(str(x) for x in client.version)
|
||||
ctx.notify_all(
|
||||
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has joined the game. "
|
||||
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) "
|
||||
f"playing {ctx.games[client.slot]} has joined. "
|
||||
f"Client({version_str}), {client.tags}).")
|
||||
|
||||
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
|
||||
@@ -884,15 +901,18 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
|
||||
@mark_raw
|
||||
def _cmd_hint(self, item_or_location: str = "") -> bool:
|
||||
"""Use !hint {item_name/location_name}, for example !hint Lamp or !hint Link's House. """
|
||||
"""Use !hint {item_name/location_name},
|
||||
for example !hint Lamp or !hint Link's House to get a spoiler peek for that location or item.
|
||||
If hint costs are on, this will only give you one new result,
|
||||
you can rerun the command to get more in that case."""
|
||||
points_available = get_client_points(self.ctx, self.client)
|
||||
if not item_or_location:
|
||||
self.output(f"A hint costs {self.ctx.get_hint_cost(self.client.slot)} points. "
|
||||
f"You have {points_available} points.")
|
||||
hints = {hint.re_check(self.ctx, self.client.team) for hint in
|
||||
self.ctx.hints[self.client.team, self.client.slot]}
|
||||
self.ctx.hints[self.client.team, self.client.slot] = hints
|
||||
notify_hints(self.ctx, self.client.team, list(hints))
|
||||
self.output(f"A hint costs {self.ctx.get_hint_cost(self.client.slot)} points. "
|
||||
f"You have {points_available} points.")
|
||||
return True
|
||||
else:
|
||||
world = proxy_worlds[self.ctx.games[self.client.slot]]
|
||||
@@ -924,11 +944,11 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
if not not_found_hints: # everything's been found, no need to pay
|
||||
can_pay = 1000
|
||||
elif cost:
|
||||
can_pay = points_available // cost
|
||||
can_pay = int((points_available // cost) > 0) # limit to 1 new hint per call
|
||||
else:
|
||||
can_pay = 1000
|
||||
|
||||
random.shuffle(not_found_hints)
|
||||
self.ctx.random.shuffle(not_found_hints)
|
||||
|
||||
hints = found_hints
|
||||
while can_pay > 0:
|
||||
@@ -946,7 +966,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
if not_found_hints:
|
||||
if hints:
|
||||
self.output(
|
||||
"Could not pay for everything. Rerun the hint later with more points to get the remaining hints.")
|
||||
"There may be more hintables, you can rerun the command to find more.")
|
||||
else:
|
||||
self.output(f"You can't afford the hint. "
|
||||
f"You have {points_available} points and need at least "
|
||||
@@ -1098,13 +1118,26 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
elif cmd == 'StatusUpdate':
|
||||
update_client_status(ctx, client, args["status"])
|
||||
|
||||
if cmd == 'Say':
|
||||
elif cmd == 'Say':
|
||||
if "text" not in args or type(args["text"]) is not str or not args["text"].isprintable():
|
||||
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'Say'}])
|
||||
return
|
||||
|
||||
client.messageprocessor(args["text"])
|
||||
|
||||
elif cmd == "Bounce":
|
||||
games = set(args.get("games", []))
|
||||
tags = set(args.get("tags", []))
|
||||
slots = set(args.get("slots", []))
|
||||
args["cmd"] = "Bounced"
|
||||
msg = ctx.dumper([args])
|
||||
|
||||
for bounceclient in ctx.endpoints:
|
||||
if client.team == bounceclient.team and (ctx.games[bounceclient.slot] in games or
|
||||
set(bounceclient.tags) & tags or
|
||||
bounceclient.slot in slots):
|
||||
await ctx.send_encoded_msgs(bounceclient, msg)
|
||||
|
||||
|
||||
def update_client_status(ctx: Context, client: Client, new_status: ClientStatus):
|
||||
current = ctx.client_game_state[client.team, client.slot]
|
||||
@@ -1410,19 +1443,6 @@ async def main(args: argparse.Namespace):
|
||||
root.withdraw()
|
||||
data_filename = tkinter.filedialog.askopenfilename(filetypes=(("Multiworld data", "*.archipelago *.zip"),))
|
||||
|
||||
if data_filename.endswith(".zip"):
|
||||
import zipfile
|
||||
with zipfile.ZipFile(data_filename) as zf:
|
||||
for file in zf.namelist():
|
||||
if file.endswith(".archipelago"):
|
||||
import os
|
||||
data_filename = os.path.join(os.path.dirname(data_filename), file)
|
||||
with open(data_filename, "wb") as f:
|
||||
f.write(zf.read(file))
|
||||
break
|
||||
else:
|
||||
raise Exception("No .archipelago found in archive.")
|
||||
|
||||
ctx.load(data_filename, args.use_embedded_options)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -244,7 +244,7 @@ class JSONtoTextParser(metaclass=HandlerMeta):
|
||||
return self._handle_item_name(node)
|
||||
|
||||
def _handle_entrance_name(self, node: JSONMessagePart):
|
||||
node["color"] = 'white_bg;black'
|
||||
node["color"] = 'blue'
|
||||
return self._handle_color(node)
|
||||
|
||||
|
||||
|
||||
51
Options.py
@@ -7,12 +7,15 @@ class AssembleOptions(type):
|
||||
def __new__(mcs, name, bases, attrs):
|
||||
options = attrs["options"] = {}
|
||||
name_lookup = attrs["name_lookup"] = {}
|
||||
# merge parent class options
|
||||
for base in bases:
|
||||
if hasattr(base, "options"):
|
||||
options.update(base.options)
|
||||
name_lookup.update(name_lookup)
|
||||
name_lookup.update(base.name_lookup)
|
||||
new_options = {name[7:].lower(): option_id for name, option_id in attrs.items() if
|
||||
name.startswith("option_")}
|
||||
if "random" in new_options:
|
||||
raise Exception("Choice option 'random' cannot be manually assigned.")
|
||||
attrs["name_lookup"].update({option_id: name for name, option_id in new_options.items()})
|
||||
options.update(new_options)
|
||||
|
||||
@@ -30,24 +33,40 @@ class AssembleOptions(type):
|
||||
attrs["__init__"] = validate_decorator(attrs["__init__"])
|
||||
return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs)
|
||||
|
||||
|
||||
class Option(metaclass=AssembleOptions):
|
||||
value: int
|
||||
name_lookup: typing.Dict[int, str]
|
||||
default = 0
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}({self.get_option_name()})"
|
||||
# convert option_name_long into Name Long as displayname, otherwise name_long is the result.
|
||||
# Handled in get_option_name()
|
||||
autodisplayname = False
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.get_current_option_name()})"
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.value)
|
||||
|
||||
def get_option_name(self):
|
||||
@property
|
||||
def current_key(self) -> str:
|
||||
return self.name_lookup[self.value]
|
||||
|
||||
def __int__(self):
|
||||
def get_current_option_name(self) -> str:
|
||||
"""For display purposes."""
|
||||
return self.get_option_name(self.value)
|
||||
|
||||
def get_option_name(self, value: typing.Any) -> str:
|
||||
if self.autodisplayname:
|
||||
return self.name_lookup[self.value].replace("_", " ").title()
|
||||
else:
|
||||
return self.name_lookup[self.value]
|
||||
|
||||
def __int__(self) -> int:
|
||||
return self.value
|
||||
|
||||
def __bool__(self):
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self.value)
|
||||
|
||||
@classmethod
|
||||
@@ -95,20 +114,28 @@ class Toggle(Option):
|
||||
def __int__(self):
|
||||
return int(self.value)
|
||||
|
||||
def get_option_name(self):
|
||||
return bool(self.value)
|
||||
def get_option_name(self, value):
|
||||
return ["No", "Yes"][int(value)]
|
||||
|
||||
class DefaultOnToggle(Toggle):
|
||||
default = 1
|
||||
|
||||
|
||||
class Choice(Option):
|
||||
autodisplayname = True
|
||||
|
||||
def __init__(self, value: int):
|
||||
self.value: int = value
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str) -> Choice:
|
||||
text = text.lower()
|
||||
# TODO: turn on after most people have adjusted their yamls to no longer have suboptions with "random" in them
|
||||
# maybe in 0.2?
|
||||
# if text == "random":
|
||||
# return cls(random.choice(list(cls.options.values())))
|
||||
for optionname, value in cls.options.items():
|
||||
if optionname == text.lower():
|
||||
if optionname == text:
|
||||
return cls(value)
|
||||
raise KeyError(
|
||||
f'Could not find option "{text}" for "{cls.__name__}", '
|
||||
@@ -152,7 +179,7 @@ class Range(Option, int):
|
||||
return cls(data)
|
||||
return cls.from_text(str(data))
|
||||
|
||||
def get_option_name(self):
|
||||
def get_option_name(self, value):
|
||||
return str(self.value)
|
||||
|
||||
def __str__(self):
|
||||
@@ -189,8 +216,8 @@ class OptionDict(Option):
|
||||
else:
|
||||
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
|
||||
|
||||
def get_option_name(self):
|
||||
return str(self.value)
|
||||
def get_option_name(self, value):
|
||||
return str(value)
|
||||
|
||||
|
||||
local_objective = Toggle # local triforce pieces, local dungeon prizes etc.
|
||||
|
||||
39
Patch.py
@@ -2,7 +2,6 @@ import bsdiff4
|
||||
import yaml
|
||||
import os
|
||||
import lzma
|
||||
import hashlib
|
||||
import threading
|
||||
import concurrent.futures
|
||||
import zipfile
|
||||
@@ -10,37 +9,13 @@ import sys
|
||||
from typing import Tuple, Optional
|
||||
|
||||
import Utils
|
||||
from worlds.alttp.Rom import JAP10HASH
|
||||
|
||||
|
||||
current_patch_version = 2
|
||||
|
||||
|
||||
def get_base_rom_path(file_name: str = "") -> str:
|
||||
options = Utils.get_options()
|
||||
if not file_name:
|
||||
file_name = options["lttp_options"]["rom_file"]
|
||||
if not os.path.exists(file_name):
|
||||
file_name = Utils.local_path(file_name)
|
||||
return file_name
|
||||
|
||||
|
||||
def get_base_rom_bytes(file_name: str = "") -> bytes:
|
||||
from worlds.alttp.Rom import read_rom
|
||||
base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None)
|
||||
if not base_rom_bytes:
|
||||
file_name = get_base_rom_path(file_name)
|
||||
base_rom_bytes = bytes(read_rom(open(file_name, "rb")))
|
||||
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
if JAP10HASH != basemd5.hexdigest():
|
||||
raise Exception('Supplied Base Rom does not match known MD5 for JAP(1.0) release. '
|
||||
'Get the correct game and version, then dump it')
|
||||
get_base_rom_bytes.base_rom_bytes = base_rom_bytes
|
||||
return base_rom_bytes
|
||||
|
||||
|
||||
def generate_yaml(patch: bytes, metadata: Optional[dict] = None) -> bytes:
|
||||
from worlds.alttp.Rom import JAP10HASH
|
||||
patch = yaml.dump({"meta": metadata,
|
||||
"patch": patch,
|
||||
"game": "A Link to the Past",
|
||||
@@ -52,6 +27,7 @@ def generate_yaml(patch: bytes, metadata: Optional[dict] = None) -> bytes:
|
||||
|
||||
|
||||
def generate_patch(rom: bytes, metadata: Optional[dict] = None) -> bytes:
|
||||
from worlds.alttp.Rom import get_base_rom_bytes
|
||||
if metadata is None:
|
||||
metadata = {}
|
||||
patch = bsdiff4.diff(get_base_rom_bytes(), rom)
|
||||
@@ -71,6 +47,7 @@ def create_patch_file(rom_file_to_patch: str, server: str = "", destination: str
|
||||
|
||||
|
||||
def create_rom_bytes(patch_file: str, ignore_version: bool = False) -> Tuple[dict, str, bytearray]:
|
||||
from worlds.alttp.Rom import get_base_rom_bytes
|
||||
data = Utils.parse_yaml(lzma.decompress(load_bytes(patch_file)).decode("utf-8-sig"))
|
||||
if not ignore_version and data["compatible_version"] > current_patch_version:
|
||||
raise RuntimeError("Patch file is incompatible with this patcher, likely an update is required.")
|
||||
@@ -184,3 +161,11 @@ if __name__ == "__main__":
|
||||
|
||||
traceback.print_exc()
|
||||
input("Press enter to close.")
|
||||
|
||||
|
||||
def read_rom(stream, strip_header=True) -> bytearray:
|
||||
"""Reads rom into bytearray and optionally strips off any smc header"""
|
||||
buffer = bytearray(stream.read())
|
||||
if strip_header and len(buffer) % 0x400 == 0x200:
|
||||
return buffer[0x200:]
|
||||
return buffer
|
||||
|
||||
@@ -6,6 +6,7 @@ Currently, the following games are supported:
|
||||
* The Legend of Zelda: A Link to the Past
|
||||
* Factorio
|
||||
* Minecraft
|
||||
* Subnautica
|
||||
|
||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial).
|
||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||
|
||||
45
Utils.py
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import typing
|
||||
|
||||
|
||||
def tuplize_version(version: str) -> typing.Tuple[int, ...]:
|
||||
def tuplize_version(version: str) -> Version:
|
||||
return Version(*(int(piece, 10) for piece in version.split(".")))
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ class Version(typing.NamedTuple):
|
||||
build: int
|
||||
|
||||
|
||||
__version__ = "0.1.5"
|
||||
__version__ = "0.1.6"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
import builtins
|
||||
@@ -51,24 +51,6 @@ def snes_to_pc(value):
|
||||
return ((value & 0x7F0000) >> 1) | (value & 0x7FFF)
|
||||
|
||||
|
||||
def parse_player_names(names, players, teams):
|
||||
names = tuple(n for n in (n.strip() for n in names.split(",")) if n)
|
||||
if len(names) != len(set(names)):
|
||||
name_counter = collections.Counter(names)
|
||||
raise ValueError(f"Duplicate Player names is not supported, "
|
||||
f'found multiple "{name_counter.most_common(1)[0][0]}".')
|
||||
ret = []
|
||||
while names or len(ret) < teams:
|
||||
team = [n[:16] for n in names[:players]]
|
||||
# 16 bytes in rom per player, which will map to more in unicode, but those characters later get filtered
|
||||
while len(team) != players:
|
||||
team.append(f"Player{len(team) + 1}")
|
||||
ret.append(team)
|
||||
|
||||
names = names[players:]
|
||||
return ret
|
||||
|
||||
|
||||
def cache_argsless(function):
|
||||
if function.__code__.co_argcount:
|
||||
raise Exception("Can only cache 0 argument functions with this cache.")
|
||||
@@ -137,6 +119,7 @@ def open_file(filename):
|
||||
parse_yaml = safe_load
|
||||
unsafe_parse_yaml = functools.partial(load, Loader=Loader)
|
||||
|
||||
|
||||
@cache_argsless
|
||||
def get_public_ipv4() -> str:
|
||||
import socket
|
||||
@@ -153,6 +136,7 @@ def get_public_ipv4() -> str:
|
||||
pass # we could be offline, in a local game, so no point in erroring out
|
||||
return ip
|
||||
|
||||
|
||||
@cache_argsless
|
||||
def get_public_ipv6() -> str:
|
||||
import socket
|
||||
@@ -166,6 +150,7 @@ def get_public_ipv6() -> str:
|
||||
pass # we could be offline, in a local game, or ipv6 may not be available
|
||||
return ip
|
||||
|
||||
|
||||
@cache_argsless
|
||||
def get_default_options() -> dict:
|
||||
# Refer to host.yaml for comments as to what all these options mean.
|
||||
@@ -217,14 +202,6 @@ def get_default_options() -> dict:
|
||||
return options
|
||||
|
||||
|
||||
blacklisted_options = {"multi_mystery_options.cpu_threads",
|
||||
"multi_mystery_options.max_attempts",
|
||||
"multi_mystery_options.take_first_working",
|
||||
"multi_mystery_options.keep_all_seeds",
|
||||
"multi_mystery_options.log_output_path",
|
||||
"multi_mystery_options.log_level"}
|
||||
|
||||
|
||||
def update_options(src: dict, dest: dict, filename: str, keys: list) -> dict:
|
||||
import logging
|
||||
for key, value in src.items():
|
||||
@@ -233,17 +210,18 @@ def update_options(src: dict, dest: dict, filename: str, keys: list) -> dict:
|
||||
option_name = '.'.join(new_keys)
|
||||
if key not in dest:
|
||||
dest[key] = value
|
||||
if filename.endswith("options.yaml") and option_name not in blacklisted_options:
|
||||
if filename.endswith("options.yaml"):
|
||||
logging.info(f"Warning: {filename} is missing {option_name}")
|
||||
elif isinstance(value, dict):
|
||||
if not isinstance(dest.get(key, None), dict):
|
||||
if filename.endswith("options.yaml") and option_name not in blacklisted_options:
|
||||
if filename.endswith("options.yaml"):
|
||||
logging.info(f"Warning: {filename} has {option_name}, but it is not a dictionary. overwriting.")
|
||||
dest[key] = value
|
||||
else:
|
||||
dest[key] = update_options(value, dest[key], filename, new_keys)
|
||||
return dest
|
||||
|
||||
|
||||
@cache_argsless
|
||||
def get_options() -> dict:
|
||||
if not hasattr(get_options, "options"):
|
||||
@@ -308,11 +286,11 @@ def get_adjuster_settings(romfile: str) -> typing.Tuple[str, bool]:
|
||||
|
||||
if adjuster_settings:
|
||||
import pprint
|
||||
import Patch
|
||||
from worlds.alttp.Rom import get_base_rom_path
|
||||
adjuster_settings.rom = romfile
|
||||
adjuster_settings.baserom = Patch.get_base_rom_path()
|
||||
adjuster_settings.baserom = get_base_rom_path()
|
||||
adjuster_settings.world = None
|
||||
whitelist = {"disablemusic", "fastmenu", "heartbeep", "heartcolor", "ow_palettes", "quickswap",
|
||||
whitelist = {"music", "menuspeed", "heartbeep", "heartcolor", "ow_palettes", "quickswap",
|
||||
"uw_palettes", "sprite"}
|
||||
printed_options = {name: value for name, value in vars(adjuster_settings).items() if name in whitelist}
|
||||
if hasattr(adjuster_settings, "sprite_pool"):
|
||||
@@ -358,6 +336,7 @@ def get_adjuster_settings(romfile: str) -> typing.Tuple[str, bool]:
|
||||
return romfile, adjusted
|
||||
return romfile, False
|
||||
|
||||
|
||||
@cache_argsless
|
||||
def get_unique_identifier():
|
||||
uuid = persistent_load().get("client", {}).get("uuid", None)
|
||||
|
||||
@@ -99,14 +99,20 @@ games_list = {
|
||||
Minecraft is a game about creativity. In a world made entirely of cubes, you explore, discover, mine,
|
||||
craft, and try not to explode. Delve deep into the earth and discover abandoned mines, ancient
|
||||
structures, and materials to create a portal to another world. Defeat the Ender Dragon, and claim
|
||||
victory!""")
|
||||
victory!"""),
|
||||
"Subnautica": ("Subnautica",
|
||||
"""
|
||||
Subnautica is an undersea exploration game. Stranded on an alien world, you become infected by
|
||||
an unknown bacteria. The planet's automatic quarantine will shoot you down if you try to leave.
|
||||
You must find a cure for yourself, build an escape rocket, and leave the planet.
|
||||
"""),
|
||||
}
|
||||
|
||||
|
||||
# Player settings pages
|
||||
@app.route('/games/<string:game>/player-settings')
|
||||
def player_settings(game):
|
||||
return render_template(f"player-settings.html")
|
||||
return render_template(f"player-settings.html", game=game)
|
||||
|
||||
|
||||
# Game sub-pages
|
||||
|
||||
@@ -45,10 +45,10 @@ def download_raw_patch(seed_id, player_id: int):
|
||||
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](seed_id)}.apbp"
|
||||
return send_file(patch_data, as_attachment=True, attachment_filename=fname)
|
||||
|
||||
@app.route("/slot_file/<suuid:seed_id>/<int:player_id>")
|
||||
def download_slot_file(seed_id, player_id: int):
|
||||
seed = Seed.get(id=seed_id)
|
||||
slot_data: Slot = select(patch for patch in seed.slots if
|
||||
@app.route("/slot_file/<suuid:room_id>/<int:player_id>")
|
||||
def download_slot_file(room_id, player_id: int):
|
||||
room = Room.get(id=room_id)
|
||||
slot_data: Slot = select(patch for patch in room.seed.slots if
|
||||
patch.player_id == player_id).first()
|
||||
|
||||
if not slot_data:
|
||||
@@ -57,7 +57,10 @@ def download_slot_file(seed_id, player_id: int):
|
||||
import io
|
||||
|
||||
if slot_data.game == "Minecraft":
|
||||
fname = f"AP_{app.jinja_env.filters['suuid'](seed_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmc"
|
||||
from worlds.minecraft import mc_update_output
|
||||
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmc"
|
||||
data = mc_update_output(slot_data.data, server=app.config['PATCH_TARGET'], port=room.last_port)
|
||||
return send_file(io.BytesIO(data), as_attachment=True, attachment_filename=fname)
|
||||
elif slot_data.game == "Factorio":
|
||||
with zipfile.ZipFile(io.BytesIO(slot_data.data)) as zf:
|
||||
for name in zf.namelist():
|
||||
|
||||
@@ -91,8 +91,6 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
|
||||
erargs.name[player] = os.path.splitext(os.path.split(playerfile)[-1])[0]
|
||||
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
|
||||
|
||||
erargs.names = ",".join(erargs.name[i] for i in range(1, playercount + 1))
|
||||
del (erargs.name)
|
||||
ERmain(erargs, seed)
|
||||
|
||||
return upload_to_db(target.name, owner, sid, race)
|
||||
|
||||
@@ -48,6 +48,16 @@ def create():
|
||||
|
||||
game_options[option_name] = this_option
|
||||
|
||||
elif hasattr(option, "range_start") and hasattr(option, "range_end"):
|
||||
game_options[option_name] = {
|
||||
"type": "range",
|
||||
"friendlyName": option.friendly_name if hasattr(option, "friendly_name") else option_name,
|
||||
"description": option.__doc__ if option.__doc__ else "Please document me!",
|
||||
"defaultValue": option.default if hasattr(option, "default") else option.range_start,
|
||||
"min": option.range_start,
|
||||
"max": option.range_end,
|
||||
}
|
||||
|
||||
player_settings["gameOptions"] = game_options
|
||||
|
||||
with open(os.path.join(target_folder, game_name + ".json"), "w") as f:
|
||||
|
||||
49
WebHostLib/static/assets/minecraftTracker.js
Normal file
@@ -0,0 +1,49 @@
|
||||
window.addEventListener('load', () => {
|
||||
// Reload tracker every 15 seconds
|
||||
const url = window.location;
|
||||
setInterval(() => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState !== 4) { return; }
|
||||
|
||||
// Create a fake DOM using the returned HTML
|
||||
const domParser = new DOMParser();
|
||||
const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html');
|
||||
|
||||
// Update item tracker
|
||||
document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML;
|
||||
// Update only counters in the location-table
|
||||
let counters = document.getElementsByClassName('counter');
|
||||
const fakeCounters = fakeDOM.getElementsByClassName('counter');
|
||||
for (let i = 0; i < counters.length; i++) {
|
||||
counters[i].innerHTML = fakeCounters[i].innerHTML;
|
||||
}
|
||||
};
|
||||
ajax.open('GET', url);
|
||||
ajax.send();
|
||||
}, 15000)
|
||||
|
||||
// Collapsible advancement sections
|
||||
const categories = document.getElementsByClassName("location-category");
|
||||
for (let i = 0; i < categories.length; i++) {
|
||||
let hide_id = categories[i].id.split('-')[0];
|
||||
if (hide_id == 'Total') {
|
||||
continue;
|
||||
}
|
||||
categories[i].addEventListener('click', function() {
|
||||
// Toggle the advancement list
|
||||
document.getElementById(hide_id).classList.toggle("hide");
|
||||
// Change text of the header
|
||||
const tab_header = document.getElementById(hide_id+'-header').children[0];
|
||||
const orig_text = tab_header.innerHTML;
|
||||
let new_text;
|
||||
if (orig_text.includes("▼")) {
|
||||
new_text = orig_text.replace("▼", "▲");
|
||||
}
|
||||
else {
|
||||
new_text = orig_text.replace("▲", "▼");
|
||||
}
|
||||
tab_header.innerHTML = new_text;
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,8 +1,7 @@
|
||||
let gameName = null;
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const urlMatches = window.location.href.match(/^.*\/(.*)\/player-settings/);
|
||||
gameName = decodeURIComponent(urlMatches[1]);
|
||||
gameName = document.getElementById('player-settings').getAttribute('data-game');
|
||||
|
||||
// Update game name on page
|
||||
document.getElementById('game-name').innerHTML = gameName;
|
||||
@@ -90,22 +89,62 @@ const buildOptionsTable = (settings, romOpts = false) => {
|
||||
|
||||
// td Right
|
||||
const tdr = document.createElement('td');
|
||||
const select = document.createElement('select');
|
||||
select.setAttribute('id', setting);
|
||||
select.setAttribute('data-key', setting);
|
||||
if (romOpts) { select.setAttribute('data-romOpt', '1'); }
|
||||
settings[setting].options.forEach((opt) => {
|
||||
const option = document.createElement('option');
|
||||
option.setAttribute('value', opt.value);
|
||||
option.innerText = opt.name;
|
||||
if ((isNaN(currentSettings[setting]) && (parseInt(opt.value, 10) === parseInt(currentSettings[setting]))) ||
|
||||
(opt.value === currentSettings[setting])) {
|
||||
option.selected = true;
|
||||
}
|
||||
select.appendChild(option);
|
||||
});
|
||||
select.addEventListener('change', (event) => updateGameSetting(event));
|
||||
tdr.appendChild(select);
|
||||
let element = null;
|
||||
|
||||
switch(settings[setting].type){
|
||||
case 'select':
|
||||
element = document.createElement('div');
|
||||
element.classList.add('select-container');
|
||||
let select = document.createElement('select');
|
||||
select.setAttribute('id', setting);
|
||||
select.setAttribute('data-key', setting);
|
||||
if (romOpts) { select.setAttribute('data-romOpt', '1'); }
|
||||
settings[setting].options.forEach((opt) => {
|
||||
const option = document.createElement('option');
|
||||
option.setAttribute('value', opt.value);
|
||||
option.innerText = opt.name;
|
||||
if ((isNaN(currentSettings[gameName][setting]) &&
|
||||
(parseInt(opt.value, 10) === parseInt(currentSettings[gameName][setting]))) ||
|
||||
(opt.value === currentSettings[gameName][setting]))
|
||||
{
|
||||
option.selected = true;
|
||||
}
|
||||
select.appendChild(option);
|
||||
});
|
||||
select.addEventListener('change', (event) => updateGameSetting(event));
|
||||
element.appendChild(select);
|
||||
break;
|
||||
|
||||
case 'range':
|
||||
element = document.createElement('div');
|
||||
element.classList.add('range-container');
|
||||
|
||||
let range = document.createElement('input');
|
||||
range.setAttribute('type', 'range');
|
||||
range.setAttribute('data-key', setting);
|
||||
range.setAttribute('min', settings[setting].min);
|
||||
range.setAttribute('max', settings[setting].max);
|
||||
range.value = currentSettings[gameName][setting];
|
||||
range.addEventListener('change', (event) => {
|
||||
document.getElementById(`${setting}-value`).innerText = event.target.value;
|
||||
updateGameSetting(event);
|
||||
});
|
||||
element.appendChild(range);
|
||||
|
||||
let rangeVal = document.createElement('span');
|
||||
rangeVal.classList.add('range-value');
|
||||
rangeVal.setAttribute('id', `${setting}-value`);
|
||||
rangeVal.innerText = currentSettings[gameName][setting] ?? settings[setting].defaultValue;
|
||||
element.appendChild(rangeVal);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error(`Unknown setting type: ${settings[setting].type}`);
|
||||
console.error(setting);
|
||||
return;
|
||||
}
|
||||
|
||||
tdr.appendChild(element);
|
||||
tr.appendChild(tdr);
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
@@ -15,8 +15,8 @@ One Server Host exists per Factorio World in an Archipelago Multiworld, any numb
|
||||
## Installation Procedures
|
||||
|
||||
### Dedicated Server Setup
|
||||
You need a dedicated isolated Factorio installation that the FactorioClient can take control over, if you intend to both emit a world and play, you need to follow both this setup and the player setup.
|
||||
This requires two Factorio installations. The easiest and cheapest way to do so is to either buy or register a Factorio on factorio.com, which allows you to download as many Factorio games as you want.
|
||||
You need a dedicated isolated Factorio installation that the FactorioClient can take control over. If you intend to both host a world and play on the same device, you will need two separate Factorio installations; one for the FactorioClient to hook into and control, and one for you to play on.
|
||||
The easiest and cheapest way to do so is to either buy or register a Factorio key on factorio.com, which allows you to download as many Factorio games as you want. If you own a steam copy already you can link your account on the website.
|
||||
1. Download the latest Factorio from https://factorio.com/download for your system, for Windows the recommendation is "win64-manual".
|
||||
|
||||
2. Make sure the Factorio you play and the Factorio you use for hosting do not share paths. If you downloaded the "manual" version, this is already the case, otherwise, go into the hosting Factorio's folder and put the following text into its `config-path.cfg`:
|
||||
@@ -24,26 +24,29 @@ This requires two Factorio installations. The easiest and cheapest way to do so
|
||||
config-path=__PATH__executable__/../../config
|
||||
use-system-read-write-data-directories=false
|
||||
```
|
||||
3. Navigate to where you installed Archipelago and open the host.yaml file as text. Find the entry `executable` under `factorio_options` and set it to point to your Factorio. If you put Factorio into your Archipelago folder, this would already match.
|
||||
|
||||
|
||||
3. In this same folder if there are shortcuts named "mods" and "saves" delete these and replace with folders with the same names.
|
||||
4. Navigate to where you installed ArchipelagoFactorioClient and open the host.yaml file as text. Find the entry `executable` under `factorio_options` and set it to point to your hosting Factorio.exe. If you put Factorio into your Archipelago folder, this would already match.<br>
|
||||
ex.
|
||||
```yaml
|
||||
factorio_options:
|
||||
executable: C:\\Program Files\\factorio\\bin\\x64\\factorio"
|
||||
```
|
||||
### Player Setup
|
||||
- Manually install the AP mod for the correct world you want to join, then use Factorio's built-in multiplayer.
|
||||
- Manually install the AP mod for the correct world you want to join, then use Factorio's built-in multiplayer. If you're connecting to a FactorioClient on the same system you will connect to localhost
|
||||
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
1. Install the generated Factorio AP Mod (would be in <Factorio Directory>/Mods after step 2 of Setup)
|
||||
1. Install the generated Factorio AP Mod (would be in /Mods after step 2 of Setup)
|
||||
|
||||
2. Run FactorioClient, it should launch a Factorio server, which you can control with `/factorio <original factorio commands>`,
|
||||
|
||||
* It should start up, create a world and become ready for Factorio connections.
|
||||
2. Run FactorioClient, it should launch a Factorio server, which you can control with /factorio <original factorio commands>,
|
||||
|
||||
3. In FactorioClient, do `/connect <Archipelago Server Address>` to join that multiworld. You can find further commands with `/help` as well as `!help` once connected.
|
||||
* It should start up, create a world and become ready for Factorio connections.
|
||||
3. In FactorioClient, do /connect <Archipelago Server Address> to join that multiworld. You can find further commands with /help as well as !help once connected.
|
||||
|
||||
* / commands are run on your local client, ! commands are requests for the AP server
|
||||
* / commands are run on your local client, ! commands are requests for the AP server
|
||||
|
||||
* Players should be able to connect to your Factorio Server and begin playing.
|
||||
|
||||
4. You can join yourself by connecting to address localhost, other people will need to connect to your IP and you may need to port forward for the Factorio Server for those connections.
|
||||
|
||||
* Players should be able to connect to your Factorio Server and begin playing.
|
||||
|
||||
4. You can join yourself by connecting to address `localhost`, other people will need to connect to your IP
|
||||
and you may need to port forward for the Factorio Server for those connections.
|
||||
@@ -1,54 +1,12 @@
|
||||
# Minecraft Randomizer Setup Guide
|
||||
|
||||
#Automatic Hosting Install
|
||||
- download and install [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) and choose the `Minecraft Client` module
|
||||
|
||||
## Required Software
|
||||
|
||||
### Server Host
|
||||
- [Minecraft Forge](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.16.5.html)
|
||||
- [Minecraft Archipelago Randomizer Mod](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
|
||||
|
||||
### Players
|
||||
- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-edition)
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
### Dedicated Server Setup
|
||||
Only one person has to do this setup and host a dedicated server for everyone else playing to connect to.
|
||||
1. Download the 1.16.5 **Minecraft Forge** installer from the link above, making sure to download the most recent recommended version.
|
||||
|
||||
2. Run the `forge-1.16.5-xx.x.x-installer.jar` file and choose **install server**.
|
||||
- On this page you will also choose where to install the server to remember this directory it's important in the next step.
|
||||
|
||||
3. Navigate to where you installed the server and open `forge-1.16.5-xx.x.x.jar`
|
||||
- Upon first launch of the server it will close and ask you to accept Minecraft's EULA. There will be a new file called `eula.txt` that contains a link to Minecraft's EULA, and a line that you need to change to `eula=true` to accept Minecraft's EULA.
|
||||
- This will create the appropriate directories for you to place the files in the following step.
|
||||
|
||||
4. Place the `aprandomizer-x.x.x.jar` from the link above file into the `mods` folder of the above installation of your forge server.
|
||||
- Once again run the server, it will load up and generate the required directory `APData` for when you are ready to play a game!
|
||||
|
||||
### Basic Player Setup
|
||||
- Purchase and install Minecraft from the above link.
|
||||
|
||||
**You're Done**.
|
||||
|
||||
Players only need to have a Vanilla unmodified version of Minecraft to play!
|
||||
|
||||
### Advanced Player Setup
|
||||
***This is not required to play a randomized minecraft game.***
|
||||
however this recommended as it helps make the experience more enjoyable.
|
||||
|
||||
#### Recomended Mods
|
||||
- [JourneyMap](https://www.curseforge.com/minecraft/mc-mods/journeymap) (Minimap)
|
||||
|
||||
|
||||
1. Install and run Minecraft from the link above at least once.
|
||||
2. Run the `forge-1.16.5-xx.x.x-installer.jar` file and choose **install client**.
|
||||
- Start Minecraft forge at least once to create the directories needed for the next steps.
|
||||
3. Navigate to your minecraft install directory and place desired mods `.jar` file the in the `mods` directory.
|
||||
- The default install directories are as follows.
|
||||
- Windows `%APPDATA%\.minecraft\mods`
|
||||
- macOS `~/Library/Application Support/minecraft/mods`
|
||||
- Linux `~/.minecraft/mods`
|
||||
|
||||
## Configuring your YAML file
|
||||
|
||||
### What is a YAML file and why do I need one?
|
||||
@@ -60,10 +18,10 @@ can all have different options.
|
||||
### Where do I get a YAML file?
|
||||
A basic minecraft yaml will look like this.
|
||||
```yaml
|
||||
description: Template Name
|
||||
description: Basic Minecraft Yaml
|
||||
# Your name in-game. Spaces will be replaced with underscores and
|
||||
# there is a 16 character limit
|
||||
name: YourName
|
||||
name: YourName
|
||||
game: Minecraft
|
||||
|
||||
# Shared Options supported by all games:
|
||||
@@ -71,44 +29,61 @@ accessibility: locations
|
||||
progression_balancing: on
|
||||
# Minecraft Specific Options
|
||||
|
||||
# Number of advancements required (out of 92 total) to spawn the
|
||||
# Ender Dragon and complete the game.
|
||||
advancement_goal:
|
||||
few: 0 #30
|
||||
normal: 1 #50
|
||||
many: 0 #70
|
||||
Minecraft:
|
||||
# Number of advancements required (87 max) to spawn the Ender Dragon and complete the game.
|
||||
advancement_goal: 50
|
||||
|
||||
# Modifies the level of items logically required for exploring
|
||||
# dangerous areas and fighting bosses.
|
||||
combat_difficulty:
|
||||
easy: 0
|
||||
normal: 1
|
||||
hard: 0
|
||||
# Number of dragon egg shards to collect (30 max) before the Ender Dragon will spawn.
|
||||
egg_shards_required: 10
|
||||
|
||||
# Junk-fills certain RNG-reliant or tedious advancements with XP rewards.
|
||||
include_hard_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
# Number of egg shards available in the pool (30 max).
|
||||
egg_shards_available: 15
|
||||
|
||||
# Junk-fills extremely difficult advancements;
|
||||
# this is only How Did We Get Here? and Adventuring Time.
|
||||
include_insane_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
# Modifies the level of items logically required for
|
||||
# exploring dangerous areas and fighting bosses.
|
||||
combat_difficulty:
|
||||
easy: 0
|
||||
normal: 1
|
||||
hard: 0
|
||||
|
||||
# Junk-fills certain RNG-reliant or tedious advancements.
|
||||
include_hard_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
|
||||
# Junk-fills extremely difficult advancements;
|
||||
# this is only How Did We Get Here? and Adventuring Time.
|
||||
include_insane_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
|
||||
# Some advancements require defeating the Ender Dragon;
|
||||
# this will junk-fill them so you won't have to finish to send some items.
|
||||
include_postgame_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
# Some advancements require defeating the Ender Dragon;
|
||||
# this will junk-fill them, so you won't have to finish them to send some items.
|
||||
include_postgame_advancements:
|
||||
on: 0
|
||||
off: 1
|
||||
|
||||
# Enables shuffling of villages, outposts, fortresses, bastions, and end cities.
|
||||
shuffle_structures:
|
||||
on: 0
|
||||
off: 1
|
||||
|
||||
#enables shuffling of villages, outposts, fortresses, bastions, and end cities.
|
||||
shuffle_structures:
|
||||
on: 1
|
||||
off: 0
|
||||
# Adds structure compasses to the item pool,
|
||||
# which point to the nearest indicated structure.
|
||||
structure_compasses:
|
||||
on: 0
|
||||
off: 1
|
||||
|
||||
# Replaces a percentage of junk items with bee traps
|
||||
# which spawn multiple angered bees around every player when received.
|
||||
bee_traps:
|
||||
0: 1
|
||||
25: 0
|
||||
50: 0
|
||||
75: 0
|
||||
100: 0
|
||||
```
|
||||
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
### Obtain your Minecraft data file
|
||||
@@ -118,8 +93,7 @@ When you join a multiworld game, you will be asked to provide your YAML file to
|
||||
is done, the host will provide you with either a link to download your data file, or with a zip file containing
|
||||
everyone's data files. Your data file should have a `.apmc` extension.
|
||||
|
||||
Put your data file in your forge servers `APData` folder. Make sure to remove any previous data file that was in there
|
||||
previously.
|
||||
double click on your `.apmc` file to have the minecraft client auto-launch the installed forge server.
|
||||
|
||||
### Connect to the MultiServer
|
||||
After having placed your data file in the `APData` folder, start the Forge server and make sure you have OP
|
||||
@@ -134,3 +108,23 @@ When the console tells you that you have joined the room, you're ready to begin
|
||||
on successfully joining a multiworld game! At this point any additional minecraft players may connect to your
|
||||
forge server.
|
||||
|
||||
|
||||
## Manual Installation Procedures
|
||||
this is only required if you wish to set up a forge install yourself, its recommended to just use the Archipelago Installer.
|
||||
###Required Software
|
||||
- [Minecraft Forge](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.16.5.html)
|
||||
- [Minecraft Archipelago Randomizer Mod](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
|
||||
**DO NOT INSTALL THIS ON YOUR CLIENT**
|
||||
### Dedicated Server Setup
|
||||
Only one person has to do this setup and host a dedicated server for everyone else playing to connect to.
|
||||
1. Download the 1.16.5 **Minecraft Forge** installer from the link above, making sure to download the most recent recommended version.
|
||||
|
||||
2. Run the `forge-1.16.5-xx.x.x-installer.jar` file and choose **install server**.
|
||||
- On this page you will also choose where to install the server to remember this directory it's important in the next step.
|
||||
|
||||
3. Navigate to where you installed the server and open `forge-1.16.5-xx.x.x.jar`
|
||||
- Upon first launch of the server it will close and ask you to accept Minecraft's EULA. There will be a new file called `eula.txt` that contains a link to Minecraft's EULA, and a line that you need to change to `eula=true` to accept Minecraft's EULA.
|
||||
- This will create the appropriate directories for you to place the files in the following step.
|
||||
|
||||
4. Place the `aprandomizer-x.x.x.jar` from the link above file into the `mods` folder of the above installation of your forge server.
|
||||
- Once again run the server, it will load up and generate the required directory `APData` for when you are ready to play a game!
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
html{
|
||||
background-image: url('../../static/backgrounds/grass/grass-0007-large.png');
|
||||
background-repeat: repeat;
|
||||
background-size: 650px 650px;
|
||||
}
|
||||
|
||||
#player-settings{
|
||||
max-width: 1000px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
color: #eeffeb;
|
||||
}
|
||||
|
||||
#player-settings #player-settings-button-row{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#player-settings code{
|
||||
background-color: #d9cd8e;
|
||||
border-radius: 4px;
|
||||
padding-left: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
#player-settings #user-message{
|
||||
display: none;
|
||||
width: calc(100% - 8px);
|
||||
background-color: #ffe86b;
|
||||
border-radius: 4px;
|
||||
color: #000000;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#player-settings #user-message.visible{
|
||||
display: block;
|
||||
}
|
||||
|
||||
#player-settings h1{
|
||||
font-size: 2.5rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 4px #000000;
|
||||
}
|
||||
|
||||
#player-settings h2{
|
||||
font-size: 2rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffe993;
|
||||
text-transform: lowercase;
|
||||
text-shadow: 1px 1px 2px #000000;
|
||||
}
|
||||
|
||||
#player-settings h3, #player-settings h4, #player-settings h5, #player-settings h6{
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#player-settings a{
|
||||
color: #ffef00;
|
||||
}
|
||||
|
||||
#player-settings input:not([type]){
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
#player-settings input:not([type]):focus{
|
||||
border: 1px solid #ffffff;
|
||||
}
|
||||
|
||||
#player-settings select{
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
#player-settings #game-options, #player-settings #rom-options{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#player-settings .left, #player-settings .right{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-settings table select{
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
#player-settings table label{
|
||||
display: block;
|
||||
min-width: 200px;
|
||||
margin-right: 4px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@media all and (max-width: 1000px), all and (orientation: portrait){
|
||||
#player-settings #game-options, #player-settings #rom-options{
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#player-settings .left, #player-settings .right{
|
||||
flex-grow: unset;
|
||||
}
|
||||
|
||||
#game-options table label, #rom-options table label{
|
||||
display: block;
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
102
WebHostLib/static/styles/minecraftTracker.css
Normal file
@@ -0,0 +1,102 @@
|
||||
#player-tracker-wrapper{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#inventory-table{
|
||||
border-top: 2px solid #000000;
|
||||
border-left: 2px solid #000000;
|
||||
border-right: 2px solid #000000;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
padding: 3px 3px 10px;
|
||||
width: 384px;
|
||||
background-color: #42b149;
|
||||
}
|
||||
|
||||
#inventory-table td{
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#inventory-table img{
|
||||
height: 100%;
|
||||
max-width: 40px;
|
||||
max-height: 40px;
|
||||
filter: grayscale(100%) contrast(75%) brightness(30%);
|
||||
}
|
||||
|
||||
#inventory-table img.acquired{
|
||||
filter: none;
|
||||
}
|
||||
|
||||
#inventory-table div.counted-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#inventory-table div.item-count {
|
||||
position: absolute;
|
||||
color: white;
|
||||
font-family: "Minecraftia", monospace;
|
||||
font-weight: bold;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
#location-table{
|
||||
width: 384px;
|
||||
border-left: 2px solid #000000;
|
||||
border-right: 2px solid #000000;
|
||||
border-bottom: 2px solid #000000;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
background-color: #42b149;
|
||||
padding: 0 3px 3px;
|
||||
font-family: "Minecraftia", monospace;
|
||||
font-size: 14px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#location-table th{
|
||||
vertical-align: middle;
|
||||
text-align: left;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
#location-table td{
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
#location-table td.counter {
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#location-table td.toggle-arrow {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#location-table tr#Total-header {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#location-table img{
|
||||
height: 100%;
|
||||
max-width: 30px;
|
||||
max-height: 30px;
|
||||
}
|
||||
|
||||
#location-table tbody.locations {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#location-table td.location-name {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
html{
|
||||
background-image: url('../../static/backgrounds/grass/grass-0007-large.png');
|
||||
background-image: url('../static/backgrounds/grass/grass-0007-large.png');
|
||||
background-repeat: repeat;
|
||||
background-size: 650px 650px;
|
||||
}
|
||||
@@ -101,8 +101,28 @@ html{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-settings table select{
|
||||
width: 250px;
|
||||
#player-settings table .select-container{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#player-settings table .select-container select{
|
||||
min-width: 200px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-settings table .range-container{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#player-settings table .range-container input[type=range]{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-settings table .range-value{
|
||||
min-width: 20px;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
#player-settings table label{
|
||||
@@ -113,7 +133,7 @@ html{
|
||||
}
|
||||
|
||||
@media all and (max-width: 1000px), all and (orientation: portrait){
|
||||
#player-settings #game-options, #player-settings #rom-options{
|
||||
#player-settings #game-options{
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@@ -122,7 +142,7 @@ html{
|
||||
flex-grow: unset;
|
||||
}
|
||||
|
||||
#game-options table label, #rom-options table label{
|
||||
#game-options table label{
|
||||
display: block;
|
||||
min-width: 200px;
|
||||
}
|
||||
3
WebHostLib/static/styles/subnautica/subnautica.css
Normal file
@@ -0,0 +1,3 @@
|
||||
#subnautica{
|
||||
margin: 1rem;
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
html{
|
||||
background-image: url('../../static/backgrounds/grass/grass-0007-large.png');
|
||||
background-repeat: repeat;
|
||||
background-size: 650px 650px;
|
||||
}
|
||||
|
||||
#player-settings{
|
||||
max-width: 1000px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
color: #eeffeb;
|
||||
}
|
||||
|
||||
#player-settings #player-settings-button-row{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#player-settings code{
|
||||
background-color: #d9cd8e;
|
||||
border-radius: 4px;
|
||||
padding-left: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
#player-settings #user-message{
|
||||
display: none;
|
||||
width: calc(100% - 8px);
|
||||
background-color: #ffe86b;
|
||||
border-radius: 4px;
|
||||
color: #000000;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#player-settings #user-message.visible{
|
||||
display: block;
|
||||
}
|
||||
|
||||
#player-settings h1{
|
||||
font-size: 2.5rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 4px #000000;
|
||||
}
|
||||
|
||||
#player-settings h2{
|
||||
font-size: 2rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffe993;
|
||||
text-transform: lowercase;
|
||||
text-shadow: 1px 1px 2px #000000;
|
||||
}
|
||||
|
||||
#player-settings h3, #player-settings h4, #player-settings h5, #player-settings h6{
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#player-settings a{
|
||||
color: #ffef00;
|
||||
}
|
||||
|
||||
#player-settings input:not([type]){
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
#player-settings input:not([type]):focus{
|
||||
border: 1px solid #ffffff;
|
||||
}
|
||||
|
||||
#player-settings select{
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
#player-settings #game-options, #player-settings #rom-options{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#player-settings .left, #player-settings .right{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-settings table select{
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
#player-settings table label{
|
||||
display: block;
|
||||
min-width: 200px;
|
||||
margin-right: 4px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@media all and (max-width: 1000px), all and (orientation: portrait){
|
||||
#player-settings #game-options, #player-settings #rom-options{
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#player-settings .left, #player-settings .right{
|
||||
flex-grow: unset;
|
||||
}
|
||||
|
||||
#game-options table label, #rom-options table label{
|
||||
display: block;
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
15
WebHostLib/templates/games/Subnautica/Subnautica.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>Subnautica</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/subnautica/subnautica.css") }}" />
|
||||
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="subnautica">
|
||||
Coming Soon™
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -11,10 +11,10 @@
|
||||
<ul>
|
||||
{% for patch in room.seed.slots|list|sort(attribute="player_id") %}
|
||||
{% if patch.game == "Minecraft" %}
|
||||
<li><a href="{{ url_for("download_slot_file", seed_id=room.seed.id, player_id=patch.player_id) }}">
|
||||
<li><a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}">
|
||||
APMC for player {{ patch.player_id }} - {{ patch.player_name }}</a></li>
|
||||
{% elif patch.game == "Factorio" %}
|
||||
<li><a href="{{ url_for("download_slot_file", seed_id=room.seed.id, player_id=patch.player_id) }}">
|
||||
<li><a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}">
|
||||
Mod for player {{ patch.player_id }} - {{ patch.player_name }}</a></li>
|
||||
{% else %}
|
||||
<li><a href="{{ url_for("download_patch", patch_id=patch.id, room_id=room.id) }}">
|
||||
|
||||
66
WebHostLib/templates/minecraftTracker.html
Normal file
@@ -0,0 +1,66 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ player_name }}'s Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/minecraftTracker.css') }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/minecraftTracker.js') }}"></script>
|
||||
<link rel="stylesheet" media="screen" href="https://fontlibrary.org//face/minecraftia" type="text/css"/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<table id="inventory-table">
|
||||
<tr>
|
||||
<td><img src="{{ tools_url }}" class="{{ 'acquired' }}" title="Progressive Tools" /></td>
|
||||
<td><img src="{{ weapons_url }}" class="{{ 'acquired' }}" title="Progressive Weapons" /></td>
|
||||
<td><img src="{{ armor_url }}" class="{{ 'acquired' }}" title="Progressive Armor" /></td>
|
||||
<td><img src="{{ resource_crafting_url }}" class="{{ 'acquired' if 'Progressive Resource Crafting' in acquired_items }}"
|
||||
title="Progressive Resource Crafting" /></td>
|
||||
<td><img src="{{ icons['Brewing Stand'] }}" class="{{ 'acquired' if 'Brewing' in acquired_items }}" title="Brewing" /></td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Ender Pearl'] }}" class="{{ 'acquired' if '3 Ender Pearls' in acquired_items }}" title="Ender Pearls" />
|
||||
<div class="item-count">{{ pearls_count }}</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Bucket'] }}" class="{{ 'acquired' if 'Bucket' in acquired_items }}" title="Bucket" /></td>
|
||||
<td><img src="{{ icons['Bow'] }}" class="{{ 'acquired' if 'Archery' in acquired_items }}" title="Archery" /></td>
|
||||
<td><img src="{{ icons['Shield'] }}" class="{{ 'acquired' if 'Shield' in acquired_items }}" title="Shield" /></td>
|
||||
<td><img src="{{ icons['Red Bed'] }}" class="{{ 'acquired' if 'Bed' in acquired_items }}" title="Bed" /></td>
|
||||
<td><img src="{{ icons['Water Bottle'] }}" class="{{ 'acquired' if 'Bottles' in acquired_items }}" title="Bottles" /></td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Netherite Scrap'] }}" class="{{ 'acquired' if '8 Netherite Scrap' in acquired_items }}" title="Netherite Scrap" />
|
||||
<div class="item-count">{{ scrap_count }}</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Flint and Steel'] }}" class="{{ 'acquired' if 'Flint and Steel' in acquired_items }}" title="Flint and Steel" /></td>
|
||||
<td><img src="{{ icons['Enchanting Table'] }}" class="{{ 'acquired' if 'Enchanting' in acquired_items }}" title="Enchanting" /></td>
|
||||
<td><img src="{{ icons['Fishing Rod'] }}" class="{{ 'acquired' if 'Fishing Rod' in acquired_items }}" title="Fishing Rod" /></td>
|
||||
<td><img src="{{ icons['Campfire'] }}" class="{{ 'acquired' if 'Campfire' in acquired_items }}" title="Campfire" /></td>
|
||||
<td><img src="{{ icons['Dragon Head'] }}" class="{{ 'acquired' if game_finished }}" title="Ender Dragon" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
<table id="location-table">
|
||||
{% for area in checks_done %}
|
||||
<tr class="location-category" id="{{area}}-header">
|
||||
<td>{{ area }} {{'▼' if area != 'Total'}}</td>
|
||||
<td class="counter">{{ checks_done[area] }} / {{ checks_in_area[area] }}</td>
|
||||
</tr>
|
||||
<tbody class="locations hide" id="{{area}}">
|
||||
{% for location in location_info[area] %}
|
||||
<tr>
|
||||
<td class="location-name">{{ location }}</td>
|
||||
<td class="counter">{{ '✔' if location_info[area][location] else '' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,8 +1,8 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>A Link to the Past Settings</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/zelda3/player-settings.css") }}" />
|
||||
<title>{{ game }} Settings</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/player-settings.css") }}" />
|
||||
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/player-settings.js") }}"></script>
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="player-settings">
|
||||
<div id="player-settings" data-game="{{ game }}">
|
||||
<div id="user-message"></div>
|
||||
<h1><span id="game-name">Player</span> Settings</h1>
|
||||
<p>Choose the options you would like to play with! You may generate a single-player game from this page,
|
||||
|
||||
@@ -424,6 +424,101 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
||||
key_locations=player_small_key_locations[tracked_player],
|
||||
big_key_locations=player_big_key_locations[tracked_player],
|
||||
**display_data)
|
||||
elif games[tracked_player] == "Minecraft":
|
||||
minecraft_icons = {
|
||||
"Wooden Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d2/Wooden_Pickaxe_JE3_BE3.png",
|
||||
"Stone Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c4/Stone_Pickaxe_JE2_BE2.png",
|
||||
"Iron Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d1/Iron_Pickaxe_JE3_BE2.png",
|
||||
"Diamond Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e7/Diamond_Pickaxe_JE3_BE3.png",
|
||||
"Wooden Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d5/Wooden_Sword_JE2_BE2.png",
|
||||
"Stone Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b1/Stone_Sword_JE2_BE2.png",
|
||||
"Iron Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/8/8e/Iron_Sword_JE2_BE2.png",
|
||||
"Diamond Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/4/44/Diamond_Sword_JE3_BE3.png",
|
||||
"Leather Tunic": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b7/Leather_Tunic_JE4_BE2.png",
|
||||
"Iron Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Iron_Chestplate_JE2_BE2.png",
|
||||
"Diamond Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e0/Diamond_Chestplate_JE3_BE2.png",
|
||||
"Iron Ingot": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Iron_Ingot_JE3_BE2.png",
|
||||
"Block of Iron": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7e/Block_of_Iron_JE4_BE3.png",
|
||||
"Brewing Stand": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fa/Brewing_Stand.png",
|
||||
"Ender Pearl": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/f6/Ender_Pearl_JE3_BE2.png",
|
||||
"Bucket": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Bucket_JE2_BE2.png",
|
||||
"Bow": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/a/ab/Bow_%28Pull_2%29_JE1_BE1.png",
|
||||
"Shield": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c6/Shield_JE2_BE1.png",
|
||||
"Red Bed": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/dc/Red_Bed_JE4_BE3.png",
|
||||
"Netherite Scrap": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/33/Netherite_Scrap_JE2_BE1.png",
|
||||
"Flint and Steel": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/94/Flint_and_Steel_JE4_BE2.png",
|
||||
"Enchanting Table": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Enchanting_Table.gif",
|
||||
"Fishing Rod": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7f/Fishing_Rod_JE2_BE2.png",
|
||||
"Campfire": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/91/Campfire_JE2_BE2.gif",
|
||||
"Water Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png",
|
||||
"Dragon Head": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b6/Dragon_Head.png",
|
||||
}
|
||||
|
||||
minecraft_location_ids = {
|
||||
"Story": [42073, 42080, 42081, 42023, 42082, 42027, 42039, 42085, 42002, 42009, 42010,
|
||||
42070, 42041, 42049, 42090, 42004, 42031, 42025, 42029, 42051, 42077, 42089],
|
||||
"Nether": [42017, 42044, 42069, 42058, 42034, 42060, 42066, 42076, 42064, 42071, 42021,
|
||||
42062, 42008, 42061, 42033, 42011, 42006, 42019, 42000, 42040, 42001, 42015, 42014],
|
||||
"The End": [42052, 42005, 42012, 42032, 42030, 42042, 42018, 42038, 42046],
|
||||
"Adventure": [42047, 42086, 42087, 42050, 42059, 42055, 42072, 42003, 42035, 42016, 42020,
|
||||
42048, 42054, 42068, 42043, 42074, 42075, 42024, 42026, 42037, 42045, 42056, 42088],
|
||||
"Husbandry": [42065, 42067, 42078, 42022, 42007, 42079, 42013, 42028,
|
||||
42036, 42057, 42063, 42053, 42083, 42084, 42091]
|
||||
}
|
||||
|
||||
display_data = {}
|
||||
|
||||
# Determine display for progressive items
|
||||
progressive_items = {
|
||||
"Progressive Tools": 45013,
|
||||
"Progressive Weapons": 45012,
|
||||
"Progressive Armor": 45014,
|
||||
"Progressive Resource Crafting": 45001
|
||||
}
|
||||
progressive_names = {
|
||||
"Progressive Tools": ["Wooden Pickaxe", "Stone Pickaxe", "Iron Pickaxe", "Diamond Pickaxe"],
|
||||
"Progressive Weapons": ["Wooden Sword", "Stone Sword", "Iron Sword", "Diamond Sword"],
|
||||
"Progressive Armor": ["Leather Tunic", "Iron Chestplate", "Diamond Chestplate"],
|
||||
"Progressive Resource Crafting": ["Iron Ingot", "Iron Ingot", "Block of Iron"]
|
||||
}
|
||||
for item_name, item_id in progressive_items.items():
|
||||
level = min(inventory[item_id], len(progressive_names[item_name])-1)
|
||||
display_name = progressive_names[item_name][level]
|
||||
base_name = item_name.split(maxsplit=1)[1].lower().replace(' ', '_')
|
||||
display_data[base_name+"_url"] = minecraft_icons[display_name]
|
||||
|
||||
# Multi-items
|
||||
multi_items = {
|
||||
"3 Ender Pearls": 45029,
|
||||
"8 Netherite Scrap": 45015
|
||||
}
|
||||
for item_name, item_id in multi_items.items():
|
||||
base_name = item_name.split()[-1].lower()
|
||||
count = inventory[item_id]
|
||||
if count >= 0:
|
||||
display_data[base_name+"_count"] = count
|
||||
|
||||
# Victory condition
|
||||
game_state = multisave.get("client_game_state", {}).get((tracked_team, tracked_player), 0)
|
||||
display_data['game_finished'] = game_state == 30
|
||||
|
||||
# Turn location IDs into advancement tab counts
|
||||
checked_locations = multisave.get("location_checks", {}).get((tracked_team, tracked_player), set())
|
||||
lookup_name = lambda id: lookup_any_location_id_to_name[id]
|
||||
location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations}
|
||||
for tab_name, tab_locations in minecraft_location_ids.items()}
|
||||
checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations])
|
||||
for tab_name, tab_locations in minecraft_location_ids.items()}
|
||||
checks_done['Total'] = len(checked_locations)
|
||||
checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in minecraft_location_ids.items()}
|
||||
checks_in_area['Total'] = sum(checks_in_area.values())
|
||||
|
||||
return render_template("minecraftTracker.html",
|
||||
inventory=inventory, icons=minecraft_icons, acquired_items={lookup_any_item_id_to_name[id] for id in inventory if id in lookup_any_item_id_to_name},
|
||||
player=tracked_player, team=tracked_team, room=room, player_name=player_name,
|
||||
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
|
||||
**display_data)
|
||||
|
||||
else:
|
||||
checked_locations = multisave.get("location_checks", {}).get((tracked_team, tracked_player), set())
|
||||
return render_template("genericTracker.html",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
size_hint_y: None
|
||||
height: self.texture_size[1]
|
||||
font_size: dp(20)
|
||||
markup: True
|
||||
<UILog>:
|
||||
viewclass: 'Row'
|
||||
scroll_y: 0
|
||||
|
||||
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 31 KiB |
BIN
data/mcicon.ico
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
@@ -43,6 +43,7 @@ These packets are are sent from the multiworld server to the client. They are no
|
||||
* [Print](#Print)
|
||||
* [PrintJSON](#PrintJSON)
|
||||
* [DataPackage](#DataPackage)
|
||||
* [Bounced](#Bounced)
|
||||
|
||||
### RoomInfo
|
||||
Sent to clients when they connect to an Archipelago server.
|
||||
@@ -148,6 +149,15 @@ Sent to clients to provide what is known as a 'data package' which contains info
|
||||
| ---- | ---- | ----- |
|
||||
| data | DataPackageObject | The data package as a JSON object. More details on its contents may be found at [Data Package Contents](#Data-Package-Contents) |
|
||||
|
||||
### Bounced
|
||||
Sent to clients after a client requested this message be sent to them, more info in the Bounce package.
|
||||
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| data | dict | The data in the Bounce package copied |
|
||||
|
||||
|
||||
## (Client -> Server)
|
||||
These packets are sent purely from client to server. They are not accepted by clients.
|
||||
|
||||
@@ -158,6 +168,7 @@ These packets are sent purely from client to server. They are not accepted by cl
|
||||
* [StatusUpdate](#StatusUpdate)
|
||||
* [Say](#Say)
|
||||
* [GetDataPackage](#GetDataPackage)
|
||||
* [Bounce](#Bounce)
|
||||
|
||||
### Connect
|
||||
Sent by the client to initiate a connection to an Archipelago game session.
|
||||
@@ -220,6 +231,19 @@ Requests the data package from the server. Does not require client authenticatio
|
||||
| ------ | ----- | ------ |
|
||||
| exlusions | list[str] | Optional. If specified, will not send back the specified data. Such as, ["Factorio"] -> Datapackage without Factorio data.|
|
||||
|
||||
### Bounce
|
||||
Send this message to the server, tell it which clients should receive the message and
|
||||
the server will forward the message to all those targets to which any one requirement applies.
|
||||
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ------ | ----- | ------ |
|
||||
| games | list[str] | Optional. Game names that should receive this message |
|
||||
| slots | list[int] | Optional. Player IDs that should receive this message |
|
||||
| tags | list[str] | Optional. Client tags that should receive this message |
|
||||
| data | dict | Any data you want to send |
|
||||
|
||||
|
||||
## Appendix
|
||||
### NetworkPlayer
|
||||
A list of objects. Each object denotes one player. Each object has four fields about the player, in this order: `team`, `slot`, `alias`, and `name`. `team` and `slot` are ints, `alias` and `name` are strs.
|
||||
@@ -340,8 +364,13 @@ Note:
|
||||
#### Contents
|
||||
| Name | Type | Notes |
|
||||
| ------ | ----- | ------ |
|
||||
| games | dict[str, dict] | Mapping of all Games and their respective data |
|
||||
| games[<game_name>]["item_name_to_id"] | dict[int, str] | Mapping of all item names to their respective ID. |
|
||||
| games[<game_name>]["location_name_to_id"] | dict[str, int] | Mapping of all location names to their respective ID. |
|
||||
| games[<game_name>]["version"] | int | Version number of this game's data |
|
||||
| games | dict[str, GameData] | Mapping of all Games and their respective data |
|
||||
| version | int | Sum of all per-game version numbers, for clients that don't bother with per-game caching/updating. |
|
||||
|
||||
#### GameData
|
||||
GameData is a **dict** but contains these keys and values. It's broken out into another "type" for ease of documentation.
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| item_name_to_id | dict[str, int] | Mapping of all item names to their respective ID. |
|
||||
| location_name_to_id | dict[str, int] | Mapping of all location names to their respective ID. |
|
||||
| version | int | Version number of this game's data |
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
<node id="n0">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="608.5730559999993" y="400.2375040000002"/>
|
||||
<y:Geometry height="50.48000000000002" width="524.7469440000009" x="298.1461119999983" y="400.2375040000002"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="106.052734375" x="54.13363281249997" xml:space="preserve" y="15.889414062500009">Archipelago Server<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="106.052734375" x="209.34710481250045" xml:space="preserve" y="15.889414062500009">Archipelago Server<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
@@ -124,7 +124,7 @@
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="608.5730559999993" y="550.637504"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="56.025390625" x="79.14730468749997" xml:space="preserve" y="15.889414062500009">LttPClient<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="104.04296875" x="55.13851562499997" xml:space="preserve" y="15.889414062500009">LttPClient/Z3Client<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
@@ -135,7 +135,7 @@
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="608.5730559999993" y="667.1550080000001"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="77.359375" x="68.48031249999997" xml:space="preserve" y="15.889414062500009">QUSB2SNES<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="24.00390625" x="95.15804687499997" xml:space="preserve" y="15.889414062500009">SNI<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
@@ -234,7 +234,7 @@
|
||||
<y:ProxyAutoBoundsNode>
|
||||
<y:Realizers active="0">
|
||||
<y:GroupNode>
|
||||
<y:Geometry height="211.29396884375012" width="244.31999999999994" x="287.1461119999983" y="362.8610391562502"/>
|
||||
<y:Geometry height="211.29396884375012" width="244.31999999999994" x="298.1461119999983" y="513.26103915625"/>
|
||||
<y:Fill color="#F5F5F5" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" type="dashed" width="1.0"/>
|
||||
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="244.31999999999994" x="0.0" xml:space="preserve" y="0.0">Minecraft</y:NodeLabel>
|
||||
@@ -260,7 +260,7 @@
|
||||
<node id="n4::n0">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="302.1461119999983" y="400.2375040000002"/>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="313.1461119999983" y="550.637504"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="173.40625" x="20.456874999999968" xml:space="preserve" y="15.889414062500009">Modded Minecraft Forge Server<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
@@ -271,7 +271,7 @@
|
||||
<node id="n4::n1">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="302.14611199999837" y="508.6750080000003"/>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="313.14611199999837" y="659.0750080000001"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="117.373046875" x="48.47347656249997" xml:space="preserve" y="15.889414062500009">Any Minecraft Clients<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
@@ -281,13 +281,25 @@
|
||||
</node>
|
||||
</graph>
|
||||
</node>
|
||||
<node id="n5">
|
||||
<data key="d6">
|
||||
<y:ShapeNode>
|
||||
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="313.1461119999983" y="270.35625600000026"/>
|
||||
<y:Fill color="#FFCC00" transparent="false"/>
|
||||
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="110.74609375" x="51.78695312499997" xml:space="preserve" y="15.889414062500009">Modded Subnautica<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||
<y:Shape type="rectangle"/>
|
||||
</y:ShapeNode>
|
||||
</data>
|
||||
</node>
|
||||
<edge id="e0" source="n2::n0" target="n0">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
<y:Path sx="0.0" sy="0.0" tx="155.21347200000048" ty="-25.23531650000018">
|
||||
<y:Point x="715.7330559999992" y="458.5"/>
|
||||
</y:Path>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.359375" x="17.160336273436542" xml:space="preserve" y="-59.31059414453114">WebSocket<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="49.83999999999992" distanceToCenter="true" position="right" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
@@ -295,9 +307,10 @@
|
||||
<edge id="e1" source="n0" target="n2::n0">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
<y:Path sx="155.21347200000048" sy="-7.977504000000181" tx="0.0" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.359375" x="28.320336273436396" xml:space="preserve" y="24.612501558593806">WebSocket<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="60.999999999999886" distanceToCenter="true" position="left" ratio="0.2753403078759233" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
@@ -357,7 +370,7 @@
|
||||
<edge id="e3" source="n1::n0" target="n0">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="71.68000000000018" ty="0.0"/>
|
||||
<y:Path sx="0.0" sy="0.0" tx="175.50327055767139" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.359375" x="-92.86621678125107" xml:space="preserve" y="-39.350590482421694">WebSocket<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
@@ -424,7 +437,7 @@
|
||||
<edge id="e4" source="n3::n2" target="n0">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
<y:Path sx="0.0" sy="0.0" tx="155.21347200000048" ty="-3.977504000000181"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="80.04296875" x="27.978539398436737" xml:space="preserve" y="30.350051386718974">Subprocesses<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="68.0" distanceToCenter="true" position="left" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
@@ -477,7 +490,7 @@
|
||||
<edge id="e5" source="n0" target="n4::n0">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
<y:Path sx="-140.21347200000048" sy="-1.977504000000181" tx="0.0" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
@@ -487,10 +500,10 @@
|
||||
<edge id="e6" source="n4::n0" target="n0">
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
<y:Path sx="0.0" sy="0.0" tx="-140.21347200000048" ty="-12.977504000000181"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.359375" x="21.74755551171802" xml:space="preserve" y="-39.350590482421694">WebSocket<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="left" ratio="1.0" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.359375" x="32.320302673826404" xml:space="preserve" y="-78.31059414453114">WebSocket<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="65.0" distanceToCenter="true" position="right" ratio="0.7667833843973411" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
@@ -511,7 +524,30 @@
|
||||
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="28.0" x="15.999990173826404" xml:space="preserve" y="-38.32934824804664">TCP<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="28.0" x="15.999990173826461" xml:space="preserve" y="-38.32934214453121">TCP<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e7" source="n5" target="n0">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="0.0" sy="0.0" tx="-138.06736000000217" ty="25.21780849999982"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.359375" x="33.223286918485144" xml:space="preserve" y="31.70008944921858">WebSocket<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="65.32870706273742" distanceToCenter="true" position="left" ratio="0.5266279296932641" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
</edge>
|
||||
<edge id="e8" source="n0" target="n5">
|
||||
<data key="d9"/>
|
||||
<data key="d10">
|
||||
<y:PolyLineEdge>
|
||||
<y:Path sx="-140.21347200000048" sy="4.022495999999819" tx="0.0" ty="0.0"/>
|
||||
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||
<y:Arrows source="none" target="standard"/>
|
||||
<y:BendStyle smoothed="false"/>
|
||||
</y:PolyLineEdge>
|
||||
</data>
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
#define sourcepath "build_factorio\exe.win-amd64-3.8\"
|
||||
#define MyAppName "Archipelago Factorio Client"
|
||||
#define MyAppExeName "ArchipelagoGraphicalFactorioClient.exe"
|
||||
#define MyAppIcon "data/icon.ico"
|
||||
|
||||
[Setup]
|
||||
; NOTE: The value of AppId uniquely identifies this application.
|
||||
; Do not use the same AppId value in installers for other applications.
|
||||
AppId={{D13CEBD0-F1D5-4435-A4A6-5243F934613F}}
|
||||
AppName={#MyAppName}
|
||||
AppVerName={#MyAppName}
|
||||
DefaultDirName={commonappdata}\{#MyAppName}
|
||||
DisableProgramGroupPage=yes
|
||||
DefaultGroupName=Archipelago
|
||||
OutputDir=setups
|
||||
OutputBaseFilename=Setup {#MyAppName}
|
||||
Compression=lzma2
|
||||
SolidCompression=yes
|
||||
LZMANumBlockThreads=8
|
||||
ArchitecturesInstallIn64BitMode=x64
|
||||
ChangesAssociations=yes
|
||||
ArchitecturesAllowed=x64
|
||||
AllowNoIcons=yes
|
||||
SetupIconFile={#MyAppIcon}
|
||||
UninstallDisplayIcon={app}\{#MyAppExeName}
|
||||
SignTool= signtool
|
||||
LicenseFile= LICENSE
|
||||
WizardStyle= modern
|
||||
SetupLogging=yes
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}";
|
||||
|
||||
|
||||
[Dirs]
|
||||
NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify;
|
||||
|
||||
[Files]
|
||||
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
|
||||
Source: "{#sourcepath}*"; Excludes: "*.sfc, *.log, data\sprites\alttpr"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\{#MyAppName} Folder"; Filename: "{app}";
|
||||
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}";
|
||||
Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon
|
||||
Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
||||
|
||||
[Run]
|
||||
Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..."
|
||||
|
||||
[UninstallDelete]
|
||||
Type: dirifempty; Name: "{app}"
|
||||
|
||||
|
||||
[Code]
|
||||
// See: https://stackoverflow.com/a/51614652/2287576
|
||||
function IsVCRedist64BitNeeded(): boolean;
|
||||
var
|
||||
strVersion: string;
|
||||
begin
|
||||
if (RegQueryStringValue(HKEY_LOCAL_MACHINE,
|
||||
'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64', 'Version', strVersion)) then
|
||||
begin
|
||||
// Is the installed version at least the packaged one ?
|
||||
Log('VC Redist x64 Version : found ' + strVersion);
|
||||
Result := (CompareStr(strVersion, 'v14.29.30037') < 0);
|
||||
end
|
||||
else
|
||||
begin
|
||||
// Not even an old version installed
|
||||
Log('VC Redist x64 is not already installed');
|
||||
Result := True;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
@@ -45,9 +45,6 @@ server_options:
|
||||
log_network: 0
|
||||
# Options for Generation
|
||||
generator:
|
||||
# Teams
|
||||
# Note that this feature is TODO: to move it to dynamic creation on server, not during generation
|
||||
teams: 1
|
||||
# Location of your Enemizer CLI, available here: https://github.com/Ijwu/Enemizer/releases
|
||||
enemizer_path: "EnemizerCLI/EnemizerCLI.Core.exe"
|
||||
# Folder from which the player yaml files are pulled from
|
||||
@@ -85,4 +82,7 @@ lttp_options:
|
||||
# Alternatively, a path to a program to open the .sfc file with
|
||||
rom_start: true
|
||||
factorio_options:
|
||||
executable: "factorio\\bin\\x64\\factorio"
|
||||
executable: "factorio\\bin\\x64\\factorio"
|
||||
minecraft_options:
|
||||
forge_directory: "Minecraft Forge server"
|
||||
max_heap_size: "2G"
|
||||
@@ -1,19 +1,25 @@
|
||||
#define sourcepath "build\exe.win-amd64-3.8\"
|
||||
#define sourcepath "build\exe.win-amd64-3.8"
|
||||
#define MyAppName "Archipelago"
|
||||
#define MyAppExeName "ArchipelagoLttPClient.exe"
|
||||
#define MyAppExeName "ArchipelagoServer.exe"
|
||||
#define MyAppIcon "data/icon.ico"
|
||||
#dim VersionTuple[4]
|
||||
#define MyAppVersion ParseVersion('build\exe.win-amd64-3.8\ArchipelagoServer.exe', VersionTuple[0], VersionTuple[1], VersionTuple[2], VersionTuple[3])
|
||||
#define MyAppVersionText Str(VersionTuple[0])+"."+Str(VersionTuple[1])+"."+Str(VersionTuple[2])
|
||||
|
||||
|
||||
[Setup]
|
||||
; NOTE: The value of AppId uniquely identifies this application.
|
||||
; Do not use the same AppId value in installers for other applications.
|
||||
AppId={{918BA46A-FAB8-460C-9DFF-AE691E1C865B}}
|
||||
AppName={#MyAppName}
|
||||
AppVerName={#MyAppName}
|
||||
AppCopyright=Distributed under MIT License
|
||||
AppVerName={#MyAppName} {#MyAppVersionText}
|
||||
VersionInfoVersion={#MyAppVersion}
|
||||
DefaultDirName={commonappdata}\{#MyAppName}
|
||||
DisableProgramGroupPage=yes
|
||||
DefaultGroupName=Archipelago
|
||||
OutputDir=setups
|
||||
OutputBaseFilename=Setup {#MyAppName}
|
||||
OutputBaseFilename=Setup {#MyAppName} {#MyAppVersionText}
|
||||
Compression=lzma2
|
||||
SolidCompression=yes
|
||||
LZMANumBlockThreads=8
|
||||
@@ -23,6 +29,7 @@ ArchitecturesAllowed=x64
|
||||
AllowNoIcons=yes
|
||||
SetupIconFile={#MyAppIcon}
|
||||
UninstallDisplayIcon={app}\{#MyAppExeName}
|
||||
; you will likely have to remove the following signtool line when testing/debugging localy. Don't include that change in PRs.
|
||||
SignTool= signtool
|
||||
LicenseFile= LICENSE
|
||||
WizardStyle= modern
|
||||
@@ -34,44 +41,86 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}";
|
||||
|
||||
[Types]
|
||||
Name: "full"; Description: "Full installation"
|
||||
Name: "hosting"; Description: "Installation for hosting purposes"
|
||||
Name: "playing"; Description: "Installation for playing purposes"
|
||||
Name: "custom"; Description: "Custom installation"; Flags: iscustom
|
||||
|
||||
[Components]
|
||||
Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed
|
||||
Name: "generator"; Description: "Generator"; Types: full hosting
|
||||
Name: "server"; Description: "Server"; Types: full hosting
|
||||
Name: "client"; Description: "Clients"; Types: full playing
|
||||
Name: "client/lttp"; Description: "A Link to the Past"; Types: full playing hosting
|
||||
Name: "client/factorio"; Description: "Factorio"; Types: full playing
|
||||
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
|
||||
|
||||
[Dirs]
|
||||
NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify;
|
||||
|
||||
[Files]
|
||||
Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external
|
||||
Source: "{#sourcepath}*"; Excludes: "*.sfc, *.log, data\sprites\alttpr"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external; Components: client/lttp or generator
|
||||
Source: "{#sourcepath}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, *exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "{#sourcepath}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/lttp
|
||||
Source: "{#sourcepath}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator
|
||||
|
||||
Source: "{#sourcepath}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator
|
||||
Source: "{#sourcepath}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server
|
||||
Source: "{#sourcepath}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio
|
||||
Source: "{#sourcepath}\ArchipelagoLttPClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/lttp
|
||||
Source: "{#sourcepath}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/lttp
|
||||
Source: "{#sourcepath}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft
|
||||
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
|
||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||
|
||||
;minecraft temp files
|
||||
Source: "{tmp}\forge-installer.jar"; DestDir: "{app}"; Flags: skipifsourcedoesntexist external deleteafterinstall; Components: client/minecraft
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\{#MyAppName} Folder"; Filename: "{app}";
|
||||
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}";
|
||||
Name: "{group}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Components: server
|
||||
Name: "{group}\{#MyAppName} LttP Client"; Filename: "{app}\ArchipelagoLttPClient.exe"; Components: client/lttp
|
||||
Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Components: client/factorio
|
||||
Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft
|
||||
Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon
|
||||
Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
||||
Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Components: server
|
||||
Name: "{commondesktop}\{#MyAppName} LttP Client"; Filename: "{app}\ArchipelagoLttPClient.exe"; Tasks: desktopicon; Components: client/lttp
|
||||
Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Tasks: desktopicon; Components: client/factorio
|
||||
|
||||
[Run]
|
||||
|
||||
Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..."
|
||||
Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."
|
||||
Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Components: client/lttp or generator
|
||||
Filename: "{app}\jre8\bin\java.exe"; Parameters: "-jar ""{app}\forge-installer.jar"" --installServer ""{app}\Minecraft Forge server"""; Flags: runhidden; Check: IsForgeNeeded(); StatusMsg: "Installing Forge Server..."; Components: client/minecraft
|
||||
|
||||
[UninstallDelete]
|
||||
Type: dirifempty; Name: "{app}"
|
||||
|
||||
[Registry]
|
||||
|
||||
Root: HKCR; Subkey: ".apbp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""
|
||||
Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Archipelago Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""
|
||||
Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\{#MyAppExeName},0"; ValueType: string; ValueName: ""
|
||||
Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; ValueType: string; ValueName: ""
|
||||
Root: HKCR; Subkey: ".apbp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/lttp
|
||||
Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Archipelago Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/lttp
|
||||
Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\ArchipelagoLttPClient.exe,0"; ValueType: string; ValueName: ""; Components: client/lttp
|
||||
Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\ArchipelagoLttPClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/lttp
|
||||
|
||||
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{app}\ArchipelagoServer.exe"" ""%1"""; ValueType: string; ValueName: ""
|
||||
Root: HKCR; Subkey: ".apmc"; ValueData: "{#MyAppName}mcdata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/minecraft
|
||||
Root: HKCR; Subkey: "{#MyAppName}mcdata"; ValueData: "Archipelago Minecraft Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/minecraft
|
||||
Root: HKCR; Subkey: "{#MyAppName}mcdata\DefaultIcon"; ValueData: "{app}\ArchipelagoMinecraftClient.exe,0"; ValueType: string; ValueName: ""; Components: client/minecraft
|
||||
Root: HKCR; Subkey: "{#MyAppName}mcdata\shell\open\command"; ValueData: """{app}\ArchipelagoMinecraftClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/minecraft
|
||||
|
||||
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: server
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: server
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""; Components: server
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{app}\ArchipelagoServer.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: server
|
||||
|
||||
|
||||
|
||||
[Code]
|
||||
const
|
||||
SHCONTCH_NOPROGRESSBOX = 4;
|
||||
SHCONTCH_RESPONDYESTOALL = 16;
|
||||
FORGE_VERSION = '1.16.5-36.2.0';
|
||||
|
||||
// See: https://stackoverflow.com/a/51614652/2287576
|
||||
function IsVCRedist64BitNeeded(): boolean;
|
||||
var
|
||||
@@ -92,11 +141,54 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
function IsForgeNeeded(): boolean;
|
||||
begin
|
||||
Result := True;
|
||||
if (FileExists(ExpandConstant('{app}')+'\Minecraft Forge Server\forge-'+FORGE_VERSION+'.jar')) then
|
||||
Result := False;
|
||||
end;
|
||||
|
||||
function IsJavaNeeded(): boolean;
|
||||
begin
|
||||
Result := True;
|
||||
if (FileExists(ExpandConstant('{app}')+'\jre8\bin\java.exe')) then
|
||||
Result := False;
|
||||
end;
|
||||
|
||||
function OnDownloadMinecraftProgress(const Url, FileName: String; const Progress, ProgressMax: Int64): Boolean;
|
||||
begin
|
||||
if Progress = ProgressMax then
|
||||
Log(Format('Successfully downloaded Minecraft additional files to {tmp}: %s', [FileName]));
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
procedure UnZip(ZipPath, TargetPath: string);
|
||||
var
|
||||
Shell: Variant;
|
||||
ZipFile: Variant;
|
||||
TargetFolder: Variant;
|
||||
begin
|
||||
Shell := CreateOleObject('Shell.Application');
|
||||
|
||||
ZipFile := Shell.NameSpace(ZipPath);
|
||||
if VarIsClear(ZipFile) then
|
||||
RaiseException(
|
||||
Format('ZIP file "%s" does not exist or cannot be opened', [ZipPath]));
|
||||
|
||||
TargetFolder := Shell.NameSpace(TargetPath);
|
||||
if VarIsClear(TargetFolder) then
|
||||
RaiseException(Format('Target path "%s" does not exist', [TargetPath]));
|
||||
|
||||
TargetFolder.CopyHere(
|
||||
ZipFile.Items, SHCONTCH_NOPROGRESSBOX or SHCONTCH_RESPONDYESTOALL);
|
||||
end;
|
||||
|
||||
var ROMFilePage: TInputFileWizardPage;
|
||||
var R : longint;
|
||||
var rom: string;
|
||||
var MinecraftDownloadPage: TDownloadWizardPage;
|
||||
|
||||
procedure InitializeWizard();
|
||||
procedure AddRomPage();
|
||||
begin
|
||||
rom := FileSearch('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', WizardDirValue());
|
||||
if Length(rom) > 0 then
|
||||
@@ -113,15 +205,64 @@ begin
|
||||
rom := ''
|
||||
ROMFilePage :=
|
||||
CreateInputFilePage(
|
||||
wpLicense,
|
||||
wpSelectComponents,
|
||||
'Select ROM File',
|
||||
'Where is your Zelda no Densetsu - Kamigami no Triforce (Japan).sfc located?',
|
||||
'Select the file, then click Next.');
|
||||
|
||||
ROMFilePage.Add(
|
||||
'Location of ROM file:',
|
||||
'SNES ROM files|*.sfc|All files|*.*',
|
||||
'.sfc');
|
||||
'Location of ROM file:',
|
||||
'SNES ROM files|*.sfc|All files|*.*',
|
||||
'.sfc');
|
||||
end;
|
||||
|
||||
procedure AddMinecraftDownloads();
|
||||
begin
|
||||
MinecraftDownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), @OnDownloadMinecraftProgress);
|
||||
end;
|
||||
|
||||
function NextButtonClick(CurPageID: Integer): Boolean;
|
||||
begin
|
||||
if (CurPageID = wpReady) and (WizardIsComponentSelected('client/minecraft')) then begin
|
||||
MinecraftDownloadPage.Clear;
|
||||
if(IsForgeNeeded()) then
|
||||
MinecraftDownloadPage.Add('https://maven.minecraftforge.net/net/minecraftforge/forge/'+FORGE_VERSION+'/forge-'+FORGE_VERSION+'-installer.jar','forge-installer.jar','');
|
||||
if(IsJavaNeedeD()) then
|
||||
MinecraftDownloadPage.Add('https://corretto.aws/downloads/latest/amazon-corretto-8-x64-windows-jre.zip','java.zip','');
|
||||
MinecraftDownloadPage.Show;
|
||||
try
|
||||
try
|
||||
MinecraftDownloadPage.Download;
|
||||
Result := True;
|
||||
except
|
||||
if MinecraftDownloadPage.AbortedByUser then
|
||||
Log('Aborted by user.')
|
||||
else
|
||||
SuppressibleMsgBox(AddPeriod(GetExceptionMessage), mbCriticalError, MB_OK, IDOK);
|
||||
Result := False;
|
||||
end;
|
||||
finally
|
||||
if( isJavaNeeded() ) then
|
||||
UnZip(ExpandConstant('{tmp}')+'\java.zip',ExpandConstant('{app}'));
|
||||
MinecraftDownloadPage.Hide;
|
||||
end;
|
||||
Result := True;
|
||||
end else
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
procedure InitializeWizard();
|
||||
begin
|
||||
AddRomPage();
|
||||
AddMinecraftDownloads();
|
||||
end;
|
||||
|
||||
|
||||
function ShouldSkipPage(PageID: Integer): Boolean;
|
||||
begin
|
||||
Result := False;
|
||||
if (assigned(ROMFilePage)) and (PageID = ROMFilePage.ID) then
|
||||
Result := not (WizardIsComponentSelected('client/lttp') or WizardIsComponentSelected('generator'));
|
||||
end;
|
||||
|
||||
function GetROMPath(Param: string): string;
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
#define sourcepath "build\exe.win-amd64-3.9\"
|
||||
#define MyAppName "Archipelago"
|
||||
#define MyAppExeName "ArchipelagoLttPClient.exe"
|
||||
#define MyAppIcon "data/icon.ico"
|
||||
|
||||
[Setup]
|
||||
; NOTE: The value of AppId uniquely identifies this application.
|
||||
; Do not use the same AppId value in installers for other applications.
|
||||
AppId={{918BA46A-FAB8-460C-9DFF-AE691E1C865B}}
|
||||
AppName={#MyAppName}
|
||||
AppVerName={#MyAppName}
|
||||
DefaultDirName={commonappdata}\{#MyAppName}
|
||||
DisableProgramGroupPage=yes
|
||||
DefaultGroupName=Archipelago
|
||||
OutputDir=setups
|
||||
OutputBaseFilename=Setup {#MyAppName}
|
||||
Compression=lzma2
|
||||
SolidCompression=yes
|
||||
LZMANumBlockThreads=8
|
||||
ArchitecturesInstallIn64BitMode=x64
|
||||
ChangesAssociations=yes
|
||||
ArchitecturesAllowed=x64
|
||||
AllowNoIcons=yes
|
||||
SetupIconFile={#MyAppIcon}
|
||||
UninstallDisplayIcon={app}\{#MyAppExeName}
|
||||
SignTool= signtool
|
||||
LicenseFile= LICENSE
|
||||
WizardStyle= modern
|
||||
SetupLogging=yes
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}";
|
||||
|
||||
|
||||
[Dirs]
|
||||
NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify;
|
||||
|
||||
[Files]
|
||||
Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external
|
||||
Source: "{#sourcepath}*"; Excludes: "*.sfc, *.log, data\sprites\alttpr"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
|
||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\{#MyAppName} Folder"; Filename: "{app}";
|
||||
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}";
|
||||
Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon
|
||||
Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
||||
|
||||
[Run]
|
||||
Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..."
|
||||
Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."
|
||||
|
||||
[UninstallDelete]
|
||||
Type: dirifempty; Name: "{app}"
|
||||
|
||||
[Registry]
|
||||
|
||||
Root: HKCR; Subkey: ".apbp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""
|
||||
Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Archipelago Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""
|
||||
Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\{#MyAppExeName},0"; ValueType: string; ValueName: ""
|
||||
Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; ValueType: string; ValueName: ""
|
||||
|
||||
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{app}\ArchipelagoServer.exe"" ""%1"""; ValueType: string; ValueName: ""
|
||||
|
||||
|
||||
|
||||
[Code]
|
||||
// See: https://stackoverflow.com/a/51614652/2287576
|
||||
function IsVCRedist64BitNeeded(): boolean;
|
||||
var
|
||||
strVersion: string;
|
||||
begin
|
||||
if (RegQueryStringValue(HKEY_LOCAL_MACHINE,
|
||||
'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64', 'Version', strVersion)) then
|
||||
begin
|
||||
// Is the installed version at least the packaged one ?
|
||||
Log('VC Redist x64 Version : found ' + strVersion);
|
||||
Result := (CompareStr(strVersion, 'v14.29.30037') < 0);
|
||||
end
|
||||
else
|
||||
begin
|
||||
// Not even an old version installed
|
||||
Log('VC Redist x64 is not already installed');
|
||||
Result := True;
|
||||
end;
|
||||
end;
|
||||
|
||||
var ROMFilePage: TInputFileWizardPage;
|
||||
var R : longint;
|
||||
var rom: string;
|
||||
|
||||
procedure InitializeWizard();
|
||||
begin
|
||||
rom := FileSearch('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', WizardDirValue());
|
||||
if Length(rom) > 0 then
|
||||
begin
|
||||
log('existing ROM found');
|
||||
log(IntToStr(CompareStr(GetMD5OfFile(rom), '03a63945398191337e896e5771f77173')));
|
||||
if CompareStr(GetMD5OfFile(rom), '03a63945398191337e896e5771f77173') = 0 then
|
||||
begin
|
||||
log('existing ROM verified');
|
||||
exit;
|
||||
end;
|
||||
log('existing ROM failed verification');
|
||||
end;
|
||||
rom := ''
|
||||
ROMFilePage :=
|
||||
CreateInputFilePage(
|
||||
wpLicense,
|
||||
'Select ROM File',
|
||||
'Where is your Zelda no Densetsu - Kamigami no Triforce (Japan).sfc located?',
|
||||
'Select the file, then click Next.');
|
||||
|
||||
ROMFilePage.Add(
|
||||
'Location of ROM file:',
|
||||
'SNES ROM files|*.sfc|All files|*.*',
|
||||
'.sfc');
|
||||
end;
|
||||
|
||||
function GetROMPath(Param: string): string;
|
||||
begin
|
||||
if Length(rom) > 0 then
|
||||
Result := rom
|
||||
else if Assigned(RomFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetMD5OfFile(ROMFilePage.Values[0]), '03a63945398191337e896e5771f77173')
|
||||
if R <> 0 then
|
||||
MsgBox('ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||
|
||||
Result := ROMFilePage.Values[0]
|
||||
end
|
||||
else
|
||||
Result := '';
|
||||
end;
|
||||
153
kvui.py
Normal file
@@ -0,0 +1,153 @@
|
||||
import os
|
||||
import logging
|
||||
|
||||
os.environ["KIVY_NO_CONSOLELOG"] = "1"
|
||||
os.environ["KIVY_NO_FILELOG"] = "1"
|
||||
os.environ["KIVY_NO_ARGS"] = "1"
|
||||
from kivy.app import App
|
||||
from kivy.base import ExceptionHandler, ExceptionManager, Config
|
||||
from kivy.uix.gridlayout import GridLayout
|
||||
from kivy.uix.textinput import TextInput
|
||||
from kivy.uix.recycleview import RecycleView
|
||||
from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem
|
||||
from kivy.utils import escape_markup
|
||||
from kivy.lang import Builder
|
||||
|
||||
import Utils
|
||||
from NetUtils import JSONtoTextParser, JSONMessagePart
|
||||
|
||||
class GameManager(App):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago"),
|
||||
]
|
||||
|
||||
def __init__(self, ctx):
|
||||
self.ctx = ctx
|
||||
self.commandprocessor = ctx.command_processor(ctx)
|
||||
self.icon = r"data/icon.png"
|
||||
self.json_to_kivy_parser = KivyJSONtoTextParser(ctx)
|
||||
self.log_panels = {}
|
||||
super(GameManager, self).__init__()
|
||||
|
||||
def build(self):
|
||||
self.grid = GridLayout()
|
||||
self.grid.cols = 1
|
||||
|
||||
self.tabs = TabbedPanel()
|
||||
self.tabs.default_tab_text = "All"
|
||||
|
||||
self.log_panels["All"] = self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name)
|
||||
for logger_name, name in
|
||||
self.logging_pairs))
|
||||
|
||||
for logger_name, display_name in self.logging_pairs:
|
||||
bridge_logger = logging.getLogger(logger_name)
|
||||
panel = TabbedPanelItem(text=display_name)
|
||||
self.log_panels[display_name] = panel.content = UILog(bridge_logger)
|
||||
self.tabs.add_widget(panel)
|
||||
|
||||
self.grid.add_widget(self.tabs)
|
||||
textinput = TextInput(size_hint_y=None, height=30, multiline=False)
|
||||
textinput.bind(on_text_validate=self.on_message)
|
||||
self.grid.add_widget(textinput)
|
||||
self.commandprocessor("/help")
|
||||
return self.grid
|
||||
|
||||
def on_stop(self):
|
||||
self.ctx.exit_event.set()
|
||||
|
||||
def on_message(self, textinput: TextInput):
|
||||
try:
|
||||
input_text = textinput.text.strip()
|
||||
textinput.text = ""
|
||||
|
||||
if self.ctx.input_requests > 0:
|
||||
self.ctx.input_requests -= 1
|
||||
self.ctx.input_queue.put_nowait(input_text)
|
||||
elif input_text:
|
||||
self.commandprocessor(input_text)
|
||||
except Exception as e:
|
||||
logging.getLogger("Client").exception(e)
|
||||
|
||||
def print_json(self, data):
|
||||
text = self.json_to_kivy_parser(data)
|
||||
self.log_panels["Archipelago"].on_message_markup(text)
|
||||
self.log_panels["All"].on_message_markup(text)
|
||||
|
||||
|
||||
class FactorioManager(GameManager):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago"),
|
||||
("FactorioServer", "Factorio Server Log"),
|
||||
("FactorioWatcher", "Bridge Data Log"),
|
||||
]
|
||||
title = "Archipelago Factorio Client"
|
||||
|
||||
class LttPManager(GameManager):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago"),
|
||||
("SNES", "SNES"),
|
||||
]
|
||||
title = "Archipelago LttP Client"
|
||||
|
||||
class LogtoUI(logging.Handler):
|
||||
def __init__(self, on_log):
|
||||
super(LogtoUI, self).__init__(logging.DEBUG)
|
||||
self.on_log = on_log
|
||||
|
||||
def handle(self, record: logging.LogRecord) -> None:
|
||||
self.on_log(record)
|
||||
|
||||
|
||||
class UILog(RecycleView):
|
||||
cols = 1
|
||||
|
||||
def __init__(self, *loggers_to_handle, **kwargs):
|
||||
super(UILog, self).__init__(**kwargs)
|
||||
self.data = []
|
||||
for logger in loggers_to_handle:
|
||||
logger.addHandler(LogtoUI(self.on_log))
|
||||
|
||||
def on_log(self, record: logging.LogRecord) -> None:
|
||||
self.data.append({"text": escape_markup(record.getMessage())})
|
||||
|
||||
def on_message_markup(self, text):
|
||||
self.data.append({"text": text})
|
||||
|
||||
|
||||
class E(ExceptionHandler):
|
||||
logger = logging.getLogger("Client")
|
||||
|
||||
def handle_exception(self, inst):
|
||||
self.logger.exception(inst)
|
||||
return ExceptionManager.RAISE
|
||||
|
||||
|
||||
class KivyJSONtoTextParser(JSONtoTextParser):
|
||||
color_codes = {
|
||||
# not exact color names, close enough but decent looking
|
||||
"black": "000000",
|
||||
"red": "EE0000",
|
||||
"green": "00FF7F",
|
||||
"yellow": "FAFAD2",
|
||||
"blue": "6495ED",
|
||||
"magenta": "EE00EE",
|
||||
"cyan": "00EEEE",
|
||||
"white": "FFFFFF"
|
||||
}
|
||||
|
||||
def _handle_color(self, node: JSONMessagePart):
|
||||
colors = node["color"].split(";")
|
||||
node["text"] = escape_markup(node["text"])
|
||||
for color in colors:
|
||||
color_code = self.color_codes.get(color, None)
|
||||
if color_code:
|
||||
node["text"] = f"[color={color_code}]{node['text']}[/color]"
|
||||
return self._handle_text(node)
|
||||
return self._handle_text(node)
|
||||
|
||||
|
||||
ExceptionManager.add_handler(E())
|
||||
|
||||
Config.set("input", "mouse", "mouse,disable_multitouch")
|
||||
Builder.load_file(Utils.local_path("data", "client.kv"))
|
||||
@@ -23,13 +23,13 @@ name: YourName{number} # Your name in-game. Spaces will be replaced with undersc
|
||||
#{PLAYER} will be replaced with the player's slot number if that slot number is greater than 1.
|
||||
#{number} will be replaced with the counter value of the name.
|
||||
#{NUMBER} will be replaced with the counter value of the name if the counter value is greater than 1.
|
||||
game:
|
||||
A Link to the Past: 1
|
||||
Factorio: 1
|
||||
Minecraft: 1
|
||||
Subnautica: 1
|
||||
game: # Pick a game to play
|
||||
A Link to the Past: 0
|
||||
Factorio: 0
|
||||
Minecraft: 0
|
||||
Subnautica: 0
|
||||
requires:
|
||||
version: 0.1.5 # Version of Archipelago required for this yaml to work as expected.
|
||||
version: 0.1.6 # Version of Archipelago required for this yaml to work as expected.
|
||||
# Shared Options supported by all games:
|
||||
accessibility:
|
||||
items: 0 # Guarantees you will be able to acquire all items, but you may not be able to access all locations
|
||||
@@ -94,6 +94,10 @@ Factorio:
|
||||
hard : 0
|
||||
very_hard : 0
|
||||
insane : 0
|
||||
silo:
|
||||
vanilla: 1
|
||||
randomize_recipe: 0
|
||||
spawn: 0 # spawn silo near player spawn point
|
||||
free_samples:
|
||||
none: 1
|
||||
single_craft: 0
|
||||
@@ -102,6 +106,7 @@ Factorio:
|
||||
progressive:
|
||||
on: 1
|
||||
off: 0
|
||||
grouped_random: 0
|
||||
tech_tree_information:
|
||||
none: 0
|
||||
advancement: 0 # show which items are a logical advancement
|
||||
@@ -112,6 +117,40 @@ Factorio:
|
||||
starting_items:
|
||||
burner-mining-drill: 19
|
||||
stone-furnace: 19
|
||||
# Note: Total amount of traps cannot exceed 4, if the sum of them is higher it will get automatically capped.
|
||||
evolution_traps:
|
||||
# Trap items that when received increase the enemy evolution.
|
||||
0: 1
|
||||
1: 0
|
||||
2: 0
|
||||
3: 0
|
||||
4: 0
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-middle: 0
|
||||
random-high: 0
|
||||
evolution_trap_increase:
|
||||
# If present, % increase of Evolution with each trap received.
|
||||
5: 0
|
||||
10: 1
|
||||
15: 0
|
||||
20: 0
|
||||
100: 0
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-middle: 0
|
||||
random-high: 0
|
||||
attack_traps:
|
||||
# Trap items that when received trigger an attack on your base.
|
||||
0: 1
|
||||
1: 0
|
||||
2: 0
|
||||
3: 0
|
||||
4: 0
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-middle: 0
|
||||
random-high: 0
|
||||
world_gen:
|
||||
# frequency, size, richness, terrain segmentation, starting area and water are all of https://wiki.factorio.com/Types/MapGenSize
|
||||
# inverse of water scale
|
||||
@@ -184,12 +223,22 @@ Factorio:
|
||||
min_expansion_cooldown: 14400 # 1 to 60 min in ticks
|
||||
max_expansion_cooldown: 216000 # 5 to 180 min in ticks
|
||||
Minecraft:
|
||||
advancement_goal: 50 # Number of advancements required (out of 92 total) to spawn the Ender Dragon and complete the game.
|
||||
advancement_goal: 50 # Number of advancements required (87 max) to spawn the Ender Dragon and complete the game.
|
||||
egg_shards_required: # Number of dragon egg shards to collect (30 max) before the Ender Dragon will spawn.
|
||||
0: 1
|
||||
5: 0
|
||||
10: 0
|
||||
20: 0
|
||||
egg_shards_available: # Number of egg shards available in the pool (30 max).
|
||||
0: 1
|
||||
5: 0
|
||||
10: 0
|
||||
20: 0
|
||||
combat_difficulty: # Modifies the level of items logically required for exploring dangerous areas and fighting bosses.
|
||||
easy: 0
|
||||
normal: 1
|
||||
hard: 0
|
||||
include_hard_advancements: # Junk-fills certain RNG-reliant or tedious advancements with XP rewards.
|
||||
include_hard_advancements: # Junk-fills certain RNG-reliant or tedious advancements.
|
||||
on: 0
|
||||
off: 1
|
||||
include_insane_advancements: # Junk-fills extremely difficult advancements; this is only How Did We Get Here? and Adventuring Time.
|
||||
@@ -204,9 +253,12 @@ Minecraft:
|
||||
structure_compasses: # Adds structure compasses to the item pool, which point to the nearest indicated structure.
|
||||
on: 0
|
||||
off: 1
|
||||
bee_traps: # Adds bee traps to the item pool, which spawn multiple angered bees around every player when received.
|
||||
on: 0
|
||||
off: 1
|
||||
bee_traps: # Replaces a percentage of junk items with bee traps, which spawn multiple angered bees around every player when received.
|
||||
0: 1
|
||||
25: 0
|
||||
50: 0
|
||||
75: 0
|
||||
100: 0
|
||||
A Link to the Past:
|
||||
### Logic Section ###
|
||||
glitches_required: # Determine the logic required to complete the seed
|
||||
@@ -255,7 +307,7 @@ A Link to the Past:
|
||||
progressive: # Enable or disable progressive items (swords, shields, bow)
|
||||
on: 50 # All items are progressive
|
||||
off: 0 # No items are progressive
|
||||
random: 0 # Randomly decides for all items. Swords could be progressive, shields might not be
|
||||
grouped_random: 0 # Randomly decides for all items. Swords could be progressive, shields might not be
|
||||
entrance_shuffle:
|
||||
none: 50 # Vanilla game map. All entrances and exits lead to their original locations. You probably want this option
|
||||
dungeonssimple: 0 # Shuffle just dungeons amongst each other, swapping dungeons entirely, so Hyrule Castle is always 1 dungeon
|
||||
@@ -396,7 +448,7 @@ A Link to the Past:
|
||||
enemy_damage:
|
||||
default: 50 # Vanilla enemy damage
|
||||
shuffled: 0 # Enemies deal 0 to 4 hearts and armor helps
|
||||
random: 0 # Enemies deal 0 to 8 hearts and armor just reshuffles the damage
|
||||
chaos: 0 # Enemies deal 0 to 8 hearts and armor just reshuffles the damage
|
||||
enemy_health:
|
||||
default: 50 # Vanilla enemy HP
|
||||
easy: 0 # Enemies have reduced health
|
||||
@@ -509,9 +561,9 @@ A Link to the Past:
|
||||
# You can combine these events like this. randomonhit-enter-exit if you want it on hit, enter, exit.
|
||||
randomonall: 0 # Random sprite on any and all currently supported events. Refer to above for the supported events.
|
||||
Link: 50 # To add other sprites: open the gui/Creator, go to adjust, select a sprite and write down the name the gui calls it
|
||||
disablemusic: # If "on", all in-game music will be disabled
|
||||
on: 0
|
||||
off: 50
|
||||
music: # If "off", all in-game music will be disabled
|
||||
on: 50
|
||||
off: 0
|
||||
quickswap: # Enable switching items by pressing the L+R shoulder buttons
|
||||
on: 50
|
||||
off: 0
|
||||
@@ -544,7 +596,7 @@ A Link to the Past:
|
||||
off: 0
|
||||
ow_palettes: # Change the colors of the overworld
|
||||
default: 50 # No changes
|
||||
random: 0 # Shuffle the colors, with harmony in mind
|
||||
good: 0 # Shuffle the colors, with harmony in mind
|
||||
blackout: 0 # everything black / blind mode
|
||||
grayscale: 0
|
||||
negative: 0
|
||||
@@ -554,7 +606,7 @@ A Link to the Past:
|
||||
puke: 0
|
||||
uw_palettes: # Change the colors of caves and dungeons
|
||||
default: 50 # No changes
|
||||
random: 0 # Shuffle the colors, with harmony in mind
|
||||
good: 0 # Shuffle the colors, with harmony in mind
|
||||
blackout: 0 # everything black / blind mode
|
||||
grayscale: 0
|
||||
negative: 0
|
||||
@@ -564,7 +616,7 @@ A Link to the Past:
|
||||
puke: 0
|
||||
hud_palettes: # Change the colors of the hud
|
||||
default: 50 # No changes
|
||||
random: 0 # Shuffle the colors, with harmony in mind
|
||||
good: 0 # Shuffle the colors, with harmony in mind
|
||||
blackout: 0 # everything black / blind mode
|
||||
grayscale: 0
|
||||
negative: 0
|
||||
@@ -574,7 +626,7 @@ A Link to the Past:
|
||||
puke: 0
|
||||
sword_palettes: # Change the colors of swords
|
||||
default: 50 # No changes
|
||||
random: 0 # Shuffle the colors, with harmony in mind
|
||||
good: 0 # Shuffle the colors, with harmony in mind
|
||||
blackout: 0 # everything black / blind mode
|
||||
grayscale: 0
|
||||
negative: 0
|
||||
@@ -584,7 +636,7 @@ A Link to the Past:
|
||||
puke: 0
|
||||
shield_palettes: # Change the colors of shields
|
||||
default: 50 # No changes
|
||||
random: 0 # Shuffle the colors, with harmony in mind
|
||||
good: 0 # Shuffle the colors, with harmony in mind
|
||||
blackout: 0 # everything black / blind mode
|
||||
grayscale: 0
|
||||
negative: 0
|
||||
@@ -641,7 +693,7 @@ linked_options:
|
||||
singularity: 1
|
||||
enemy_damage:
|
||||
shuffled: 1
|
||||
random: 1
|
||||
chaos: 1
|
||||
enemy_health:
|
||||
easy: 1
|
||||
hard: 1
|
||||
|
||||
145
setup.py
@@ -4,18 +4,21 @@ import sys
|
||||
import sysconfig
|
||||
from pathlib import Path
|
||||
import cx_Freeze
|
||||
from kivy_deps import sdl2, glew
|
||||
from Utils import version_tuple
|
||||
|
||||
is_64bits = sys.maxsize > 2 ** 32
|
||||
|
||||
folder = "exe.{platform}-{version}".format(platform=sysconfig.get_platform(),
|
||||
version=sysconfig.get_python_version())
|
||||
buildfolder = Path("build", folder)
|
||||
arch_folder = "exe.{platform}-{version}".format(platform=sysconfig.get_platform(),
|
||||
version=sysconfig.get_python_version())
|
||||
buildfolder = Path("build", arch_folder)
|
||||
sbuildfolder = str(buildfolder)
|
||||
libfolder = Path(buildfolder, "lib")
|
||||
library = Path(libfolder, "library.zip")
|
||||
print("Outputting to: " + sbuildfolder)
|
||||
|
||||
icon = os.path.join("data", "icon.ico")
|
||||
mcicon = os.path.join("data", "mcicon.ico")
|
||||
|
||||
if os.path.exists("X:/pw.txt"):
|
||||
print("Using signtool")
|
||||
@@ -38,38 +41,56 @@ def _threaded_hash(filepath):
|
||||
os.makedirs(buildfolder, exist_ok=True)
|
||||
|
||||
|
||||
def manifest_creation(folder):
|
||||
def manifest_creation(folder, create_hashes=False):
|
||||
# Since the setup is now split into components and the manifest is not,
|
||||
# it makes most sense to just remove the hashes for now. Not aware of anyone using them.
|
||||
hashes = {}
|
||||
manifestpath = os.path.join(folder, "manifest.json")
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
pool = ThreadPoolExecutor()
|
||||
for dirpath, dirnames, filenames in os.walk(folder):
|
||||
for filename in filenames:
|
||||
path = os.path.join(dirpath, filename)
|
||||
hashes[os.path.relpath(path, start=folder)] = pool.submit(_threaded_hash, path)
|
||||
if create_hashes:
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
pool = ThreadPoolExecutor()
|
||||
for dirpath, dirnames, filenames in os.walk(folder):
|
||||
for filename in filenames:
|
||||
path = os.path.join(dirpath, filename)
|
||||
hashes[os.path.relpath(path, start=folder)] = pool.submit(_threaded_hash, path)
|
||||
|
||||
import json
|
||||
from Utils import version_tuple
|
||||
manifest = {"buildtime": buildtime.isoformat(sep=" ", timespec="seconds"),
|
||||
"hashes": {path: hash.result() for path, hash in hashes.items()},
|
||||
"version": version_tuple}
|
||||
manifest = {
|
||||
"buildtime": buildtime.isoformat(sep=" ", timespec="seconds"),
|
||||
"hashes": {path: hash.result() for path, hash in hashes.items()},
|
||||
"version": version_tuple}
|
||||
|
||||
json.dump(manifest, open(manifestpath, "wt"), indent=4)
|
||||
print("Created Manifest")
|
||||
|
||||
|
||||
def remove_sprites_from_folder(folder):
|
||||
for file in os.listdir(folder):
|
||||
if file != ".gitignore":
|
||||
os.remove(folder / file)
|
||||
|
||||
|
||||
scripts = {
|
||||
"LttPClient.py": "ArchipelagoLttPClient",
|
||||
"MultiServer.py": "ArchipelagoServer",
|
||||
"Generate.py": "ArchipelagoGenerate",
|
||||
"LttPAdjuster.py": "ArchipelagoLttPAdjuster"
|
||||
# Core
|
||||
"MultiServer.py": ("ArchipelagoServer", False, icon),
|
||||
"Generate.py": ("ArchipelagoGenerate", False, icon),
|
||||
# LttP
|
||||
"LttPClient.py": ("ArchipelagoLttPClient", True, icon),
|
||||
"LttPAdjuster.py": ("ArchipelagoLttPAdjuster", True, icon),
|
||||
# Factorio
|
||||
"FactorioClient.py": ("ArchipelagoFactorioClient", True, icon),
|
||||
# Minecraft
|
||||
"MinecraftClient.py": ("ArchipelagoMinecraftClient", False, mcicon),
|
||||
}
|
||||
|
||||
exes = []
|
||||
|
||||
for script, scriptname in scripts.items():
|
||||
for script, (scriptname, gui, icon) in scripts.items():
|
||||
exes.append(cx_Freeze.Executable(
|
||||
script=script,
|
||||
target_name=scriptname + ("" if sys.platform == "linux" else ".exe"),
|
||||
icon=icon,
|
||||
base="Win32GUI" if sys.platform == "win32" and gui else None
|
||||
))
|
||||
|
||||
import datetime
|
||||
@@ -78,21 +99,21 @@ buildtime = datetime.datetime.utcnow()
|
||||
|
||||
cx_Freeze.setup(
|
||||
name="Archipelago",
|
||||
version=f"{buildtime.year}.{buildtime.month}.{buildtime.day}.{buildtime.hour}",
|
||||
version=f"{version_tuple.major}.{version_tuple.minor}.{version_tuple.build}",
|
||||
description="Archipelago",
|
||||
executables=exes,
|
||||
options={
|
||||
"build_exe": {
|
||||
"packages": ["websockets", "worlds"],
|
||||
"packages": ["websockets", "worlds", "kivy"],
|
||||
"includes": [],
|
||||
"excludes": ["numpy", "Cython", "PySide2", "PIL",
|
||||
"pandas"],
|
||||
"zip_include_packages": ["*"],
|
||||
"zip_exclude_packages": ["worlds"],
|
||||
"zip_exclude_packages": ["worlds", "kivy"],
|
||||
"include_files": [],
|
||||
"include_msvcr": True,
|
||||
"include_msvcr": False,
|
||||
"replace_paths": [("*", "")],
|
||||
"optimize": 2,
|
||||
"optimize": 1,
|
||||
"build_exe": buildfolder
|
||||
},
|
||||
},
|
||||
@@ -113,6 +134,10 @@ def installfile(path, keep_content=False):
|
||||
print('Warning,', path, 'not found')
|
||||
|
||||
|
||||
for folder in sdl2.dep_bins + glew.dep_bins:
|
||||
shutil.copytree(folder, libfolder, dirs_exist_ok=True)
|
||||
print('copying', folder, '->', libfolder)
|
||||
|
||||
extra_data = ["LICENSE", "data", "EnemizerCLI", "host.yaml", "SNI", "meta.yaml"]
|
||||
|
||||
for data in extra_data:
|
||||
@@ -138,76 +163,6 @@ if signtool:
|
||||
print(f"Signing SNI")
|
||||
os.system(signtool + os.path.join(buildfolder, "SNI", "SNI.exe"))
|
||||
|
||||
alttpr_sprites_folder = buildfolder / "data" / "sprites" / "alttpr"
|
||||
for file in os.listdir(alttpr_sprites_folder):
|
||||
if file != ".gitignore":
|
||||
os.remove(alttpr_sprites_folder / file)
|
||||
|
||||
manifest_creation(buildfolder)
|
||||
|
||||
buildfolder = Path("build_factorio", folder)
|
||||
sbuildfolder = str(buildfolder)
|
||||
libfolder = Path(buildfolder, "lib")
|
||||
library = Path(libfolder, "library.zip")
|
||||
print("Outputting Factorio Client to: " + sbuildfolder)
|
||||
|
||||
os.makedirs(buildfolder, exist_ok=True)
|
||||
|
||||
|
||||
exes = [
|
||||
cx_Freeze.Executable(
|
||||
script="FactorioClient.py",
|
||||
target_name="ArchipelagoFactorioClient" + ("" if sys.platform == "linux" else ".exe"),
|
||||
icon=icon,
|
||||
base="Win32GUI" if sys.platform == "win32" else None
|
||||
)]
|
||||
|
||||
import datetime
|
||||
|
||||
buildtime = datetime.datetime.utcnow()
|
||||
|
||||
cx_Freeze.setup(
|
||||
name="Archipelago Factorio Client",
|
||||
version=f"{buildtime.year}.{buildtime.month}.{buildtime.day}.{buildtime.hour}",
|
||||
description="Archipelago Factorio Client",
|
||||
executables=exes,
|
||||
options={
|
||||
"build_exe": {
|
||||
"packages": ["websockets", "kivy", "worlds"],
|
||||
"includes": [],
|
||||
"excludes": ["numpy", "Cython", "PySide2", "PIL",
|
||||
"pandas"],
|
||||
"zip_include_packages": ["*"],
|
||||
"zip_exclude_packages": ["kivy", "worlds"],
|
||||
"include_files": [],
|
||||
"include_msvcr": True,
|
||||
"replace_paths": [("*", "")],
|
||||
"optimize": 2,
|
||||
"build_exe": buildfolder
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
extra_data = ["LICENSE", "data", "host.yaml", "meta.yaml"]
|
||||
from kivy_deps import sdl2, glew
|
||||
for folder in sdl2.dep_bins+glew.dep_bins:
|
||||
shutil.copytree(folder, buildfolder, dirs_exist_ok=True)
|
||||
for data in extra_data:
|
||||
installfile(Path(data))
|
||||
|
||||
|
||||
os.makedirs(buildfolder / "Players", exist_ok=True)
|
||||
shutil.copyfile("playerSettings.yaml", buildfolder / "Players" / "weightedSettings.yaml")
|
||||
|
||||
if signtool:
|
||||
for exe in exes:
|
||||
print(f"Signing {exe.target_name}")
|
||||
os.system(signtool + os.path.join(buildfolder, exe.target_name))
|
||||
|
||||
alttpr_sprites_folder = buildfolder / "data" / "sprites" / "alttpr"
|
||||
for file in os.listdir(alttpr_sprites_folder):
|
||||
if file != ".gitignore":
|
||||
os.remove(alttpr_sprites_folder / file)
|
||||
remove_sprites_from_folder(buildfolder / "data" / "sprites" / "alttpr")
|
||||
|
||||
manifest_creation(buildfolder)
|
||||
|
||||
@@ -6,31 +6,31 @@ class TestEntrances(TestMinecraft):
|
||||
self.run_entrance_tests([
|
||||
['Nether Portal', False, []],
|
||||
['Nether Portal', False, [], ['Flint and Steel']],
|
||||
['Nether Portal', False, [], ['Ingot Crafting']],
|
||||
['Nether Portal', False, [], ['Progressive Resource Crafting']],
|
||||
['Nether Portal', False, [], ['Progressive Tools']],
|
||||
['Nether Portal', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
|
||||
['Nether Portal', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Bucket']],
|
||||
['Nether Portal', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools']],
|
||||
['Nether Portal', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Bucket']],
|
||||
['Nether Portal', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools']],
|
||||
|
||||
['End Portal', False, []],
|
||||
['End Portal', False, [], ['Brewing']],
|
||||
['End Portal', False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']],
|
||||
['End Portal', False, [], ['Flint and Steel']],
|
||||
['End Portal', False, [], ['Ingot Crafting']],
|
||||
['End Portal', False, [], ['Progressive Resource Crafting']],
|
||||
['End Portal', False, [], ['Progressive Tools']],
|
||||
['End Portal', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
|
||||
['End Portal', False, [], ['Progressive Weapons']],
|
||||
['End Portal', False, [], ['Progressive Armor', 'Shield']],
|
||||
['End Portal', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Bucket',
|
||||
['End Portal', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Bucket',
|
||||
'Progressive Weapons', 'Progressive Armor',
|
||||
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
|
||||
['End Portal', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Bucket',
|
||||
['End Portal', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Bucket',
|
||||
'Progressive Weapons', 'Shield',
|
||||
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
|
||||
['End Portal', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools',
|
||||
['End Portal', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools',
|
||||
'Progressive Weapons', 'Progressive Armor',
|
||||
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
|
||||
['End Portal', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools',
|
||||
['End Portal', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools',
|
||||
'Progressive Weapons', 'Shield',
|
||||
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
|
||||
])
|
||||
@@ -39,53 +39,53 @@ class TestEntrances(TestMinecraft):
|
||||
self.run_entrance_tests([ # Structures 1 and 2 should be logically equivalent
|
||||
['Overworld Structure 1', False, []],
|
||||
['Overworld Structure 1', False, [], ['Progressive Weapons']],
|
||||
['Overworld Structure 1', False, [], ['Ingot Crafting', 'Campfire']],
|
||||
['Overworld Structure 1', True, ['Progressive Weapons', 'Ingot Crafting']],
|
||||
['Overworld Structure 1', False, [], ['Progressive Resource Crafting', 'Campfire']],
|
||||
['Overworld Structure 1', True, ['Progressive Weapons', 'Progressive Resource Crafting']],
|
||||
['Overworld Structure 1', True, ['Progressive Weapons', 'Campfire']],
|
||||
|
||||
['Overworld Structure 2', False, []],
|
||||
['Overworld Structure 2', False, [], ['Progressive Weapons']],
|
||||
['Overworld Structure 2', False, [], ['Ingot Crafting', 'Campfire']],
|
||||
['Overworld Structure 2', True, ['Progressive Weapons', 'Ingot Crafting']],
|
||||
['Overworld Structure 2', False, [], ['Progressive Resource Crafting', 'Campfire']],
|
||||
['Overworld Structure 2', True, ['Progressive Weapons', 'Progressive Resource Crafting']],
|
||||
['Overworld Structure 2', True, ['Progressive Weapons', 'Campfire']],
|
||||
|
||||
['Nether Structure 1', False, []],
|
||||
['Nether Structure 1', False, [], ['Flint and Steel']],
|
||||
['Nether Structure 1', False, [], ['Ingot Crafting']],
|
||||
['Nether Structure 1', False, [], ['Progressive Resource Crafting']],
|
||||
['Nether Structure 1', False, [], ['Progressive Tools']],
|
||||
['Nether Structure 1', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
|
||||
['Nether Structure 1', False, [], ['Progressive Weapons']],
|
||||
['Nether Structure 1', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Bucket', 'Progressive Weapons']],
|
||||
['Nether Structure 1', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons']],
|
||||
['Nether Structure 1', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Bucket', 'Progressive Weapons']],
|
||||
['Nether Structure 1', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons']],
|
||||
|
||||
['Nether Structure 2', False, []],
|
||||
['Nether Structure 2', False, [], ['Flint and Steel']],
|
||||
['Nether Structure 2', False, [], ['Ingot Crafting']],
|
||||
['Nether Structure 2', False, [], ['Progressive Resource Crafting']],
|
||||
['Nether Structure 2', False, [], ['Progressive Tools']],
|
||||
['Nether Structure 2', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
|
||||
['Nether Structure 2', False, [], ['Progressive Weapons']],
|
||||
['Nether Structure 2', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Bucket', 'Progressive Weapons']],
|
||||
['Nether Structure 2', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons']],
|
||||
['Nether Structure 2', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Bucket', 'Progressive Weapons']],
|
||||
['Nether Structure 2', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons']],
|
||||
|
||||
['The End Structure', False, []],
|
||||
['The End Structure', False, [], ['Brewing']],
|
||||
['The End Structure', False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']],
|
||||
['The End Structure', False, [], ['Flint and Steel']],
|
||||
['The End Structure', False, [], ['Ingot Crafting']],
|
||||
['The End Structure', False, [], ['Progressive Resource Crafting']],
|
||||
['The End Structure', False, [], ['Progressive Tools']],
|
||||
['The End Structure', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
|
||||
['The End Structure', False, [], ['Progressive Weapons']],
|
||||
['The End Structure', False, [], ['Progressive Armor', 'Shield']],
|
||||
['The End Structure', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Bucket',
|
||||
['The End Structure', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Bucket',
|
||||
'Progressive Weapons', 'Progressive Armor',
|
||||
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
|
||||
['The End Structure', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Bucket',
|
||||
['The End Structure', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Bucket',
|
||||
'Progressive Weapons', 'Shield',
|
||||
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
|
||||
['The End Structure', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools',
|
||||
['The End Structure', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools',
|
||||
'Progressive Weapons', 'Progressive Armor',
|
||||
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
|
||||
['The End Structure', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools',
|
||||
['The End Structure', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools',
|
||||
'Progressive Weapons', 'Shield',
|
||||
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ from BaseClasses import MultiWorld
|
||||
from worlds import AutoWorld
|
||||
from worlds.minecraft import MinecraftWorld
|
||||
from worlds.minecraft.Items import MinecraftItem, item_table
|
||||
from worlds.minecraft.Options import AdvancementGoal, CombatDifficulty
|
||||
from Options import Toggle
|
||||
from worlds.minecraft.Options import AdvancementGoal, CombatDifficulty, BeeTraps
|
||||
from Options import Toggle, Range
|
||||
|
||||
# Converts the name of an item into an item object
|
||||
def MCItemFactory(items, player: int):
|
||||
@@ -36,8 +36,10 @@ class TestMinecraft(TestBase):
|
||||
setattr(self.world, "advancement_goal", {1: AdvancementGoal(30)})
|
||||
setattr(self.world, "shuffle_structures", {1: Toggle(False)})
|
||||
setattr(self.world, "combat_difficulty", {1: CombatDifficulty(1)}) # normal
|
||||
setattr(self.world, "bee_traps", {1: Toggle(False)})
|
||||
setattr(self.world, "bee_traps", {1: BeeTraps(0)})
|
||||
setattr(self.world, "structure_compasses", {1: Toggle(False)})
|
||||
setattr(self.world, "egg_shards_required", {1: Range(0)})
|
||||
setattr(self.world, "egg_shards_available", {1: Range(0)})
|
||||
AutoWorld.call_single(self.world, "create_regions", 1)
|
||||
AutoWorld.call_single(self.world, "generate_basic", 1)
|
||||
AutoWorld.call_single(self.world, "set_rules", 1)
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
from __future__ import annotations
|
||||
from typing import Dict, Set, Tuple
|
||||
from typing import Dict, Set, Tuple, List, Optional
|
||||
|
||||
from BaseClasses import MultiWorld, Item, CollectionState
|
||||
from BaseClasses import MultiWorld, Item, CollectionState, Location
|
||||
|
||||
|
||||
class AutoWorldRegister(type):
|
||||
world_types:Dict[str, World] = {}
|
||||
world_types: Dict[str, World] = {}
|
||||
|
||||
def __new__(cls, name, bases, dct):
|
||||
dct["all_names"] = dct["item_names"] | dct["location_names"] | set(dct.get("item_name_groups", {}))
|
||||
# filter out any events
|
||||
dct["item_name_to_id"] = {name: id for name, id in dct["item_name_to_id"].items() if id}
|
||||
dct["location_name_to_id"] = {name: id for name, id in dct["location_name_to_id"].items() if id}
|
||||
# build reverse lookups
|
||||
dct["item_id_to_name"] = {code: name for name, code in dct["item_name_to_id"].items()}
|
||||
dct["location_id_to_name"] = {code: name for name, code in dct["location_name_to_id"].items()}
|
||||
|
||||
# build rest
|
||||
dct["item_names"] = frozenset(dct["item_name_to_id"])
|
||||
dct["location_names"] = frozenset(dct["location_name_to_id"])
|
||||
dct["all_names"] = dct["item_names"] | dct["location_names"] | set(dct.get("item_name_groups", {}))
|
||||
|
||||
# construct class
|
||||
new_class = super().__new__(cls, name, bases, dct)
|
||||
if "game" in dct:
|
||||
@@ -39,8 +44,22 @@ def call_single(world: MultiWorld, method_name: str, player: int, *args):
|
||||
|
||||
|
||||
def call_all(world: MultiWorld, method_name: str, *args):
|
||||
world_types = set()
|
||||
for player in world.player_ids:
|
||||
world_types.add(world.worlds[player].__class__)
|
||||
call_single(world, method_name, player, *args)
|
||||
for world_type in world_types:
|
||||
stage_callable = getattr(world_type, f"stage_{method_name}", None)
|
||||
if stage_callable:
|
||||
stage_callable(world, *args)
|
||||
|
||||
|
||||
def call_stage(world: MultiWorld, method_name: str, *args):
|
||||
world_types = {world.worlds[player].__class__ for player in world.player_ids}
|
||||
for world_type in world_types:
|
||||
stage_callable = getattr(world_type, f"stage_{method_name}", None)
|
||||
if stage_callable:
|
||||
stage_callable(world, *args)
|
||||
|
||||
|
||||
class World(metaclass=AutoWorldRegister):
|
||||
@@ -48,21 +67,16 @@ class World(metaclass=AutoWorldRegister):
|
||||
A Game should have its own subclass of World in which it defines the required data structures."""
|
||||
|
||||
options: dict = {} # link your Options mapping
|
||||
game: str # name the game
|
||||
game: str # name the game
|
||||
topology_present: bool = False # indicate if world type has any meaningful layout/pathing
|
||||
item_names: Set[str] = frozenset() # set of all potential item names
|
||||
# maps item group names to sets of items. Example: "Weapons" -> {"Sword", "Bow"}
|
||||
item_name_groups: Dict[str, Set[str]] = {}
|
||||
location_names: Set[str] = frozenset() # set of all potential location names
|
||||
all_names: Set[str] = frozenset() # gets automatically populated with all item, item group and location names
|
||||
|
||||
# map names to their IDs
|
||||
item_name_to_id: Dict[str, int] = {}
|
||||
location_name_to_id: Dict[str, int] = {}
|
||||
|
||||
# reverse, automatically generated
|
||||
item_id_to_name: Dict[int, str] = {}
|
||||
location_id_to_name: Dict[int, str] = {}
|
||||
# maps item group names to sets of items. Example: "Weapons" -> {"Sword", "Bow"}
|
||||
item_name_groups: Dict[str, Set[str]] = {}
|
||||
|
||||
data_version = 1 # increment this every time something in your world's names/id mappings changes.
|
||||
|
||||
@@ -78,11 +92,21 @@ class World(metaclass=AutoWorldRegister):
|
||||
world: MultiWorld
|
||||
player: int
|
||||
|
||||
# automatically generated
|
||||
item_id_to_name: Dict[int, str]
|
||||
location_id_to_name: Dict[int, str]
|
||||
|
||||
item_names: Set[str] # set of all potential item names
|
||||
location_names: Set[str] # set of all potential location names
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
self.world = world
|
||||
self.player = player
|
||||
|
||||
# overridable methods that get called by Main.py, sorted by execution order
|
||||
# can also be implemented as a classmethod and called "stage_<original_name",
|
||||
# in that case the MultiWorld object is passed as an argument and it gets called once for the entire multiworld.
|
||||
# An example of this can be found in alttp as stage_pre_fill
|
||||
def generate_early(self):
|
||||
pass
|
||||
|
||||
@@ -98,6 +122,16 @@ class World(metaclass=AutoWorldRegister):
|
||||
def generate_basic(self):
|
||||
pass
|
||||
|
||||
def pre_fill(self):
|
||||
"""Optional method that is supposed to be used for special fill stages. This is run *after* plando."""
|
||||
pass
|
||||
|
||||
def fill_hook(cls, progitempool: List[Item], nonexcludeditempool: List[Item],
|
||||
localrestitempool: Dict[int, List[Item]], restitempool: List[Item], fill_locations: List[Location]):
|
||||
"""Special method that gets called as part of distribute_items_restrictive (main fill).
|
||||
This gets called once per present world type."""
|
||||
pass
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
"""This method gets called from a threadpool, do not use world.random here.
|
||||
If you need any last-second randomization, use MultiWorld.slot_seeds[slot] instead."""
|
||||
@@ -107,23 +141,42 @@ class World(metaclass=AutoWorldRegister):
|
||||
"""Fill in the slot_data field in the Connected network package."""
|
||||
return {}
|
||||
|
||||
def modify_multidata(self, multidata: dict):
|
||||
"""For deeper modification of server multidata."""
|
||||
pass
|
||||
|
||||
def get_required_client_version(self) -> Tuple[int, int, int]:
|
||||
return 0, 0, 3
|
||||
|
||||
# end of Main.py calls
|
||||
|
||||
def collect(self, state: CollectionState, item: Item) -> bool:
|
||||
"""Collect an item into state. For speed reasons items that aren't logically useful get skipped."""
|
||||
def collect_item(self, state: CollectionState, item: Item) -> Optional[str]:
|
||||
"""Collect an item name into state. For speed reasons items that aren't logically useful get skipped.
|
||||
Collect None to skip item."""
|
||||
if item.advancement:
|
||||
state.prog_items[item.name, item.player] += 1
|
||||
return True # indicate that a logical state change has occured
|
||||
return False
|
||||
return item.name
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
"""Create an item for this world type and player.
|
||||
Warning: this may be called with self.world = None, for example by MultiServer"""
|
||||
raise NotImplementedError
|
||||
|
||||
# following methods should not need to be overriden.
|
||||
def collect(self, state: CollectionState, item: Item) -> bool:
|
||||
name = self.collect_item(state, item)
|
||||
if name:
|
||||
state.prog_items[name, item.player] += 1
|
||||
return True
|
||||
return False
|
||||
|
||||
def remove(self, state: CollectionState, item: Item) -> bool:
|
||||
name = self.collect_item(state, item)
|
||||
if name:
|
||||
state.prog_items[name, item.player] -= 1
|
||||
if state.prog_items[name, item.player] < 1:
|
||||
del (state.prog_items[name, item.player])
|
||||
return True
|
||||
return False
|
||||
|
||||
# any methods attached to this can be used as part of CollectionState,
|
||||
# please use a prefix as all of them get clobbered together
|
||||
|
||||
@@ -27,8 +27,10 @@ for world_name, world in AutoWorldRegister.world_types.items():
|
||||
lookup_any_location_id_to_name.update(world.location_id_to_name)
|
||||
|
||||
network_data_package = {
|
||||
# Remove with 0.2.0
|
||||
"lookup_any_location_id_to_name": lookup_any_location_id_to_name, # legacy, to be removed
|
||||
"lookup_any_item_id_to_name": lookup_any_item_id_to_name, # legacy, to be removed
|
||||
|
||||
"version": sum(world.data_version for world in AutoWorldRegister.world_types.values()),
|
||||
"games": games,
|
||||
}
|
||||
|
||||
@@ -44,75 +44,6 @@ def create_dungeons(world, player):
|
||||
|
||||
world.dungeons += [ES, EP, DP, ToH, AT, PoD, TT, SW, SP, IP, MM, TR, GT]
|
||||
|
||||
def fill_dungeons(world):
|
||||
#All chests on the freebes list locked behind a key in room with no other exit
|
||||
freebes = ['Ganons Tower - Map Chest', 'Palace of Darkness - Harmless Hellway', 'Palace of Darkness - Big Key Chest', 'Turtle Rock - Big Key Chest']
|
||||
|
||||
all_state_base = world.get_all_state()
|
||||
|
||||
dungeons = [(list(dungeon.regions), dungeon.big_key, list(dungeon.small_keys), list(dungeon.dungeon_items)) for dungeon in world.dungeons]
|
||||
|
||||
loopcnt = 0
|
||||
while dungeons:
|
||||
loopcnt += 1
|
||||
dungeon_regions, big_key, small_keys, dungeon_items = dungeons.pop(0)
|
||||
# this is what we need to fill
|
||||
dungeon_locations = [location for location in world.get_unfilled_locations() if location.parent_region.name in dungeon_regions]
|
||||
world.random.shuffle(dungeon_locations)
|
||||
|
||||
all_state = all_state_base.copy()
|
||||
|
||||
# first place big key
|
||||
if big_key is not None:
|
||||
bk_location = None
|
||||
for location in dungeon_locations:
|
||||
if location.item_rule(big_key):
|
||||
bk_location = location
|
||||
break
|
||||
|
||||
if bk_location is None:
|
||||
raise RuntimeError('No suitable location for %s' % big_key)
|
||||
|
||||
world.push_item(bk_location, big_key, False)
|
||||
bk_location.event = True
|
||||
bk_location.locked = True
|
||||
dungeon_locations.remove(bk_location)
|
||||
big_key = None
|
||||
|
||||
# next place small keys
|
||||
while small_keys:
|
||||
small_key = small_keys.pop()
|
||||
all_state.sweep_for_events()
|
||||
sk_location = None
|
||||
for location in dungeon_locations:
|
||||
if location.name in freebes or (location.can_reach(all_state) and location.item_rule(small_key)):
|
||||
sk_location = location
|
||||
break
|
||||
|
||||
if sk_location is None:
|
||||
# need to retry this later
|
||||
small_keys.append(small_key)
|
||||
dungeons.append((dungeon_regions, big_key, small_keys, dungeon_items))
|
||||
# infinite regression protection
|
||||
if loopcnt < (30 * world.players):
|
||||
break
|
||||
else:
|
||||
raise RuntimeError('No suitable location for %s' % small_key)
|
||||
|
||||
world.push_item(sk_location, small_key, False)
|
||||
sk_location.event = True
|
||||
sk_location.locked = True
|
||||
dungeon_locations.remove(sk_location)
|
||||
|
||||
if small_keys:
|
||||
# key placement not finished, loop again
|
||||
continue
|
||||
|
||||
# next place dungeon items
|
||||
for dungeon_item in dungeon_items:
|
||||
di_location = dungeon_locations.pop()
|
||||
world.push_item(di_location, dungeon_item, False)
|
||||
|
||||
|
||||
def get_dungeon_item_pool(world):
|
||||
items = [item for dungeon in world.dungeons for item in dungeon.all_items]
|
||||
@@ -120,28 +51,22 @@ def get_dungeon_item_pool(world):
|
||||
item.world = world
|
||||
return items
|
||||
|
||||
|
||||
def fill_dungeons_restrictive(world):
|
||||
"""Places dungeon-native items into their dungeons, places nothing if everything is shuffled outside."""
|
||||
restricted_players = {player for player, restricted in world.restrict_dungeon_item_on_boss.items() if restricted}
|
||||
|
||||
locations = [location for location in world.get_unfilled_dungeon_locations()
|
||||
if not (location.player in restricted_players and location.name in lookup_boss_drops)] # filter boss
|
||||
|
||||
world.random.shuffle(locations)
|
||||
all_state_base = world.get_all_state()
|
||||
|
||||
# with shuffled dungeon items they are distributed as part of the normal item pool
|
||||
for item in world.get_items():
|
||||
if (item.smallkey and world.keyshuffle[item.player]) or (item.bigkey and world.bigkeyshuffle[item.player]):
|
||||
all_state_base.collect(item, True)
|
||||
item.advancement = True
|
||||
|
||||
dungeon_items = [item for item in get_dungeon_item_pool(world) if (((item.smallkey and not world.keyshuffle[item.player])
|
||||
or (item.bigkey and not world.bigkeyshuffle[item.player])
|
||||
or (item.map and not world.mapshuffle[item.player])
|
||||
or (item.compass and not world.compassshuffle[item.player])
|
||||
) and world.goal[item.player] != 'icerodhunt')] #
|
||||
dungeon_items = [item for item in get_dungeon_item_pool(world) if
|
||||
(((item.smallkey and not world.keyshuffle[item.player])
|
||||
or (item.bigkey and not world.bigkeyshuffle[item.player])
|
||||
or (item.map and not world.mapshuffle[item.player])
|
||||
or (item.compass and not world.compassshuffle[item.player])
|
||||
) and world.goal[item.player] != 'icerodhunt')]
|
||||
if dungeon_items:
|
||||
restricted_players = {player for player, restricted in world.restrict_dungeon_item_on_boss.items() if restricted}
|
||||
locations = [location for location in world.get_unfilled_dungeon_locations()
|
||||
if not (location.player in restricted_players and location.name in lookup_boss_drops)] # filter boss
|
||||
|
||||
world.random.shuffle(locations)
|
||||
all_state_base = world.get_all_state()
|
||||
# sort in the order Big Key, Small Key, Other before placing dungeon items
|
||||
sort_order = {"BigKey": 3, "SmallKey": 2}
|
||||
dungeon_items.sort(key=lambda item: sort_order.get(item.type, 1))
|
||||
|
||||
@@ -143,20 +143,7 @@ def parse_arguments(argv, no_defaults=False):
|
||||
off.
|
||||
Off: Dungeon counters are never shown.
|
||||
''')
|
||||
parser.add_argument('--progressive', default=defval('on'), const='normal', nargs='?', choices=['on', 'off', 'random'],
|
||||
help='''\
|
||||
Select progressive equipment setting. Affects available itempool. (default: %(default)s)
|
||||
On: Swords, Shields, Armor, and Gloves will
|
||||
all be progressive equipment. Each subsequent
|
||||
item of the same type the player finds will
|
||||
upgrade that piece of equipment by one stage.
|
||||
Off: Swords, Shields, Armor, and Gloves will not
|
||||
be progressive equipment. Higher level items may
|
||||
be found at any time. Downgrades are not possible.
|
||||
Random: Swords, Shields, Armor, and Gloves will, per
|
||||
category, be randomly progressive or not.
|
||||
Link will die in one hit.
|
||||
''')
|
||||
|
||||
parser.add_argument('--algorithm', default=defval('balanced'), const='balanced', nargs='?',
|
||||
choices=['freshness', 'flood', 'vt25', 'vt26', 'balanced'],
|
||||
help='''\
|
||||
@@ -218,22 +205,7 @@ def parse_arguments(argv, no_defaults=False):
|
||||
--seed given will produce the same 10 (different) roms each
|
||||
time).
|
||||
''', type=int)
|
||||
parser.add_argument('--fastmenu', default=defval('normal'), const='normal', nargs='?',
|
||||
choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
|
||||
help='''\
|
||||
Select the rate at which the menu opens and closes.
|
||||
(default: %(default)s)
|
||||
''')
|
||||
parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', action='store_true')
|
||||
parser.add_argument('--disablemusic', help='Disables game music.', action='store_true')
|
||||
parser.add_argument('--triforcehud', default='hide_goal', const='hide_goal', nargs='?', choices=['normal', 'hide_goal', 'hide_required', 'hide_both'],
|
||||
help='''\
|
||||
Hide the triforce hud in certain circumstances.
|
||||
hide_goal will hide the hud until finding a triforce piece, hide_required will hide the total amount needed to win
|
||||
(Both can be revealed when speaking to Murahalda)
|
||||
(default: %(default)s)
|
||||
''')
|
||||
parser.add_argument('--enableflashing', help='Reenable flashing animations (unfriendly to epilepsy, always disabled in race roms)', action='store_false', dest="reduceflashing")
|
||||
|
||||
parser.add_argument('--mapshuffle', default=defval(False),
|
||||
help='Maps are no longer restricted to their dungeons, but can be anywhere',
|
||||
action='store_true')
|
||||
@@ -276,19 +248,6 @@ def parse_arguments(argv, no_defaults=False):
|
||||
If set, the Pyramid Hole and Ganon's Tower are not
|
||||
included entrance shuffle pool.
|
||||
''', action='store_false', dest='shuffleganon')
|
||||
parser.add_argument('--heartbeep', default=defval('normal'), const='normal', nargs='?', choices=['double', 'normal', 'half', 'quarter', 'off'],
|
||||
help='''\
|
||||
Select the rate at which the heart beep sound is played at
|
||||
low health. (default: %(default)s)
|
||||
''')
|
||||
parser.add_argument('--heartcolor', default=defval('red'), const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'],
|
||||
help='Select the color of Link\'s heart meter. (default: %(default)s)')
|
||||
parser.add_argument('--ow_palettes', default=defval('default'), choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
|
||||
parser.add_argument('--uw_palettes', default=defval('default'), choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
|
||||
parser.add_argument('--hud_palettes', default=defval('default'), choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
|
||||
parser.add_argument('--shield_palettes', default=defval('default'), choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
|
||||
parser.add_argument('--sword_palettes', default=defval('default'), choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
|
||||
parser.add_argument('--link_palettes', default=defval('default'), choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
|
||||
|
||||
parser.add_argument('--sprite', help='''\
|
||||
Path to a sprite sheet to use for Link. Needs to be in
|
||||
@@ -380,15 +339,14 @@ def parse_arguments(argv, no_defaults=False):
|
||||
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
|
||||
'local_items', 'non_local_items', 'retro', 'accessibility', 'hints', 'beemizer',
|
||||
'shufflebosses', 'enemy_shuffle', 'enemy_health', 'enemy_damage', 'shufflepots',
|
||||
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor',
|
||||
'heartbeep', "progression_balancing", "triforce_pieces_available",
|
||||
'sprite',
|
||||
"progression_balancing", "triforce_pieces_available",
|
||||
"triforce_pieces_required", "shop_shuffle",
|
||||
"required_medallions", "start_hints",
|
||||
"plando_items", "plando_texts", "plando_connections", "er_seeds",
|
||||
'progressive', 'dungeon_counters', 'glitch_boots', 'killable_thieves',
|
||||
'dungeon_counters', 'glitch_boots', 'killable_thieves',
|
||||
'tile_shuffle', 'bush_shuffle', 'shuffle_prizes', 'sprite_pool', 'dark_room_logic',
|
||||
'restrict_dungeon_item_on_boss', 'reduceflashing', 'game',
|
||||
'hud_palettes', 'sword_palettes', 'shield_palettes', 'link_palettes', 'triforcehud']:
|
||||
'restrict_dungeon_item_on_boss', 'game']:
|
||||
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
|
||||
if player == 1:
|
||||
setattr(ret, name, {1: value})
|
||||
|
||||
@@ -1064,7 +1064,7 @@ def link_entrances(world, player):
|
||||
connect_doors(world, single_doors, door_targets, player)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f'{world.shuffle[player]} Shuffling not supported yet. Player {world.get_player_names(player)}')
|
||||
f'{world.shuffle[player]} Shuffling not supported yet. Player {world.get_player_name(player)}')
|
||||
|
||||
# mandatory hybrid major glitches connections
|
||||
if world.logic[player] in ['hybridglitches', 'nologic']:
|
||||
|
||||
@@ -399,7 +399,7 @@ def generate_itempool(world):
|
||||
if additional_triforce_pieces:
|
||||
if additional_triforce_pieces > len(nonprogressionitems):
|
||||
raise FillError(f"Not enough non-progression items to replace with Triforce pieces found for player "
|
||||
f"{world.get_player_names(player)}.")
|
||||
f"{world.get_player_name(player)}.")
|
||||
progressionitems += [ItemFactory("Triforce Piece", player)] * additional_triforce_pieces
|
||||
nonprogressionitems.sort(key=lambda item: int("Heart" in item.name)) # try to keep hearts in the pool
|
||||
nonprogressionitems = nonprogressionitems[additional_triforce_pieces:]
|
||||
@@ -492,6 +492,7 @@ def set_up_take_anys(world, player):
|
||||
|
||||
world.initialize_regions()
|
||||
|
||||
|
||||
def create_dynamic_shop_locations(world, player):
|
||||
for shop in world.shops:
|
||||
if shop.region.player == player:
|
||||
@@ -511,35 +512,7 @@ def create_dynamic_shop_locations(world, player):
|
||||
loc.locked = True
|
||||
|
||||
|
||||
def fill_prizes(world, attempts=15):
|
||||
all_state = world.get_all_state(keys=True)
|
||||
for player in world.get_game_players("A Link to the Past"):
|
||||
crystals = ItemFactory(['Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6'], player)
|
||||
crystal_locations = [world.get_location('Turtle Rock - Prize', player), world.get_location('Eastern Palace - Prize', player), world.get_location('Desert Palace - Prize', player), world.get_location('Tower of Hera - Prize', player), world.get_location('Palace of Darkness - Prize', player),
|
||||
world.get_location('Thieves\' Town - Prize', player), world.get_location('Skull Woods - Prize', player), world.get_location('Swamp Palace - Prize', player), world.get_location('Ice Palace - Prize', player),
|
||||
world.get_location('Misery Mire - Prize', player)]
|
||||
placed_prizes = {loc.item.name for loc in crystal_locations if loc.item}
|
||||
unplaced_prizes = [crystal for crystal in crystals if crystal.name not in placed_prizes]
|
||||
empty_crystal_locations = [loc for loc in crystal_locations if not loc.item]
|
||||
for attempt in range(attempts):
|
||||
try:
|
||||
prizepool = unplaced_prizes.copy()
|
||||
prize_locs = empty_crystal_locations.copy()
|
||||
world.random.shuffle(prize_locs)
|
||||
fill_restrictive(world, all_state, prize_locs, prizepool, True, lock=True)
|
||||
except FillError as e:
|
||||
logging.getLogger('').exception("Failed to place dungeon prizes (%s). Will retry %s more times", e,
|
||||
attempts - attempt)
|
||||
for location in empty_crystal_locations:
|
||||
location.item = None
|
||||
continue
|
||||
break
|
||||
else:
|
||||
raise FillError('Unable to place dungeon prizes')
|
||||
|
||||
|
||||
def get_pool_core(world, player: int):
|
||||
progressive = world.progressive[player]
|
||||
shuffle = world.shuffle[player]
|
||||
difficulty = world.difficulty[player]
|
||||
timer = world.timer[player]
|
||||
@@ -563,16 +536,14 @@ def get_pool_core(world, player: int):
|
||||
assert loc not in placed_items
|
||||
placed_items[loc] = item
|
||||
|
||||
def want_progressives():
|
||||
return world.random.choice([True, False]) if progressive == 'random' else progressive == 'on'
|
||||
|
||||
# provide boots to major glitch dependent seeds
|
||||
if logic in {'owglitches', 'hybridglitches', 'nologic'} and world.glitch_boots[player] and goal != 'icerodhunt':
|
||||
precollected_items.append('Pegasus Boots')
|
||||
pool.remove('Pegasus Boots')
|
||||
pool.append('Rupees (20)')
|
||||
want_progressives = world.progressive[player].want_progressives
|
||||
|
||||
if want_progressives():
|
||||
if want_progressives(world.random):
|
||||
pool.extend(diff.progressiveglove)
|
||||
else:
|
||||
pool.extend(diff.basicglove)
|
||||
@@ -599,22 +570,22 @@ def get_pool_core(world, player: int):
|
||||
thisbottle = world.random.choice(diff.bottles)
|
||||
pool.append(thisbottle)
|
||||
|
||||
if want_progressives():
|
||||
if want_progressives(world.random):
|
||||
pool.extend(diff.progressiveshield)
|
||||
else:
|
||||
pool.extend(diff.basicshield)
|
||||
|
||||
if want_progressives():
|
||||
if want_progressives(world.random):
|
||||
pool.extend(diff.progressivearmor)
|
||||
else:
|
||||
pool.extend(diff.basicarmor)
|
||||
|
||||
if want_progressives():
|
||||
if want_progressives(world.random):
|
||||
pool.extend(diff.progressivemagic)
|
||||
else:
|
||||
pool.extend(diff.basicmagic)
|
||||
|
||||
if want_progressives():
|
||||
if want_progressives(world.random):
|
||||
pool.extend(diff.progressivebow)
|
||||
elif (swordless or logic == 'noglitches') and goal != 'icerodhunt':
|
||||
swordless_bows = ['Bow', 'Silver Bow']
|
||||
@@ -627,7 +598,7 @@ def get_pool_core(world, player: int):
|
||||
if swordless:
|
||||
pool.extend(diff.swordless)
|
||||
else:
|
||||
progressive_swords = want_progressives()
|
||||
progressive_swords = want_progressives(world.random)
|
||||
pool.extend(diff.progressivesword if progressive_swords else diff.basicsword)
|
||||
|
||||
extraitems = total_items_to_place - len(pool) - len(placed_items)
|
||||
|
||||
@@ -136,58 +136,58 @@ item_table = {'Bow': ItemData(True, None, 0x0B, 'You have\nchosen the\narcher cl
|
||||
'Multi RNG': ItemData(False, None, 0x63, 'something you may already have', None, None, None, None, 'unknown boy somethings again', 'a total mystery'),
|
||||
'Magic Upgrade (1/2)': ItemData(True, None, 0x4E, 'Your magic\npower has been\ndoubled!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again', 'Half Magic'), # can be required to beat mothula in an open seed in very very rare circumstance
|
||||
'Magic Upgrade (1/4)': ItemData(True, None, 0x4F, 'Your magic\npower has been\nquadrupled!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again', 'Quarter Magic'), # can be required to beat mothula in an open seed in very very rare circumstance
|
||||
'Small Key (Eastern Palace)': ItemData(False, 'SmallKey', 0xA2, 'A small key to Armos Knights', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Eastern Palace'),
|
||||
'Big Key (Eastern Palace)': ItemData(False, 'BigKey', 0x9D, 'A big key to Armos Knights', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Eastern Palace'),
|
||||
'Small Key (Eastern Palace)': ItemData(True, 'SmallKey', 0xA2, 'A small key to Armos Knights', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Eastern Palace'),
|
||||
'Big Key (Eastern Palace)': ItemData(True, 'BigKey', 0x9D, 'A big key to Armos Knights', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Eastern Palace'),
|
||||
'Compass (Eastern Palace)': ItemData(False, 'Compass', 0x8D, 'Now you can find the Armos Knights!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Eastern Palace'),
|
||||
'Map (Eastern Palace)': ItemData(False, 'Map', 0x7D, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Eastern Palace'),
|
||||
'Small Key (Desert Palace)': ItemData(False, 'SmallKey', 0xA3, 'A small key to the desert', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Desert Palace'),
|
||||
'Big Key (Desert Palace)': ItemData(False, 'BigKey', 0x9C, 'A big key to the desert', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Desert Palace'),
|
||||
'Small Key (Desert Palace)': ItemData(True, 'SmallKey', 0xA3, 'A small key to the desert', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Desert Palace'),
|
||||
'Big Key (Desert Palace)': ItemData(True, 'BigKey', 0x9C, 'A big key to the desert', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Desert Palace'),
|
||||
'Compass (Desert Palace)': ItemData(False, 'Compass', 0x8C, 'Now you can find Lanmolas!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Desert Palace'),
|
||||
'Map (Desert Palace)': ItemData(False, 'Map', 0x7C, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Desert Palace'),
|
||||
'Small Key (Tower of Hera)': ItemData(False, 'SmallKey', 0xAA, 'A small key to Hera', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Tower of Hera'),
|
||||
'Big Key (Tower of Hera)': ItemData(False, 'BigKey', 0x95, 'A big key to Hera', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Tower of Hera'),
|
||||
'Small Key (Tower of Hera)': ItemData(True, 'SmallKey', 0xAA, 'A small key to Hera', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Tower of Hera'),
|
||||
'Big Key (Tower of Hera)': ItemData(True, 'BigKey', 0x95, 'A big key to Hera', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Tower of Hera'),
|
||||
'Compass (Tower of Hera)': ItemData(False, 'Compass', 0x85, 'Now you can find Moldorm!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Tower of Hera'),
|
||||
'Map (Tower of Hera)': ItemData(False, 'Map', 0x75, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Tower of Hera'),
|
||||
'Small Key (Hyrule Castle)': ItemData(False, 'SmallKey', 0xA0, 'A small key to the castle', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Hyrule Castle'),
|
||||
'Big Key (Hyrule Castle)': ItemData(False, 'BigKey', 0x9F, 'A big key to the castle', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Hyrule Castle'),
|
||||
'Small Key (Hyrule Castle)': ItemData(True, 'SmallKey', 0xA0, 'A small key to the castle', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Hyrule Castle'),
|
||||
'Big Key (Hyrule Castle)': ItemData(True, 'BigKey', 0x9F, 'A big key to the castle', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Hyrule Castle'),
|
||||
'Compass (Hyrule Castle)': ItemData(False, 'Compass', 0x8F, 'Now you can find no boss!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Hyrule Castle'),
|
||||
'Map (Hyrule Castle)': ItemData(False, 'Map', 0x7F, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Hyrule Castle'),
|
||||
'Small Key (Agahnims Tower)': ItemData(False, 'SmallKey', 0xA4, 'A small key to Agahnim', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Castle Tower'),
|
||||
'Small Key (Agahnims Tower)': ItemData(True, 'SmallKey', 0xA4, 'A small key to Agahnim', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Castle Tower'),
|
||||
# doors-specific items, baserom will not be able to understand these
|
||||
'Big Key (Agahnims Tower)': ItemData(False, 'BigKey', 0x9B, 'A big key to Agahnim', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Castle Tower'),
|
||||
'Big Key (Agahnims Tower)': ItemData(True, 'BigKey', 0x9B, 'A big key to Agahnim', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Castle Tower'),
|
||||
'Compass (Agahnims Tower)': ItemData(False, 'Compass', 0x8B, 'Now you can find Aga1!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds null again', 'a compass to Castle Tower'),
|
||||
'Map (Agahnims Tower)': ItemData(False, 'Map', 0x7B, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Castle Tower'),
|
||||
# end of doors-specific items
|
||||
'Small Key (Palace of Darkness)': ItemData(False, 'SmallKey', 0xA6, 'A small key to darkness', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Palace of Darkness'),
|
||||
'Big Key (Palace of Darkness)': ItemData(False, 'BigKey', 0x99, 'A big key to darkness', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Palace of Darkness'),
|
||||
'Small Key (Palace of Darkness)': ItemData(True, 'SmallKey', 0xA6, 'A small key to darkness', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Palace of Darkness'),
|
||||
'Big Key (Palace of Darkness)': ItemData(True, 'BigKey', 0x99, 'A big key to darkness', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Palace of Darkness'),
|
||||
'Compass (Palace of Darkness)': ItemData(False, 'Compass', 0x89, 'Now you can find Helmasaur King!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Palace of Darkness'),
|
||||
'Map (Palace of Darkness)': ItemData(False, 'Map', 0x79, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Palace of Darkness'),
|
||||
'Small Key (Thieves Town)': ItemData(False, 'SmallKey', 0xAB, 'A small key to thievery', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Thieves\' Town'),
|
||||
'Big Key (Thieves Town)': ItemData(False, 'BigKey', 0x94, 'A big key to thievery', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Thieves\' Town'),
|
||||
'Small Key (Thieves Town)': ItemData(True, 'SmallKey', 0xAB, 'A small key to thievery', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Thieves\' Town'),
|
||||
'Big Key (Thieves Town)': ItemData(True, 'BigKey', 0x94, 'A big key to thievery', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Thieves\' Town'),
|
||||
'Compass (Thieves Town)': ItemData(False, 'Compass', 0x84, 'Now you can find Blind!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Thieves\' Town'),
|
||||
'Map (Thieves Town)': ItemData(False, 'Map', 0x74, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Thieves\' Town'),
|
||||
'Small Key (Skull Woods)': ItemData(False, 'SmallKey', 0xA8, 'A small key to the woods', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Skull Woods'),
|
||||
'Big Key (Skull Woods)': ItemData(False, 'BigKey', 0x97, 'A big key to the woods', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Skull Woods'),
|
||||
'Small Key (Skull Woods)': ItemData(True, 'SmallKey', 0xA8, 'A small key to the woods', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Skull Woods'),
|
||||
'Big Key (Skull Woods)': ItemData(True, 'BigKey', 0x97, 'A big key to the woods', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Skull Woods'),
|
||||
'Compass (Skull Woods)': ItemData(False, 'Compass', 0x87, 'Now you can find Mothula!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Skull Woods'),
|
||||
'Map (Skull Woods)': ItemData(False, 'Map', 0x77, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Skull Woods'),
|
||||
'Small Key (Swamp Palace)': ItemData(False, 'SmallKey', 0xA5, 'A small key to the swamp', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Swamp Palace'),
|
||||
'Big Key (Swamp Palace)': ItemData(False, 'BigKey', 0x9A, 'A big key to the swamp', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Swamp Palace'),
|
||||
'Small Key (Swamp Palace)': ItemData(True, 'SmallKey', 0xA5, 'A small key to the swamp', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Swamp Palace'),
|
||||
'Big Key (Swamp Palace)': ItemData(True, 'BigKey', 0x9A, 'A big key to the swamp', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Swamp Palace'),
|
||||
'Compass (Swamp Palace)': ItemData(False, 'Compass', 0x8A, 'Now you can find Arrghus!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Swamp Palace'),
|
||||
'Map (Swamp Palace)': ItemData(False, 'Map', 0x7A, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Swamp Palace'),
|
||||
'Small Key (Ice Palace)': ItemData(False, 'SmallKey', 0xA9, 'A small key to the iceberg', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Ice Palace'),
|
||||
'Big Key (Ice Palace)': ItemData(False, 'BigKey', 0x96, 'A big key to the iceberg', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Ice Palace'),
|
||||
'Small Key (Ice Palace)': ItemData(True, 'SmallKey', 0xA9, 'A small key to the iceberg', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Ice Palace'),
|
||||
'Big Key (Ice Palace)': ItemData(True, 'BigKey', 0x96, 'A big key to the iceberg', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Ice Palace'),
|
||||
'Compass (Ice Palace)': ItemData(False, 'Compass', 0x86, 'Now you can find Kholdstare!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Ice Palace'),
|
||||
'Map (Ice Palace)': ItemData(False, 'Map', 0x76, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Ice Palace'),
|
||||
'Small Key (Misery Mire)': ItemData(False, 'SmallKey', 0xA7, 'A small key to the mire', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Misery Mire'),
|
||||
'Big Key (Misery Mire)': ItemData(False, 'BigKey', 0x98, 'A big key to the mire', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Misery Mire'),
|
||||
'Small Key (Misery Mire)': ItemData(True, 'SmallKey', 0xA7, 'A small key to the mire', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Misery Mire'),
|
||||
'Big Key (Misery Mire)': ItemData(True, 'BigKey', 0x98, 'A big key to the mire', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Misery Mire'),
|
||||
'Compass (Misery Mire)': ItemData(False, 'Compass', 0x88, 'Now you can find Vitreous!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Misery Mire'),
|
||||
'Map (Misery Mire)': ItemData(False, 'Map', 0x78, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Misery Mire'),
|
||||
'Small Key (Turtle Rock)': ItemData(False, 'SmallKey', 0xAC, 'A small key to the pipe maze', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Turtle Rock'),
|
||||
'Big Key (Turtle Rock)': ItemData(False, 'BigKey', 0x93, 'A big key to the pipe maze', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Turtle Rock'),
|
||||
'Small Key (Turtle Rock)': ItemData(True, 'SmallKey', 0xAC, 'A small key to the pipe maze', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Turtle Rock'),
|
||||
'Big Key (Turtle Rock)': ItemData(True, 'BigKey', 0x93, 'A big key to the pipe maze', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Turtle Rock'),
|
||||
'Compass (Turtle Rock)': ItemData(False, 'Compass', 0x83, 'Now you can find Trinexx!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Turtle Rock'),
|
||||
'Map (Turtle Rock)': ItemData(False, 'Map', 0x73, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Turtle Rock'),
|
||||
'Small Key (Ganons Tower)': ItemData(False, 'SmallKey', 0xAD, 'A small key to the evil tower', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Ganon\'s Tower'),
|
||||
'Big Key (Ganons Tower)': ItemData(False, 'BigKey', 0x92, 'A big key to the evil tower', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Ganon\'s Tower'),
|
||||
'Small Key (Ganons Tower)': ItemData(True, 'SmallKey', 0xAD, 'A small key to the evil tower', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Ganon\'s Tower'),
|
||||
'Big Key (Ganons Tower)': ItemData(True, 'BigKey', 0x92, 'A big key to the evil tower', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Ganon\'s Tower'),
|
||||
'Compass (Ganons Tower)': ItemData(False, 'Compass', 0x82, 'Now you can find Agahnim!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Ganon\'s Tower'),
|
||||
'Map (Ganons Tower)': ItemData(False, 'Map', 0x72, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Ganon\'s Tower'),
|
||||
'Small Key (Universal)': ItemData(False, None, 0xAF, 'A small key for any door', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key'),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import typing
|
||||
import random
|
||||
|
||||
from Options import Choice, Range, Option
|
||||
from Options import Choice, Range, Option, Toggle, DefaultOnToggle
|
||||
|
||||
|
||||
class Logic(Choice):
|
||||
@@ -70,8 +71,124 @@ class Enemies(Choice):
|
||||
option_shuffled = 1
|
||||
option_chaos = 2
|
||||
|
||||
|
||||
class Progressive(Choice):
|
||||
displayname = "Progressive Items"
|
||||
option_off = 0
|
||||
option_grouped_random = 1
|
||||
option_on = 2
|
||||
alias_false = 0
|
||||
alias_true = 2
|
||||
default = 2
|
||||
alias_random = 1
|
||||
|
||||
def want_progressives(self, random):
|
||||
return random.choice([True, False]) if self.value == self.option_grouped_random else bool(self.value)
|
||||
|
||||
class Palette(Choice):
|
||||
option_default = 0
|
||||
option_good = 1
|
||||
option_blackout = 2
|
||||
option_puke = 3
|
||||
option_classic = 4
|
||||
option_grayscale = 5
|
||||
option_negative = 6
|
||||
option_dizzy = 7
|
||||
option_sick = 8
|
||||
alias_random = 1
|
||||
|
||||
|
||||
class OWPalette(Palette):
|
||||
displayname = "Overworld Palette"
|
||||
|
||||
|
||||
class UWPalette(Palette):
|
||||
displayname = "Underworld Palette"
|
||||
|
||||
|
||||
class HUDPalette(Palette):
|
||||
displayname = "Menu Palette"
|
||||
|
||||
|
||||
class SwordPalette(Palette):
|
||||
displayname = "Sword Palette"
|
||||
|
||||
|
||||
class ShieldPalette(Palette):
|
||||
displayname = "Shield Palette"
|
||||
|
||||
|
||||
class LinkPalette(Palette):
|
||||
displayname = "Link Palette"
|
||||
|
||||
|
||||
class HeartBeep(Choice):
|
||||
displayname = "Heart Beep Rate"
|
||||
option_normal = 0
|
||||
option_double = 1
|
||||
option_half = 2,
|
||||
option_quarter = 3
|
||||
option_off = 4
|
||||
|
||||
|
||||
class HeartColor(Choice):
|
||||
displayname = "Heart Color"
|
||||
option_red = 0
|
||||
option_blue = 1
|
||||
option_green = 2
|
||||
option_yellow = 3
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str) -> Choice:
|
||||
# remove when this becomes a base Choice feature
|
||||
if text == "random":
|
||||
return cls(random.randint(0, 3))
|
||||
return super(HeartColor, cls).from_text(text)
|
||||
|
||||
class QuickSwap(DefaultOnToggle):
|
||||
displayname = "L/R Quickswapping"
|
||||
|
||||
|
||||
class MenuSpeed(Choice):
|
||||
displayname = "Menu Speed"
|
||||
option_normal = 0
|
||||
option_instant = 1,
|
||||
option_double = 2
|
||||
option_triple = 3
|
||||
option_quadruple = 4
|
||||
option_half = 5
|
||||
|
||||
|
||||
class Music(DefaultOnToggle):
|
||||
displayname = "Play music"
|
||||
|
||||
class ReduceFlashing(DefaultOnToggle):
|
||||
displayname = "Reduce Screen Flashes"
|
||||
|
||||
class TriforceHud(Choice):
|
||||
displayname = "Display Method for Triforce Hunt"
|
||||
option_normal = 0
|
||||
option_hide_goal = 1
|
||||
option_hide_required = 2
|
||||
option_hide_both = 3
|
||||
|
||||
alttp_options: typing.Dict[str, type(Option)] = {
|
||||
"crystals_needed_for_gt": CrystalsTower,
|
||||
"crystals_needed_for_ganon": CrystalsGanon,
|
||||
"progressive": Progressive,
|
||||
"shop_item_slots": ShopItemSlots,
|
||||
}
|
||||
"ow_palettes": OWPalette,
|
||||
"uw_palettes": UWPalette,
|
||||
"hud_palettes": HUDPalette,
|
||||
"sword_palettes": SwordPalette,
|
||||
"shield_palettes": ShieldPalette,
|
||||
"link_palettes": LinkPalette,
|
||||
"heartbeep": HeartBeep,
|
||||
"heartcolor": HeartColor,
|
||||
"quickswap": QuickSwap,
|
||||
"menuspeed": MenuSpeed,
|
||||
"music": Music,
|
||||
"reduceflashing": ReduceFlashing,
|
||||
"triforcehud": TriforceHud
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import Utils
|
||||
from Patch import read_rom
|
||||
|
||||
JAP10HASH = '03a63945398191337e896e5771f77173'
|
||||
RANDOMIZERBASEHASH = '13a75c5dd28055fbcf8f69bd8161871d'
|
||||
|
||||
@@ -31,7 +34,7 @@ from worlds.alttp.Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts
|
||||
DeathMountain_texts, \
|
||||
LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, \
|
||||
SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names
|
||||
from Utils import output_path, local_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen
|
||||
from Utils import local_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen
|
||||
from worlds.alttp.Items import ItemFactory, item_table
|
||||
from worlds.alttp.EntranceShuffle import door_addresses
|
||||
import Patch
|
||||
@@ -168,14 +171,6 @@ class LocalRom(object):
|
||||
self.write_int32(startaddress + (i * 4), value)
|
||||
|
||||
|
||||
def read_rom(stream) -> bytearray:
|
||||
"Reads rom into bytearray and strips off any smc header"
|
||||
buffer = bytearray(stream.read())
|
||||
if len(buffer) % 0x400 == 0x200:
|
||||
buffer = buffer[0x200:]
|
||||
return buffer
|
||||
|
||||
|
||||
check_lock = threading.Lock()
|
||||
|
||||
|
||||
@@ -279,11 +274,11 @@ def apply_random_sprite_on_event(rom: LocalRom, sprite, local_random, allow_rand
|
||||
rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette)
|
||||
|
||||
|
||||
def patch_enemizer(world, team: int, player: int, rom: LocalRom, enemizercli, output_directory):
|
||||
def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_directory):
|
||||
check_enemizer(enemizercli)
|
||||
randopatch_path = os.path.abspath(os.path.join(output_directory, f'enemizer_randopatch_{team}_{player}.sfc'))
|
||||
options_path = os.path.abspath(os.path.join(output_directory, f'enemizer_options_{team}_{player}.json'))
|
||||
enemizer_output_path = os.path.abspath(os.path.join(output_directory, f'enemizer_output_{team}_{player}.sfc'))
|
||||
randopatch_path = os.path.abspath(os.path.join(output_directory, f'enemizer_randopatch_{player}.sfc'))
|
||||
options_path = os.path.abspath(os.path.join(output_directory, f'enemizer_options_{player}.json'))
|
||||
enemizer_output_path = os.path.abspath(os.path.join(output_directory, f'enemizer_output_{player}.sfc'))
|
||||
|
||||
# write options file for enemizer
|
||||
options = {
|
||||
@@ -546,7 +541,6 @@ class Sprite():
|
||||
self.valid = False
|
||||
|
||||
def get_vanilla_sprite_data(self):
|
||||
from Patch import get_base_rom_path
|
||||
file_name = get_base_rom_path()
|
||||
base_rom_bytes = bytes(read_rom(open(file_name, "rb")))
|
||||
Sprite.sprite = base_rom_bytes[0x80000:0x87000]
|
||||
@@ -756,7 +750,7 @@ def get_nonnative_item_sprite(item: str) -> int:
|
||||
# https://discord.com/channels/731205301247803413/827141303330406408/852102450822905886
|
||||
|
||||
|
||||
def patch_rom(world, rom, player, team, enemized):
|
||||
def patch_rom(world, rom, player, enemized):
|
||||
local_random = world.slot_seeds[player]
|
||||
|
||||
# progressive bow silver arrow hint hack
|
||||
@@ -1645,7 +1639,7 @@ def patch_rom(world, rom, player, team, enemized):
|
||||
rom.write_byte(0x4BA1D, tile_set.get_len())
|
||||
rom.write_bytes(0x4BA2A, tile_set.get_bytes())
|
||||
|
||||
write_strings(rom, world, player, team)
|
||||
write_strings(rom, world, player)
|
||||
|
||||
# remote items flag, does not currently work
|
||||
rom.write_byte(0x18637C, int(world.worlds[player].remote_items))
|
||||
@@ -1654,13 +1648,13 @@ def patch_rom(world, rom, player, team, enemized):
|
||||
# 21 bytes
|
||||
from Main import __version__
|
||||
# TODO: Adjust Enemizer to accept AP and AD
|
||||
rom.name = bytearray(f'BM{__version__.replace(".", "")[0:3]}_{team + 1}_{player}_{world.seed:09}\0', 'utf8')[:21]
|
||||
rom.name = bytearray(f'BM{__version__.replace(".", "")[0:3]}_{player}_{world.seed:11}\0', 'utf8')[:21]
|
||||
rom.name.extend([0] * (21 - len(rom.name)))
|
||||
rom.write_bytes(0x7FC0, rom.name)
|
||||
|
||||
# set player names
|
||||
for p in range(1, min(world.players, 255) + 1):
|
||||
rom.write_bytes(0x195FFC + ((p - 1) * 32), hud_format_text(world.player_names[p][team]))
|
||||
rom.write_bytes(0x195FFC + ((p - 1) * 32), hud_format_text(world.player_name[p]))
|
||||
|
||||
# Write title screen Code
|
||||
hashint = int(rom.get_hash(), 16)
|
||||
@@ -1756,13 +1750,13 @@ def hud_format_text(text):
|
||||
return output[:32]
|
||||
|
||||
|
||||
def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite: str, palettes_options,
|
||||
def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, sprite: str, palettes_options,
|
||||
world=None, player=1, allow_random_on_event=False, reduceflashing=False,
|
||||
triforcehud: str = None):
|
||||
local_random = random if not world else world.slot_seeds[player]
|
||||
|
||||
disable_music: bool = not music
|
||||
# enable instant item menu
|
||||
if fastmenu == 'instant':
|
||||
if menuspeed == 'instant':
|
||||
rom.write_byte(0x6DD9A, 0x20)
|
||||
rom.write_byte(0x6DF2A, 0x20)
|
||||
rom.write_byte(0x6E0E9, 0x20)
|
||||
@@ -1770,15 +1764,15 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr
|
||||
rom.write_byte(0x6DD9A, 0x11)
|
||||
rom.write_byte(0x6DF2A, 0x12)
|
||||
rom.write_byte(0x6E0E9, 0x12)
|
||||
if fastmenu == 'instant':
|
||||
if menuspeed == 'instant':
|
||||
rom.write_byte(0x180048, 0xE8)
|
||||
elif fastmenu == 'double':
|
||||
elif menuspeed == 'double':
|
||||
rom.write_byte(0x180048, 0x10)
|
||||
elif fastmenu == 'triple':
|
||||
elif menuspeed == 'triple':
|
||||
rom.write_byte(0x180048, 0x18)
|
||||
elif fastmenu == 'quadruple':
|
||||
elif menuspeed == 'quadruple':
|
||||
rom.write_byte(0x180048, 0x20)
|
||||
elif fastmenu == 'half':
|
||||
elif menuspeed == 'half':
|
||||
rom.write_byte(0x180048, 0x04)
|
||||
else:
|
||||
rom.write_byte(0x180048, 0x08)
|
||||
@@ -1854,7 +1848,7 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr
|
||||
while True:
|
||||
yield ColorF(local_random.random(), local_random.random(), local_random.random())
|
||||
|
||||
if mode == 'random':
|
||||
if mode == 'good':
|
||||
mode = 'maseya'
|
||||
z3pr.randomize(rom.buffer, mode, offset_collections=offsets_array, random_colors=next_color_generator())
|
||||
|
||||
@@ -2075,7 +2069,7 @@ def write_string_to_rom(rom, target, string):
|
||||
rom.write_bytes(address, MultiByteTextMapper.convert(string, maxbytes))
|
||||
|
||||
|
||||
def write_strings(rom, world, player, team):
|
||||
def write_strings(rom, world, player):
|
||||
local_random = world.slot_seeds[player]
|
||||
|
||||
tt = TextTable()
|
||||
@@ -2098,11 +2092,11 @@ def write_strings(rom, world, player, team):
|
||||
hint = dest.hint_text if dest.hint_text else "something"
|
||||
if dest.player != player:
|
||||
if ped_hint:
|
||||
hint += f" for {world.player_names[dest.player][team]}!"
|
||||
hint += f" for {world.player_name[dest.player]}!"
|
||||
elif type(dest) in [Region, ALttPLocation]:
|
||||
hint += f" in {world.player_names[dest.player][team]}'s world"
|
||||
hint += f" in {world.player_name[dest.player]}'s world"
|
||||
else:
|
||||
hint += f" for {world.player_names[dest.player][team]}"
|
||||
hint += f" for {world.player_name[dest.player]}"
|
||||
return hint
|
||||
|
||||
# For hints, first we write hints about entrances, some from the inconvenient list others from all reasonable entrances.
|
||||
@@ -2934,3 +2928,27 @@ hash_alphabet = [
|
||||
"Lamp", "Hammer", "Shovel", "Flute", "Bug Net", "Book", "Bottle", "Potion", "Cane", "Cape", "Mirror", "Boots",
|
||||
"Gloves", "Flippers", "Pearl", "Shield", "Tunic", "Heart", "Map", "Compass", "Key"
|
||||
]
|
||||
|
||||
|
||||
def get_base_rom_bytes(file_name: str = "") -> bytes:
|
||||
base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None)
|
||||
if not base_rom_bytes:
|
||||
file_name = get_base_rom_path(file_name)
|
||||
base_rom_bytes = bytes(read_rom(open(file_name, "rb")))
|
||||
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
if JAP10HASH != basemd5.hexdigest():
|
||||
raise Exception('Supplied Base Rom does not match known MD5 for JAP(1.0) release. '
|
||||
'Get the correct game and version, then dump it')
|
||||
get_base_rom_bytes.base_rom_bytes = base_rom_bytes
|
||||
return base_rom_bytes
|
||||
|
||||
|
||||
def get_base_rom_path(file_name: str = "") -> str:
|
||||
options = Utils.get_options()
|
||||
if not file_name:
|
||||
file_name = options["lttp_options"]["rom_file"]
|
||||
if not os.path.exists(file_name):
|
||||
file_name = Utils.local_path(file_name)
|
||||
return file_name
|
||||
@@ -118,7 +118,7 @@ def mirrorless_path_to_castle_courtyard(world, player):
|
||||
else:
|
||||
queue.append((entrance.connected_region, new_path))
|
||||
|
||||
raise Exception(f"Could not find mirrorless path to castle courtyard for Player {player} ({world.get_player_names(player)})")
|
||||
raise Exception(f"Could not find mirrorless path to castle courtyard for Player {player} ({world.get_player_name(player)})")
|
||||
|
||||
|
||||
def set_defeat_dungeon_boss_rule(location):
|
||||
|
||||
@@ -127,7 +127,7 @@ Triforce_texts = [
|
||||
"\n Honk.",
|
||||
]
|
||||
BombShop2_texts = ['Bombs!\nBombs!\nBiggest!\nBestest!\nGreatest!\nBoomiest!']
|
||||
Sahasrahla2_texts = ['You already got my item, idiot.', 'Why are you still talking to me?', 'Have you met my brother, Hasarahshla?']
|
||||
Sahasrahla2_texts = ['You already have my item.', 'Why are you still talking to me?', 'Have you met my brother, Hasarahshla?']
|
||||
Blind_texts = [
|
||||
"I hate insect\npuns, they\nreally bug me.",
|
||||
"I haven't seen\nthe eye doctor\nin years.",
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import random
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
|
||||
from BaseClasses import Item, CollectionState
|
||||
from .SubClasses import ALttPItem
|
||||
@@ -10,10 +13,14 @@ from .Rules import set_rules
|
||||
from .ItemPool import generate_itempool
|
||||
from .Shops import create_shops
|
||||
from .Dungeons import create_dungeons
|
||||
from .Rom import LocalRom, patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, get_hash_string
|
||||
import Patch
|
||||
|
||||
from .InvertedRegions import create_inverted_regions, mark_dark_world_regions
|
||||
from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect
|
||||
|
||||
lttp_logger = logging.getLogger("A Link to the Past")
|
||||
|
||||
class ALTTPWorld(World):
|
||||
game: str = "A Link to the Past"
|
||||
options = alttp_options
|
||||
@@ -34,6 +41,8 @@ class ALTTPWorld(World):
|
||||
create_items = generate_itempool
|
||||
|
||||
def create_regions(self):
|
||||
self.rom_name_available_event = threading.Event()
|
||||
|
||||
player = self.player
|
||||
world = self.world
|
||||
if world.open_pyramid[player] == 'goal':
|
||||
@@ -77,59 +86,154 @@ class ALTTPWorld(World):
|
||||
world.random = old_random
|
||||
plando_connect(world, player)
|
||||
|
||||
def collect(self, state: CollectionState, item: Item) -> bool:
|
||||
def collect_item(self, state: CollectionState, item: Item):
|
||||
if item.name.startswith('Progressive '):
|
||||
if 'Sword' in item.name:
|
||||
if state.has('Golden Sword', item.player):
|
||||
pass
|
||||
elif state.has('Tempered Sword', item.player) and self.world.difficulty_requirements[
|
||||
item.player].progressive_sword_limit >= 4:
|
||||
state.prog_items['Golden Sword', item.player] += 1
|
||||
return True
|
||||
return 'Golden Sword'
|
||||
elif state.has('Master Sword', item.player) and self.world.difficulty_requirements[
|
||||
item.player].progressive_sword_limit >= 3:
|
||||
state.prog_items['Tempered Sword', item.player] += 1
|
||||
return True
|
||||
return 'Tempered Sword'
|
||||
elif state.has('Fighter Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 2:
|
||||
state.prog_items['Master Sword', item.player] += 1
|
||||
return True
|
||||
return 'Master Sword'
|
||||
elif self.world.difficulty_requirements[item.player].progressive_sword_limit >= 1:
|
||||
state.prog_items['Fighter Sword', item.player] += 1
|
||||
return True
|
||||
return 'Fighter Sword'
|
||||
elif 'Glove' in item.name:
|
||||
if state.has('Titans Mitts', item.player):
|
||||
pass
|
||||
return
|
||||
elif state.has('Power Glove', item.player):
|
||||
state.prog_items['Titans Mitts', item.player] += 1
|
||||
return True
|
||||
return 'Titans Mitts'
|
||||
else:
|
||||
state.prog_items['Power Glove', item.player] += 1
|
||||
return True
|
||||
return 'Power Glove'
|
||||
elif 'Shield' in item.name:
|
||||
if state.has('Mirror Shield', item.player):
|
||||
pass
|
||||
return
|
||||
elif state.has('Red Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 3:
|
||||
state.prog_items['Mirror Shield', item.player] += 1
|
||||
return True
|
||||
return 'Mirror Shield'
|
||||
elif state.has('Blue Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 2:
|
||||
state.prog_items['Red Shield', item.player] += 1
|
||||
return True
|
||||
return 'Red Shield'
|
||||
elif self.world.difficulty_requirements[item.player].progressive_shield_limit >= 1:
|
||||
state.prog_items['Blue Shield', item.player] += 1
|
||||
return True
|
||||
return 'Blue Shield'
|
||||
elif 'Bow' in item.name:
|
||||
if state.has('Silver', item.player):
|
||||
pass
|
||||
return
|
||||
elif state.has('Bow', item.player) and self.world.difficulty_requirements[item.player].progressive_bow_limit >= 2:
|
||||
state.prog_items['Silver Bow', item.player] += 1
|
||||
return True
|
||||
return 'Silver Bow'
|
||||
elif self.world.difficulty_requirements[item.player].progressive_bow_limit >= 1:
|
||||
state.prog_items['Bow', item.player] += 1
|
||||
return True
|
||||
elif item.advancement or item.smallkey or item.bigkey:
|
||||
state.prog_items[item.name, item.player] += 1
|
||||
return True
|
||||
return False
|
||||
return 'Bow'
|
||||
elif item.advancement:
|
||||
return item.name
|
||||
|
||||
def pre_fill(self):
|
||||
from Fill import fill_restrictive, FillError
|
||||
attempts = 5
|
||||
world = self.world
|
||||
player = self.player
|
||||
all_state = world.get_all_state(keys=True)
|
||||
crystals = [self.create_item(name) for name in ['Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6']]
|
||||
crystal_locations = [world.get_location('Turtle Rock - Prize', player),
|
||||
world.get_location('Eastern Palace - Prize', player),
|
||||
world.get_location('Desert Palace - Prize', player),
|
||||
world.get_location('Tower of Hera - Prize', player),
|
||||
world.get_location('Palace of Darkness - Prize', player),
|
||||
world.get_location('Thieves\' Town - Prize', player),
|
||||
world.get_location('Skull Woods - Prize', player),
|
||||
world.get_location('Swamp Palace - Prize', player),
|
||||
world.get_location('Ice Palace - Prize', player),
|
||||
world.get_location('Misery Mire - Prize', player)]
|
||||
placed_prizes = {loc.item.name for loc in crystal_locations if loc.item}
|
||||
unplaced_prizes = [crystal for crystal in crystals if crystal.name not in placed_prizes]
|
||||
empty_crystal_locations = [loc for loc in crystal_locations if not loc.item]
|
||||
for attempt in range(attempts):
|
||||
try:
|
||||
prizepool = unplaced_prizes.copy()
|
||||
prize_locs = empty_crystal_locations.copy()
|
||||
world.random.shuffle(prize_locs)
|
||||
fill_restrictive(world, all_state, prize_locs, prizepool, True, lock=True)
|
||||
except FillError as e:
|
||||
lttp_logger.exception("Failed to place dungeon prizes (%s). Will retry %s more times", e,
|
||||
attempts - attempt)
|
||||
for location in empty_crystal_locations:
|
||||
location.item = None
|
||||
continue
|
||||
break
|
||||
else:
|
||||
raise FillError('Unable to place dungeon prizes')
|
||||
|
||||
@classmethod
|
||||
def stage_pre_fill(cls, world):
|
||||
from .Dungeons import fill_dungeons_restrictive
|
||||
fill_dungeons_restrictive(world)
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
world = self.world
|
||||
player = self.player
|
||||
try:
|
||||
use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player]
|
||||
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
|
||||
or world.shufflepots[player] or world.bush_shuffle[player]
|
||||
or world.killable_thieves[player])
|
||||
|
||||
rom = LocalRom(world.alttp_rom)
|
||||
|
||||
patch_rom(world, rom, player, use_enemizer)
|
||||
|
||||
if use_enemizer:
|
||||
patch_enemizer(world, player, rom, world.enemizer, output_directory)
|
||||
|
||||
if world.is_race:
|
||||
patch_race_rom(rom, world, player)
|
||||
|
||||
world.spoiler.hashes[player] = get_hash_string(rom.hash)
|
||||
|
||||
palettes_options = {
|
||||
'dungeon': world.uw_palettes[player],
|
||||
'overworld': world.ow_palettes[player],
|
||||
'hud': world.hud_palettes[player],
|
||||
'sword': world.sword_palettes[player],
|
||||
'shield': world.shield_palettes[player],
|
||||
'link': world.link_palettes[player]
|
||||
}
|
||||
palettes_options = {key: option.current_key for key, option in palettes_options.items()}
|
||||
|
||||
apply_rom_settings(rom, world.heartbeep[player].current_key,
|
||||
world.heartcolor[player].current_key,
|
||||
world.quickswap[player],
|
||||
world.menuspeed[player].current_key,
|
||||
world.music[player],
|
||||
world.sprite[player],
|
||||
palettes_options, world, player, True,
|
||||
reduceflashing=world.reduceflashing[player] or world.is_race,
|
||||
triforcehud=world.triforcehud[player].current_key)
|
||||
|
||||
outfilepname = f'_P{player}'
|
||||
outfilepname += f"_{world.player_name[player].replace(' ', '_')}" \
|
||||
if world.player_name[player] != 'Player%d' % player else ''
|
||||
|
||||
rompath = os.path.join(output_directory, f'AP_{world.seed_name}{outfilepname}.sfc')
|
||||
rom.write_to_file(rompath, hide_enemizer=True)
|
||||
Patch.create_patch_file(rompath, player=player, player_name=world.player_name[player])
|
||||
os.unlink(rompath)
|
||||
self.rom_name = rom.name
|
||||
except:
|
||||
raise
|
||||
finally:
|
||||
self.rom_name_available_event.set() # make sure threading continues and errors are collected
|
||||
|
||||
def modify_multidata(self, multidata: dict):
|
||||
import base64
|
||||
# wait for self.rom_name to be available.
|
||||
self.rom_name_available_event.wait()
|
||||
rom_name = getattr(self, "rom_name", None)
|
||||
# we skip in case of error, so that the original error in the output thread is the one that gets raised
|
||||
if rom_name:
|
||||
new_name = base64.b64encode(bytes(self.rom_name)).decode()
|
||||
payload = multidata["connect_names"][self.world.player_name[self.player]]
|
||||
multidata["connect_names"][new_name] = payload
|
||||
del (multidata["connect_names"][self.world.player_name[self.player]])
|
||||
|
||||
def get_required_client_version(self) -> tuple:
|
||||
return max((0, 1, 4), super(ALTTPWorld, self).get_required_client_version())
|
||||
@@ -137,4 +241,66 @@ class ALTTPWorld(World):
|
||||
def create_item(self, name: str) -> Item:
|
||||
return ALttPItem(name, self.player, **as_dict_item_table[name])
|
||||
|
||||
@classmethod
|
||||
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, restitempool, fill_locations):
|
||||
trash_counts = {}
|
||||
standard_keyshuffle_players = set()
|
||||
for player in world.get_game_players("A Link to the Past"):
|
||||
if world.mode[player] == 'standard' and world.keyshuffle[player] is True:
|
||||
standard_keyshuffle_players.add(player)
|
||||
if not world.ganonstower_vanilla[player] or \
|
||||
world.logic[player] in {'owglitches', 'hybridglitches', "nologic"}:
|
||||
pass
|
||||
elif 'triforcehunt' in world.goal[player] and ('local' in world.goal[player] or world.players == 1):
|
||||
trash_counts[player] = world.random.randint(world.crystals_needed_for_gt[player] * 2,
|
||||
world.crystals_needed_for_gt[player] * 4)
|
||||
else:
|
||||
trash_counts[player] = world.random.randint(0, world.crystals_needed_for_gt[player] * 2)
|
||||
|
||||
# Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots
|
||||
# TODO: this might be worthwhile to introduce as generic option for various games and then optimize it
|
||||
if standard_keyshuffle_players:
|
||||
viable = []
|
||||
for location in world.get_locations():
|
||||
if location.player in standard_keyshuffle_players \
|
||||
and location.item is None \
|
||||
and location.can_reach(world.state):
|
||||
viable.append(location)
|
||||
world.random.shuffle(viable)
|
||||
for player in standard_keyshuffle_players:
|
||||
key = world.create_item("Small Key (Hyrule Castle)", player)
|
||||
loc = viable.pop()
|
||||
loc.place_locked_item(key)
|
||||
fill_locations.remove(loc)
|
||||
world.random.shuffle(fill_locations)
|
||||
# TODO: investigate not creating the key in the first place
|
||||
progitempool[:] = [item for item in progitempool if
|
||||
item.player not in standard_keyshuffle_players or
|
||||
item.name != "Small Key (Hyrule Castle)"]
|
||||
|
||||
if trash_counts:
|
||||
locations_mapping = {player: [] for player in trash_counts}
|
||||
for location in fill_locations:
|
||||
if 'Ganons Tower' in location.name and location.player in locations_mapping:
|
||||
locations_mapping[location.player].append(location)
|
||||
|
||||
for player, trash_count in trash_counts.items():
|
||||
gtower_locations = locations_mapping[player]
|
||||
world.random.shuffle(gtower_locations)
|
||||
localrest = localrestitempool[player]
|
||||
if localrest:
|
||||
gt_item_pool = restitempool + localrest
|
||||
world.random.shuffle(gt_item_pool)
|
||||
else:
|
||||
gt_item_pool = restitempool.copy()
|
||||
|
||||
while gtower_locations and gt_item_pool and trash_count > 0:
|
||||
spot_to_fill = gtower_locations.pop()
|
||||
item_to_place = gt_item_pool.pop()
|
||||
if item_to_place in localrest:
|
||||
localrest.remove(item_to_place)
|
||||
else:
|
||||
restitempool.remove(item_to_place)
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
fill_locations.remove(spot_to_fill) # very slow, unfortunately
|
||||
trash_count -= 1
|
||||
|
||||
@@ -49,7 +49,7 @@ def generate_mod(world, output_directory: str):
|
||||
global data_final_template, locale_template, control_template, data_template
|
||||
with template_load_lock:
|
||||
if not data_final_template:
|
||||
mod_template_folder = Utils.local_path("data", "factorio", "mod_template")
|
||||
mod_template_folder = os.path.join(os.path.dirname(__file__), "data", "mod_template")
|
||||
template_env: Optional[jinja2.Environment] = \
|
||||
jinja2.Environment(loader=jinja2.FileSystemLoader([mod_template_folder]))
|
||||
data_template = template_env.get_template("data.lua")
|
||||
@@ -57,12 +57,12 @@ def generate_mod(world, output_directory: str):
|
||||
locale_template = template_env.get_template(r"locale/en/locale.cfg")
|
||||
control_template = template_env.get_template("control.lua")
|
||||
# get data for templates
|
||||
player_names = {x: multiworld.player_names[x][0] for x in multiworld.player_ids}
|
||||
player_names = {x: multiworld.player_name[x] for x in multiworld.player_ids}
|
||||
locations = []
|
||||
for location in multiworld.get_filled_locations(player):
|
||||
if location.address:
|
||||
locations.append((location.name, location.item.name, location.item.player, location.item.advancement))
|
||||
mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.player_names[player][0]}"
|
||||
mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.player_name[player]}"
|
||||
tech_cost_scale = {0: 0.1,
|
||||
1: 0.25,
|
||||
2: 0.5,
|
||||
@@ -70,15 +70,27 @@ def generate_mod(world, output_directory: str):
|
||||
4: 2,
|
||||
5: 5,
|
||||
6: 10}[multiworld.tech_cost[player].value]
|
||||
random = multiworld.slot_seeds[player]
|
||||
|
||||
def flop_random(low, high, base=None):
|
||||
"""Guarentees 50% bwlo base and 50% above base, uniform distribution in each direction."""
|
||||
if base:
|
||||
distance = random.random()
|
||||
if random.randint(0, 1):
|
||||
return base + (high-base) * distance
|
||||
else:
|
||||
return base - (base-low) * distance
|
||||
return random.uniform(low, high)
|
||||
|
||||
template_data = {"locations": locations, "player_names": player_names, "tech_table": tech_table,
|
||||
"base_tech_table": base_tech_table, "tech_to_progressive_lookup": tech_to_progressive_lookup,
|
||||
"mod_name": mod_name, "allowed_science_packs": multiworld.max_science_pack[player].get_allowed_packs(),
|
||||
"tech_cost_scale": tech_cost_scale, "custom_technologies": multiworld.worlds[player].custom_technologies,
|
||||
"tech_tree_layout_prerequisites": multiworld.tech_tree_layout_prerequisites[player],
|
||||
"slot_name": multiworld.player_names[player][0], "seed_name": multiworld.seed_name,
|
||||
"slot_name": multiworld.player_name[player], "seed_name": multiworld.seed_name,
|
||||
"starting_items": multiworld.starting_items[player], "recipes": recipes,
|
||||
"random": multiworld.slot_seeds[player], "static_nodes": multiworld.worlds[player].static_nodes,
|
||||
"random": random, "flop_random": flop_random,
|
||||
"static_nodes": multiworld.worlds[player].static_nodes,
|
||||
"recipe_time_scale": recipe_time_scales[multiworld.recipe_time[player].value],
|
||||
"free_sample_blacklist": {item : 1 for item in free_sample_blacklist},
|
||||
"progressive_technology_table": {tech.name : tech.progressive for tech in
|
||||
@@ -95,7 +107,7 @@ def generate_mod(world, output_directory: str):
|
||||
mod_dir = os.path.join(output_directory, mod_name + "_" + Utils.__version__)
|
||||
en_locale_dir = os.path.join(mod_dir, "locale", "en")
|
||||
os.makedirs(en_locale_dir, exist_ok=True)
|
||||
shutil.copytree(Utils.local_path("data", "factorio", "mod"), mod_dir, dirs_exist_ok=True)
|
||||
shutil.copytree(os.path.join(os.path.dirname(__file__), "data", "mod"), mod_dir, dirs_exist_ok=True)
|
||||
with open(os.path.join(mod_dir, "data.lua"), "wt") as f:
|
||||
f.write(data_template_code)
|
||||
with open(os.path.join(mod_dir, "data-final-fixes.lua"), "wt") as f:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
import typing
|
||||
|
||||
from Options import Choice, OptionDict, Option, DefaultOnToggle
|
||||
from Options import Choice, OptionDict, Option, DefaultOnToggle, Range
|
||||
from schema import Schema, Optional, And, Or
|
||||
|
||||
# schema helpers
|
||||
@@ -11,6 +11,7 @@ LuaBool = Or(bool, And(int, lambda n: n in (0, 1)))
|
||||
|
||||
class MaxSciencePack(Choice):
|
||||
"""Maximum level of science pack required to complete the game."""
|
||||
displayname = "Maximum Required Science Pack"
|
||||
option_automation_science_pack = 0
|
||||
option_logistic_science_pack = 1
|
||||
option_military_science_pack = 2
|
||||
@@ -34,6 +35,7 @@ class MaxSciencePack(Choice):
|
||||
|
||||
class TechCost(Choice):
|
||||
"""How expensive are the technologies."""
|
||||
displayname = "Technology Cost Scale"
|
||||
option_very_easy = 0
|
||||
option_easy = 1
|
||||
option_kind = 2
|
||||
@@ -44,8 +46,18 @@ class TechCost(Choice):
|
||||
default = 3
|
||||
|
||||
|
||||
class Silo(Choice):
|
||||
"""Ingredients to craft rocket silo or auto-place if set to spawn."""
|
||||
displayname = "Rocket Silo"
|
||||
option_vanilla = 0
|
||||
option_randomize_recipe = 1
|
||||
option_spawn = 2
|
||||
default = 0
|
||||
|
||||
|
||||
class FreeSamples(Choice):
|
||||
"""Get free items with your technologies."""
|
||||
displayname = "Free Samples"
|
||||
option_none = 0
|
||||
option_single_craft = 1
|
||||
option_half_stack = 2
|
||||
@@ -55,6 +67,7 @@ class FreeSamples(Choice):
|
||||
|
||||
class TechTreeLayout(Choice):
|
||||
"""Selects how the tech tree nodes are interwoven."""
|
||||
displayname = "Technology Tree Layout"
|
||||
option_single = 0
|
||||
option_small_diamonds = 1
|
||||
option_medium_diamonds = 2
|
||||
@@ -70,6 +83,7 @@ class TechTreeLayout(Choice):
|
||||
|
||||
class TechTreeInformation(Choice):
|
||||
"""How much information should be displayed in the tech tree."""
|
||||
displayname = "Technology Tree Information"
|
||||
option_none = 0
|
||||
option_advancement = 1
|
||||
option_full = 2
|
||||
@@ -78,6 +92,7 @@ class TechTreeInformation(Choice):
|
||||
|
||||
class RecipeTime(Choice):
|
||||
"""randomize the time it takes for any recipe to craft, this includes smelting, chemical lab, hand crafting etc."""
|
||||
displayname = "Recipe Time"
|
||||
option_vanilla = 0
|
||||
option_fast = 1
|
||||
option_normal = 2
|
||||
@@ -85,28 +100,55 @@ class RecipeTime(Choice):
|
||||
option_chaos = 5
|
||||
|
||||
|
||||
# TODO: implement random
|
||||
class Progressive(Choice):
|
||||
displayname = "Progressive Technologies"
|
||||
option_off = 0
|
||||
option_random = 1
|
||||
option_grouped_random = 1
|
||||
option_on = 2
|
||||
alias_false = 0
|
||||
alias_true = 2
|
||||
default = 2
|
||||
alias_random = 1
|
||||
|
||||
def want_progressives(self, random):
|
||||
return random.choice([True, False]) if self.value == self.option_random else int(self.value)
|
||||
return random.choice([True, False]) if self.value == self.option_grouped_random else bool(self.value)
|
||||
|
||||
|
||||
class RecipeIngredients(Choice):
|
||||
"""Select if rocket, or rocket + science pack ingredients should be random."""
|
||||
displayname = "Random Recipe Ingredients Level"
|
||||
option_rocket = 0
|
||||
option_science_pack = 1
|
||||
|
||||
|
||||
class FactorioStartItems(OptionDict):
|
||||
displayname = "Starting Items"
|
||||
default = {"burner-mining-drill": 19, "stone-furnace": 19}
|
||||
|
||||
|
||||
class TrapCount(Range):
|
||||
range_end = 4
|
||||
|
||||
|
||||
class AttackTrapCount(TrapCount):
|
||||
"""Trap items that when received trigger an attack on your base."""
|
||||
displayname = "Attack Traps"
|
||||
|
||||
|
||||
class EvolutionTrapCount(TrapCount):
|
||||
"""Trap items that when received increase the enemy evolution."""
|
||||
displayname = "Evolution Traps"
|
||||
|
||||
|
||||
class EvolutionTrapIncrease(Range):
|
||||
displayname = "Evolution Trap % Effect"
|
||||
range_start = 1
|
||||
default = 10
|
||||
range_end = 100
|
||||
|
||||
|
||||
class FactorioWorldGen(OptionDict):
|
||||
displayname = "World Generation"
|
||||
# FIXME: do we want default be a rando-optimized default or in-game DS?
|
||||
value: typing.Dict[str, typing.Dict[str, typing.Any]]
|
||||
default = {
|
||||
@@ -238,16 +280,24 @@ class FactorioWorldGen(OptionDict):
|
||||
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
|
||||
|
||||
|
||||
class ImportedBlueprint(DefaultOnToggle):
|
||||
displayname = "Blueprints"
|
||||
|
||||
|
||||
factorio_options: typing.Dict[str, type(Option)] = {
|
||||
"max_science_pack": MaxSciencePack,
|
||||
"tech_tree_layout": TechTreeLayout,
|
||||
"tech_cost": TechCost,
|
||||
"silo": Silo,
|
||||
"free_samples": FreeSamples,
|
||||
"tech_tree_information": TechTreeInformation,
|
||||
"starting_items": FactorioStartItems,
|
||||
"recipe_time": RecipeTime,
|
||||
"recipe_ingredients": RecipeIngredients,
|
||||
"imported_blueprints": DefaultOnToggle,
|
||||
"imported_blueprints": ImportedBlueprint,
|
||||
"world_gen": FactorioWorldGen,
|
||||
"progressive": DefaultOnToggle
|
||||
"progressive": Progressive,
|
||||
"evolution_traps": EvolutionTrapCount,
|
||||
"attack_traps": AttackTrapCount,
|
||||
"evolution_trap_increase": EvolutionTrapIncrease,
|
||||
}
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
from __future__ import annotations
|
||||
# Factorio technologies are imported from a .json document in /data
|
||||
from typing import Dict, Set, FrozenSet, Tuple
|
||||
from collections import Counter, defaultdict
|
||||
from typing import Dict, Set, FrozenSet, Tuple, Union, List
|
||||
from collections import Counter
|
||||
import os
|
||||
import json
|
||||
import string
|
||||
|
||||
import Utils
|
||||
import logging
|
||||
import functools
|
||||
|
||||
from . import Options
|
||||
|
||||
factorio_id = 2 ** 17
|
||||
source_folder = Utils.local_path("data", "factorio")
|
||||
factorio_id = factorio_base_id = 2 ** 17
|
||||
source_folder = os.path.join(os.path.dirname(__file__), "data")
|
||||
|
||||
with open(os.path.join(source_folder, "techs.json")) as f:
|
||||
raw = json.load(f)
|
||||
@@ -38,11 +37,24 @@ class FactorioElement():
|
||||
|
||||
|
||||
class Technology(FactorioElement): # maybe make subclass of Location?
|
||||
def __init__(self, name: str, ingredients: Set[str], factorio_id: int, progressive: Tuple[str] = ()):
|
||||
has_modifier: bool
|
||||
factorio_id: int
|
||||
name: str
|
||||
ingredients: Set[str]
|
||||
progressive: Tuple[str]
|
||||
unlocks: Union[Set[str], bool] # bool case is for progressive technologies
|
||||
|
||||
def __init__(self, name: str, ingredients: Set[str], factorio_id: int, progressive: Tuple[str] = (),
|
||||
has_modifier: bool = False, unlocks: Union[Set[str], bool] = None):
|
||||
self.name = name
|
||||
self.factorio_id = factorio_id
|
||||
self.ingredients = ingredients
|
||||
self.progressive = progressive
|
||||
self.has_modifier = has_modifier
|
||||
if unlocks:
|
||||
self.unlocks = unlocks
|
||||
else:
|
||||
self.unlocks = set()
|
||||
|
||||
def build_rule(self, player: int):
|
||||
logging.debug(f"Building rules for {self.name}")
|
||||
@@ -63,6 +75,9 @@ class Technology(FactorioElement): # maybe make subclass of Location?
|
||||
def get_custom(self, world, allowed_packs: Set[str], player: int) -> CustomTechnology:
|
||||
return CustomTechnology(self, world, allowed_packs, player)
|
||||
|
||||
def useful(self) -> bool:
|
||||
return self.has_modifier or self.unlocks
|
||||
|
||||
|
||||
class CustomTechnology(Technology):
|
||||
"""A particularly configured Technology for a world."""
|
||||
@@ -82,12 +97,14 @@ class Recipe(FactorioElement):
|
||||
category: str
|
||||
ingredients: Dict[str, int]
|
||||
products: Dict[str, int]
|
||||
energy: float
|
||||
|
||||
def __init__(self, name: str, category: str, ingredients: Dict[str, int], products: Dict[str, int]):
|
||||
def __init__(self, name: str, category: str, ingredients: Dict[str, int], products: Dict[str, int], energy: float):
|
||||
self.name = name
|
||||
self.category = category
|
||||
self.ingredients = ingredients
|
||||
self.products = products
|
||||
self.energy = energy
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}({self.name})"
|
||||
@@ -112,7 +129,7 @@ class Recipe(FactorioElement):
|
||||
@property
|
||||
def rel_cost(self) -> float:
|
||||
ingredients = sum(self.ingredients.values())
|
||||
return min(ingredients/amount for product, amount in self.products.items())
|
||||
return min(ingredients / amount for product, amount in self.products.items())
|
||||
|
||||
@property
|
||||
def base_cost(self) -> Dict[str, int]:
|
||||
@@ -120,44 +137,60 @@ class Recipe(FactorioElement):
|
||||
for ingredient, cost in self.ingredients.items():
|
||||
if ingredient in all_product_sources:
|
||||
for recipe in all_product_sources[ingredient]:
|
||||
ingredients.update({name: amount*cost/recipe.products[ingredient] for name, amount in recipe.base_cost.items()})
|
||||
ingredients.update({name: amount * cost / recipe.products[ingredient] for name, amount in
|
||||
recipe.base_cost.items()})
|
||||
else:
|
||||
ingredients[ingredient] += cost
|
||||
return ingredients
|
||||
|
||||
@property
|
||||
def total_energy(self) -> float:
|
||||
"""Total required energy (crafting time) for single craft"""
|
||||
# TODO: multiply mining energy by 2 since drill has 0.5 speed
|
||||
total_energy = self.energy
|
||||
for ingredient, cost in self.ingredients.items():
|
||||
if ingredient in all_product_sources:
|
||||
for ingredient_recipe in all_product_sources[ingredient]: # FIXME: this may select the wrong recipe
|
||||
craft_count = max((n for name, n in ingredient_recipe.products.items() if name == ingredient))
|
||||
total_energy += ingredient_recipe.total_energy / craft_count * cost
|
||||
break
|
||||
return total_energy
|
||||
|
||||
|
||||
class Machine(FactorioElement):
|
||||
def __init__(self, name, categories):
|
||||
self.name: str = name
|
||||
self.categories: set = categories
|
||||
|
||||
|
||||
recipe_sources: Dict[str, Set[str]] = {} # recipe_name -> technology source
|
||||
|
||||
# recipes and technologies can share names in Factorio
|
||||
for technology_name in sorted(raw):
|
||||
data = raw[technology_name]
|
||||
current_ingredients = set(data["ingredients"])
|
||||
technology = Technology(technology_name, current_ingredients, factorio_id)
|
||||
technology = Technology(technology_name, current_ingredients, factorio_id,
|
||||
has_modifier=data["has_modifier"], unlocks=set(data["unlocks"]))
|
||||
factorio_id += 1
|
||||
tech_table[technology_name] = technology.factorio_id
|
||||
technology_table[technology_name] = technology
|
||||
|
||||
recipe_sources: Dict[str, str] = {} # recipe_name -> technology source
|
||||
|
||||
for technology, data in raw.items():
|
||||
for recipe_name in data["unlocks"]:
|
||||
recipe_sources.setdefault(recipe_name, set()).add(technology)
|
||||
for recipe_name in technology.unlocks:
|
||||
recipe_sources.setdefault(recipe_name, set()).add(technology_name)
|
||||
|
||||
del (raw)
|
||||
|
||||
recipes = {}
|
||||
all_product_sources: Dict[str, Set[Recipe]] = {"character": set()}
|
||||
# add uranium mining to logic graph. TODO: add to automatic extractor for mod support
|
||||
raw_recipes["uranium-ore"] = {"ingredients": {"sulfuric-acid": 1}, "products": {"uranium-ore": 1}, "category": "mining"}
|
||||
raw_recipes["uranium-ore"] = {"ingredients": {"sulfuric-acid": 1}, "products": {"uranium-ore": 1}, "category": "mining",
|
||||
"energy": 2}
|
||||
|
||||
for recipe_name, recipe_data in raw_recipes.items():
|
||||
# example:
|
||||
# "accumulator":{"ingredients":{"iron-plate":2,"battery":5},"products":{"accumulator":1},"category":"crafting"}
|
||||
|
||||
recipe = Recipe(recipe_name, recipe_data["category"], recipe_data["ingredients"], recipe_data["products"])
|
||||
# FIXME: add mining?
|
||||
recipe = Recipe(recipe_name, recipe_data["category"], recipe_data["ingredients"],
|
||||
recipe_data["products"], recipe_data["energy"] if "energy" in recipe_data else 0)
|
||||
recipes[recipe_name] = recipe
|
||||
if set(recipe.products).isdisjoint(
|
||||
# prevents loop recipes like uranium centrifuging
|
||||
@@ -177,7 +210,7 @@ for name, categories in raw_machines.items():
|
||||
# add electric mining drill as a crafting machine to resolve uranium-ore
|
||||
machines["electric-mining-drill"] = Machine("electric-mining-drill", {"mining"})
|
||||
machines["assembling-machine-1"].categories.add("crafting-with-fluid") # mod enables this
|
||||
machines["character"].categories.add("basic-crafting") # somehow this is implied and not exported
|
||||
machines["character"].categories.add("basic-crafting") # somehow this is implied and not exported
|
||||
del (raw_machines)
|
||||
|
||||
# build requirements graph for all technology ingredients
|
||||
@@ -249,25 +282,26 @@ for category_name, machine_name in machine_per_category.items():
|
||||
techs |= recursively_get_unlocking_technologies(machine_name)
|
||||
required_category_technologies[category_name] = frozenset(techs)
|
||||
|
||||
required_technologies: Dict[str, FrozenSet[Technology]] = Utils.KeyedDefaultDict(lambda ingredient_name : frozenset(
|
||||
required_technologies: Dict[str, FrozenSet[Technology]] = Utils.KeyedDefaultDict(lambda ingredient_name: frozenset(
|
||||
recursively_get_unlocking_technologies(ingredient_name, unlock_func=unlock)))
|
||||
|
||||
|
||||
advancement_technologies: Set[str] = set()
|
||||
for ingredient_name in all_ingredient_names:
|
||||
technologies = required_technologies[ingredient_name]
|
||||
advancement_technologies |= {technology.name for technology in technologies}
|
||||
|
||||
|
||||
@functools.lru_cache(10)
|
||||
def get_rocket_requirements(recipe: Recipe) -> Set[str]:
|
||||
techs = recursively_get_unlocking_technologies("rocket-silo")
|
||||
for ingredient in recipe.ingredients:
|
||||
def get_rocket_requirements(silo_recipe: Recipe, part_recipe: Recipe) -> Set[str]:
|
||||
techs = set()
|
||||
if silo_recipe:
|
||||
for ingredient in silo_recipe.ingredients:
|
||||
techs |= recursively_get_unlocking_technologies(ingredient)
|
||||
for ingredient in part_recipe.ingredients:
|
||||
techs |= recursively_get_unlocking_technologies(ingredient)
|
||||
return {tech.name for tech in techs}
|
||||
|
||||
|
||||
free_sample_blacklist = all_ingredient_names | {"rocket-part"}
|
||||
free_sample_blacklist: Set[str] = all_ingredient_names | {"rocket-part"}
|
||||
|
||||
rocket_recipes = {
|
||||
Options.MaxSciencePack.option_space_science_pack:
|
||||
@@ -290,7 +324,7 @@ advancement_technologies |= {tech.name for tech in required_technologies["rocket
|
||||
|
||||
# progressive technologies
|
||||
# auto-progressive
|
||||
progressive_rows = {}
|
||||
progressive_rows: Dict[str, Union[List[str], Tuple[str, ...]]] = {}
|
||||
progressive_incs = set()
|
||||
for tech_name in tech_table:
|
||||
if tech_name.endswith("-1"):
|
||||
@@ -299,17 +333,17 @@ for tech_name in tech_table:
|
||||
progressive_incs.add(tech_name)
|
||||
|
||||
for root, progressive in progressive_rows.items():
|
||||
seeking = root[:-1]+str(int(root[-1])+1)
|
||||
seeking = root[:-1] + str(int(root[-1]) + 1)
|
||||
while seeking in progressive_incs:
|
||||
progressive.append(seeking)
|
||||
progressive_incs.remove(seeking)
|
||||
seeking = seeking[:-1]+str(int(seeking[-1])+1)
|
||||
seeking = seeking[:-1] + str(int(seeking[-1]) + 1)
|
||||
|
||||
# make root entry the progressive name
|
||||
for old_name in set(progressive_rows):
|
||||
prog_name = "progressive-" + old_name.rsplit("-", 1)[0]
|
||||
progressive_rows[prog_name] = tuple([old_name] + progressive_rows[old_name])
|
||||
del(progressive_rows[old_name])
|
||||
del (progressive_rows[old_name])
|
||||
|
||||
# no -1 start
|
||||
base_starts = set()
|
||||
@@ -318,17 +352,16 @@ for remnant in progressive_incs:
|
||||
base_starts.add(remnant[:-2])
|
||||
|
||||
for root in base_starts:
|
||||
seeking = root+"-2"
|
||||
seeking = root + "-2"
|
||||
progressive = [root]
|
||||
while seeking in progressive_incs:
|
||||
progressive.append(seeking)
|
||||
seeking = seeking[:-1]+str(int(seeking[-1])+1)
|
||||
progressive_rows["progressive-"+root] = tuple(progressive)
|
||||
seeking = seeking[:-1] + str(int(seeking[-1]) + 1)
|
||||
progressive_rows["progressive-" + root] = tuple(progressive)
|
||||
|
||||
# science packs
|
||||
progressive_rows["progressive-science-pack"] = tuple(Options.MaxSciencePack.get_ordered_science_packs())[1:]
|
||||
|
||||
|
||||
# manual progressive
|
||||
progressive_rows["progressive-processing"] = (
|
||||
"steel-processing",
|
||||
@@ -336,7 +369,8 @@ progressive_rows["progressive-processing"] = (
|
||||
"uranium-processing", "kovarex-enrichment-process", "nuclear-fuel-reprocessing")
|
||||
progressive_rows["progressive-rocketry"] = ("rocketry", "explosive-rocketry", "atomic-bomb")
|
||||
progressive_rows["progressive-vehicle"] = ("automobilism", "tank", "spidertron")
|
||||
progressive_rows["progressive-train-network"] = ("railway", "fluid-wagon", "automated-rail-transportation", "rail-signals")
|
||||
progressive_rows["progressive-train-network"] = ("railway", "fluid-wagon",
|
||||
"automated-rail-transportation", "rail-signals")
|
||||
progressive_rows["progressive-engine"] = ("engine", "electric-engine")
|
||||
progressive_rows["progressive-armor"] = ("heavy-armor", "modular-armor", "power-armor", "power-armor-mk2")
|
||||
progressive_rows["progressive-personal-battery"] = ("battery-equipment", "battery-mk2-equipment")
|
||||
@@ -345,18 +379,40 @@ progressive_rows["progressive-wall"] = ("stone-wall", "gate")
|
||||
progressive_rows["progressive-follower"] = ("defender", "distractor", "destroyer")
|
||||
progressive_rows["progressive-inserter"] = ("fast-inserter", "stack-inserter")
|
||||
|
||||
sorted_rows = sorted(progressive_rows)
|
||||
# to keep ID mappings the same.
|
||||
# If there's a breaking change at some point, then this should be moved in with the sorted ordering
|
||||
progressive_rows["progressive-turret"] = ("gun-turret", "laser-turret")
|
||||
sorted_rows.append("progressive-turret")
|
||||
progressive_rows["progressive-flamethrower"] = ("flamethrower",) # leaving out flammables, as they do nothing
|
||||
sorted_rows.append("progressive-flamethrower")
|
||||
progressive_rows["progressive-personal-roboport-equipment"] = ("personal-roboport-equipment",
|
||||
"personal-roboport-mk2-equipment")
|
||||
sorted_rows.append("progressive-personal-roboport-equipment")
|
||||
# integrate into
|
||||
source_target_mapping: Dict[str, str] = {
|
||||
"progressive-braking-force": "progressive-train-network",
|
||||
"progressive-inserter-capacity-bonus": "progressive-inserter",
|
||||
"progressive-refined-flammables": "progressive-flamethrower"
|
||||
}
|
||||
|
||||
for source, target in source_target_mapping.items():
|
||||
progressive_rows[target] += progressive_rows[source]
|
||||
|
||||
base_tech_table = tech_table.copy() # without progressive techs
|
||||
base_technology_table = technology_table.copy()
|
||||
|
||||
progressive_tech_table: Dict[str, int] = {}
|
||||
progressive_technology_table: Dict[str, Technology] = {}
|
||||
|
||||
for root in sorted(progressive_rows):
|
||||
for root in sorted_rows:
|
||||
progressive = progressive_rows[root]
|
||||
assert all(tech in tech_table for tech in progressive)
|
||||
factorio_id += 1
|
||||
progressive_technology = Technology(root, technology_table[progressive_rows[root][0]].ingredients, factorio_id,
|
||||
progressive)
|
||||
progressive,
|
||||
has_modifier=any(technology_table[tech].has_modifier for tech in progressive),
|
||||
unlocks=any(technology_table[tech].unlocks for tech in progressive))
|
||||
progressive_tech_table[root] = progressive_technology.factorio_id
|
||||
progressive_technology_table[root] = progressive_technology
|
||||
if any(tech in advancement_technologies for tech in progressive):
|
||||
@@ -364,8 +420,9 @@ for root in sorted(progressive_rows):
|
||||
|
||||
tech_to_progressive_lookup: Dict[str, str] = {}
|
||||
for technology in progressive_technology_table.values():
|
||||
for progressive in technology.progressive:
|
||||
tech_to_progressive_lookup[progressive] = technology.name
|
||||
if technology.name not in source_target_mapping:
|
||||
for progressive in technology.progressive:
|
||||
tech_to_progressive_lookup[progressive] = technology.name
|
||||
|
||||
tech_table.update(progressive_tech_table)
|
||||
technology_table.update(progressive_technology_table)
|
||||
@@ -374,10 +431,13 @@ technology_table.update(progressive_technology_table)
|
||||
common_tech_table: Dict[str, int] = {tech_name: tech_id for tech_name, tech_id in base_tech_table.items()
|
||||
if tech_name not in progressive_tech_table}
|
||||
|
||||
useless_technologies: Set[str] = {tech_name for tech_name in common_tech_table
|
||||
if not technology_table[tech_name].useful()}
|
||||
|
||||
lookup_id_to_name: Dict[int, str] = {item_id: item_name for item_name, item_id in tech_table.items()}
|
||||
|
||||
rel_cost = {
|
||||
"wood" : 10000,
|
||||
"wood": 10000,
|
||||
"iron-ore": 1,
|
||||
"copper-ore": 1,
|
||||
"stone": 1,
|
||||
@@ -390,8 +450,9 @@ rel_cost = {
|
||||
}
|
||||
|
||||
# forbid liquids for now, TODO: allow a single liquid per assembler
|
||||
blacklist = all_ingredient_names | {"rocket-part", "crude-oil", "water", "sulfuric-acid", "petroleum-gas", "light-oil",
|
||||
"heavy-oil", "lubricant", "steam"}
|
||||
blacklist: Set[str] = all_ingredient_names | {"rocket-part", "crude-oil", "water", "sulfuric-acid", "petroleum-gas",
|
||||
"light-oil", "heavy-oil", "lubricant", "steam"}
|
||||
|
||||
|
||||
@Utils.cache_argsless
|
||||
def get_science_pack_pools() -> Dict[str, Set[str]]:
|
||||
@@ -403,7 +464,6 @@ def get_science_pack_pools() -> Dict[str, Set[str]]:
|
||||
cost += rel_cost.get(ingredient_name, 1) * amount
|
||||
return cost
|
||||
|
||||
|
||||
science_pack_pools = {}
|
||||
already_taken = blacklist.copy()
|
||||
current_difficulty = 5
|
||||
@@ -416,4 +476,4 @@ def get_science_pack_pools() -> Dict[str, Set[str]]:
|
||||
current -= already_taken
|
||||
already_taken |= current
|
||||
current_difficulty *= 2
|
||||
return science_pack_pools
|
||||
return science_pack_pools
|
||||
|
||||
@@ -1,44 +1,65 @@
|
||||
import collections
|
||||
|
||||
from ..AutoWorld import World
|
||||
|
||||
from BaseClasses import Region, Entrance, Location, Item
|
||||
from .Technologies import base_tech_table, recipe_sources, base_technology_table, advancement_technologies, \
|
||||
all_ingredient_names, required_technologies, get_rocket_requirements, rocket_recipes, \
|
||||
all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, rocket_recipes, \
|
||||
progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table, \
|
||||
get_science_pack_pools, Recipe, recipes, technology_table, tech_table
|
||||
get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies
|
||||
from .Shapes import get_shapes
|
||||
from .Mod import generate_mod
|
||||
from .Options import factorio_options
|
||||
from .Options import factorio_options, Silo
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
class FactorioItem(Item):
|
||||
game = "Factorio"
|
||||
|
||||
|
||||
all_items = tech_table.copy()
|
||||
all_items["Attack Trap"] = factorio_base_id - 1
|
||||
all_items["Evolution Trap"] = factorio_base_id - 2
|
||||
|
||||
|
||||
class Factorio(World):
|
||||
game: str = "Factorio"
|
||||
static_nodes = {"automation", "logistics", "rocket-silo"}
|
||||
custom_recipes = {}
|
||||
additional_advancement_technologies = set()
|
||||
item_names = frozenset(tech_table)
|
||||
location_names = frozenset(base_tech_table)
|
||||
|
||||
item_name_to_id = tech_table
|
||||
item_name_to_id = all_items
|
||||
location_name_to_id = base_tech_table
|
||||
|
||||
data_version = 5
|
||||
|
||||
def generate_basic(self):
|
||||
player = self.player
|
||||
want_progressives = collections.defaultdict(lambda: self.world.progressive[player].
|
||||
want_progressives(self.world.random))
|
||||
skip_silo = self.world.silo[player].value == Silo.option_spawn
|
||||
evolution_traps_wanted = self.world.evolution_traps[player].value
|
||||
attack_traps_wanted = self.world.attack_traps[player].value
|
||||
traps_wanted = ["Evolution Trap"] * evolution_traps_wanted + ["Attack Trap"] * attack_traps_wanted
|
||||
self.world.random.shuffle(traps_wanted)
|
||||
for tech_name in base_tech_table:
|
||||
if self.world.progressive:
|
||||
item_name = tech_to_progressive_lookup.get(tech_name, tech_name)
|
||||
if traps_wanted and tech_name in useless_technologies:
|
||||
self.world.itempool.append(self.create_item(traps_wanted.pop()))
|
||||
elif skip_silo and tech_name == "rocket-silo":
|
||||
pass
|
||||
else:
|
||||
item_name = item_name
|
||||
tech_item = self.create_item(item_name)
|
||||
if tech_name in self.static_nodes:
|
||||
self.world.get_location(tech_name, self.player).place_locked_item(tech_item)
|
||||
else:
|
||||
self.world.itempool.append(tech_item)
|
||||
map_basic_settings = self.world.world_gen[self.player].value["basic"]
|
||||
progressive_item_name = tech_to_progressive_lookup.get(tech_name, tech_name)
|
||||
want_progressive = want_progressives[progressive_item_name]
|
||||
item_name = progressive_item_name if want_progressive else tech_name
|
||||
tech_item = self.create_item(item_name)
|
||||
if tech_name in self.static_nodes:
|
||||
self.world.get_location(tech_name, player).place_locked_item(tech_item)
|
||||
else:
|
||||
self.world.itempool.append(tech_item)
|
||||
map_basic_settings = self.world.world_gen[player].value["basic"]
|
||||
if map_basic_settings.get("seed", None) is None: # allow seed 0
|
||||
map_basic_settings["seed"] = self.world.slot_seeds[self.player].randint(0, 2**32-1) # 32 bit uint
|
||||
map_basic_settings["seed"] = self.world.slot_seeds[player].randint(0, 2 ** 32 - 1) # 32 bit uint
|
||||
|
||||
generate_output = generate_mod
|
||||
|
||||
@@ -49,7 +70,10 @@ class Factorio(World):
|
||||
menu.exits.append(crash)
|
||||
nauvis = Region("Nauvis", None, "Nauvis", player, self.world)
|
||||
|
||||
skip_silo = self.world.silo[self.player].value == Silo.option_spawn
|
||||
for tech_name, tech_id in base_tech_table.items():
|
||||
if skip_silo and tech_name == "rocket-silo":
|
||||
continue
|
||||
tech = Location(player, tech_name, tech_id, nauvis)
|
||||
nauvis.locations.append(tech)
|
||||
tech.game = "Factorio"
|
||||
@@ -92,7 +116,10 @@ class Factorio(World):
|
||||
location.access_rule = lambda state, ingredient=ingredient: \
|
||||
all(state.has(technology.name, player) for technology in required_technologies[ingredient])
|
||||
|
||||
skip_silo = self.world.silo[self.player].value == Silo.option_spawn
|
||||
for tech_name, technology in self.custom_technologies.items():
|
||||
if skip_silo and tech_name == "rocket-silo":
|
||||
continue
|
||||
location = world.get_location(tech_name, player)
|
||||
Rules.set_rule(location, technology.build_rule(player))
|
||||
prequisites = shapes.get(tech_name)
|
||||
@@ -101,27 +128,102 @@ class Factorio(World):
|
||||
Rules.add_rule(location, lambda state,
|
||||
locations=locations: all(state.can_reach(loc) for loc in locations))
|
||||
|
||||
victory_tech_names = get_rocket_requirements(self.custom_recipes["rocket-part"])
|
||||
silo_recipe = None if self.world.silo[self.player].value == Silo.option_spawn \
|
||||
else self.custom_recipes["rocket-silo"] \
|
||||
if "rocket-silo" in self.custom_recipes \
|
||||
else next(iter(all_product_sources.get("rocket-silo")))
|
||||
part_recipe = self.custom_recipes["rocket-part"]
|
||||
victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe)
|
||||
world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player)
|
||||
for technology in
|
||||
victory_tech_names)
|
||||
|
||||
world.completion_condition[player] = lambda state: state.has('Victory', player)
|
||||
|
||||
def collect(self, state, item) -> bool:
|
||||
def collect_item(self, state, item):
|
||||
if item.advancement and item.name in progressive_technology_table:
|
||||
prog_table = progressive_technology_table[item.name].progressive
|
||||
for item_name in prog_table:
|
||||
if not state.has(item_name, item.player):
|
||||
state.prog_items[item_name, item.player] += 1
|
||||
return True
|
||||
return super(Factorio, self).collect(state, item)
|
||||
return item_name
|
||||
|
||||
return super(Factorio, self).collect_item(state, item)
|
||||
|
||||
def get_required_client_version(self) -> tuple:
|
||||
return max((0, 1, 5), super(Factorio, self).get_required_client_version())
|
||||
return max((0, 1, 6), super(Factorio, self).get_required_client_version())
|
||||
|
||||
options = factorio_options
|
||||
|
||||
def make_balanced_recipe(self, original: Recipe, pool: list, factor: float = 1) -> Recipe:
|
||||
"""Generate a recipe from pool with time and cost similar to original * factor"""
|
||||
new_ingredients = {}
|
||||
self.world.random.shuffle(pool)
|
||||
target_raw = int(sum((count for ingredient, count in original.base_cost.items())) * factor)
|
||||
target_energy = original.total_energy * factor
|
||||
target_num_ingredients = len(original.ingredients)
|
||||
remaining_raw = target_raw
|
||||
remaining_energy = target_energy
|
||||
remaining_num_ingredients = target_num_ingredients
|
||||
fallback_pool = []
|
||||
|
||||
# fill all but one slot with random ingredients, last with a good match
|
||||
while remaining_num_ingredients > 0 and len(pool) > 0:
|
||||
if remaining_num_ingredients == 1:
|
||||
max_raw = 1.1 * remaining_raw
|
||||
min_raw = 0.9 * remaining_raw
|
||||
max_energy = 1.1 * remaining_energy
|
||||
min_energy = 1.1 * remaining_energy
|
||||
else:
|
||||
max_raw = remaining_raw * 0.75
|
||||
min_raw = (remaining_raw - max_raw) / remaining_num_ingredients
|
||||
max_energy = remaining_energy * 0.75
|
||||
min_energy = (remaining_energy - max_energy) / remaining_num_ingredients
|
||||
ingredient = pool.pop()
|
||||
if ingredient not in recipes:
|
||||
logging.warning(f"missing recipe for {ingredient}")
|
||||
continue
|
||||
ingredient_recipe = recipes[ingredient]
|
||||
ingredient_raw = sum((count for ingredient, count in ingredient_recipe.base_cost.items()))
|
||||
ingredient_energy = ingredient_recipe.total_energy
|
||||
min_num_raw = min_raw/ingredient_raw
|
||||
max_num_raw = max_raw/ingredient_raw
|
||||
min_num_energy = min_energy/ingredient_energy
|
||||
max_num_energy = max_energy/ingredient_energy
|
||||
min_num = int(max(1, min_num_raw, min_num_energy))
|
||||
max_num = int(min(1000, max_num_raw, max_num_energy))
|
||||
if min_num > max_num:
|
||||
fallback_pool.append(ingredient)
|
||||
continue # can't use that ingredient
|
||||
num = self.world.random.randint(min_num,max_num)
|
||||
new_ingredients[ingredient] = num
|
||||
remaining_raw -= num * ingredient_raw
|
||||
remaining_energy -= num * ingredient_energy
|
||||
remaining_num_ingredients -= 1
|
||||
|
||||
# fill failed slots with whatever we got
|
||||
pool = fallback_pool
|
||||
while remaining_num_ingredients > 0 and len(pool) > 0:
|
||||
ingredient = pool.pop()
|
||||
if ingredient not in recipes:
|
||||
logging.warning(f"missing recipe for {ingredient}")
|
||||
continue
|
||||
ingredient_recipe = recipes[ingredient]
|
||||
ingredient_raw = sum((count for ingredient, count in ingredient_recipe.base_cost.items()))
|
||||
ingredient_energy = ingredient_recipe.total_energy
|
||||
num_raw = remaining_raw/ingredient_raw/remaining_num_ingredients
|
||||
num_energy = remaining_energy/ingredient_energy/remaining_num_ingredients
|
||||
num = int(min(num_raw, num_energy))
|
||||
if num < 1: continue
|
||||
new_ingredients[ingredient] = num
|
||||
remaining_raw -= num * ingredient_raw
|
||||
remaining_energy -= num * ingredient_energy
|
||||
remaining_num_ingredients -= 1
|
||||
|
||||
if remaining_num_ingredients > 1:
|
||||
logging.warning("could not randomize recipe")
|
||||
|
||||
return Recipe(original.name, original.category, new_ingredients, original.products, original.energy)
|
||||
|
||||
def set_custom_technologies(self):
|
||||
custom_technologies = {}
|
||||
allowed_packs = self.world.max_science_pack[self.player].get_allowed_packs()
|
||||
@@ -136,7 +238,8 @@ class Factorio(World):
|
||||
self.world.random.shuffle(valid_pool)
|
||||
self.custom_recipes = {"rocket-part": Recipe("rocket-part", original_rocket_part.category,
|
||||
{valid_pool[x] : 10 for x in range(3)},
|
||||
original_rocket_part.products)}
|
||||
original_rocket_part.products,
|
||||
original_rocket_part.energy)}
|
||||
self.additional_advancement_technologies = {tech.name for tech in
|
||||
self.custom_recipes["rocket-part"].recursive_unlocking_technologies}
|
||||
|
||||
@@ -150,11 +253,21 @@ class Factorio(World):
|
||||
new_ingredients = {}
|
||||
for _ in original.ingredients:
|
||||
new_ingredients[valid_pool.pop()] = 1
|
||||
new_recipe = Recipe(pack, original.category, new_ingredients, original.products)
|
||||
new_recipe = Recipe(pack, original.category, new_ingredients, original.products, original.energy)
|
||||
self.additional_advancement_technologies |= {tech.name for tech in
|
||||
new_recipe.recursive_unlocking_technologies}
|
||||
self.custom_recipes[pack] = new_recipe
|
||||
|
||||
if self.world.silo[self.player].value == Silo.option_randomize_recipe:
|
||||
valid_pool = []
|
||||
for pack in self.world.max_science_pack[self.player].get_allowed_packs():
|
||||
valid_pool += sorted(science_pack_pools[pack])
|
||||
new_recipe = self.make_balanced_recipe(recipes["rocket-silo"], valid_pool,
|
||||
factor = (self.world.max_science_pack[self.player].value+1)/7)
|
||||
self.additional_advancement_technologies |= {tech.name for tech in
|
||||
new_recipe.recursive_unlocking_technologies}
|
||||
self.custom_recipes["rocket-silo"] = new_recipe
|
||||
|
||||
# handle marking progressive techs as advancement
|
||||
prog_add = set()
|
||||
for tech in self.additional_advancement_technologies:
|
||||
@@ -163,7 +276,9 @@ class Factorio(World):
|
||||
self.additional_advancement_technologies |= prog_add
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
assert name in tech_table
|
||||
return FactorioItem(name, name in advancement_technologies or
|
||||
name in self.additional_advancement_technologies,
|
||||
tech_table[name], self.player)
|
||||
if name in tech_table:
|
||||
return FactorioItem(name, name in advancement_technologies or
|
||||
name in self.additional_advancement_technologies,
|
||||
tech_table[name], self.player)
|
||||
elif name in all_items:
|
||||
return FactorioItem(name, False, all_items[name], self.player)
|
||||
|
||||
BIN
worlds/factorio/data/mod/graphics/icons/ap.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
worlds/factorio/data/mod/graphics/icons/ap_unimportant.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
@@ -7,6 +7,7 @@ FREE_SAMPLES = {{ free_samples }}
|
||||
SLOT_NAME = "{{ slot_name }}"
|
||||
SEED_NAME = "{{ seed_name }}"
|
||||
FREE_SAMPLE_BLACKLIST = {{ dict_to_lua(free_sample_blacklist) }}
|
||||
TRAP_EVO_FACTOR = {{ evolution_trap_increase }} / 100
|
||||
|
||||
{% if not imported_blueprints -%}
|
||||
function set_permissions()
|
||||
@@ -18,19 +19,56 @@ function set_permissions()
|
||||
end
|
||||
{%- endif %}
|
||||
|
||||
|
||||
function check_spawn_silo(force)
|
||||
if force.players and #force.players > 0 and force.get_entity_count("rocket-silo") < 1 then
|
||||
local surface = game.get_surface(1)
|
||||
local spawn_position = force.get_spawn_position(surface)
|
||||
spawn_entity(surface, force, "rocket-silo", spawn_position.x, spawn_position.y, 80, true, true)
|
||||
end
|
||||
end
|
||||
|
||||
function check_despawn_silo(force)
|
||||
if not force.players or #force.players < 1 and force.get_entity_count("rocket-silo") > 0 then
|
||||
local surface = game.get_surface(1)
|
||||
local spawn_position = force.get_spawn_position(surface)
|
||||
local x1 = spawn_position.x - 41
|
||||
local x2 = spawn_position.x + 41
|
||||
local y1 = spawn_position.y - 41
|
||||
local y2 = spawn_position.y + 41
|
||||
local silos = surface.find_entities_filtered{area = { {x1, y1}, {x2, y2} },
|
||||
name = "rocket-silo",
|
||||
force = force}
|
||||
for i,silo in ipairs(silos) do
|
||||
silo.destructible = true
|
||||
silo.destroy()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Initialize force data, either from it being created or already being part of the game when the mod was added.
|
||||
function on_force_created(event)
|
||||
--event.force appears to be LuaForce.name, not LuaForce
|
||||
game.forces[event.force].research_queue_enabled = true
|
||||
local force = event.force
|
||||
if type(event.force) == "string" then -- should be of type LuaForce
|
||||
force = game.forces[force]
|
||||
end
|
||||
force.research_queue_enabled = true
|
||||
local data = {}
|
||||
data['earned_samples'] = {{ dict_to_lua(starting_items) }}
|
||||
data["victory"] = 0
|
||||
global.forcedata[event.force] = data
|
||||
{%- if silo == 2 %}
|
||||
check_spawn_silo(force)
|
||||
{%- endif %}
|
||||
end
|
||||
script.on_event(defines.events.on_force_created, on_force_created)
|
||||
|
||||
-- Destroy force data. This doesn't appear to be currently possible with the Factorio API, but here for completeness.
|
||||
function on_force_destroyed(event)
|
||||
{%- if silo == 2 %}
|
||||
check_despawn_silo(event.force)
|
||||
{%- endif %}
|
||||
global.forcedata[event.force.name] = nil
|
||||
end
|
||||
|
||||
@@ -44,9 +82,21 @@ function on_player_created(event)
|
||||
data['pending_samples'] = table.deepcopy(global.forcedata[player.force.name]['earned_samples'])
|
||||
global.playerdata[player.index] = data
|
||||
update_player(player.index) -- Attempt to send pending free samples, if relevant.
|
||||
{%- if silo == 2 %}
|
||||
check_spawn_silo(game.players[event.player_index].force)
|
||||
{%- endif %}
|
||||
end
|
||||
script.on_event(defines.events.on_player_created, on_player_created)
|
||||
|
||||
-- Create/destroy silo for force if player switched force
|
||||
function on_player_changed_force(event)
|
||||
{%- if silo == 2 %}
|
||||
check_despawn_silo(event.force)
|
||||
check_spawn_silo(game.players[event.player_index].force)
|
||||
{%- endif %}
|
||||
end
|
||||
script.on_event(defines.events.on_player_changed_force, on_player_changed_force)
|
||||
|
||||
function on_player_removed(event)
|
||||
global.playerdata[event.player_index] = nil
|
||||
end
|
||||
@@ -195,6 +245,106 @@ function chain_lookup(table, ...)
|
||||
end
|
||||
|
||||
|
||||
function spawn_entity(surface, force, name, x, y, radius, randomize, avoid_ores)
|
||||
local prototype = game.entity_prototypes[name]
|
||||
local args = { -- For can_place_entity and place_entity
|
||||
name = prototype.name,
|
||||
position = {x = x, y = y},
|
||||
force = force.name,
|
||||
build_check_type = defines.build_check_type.blueprint_ghost,
|
||||
forced = true
|
||||
}
|
||||
|
||||
local box = prototype.selection_box
|
||||
local dims = {
|
||||
w = box.right_bottom.x - box.left_top.x,
|
||||
h = box.right_bottom.y - box.left_top.y
|
||||
}
|
||||
local entity_radius = math.ceil(math.max(dims.w, dims.h) / math.sqrt(2) / 2)
|
||||
local bounds = {
|
||||
xmin = math.ceil(x - radius - box.left_top.x),
|
||||
xmax = math.floor(x + radius - box.right_bottom.x),
|
||||
ymin = math.ceil(y - radius - box.left_top.y),
|
||||
ymax = math.floor(y + radius - box.right_bottom.y)
|
||||
}
|
||||
|
||||
local new_entity = nil
|
||||
local attempts = 1000
|
||||
for i = 1,attempts do -- Try multiple times
|
||||
-- Find a position
|
||||
if (randomize and i < attempts-3) or (not randomize and i ~= 1) then
|
||||
args.position.x = math.random(bounds.xmin, bounds.xmax)
|
||||
args.position.y = math.random(bounds.ymin, bounds.ymax)
|
||||
elseif randomize then
|
||||
args.position.x = x + (i + 3 - attempts) * dims.w
|
||||
args.position.y = y + (i + 3 - attempts) * dims.h
|
||||
end
|
||||
-- Generate required chunks
|
||||
local x1 = args.position.x + box.left_top.x
|
||||
local x2 = args.position.x + box.right_bottom.x
|
||||
local y1 = args.position.y + box.left_top.y
|
||||
local y2 = args.position.y + box.right_bottom.y
|
||||
if not surface.is_chunk_generated({x = x1, y = y1}) or
|
||||
not surface.is_chunk_generated({x = x2, y = y1}) or
|
||||
not surface.is_chunk_generated({x = x1, y = y2}) or
|
||||
not surface.is_chunk_generated({x = x2, y = y2}) then
|
||||
surface.request_to_generate_chunks(args.position, entity_radius)
|
||||
surface.force_generate_chunk_requests()
|
||||
end
|
||||
-- Try to place entity
|
||||
if surface.can_place_entity(args) then
|
||||
-- Can hypothetically place this entity here. Destroy everything underneath it.
|
||||
local collision_area = {
|
||||
{
|
||||
args.position.x + prototype.collision_box.left_top.x,
|
||||
args.position.y + prototype.collision_box.left_top.y
|
||||
},
|
||||
{
|
||||
args.position.x + prototype.collision_box.right_bottom.x,
|
||||
args.position.y + prototype.collision_box.right_bottom.y
|
||||
}
|
||||
}
|
||||
local entities = surface.find_entities_filtered {
|
||||
area = collision_area,
|
||||
collision_mask = prototype.collision_mask
|
||||
}
|
||||
local can_place = true
|
||||
for _, entity in pairs(entities) do
|
||||
if entity.force and entity.force.name ~= 'neutral' then
|
||||
can_place = false
|
||||
break
|
||||
end
|
||||
end
|
||||
local allow_placement_on_resources = not avoid_ores or i > attempts/2
|
||||
if can_place and not allow_placement_on_resources then
|
||||
local resources = surface.find_entities_filtered {
|
||||
area = collision_area,
|
||||
type = 'resource'
|
||||
}
|
||||
can_place = (next(resources) == nil)
|
||||
end
|
||||
if can_place then
|
||||
for _, entity in pairs(entities) do
|
||||
entity.destroy({do_cliff_correction=true, raise_destroy=true})
|
||||
end
|
||||
args.build_check_type = defines.build_check_type.script
|
||||
args.create_build_effect_smoke = false
|
||||
new_entity = surface.create_entity(args)
|
||||
if new_entity then
|
||||
new_entity.destructible = false
|
||||
new_entity.minable = false
|
||||
new_entity.rotatable = false
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if new_entity == nil then
|
||||
force.print("Failed to place " .. args.name .. " in " .. serpent.line({x = x, y = y, radius = radius}))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- add / commands
|
||||
commands.add_command("ap-sync", "Used by the Archipelago client to get progress information", function(call)
|
||||
local force
|
||||
@@ -217,6 +367,9 @@ commands.add_command("ap-sync", "Used by the Archipelago client to get progress
|
||||
rcon.print(game.table_to_json({["slot_name"] = SLOT_NAME, ["seed_name"] = SEED_NAME, ["info"] = data_collection}))
|
||||
end)
|
||||
|
||||
commands.add_command("ap-print", "Used by the Archipelago client to print messages", function (call)
|
||||
game.print(call.parameter)
|
||||
end)
|
||||
|
||||
commands.add_command("ap-get-technology", "Grant a technology, used by the Archipelago Client.", function(call)
|
||||
if global.index_sync == nil then
|
||||
@@ -225,15 +378,23 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
|
||||
local tech
|
||||
local force = game.forces["player"]
|
||||
chunks = split(call.parameter, "\t")
|
||||
local tech_name = chunks[1]
|
||||
local item_name = chunks[1]
|
||||
local index = chunks[2]
|
||||
local source = chunks[3] or "Archipelago"
|
||||
if progressive_technologies[tech_name] ~= nil then
|
||||
if index == -1 then -- for coop sync and restoring from an older savegame
|
||||
tech = force.technologies[item_name]
|
||||
if tech.researched ~= true then
|
||||
game.print({"", "Received [technology=" .. tech.name .. "] as it is already checked."})
|
||||
game.play_sound({path="utility/research_completed"})
|
||||
tech.researched = true
|
||||
return
|
||||
end
|
||||
elseif progressive_technologies[item_name] ~= nil then
|
||||
if global.index_sync[index] == nil then -- not yet received prog item
|
||||
global.index_sync[index] = tech_name
|
||||
local tech_stack = progressive_technologies[tech_name]
|
||||
for _, tech_name in ipairs(tech_stack) do
|
||||
tech = force.technologies[tech_name]
|
||||
global.index_sync[index] = item_name
|
||||
local tech_stack = progressive_technologies[item_name]
|
||||
for _, item_name in ipairs(tech_stack) do
|
||||
tech = force.technologies[item_name]
|
||||
if tech.researched ~= true then
|
||||
game.print({"", "Received [technology=" .. tech.name .. "] from ", source})
|
||||
game.play_sound({path="utility/research_completed"})
|
||||
@@ -242,8 +403,8 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif force.technologies[tech_name] ~= nil then
|
||||
tech = force.technologies[tech_name]
|
||||
elseif force.technologies[item_name] ~= nil then
|
||||
tech = force.technologies[item_name]
|
||||
if tech ~= nil then
|
||||
if global.index_sync[index] ~= nil and global.index_sync[index] ~= tech then
|
||||
game.print("Warning: Desync Detected. Duplicate/Missing items may occur.")
|
||||
@@ -255,8 +416,21 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
|
||||
tech.researched = true
|
||||
end
|
||||
end
|
||||
elseif item_name == "Attack Trap" then
|
||||
if global.index_sync[index] == nil then -- not yet received trap
|
||||
game.print({"", "Received Attack Trap from ", source})
|
||||
global.index_sync[index] = item_name
|
||||
local spawn_position = force.get_spawn_position(game.get_surface(1))
|
||||
game.surfaces["nauvis"].build_enemy_base(spawn_position, 25)
|
||||
end
|
||||
elseif item_name == "Evolution Trap" then
|
||||
if global.index_sync[index] == nil then -- not yet received trap
|
||||
global.index_sync[index] = item_name
|
||||
game.forces["enemy"].evolution_factor = game.forces["enemy"].evolution_factor + TRAP_EVO_FACTOR
|
||||
game.print({"", "Received Evolution Trap from ", source, ". New factor:", game.forces["enemy"].evolution_factor})
|
||||
end
|
||||
else
|
||||
game.print("Unknown Technology " .. tech_name)
|
||||
game.print("Unknown Item " .. item_name)
|
||||
end
|
||||
end)
|
||||
|
||||
@@ -265,5 +439,13 @@ commands.add_command("ap-rcon-info", "Used by the Archipelago client to get info
|
||||
rcon.print(game.table_to_json({["slot_name"] = SLOT_NAME, ["seed_name"] = SEED_NAME}))
|
||||
end)
|
||||
|
||||
|
||||
{% if allow_cheats -%}
|
||||
commands.add_command("ap-spawn-silo", "Attempts to spawn a silo around 0,0", function(call)
|
||||
spawn_entity(game.player.surface, game.player.force, "rocket-silo", 0, 0, 80, true, true)
|
||||
end)
|
||||
{% endif -%}
|
||||
|
||||
|
||||
-- data
|
||||
progressive_technologies = {{ dict_to_lua(progressive_technology_table) }}
|
||||
progressive_technologies = {{ dict_to_lua(progressive_technology_table) }}
|
||||
@@ -51,20 +51,32 @@ function copy_factorio_icon(tech, tech_source)
|
||||
tech.icon_size = table.deepcopy(technologies[tech_source].icon_size)
|
||||
end
|
||||
|
||||
{# This got complex, but seems to be required to hit all corner cases #}
|
||||
function adjust_energy(recipe_name, factor)
|
||||
local recipe = data.raw.recipe[recipe_name]
|
||||
local energy = recipe.energy_required
|
||||
if (energy ~= nil) then
|
||||
data.raw.recipe[recipe_name].energy_required = energy * factor
|
||||
end
|
||||
if (recipe.normal ~= nil and recipe.normal.energy_required ~= nil) then
|
||||
energy = recipe.normal.energy_required
|
||||
|
||||
if (recipe.normal ~= nil) then
|
||||
if (recipe.normal.energy_required == nil) then
|
||||
energy = 0.5
|
||||
else
|
||||
energy = recipe.normal.energy_required
|
||||
end
|
||||
recipe.normal.energy_required = energy * factor
|
||||
end
|
||||
if (recipe.expensive ~= nil and recipe.expensive.energy_required ~= nil) then
|
||||
energy = recipe.expensive.energy_required
|
||||
if (recipe.expensive ~= nil) then
|
||||
if (recipe.expensive.energy_required == nil) then
|
||||
energy = 0.5
|
||||
else
|
||||
energy = recipe.expensive.energy_required
|
||||
end
|
||||
recipe.expensive.energy_required = energy * factor
|
||||
end
|
||||
if (energy ~= nil) then
|
||||
data.raw.recipe[recipe_name].energy_required = energy * factor
|
||||
elseif (recipe.expensive == nil and recipe.normal == nil) then
|
||||
data.raw.recipe[recipe_name].energy_required = 0.5 * factor
|
||||
end
|
||||
end
|
||||
|
||||
data.raw["assembling-machine"]["assembling-machine-1"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories)
|
||||
@@ -85,6 +97,11 @@ new_tree_copy.unit.count = math.max(1, math.floor(new_tree_copy.unit.count * {{
|
||||
{%- if (tech_tree_information == 2 or original_tech_name in static_nodes) and item_name in base_tech_table -%}
|
||||
{#- copy Factorio Technology Icon -#}
|
||||
copy_factorio_icon(new_tree_copy, "{{ item_name }}")
|
||||
{%- if original_tech_name == "rocket-silo" and original_tech_name in static_nodes %}
|
||||
{%- for ingredient in custom_recipes["rocket-part"].ingredients %}
|
||||
table.insert(new_tree_copy.effects, {type = "nothing", effect_description = "Ingredient {{ loop.index }}: {{ ingredient }}"})
|
||||
{% endfor -%}
|
||||
{% endif -%}
|
||||
{%- elif (tech_tree_information == 2 or original_tech_name in static_nodes) and item_name in progressive_technology_table -%}
|
||||
copy_factorio_icon(new_tree_copy, "{{ progressive_technology_table[item_name][0] }}")
|
||||
{%- else -%}
|
||||
@@ -103,7 +120,12 @@ data:extend{new_tree_copy}
|
||||
{% if recipe_time_scale %}
|
||||
{%- for recipe_name, recipe in recipes.items() %}
|
||||
{%- if recipe.category != "mining" %}
|
||||
adjust_energy("{{ recipe_name }}", {{ random.triangular(*recipe_time_scale) }})
|
||||
adjust_energy("{{ recipe_name }}", {{ flop_random(*recipe_time_scale) }})
|
||||
{%- endif %}
|
||||
{%- endfor -%}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{%- if silo==2 %}
|
||||
-- disable silo research for pre-placed silo
|
||||
technologies["rocket-silo"].hidden = true
|
||||
{%- endif %}
|
||||
1
worlds/factorio/data/recipes.json
Normal file
1
worlds/factorio/data/techs.json
Normal file
@@ -12,7 +12,7 @@ def locality_rules(world, player):
|
||||
def exclusion_rules(world, player: int, excluded_locations: set):
|
||||
for loc_name in excluded_locations:
|
||||
location = world.get_location(loc_name, player)
|
||||
add_item_rule(location, lambda i: not (i.advancement or i.smallkey or i.bigkey or i.never_exclude))
|
||||
add_item_rule(location, lambda i: not (i.advancement or i.never_exclude))
|
||||
|
||||
|
||||
def set_rule(spot, rule):
|
||||
|
||||
@@ -15,8 +15,6 @@ from ..AutoWorld import World, LogicMixin
|
||||
class HKWorld(World):
|
||||
game: str = "Hollow Knight"
|
||||
options = hollow_knight_options
|
||||
item_names: Set[str] = frozenset(item_table)
|
||||
location_names: Set[str] = frozenset(lookup_name_to_id)
|
||||
|
||||
item_name_to_id = {name: data.id for name, data in item_table.items() if data.type != "Event"}
|
||||
location_name_to_id = lookup_name_to_id
|
||||
|
||||
@@ -13,8 +13,8 @@ class MinecraftItem(Item):
|
||||
|
||||
item_table = {
|
||||
"Archery": ItemData(45000, True),
|
||||
"Ingot Crafting": ItemData(45001, True),
|
||||
"Resource Blocks": ItemData(45002, True),
|
||||
"Progressive Resource Crafting": ItemData(45001, True),
|
||||
# "Resource Blocks": ItemData(45002, True),
|
||||
"Brewing": ItemData(45003, True),
|
||||
"Enchanting": ItemData(45004, True),
|
||||
"Bucket": ItemData(45005, True),
|
||||
@@ -55,38 +55,48 @@ item_table = {
|
||||
"Structure Compass (Bastion Remnant)": ItemData(45040, True),
|
||||
"Structure Compass (End City)": ItemData(45041, True),
|
||||
"Shulker Box": ItemData(45042, False),
|
||||
"Dragon Egg Shard": ItemData(45043, True),
|
||||
"Bee Trap (Minecraft)": ItemData(45100, False),
|
||||
|
||||
"Victory": ItemData(None, True)
|
||||
}
|
||||
|
||||
# If not listed here then has frequency 1
|
||||
item_frequencies = {
|
||||
# 33 required items
|
||||
required_items = {
|
||||
"Archery": 1,
|
||||
"Progressive Resource Crafting": 2,
|
||||
"Brewing": 1,
|
||||
"Enchanting": 1,
|
||||
"Bucket": 1,
|
||||
"Flint and Steel": 1,
|
||||
"Bed": 1,
|
||||
"Bottles": 1,
|
||||
"Shield": 1,
|
||||
"Fishing Rod": 1,
|
||||
"Campfire": 1,
|
||||
"Progressive Weapons": 3,
|
||||
"Progressive Tools": 3,
|
||||
"Progressive Tools": 3,
|
||||
"Progressive Armor": 2,
|
||||
"8 Netherite Scrap": 2,
|
||||
"8 Emeralds": 0,
|
||||
"4 Emeralds": 8,
|
||||
"4 Diamond Ore": 4,
|
||||
"16 Iron Ore": 4,
|
||||
"500 XP": 0,
|
||||
"100 XP": 0,
|
||||
"50 XP": 21,
|
||||
"3 Ender Pearls": 4,
|
||||
"4 Lapis Lazuli": 2,
|
||||
"16 Porkchops": 8,
|
||||
"8 Gold Ore": 4,
|
||||
"Rotten Flesh": 4,
|
||||
"Single Arrow": 0,
|
||||
"32 Arrows": 4,
|
||||
"Structure Compass (Village)": 0,
|
||||
"Structure Compass (Pillager Outpost)": 0,
|
||||
"Structure Compass (Nether Fortress)": 0,
|
||||
"Structure Compass (Bastion Remnant)": 0,
|
||||
"Structure Compass (End City)": 0,
|
||||
"Shulker Box": 0,
|
||||
"Bee Trap (Minecraft)": 0,
|
||||
"8 Netherite Scrap": 2,
|
||||
"Channeling Book": 1,
|
||||
"Silk Touch Book": 1,
|
||||
"Sharpness III Book": 1,
|
||||
"Piercing IV Book": 1,
|
||||
"Looting III Book": 1,
|
||||
"Infinity Book": 1,
|
||||
"3 Ender Pearls": 4,
|
||||
"Saddle": 1,
|
||||
}
|
||||
|
||||
junk_weights = {
|
||||
"4 Emeralds": 2,
|
||||
"4 Diamond Ore": 1,
|
||||
"16 Iron Ore": 1,
|
||||
"50 XP": 4,
|
||||
"16 Porkchops": 2,
|
||||
"8 Gold Ore": 1,
|
||||
"Rotten Flesh": 1,
|
||||
"32 Arrows": 1,
|
||||
}
|
||||
|
||||
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code}
|
||||
|
||||
@@ -15,6 +15,16 @@ class CombatDifficulty(Choice):
|
||||
default = 1
|
||||
|
||||
|
||||
class BeeTraps(Range):
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
|
||||
|
||||
class EggShards(Range):
|
||||
range_start = 0
|
||||
range_end = 30
|
||||
|
||||
|
||||
minecraft_options: typing.Dict[str, type(Option)] = {
|
||||
"advancement_goal": AdvancementGoal,
|
||||
"combat_difficulty": CombatDifficulty,
|
||||
@@ -23,5 +33,7 @@ minecraft_options: typing.Dict[str, type(Option)] = {
|
||||
"include_postgame_advancements": Toggle,
|
||||
"shuffle_structures": Toggle,
|
||||
"structure_compasses": Toggle,
|
||||
"bee_traps": Toggle
|
||||
"bee_traps": BeeTraps,
|
||||
"egg_shards_required": EggShards,
|
||||
"egg_shards_available": EggShards
|
||||
}
|
||||
@@ -13,7 +13,7 @@ def link_minecraft_structures(world, player):
|
||||
try:
|
||||
assert len(exits) == len(structs)
|
||||
except AssertionError as e: # this should never happen
|
||||
raise Exception(f"Could not obtain equal numbers of Minecraft exits and structures for player {player} ({world.player_names[player]})")
|
||||
raise Exception(f"Could not obtain equal numbers of Minecraft exits and structures for player {player} ({world.player_name[player]})")
|
||||
|
||||
pairs = {}
|
||||
|
||||
@@ -23,7 +23,7 @@ def link_minecraft_structures(world, player):
|
||||
exits.remove(exit)
|
||||
structs.remove(struct)
|
||||
else:
|
||||
raise Exception(f"Invalid connection: {exit} => {struct} for player {player} ({world.player_names[player]})")
|
||||
raise Exception(f"Invalid connection: {exit} => {struct} for player {player} ({world.player_name[player]})")
|
||||
|
||||
# Connect plando structures first
|
||||
if world.plando_connections[player]:
|
||||
@@ -38,7 +38,7 @@ def link_minecraft_structures(world, player):
|
||||
try:
|
||||
exit = world.random.choice([e for e in exits if e not in illegal_connections.get(struct, [])])
|
||||
except IndexError:
|
||||
raise Exception(f"No valid structure placements remaining for player {player} ({world.player_names[player]})")
|
||||
raise Exception(f"No valid structure placements remaining for player {player} ({world.player_name[player]})")
|
||||
set_pair(exit, struct)
|
||||
else: # write remaining default connections
|
||||
for (exit, struct) in default_connections:
|
||||
@@ -49,7 +49,7 @@ def link_minecraft_structures(world, player):
|
||||
try:
|
||||
assert len(exits) == len(structs) == 0
|
||||
except AssertionError:
|
||||
raise Exception(f"Failed to connect all Minecraft structures for player {player} ({world.player_names[player]})")
|
||||
raise Exception(f"Failed to connect all Minecraft structures for player {player} ({world.player_name[player]})")
|
||||
|
||||
for exit in exits_spoiler:
|
||||
world.get_entrance(exit, player).connect(world.get_region(pairs[exit], player))
|
||||
|
||||
@@ -7,10 +7,10 @@ from ..AutoWorld import LogicMixin
|
||||
class MinecraftLogic(LogicMixin):
|
||||
|
||||
def _mc_has_iron_ingots(self, player: int):
|
||||
return self.has('Progressive Tools', player) and self.has('Ingot Crafting', player)
|
||||
return self.has('Progressive Tools', player) and self.has('Progressive Resource Crafting', player)
|
||||
|
||||
def _mc_has_gold_ingots(self, player: int):
|
||||
return self.has('Ingot Crafting', player) and (self.has('Progressive Tools', player, 2) or self.can_reach('The Nether', 'Region', player))
|
||||
return self.has('Progressive Resource Crafting', player) and (self.has('Progressive Tools', player, 2) or self.can_reach('The Nether', 'Region', player))
|
||||
|
||||
def _mc_has_diamond_pickaxe(self, player: int):
|
||||
return self.has('Progressive Tools', player, 3) and self._mc_has_iron_ingots(player)
|
||||
@@ -19,38 +19,40 @@ class MinecraftLogic(LogicMixin):
|
||||
return self.has('Archery', player) and self._mc_has_iron_ingots(player)
|
||||
|
||||
def _mc_has_bottle(self, player: int):
|
||||
return self.has('Bottles', player) and self.has('Ingot Crafting', player)
|
||||
return self.has('Bottles', player) and self.has('Progressive Resource Crafting', player)
|
||||
|
||||
def _mc_can_enchant(self, player: int):
|
||||
return self.has('Enchanting', player) and self._mc_has_diamond_pickaxe(player) # mine obsidian and lapis
|
||||
|
||||
def _mc_can_use_anvil(self, player: int):
|
||||
return self.has('Enchanting', player) and self.has('Resource Blocks', player) and self._mc_has_iron_ingots(player)
|
||||
return self.has('Enchanting', player) and self.has('Progressive Resource Crafting', player, 2) and self._mc_has_iron_ingots(player)
|
||||
|
||||
def _mc_fortress_loot(self, player: int): # saddles, blaze rods, wither skulls
|
||||
return self.can_reach('Nether Fortress', 'Region', player) and self._mc_basic_combat(player)
|
||||
|
||||
def _mc_can_brew_potions(self, player: int):
|
||||
def _mc_can_brew_potions(self, player: int):
|
||||
return self._mc_fortress_loot(player) and self.has('Brewing', player) and self._mc_has_bottle(player)
|
||||
|
||||
def _mc_can_piglin_trade(self, player: int):
|
||||
return self._mc_has_gold_ingots(player) and (self.can_reach('The Nether', 'Region', player) or self.can_reach('Bastion Remnant', 'Region', player))
|
||||
def _mc_can_piglin_trade(self, player: int):
|
||||
return self._mc_has_gold_ingots(player) and (
|
||||
self.can_reach('The Nether', 'Region', player) or self.can_reach('Bastion Remnant', 'Region',
|
||||
player))
|
||||
|
||||
def _mc_enter_stronghold(self, player: int):
|
||||
def _mc_enter_stronghold(self, player: int):
|
||||
return self._mc_fortress_loot(player) and self.has('Brewing', player) and self.has('3 Ender Pearls', player)
|
||||
|
||||
# Difficulty-dependent functions
|
||||
def _mc_combat_difficulty(self, player: int):
|
||||
return self.world.combat_difficulty[player].get_option_name()
|
||||
def _mc_combat_difficulty(self, player: int):
|
||||
return self.world.combat_difficulty[player].current_key
|
||||
|
||||
def _mc_can_adventure(self, player: int):
|
||||
if self._mc_combat_difficulty(player) == 'easy':
|
||||
if self._mc_combat_difficulty(player) == 'easy':
|
||||
return self.has('Progressive Weapons', player, 2) and self._mc_has_iron_ingots(player)
|
||||
elif self._mc_combat_difficulty(player) == 'hard':
|
||||
elif self._mc_combat_difficulty(player) == 'hard':
|
||||
return True
|
||||
return self.has('Progressive Weapons', player) and (self.has('Ingot Crafting', player) or self.has('Campfire', player))
|
||||
return self.has('Progressive Weapons', player) and (self.has('Progressive Resource Crafting', player) or self.has('Campfire', player))
|
||||
|
||||
def _mc_basic_combat(self, player: int):
|
||||
def _mc_basic_combat(self, player: int):
|
||||
if self._mc_combat_difficulty(player) == 'easy':
|
||||
return self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player) and \
|
||||
self.has('Shield', player) and self._mc_has_iron_ingots(player)
|
||||
@@ -81,7 +83,7 @@ class MinecraftLogic(LogicMixin):
|
||||
|
||||
def _mc_can_kill_ender_dragon(self, player: int):
|
||||
# Since it is possible to kill the dragon without getting any of the advancements related to it, we need to require that it can be respawned.
|
||||
respawn_dragon = self.can_reach('The Nether', 'Region', player) and self.has('Ingot Crafting', player)
|
||||
respawn_dragon = self.can_reach('The Nether', 'Region', player) and self.has('Progressive Resource Crafting', player)
|
||||
if self._mc_combat_difficulty(player) == 'easy':
|
||||
return respawn_dragon and self.has("Progressive Weapons", player, 3) and self.has("Progressive Armor", player, 2) and \
|
||||
self.has('Archery', player) and self._mc_can_brew_potions(player) and self._mc_can_enchant(player)
|
||||
@@ -115,8 +117,9 @@ def set_rules(world: MultiWorld, player: int):
|
||||
# 92 total advancements. Goal is to complete X advancements and then Free the End.
|
||||
# There are 5 advancements which cannot be included for dragon spawning (4 postgame, Free the End)
|
||||
# Hence the true maximum is (92 - 5) = 87
|
||||
goal = int(world.advancement_goal[player].value)
|
||||
can_complete = lambda state: len(reachable_locations(state)) >= goal and state.can_reach('The End', 'Region', player) and state._mc_can_kill_ender_dragon(player)
|
||||
goal = world.advancement_goal[player]
|
||||
egg_shards = min(world.egg_shards_required[player], world.egg_shards_available[player])
|
||||
can_complete = lambda state: len(reachable_locations(state)) >= goal and state.has("Dragon Egg Shard", player, egg_shards) and state.can_reach('The End', 'Region', player) and state._mc_can_kill_ender_dragon(player)
|
||||
|
||||
if world.logic[player] != 'nologic':
|
||||
world.completion_condition[player] = lambda state: state.has('Victory', player)
|
||||
@@ -147,7 +150,7 @@ def set_rules(world: MultiWorld, player: int):
|
||||
state.can_reach('Bring Home the Beacon', 'Location', player)) # Resistance
|
||||
set_rule(world.get_location("Best Friends Forever", player), lambda state: True)
|
||||
set_rule(world.get_location("Bring Home the Beacon", player), lambda state: state._mc_can_kill_wither(player) and
|
||||
state._mc_has_diamond_pickaxe(player) and state.has("Ingot Crafting", player) and state.has("Resource Blocks", player))
|
||||
state._mc_has_diamond_pickaxe(player) and state.has("Progressive Resource Crafting", player, 2))
|
||||
set_rule(world.get_location("Not Today, Thank You", player), lambda state: state.has("Shield", player) and state._mc_has_iron_ingots(player))
|
||||
set_rule(world.get_location("Isn't It Iron Pick", player), lambda state: state.has("Progressive Tools", player, 2) and state._mc_has_iron_ingots(player))
|
||||
set_rule(world.get_location("Local Brewery", player), lambda state: state._mc_can_brew_potions(player))
|
||||
@@ -187,10 +190,10 @@ def set_rules(world: MultiWorld, player: int):
|
||||
state._mc_can_use_anvil(player) and state._mc_can_enchant(player))
|
||||
set_rule(world.get_location("The End... Again...", player), lambda state: can_complete(state))
|
||||
set_rule(world.get_location("Acquire Hardware", player), lambda state: state._mc_has_iron_ingots(player))
|
||||
set_rule(world.get_location("Not Quite \"Nine\" Lives", player), lambda state: state._mc_can_piglin_trade(player) and state.has("Resource Blocks", player))
|
||||
set_rule(world.get_location("Not Quite \"Nine\" Lives", player), lambda state: state._mc_can_piglin_trade(player) and state.has("Progressive Resource Crafting", player, 2))
|
||||
set_rule(world.get_location("Cover Me With Diamonds", player), lambda state: state.has("Progressive Armor", player, 2) and state.can_reach("Diamonds!", "Location", player))
|
||||
set_rule(world.get_location("Sky's the Limit", player), lambda state: state._mc_basic_combat(player))
|
||||
set_rule(world.get_location("Hired Help", player), lambda state: state.has("Resource Blocks", player) and state._mc_has_iron_ingots(player))
|
||||
set_rule(world.get_location("Hired Help", player), lambda state: state.has("Progressive Resource Crafting", player, 2) and state._mc_has_iron_ingots(player))
|
||||
set_rule(world.get_location("Return to Sender", player), lambda state: True)
|
||||
set_rule(world.get_location("Sweet Dreams", player), lambda state: state.has("Bed", player) or state.can_reach('Village', 'Region', player))
|
||||
set_rule(world.get_location("You Need a Mint", player), lambda state: can_complete(state) and state._mc_has_bottle(player))
|
||||
@@ -209,10 +212,10 @@ def set_rules(world: MultiWorld, player: int):
|
||||
set_rule(world.get_location("Hero of the Village", player), lambda state: state._mc_complete_raid(player))
|
||||
set_rule(world.get_location("Hidden in the Depths", player), lambda state: state._mc_can_brew_potions(player) and state.has("Bed", player) and state._mc_has_diamond_pickaxe(player)) # bed mining :)
|
||||
set_rule(world.get_location("Beaconator", player), lambda state: state._mc_can_kill_wither(player) and state._mc_has_diamond_pickaxe(player) and
|
||||
state.has("Ingot Crafting", player) and state.has("Resource Blocks", player))
|
||||
state.has("Progressive Resource Crafting", player, 2))
|
||||
set_rule(world.get_location("Withering Heights", player), lambda state: state._mc_can_kill_wither(player))
|
||||
set_rule(world.get_location("A Balanced Diet", player), lambda state: state._mc_has_bottle(player) and state._mc_has_gold_ingots(player) and # honey bottle; gapple
|
||||
state.has("Resource Blocks", player) and state.can_reach('The End', 'Region', player)) # notch apple, chorus fruit
|
||||
state.has("Progressive Resource Crafting", player, 2) and state.can_reach('The End', 'Region', player)) # notch apple, chorus fruit
|
||||
set_rule(world.get_location("Subspace Bubble", player), lambda state: state._mc_has_diamond_pickaxe(player))
|
||||
set_rule(world.get_location("Husbandry", player), lambda state: True)
|
||||
set_rule(world.get_location("Country Lode, Take Me Home", player), lambda state: state.can_reach("Hidden in the Depths", "Location", player) and state._mc_has_gold_ingots(player))
|
||||
@@ -226,14 +229,14 @@ def set_rules(world: MultiWorld, player: int):
|
||||
set_rule(world.get_location("Sticky Situation", player), lambda state: state.has("Campfire", player) and state._mc_has_bottle(player))
|
||||
set_rule(world.get_location("Ol' Betsy", player), lambda state: state._mc_craft_crossbow(player))
|
||||
set_rule(world.get_location("Cover Me in Debris", player), lambda state: state.has("Progressive Armor", player, 2) and
|
||||
state.has("8 Netherite Scrap", player, 2) and state.has("Ingot Crafting", player) and
|
||||
state.has("8 Netherite Scrap", player, 2) and state.has("Progressive Resource Crafting", player) and
|
||||
state.can_reach("Diamonds!", "Location", player) and state.can_reach("Hidden in the Depths", "Location", player))
|
||||
set_rule(world.get_location("The End?", player), lambda state: True)
|
||||
set_rule(world.get_location("The Parrots and the Bats", player), lambda state: True)
|
||||
set_rule(world.get_location("A Complete Catalogue", player), lambda state: True) # kill fish for raw
|
||||
set_rule(world.get_location("Getting Wood", player), lambda state: True)
|
||||
set_rule(world.get_location("Time to Mine!", player), lambda state: True)
|
||||
set_rule(world.get_location("Hot Topic", player), lambda state: state.has("Ingot Crafting", player))
|
||||
set_rule(world.get_location("Hot Topic", player), lambda state: state.has("Progressive Resource Crafting", player))
|
||||
set_rule(world.get_location("Bake Bread", player), lambda state: True)
|
||||
set_rule(world.get_location("The Lie", player), lambda state: state._mc_has_iron_ingots(player) and state.has("Bucket", player))
|
||||
set_rule(world.get_location("On a Rail", player), lambda state: state._mc_has_iron_ingots(player) and state.has('Progressive Tools', player, 2)) # powered rails
|
||||
@@ -244,4 +247,4 @@ def set_rules(world: MultiWorld, player: int):
|
||||
set_rule(world.get_location("Overkill", player), lambda state: state._mc_can_brew_potions(player) and
|
||||
(state.has("Progressive Weapons", player) or state.can_reach('The Nether', 'Region', player))) # strength 1 + stone axe crit OR strength 2 + wood axe crit
|
||||
set_rule(world.get_location("Librarian", player), lambda state: state.has("Enchanting", player))
|
||||
set_rule(world.get_location("Overpowered", player), lambda state: state.has("Resource Blocks", player) and state._mc_has_gold_ingots(player))
|
||||
set_rule(world.get_location("Overpowered", player), lambda state: state.has("Progressive Resource Crafting", player, 2) and state._mc_has_gold_ingots(player))
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import os
|
||||
import json
|
||||
from base64 import b64encode, b64decode
|
||||
from math import ceil
|
||||
|
||||
|
||||
from .Items import MinecraftItem, item_table, item_frequencies
|
||||
from .Items import MinecraftItem, item_table, required_items, junk_weights
|
||||
from .Locations import MinecraftAdvancement, advancement_table, exclusion_table, events_table
|
||||
from .Regions import mc_regions, link_minecraft_structures, default_connections
|
||||
from .Rules import set_rules
|
||||
@@ -11,30 +13,30 @@ from BaseClasses import Region, Entrance, Item
|
||||
from .Options import minecraft_options
|
||||
from ..AutoWorld import World
|
||||
|
||||
client_version = 5
|
||||
client_version = 6
|
||||
|
||||
class MinecraftWorld(World):
|
||||
game: str = "Minecraft"
|
||||
options = minecraft_options
|
||||
topology_present = True
|
||||
item_names = frozenset(item_table)
|
||||
location_names = frozenset(advancement_table)
|
||||
|
||||
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
||||
location_name_to_id = {name: data.id for name, data in advancement_table.items()}
|
||||
|
||||
data_version = 2
|
||||
data_version = 3
|
||||
|
||||
def _get_mc_data(self):
|
||||
exits = [connection[0] for connection in default_connections]
|
||||
return {
|
||||
'world_seed': self.world.slot_seeds[self.player].getrandbits(32),
|
||||
# consistent and doesn't interfere with other generation
|
||||
'seed_name': self.world.seed_name,
|
||||
'player_name': self.world.get_player_names(self.player),
|
||||
'player_name': self.world.get_player_name(self.player),
|
||||
'player_id': self.player,
|
||||
'client_version': client_version,
|
||||
'structures': {exit: self.world.get_entrance(exit, self.player).connected_region.name for exit in exits},
|
||||
'advancement_goal': self.world.advancement_goal[self.player],
|
||||
'egg_shards_required': self.world.egg_shards_required[self.player],
|
||||
'egg_shards_available': self.world.egg_shards_available[self.player],
|
||||
'race': self.world.is_race
|
||||
}
|
||||
|
||||
@@ -42,19 +44,24 @@ class MinecraftWorld(World):
|
||||
|
||||
# Generate item pool
|
||||
itempool = []
|
||||
pool_counts = item_frequencies.copy()
|
||||
# Replace Rotten Flesh with bee traps
|
||||
if self.world.bee_traps[self.player]:
|
||||
pool_counts.update({"Rotten Flesh": 0, "Bee Trap (Minecraft)": 4})
|
||||
# Add structure compasses to the pool, replacing 50 XP
|
||||
junk_pool = junk_weights.copy()
|
||||
# Add all required progression items
|
||||
for (name, num) in required_items.items():
|
||||
itempool += [name] * num
|
||||
# Add structure compasses if desired
|
||||
if self.world.structure_compasses[self.player]:
|
||||
structures = [connection[1] for connection in default_connections]
|
||||
for struct_name in structures:
|
||||
pool_counts[f"Structure Compass ({struct_name})"] = 1
|
||||
pool_counts["50 XP"] -= 1
|
||||
for item_name in item_table:
|
||||
for count in range(pool_counts.get(item_name, 1)):
|
||||
itempool.append(self.create_item(item_name))
|
||||
itempool.append(f"Structure Compass ({struct_name})")
|
||||
# Add dragon egg shards
|
||||
itempool += ["Dragon Egg Shard"] * self.world.egg_shards_available[self.player]
|
||||
# Add bee traps if desired
|
||||
bee_trap_quantity = ceil(self.world.bee_traps[self.player] * (len(self.location_names)-len(itempool)) * 0.01)
|
||||
itempool += ["Bee Trap (Minecraft)"] * bee_trap_quantity
|
||||
# Fill remaining items with randomly generated junk
|
||||
itempool += self.world.random.choices(list(junk_pool.keys()), weights=list(junk_pool.values()), k=len(self.location_names)-len(itempool))
|
||||
# Convert itempool into real items
|
||||
itempool = [item for item in map(lambda name: self.create_item(name), itempool)]
|
||||
|
||||
# Choose locations to automatically exclude based on settings
|
||||
exclusion_pool = set()
|
||||
@@ -67,7 +74,7 @@ class MinecraftWorld(World):
|
||||
# Prefill the Ender Dragon with the completion condition
|
||||
completion = self.create_item("Victory")
|
||||
self.world.get_location("Ender Dragon", self.player).place_locked_item(completion)
|
||||
itempool.remove(completion)
|
||||
|
||||
self.world.itempool += itempool
|
||||
|
||||
def set_rules(self):
|
||||
@@ -87,11 +94,8 @@ class MinecraftWorld(World):
|
||||
link_minecraft_structures(self.world, self.player)
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
import json
|
||||
from base64 import b64encode
|
||||
|
||||
data = self._get_mc_data()
|
||||
filename = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_player_names(self.player)}.apmc"
|
||||
filename = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_player_name(self.player)}.apmc"
|
||||
with open(os.path.join(output_directory, filename), 'wb') as f:
|
||||
f.write(b64encode(bytes(json.dumps(data), 'utf-8')))
|
||||
|
||||
@@ -109,3 +113,9 @@ class MinecraftWorld(World):
|
||||
if name in nonexcluded_items: # prevent books from going on excluded locations
|
||||
item.never_exclude = True
|
||||
return item
|
||||
|
||||
def mc_update_output(raw_data, server, port):
|
||||
data = json.loads(b64decode(raw_data))
|
||||
data['server'] = server
|
||||
data['port'] = port
|
||||
return b64encode(bytes(json.dumps(data), 'utf-8'))
|
||||
|
||||
@@ -14,9 +14,6 @@ class OriBlindForest(World):
|
||||
|
||||
topology_present = True
|
||||
|
||||
item_names = frozenset(item_table)
|
||||
location_names = frozenset(lookup_name_to_id)
|
||||
|
||||
item_name_to_id = item_table
|
||||
location_name_to_id = lookup_name_to_id
|
||||
|
||||
|
||||
@@ -203,9 +203,9 @@ def can_access_location(state, player, loc):
|
||||
pos_z = loc.get("position").get("z")
|
||||
depth = -pos_y # y-up
|
||||
map_center_dist = math.sqrt(pos_x**2 + pos_z**2)
|
||||
aurora_dist = math.sqrt((pos_x - 1040)**2 + (pos_z - -160)**2)
|
||||
aurora_dist = math.sqrt((pos_x - 1038.0)**2 + (pos_y - -3.4)**2 + (pos_z - -163.1)**2)
|
||||
|
||||
need_radiation_suit = aurora_dist < 940
|
||||
need_radiation_suit = aurora_dist < 950
|
||||
need_laser_cutter = loc.get("need_laser_cutter", False)
|
||||
need_propulsion_cannon = loc.get("need_propulsion_cannon", False)
|
||||
|
||||
@@ -231,7 +231,6 @@ def set_location_rule(world, player, loc):
|
||||
|
||||
|
||||
def set_rules(world, player):
|
||||
logging.warning(type(location_table))
|
||||
for loc in location_table:
|
||||
set_location_rule(world, player, loc)
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ from ..AutoWorld import World
|
||||
|
||||
class SubnauticaWorld(World):
|
||||
game: str = "Subnautica"
|
||||
item_names: Set[str] = frozenset(items_lookup_name_to_id)
|
||||
location_names: Set[str] = frozenset(locations_lookup_name_to_id)
|
||||
|
||||
item_name_to_id = items_lookup_name_to_id
|
||||
location_name_to_id = locations_lookup_name_to_id
|
||||
|
||||