-
Notifications
You must be signed in to change notification settings - Fork 27
/
Copy pathsettings.py
372 lines (349 loc) · 26.5 KB
/
settings.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
from typing import List, Tuple, Optional, Union
import os
class Setting:
def __init__(self, key: str,
category: str, short_key: str, label: str, *,
description: str, multiworld: bool = True, aesthetic: bool = False, options: Optional[List[Tuple[str, str, str]]] = None,
default: Optional[Union[bool, float, str]] = None, placeholder: Optional[str] = None):
if options:
assert default in [option_key for option_key, option_short, option_label in options], f"{default} not in {options}"
short_options = set()
for option_key, option_short, option_label in options:
assert option_short != "" or option_key == default, f"No short option for non default {label}:{option_key}"
assert option_short not in short_options, "Duplicate short option value..."
short_options.add(option_short)
self.key = key
self.category = category
self.short_key = short_key
self.label = label
self.description = description
self.multiworld = multiworld
self.aesthetic = aesthetic
self.options = options
self.default = default
self.placeholder = placeholder
self.value = default
def set(self, value):
if isinstance(self.default, bool):
if not isinstance(value, bool):
value = bool(int(value))
elif not isinstance(value, type(self.default)):
try:
value = type(self.default)(value)
except ValueError:
raise ValueError(f"{value} is not an accepted value for {self.key} setting")
if self.options:
if value not in [k for k, s, v in self.options]:
raise ValueError(f"{value} is not an accepted value for {self.key} setting")
self.value = value
def getShortValue(self):
if self.options:
for option_key, option_short, option_label in self.options:
if self.value == option_key:
return option_short
return str(self.value) + ">"
def toJson(self):
result = {
"key": self.key,
"category": self.category,
"short_key": self.short_key,
"label": self.label,
"description": self.description,
"multiworld": self.multiworld,
"aesthetic": self.aesthetic,
"default": self.default,
}
if self.options:
result["options"] = [{"key": option_key, "short": option_short, "label": option_label} for option_key, option_short, option_label in self.options]
if self.placeholder:
result["placeholder"] = self.placeholder
return result
class Settings:
def __init__(self, multiworld_count=None):
gfx_options = [('', '', 'Default')]
gfx_path = os.path.join(os.path.dirname(__file__), "gfx")
for filename in sorted(os.listdir(gfx_path)):
if filename in {"template.png", "template_extended.png"}:
continue
if filename.endswith(".bin"):
gfx_options.append((filename, filename + ">", filename[:-4]))
if filename.endswith(".png") and not filename.endswith(".bin.png"):
gfx_options.append((filename, filename + ">", filename[:-4]))
self.__all = [
Setting('seed', 'Main', '<', 'Seed', placeholder='Leave empty for random seed', default="", multiworld=False,
description="""For multiple people to generate the same randomization result, enter the generated seed number here.
Note, not all strings are valid seeds."""),
Setting('logic', 'Main', 'L', 'Logic', options=[('casual', 'c', 'Casual'), ('', '', 'Normal'), ('hard', 'h', 'Hard'), ('glitched', 'g', 'Glitched'), ('hell', 'H', 'Hell')], default='',
description="""Affects where items are allowed to be placed.
[Casual] Same as normal, except that a few more complex options are removed, like removing bushes with powder and killing enemies with powder or bombs.
[Normal] playable without using any tricks or glitches. Requires nothing to be done outside of normal item usage.
[Hard] More advanced techniques may be required, but glitches are not. Examples include tricky jumps, killing enemies with only pots and skipping keys with smart routing.
[Glitched] Advanced glitches and techniques may be required, but extremely difficult or tedious tricks are not required. Examples include Bomb Triggers, Super Jumps and Jesus Jumps.
[Hell] Obscure and hard techniques may be required. Examples include featherless jumping with boots and/or hookshot, sequential pit buffers and unclipped superjumps. Things in here can be extremely hard to do or very time consuming. Only insane people go for this."""),
Setting('forwardfactor', 'Main', 'F', 'Forward Factor', default=0.0,
description="Forward item weight adjustment factor, lower values generate more rear heavy seeds while higher values generate front heavy seeds. Default is 0.5."),
Setting('accessibility', 'Main', 'A', 'Accessibility', options=[('all', 'a', '100% Locations'), ('goal', 'g', 'Beatable')], default='all',
description="""
[100% Locations] guaranteed that every single item can be reached and gained.
[Beatable] only guarantees that the game is beatable. Certain items/chests might never be reachable."""),
Setting('race', 'Main', 'V', 'Race mode', default=False, multiworld=False,
description="""
Spoiler logs can not be generated for ROMs generated with race mode enabled, and seed generation is slightly different."""),
# Setting('spoilerformat', 'Main', 'Spoiler Format', options=[('none', 'None'), ('text', 'Text'), ('json', 'JSON')], default='none', multiworld=False,
# description="""Affects how the spoiler log is generated.
# [None] No spoiler log is generated. One can still be manually dumped later.
# [Text] Creates a .txt file meant for a human to read.
# [JSON] Creates a .json file with a little more information and meant for a computer to read.""")
Setting('heartpiece', 'Items', 'h', 'Randomize heart pieces', default=True,
description='Includes heart pieces in the item pool'),
Setting('seashells', 'Items', 's', 'Randomize hidden seashells', default=True,
description='Randomizes the secret sea shells hiding in the ground/trees/mansion. (chests are always randomized)'),
Setting('heartcontainers', 'Items', 'H', 'Randomize heart containers', default=True,
description='Includes boss heart container drops in the item pool'),
Setting('instruments', 'Items', 'I', 'Randomize instruments', default=False,
description='Instruments are placed on random locations, dungeon goal will just contain a random item.'),
Setting('tradequest', 'Items', 'T', 'Randomize trade quest', default=True,
description='Trade quest items are randomized, each NPC takes its normal trade quest item, but gives a random item'),
Setting('witch', 'Items', 'W', 'Randomize item given by the witch', default=True,
description='Adds both the toadstool and the reward for giving the toadstool to the witch to the item pool'),
Setting('rooster', 'Items', 'R', 'Add the rooster', default=True,
description='Adds the rooster to the item pool. Without this option, the rooster spot is still a check giving an item. But you will never find the rooster. In that case, any rooster spot is accessible without rooster by other means.'),
Setting('boomerang', 'Items', 'Z', 'Boomerang trade', options=[('default', 'd', 'Normal'), ('trade', 't', 'Trade'), ('gift', 'g', 'Gift')], default='gift',
description="""
[Normal], requires magnifier to get the boomerang.
[Trade], allows to trade an inventory item for a random other inventory item boomerang is shuffled.
[Gift], You get a random gift of any item, and the boomerang is shuffled."""),
Setting('dungeon_items', 'Gameplay', 'D', 'Dungeon items', options=[('', '', 'Standard'),
('smallkeys', 's', 'Small keys'),
('nightmarekeys', 'n', 'Nightmare keys'),
('localkeys', 'L', 'Map/Compass/Beaks'),
('localnightmarekey', 'N', 'MCB + SmallKeys'),
('keysanity', 'K', 'Keysanity'),
('keysy', 'k', 'Keysy')], default='',
description="""Sets if dungeon items can only be in their respective dungeon, or everywhere.
[Standard] dungeon items are only in their dungeon.
[Maps/.../..] specified items can be anywhere
[Keysanity] all dungeon items can be anywhere.
[Keysy] no keys, key doors are already open."""),
Setting('randomstartlocation', 'Entrances', 'r', 'Random start location', default=False,
description='Randomize where your starting house is located'),
Setting('dungeonshuffle', 'Entrances', 'u', 'Dungeon shuffle', default=False,
description='Randomizes the dungeon that each dungeon entrance leads to'),
Setting('entranceshuffle', 'Entrances', 'E', 'Entrance randomizer', options=[("none", '', "Default"), ("simple", 's', "Simple"), ("split", 'S', "Split"), ("mixed", 'm', "Mixed"), ("wild", 'w', "Wild"), ("chaos", "c", "Chaos"), ("insane", 'i', "Insane"), ("madness", 'M', "Madness")], default='none',
description="""Randomizes where overworld entrances lead to.
[Simple] single entrance caves that contain items are randomized
[Split] Connector caves are also randomized, in a separate pool from single entrance caves
[Mixed] Connector caves are also randomized, in the same pool as single entrance caves
[Wild] Connections can go from overworld to overworld, or inside to inside
[Chaos] Entrance and exits are decoupled.
[Insane] Combines chaos and wild, anything goes anywhere, there is no god.
[Madness] Even worse then insane, it makes it so multiple entrances can lead to the same location
If random start location and/or dungeon shuffle is enabled, then these will be shuffled with all the entrances."""),
Setting('shufflejunk', 'Entrances', 'j', 'Shuffle itemless entrances', default=False,
description="Caves/houses without items are also randomized when entranceshuffle is set"),
Setting('shuffleannoying', 'Entrances', 'a', 'Shuffle annoying entrances', default=False,
description="A few very annoying entrances (Mamu and the Raft House) will also be randomized when entranceshuffle is set"),
Setting('shufflewater', 'Entrances', 'w', 'Shuffle water entrances', default=False,
description="Entrances that lead to water (Manbo and Damp Cave) will also be randomized when entranceshuffle is set. Use the warp-to-home from the save&quit menu if you get stuck (hold A+B+Start+Select until it works)."),
Setting('boss', 'Gameplay', 'B', 'Boss shuffle', options=[('default', '', 'Normal'), ('shuffle', 's', 'Shuffle'), ('random', 'r', 'Randomize')], default='default',
description='Randomizes the dungeon bosses that each dungeon has'),
Setting('miniboss', 'Gameplay', 'b', 'Miniboss shuffle', options=[('default', '', 'Normal'), ('shuffle', 's', 'Shuffle'), ('random', 'r', 'Randomize')], default='default',
description='Randomizes the dungeon minibosses that each dungeon has'),
Setting('enemies', 'Gameplay', 'e', 'Enemizer', options=[('default', '', 'None'), ('overworld', 'o', 'Overworld')], default='default',
description='Randomizes which enemies are placed'),
Setting('goal', 'Gameplay', 'G', 'Goal', options=[('8', '8', '8 instruments'), ('7', '7', '7 instruments'), ('6', '6', '6 instruments'),
('5', '5', '5 instruments'), ('4', '4', '4 instruments'), ('3', '3', '3 instruments'),
('2', '2', '2 instruments'), ('1', '1', '1 instrument'), ('0', '0', 'No instruments'),
('open', 'O', 'Egg already open'), ('random', 'R', 'Random instrument count'),
('open-4', '<', 'Random short game (0-4)'), ('5-8', '>', 'Random long game (5-8)'),
('seashells', 'S', 'Seashell hunt (20)'), ('bingo', 'b', 'Bingo!'),
('bingo-double', 'd', 'Double Bingo!'), ('bingo-triple', 't', 'Triple Bingo!'),
('bingo-full', 'B', 'Bingo-25!'), ('maze', 'm', 'Sign Maze'), ('specific', 's', '4 specific instruments')], default='8',
description="""Changes the goal of the game.
[1-8 instruments], number of instruments required to open the egg.
[No instruments] open the egg without instruments, still requires the ocarina with the balled of the windfish
[Egg already open] the egg is already open, just head for it once you have the items needed to defeat the boss.
[Randomized instrument count] random number of instruments required to open the egg, between 0 and 8.
[Random short/long game] random number of instruments required to open the egg, chosen between 0-4 and 5-8 respectively.
[Seashell hunt] egg will open once you collected 20 seashells. Instruments are replaced by seashells and shuffled.
[Bingo] Generate a 5x5 bingo board with various goals. Complete one row/column or diagonal to win!
[Double/Triple Bingo] Bingo, but need to complete multiple rows/columns/diagonals to win!
[Bingo-25] Bingo, but need to fill the whole bingo card to win!
[Sign Maze] Go on a long trip on the overworld sign maze to open the egg."""),
Setting('itempool', 'Gameplay', 'P', 'Item pool', options=[('', '', 'Normal'), ('casual', 'c', 'Casual'), ('pain', 'p', 'Path of Pain'), ('keyup', 'k', 'More keys')], default='',
description="""Effects which items are shuffled.
[Casual] places more inventory and key items so the seed is easier.
[More keys] adds more small keys and extra nightmare keys so dungeons are easier.
[Path of pain]... just find out yourself."""),
Setting('hpmode', 'Gameplay', 'm', 'Health mode', options=[('default', '', 'Normal'), ('inverted', 'i', 'Inverted'), ('1', '1', 'Start with 1 heart'), ('low', 'l', 'Low max')], default='default',
description="""
[Normal} health works as you would expect.
[Inverted] you start with 9 heart containers, but killing a boss will take a heartcontainer instead of giving one.
[Start with 1] normal game, you just start with 1 heart instead of 3.
[Low max] replace heart containers with heart pieces."""),
Setting('hardmode', 'Gameplay', 'X', 'Hard mode', options=[('none', '', 'Disabled'), ('oracle', 'O', 'Oracle'), ('hero', 'H', 'Hero'), ('ohko', '1', 'One hit KO')], default='none',
description="""
[Oracle] Less iframes and heath from drops. Bombs damage yourself. Water damages you without flippers. No piece of power or acorn.
[Hero] Switch version hero mode, double damage, no heart/fairy drops.
[One hit KO] You die on a single hit, always."""),
Setting('steal', 'Gameplay', 't', 'Stealing from the shop',
options=[('always', 'a', 'Always'), ('never', 'n', 'Never'), ('default', '', 'Normal'),
('ggs', 'g', 'GGS')], default='default',
description="""Effects when you can steal from the shop. Stealing is bad and never in logic.
[Normal] requires the sword before you can steal.
[Always] you can always steal from the shop.
[Never] you can never steal from the shop.
[GGS] Glitches get stitches, do not try to rob the shopkeeper by S&Q..."""),
Setting('evilshop', 'Special', 'v', 'Evil shop', options=[('', '', 'Disabled'), ('enabled', 'e', 'Enabled')], default='',
description='Replaces the grandpa house with an evil shop, where you can sell heart pieces.'),
Setting('bowwow', 'Special', 'g', 'Good boy mode', options=[('normal', '', 'Disabled'), ('always', 'a', 'Enabled'), ('swordless', 's', 'Enabled (swordless)')], default='normal',
description='Allows BowWow to be taken into any area, damage bosses and more enemies. If enabled you always start with bowwow. Swordless option removes the swords from the game and requires you to beat the game without a sword and just bowwow.'),
Setting('overworld', 'Special', 'O', 'Overworld', options=[('normal', '', 'Normal'), ('dungeondive', 'D', 'Dungeon dive'), ('nodungeons', 'N', 'No dungeons'), ('dungeonchain', 'C', 'Dungeon chain'), ('random', 'R', 'Randomized'), ('alttp', 'A', 'ALttP')], default='normal',
description="""
[Dungeon Dive] Create a different overworld where all the dungeons are directly accessible and almost no chests are located in the overworld.
[No dungeons] All dungeons only consist of a boss fight and a instrument reward. Rest of the dungeon is removed.
[Dungeon Chain] Overworld is fully removed and all dungeons are chained together.
[Random] Creates a randomized overworld WARNING: This will error out often during generation, work in progress."""),
Setting('owlstatues', 'Special', 'o', 'Owl statues', options=[('', '', 'Never'), ('dungeon', 'D', 'In dungeons'), ('overworld', 'O', 'On the overworld'), ('both', 'B', 'Dungeons and Overworld')], default='',
description='Replaces the hints from owl statues with additional randomized items'),
Setting('keyholesanity', 'Special', 'K', 'Keyhole sanity', default=False,
description='Makes the overworld keyholes give rewards and turns opening dungeons into findable items'),
Setting('shopsanity', 'Special', 'N', 'Shopsanity', options=[('', '', 'Disabled'), ('basic', 'b', 'Basic'), ('important', 'i', 'Important')], default='',
description='''
Turns all the phone booths into extra shops, and lowers the prices, and allows buying the two shop items independent of each other.
[Basic] Just extra shops
[Important] Shops are guaranteed to have important items
'''),
Setting('superweapons', 'Special', 'q', 'Enable super weapons', default=False,
description='All items will be more powerful, faster, harder, bigger stronger. You name it.'),
Setting('quickswap', 'User options', 'Q', 'Quickswap', options=[('none', '', 'Disabled'), ('a', 'a', 'Swap A button'), ('b', 'b', 'Swap B button')], default='none',
description='Adds that the select button swaps with either A or B. The item is swapped with the top inventory slot. The map is not available when quickswap is enabled.',
aesthetic=True),
Setting('textmode', 'User options', 'f', 'Text mode', options=[('fast', '', 'Fast'), ('default', 'd', 'Normal'), ('none', 'n', 'No-text')], default='fast',
description="""[Fast] makes text appear twice as fast.
[No-Text] removes all text from the game""", aesthetic=True),
Setting('lowhpbeep', 'User options', 'p', 'Low HP beeps', options=[('none', 'D', 'Disabled'), ('slow', 'S', 'Slow'), ('default', 'N', 'Normal')], default='slow',
description='Slows or disables the low health beeping sound', aesthetic=True),
Setting('noflash', 'User options', 'l', 'Remove flashing lights', default=True,
description='Remove the flashing light effects from Mamu, shopkeeper and MadBatter. Useful for capture cards and people that are sensitive for these things.',
aesthetic=True),
Setting('nagmessages', 'User options', 'S', 'Show nag messages', default=False,
description='Enables the nag messages normally shown when touching stones and crystals',
aesthetic=True),
Setting('gfxmod', 'User options', 'c', 'Graphics', options=gfx_options, default='',
description='Generally affects at least Link\'s sprite, but can alter any graphics in the game',
aesthetic=True),
Setting('follower', 'User options', 'x', 'Follower', options=[('', '', 'None'), ('fox', 'f', 'Fox'), ('navi', 'n', 'Navi'), ('ghost', 'g', 'Ghost'), ('yipyip', 'y', 'YipYip')], default='',
description='Gives you a pet follower in the game.',
aesthetic=True),
Setting('linkspalette', 'User options', 'C', "Link's color",
options=[('-1', '-', 'Normal'), ('0', '0', 'Green'), ('1', '1', 'Yellow'), ('2', '2', 'Red'), ('3', '3', 'Blue'),
('4', '4', 'Inverted Red'), ('5', '5', 'Inverted Blue'), ('6', '6', '?? C'), ('7', '7', '?? D')], default='-1', aesthetic=True,
description="""Allows you to force a certain color on link.
[Normal] color of link depends on the tunic.
[Green/Yellow/Red/Blue] forces link into one of these colors.
[?? C/D] colors of link are usually inverted and color depends on the area you are in."""),
Setting('music', 'User options', 'M', 'Music', options=[('', '', 'Default'), ('random', 'r', 'Random'), ('off', 'o', 'Disable'), ('shifted', 's', 'Tone shifted')], default='',
description="""
[Random] Randomizes overworld and dungeon music'
[Disable] no music in the whole game
[Tone shifted] Tone shifts the musics, making it sound different""",
aesthetic=True),
]
self.__by_key = {s.key: s for s in self.__all}
self.__multiworld_count = multiworld_count
self.__multiworld_settings = []
# Make sure all short keys are unique
short_keys = {}
for s in self.__all:
assert s.short_key not in short_keys, f"{s.label}: {short_keys[s.short_key].label}"
short_keys[s.short_key] = s
@property
def multiworld(self): # returns the amount of multiworld players.
if self.__multiworld_settings:
return len(self.__multiworld_settings)
return self.__multiworld_count
@property
def multiworld_settings(self):
return self.__multiworld_settings
def __getattr__(self, item):
return self.__by_key[item].value
def __setattr__(self, key, value):
if not key.startswith("_") and key in self.__by_key:
self.__by_key[key].set(value)
else:
super().__setattr__(key, value)
def loadShortString(self, value):
for setting in self.__all:
if isinstance(setting.default, bool):
setting.value = False
index = 0
while index < len(value):
key = value[index]
index += 1
for setting in self.__all:
if setting.short_key != key:
continue
if isinstance(setting.default, bool):
setting.value = True
elif setting.options:
for option_key, option_short, option_label in setting.options:
if option_key != setting.default and value[index:].startswith(option_short):
setting.value = option_key
index += len(option_short)
break
else:
end_of_param = value.find(">", index)
setting.set(value[index:end_of_param])
index = end_of_param + 1
def getShortString(self):
result = ""
for setting in self.__all:
if isinstance(setting.default, bool):
if setting.value:
result += setting.short_key
elif setting.value != setting.default:
result += setting.short_key + setting.getShortValue()
return result
def validate(self):
def req(setting: str, value: str, message: str) -> None:
if getattr(self, setting) != value:
print("Warning: %s (setting adjusted automatically)" % message)
setattr(self, setting, value)
def dis(setting: str, value: str, new_value: str, message: str) -> None:
if getattr(self, setting) == value:
print("Warning: %s (setting adjusted automatically)" % message)
setattr(self, setting, new_value)
if self.goal in ("bingo", "bingo-double", "bingo-triple", "bingo-full"):
req("overworld", "normal", "Bingo goal does not work with dungeondive")
req("accessibility", "all", "Bingo goal needs 'all' accessibility")
dis("steal", "never", "default", "With bingo goal, stealing should be allowed")
dis("boss", "random", "shuffle", "With bingo goal, bosses need to be on normal or shuffle")
dis("miniboss", "random", "shuffle", "With bingo goal, minibosses need to be on normal or shuffle")
if self.goal == "maze":
req("overworld", "normal", "Maze goal does not work with dungeondive")
req("accessibility", "all", "Maze goal needs 'all' accessibility")
if self.itempool == "pain":
req("heartpiece", True, "Path of pain removes heart pieces")
if self.overworld == "dungeondive":
dis("owlstatues", "overworld", "", "Dungeon dive does not work with owl statues in overworld")
dis("owlstatues", "both", "dungeon", "Dungeon dive does not work with owl statues in overworld")
if (self.itempool == "pain") & (self.owlstatues == ""):
req("accessibility", "goal", "Dungeon dive does not leave enough rupees in itempool for all accessibility")
if self.overworld == "nodungeons":
dis("owlstatues", "dungeon", "", "No dungeons does not work with owl statues in dungeons")
dis("owlstatues", "both", "overworld", "No dungeons does not work with owl statues in dungeons")
if self.overworld == "random":
self.goal = "4" # Force 4 dungeon goal for random overworld right now.
def set(self, value: str) -> None:
if "=" in value:
key, value = value.split("=", 1)
else:
key, value = value, "1"
if key not in self.__by_key:
raise ValueError(f"Setting {key} not found")
self.__by_key[key].set(value)
def toJson(self):
return [s.toJson() for s in self.__all]
def __iter__(self):
return iter(self.__all)