Merged in DR v1.0.1.0
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import random
|
||||
from Utils import int16_as_bytes
|
||||
from Utils import int16_as_bytes, snes_to_pc
|
||||
|
||||
|
||||
class SFX(object):
|
||||
@@ -153,16 +153,13 @@ def shuffle_sfx_data():
|
||||
|
||||
|
||||
sfx_table = {
|
||||
2: 0x1a8c29,
|
||||
3: 0x1A8D25
|
||||
2: 0x1A8BD0,
|
||||
3: 0x1A8CCC
|
||||
}
|
||||
|
||||
# 0x1a8c29
|
||||
# d8059
|
||||
|
||||
sfx_accompaniment_table = {
|
||||
2: 0x1A8CA7,
|
||||
3: 0x1A8DA3
|
||||
2: 0x1A8C4E,
|
||||
3: 0x1A8D4A
|
||||
}
|
||||
|
||||
|
||||
@@ -171,9 +168,9 @@ def randomize_sfx(rom):
|
||||
|
||||
for shuffled_sfx in sfx_map.values():
|
||||
for sfx in shuffled_sfx.values():
|
||||
base_address = sfx_table[sfx.target_set]
|
||||
base_address = snes_to_pc(sfx_table[sfx.target_set])
|
||||
rom.write_bytes(base_address + (sfx.target_id * 2) - 2, int16_as_bytes(sfx.addr))
|
||||
ac_base = sfx_accompaniment_table[sfx.target_set]
|
||||
ac_base = snes_to_pc(sfx_accompaniment_table[sfx.target_set])
|
||||
last = sfx.target_id
|
||||
if sfx.target_chain:
|
||||
for chained in sfx.target_chain:
|
||||
|
||||
@@ -101,9 +101,12 @@ SETTINGSTOPROCESS = {
|
||||
"compassshuffle": "compassshuffle",
|
||||
"smallkeyshuffle": "keyshuffle",
|
||||
"bigkeyshuffle": "bigkeyshuffle",
|
||||
"keydropshuffle": "keydropshuffle",
|
||||
"dungeondoorshuffle": "door_shuffle",
|
||||
"dungeonintensity": "intensity",
|
||||
"keydropshuffle": "keydropshuffle",
|
||||
"dropshuffle": "dropshuffle",
|
||||
"pottery": "pottery",
|
||||
"colorizepots": "colorizepots",
|
||||
"potshuffle": "shufflepots",
|
||||
"experimental": "experimental",
|
||||
"dungeon_counters": "dungeon_counters",
|
||||
@@ -119,9 +122,12 @@ SETTINGSTOPROCESS = {
|
||||
"owpalettes": "ow_palettes",
|
||||
"uwpalettes": "uw_palettes",
|
||||
"reduce_flashing": "reduce_flashing",
|
||||
"shuffle_sfx": "shuffle_sfx"
|
||||
"shuffle_sfx": "shuffle_sfx",
|
||||
'msu_resume': 'msu_resume',
|
||||
'collection_rate': 'collection_rate',
|
||||
},
|
||||
"generation": {
|
||||
"bps": "bps",
|
||||
"createspoiler": "create_spoiler",
|
||||
"createrom": "create_rom",
|
||||
"calcplaythrough": "calc_playthrough",
|
||||
|
||||
58
source/dungeon/RoomList.py
Normal file
58
source/dungeon/RoomList.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from RoomData import DoorKind, Position
|
||||
from source.dungeon.RoomObject import RoomObject, DoorObject
|
||||
|
||||
|
||||
class Room:
|
||||
|
||||
def __init__(self, layout, layer1, layer2, doors):
|
||||
self.layout = layout
|
||||
self.layer1 = layer1
|
||||
self.layer2 = layer2
|
||||
self.doors = doors
|
||||
|
||||
def write_to_rom(self, address, rom):
|
||||
rom.write_bytes(address, self.layout)
|
||||
address += 2
|
||||
for obj in self.layer1:
|
||||
rom.write_bytes(address, obj.data)
|
||||
address += 3
|
||||
rom.write_bytes(address, [0xFF, 0xFF])
|
||||
address += 2
|
||||
for obj in self.layer2:
|
||||
rom.write_bytes(address, obj.data)
|
||||
address += 3
|
||||
rom.write_bytes(address, [0xFF, 0xFF, 0xF0, 0xFF])
|
||||
address += 4
|
||||
for door in self.doors:
|
||||
rom.write_bytes(address, door.get_bytes())
|
||||
address += 2
|
||||
rom.write_bytes(address, [0xFF, 0xFF])
|
||||
return address + 2 # where the data ended
|
||||
|
||||
|
||||
Room0127 = Room([0xE1, 0x00],
|
||||
[RoomObject(0x0AB600, [0xFE, 0x89, 0x00]),
|
||||
RoomObject(0x0AB603, [0xA2, 0xA1, 0x61]),
|
||||
RoomObject(0x0AB606, [0xFE, 0x8E, 0x81]),
|
||||
RoomObject(0x0AB609, [0xFF, 0x49, 0x02]),
|
||||
RoomObject(0x0AB60C, [0xD2, 0xA1, 0x62]),
|
||||
RoomObject(0x0AB60F, [0xFF, 0x4E, 0x83]),
|
||||
RoomObject(0x0AB612, [0x20, 0xB3, 0xDD]),
|
||||
RoomObject(0x0AB615, [0x50, 0xB3, 0xDD]),
|
||||
RoomObject(0x0AB618, [0x33, 0xCB, 0xFA]),
|
||||
RoomObject(0x0AB61B, [0x3B, 0xCB, 0xFA]),
|
||||
RoomObject(0x0AB61E, [0x43, 0xCB, 0xFA]),
|
||||
RoomObject(0x0AB621, [0x4B, 0xCB, 0xFA]),
|
||||
RoomObject(0x0AB624, [0xBF, 0x94, 0xF9]),
|
||||
RoomObject(0x0AB627, [0xB3, 0xB3, 0xFA]),
|
||||
RoomObject(0x0AB62A, [0xCB, 0xB3, 0xFA]),
|
||||
RoomObject(0x0AB62D, [0xAD, 0xC8, 0xDF]),
|
||||
RoomObject(0x0AB630, [0xC4, 0xC8, 0xDF]),
|
||||
RoomObject(0x0AB633, [0xB3, 0xE3, 0xFA]),
|
||||
RoomObject(0x0AB636, [0xCB, 0xE3, 0xFA]),
|
||||
RoomObject(0x0AB639, [0x81, 0x93, 0xC0]),
|
||||
RoomObject(0x0AB63C, [0x81, 0xD2, 0xC0]),
|
||||
RoomObject(0x0AB63F, [0xE1, 0x93, 0xC0]),
|
||||
RoomObject(0x0AB642, [0xE1, 0xD2, 0xC0])],
|
||||
[], [DoorObject(Position.SouthW, DoorKind.CaveEntrance),
|
||||
DoorObject(Position.SouthE, DoorKind.CaveEntrance)])
|
||||
34
source/dungeon/RoomObject.py
Normal file
34
source/dungeon/RoomObject.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from Utils import snes_to_pc
|
||||
|
||||
# Subtype 3 object (0x2xx by jpdasm id - see bank 01)
|
||||
# B
|
||||
Normal_Pot = (0xFA, 3, 3)
|
||||
Shuffled_Pot = (0xFB, 0, 0) # formerly weird pot, or black diagonal thing
|
||||
|
||||
|
||||
class RoomObject:
|
||||
|
||||
def __init__(self, address, data):
|
||||
self.address = address
|
||||
self.data = data
|
||||
|
||||
def change_type(self, new_type):
|
||||
type_id, datum_a, datum_b = new_type
|
||||
if 0xF8 <= type_id < 0xFC: # sub type 3
|
||||
self.data = (self.data[0] & 0xFC) | datum_a, (self.data[1] & 0xFC) | datum_b, type_id
|
||||
else:
|
||||
pass # not yet implemented
|
||||
|
||||
def write_to_rom(self, rom):
|
||||
rom.write_bytes(snes_to_pc(self.address), self.data)
|
||||
|
||||
|
||||
class DoorObject:
|
||||
|
||||
def __init__(self, pos, kind):
|
||||
self.pos = pos
|
||||
self.kind = kind
|
||||
|
||||
def get_bytes(self):
|
||||
return [self.pos.value, self.kind.value]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from tkinter import ttk, filedialog, messagebox, StringVar, Button, Entry, Frame, Label, E, W, LEFT, RIGHT, X, BOTTOM
|
||||
from AdjusterMain import adjust
|
||||
from AdjusterMain import adjust, patch
|
||||
from argparse import Namespace
|
||||
from source.classes.SpriteSelector import SpriteSelector
|
||||
import source.gui.widgets as widgets
|
||||
@@ -79,7 +79,9 @@ def adjust_page(top, parent, settings):
|
||||
romEntry2 = Entry(adjustRomFrame, textvariable=self.romVar2)
|
||||
|
||||
def RomSelect2():
|
||||
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")])
|
||||
initdir = os.path.join(os.getcwd(), settings['outputpath']) if 'outputpath' in settings else os.getcwd()
|
||||
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")],
|
||||
initialdir=initdir)
|
||||
if rom:
|
||||
settings["rom"] = rom
|
||||
self.romVar2.set(rom)
|
||||
@@ -103,7 +105,8 @@ def adjust_page(top, parent, settings):
|
||||
"quickswap": "quickswap",
|
||||
"nobgm": "disablemusic",
|
||||
"reduce_flashing": "reduce_flashing",
|
||||
"shuffle_sfx": "shuffle_sfx"
|
||||
'msu_resume': 'msu_resume',
|
||||
"shuffle_sfx": "shuffle_sfx",
|
||||
}
|
||||
guiargs = Namespace()
|
||||
for option in options:
|
||||
@@ -122,6 +125,58 @@ def adjust_page(top, parent, settings):
|
||||
messagebox.showinfo(title="Success", message="Rom patched successfully")
|
||||
|
||||
adjustButton = Button(self.frames["bottomAdjustFrame"], text='Adjust Rom', command=adjustRom)
|
||||
adjustButton.pack(side=BOTTOM, padx=(5, 0))
|
||||
adjustButton.pack(padx=(5, 0))
|
||||
|
||||
patchFileFrame = Frame(self.frames["bottomAdjustFrame"])
|
||||
patchFileLabel = Label(patchFileFrame, text='BPS Patch: ')
|
||||
self.patchVar = StringVar(value=settings["patch"])
|
||||
patchEntry = Entry(patchFileFrame, textvariable=self.patchVar)
|
||||
|
||||
def PatchSelect():
|
||||
initdir = os.path.join(os.getcwd(), settings['outputpath']) if 'outputpath' in settings else os.getcwd()
|
||||
file = filedialog.askopenfilename(filetypes=[("BPS Files", ".bps"), ("All Files", "*")], initialdir=initdir)
|
||||
if file:
|
||||
settings["patch"] = file
|
||||
self.patchVar.set(file)
|
||||
patchSelectButton = Button(patchFileFrame, text='Select Patch', command=PatchSelect)
|
||||
|
||||
patchFileLabel.pack(side=LEFT)
|
||||
patchEntry.pack(side=LEFT, fill=X, expand=True)
|
||||
patchSelectButton.pack(side=LEFT)
|
||||
patchFileFrame.pack(fill=X)
|
||||
|
||||
def patchRom():
|
||||
if output_path.cached_path is None:
|
||||
output_path.cached_path = top.settings["outputpath"]
|
||||
options = {
|
||||
"heartbeep": "heartbeep",
|
||||
"heartcolor": "heartcolor",
|
||||
"menuspeed": "fastmenu",
|
||||
"owpalettes": "ow_palettes",
|
||||
"uwpalettes": "uw_palettes",
|
||||
"quickswap": "quickswap",
|
||||
"nobgm": "disablemusic",
|
||||
"reduce_flashing": "reduce_flashing",
|
||||
'msu_resume': 'msu_resume',
|
||||
"shuffle_sfx": "shuffle_sfx",
|
||||
}
|
||||
guiargs = Namespace()
|
||||
for option in options:
|
||||
arg = options[option]
|
||||
setattr(guiargs, arg, self.widgets[option].storageVar.get())
|
||||
guiargs.patch = self.patchVar.get()
|
||||
guiargs.baserom = top.pages["randomizer"].pages["generation"].widgets["rom"].storageVar.get()
|
||||
guiargs.sprite = self.sprite
|
||||
guiargs.outputpath = os.path.dirname(guiargs.patch)
|
||||
try:
|
||||
patch(args=guiargs)
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
messagebox.showerror(title="Error while creating seed", message=str(e))
|
||||
else:
|
||||
messagebox.showinfo(title="Success", message="Rom patched successfully")
|
||||
|
||||
patchButton = Button(self.frames["bottomAdjustFrame"], text='Patch Rom', command=patchRom)
|
||||
patchButton.pack(side=BOTTOM, padx=(5, 0))
|
||||
|
||||
return self,settings
|
||||
|
||||
@@ -275,4 +275,9 @@ def create_guiargs(parent):
|
||||
|
||||
guiargs = update_deprecated_args(guiargs)
|
||||
|
||||
# Key drop shuffle stuff
|
||||
if guiargs.keydropshuffle:
|
||||
guiargs.dropshuffle = 1
|
||||
guiargs.pottery = 'keys' if guiargs.pottery == 'none' else guiargs.pottery
|
||||
|
||||
return guiargs
|
||||
|
||||
@@ -52,7 +52,7 @@ def loadcliargs(gui, args, settings=None):
|
||||
gui.pages[mainpage].pages[subpage].widgets[widget].label.configure(text=label)
|
||||
gui.pages[mainpage].pages[subpage].widgets[widget].storageVar.set(args[arg])
|
||||
# If we're on the Game Options page and it's not about Hints
|
||||
if subpage == "gameoptions" and not widget == "hints":
|
||||
if subpage == "gameoptions" and widget not in ["hints", "collection_rate"]:
|
||||
# Check if we've got settings
|
||||
# Check if we've got the widget in Adjust settings
|
||||
hasSettings = settings is not None
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import RaceRandom as random
|
||||
import logging
|
||||
from math import ceil
|
||||
from collections import defaultdict
|
||||
|
||||
from source.item.District import resolve_districts
|
||||
from BaseClasses import PotItem, PotFlags
|
||||
from DoorShuffle import validate_vanilla_reservation
|
||||
from Dungeons import dungeon_table
|
||||
from Items import item_table, ItemFactory
|
||||
from PotShuffle import vanilla_pots
|
||||
|
||||
|
||||
class ItemPoolConfig(object):
|
||||
@@ -28,7 +29,6 @@ class LocationGroup(object):
|
||||
|
||||
# flags
|
||||
self.keyshuffle = False
|
||||
self.keydropshuffle = False
|
||||
self.shopsanity = False
|
||||
self.retro = False
|
||||
|
||||
@@ -36,9 +36,8 @@ class LocationGroup(object):
|
||||
self.locations = list(locs)
|
||||
return self
|
||||
|
||||
def flags(self, k, d=False, s=False, r=False):
|
||||
def flags(self, k, s=False, r=False):
|
||||
self.keyshuffle = k
|
||||
self.keydropshuffle = d
|
||||
self.shopsanity = s
|
||||
self.retro = r
|
||||
return self
|
||||
@@ -64,34 +63,40 @@ def create_item_pool_config(world):
|
||||
config.static_placement = {}
|
||||
config.location_groups = {}
|
||||
for player in range(1, world.players + 1):
|
||||
config.static_placement[player] = vanilla_mapping.copy()
|
||||
if world.keydropshuffle[player]:
|
||||
config.static_placement[player] = defaultdict(list)
|
||||
config.static_placement[player].update(vanilla_mapping)
|
||||
if world.dropshuffle[player]:
|
||||
for item, locs in keydrop_vanilla_mapping.items():
|
||||
if item in config.static_placement[player]:
|
||||
config.static_placement[player][item].extend(locs)
|
||||
else:
|
||||
config.static_placement[player][item] = list(locs)
|
||||
config.static_placement[player][item].extend(locs)
|
||||
if world.pottery[player] not in ['none', 'cave']:
|
||||
for item, locs in potkeys_vanilla_mapping.items():
|
||||
config.static_placement[player][item].extend(locs)
|
||||
if world.pottery[player] in ['lottery', 'cave', 'dungeon']:
|
||||
for super_tile, pot_list in vanilla_pots.items():
|
||||
for pot_index, pot in enumerate(pot_list):
|
||||
if pot.item not in [PotItem.Key, PotItem.Hole, PotItem.Switch]:
|
||||
item = pot_items[pot.item]
|
||||
descriptor = 'Large Block' if pot.flags & PotFlags.Block else f'Pot #{pot_index+1}'
|
||||
location = f'{pot.room} {descriptor}'
|
||||
config.static_placement[player][item].append(location)
|
||||
if world.shopsanity[player]:
|
||||
for item, locs in shop_vanilla_mapping.items():
|
||||
if item in config.static_placement[player]:
|
||||
config.static_placement[player][item].extend(locs)
|
||||
else:
|
||||
config.static_placement[player][item] = list(locs)
|
||||
config.static_placement[player][item].extend(locs)
|
||||
if world.retro[player]:
|
||||
for item, locs in retro_vanilla_mapping.items():
|
||||
if item in config.static_placement[player]:
|
||||
config.static_placement[player][item].extend(locs)
|
||||
else:
|
||||
config.static_placement[player][item] = list(locs)
|
||||
config.static_placement[player][item].extend(locs)
|
||||
# universal keys
|
||||
universal_key_locations = []
|
||||
for item, locs in vanilla_mapping.items():
|
||||
if 'Small Key' in item:
|
||||
universal_key_locations.extend(locs)
|
||||
if world.keydropshuffle[player]:
|
||||
if world.dropshuffle[player]:
|
||||
for item, locs in keydrop_vanilla_mapping.items():
|
||||
if 'Small Key' in item:
|
||||
universal_key_locations.extend(locs)
|
||||
if world.pottery[player] not in ['none', 'cave']:
|
||||
for item, locs in potkeys_vanilla_mapping.items():
|
||||
universal_key_locations.extend(locs)
|
||||
if world.shopsanity[player]:
|
||||
single_arrow_placement = list(shop_vanilla_mapping['Red Potion'])
|
||||
single_arrow_placement.append('Red Shield Shop - Right')
|
||||
@@ -117,12 +122,14 @@ def create_item_pool_config(world):
|
||||
groups = LocationGroup('Major').locs(init_set)
|
||||
if world.bigkeyshuffle[player]:
|
||||
groups.locations.extend(mode_grouping['Big Keys'])
|
||||
if world.keydropshuffle[player]:
|
||||
if world.dropshuffle[player] != 'none':
|
||||
groups.locations.extend(mode_grouping['Big Key Drops'])
|
||||
if world.keyshuffle[player]:
|
||||
groups.locations.extend(mode_grouping['Small Keys'])
|
||||
if world.keydropshuffle[player]:
|
||||
if world.dropshuffle[player] != 'none':
|
||||
groups.locations.extend(mode_grouping['Key Drops'])
|
||||
if world.pottery[player] not in ['none', 'cave']:
|
||||
groups.locations.extend(mode_grouping['Pot Keys'])
|
||||
if world.compassshuffle[player]:
|
||||
groups.locations.extend(mode_grouping['Compasses'])
|
||||
if world.mapshuffle[player]:
|
||||
@@ -268,7 +275,7 @@ def massage_item_pool(world):
|
||||
world.itempool.remove(deleted)
|
||||
discrepancy -= 1
|
||||
if discrepancy > 0:
|
||||
logging.getLogger('').warning(f'Too many good items in pool, something will be removed at random')
|
||||
raise Exception(f'Too many required items in pool, {discrepancy} items cannot be placed')
|
||||
if world.item_pool_config.placeholders is not None:
|
||||
removed = 0
|
||||
single_rupees = [item for item in world.itempool if item.name == 'Rupee (1)']
|
||||
@@ -322,49 +329,6 @@ def count_major_items(config, world, player):
|
||||
return sum(1 for x in world.itempool if x.name in config.item_pool[player] and x.player == player)
|
||||
|
||||
|
||||
def calc_dungeon_limits(world, player):
|
||||
b, s, c, m, k, r, bi = (world.bigkeyshuffle[player], world.keyshuffle[player], world.compassshuffle[player],
|
||||
world.mapshuffle[player], world.keydropshuffle[player], world.retro[player],
|
||||
world.restrict_boss_items[player])
|
||||
if world.doorShuffle[player] in ['vanilla', 'basic']:
|
||||
limits = {}
|
||||
for dungeon, info in dungeon_table.items():
|
||||
val = info.free_items
|
||||
if bi != 'none' and info.prize:
|
||||
if bi == 'mapcompass' and (not c or not m):
|
||||
val -= 1
|
||||
if bi == 'dungeon' and (not c or not m or not (s or r) or not b):
|
||||
val -= 1
|
||||
if b:
|
||||
val += 1 if info.bk_present else 0
|
||||
if k:
|
||||
val += 1 if info.bk_drops else 0
|
||||
if s or r:
|
||||
val += info.key_num
|
||||
if k:
|
||||
val += info.key_drops
|
||||
if c:
|
||||
val += 1 if info.compass_present else 0
|
||||
if m:
|
||||
val += 1 if info.map_present else 0
|
||||
limits[dungeon] = val
|
||||
else:
|
||||
limits = 60
|
||||
if world.bigkeyshuffle[player]:
|
||||
limits += 11
|
||||
if world.keydropshuffle[player]:
|
||||
limits += 1
|
||||
if world.keyshuffle[player] or world.retro[player]:
|
||||
limits += 29
|
||||
if world.keydropshuffle[player]:
|
||||
limits += 32
|
||||
if world.compassshuffle[player]:
|
||||
limits += 11
|
||||
if world.mapshuffle[player]:
|
||||
limits += 12
|
||||
return limits
|
||||
|
||||
|
||||
def determine_major_items(world, player):
|
||||
major_item_set = set(major_items)
|
||||
if world.progressive == 'off':
|
||||
@@ -454,6 +418,19 @@ def filter_locations(item_to_place, locations, world, vanilla_skip=False, potion
|
||||
return locations
|
||||
|
||||
|
||||
def filter_pot_locations(locations, world):
|
||||
if world.algorithm == 'district':
|
||||
config = world.item_pool_config
|
||||
restricted = config.location_groups[0].locations
|
||||
filtered = [l for l in locations if l.name not in restricted or l.player not in restricted[l.name]]
|
||||
return filtered if len(filtered) > 0 else locations
|
||||
if world.algorithm == 'vanilla_fill':
|
||||
filtered = [l for l in locations if l.pot and l.pot.item in [PotItem.Chicken, PotItem.BigMagic]]
|
||||
return filtered if len(filtered) > 0 else locations
|
||||
return locations
|
||||
|
||||
|
||||
|
||||
vanilla_mapping = {
|
||||
'Green Pendant': ['Eastern Palace - Prize'],
|
||||
'Red Pendant': ['Desert Palace - Prize', 'Tower of Hera - Prize'],
|
||||
@@ -615,25 +592,31 @@ vanilla_mapping = {
|
||||
|
||||
|
||||
keydrop_vanilla_mapping = {
|
||||
'Small Key (Desert Palace)': ['Desert Palace - Desert Tiles 1 Pot Key',
|
||||
'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key'],
|
||||
'Small Key (Eastern Palace)': ['Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop'],
|
||||
'Small Key (Eastern Palace)': ['Eastern Palace - Dark Eyegore Key Drop'],
|
||||
'Small Key (Escape)': ['Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop',
|
||||
'Hyrule Castle - Key Rat Key Drop'],
|
||||
'Big Key (Escape)': ['Hyrule Castle - Big Key Drop'],
|
||||
'Small Key (Agahnims Tower)': ['Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'],
|
||||
'Small Key (Skull Woods)': ['Skull Woods - Spike Corner Key Drop'],
|
||||
'Small Key (Ice Palace)': ['Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop'],
|
||||
'Small Key (Misery Mire)': ['Misery Mire - Conveyor Crystal Key Drop'],
|
||||
'Small Key (Turtle Rock)': ['Turtle Rock - Pokey 1 Key Drop', 'Turtle Rock - Pokey 2 Key Drop'],
|
||||
'Small Key (Ganons Tower)': ['Ganons Tower - Mini Helmasaur Key Drop'],
|
||||
}
|
||||
|
||||
potkeys_vanilla_mapping = {
|
||||
'Small Key (Desert Palace)': ['Desert Palace - Desert Tiles 1 Pot Key',
|
||||
'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key'],
|
||||
'Small Key (Eastern Palace)': ['Eastern Palace - Dark Square Pot Key'],
|
||||
'Small Key (Thieves Town)': ["Thieves' Town - Hallway Pot Key", "Thieves' Town - Spike Switch Pot Key"],
|
||||
'Small Key (Skull Woods)': ['Skull Woods - West Lobby Pot Key', 'Skull Woods - Spike Corner Key Drop'],
|
||||
'Small Key (Skull Woods)': ['Skull Woods - West Lobby Pot Key'],
|
||||
'Small Key (Swamp Palace)': ['Swamp Palace - Pot Row Pot Key', 'Swamp Palace - Trench 1 Pot Key',
|
||||
'Swamp Palace - Hookshot Pot Key', 'Swamp Palace - Trench 2 Pot Key',
|
||||
'Swamp Palace - Waterway Pot Key'],
|
||||
'Small Key (Ice Palace)': ['Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop',
|
||||
'Ice Palace - Hammer Block Key Drop', 'Ice Palace - Many Pots Pot Key'],
|
||||
'Small Key (Misery Mire)': ['Misery Mire - Spikes Pot Key',
|
||||
'Misery Mire - Fishbone Pot Key', 'Misery Mire - Conveyor Crystal Key Drop'],
|
||||
'Small Key (Turtle Rock)': ['Turtle Rock - Pokey 1 Key Drop', 'Turtle Rock - Pokey 2 Key Drop'],
|
||||
'Small Key (Ice Palace)': ['Ice Palace - Hammer Block Key Drop', 'Ice Palace - Many Pots Pot Key'],
|
||||
'Small Key (Misery Mire)': ['Misery Mire - Spikes Pot Key', 'Misery Mire - Fishbone Pot Key'],
|
||||
'Small Key (Ganons Tower)': ['Ganons Tower - Conveyor Cross Pot Key', 'Ganons Tower - Double Switch Pot Key',
|
||||
'Ganons Tower - Conveyor Star Pits Pot Key', 'Ganons Tower - Mini Helmasuar Key Drop'],
|
||||
'Ganons Tower - Conveyor Star Pits Pot Key'],
|
||||
}
|
||||
|
||||
shop_vanilla_mapping = {
|
||||
@@ -682,7 +665,7 @@ mode_grouping = {
|
||||
'Maze Race', 'Spectacle Rock', 'Pyramid', "Zora's Ledge", 'Lumberjack Tree',
|
||||
'Sunken Treasure', 'Spectacle Rock Cave', 'Lost Woods Hideout', 'Checkerboard Cave', 'Peg Cave', 'Cave 45',
|
||||
'Graveyard Cave', 'Kakariko Well - Top', "Blind's Hideout - Top", 'Bonk Rock Cave', "Aginah's Cave",
|
||||
'Chest Game', 'Digging Game', 'Mire Shed - Right', 'Mimic Cave'
|
||||
'Chest Game', 'Digging Game', 'Mire Shed - Left', 'Mimic Cave'
|
||||
],
|
||||
'Big Keys': [
|
||||
'Eastern Palace - Big Key Chest', 'Ganons Tower - Big Key Chest',
|
||||
@@ -735,7 +718,7 @@ mode_grouping = {
|
||||
'Paradox Cave Upper - Left', 'Paradox Cave Upper - Right', 'Spiral Cave', 'Brewery', 'C-Shaped House',
|
||||
'Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left', 'Hype Cave - Bottom',
|
||||
'Hype Cave - Generous Guy', 'Superbunny Cave - Bottom', 'Superbunny Cave - Top', 'Hookshot Cave - Top Right',
|
||||
'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left', 'Mire Shed - Left'
|
||||
'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left', 'Mire Shed - Right'
|
||||
],
|
||||
'GT Trash': [
|
||||
'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - DMs Room - Top Left',
|
||||
@@ -751,19 +734,22 @@ mode_grouping = {
|
||||
],
|
||||
'Key Drops': [
|
||||
'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop',
|
||||
'Hyrule Castle - Key Rat Key Drop', 'Eastern Palace - Dark Square Pot Key',
|
||||
'Eastern Palace - Dark Eyegore Key Drop', 'Desert Palace - Desert Tiles 1 Pot Key',
|
||||
'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key',
|
||||
'Hyrule Castle - Key Rat Key Drop', 'Eastern Palace - Dark Eyegore Key Drop',
|
||||
'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop',
|
||||
'Skull Woods - Spike Corner Key Drop', 'Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop',
|
||||
'Misery Mire - Conveyor Crystal Key Drop', 'Turtle Rock - Pokey 1 Key Drop',
|
||||
'Turtle Rock - Pokey 2 Key Drop', 'Ganons Tower - Mini Helmasuar Key Drop',
|
||||
],
|
||||
'Pot Keys': [
|
||||
'Eastern Palace - Dark Square Pot Key', 'Desert Palace - Desert Tiles 1 Pot Key',
|
||||
'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key',
|
||||
'Swamp Palace - Pot Row Pot Key', 'Swamp Palace - Trench 1 Pot Key', 'Swamp Palace - Hookshot Pot Key',
|
||||
'Swamp Palace - Trench 2 Pot Key', 'Swamp Palace - Waterway Pot Key', 'Skull Woods - West Lobby Pot Key',
|
||||
'Skull Woods - Spike Corner Key Drop', "Thieves' Town - Hallway Pot Key",
|
||||
"Thieves' Town - Spike Switch Pot Key", 'Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop',
|
||||
"Thieves' Town - Hallway Pot Key", "Thieves' Town - Spike Switch Pot Key",
|
||||
'Ice Palace - Hammer Block Key Drop', 'Ice Palace - Many Pots Pot Key', 'Misery Mire - Spikes Pot Key',
|
||||
'Misery Mire - Fishbone Pot Key', 'Misery Mire - Conveyor Crystal Key Drop', 'Turtle Rock - Pokey 1 Key Drop',
|
||||
'Turtle Rock - Pokey 2 Key Drop', 'Ganons Tower - Conveyor Cross Pot Key',
|
||||
'Misery Mire - Fishbone Pot Key', 'Ganons Tower - Conveyor Cross Pot Key',
|
||||
'Ganons Tower - Double Switch Pot Key', 'Ganons Tower - Conveyor Star Pits Pot Key',
|
||||
'Ganons Tower - Mini Helmasuar Key Drop',
|
||||
|
||||
],
|
||||
'Big Key Drops': ['Hyrule Castle - Big Key Drop'],
|
||||
'Shops': [
|
||||
@@ -804,25 +790,24 @@ vanilla_swords = {"Link's Uncle", 'Master Sword Pedestal', 'Blacksmith', 'Pyrami
|
||||
trash_items = {
|
||||
'Nothing': -1,
|
||||
'Bee Trap': 0,
|
||||
'Rupee (1)': 1,
|
||||
'Rupees (5)': 1,
|
||||
'Rupees (20)': 1,
|
||||
|
||||
'Small Heart': 2,
|
||||
'Bee': 2,
|
||||
'Arrows (5)': 2,
|
||||
'Chicken': 2,
|
||||
'Single Bomb': 2,
|
||||
|
||||
'Bombs (3)': 3,
|
||||
'Arrows (10)': 3,
|
||||
'Bombs (10)': 3,
|
||||
|
||||
'Red Potion': 4,
|
||||
'Blue Shield': 4,
|
||||
'Rupees (50)': 4,
|
||||
'Rupees (100)': 4,
|
||||
'Rupee (1)': 1, 'Rupees (5)': 1, 'Small Heart': 1, 'Bee': 1, 'Arrows (5)': 1, 'Chicken': 1, 'Single Bomb': 1,
|
||||
'Rupees (20)': 2, 'Small Magic': 2,
|
||||
'Bombs (3)': 3, 'Arrows (10)': 3, 'Bombs (10)': 3,
|
||||
'Big Magic': 4, 'Red Potion': 4, 'Blue Shield': 4, 'Rupees (50)': 4, 'Rupees (100)': 4,
|
||||
'Rupees (300)': 5,
|
||||
|
||||
'Piece of Heart': 17
|
||||
}
|
||||
|
||||
pot_items = {
|
||||
PotItem.Nothing: 'Nothing',
|
||||
PotItem.Bomb: 'Single Bomb',
|
||||
PotItem.FiveArrows: 'Arrows (5)', # convert to 10
|
||||
PotItem.OneRupee: 'Rupee (1)',
|
||||
PotItem.FiveRupees: 'Rupees (5)',
|
||||
PotItem.Heart: 'Small Heart',
|
||||
PotItem.BigMagic: 'Big Magic', # fast fill
|
||||
PotItem.SmallMagic: 'Small Magic',
|
||||
PotItem.Chicken: 'Chicken' # fast fill
|
||||
}
|
||||
|
||||
valid_pot_items = {y: x for x, y in pot_items.items()}
|
||||
|
||||
41
source/meta/check_requirements.py
Normal file
41
source/meta/check_requirements.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import importlib.util
|
||||
import webbrowser
|
||||
from tkinter import Tk, Label, Button, Frame
|
||||
|
||||
|
||||
def check_requirements(console=False):
|
||||
check_packages = {'aenum': 'aenum',
|
||||
'fast-enum': 'fast_enum',
|
||||
'python-bps-continued': 'bps',
|
||||
'colorama': 'colorama',
|
||||
'aioconsole' : 'aioconsole',
|
||||
'websockets' : 'websockets',
|
||||
'pyyaml': 'yaml'}
|
||||
missing = []
|
||||
for package, import_name in check_packages.items():
|
||||
spec = importlib.util.find_spec(import_name)
|
||||
if spec is None:
|
||||
missing.append(package)
|
||||
if len(missing) > 0:
|
||||
packages = ','.join(missing)
|
||||
if console:
|
||||
import logging
|
||||
logger = logging.getLogger('')
|
||||
logger.error('You need to install the following python packages:')
|
||||
logger.error(f'{packages}')
|
||||
logger.error('See the step about "Installing Platform-specific dependencies":')
|
||||
logger.error('https://github.com/aerinon/ALttPDoorRandomizer/blob/DoorDev/docs/BUILDING.md')
|
||||
else:
|
||||
master = Tk()
|
||||
master.title('Error')
|
||||
frame = Frame(master)
|
||||
frame.pack(expand=True, padx =50, pady=50)
|
||||
Label(frame, text='You need to install the following python packages:').pack()
|
||||
Label(frame, text=f'{packages}').pack()
|
||||
Label(frame, text='See the step about "Installing Platform-specific dependencies":').pack()
|
||||
url = 'https://github.com/aerinon/ALttPDoorRandomizer/blob/DoorDev/docs/BUILDING.md'
|
||||
link = Label(frame, fg='blue', cursor='hand2', text=url)
|
||||
link.pack()
|
||||
link.bind('<Button-1>', lambda e: webbrowser.open_new_tab(url))
|
||||
Button(master, text='Ok', command=master.destroy).pack()
|
||||
master.mainloop()
|
||||
@@ -25,7 +25,7 @@ def main(args=None):
|
||||
|
||||
def test(testname: str, command: str):
|
||||
tests[testname] = [command]
|
||||
basecommand = f"python3.8 Mystery.py --suppress_rom"
|
||||
basecommand = f"python3.8 Mystery.py --suppress_rom --suppress_meta"
|
||||
|
||||
def gen_seed():
|
||||
taskcommand = basecommand + " " + command
|
||||
|
||||
324
source/tools/BPS.py
Normal file
324
source/tools/BPS.py
Normal file
@@ -0,0 +1,324 @@
|
||||
# Code derived from https://github.com/marcrobledo/RomPatcher.js (MIT License)
|
||||
|
||||
import sys
|
||||
|
||||
from time import perf_counter
|
||||
|
||||
from collections import defaultdict
|
||||
from binascii import crc32
|
||||
try:
|
||||
from fast_enum import FastEnum
|
||||
except ImportError:
|
||||
from enum import IntFlag as FastEnum
|
||||
|
||||
|
||||
def bps_get_vlv_len(data):
|
||||
length = 0
|
||||
while True:
|
||||
x = data & 0x7f
|
||||
data >>= 7
|
||||
if data == 0:
|
||||
length += 1
|
||||
break
|
||||
length += 1
|
||||
data -= 1
|
||||
return length
|
||||
|
||||
|
||||
def bps_read_vlv(stream):
|
||||
data, shift = 0, 1
|
||||
while True:
|
||||
x = stream.read(1)[0]
|
||||
data += (x & 0x7f) * shift
|
||||
if x & 0x80:
|
||||
return data
|
||||
shift <<= 7
|
||||
data += shift
|
||||
|
||||
|
||||
class Bps:
|
||||
def __init__(self):
|
||||
self.source_size = 0
|
||||
self.target_size = 0
|
||||
self.metadata = ''
|
||||
self.actions = []
|
||||
self.source_checksum = 0
|
||||
self.target_checksum = 0
|
||||
self.patch_checksum = 0
|
||||
|
||||
self.binary_ba = bytearray()
|
||||
self.offset = 0
|
||||
|
||||
def write_to_binary(self):
|
||||
patch_size = 4
|
||||
patch_size += bps_get_vlv_len(self.source_size)
|
||||
patch_size += bps_get_vlv_len(self.target_size)
|
||||
patch_size += bps_get_vlv_len(len(self.metadata))
|
||||
patch_size += len(self.metadata)
|
||||
|
||||
for action in self.actions:
|
||||
mode, length, data = action
|
||||
patch_size += bps_get_vlv_len(((length - 1) << 2) + mode)
|
||||
|
||||
if mode == BpsMode.BPS_ACTION_TARGET_READ:
|
||||
patch_size += length
|
||||
elif mode == BpsMode.BPS_ACTION_SOURCE_COPY or mode == BpsMode.BPS_ACTION_TARGET_COPY:
|
||||
patch_size += bps_get_vlv_len((abs(data) << 1) + (1 if data < 0 else 0))
|
||||
patch_size += 12
|
||||
|
||||
self.binary_ba = bytearray(patch_size)
|
||||
self.write_string('BPS1')
|
||||
self.bps_write_vlv(self.source_size)
|
||||
self.bps_write_vlv(self.target_size)
|
||||
self.bps_write_vlv(len(self.metadata))
|
||||
self.write_string(self.metadata)
|
||||
|
||||
for action in self.actions:
|
||||
mode, length, data = action
|
||||
self.bps_write_vlv(((length - 1) << 2) + mode)
|
||||
if mode == BpsMode.BPS_ACTION_TARGET_READ:
|
||||
self.write_bytes(data)
|
||||
elif mode == BpsMode.BPS_ACTION_SOURCE_COPY or mode == BpsMode.BPS_ACTION_TARGET_COPY:
|
||||
self.bps_write_vlv((abs(data) << 1) + (1 if data < 0 else 0))
|
||||
self.write_u32(self.source_checksum)
|
||||
self.write_u32(self.target_checksum)
|
||||
self.write_u32(self.patch_checksum)
|
||||
|
||||
def write_string(self, string):
|
||||
for ch in string:
|
||||
self.binary_ba[self.offset] = ord(ch)
|
||||
self.offset += 1
|
||||
|
||||
def write_byte(self, byte):
|
||||
self.binary_ba[self.offset] = byte
|
||||
self.offset += 1
|
||||
|
||||
def write_bytes(self, m_bytes):
|
||||
for byte in m_bytes:
|
||||
self.binary_ba[self.offset] = byte
|
||||
self.offset += 1
|
||||
|
||||
def write_u32(self, data):
|
||||
self.binary_ba[self.offset] = data & 0x000000ff
|
||||
self.binary_ba[self.offset+1] = (data & 0x0000ff00) >> 8
|
||||
self.binary_ba[self.offset+2] = (data & 0x00ff0000) >> 16
|
||||
self.binary_ba[self.offset+3] = (data & 0xff000000) >> 24
|
||||
self.offset += 4
|
||||
|
||||
def bps_write_vlv(self, data):
|
||||
while True:
|
||||
x = data & 0x7f
|
||||
data >>= 7
|
||||
if data == 0:
|
||||
self.write_byte(0x80 | x)
|
||||
break
|
||||
self.write_byte(x)
|
||||
data -= 1
|
||||
|
||||
|
||||
class BpsMode(FastEnum):
|
||||
BPS_ACTION_SOURCE_READ = 0
|
||||
BPS_ACTION_TARGET_READ = 1
|
||||
BPS_ACTION_SOURCE_COPY = 2
|
||||
BPS_ACTION_TARGET_COPY = 3
|
||||
|
||||
|
||||
def create_bps_from_data(original, modified):
|
||||
patch = Bps()
|
||||
patch.source_size = len(original)
|
||||
patch.target_size = len(modified)
|
||||
|
||||
patch.actions = create_bps_linear(original, modified)
|
||||
|
||||
patch.source_checksum = crc32(original)
|
||||
patch.target_checksum = crc32(modified)
|
||||
patch.write_to_binary()
|
||||
patch.patch_checksum = crc32(patch.binary_ba[:-4])
|
||||
patch.offset = len(patch.binary_ba) - 4
|
||||
patch.write_u32(patch.patch_checksum)
|
||||
return patch
|
||||
|
||||
|
||||
def create_bps_delta(original, modified):
|
||||
patch_actions = []
|
||||
source_data = original
|
||||
target_data = modified
|
||||
source_size = len(original)
|
||||
target_size = len(modified)
|
||||
|
||||
source_relative_offset = 0
|
||||
target_relative_offset = 0
|
||||
output_offset = 0
|
||||
|
||||
source_tree = defaultdict(list)
|
||||
source_tree_2 = defaultdict(list)
|
||||
target_tree = defaultdict(list)
|
||||
|
||||
t1_start = perf_counter()
|
||||
for offset in range(0, source_size):
|
||||
symbol = source_data[offset]
|
||||
if offset < source_size - 1:
|
||||
symbol |= source_data[offset + 1] << 8
|
||||
source_tree[symbol].append(offset)
|
||||
print(f'Elasped Time 1: {perf_counter()-t1_start}')
|
||||
|
||||
source_array = list(source_data)
|
||||
|
||||
t2_start = perf_counter()
|
||||
for offset in range(0, source_size):
|
||||
symbol = source_array[offset]
|
||||
if offset < source_size - 1:
|
||||
symbol |= source_array[offset + 1] << 8
|
||||
source_tree_2[symbol].append(offset)
|
||||
print(f'Elasped Time 2: {perf_counter()-t2_start}')
|
||||
|
||||
trl = {'target_read_length': 0}
|
||||
|
||||
def target_read_flush(buffer):
|
||||
if buffer['target_read_length']:
|
||||
action = (BpsMode.BPS_ACTION_TARGET_READ, buffer['target_read_length'], [])
|
||||
patch_actions.append(action)
|
||||
offset = output_offset - buffer['target_read_length']
|
||||
while buffer['target_read_length']:
|
||||
action[2].append(target_data[offset])
|
||||
offset += 1
|
||||
buffer['target_read_length'] -= 1
|
||||
|
||||
while output_offset < target_size:
|
||||
max_length, max_offset, mode = 0, 0, BpsMode.BPS_ACTION_TARGET_READ
|
||||
|
||||
symbol = target_data[output_offset]
|
||||
|
||||
if output_offset < target_size - 1:
|
||||
symbol |= target_data[output_offset + 1] << 8
|
||||
|
||||
# source read
|
||||
length, offset = 0, output_offset
|
||||
while offset < source_size and offset < target_size and source_data[offset] == target_data[offset]:
|
||||
length += 1
|
||||
offset += 1
|
||||
if length > max_length:
|
||||
max_length, mode = length, BpsMode.BPS_ACTION_SOURCE_READ
|
||||
|
||||
# source copy
|
||||
for node in source_tree[symbol]:
|
||||
length, x, y = 0, node, output_offset
|
||||
while x < source_size and y < target_size and source_data[x] == target_data[y]:
|
||||
length += 1
|
||||
x += 1
|
||||
y += 1
|
||||
if length > max_length:
|
||||
max_length, max_offset, mode = length, node, BpsMode.BPS_ACTION_SOURCE_COPY
|
||||
|
||||
# target copy
|
||||
for node in target_tree[symbol]:
|
||||
length, x, y = 0, node, output_offset
|
||||
while y < target_size and target_data[x] == target_data[y]:
|
||||
length += 1
|
||||
x += 1
|
||||
y += 1
|
||||
if length > max_length:
|
||||
max_length, max_offset, mode = length, node, BpsMode.BPS_ACTION_TARGET_COPY
|
||||
target_tree[symbol].append(output_offset)
|
||||
|
||||
# target read
|
||||
if max_length < 4:
|
||||
max_length = min(1, target_size - output_offset)
|
||||
mode = BpsMode.BPS_ACTION_TARGET_READ
|
||||
|
||||
if mode != BpsMode.BPS_ACTION_TARGET_READ:
|
||||
target_read_flush(trl)
|
||||
|
||||
if mode == BpsMode.BPS_ACTION_SOURCE_READ:
|
||||
patch_actions.append((mode, max_length, None))
|
||||
elif mode == BpsMode.BPS_ACTION_TARGET_READ:
|
||||
trl['target_read_length'] += max_length
|
||||
else:
|
||||
if mode == BpsMode.BPS_ACTION_SOURCE_COPY:
|
||||
relative_offset = max_offset - source_relative_offset
|
||||
source_relative_offset = max_offset + max_length
|
||||
else:
|
||||
relative_offset = max_offset - target_relative_offset
|
||||
target_relative_offset = max_offset + max_length
|
||||
patch_actions.append((mode, max_length, relative_offset))
|
||||
|
||||
output_offset += max_length
|
||||
|
||||
target_read_flush(trl)
|
||||
|
||||
return patch_actions
|
||||
|
||||
|
||||
def create_bps_linear(original, modified):
|
||||
patch_actions = []
|
||||
source_data = original
|
||||
target_data = modified
|
||||
source_size = len(original)
|
||||
target_size = len(modified)
|
||||
|
||||
target_relative_offset = 0
|
||||
output_offset = 0
|
||||
trl = {'target_read_length': 0}
|
||||
|
||||
def target_read_flush(buffer):
|
||||
if buffer['target_read_length']:
|
||||
action = (BpsMode.BPS_ACTION_TARGET_READ, buffer['target_read_length'], [])
|
||||
patch_actions.append(action)
|
||||
offset = output_offset - buffer['target_read_length']
|
||||
while buffer['target_read_length']:
|
||||
action[2].append(target_data[offset])
|
||||
offset += 1
|
||||
buffer['target_read_length'] -= 1
|
||||
|
||||
eof = min(source_size, target_size)
|
||||
while output_offset < target_size:
|
||||
src_length, n = 0, 0
|
||||
|
||||
while output_offset + n < eof:
|
||||
if source_data[output_offset + n] != target_data[output_offset + n]:
|
||||
break
|
||||
src_length += 1
|
||||
n += 1
|
||||
|
||||
rle_length, n = 0, 1
|
||||
while output_offset + n < target_size:
|
||||
if target_data[output_offset] != target_data[output_offset + n]:
|
||||
break
|
||||
rle_length += 1
|
||||
n += 1
|
||||
|
||||
if rle_length >= 4:
|
||||
trl['target_read_length'] += 1
|
||||
output_offset += 1
|
||||
target_read_flush(trl)
|
||||
|
||||
relative_offset = (output_offset - 1) - target_relative_offset
|
||||
patch_actions.append((BpsMode.BPS_ACTION_TARGET_COPY, rle_length, relative_offset))
|
||||
output_offset += rle_length
|
||||
target_relative_offset = output_offset - 1
|
||||
elif src_length >= 4:
|
||||
target_read_flush(trl)
|
||||
patch_actions.append((BpsMode.BPS_ACTION_SOURCE_READ, src_length, None))
|
||||
output_offset += src_length
|
||||
else:
|
||||
trl['target_read_length'] += 1
|
||||
output_offset += 1
|
||||
|
||||
target_read_flush(trl)
|
||||
|
||||
return patch_actions
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
with open(sys.argv[1], 'rb') as source:
|
||||
sourcedata = source.read()
|
||||
|
||||
with open(sys.argv[2], 'rb') as target:
|
||||
targetdata = target.read()
|
||||
|
||||
patch = create_bps_from_data(sourcedata, targetdata)
|
||||
with open(sys.argv[3], 'wb') as patchfile:
|
||||
patchfile.write(patch.binary_ba)
|
||||
|
||||
|
||||
|
||||
284
source/tools/Bias.py
Normal file
284
source/tools/Bias.py
Normal file
@@ -0,0 +1,284 @@
|
||||
# Demo script showing bias in VT and ER boss shuffle algorithms, with proposed fixes.
|
||||
|
||||
import random
|
||||
|
||||
boss_location_list = [
|
||||
'GT_top',
|
||||
'GT_mid',
|
||||
'TH',
|
||||
'SW',
|
||||
'EP',
|
||||
'DP',
|
||||
'PD',
|
||||
'SP',
|
||||
'TT',
|
||||
'IP',
|
||||
'MM',
|
||||
'TR',
|
||||
'GT_bot'
|
||||
]
|
||||
|
||||
entrance_rando_boss_location_list = [
|
||||
'GT_top',
|
||||
'TH',
|
||||
'SW',
|
||||
'GT_mid',
|
||||
'EP',
|
||||
'DP',
|
||||
'PD',
|
||||
'SP',
|
||||
'TT',
|
||||
'IP',
|
||||
'MM',
|
||||
'TR',
|
||||
'GT_bot'
|
||||
]
|
||||
|
||||
boss_list = [
|
||||
'Armos',
|
||||
'Lanmo',
|
||||
'Moldorm',
|
||||
'Helma',
|
||||
'Arrghus',
|
||||
'Moth',
|
||||
'Blind',
|
||||
'Khold',
|
||||
'Vitty',
|
||||
'Trinexx'
|
||||
]
|
||||
|
||||
results_dict = {
|
||||
'GT_top': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
|
||||
'GT_mid': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
|
||||
'TH': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
|
||||
'SW': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
|
||||
'EP': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
|
||||
'DP': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
|
||||
'PD': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
|
||||
'SP': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
|
||||
'TT': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
|
||||
'IP': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
|
||||
'MM': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
|
||||
'TR': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
|
||||
'GT_bot': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0}
|
||||
}
|
||||
|
||||
def can_place_boss(boss:str, dungeon_name: str) -> bool:
|
||||
if (dungeon_name == 'GT_top'):
|
||||
if boss in {'Armos', 'Arrghus', 'Blind', 'Trinexx', 'Lanmo'}:
|
||||
return False
|
||||
elif (dungeon_name == 'GT_mid'):
|
||||
if boss == 'Blind':
|
||||
return False
|
||||
elif (dungeon_name == 'TH'):
|
||||
if boss in {'Armos', 'Arrghus', 'Blind', 'Trinexx', 'Lanmo'}:
|
||||
return False
|
||||
elif (dungeon_name == 'SW'):
|
||||
if boss == 'Trinexx':
|
||||
return False
|
||||
return True
|
||||
|
||||
def resetresults():
|
||||
for boss_location in boss_location_list:
|
||||
for boss in boss_list:
|
||||
results_dict[boss_location][boss] = 0
|
||||
|
||||
def printresults(iterations):
|
||||
for boss_location in boss_location_list:
|
||||
print("Location: " + boss_location.ljust(6) + ' ', end='')
|
||||
for boss in boss_list:
|
||||
percentagestr = "{0:.2%}".format(results_dict[boss_location][boss] / iterations)
|
||||
print(boss + ": " + percentagestr.rjust(6) + ' ', end='')
|
||||
restrictive_count = results_dict[boss_location]['Armos'] + results_dict[boss_location]['Arrghus'] + results_dict[boss_location]['Blind'] + results_dict[boss_location]['Trinexx'] + results_dict[boss_location]['Lanmo']
|
||||
restrictive_pct = "{0:.2%}".format(restrictive_count / iterations)
|
||||
nonrestrictive_count = results_dict[boss_location]['Moldorm'] + results_dict[boss_location]['Helma'] + results_dict[boss_location]['Moth'] + results_dict[boss_location]['Khold'] + results_dict[boss_location]['Vitty']
|
||||
nonrestrictive_pct = "{0:.2%}".format(nonrestrictive_count / iterations)
|
||||
print("Restrictive: " + restrictive_pct.rjust(7) + " Non-restrictive: " + nonrestrictive_pct.rjust(7))
|
||||
|
||||
# Demonstration of reshuffle algorithm at:
|
||||
# https://github.com/sporchia/alttp_vt_randomizer/blob/master/app/Randomizer.php#L399
|
||||
def vt(reshuffle):
|
||||
for i in range(iterations):
|
||||
dupes = random.sample(boss_list, 3)
|
||||
temp_full_boss_list = boss_list + dupes
|
||||
shuffled_boss_list = temp_full_boss_list.copy()
|
||||
random.shuffle(shuffled_boss_list)
|
||||
for boss_location in boss_location_list:
|
||||
boss = shuffled_boss_list.pop(0)
|
||||
while (not can_place_boss(boss, boss_location)):
|
||||
shuffled_boss_list.append(boss)
|
||||
|
||||
# Proposed fix
|
||||
if reshuffle:
|
||||
random.shuffle(shuffled_boss_list)
|
||||
|
||||
boss = shuffled_boss_list.pop(0)
|
||||
results_dict[boss_location][boss] = results_dict[boss_location][boss] + 1
|
||||
|
||||
# Demonstration of reshuffle algorithm at:
|
||||
# https://github.com/KevinCathcart/ALttPEntranceRandomizer/blob/Dev/Bosses.py#L203
|
||||
def er(reshuffle):
|
||||
for i in range(iterations):
|
||||
dupes = random.sample(boss_list, 3)
|
||||
temp_full_boss_list = boss_list + dupes
|
||||
shuffled_boss_list = temp_full_boss_list.copy()
|
||||
random.shuffle(shuffled_boss_list)
|
||||
for boss_location in entrance_rando_boss_location_list:
|
||||
boss = next((b for b in shuffled_boss_list if can_place_boss(b, boss_location)), None)
|
||||
shuffled_boss_list.remove(boss)
|
||||
results_dict[boss_location][boss] = results_dict[boss_location][boss] + 1
|
||||
|
||||
# Proposed fix
|
||||
if reshuffle:
|
||||
random.shuffle(shuffled_boss_list)
|
||||
|
||||
def er_unique():
|
||||
for i in range(iterations):
|
||||
temp_full_boss_list = boss_list
|
||||
moldorm2 = random.choice([b for b in boss_list
|
||||
if b not in ["Armos", "Arrghus", "Blind", "Trinexx", "Lanmo"]])
|
||||
lanmo2 = random.choice([b for b in boss_list
|
||||
if b not in [moldorm2, "Blind"]])
|
||||
armos2 = random.choice([b for b in boss_list if b not in [moldorm2, lanmo2]])
|
||||
gt_bosses = [moldorm2, lanmo2, armos2]
|
||||
|
||||
shuffled_boss_list = temp_full_boss_list.copy()
|
||||
for boss_location in entrance_rando_boss_location_list:
|
||||
if '_' in boss_location:
|
||||
boss = random.choice([b for b in gt_bosses if can_place_boss(b, boss_location)])
|
||||
gt_bosses.remove(boss)
|
||||
else:
|
||||
boss = random.choice([b for b in shuffled_boss_list if can_place_boss(b, boss_location)])
|
||||
shuffled_boss_list.remove(boss)
|
||||
results_dict[boss_location][boss] += 1
|
||||
|
||||
def er_unique_chaos():
|
||||
for i in range(iterations):
|
||||
temp_full_boss_list = boss_list
|
||||
|
||||
shuffled_boss_list = temp_full_boss_list.copy()
|
||||
for boss_location in entrance_rando_boss_location_list:
|
||||
if '_' in boss_location:
|
||||
boss = random.choice(b for b in boss_list if can_place_boss(b, boss_location))
|
||||
else:
|
||||
boss = random.choice(b for b in shuffled_boss_list if can_place_boss(b, boss_location))
|
||||
shuffled_boss_list.remove(boss)
|
||||
results_dict[boss_location][boss] += 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
iterations = 100000
|
||||
# reshuffle = False
|
||||
#
|
||||
# vt(reshuffle)
|
||||
#
|
||||
# print("Original results:")
|
||||
#
|
||||
# printresults(iterations)
|
||||
# resetresults()
|
||||
#
|
||||
# reshuffle = True
|
||||
#
|
||||
# vt(reshuffle)
|
||||
#
|
||||
# print("")
|
||||
# print("Reshuffled results:")
|
||||
#
|
||||
# printresults(iterations)
|
||||
# resetresults()
|
||||
#
|
||||
# #ER
|
||||
# reshuffle = False
|
||||
# er(reshuffle)
|
||||
#
|
||||
# print("")
|
||||
# print("ER Original results:")
|
||||
#
|
||||
# printresults(iterations)
|
||||
# resetresults()
|
||||
#
|
||||
# reshuffle = True
|
||||
# er(reshuffle)
|
||||
#
|
||||
# print("")
|
||||
# print("ER Reshuffled results:")
|
||||
#
|
||||
# printresults(iterations)
|
||||
# resetresults()
|
||||
|
||||
er_unique()
|
||||
|
||||
print("")
|
||||
print("ER Unique results:")
|
||||
|
||||
printresults(iterations)
|
||||
resetresults()
|
||||
|
||||
er_unique_chaos()
|
||||
|
||||
print("")
|
||||
print("ER Chaos results:")
|
||||
|
||||
printresults(iterations)
|
||||
|
||||
# Results:
|
||||
#Original results:
|
||||
#Location: GT_top Armos: 0.00% Lanmo: 0.00% Moldorm: 20.00% Helma: 19.89% Arrghus: 0.00% Moth: 20.01% Blind: 0.00% Khold: 20.14% Vitty: 19.96% Trinexx: 0.00% Restrictive: 0.00% Non-restrictive: 100.00%
|
||||
#Location: GT_mid Armos: 11.05% Lanmo: 11.16% Moldorm: 11.00% Helma: 11.35% Arrghus: 11.07% Moth: 11.00% Blind: 0.00% Khold: 11.01% Vitty: 11.20% Trinexx: 11.15% Restrictive: 44.43% Non-restrictive: 55.57%
|
||||
#Location: TH Armos: 0.00% Lanmo: 0.00% Moldorm: 20.06% Helma: 19.95% Arrghus: 0.00% Moth: 20.07% Blind: 0.00% Khold: 19.88% Vitty: 20.05% Trinexx: 0.00% Restrictive: 0.00% Non-restrictive: 100.00%
|
||||
#Location: SW Armos: 11.30% Lanmo: 11.04% Moldorm: 11.30% Helma: 10.95% Arrghus: 11.09% Moth: 11.04% Blind: 11.15% Khold: 11.15% Vitty: 10.98% Trinexx: 0.00% Restrictive: 44.59% Non-restrictive: 55.42%
|
||||
#Location: EP Armos: 9.89% Lanmo: 9.90% Moldorm: 10.02% Helma: 9.97% Arrghus: 10.17% Moth: 9.98% Blind: 9.95% Khold: 10.16% Vitty: 9.90% Trinexx: 10.07% Restrictive: 49.97% Non-restrictive: 50.03%
|
||||
#Location: DP Armos: 9.98% Lanmo: 10.09% Moldorm: 9.97% Helma: 9.95% Arrghus: 10.17% Moth: 10.01% Blind: 9.87% Khold: 10.02% Vitty: 9.88% Trinexx: 10.04% Restrictive: 50.16% Non-restrictive: 49.84%
|
||||
#Location: PD Armos: 10.12% Lanmo: 9.96% Moldorm: 9.85% Helma: 9.90% Arrghus: 10.31% Moth: 9.91% Blind: 9.96% Khold: 9.87% Vitty: 9.94% Trinexx: 10.17% Restrictive: 50.52% Non-restrictive: 49.48%
|
||||
#Location: SP Armos: 10.41% Lanmo: 10.35% Moldorm: 9.61% Helma: 9.66% Arrghus: 10.29% Moth: 9.60% Blind: 10.47% Khold: 9.67% Vitty: 9.53% Trinexx: 10.42% Restrictive: 51.93% Non-restrictive: 48.07%
|
||||
#Location: TT Armos: 11.04% Lanmo: 10.98% Moldorm: 8.86% Helma: 8.95% Arrghus: 10.98% Moth: 9.12% Blind: 11.18% Khold: 8.98% Vitty: 9.03% Trinexx: 10.89% Restrictive: 55.06% Non-restrictive: 44.94%
|
||||
#Location: IP Armos: 12.10% Lanmo: 11.89% Moldorm: 7.81% Helma: 7.83% Arrghus: 11.94% Moth: 7.89% Blind: 12.84% Khold: 7.87% Vitty: 7.99% Trinexx: 11.84% Restrictive: 60.62% Non-restrictive: 39.38%
|
||||
#Location: MM Armos: 13.67% Lanmo: 13.66% Moldorm: 6.00% Helma: 6.17% Arrghus: 13.48% Moth: 6.18% Blind: 15.06% Khold: 6.25% Vitty: 6.14% Trinexx: 13.41% Restrictive: 69.28% Non-restrictive: 30.72%
|
||||
#Location: TR Armos: 15.35% Lanmo: 15.49% Moldorm: 3.87% Helma: 3.90% Arrghus: 15.31% Moth: 3.78% Blind: 19.06% Khold: 3.89% Vitty: 3.82% Trinexx: 15.53% Restrictive: 80.74% Non-restrictive: 19.26%
|
||||
#Location: GT_bot Armos: 15.24% Lanmo: 15.24% Moldorm: 1.54% Helma: 1.51% Arrghus: 15.22% Moth: 1.52% Blind: 20.44% Khold: 1.51% Vitty: 1.53% Trinexx: 26.25% Restrictive: 92.38% Non-restrictive: 7.62%
|
||||
#
|
||||
#Reshuffled results:
|
||||
#Location: GT_top Armos: 0.00% Lanmo: 0.00% Moldorm: 20.02% Helma: 19.97% Arrghus: 0.00% Moth: 20.02% Blind: 0.00% Khold: 20.07% Vitty: 19.92% Trinexx: 0.00% Restrictive: 0.00% Non-restrictive: 100.00%
|
||||
#Location: GT_mid Armos: 12.23% Lanmo: 12.05% Moldorm: 10.23% Helma: 10.24% Arrghus: 12.11% Moth: 10.31% Blind: 0.00% Khold: 10.36% Vitty: 10.33% Trinexx: 12.13% Restrictive: 48.52% Non-restrictive: 51.48%
|
||||
#Location: TH Armos: 0.00% Lanmo: 0.00% Moldorm: 19.86% Helma: 20.09% Arrghus: 0.00% Moth: 20.23% Blind: 0.00% Khold: 19.83% Vitty: 19.99% Trinexx: 0.00% Restrictive: 0.00% Non-restrictive: 100.00%
|
||||
#Location: SW Armos: 13.23% Lanmo: 13.34% Moldorm: 9.05% Helma: 9.02% Arrghus: 13.37% Moth: 9.10% Blind: 14.97% Khold: 8.98% Vitty: 8.93% Trinexx: 0.00% Restrictive: 54.92% Non-restrictive: 45.08%
|
||||
#Location: EP Armos: 11.54% Lanmo: 11.50% Moldorm: 7.96% Helma: 7.82% Arrghus: 11.76% Moth: 7.81% Blind: 12.84% Khold: 7.79% Vitty: 8.03% Trinexx: 12.95% Restrictive: 60.59% Non-restrictive: 39.41%
|
||||
#Location: DP Armos: 11.55% Lanmo: 11.49% Moldorm: 7.93% Helma: 7.85% Arrghus: 11.72% Moth: 7.69% Blind: 12.80% Khold: 7.99% Vitty: 7.74% Trinexx: 13.25% Restrictive: 60.80% Non-restrictive: 39.20%
|
||||
#Location: PD Armos: 11.79% Lanmo: 11.47% Moldorm: 7.81% Helma: 7.78% Arrghus: 11.61% Moth: 7.91% Blind: 12.81% Khold: 7.78% Vitty: 8.00% Trinexx: 13.02% Restrictive: 60.71% Non-restrictive: 39.29%
|
||||
#Location: SP Armos: 11.59% Lanmo: 11.56% Moldorm: 7.90% Helma: 7.75% Arrghus: 11.52% Moth: 8.02% Blind: 12.67% Khold: 7.92% Vitty: 7.85% Trinexx: 13.23% Restrictive: 60.57% Non-restrictive: 39.43%
|
||||
#Location: TT Armos: 11.64% Lanmo: 11.64% Moldorm: 7.77% Helma: 7.73% Arrghus: 11.70% Moth: 7.67% Blind: 12.89% Khold: 8.05% Vitty: 7.85% Trinexx: 13.07% Restrictive: 60.93% Non-restrictive: 39.07%
|
||||
#Location: IP Armos: 11.54% Lanmo: 11.77% Moldorm: 7.95% Helma: 7.97% Arrghus: 11.49% Moth: 7.80% Blind: 12.64% Khold: 7.87% Vitty: 7.89% Trinexx: 13.10% Restrictive: 60.54% Non-restrictive: 39.47%
|
||||
#Location: MM Armos: 11.69% Lanmo: 11.65% Moldorm: 7.91% Helma: 7.88% Arrghus: 11.59% Moth: 7.78% Blind: 12.80% Khold: 7.73% Vitty: 7.92% Trinexx: 13.04% Restrictive: 60.77% Non-restrictive: 39.23%
|
||||
#Location: TR Armos: 11.58% Lanmo: 11.82% Moldorm: 7.75% Helma: 7.83% Arrghus: 11.62% Moth: 7.79% Blind: 12.76% Khold: 7.90% Vitty: 7.76% Trinexx: 13.19% Restrictive: 60.97% Non-restrictive: 39.03%
|
||||
#Location: GT_bot Armos: 11.54% Lanmo: 11.60% Moldorm: 7.91% Helma: 7.87% Arrghus: 11.62% Moth: 7.90% Blind: 12.78% Khold: 7.82% Vitty: 7.86% Trinexx: 13.10% Restrictive: 60.65% Non-restrictive: 39.35%
|
||||
#
|
||||
#ER Original results:
|
||||
#Location: GT_top Armos: 0.00% Lanmo: 0.00% Moldorm: 19.94% Helma: 19.87% Arrghus: 0.00% Moth: 19.98% Blind: 0.00% Khold: 20.31% Vitty: 19.90% Trinexx: 0.00% Restrictive: 0.00% Non-restrictive: 100.00%
|
||||
#Location: GT_mid Armos: 14.15% Lanmo: 14.33% Moldorm: 4.51% Helma: 4.53% Arrghus: 14.19% Moth: 4.56% Blind: 0.00% Khold: 4.61% Vitty: 4.49% Trinexx: 34.62% Restrictive: 77.30% Non-restrictive: 22.70%
|
||||
#Location: TH Armos: 0.00% Lanmo: 0.00% Moldorm: 19.93% Helma: 19.98% Arrghus: 0.00% Moth: 20.10% Blind: 0.00% Khold: 19.75% Vitty: 20.24% Trinexx: 0.00% Restrictive: 0.00% Non-restrictive: 100.00%
|
||||
#Location: SW Armos: 21.52% Lanmo: 21.56% Moldorm: 2.76% Helma: 2.80% Arrghus: 21.50% Moth: 2.82% Blind: 21.48% Khold: 2.81% Vitty: 2.75% Trinexx: 0.00% Restrictive: 86.05% Non-restrictive: 13.95%
|
||||
#Location: EP Armos: 11.39% Lanmo: 11.33% Moldorm: 5.89% Helma: 5.69% Arrghus: 11.32% Moth: 5.83% Blind: 24.71% Khold: 5.82% Vitty: 5.76% Trinexx: 12.27% Restrictive: 71.01% Non-restrictive: 28.99%
|
||||
#Location: DP Armos: 11.62% Lanmo: 11.66% Moldorm: 8.05% Helma: 8.32% Arrghus: 11.75% Moth: 8.13% Blind: 12.46% Khold: 8.10% Vitty: 8.18% Trinexx: 11.73% Restrictive: 59.22% Non-restrictive: 40.78%
|
||||
#Location: PD Armos: 11.01% Lanmo: 10.77% Moldorm: 9.18% Helma: 9.20% Arrghus: 10.89% Moth: 9.03% Blind: 10.83% Khold: 9.13% Vitty: 9.17% Trinexx: 10.79% Restrictive: 54.29% Non-restrictive: 45.71%
|
||||
#Location: SP Armos: 10.26% Lanmo: 10.17% Moldorm: 9.73% Helma: 9.61% Arrghus: 10.33% Moth: 9.69% Blind: 10.31% Khold: 9.84% Vitty: 9.61% Trinexx: 10.45% Restrictive: 51.52% Non-restrictive: 48.48%
|
||||
#Location: TT Armos: 10.12% Lanmo: 10.01% Moldorm: 10.07% Helma: 9.97% Arrghus: 10.16% Moth: 9.92% Blind: 9.95% Khold: 9.73% Vitty: 9.96% Trinexx: 10.11% Restrictive: 50.35% Non-restrictive: 49.65%
|
||||
#Location: IP Armos: 10.03% Lanmo: 10.01% Moldorm: 10.00% Helma: 10.10% Arrghus: 9.89% Moth: 9.93% Blind: 10.13% Khold: 9.99% Vitty: 9.80% Trinexx: 10.13% Restrictive: 50.18% Non-restrictive: 49.82%
|
||||
#Location: MM Armos: 9.89% Lanmo: 9.99% Moldorm: 10.09% Helma: 10.10% Arrghus: 10.02% Moth: 9.93% Blind: 10.03% Khold: 10.00% Vitty: 10.06% Trinexx: 9.89% Restrictive: 49.81% Non-restrictive: 50.19%
|
||||
#Location: TR Armos: 10.03% Lanmo: 10.10% Moldorm: 10.01% Helma: 9.92% Arrghus: 9.94% Moth: 10.01% Blind: 10.04% Khold: 9.95% Vitty: 9.92% Trinexx: 10.08% Restrictive: 50.19% Non-restrictive: 49.81%
|
||||
#Location: GT_bot Armos: 9.90% Lanmo: 10.04% Moldorm: 9.88% Helma: 9.85% Arrghus: 10.00% Moth: 10.04% Blind: 10.09% Khold: 10.09% Vitty: 10.11% Trinexx: 10.01% Restrictive: 50.03% Non-restrictive: 49.97%
|
||||
#
|
||||
#ER Reshuffled results:
|
||||
#Location: GT_top Armos: 0.00% Lanmo: 0.00% Moldorm: 20.11% Helma: 19.81% Arrghus: 0.00% Moth: 20.22% Blind: 0.00% Khold: 20.08% Vitty: 19.78% Trinexx: 0.00% Restrictive: 0.00% Non-restrictive: 100.00%
|
||||
#Location: GT_mid Armos: 13.17% Lanmo: 13.37% Moldorm: 9.05% Helma: 9.14% Arrghus: 13.23% Moth: 9.29% Blind: 0.00% Khold: 8.96% Vitty: 9.11% Trinexx: 14.69% Restrictive: 54.45% Non-restrictive: 45.55%
|
||||
#Location: TH Armos: 0.00% Lanmo: 0.00% Moldorm: 20.03% Helma: 19.72% Arrghus: 0.00% Moth: 20.17% Blind: 0.00% Khold: 19.86% Vitty: 20.23% Trinexx: 0.00% Restrictive: 0.00% Non-restrictive: 100.00%
|
||||
#Location: SW Armos: 13.38% Lanmo: 13.41% Moldorm: 9.27% Helma: 9.33% Arrghus: 13.41% Moth: 9.27% Blind: 13.48% Khold: 9.28% Vitty: 9.17% Trinexx: 0.00% Restrictive: 53.69% Non-restrictive: 46.31%
|
||||
#Location: EP Armos: 11.50% Lanmo: 11.56% Moldorm: 7.90% Helma: 8.06% Arrghus: 11.58% Moth: 8.11% Blind: 12.72% Khold: 7.93% Vitty: 7.93% Trinexx: 12.70% Restrictive: 60.06% Non-restrictive: 39.94%
|
||||
#Location: DP Armos: 11.66% Lanmo: 11.51% Moldorm: 8.05% Helma: 7.96% Arrghus: 11.43% Moth: 7.83% Blind: 12.94% Khold: 7.89% Vitty: 7.88% Trinexx: 12.87% Restrictive: 60.40% Non-restrictive: 39.60%
|
||||
#Location: PD Armos: 11.36% Lanmo: 11.53% Moldorm: 7.89% Helma: 7.96% Arrghus: 11.46% Moth: 7.72% Blind: 13.12% Khold: 8.07% Vitty: 8.02% Trinexx: 12.87% Restrictive: 60.34% Non-restrictive: 39.66%
|
||||
#Location: SP Armos: 11.27% Lanmo: 11.38% Moldorm: 7.89% Helma: 7.98% Arrghus: 11.56% Moth: 7.89% Blind: 13.10% Khold: 8.17% Vitty: 8.08% Trinexx: 12.68% Restrictive: 59.99% Non-restrictive: 40.01%
|
||||
#Location: TT Armos: 11.32% Lanmo: 11.49% Moldorm: 7.91% Helma: 8.16% Arrghus: 11.44% Moth: 7.97% Blind: 12.93% Khold: 8.05% Vitty: 7.97% Trinexx: 12.77% Restrictive: 59.94% Non-restrictive: 40.06%
|
||||
#Location: IP Armos: 11.69% Lanmo: 11.74% Moldorm: 7.91% Helma: 8.00% Arrghus: 11.34% Moth: 7.87% Blind: 12.80% Khold: 7.82% Vitty: 7.95% Trinexx: 12.88% Restrictive: 60.44% Non-restrictive: 39.56%
|
||||
#Location: MM Armos: 11.57% Lanmo: 11.31% Moldorm: 8.13% Helma: 7.90% Arrghus: 11.32% Moth: 7.91% Blind: 13.15% Khold: 7.97% Vitty: 7.82% Trinexx: 12.91% Restrictive: 60.26% Non-restrictive: 39.74%
|
||||
#Location: TR Armos: 11.44% Lanmo: 11.30% Moldorm: 8.04% Helma: 8.13% Arrghus: 11.47% Moth: 7.90% Blind: 12.82% Khold: 8.09% Vitty: 7.96% Trinexx: 12.84% Restrictive: 59.87% Non-restrictive: 40.13%
|
||||
#Location: GT_bot Armos: 11.36% Lanmo: 11.53% Moldorm: 7.87% Helma: 7.99% Arrghus: 11.49% Moth: 7.96% Blind: 13.01% Khold: 7.98% Vitty: 8.00% Trinexx: 12.80% Restrictive: 60.19% Non-restrictive: 39.81
|
||||
0
source/tools/__init__.py
Normal file
0
source/tools/__init__.py
Normal file
Reference in New Issue
Block a user