├── Screenshot.png ├── .gitignore ├── Template (Empty).py ├── LICENSE ├── README.md ├── Template (Tutorial).py ├── Template (Amazing Mirror).py ├── classes.py ├── Readme (Tutorial).md ├── gatelib.py └── srm.py /Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mode8fx/SimpleRandomizerMaker/HEAD/Screenshot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.pyc 3 | *.spec 4 | output 5 | randomizer.py 6 | __pycache__ 7 | build 8 | dist 9 | *.bat 10 | randomizer not imported.py 11 | -------------------------------------------------------------------------------- /Template (Empty).py: -------------------------------------------------------------------------------- 1 | # This is a randomizer file for the Simple Randomizer Maker. 2 | # This file must be named randomizer.py in order to work. 3 | # For more information on what each variable means, see "Readme (Tutorial).md" 4 | 5 | from classes import * 6 | 7 | def value(name): 8 | for att in Attributes: 9 | if att.name == name: 10 | return att 11 | print("This attribute does not exist: "+name) 12 | return None 13 | 14 | ######################## 15 | # EDIT BELOW THIS LINE # 16 | ######################## 17 | 18 | Program_Name = "My Randomizer" 19 | Rom_Name = "Rom Name" 20 | Rom_File_Format = "gba" # File format (nes, gba, etc.) 21 | Rom_Hash = None 22 | About_Page_Text = "This is the text that will show on the About page." 23 | Timeout = 10 24 | Slow_Mode = False 25 | 26 | Attributes = [ 27 | 28 | ] 29 | 30 | Required_Rules = [ 31 | 32 | ] 33 | 34 | Optional_Rulesets = [ 35 | 36 | ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 GateGuy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple Randomizer Maker 2 | This is a program that allows you to easily create your own game randomizer with little to no coding. 3 | 4 | ![](https://github.com/GateGuy/SimpleRandomizerMaker/blob/master/Screenshot.png?raw=true) 5 | 6 | ## How It Works 7 | [Video Tutorial](https://www.youtube.com/watch?v=XBnwh5lcSBc) 8 | 9 | All you need are the file addresses of whatever you want to change, along with possible values for those addresses, and the maker will do everything else for you, from the actual randomization to the GUI. You can make address values completely random, or you can set rules that they must follow (like if you want certain values to change according to other values). 10 | 11 | ## Features 12 | - Create a randomizer with (basically) no coding 13 | - Add optional rulesets to make your randomizer more complex (while keeping it easy to develop) 14 | - Utilizes backtracking constraint satisfaction and attribute/rule optimization for fast generation 15 | - Dynamic GUI that changes according to settings and number of options 16 | - Built-in seed support with verification to check for invalid seeds 17 | - Generates up to 20 unique seeds at once 18 | - Supports multi-byte addresses 19 | - Supports games that use multiple files 20 | - Supports CRC32 hash verification 21 | - Optionally generates a text log of all randomized values 22 | - Includes detailed tutorial plus two sample randomizers 23 | 24 | ## Example Functions 25 | I recommend you look at the [included tutorial](https://github.com/GateGuy/SimpleRandomizerMaker/blob/master/Readme%20(Tutorial).md) and templates, but the short version is that this program works through three types of objects: Attributes (the things you want to randomize), Rules (requirements that the randomized values must follow), and Rulesets (sets of Rules that are grouped together). Here are some examples: 26 | ``` 27 | Attribute( 28 | name="My Attribute", 29 | addresses=[0x456, 0xABC], 30 | number_of_bytes=1, 31 | possible_values=[1,4,21,83,106], 32 | min_value=None, 33 | max_value=None, 34 | ), 35 | ``` 36 | ``` 37 | Rule( 38 | description="My Attribute 2 + My Attribute 3 is at least 20", 39 | left_side=value("My Attribute 2") + value("My Attribute 3"), 40 | rule_type=">=", 41 | right_side=20, 42 | ), 43 | ``` 44 | ``` 45 | Rule( 46 | description="My Attribute 1 must be one of these values", 47 | left_side=value("My Attribute 1"), 48 | rule_type="==", 49 | right_side=[1, 4, 6, 13, 18], 50 | ), 51 | ``` 52 | ``` 53 | Rule( 54 | description="These three attributes have to be different", 55 | left_side=[value("My Attribute 1"), value("My Attribute 2"), value("My Attribute 3")], 56 | rule_type="!=", 57 | right_side=None, 58 | ), 59 | ``` 60 | ``` 61 | Rule( 62 | description="My Attribute 1 is 5, or My Attribute 2 is less than 10 (or both)", 63 | left_side=[(value("My Attribute 1"), "==", 5), (value("My Attribute 2"), "<", 10)], 64 | rule_type="count", 65 | right_side=("==", True, ">=", 1), 66 | ), 67 | ``` 68 | 69 | ## Randomizers Made Using SRM 70 | ### Mega Man: The Wily Wars Randomizer 71 | - https://github.com/MaximumLance/Wily-Wars-Randomizer 72 | -------------------------------------------------------------------------------- /Template (Tutorial).py: -------------------------------------------------------------------------------- 1 | # This is a randomizer file for the Simple Randomizer Maker. 2 | # This file must be named randomizer.py in order to work. 3 | # For more information on what each variable means, see "Readme (Tutorial).md" 4 | 5 | from classes import * 6 | 7 | def value(name): 8 | for att in Attributes: 9 | if att.name == name: 10 | return att 11 | print("This attribute does not exist: "+name) 12 | return None 13 | 14 | ######################## 15 | # EDIT BELOW THIS LINE # 16 | ######################## 17 | 18 | Program_Name = "My Randomizer" 19 | Rom_Name = "My Game (USA, Europe) ROM" 20 | Rom_File_Format = "" 21 | Rom_Hash = None 22 | About_Page_Text = "" 23 | Timeout = 10 24 | Slow_Mode = False 25 | 26 | """ 27 | If you're using this file as a template, MAKE SURE YOU DELETE THESE ATTRIBUTES! 28 | """ 29 | Attributes = [ 30 | Attribute( 31 | name="My Attribute 1", 32 | addresses=[0x0123], 33 | number_of_bytes=1, 34 | is_little_endian=False, 35 | possible_values=None, # unused since min_value and max_value are used 36 | min_value=0, 37 | max_value=100, 38 | min_max_interval=1, 39 | lock_if_enabled=None, 40 | lock_unless_enabled=None, 41 | ), 42 | Attribute( 43 | name="My Attribute 2", 44 | addresses=[0x456, 0xABC], 45 | number_of_bytes=1, 46 | is_little_endian=False, 47 | possible_values=[1, 4, 21, 56, 83, 106, 119], 48 | min_value=None, # unused since possible_values is used 49 | max_value=None, # unused since possible_values is used 50 | min_max_interval=None, # unused since possible_values is used 51 | lock_if_enabled=None, 52 | lock_unless_enabled=None, 53 | ), 54 | Attribute( 55 | name="My Attribute 3", 56 | addresses=[0x147, 0x258, 0x369], 57 | number_of_bytes=2, 58 | is_little_endian=False, 59 | possible_values=[0, 25, 50, 75, 100, 125, 150, 175, 200, 225, 250, 275, 300], 60 | min_value=None, # unused since possible_values is used 61 | max_value=None, # unused since possible_values is used 62 | min_max_interval=None, # unused since possible_values is used 63 | lock_if_enabled=None, 64 | lock_unless_enabled=None, 65 | ), 66 | ] 67 | 68 | """ 69 | If you're using this file as a template, MAKE SURE YOU DELETE THESE RULES! 70 | """ 71 | Required_Rules = [ 72 | Rule( 73 | description="My Attribute 1 + My Attribute 2 is less than 150", 74 | left_side=value("My Attribute 1") + value("My Attribute 2"), 75 | rule_type="<", 76 | right_side=150, 77 | ), 78 | Rule( 79 | description="My Attribute 2 + My Attribute 3 is at least 20", 80 | left_side=value("My Attribute 2") + value("My Attribute 3"), 81 | rule_type=">=", 82 | right_side=20, 83 | ), 84 | ] 85 | 86 | """ 87 | If you're using this file as a template, MAKE SURE YOU DELETE THESE RULESETS! 88 | """ 89 | Optional_Rulesets = [ 90 | Ruleset( 91 | name="Basic Rules", 92 | description="A set of basic rules", 93 | rules=[ 94 | Rule( 95 | description="My Attribute 1 and My Attribute 2 are not equal", 96 | left_side=[value("My Attribute 1"), value("My Attribute 2")], 97 | rule_type="!=", 98 | right_side=None, 99 | ), 100 | Rule( 101 | description="My Attribute 1 has more requirements", 102 | left_side=value("My Attribute 1"), 103 | rule_type="<", 104 | right_side=(value("My Attribute 2")+5) - (value("My Attribute 3")/4), 105 | ), 106 | ], 107 | must_be_enabled=None, 108 | must_be_disabled=None, 109 | ), 110 | Ruleset( 111 | name="Advanced Rules", 112 | description="A set of advanced rules", 113 | rules=[ 114 | Rule( 115 | description="The first Attribute is an even number, the other two are odd", 116 | left_side=[(value("My Attribute 1")%2, "==", 0), (value("My Attribute 2")%2, "==", 1), (value("My Attribute 3")%2, "==", 1)], 117 | rule_type="==", 118 | right_side=None, 119 | ), 120 | Rule( 121 | description="The number of attributes that can be less than 70 is at most 2", 122 | left_side=[value("My Attribute 1"), value("My Attribute 2"), value("My Attribute 3")], 123 | rule_type="count", 124 | right_side=("<", 70, "<=", 2), 125 | ), 126 | ], 127 | must_be_enabled=None, 128 | must_be_disabled=None, 129 | ), 130 | Ruleset( 131 | name="Special Ruleset", 132 | description="This can only be enabled if Basic Rules is enabled and Advanced Rules is disabled.", 133 | rules=[], 134 | must_be_enabled=["Basic Rules"], 135 | must_be_disabled=["Advanced Rules"], 136 | ), 137 | ] -------------------------------------------------------------------------------- /Template (Amazing Mirror).py: -------------------------------------------------------------------------------- 1 | # This is a randomizer file for the Simple Randomizer Maker. 2 | # This file must be named randomizer.py in order to work. 3 | # For more information on what each variable means, see "Readme (Tutorial).md" 4 | 5 | from classes import * 6 | 7 | def value(name): 8 | for att in Attributes: 9 | if att.name == name: 10 | return att 11 | print("This attribute does not exist: "+name) 12 | return None 13 | 14 | """ 15 | Ability Values: 16 | 17 | 00 - Nothing 18 | 01 - Fire 19 | 02 - Ice 20 | 03 - Burning 21 | 04 - Wheel 22 | 05 - Parasol 23 | 06 - Cutter 24 | 07 - Beam 25 | 08 - Stone 26 | 09 - Bomb 27 | 0A - Throw 28 | 0B - Sleep 29 | 0C - Cook 30 | 0D - Laser 31 | 0E - UFO 32 | 0F - Spark 33 | 10 - Tornado 34 | 11 - Hammer 35 | 12 - Sword 36 | 13 - Cupid 37 | 14 - Fighter 38 | 15 - Magic 39 | 16 - Smash 40 | 17 - Mini 41 | 18 - Crash 42 | 19 - Missile 43 | 1A - Master 44 | 45 | The remaining values are either some sort of bug/crash, mix (like when you inhale two abilities at one), or duplicate. 46 | """ 47 | 48 | ######################## 49 | # EDIT BELOW THIS LINE # 50 | ######################## 51 | 52 | Program_Name = "Amazing Mirror Randomizer" 53 | Rom_Name = "Kirby & The Amazing Mirror (USA)" 54 | Rom_File_Format = "gba" 55 | Rom_Hash = "9f2a3048" 56 | About_Page_Text = "This is a sample randomizer for Kirby & The Amazing Mirror that changes the abilities given by a few enemies (specifically, the ones in the first area of the game)." 57 | Timeout = 10 58 | Slow_Mode = False 59 | 60 | Attributes = [ 61 | Attribute( 62 | name="Waddle Dee", 63 | addresses=[0x35164E, 0x351B76], 64 | number_of_bytes=1, 65 | is_little_endian=False, 66 | possible_values=None, # unused since min_value and max_value are used 67 | min_value=0, 68 | max_value=26, 69 | min_max_interval=1, 70 | lock_if_enabled=None, 71 | lock_unless_enabled=None, 72 | ), 73 | Attribute( 74 | name="Droppy", 75 | addresses=[0x351AFE, 0x3527D6], 76 | number_of_bytes=1, 77 | is_little_endian=False, 78 | possible_values=None, # unused since min_value and max_value are used 79 | min_value=0, 80 | max_value=26, 81 | min_max_interval=1, 82 | lock_if_enabled=None, 83 | lock_unless_enabled=None, 84 | ), 85 | Attribute( 86 | name="Leap", 87 | addresses=[0x3517B6], 88 | number_of_bytes=1, 89 | is_little_endian=False, 90 | possible_values=None, # unused since min_value and max_value are used 91 | min_value=0, 92 | max_value=26, 93 | min_max_interval=1, 94 | lock_if_enabled=None, 95 | lock_unless_enabled=None, 96 | ), 97 | Attribute( 98 | name="Big Waddle Dee", 99 | addresses=[0x3517E6], 100 | number_of_bytes=1, 101 | is_little_endian=False, 102 | possible_values=None, # unused since min_value and max_value are used 103 | min_value=0, 104 | max_value=26, 105 | min_max_interval=1, 106 | lock_if_enabled=None, 107 | lock_unless_enabled=None, 108 | ), 109 | Attribute( 110 | name="Flamer", 111 | addresses=[0x351816], 112 | number_of_bytes=1, 113 | is_little_endian=False, 114 | possible_values=None, # unused since min_value and max_value are used 115 | min_value=0, 116 | max_value=26, 117 | min_max_interval=1, 118 | lock_if_enabled=None, 119 | lock_unless_enabled=None, 120 | ), 121 | Attribute( 122 | name="Sword Knight", 123 | addresses=[0x3518BE], 124 | number_of_bytes=1, 125 | is_little_endian=False, 126 | possible_values=None, # unused since min_value and max_value are used 127 | min_value=0, 128 | max_value=26, 129 | min_max_interval=1, 130 | lock_if_enabled=None, 131 | lock_unless_enabled=None, 132 | ), 133 | Attribute( 134 | name="Cupie", 135 | addresses=[0x35176E], 136 | number_of_bytes=1, 137 | is_little_endian=False, 138 | possible_values=None, # unused since min_value and max_value are used 139 | min_value=0, 140 | max_value=26, 141 | min_max_interval=1, 142 | lock_if_enabled=None, 143 | lock_unless_enabled=None, 144 | ), 145 | ] 146 | 147 | Required_Rules = [ 148 | ] 149 | 150 | Optional_Rulesets = [ 151 | Ruleset( 152 | name="All Unique", 153 | description="All enemies give different abilities.", 154 | rules=[ 155 | Rule( 156 | description="All Unique", 157 | left_side=[value("Waddle Dee"), value("Droppy"), value("Leap"), value("Big Waddle Dee"), value("Flamer"), value("Sword Knight"), value("Cupie")], 158 | rule_type="!=", 159 | right_side=None, 160 | ), 161 | ], 162 | must_be_enabled=None, 163 | must_be_disabled=["All Master", "Smashing!"], 164 | ), 165 | Ruleset( 166 | name="All Master", 167 | description="All enemies give the Master ability (ability #26).", 168 | rules=[ 169 | Rule( 170 | description="Waddle Dee gives Master", 171 | left_side=value("Waddle Dee"), 172 | rule_type="==", 173 | right_side=26, 174 | ), 175 | Rule( 176 | description="All enemies give the same ability (by extension, they all give Master)", 177 | left_side=[value("Waddle Dee"), value("Droppy"), value("Leap"), value("Big Waddle Dee"), value("Flamer"), value("Sword Knight"), value("Cupie")], 178 | rule_type="==", 179 | right_side=None, 180 | ), 181 | ], 182 | must_be_enabled=None, 183 | must_be_disabled=["All Unique", "At Least 1 UFO", "Smashing!"], 184 | ), 185 | Ruleset( 186 | name="All Enemies Give An Ability", 187 | description="All enemies are guaranteed to give an ability.", 188 | rules=[ 189 | Rule( 190 | description="All Enemies Give An Ability", 191 | left_side=[value("Waddle Dee"), value("Droppy"), value("Leap"), value("Big Waddle Dee"), value("Flamer"), value("Sword Knight"), value("Cupie")], 192 | rule_type="count", 193 | right_side=("==", 0, "==", 0), 194 | ), 195 | ], 196 | must_be_enabled=None, 197 | must_be_disabled=None, 198 | ), 199 | Ruleset( 200 | name="At Least 1 UFO", 201 | description="At least one enemy gives the UFO ability (ability #14).", 202 | rules=[ 203 | Rule( 204 | description="At Least 1 UFO", 205 | left_side=[value("Waddle Dee"), value("Droppy"), value("Leap"), value("Big Waddle Dee"), value("Flamer"), value("Sword Knight"), value("Cupie")], 206 | rule_type="count", 207 | right_side=("==", 0x0E, ">=", 1), 208 | ), 209 | ], 210 | must_be_enabled=None, 211 | must_be_disabled=["All Master", "Smashing!"], 212 | ), 213 | Ruleset( 214 | name="All Different From Original", 215 | description="All enemies are guaranteed to give different abilities from what they usually give.", 216 | rules=[ 217 | Rule( 218 | description="Waddle Dee is different", 219 | left_side=value("Waddle Dee"), 220 | rule_type="!=", 221 | right_side=0, 222 | ), 223 | Rule( 224 | description="Droppy is different", 225 | left_side=value("Droppy"), 226 | rule_type="!=", 227 | right_side=0, 228 | ), 229 | Rule( 230 | description="Leap is different", 231 | left_side=value("Leap"), 232 | rule_type="!=", 233 | right_side=0, 234 | ), 235 | Rule( 236 | description="Big Waddle Dee is different", 237 | left_side=value("Big Waddle Dee"), 238 | rule_type="!=", 239 | right_side=0, 240 | ), 241 | Rule( 242 | description="Flamer is different", 243 | left_side=value("Flamer"), 244 | rule_type="!=", 245 | right_side=3, 246 | ), 247 | Rule( 248 | description="Sword Knight is different", 249 | left_side=value("Sword Knight"), 250 | rule_type="!=", 251 | right_side=0x12, 252 | ), 253 | Rule( 254 | description="Cupie is different", 255 | left_side=value("Cupie"), 256 | rule_type="!=", 257 | right_side=0x13, 258 | ), 259 | ], 260 | must_be_enabled=None, 261 | must_be_disabled=None, 262 | ), 263 | Ruleset( 264 | name="Smashing!", 265 | description="All enemies give either Smash, Fighting, Stone, Cutter, or Hammer.", 266 | rules=[ 267 | Rule( 268 | description="Waddle Dee is smashing", 269 | left_side=value("Waddle Dee"), 270 | rule_type="==", 271 | right_side=[0x16, 0x14, 0x8, 0x6, 0x11], 272 | ), 273 | Rule( 274 | description="Droppy is smashing", 275 | left_side=value("Droppy"), 276 | rule_type="==", 277 | right_side=[0x16, 0x14, 0x8, 0x6, 0x11], 278 | ), 279 | Rule( 280 | description="Leap is smashing", 281 | left_side=value("Leap"), 282 | rule_type="==", 283 | right_side=[0x16, 0x14, 0x8, 0x6, 0x11], 284 | ), 285 | Rule( 286 | description="Big Waddle Dee is smashing", 287 | left_side=value("Big Waddle Dee"), 288 | rule_type="==", 289 | right_side=[0x16, 0x14, 0x8, 0x6, 0x11], 290 | ), 291 | Rule( 292 | description="Flamer is smashing", 293 | left_side=value("Flamer"), 294 | rule_type="==", 295 | right_side=[0x16, 0x14, 0x8, 0x6, 0x11], 296 | ), 297 | Rule( 298 | description="Sword Knight is smashing", 299 | left_side=value("Sword Knight"), 300 | rule_type="==", 301 | right_side=[0x16, 0x14, 0x8, 0x6, 0x11], 302 | ), 303 | Rule( 304 | description="Cupie is smashing", 305 | left_side=value("Cupie"), 306 | rule_type="==", 307 | right_side=[0x16, 0x14, 0x8, 0x6, 0x11], 308 | ), 309 | ], 310 | must_be_enabled=None, 311 | must_be_disabled=["All Master", "At Least 1 UFO", "All Unique"], 312 | ), 313 | Ruleset( 314 | name="XOR Statement (move mouse over this)", 315 | description="Either Waddle Dee gives Cutter (ability #6), or Droppy gives Throw (ability #10), but not both/neither.", 316 | rules=[ 317 | Rule( 318 | description="XOR Statement", 319 | left_side=[(value("Waddle Dee"), "==", 6), (value("Droppy"), "==", 10)], 320 | rule_type="count", 321 | right_side=("==", True, "==", 1), 322 | ), 323 | ], 324 | must_be_enabled=None, 325 | must_be_disabled=["All Master"] 326 | ), 327 | ] -------------------------------------------------------------------------------- /classes.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import random 3 | import shutil 4 | import copy 5 | from math import ceil, floor 6 | from time import sleep 7 | import operator 8 | 9 | attributeCounter = 0 10 | ruleCounter = 0 11 | defaultRuleNum = 0 12 | 13 | def setDefaultRuleNum(): 14 | global defaultRuleNum 15 | global ruleCounter 16 | defaultRuleNum = ruleCounter 17 | 18 | def resetRuleCounter(): 19 | global ruleCounter 20 | global defaultRuleNum 21 | ruleCounter = defaultRuleNum 22 | 23 | class Attribute: 24 | def __init__(self, name, addresses, number_of_bytes=None, is_little_endian=False, possible_values=None, min_value=None, max_value=None, min_max_interval=1, lock_if_enabled=None, lock_unless_enabled=None): 25 | self.name = name 26 | if isinstance(addresses, list): 27 | self.addresses = addresses 28 | else: 29 | self.addresses = [addresses] 30 | for i in range(len(self.addresses)): 31 | if not isinstance(addresses[i], tuple): 32 | addresses[i] = (addresses[i], 1) 33 | self.possible_values = possible_values 34 | if possible_values is None or len(possible_values) == 0: 35 | # self.possible_values = list(range(min_value, max_value+1)) 36 | self.possible_values = [] 37 | if min_max_interval is None: 38 | min_max_interval = 1 39 | attArraySize = (max_value - min_value) / min_max_interval * sys.getsizeof(int()) 40 | if attArraySize > (1048576*50): # 50 MB 41 | print("At least one of the provided attributes has a very large number of possible values. This may (or may not) make the program run slow.") 42 | i = min_value 43 | while i <= max_value: 44 | self.possible_values.append(i) 45 | i += min_max_interval 46 | else: 47 | self.possible_values = [v for v in possible_values if (min_value is None or v >= min_value) and (max_value is None or v <= max_value)] 48 | self.default_possible_values = copy.copy(self.possible_values) 49 | if number_of_bytes is None: 50 | self.number_of_bytes = ceil(max(self.possible_values).bit_length() / 8.0) 51 | else: 52 | self.number_of_bytes = number_of_bytes 53 | self.is_little_endian = is_little_endian 54 | if lock_if_enabled is None: 55 | self.lock_if_enabled = [] 56 | elif isinstance(lock_if_enabled, list): 57 | self.lock_if_enabled = lock_if_enabled 58 | else: 59 | self.lock_if_enabled = [lock_if_enabled] 60 | for i in range(len(self.lock_if_enabled)): 61 | if not isinstance(self.lock_if_enabled[i], tuple): 62 | self.lock_if_enabled[i] = tuple([self.lock_if_enabled[i]]) 63 | if lock_unless_enabled is None: 64 | self.lock_unless_enabled = [] 65 | elif isinstance(lock_unless_enabled, list): 66 | self.lock_unless_enabled = lock_unless_enabled 67 | else: 68 | self.lock_unless_enabled = [lock_unless_enabled] 69 | for i in range(len(self.lock_unless_enabled)): 70 | if not isinstance(self.lock_unless_enabled[i], tuple): 71 | self.lock_unless_enabled[i] = tuple([self.lock_unless_enabled[i]]) 72 | 73 | self.index = 0 74 | self.value = self.possible_values[0] 75 | self.specialOperators = [] 76 | self.specialVals = [] 77 | self.rulesOnSpecialOp = [] 78 | global attributeCounter 79 | self.attributeNum = attributeCounter 80 | attributeCounter += 1 81 | def resetToFirstValue(self): 82 | self.index = 0 83 | self.value = self.possible_values[0] 84 | def resetToDefault(self): 85 | self.possible_values = copy.copy(self.default_possible_values) 86 | self.resetToFirstValue() 87 | def prepare(self): 88 | random.shuffle(self.possible_values) 89 | self.resetToFirstValue() 90 | def setToPreviousValue(self): 91 | self.index -= 1 92 | self.value = self.possible_values[self.index] 93 | def setToNextValue(self): 94 | self.index += 1 95 | try: 96 | self.value = self.possible_values[self.index] 97 | return True 98 | except: 99 | self.resetToFirstValue() 100 | return False 101 | # Allows rules to perform dynamic operations on attribute value; no pointers necessary! 102 | def performSpecialOperation(self, ruleNum): 103 | if len(self.specialOperators) == 0: 104 | return self.value 105 | comparedVal = self.value 106 | for i in range(len(self.specialOperators)): 107 | if self.rulesOnSpecialOp[i] == ruleNum: 108 | if self.specialVals[i] is not None: 109 | if not isinstance(self.specialVals[i], Attribute): 110 | func = operator.methodcaller(self.specialOperators[i], comparedVal, self.specialVals[i]) 111 | else: 112 | func = operator.methodcaller(self.specialOperators[i], comparedVal, self.specialVals[i].performSpecialOperation(ruleNum)) 113 | else: 114 | func = operator.methodcaller(self.specialOperators[i], comparedVal) 115 | comparedVal = func(operator) 116 | return comparedVal 117 | # Whenever a calculation (arithmetic, comparison, etc) is attempted on an Attribute, store it for use later. 118 | def addSpecialOperator(self, op, val): 119 | global ruleCounter 120 | 121 | self.specialOperators.append(op) 122 | self.specialVals.append(val) 123 | self.rulesOnSpecialOp.append(ruleCounter) 124 | return self 125 | def duplicateOperations(self, oldRuleNum, newRuleNum): 126 | newSO = copy.copy(self.specialOperators) 127 | newSV = copy.copy(self.specialVals) 128 | newROSO = copy.copy(self.rulesOnSpecialOp) 129 | for i in range(len(self.rulesOnSpecialOp)): 130 | if self.rulesOnSpecialOp[i] == oldRuleNum: 131 | newSO.append(self.specialOperators[i]) 132 | newSV.append(self.specialVals[i]) 133 | newROSO.append(newRuleNum) 134 | self.specialOperators = newSO 135 | self.specialVals = newSV 136 | self.rulesOnSpecialOp = newROSO 137 | def __add__(self, val): 138 | return self.addSpecialOperator("add", val) 139 | def __sub__(self, val): 140 | return self.addSpecialOperator("sub", val) 141 | def __mul__(self, val): 142 | return self.addSpecialOperator("mul", val) 143 | def __floordiv__(self, val): 144 | return self.addSpecialOperator("floordiv", val) 145 | def __truediv__(self, val): 146 | return self.addSpecialOperator("truediv", val) 147 | def __mod__(self, val): 148 | return self.addSpecialOperator("mod", val) 149 | def __pow__(self, val): 150 | return self.addSpecialOperator("pow", val) 151 | def __lshift__(self, val): 152 | return self.addSpecialOperator("lshift", val) 153 | def __rshift__(self, val): 154 | return self.addSpecialOperator("rshift", val) 155 | def __and__(self, val): 156 | return self.addSpecialOperator("and", val) 157 | def __xor__(self, val): 158 | return self.addSpecialOperator("xor", val) 159 | def __or__(self, val): 160 | return self.addSpecialOperator("or", val) 161 | def __neg__(self): 162 | return self.addSpecialOperator("neg", None) 163 | def __pos__(self): 164 | return self.addSpecialOperator("pos", None) 165 | def __abs__(self): 166 | return self.addSpecialOperator("abs", None) 167 | def __invert__(self): 168 | return self.addSpecialOperator("invert", None) 169 | 170 | ruleTypesDict = { 171 | "=" : "eq", 172 | "==" : "eq", 173 | "all equal" : "eq", 174 | "all same" : "eq", 175 | "!=" : "ne", 176 | "all not equal" : "ne", 177 | "all different" : "ne", 178 | ">" : "gt", 179 | ">=" : "ge", 180 | "=>" : "ge", 181 | "<" : "lt", 182 | "<=" : "le", 183 | "=<" : "le", 184 | "in" : "eq", 185 | "is in" : "eq", 186 | "is one of" : "eq", 187 | "not in" : "ne", 188 | "is not in" : "ne", 189 | "is not one of" : "ne", 190 | "count" : "count", 191 | } 192 | 193 | ruleTypesOtherDict = { 194 | "eq" : "==", 195 | "ne" : "!=", 196 | "gt" : ">", 197 | "ge" : ">=", 198 | "lt" : "<", 199 | "le" : "<=", 200 | "count" : "count", 201 | } 202 | 203 | class Rule: 204 | def __init__(self, left_side, rule_type, right_side=None, description="", oldRuleNum=None): 205 | self.description = description 206 | if self.description is None: 207 | self.description = "" 208 | self.left_side = left_side 209 | self.rule_type = ruleTypesDict.get(rule_type.lower()) 210 | if self.rule_type is None: 211 | self.rule_type = rule_type.lower() 212 | self.right_side = right_side 213 | global ruleCounter 214 | self.ruleNum = ruleCounter 215 | if oldRuleNum is not None: 216 | self.left_side = self.handleMidStatementAssertion(self.left_side, oldRuleNum, self.ruleNum) 217 | self.right_side = self.handleMidStatementAssertion(self.right_side, oldRuleNum, self.ruleNum) 218 | ruleCounter += 1 219 | self.relatedAttributes = [] 220 | self.storeRelatedAttributes(self.left_side) 221 | self.storeRelatedAttributes(self.right_side) 222 | def storeRelatedAttributes(self, att): 223 | if isinstance(att, Attribute): 224 | if not att in self.relatedAttributes: 225 | self.relatedAttributes.append(att) 226 | for val in att.specialVals: 227 | self.storeRelatedAttributes(val) 228 | elif isinstance(att, list) or isinstance(att, tuple): 229 | for a in att: 230 | self.storeRelatedAttributes(a) 231 | # Used for OR statements and other mid-statement comparisons/assertions 232 | def handleMidStatementAssertion(self, att, oldRuleNum, newRuleNum): 233 | if isinstance(att, tuple) and isinstance(att[0], Attribute): 234 | att[0].duplicateOperations(oldRuleNum, newRuleNum) 235 | return att[0].addSpecialOperator(ruleTypesDict.get(att[1]), att[2]) 236 | elif isinstance(att, list): 237 | for i in range(len(att)): 238 | att[i] = self.handleMidStatementAssertion(att[i], oldRuleNum, newRuleNum) 239 | return att 240 | def rulePasses(self): 241 | try: 242 | if self.rule_type == "eq" and self.right_side is not None: 243 | left = self.setSide(self.left_side) 244 | right = [] 245 | for att in self.asList(self.right_side): 246 | right.append(self.setSide(att)) 247 | return left in right 248 | elif self.rule_type == "count": 249 | left = self.asList(self.left_side) 250 | newLeft = [] 251 | for i in range(len(left)): 252 | newLeft.append(self.setSide(left[i])) 253 | left = newLeft 254 | if self.right_side[0] in ["=","=="]: 255 | count = sum(map(lambda x : x==self.right_side[1], left)) 256 | elif self.right_side[0] == "!=": 257 | count = sum(map(lambda x : x!=self.right_side[1], left)) 258 | elif self.right_side[0] == ">": 259 | count = sum(map(lambda x : x>self.right_side[1], left)) 260 | elif self.right_side[0] == ">=": 261 | count = sum(map(lambda x : x>=self.right_side[1], left)) 262 | elif self.right_side[0] == "<": 263 | count = sum(map(lambda x : x c", then Left Side is "a + b", Rule Type (see below) is ">", and Right Side (also see below) is "c". 153 | 154 | ##### Rule Type 155 | - The type of comparison. Possible comparisons are: 156 | `"=" (or "=="), "!=", ">", ">=", "<", "<=", and "count"` 157 | - Most of these are self-explanatory, but the "count" comparison lets you count how many attributes fulfill a certain requirement (see EXAMPLE 3 below). 158 | 159 | ##### Right Side 160 | - The right side of the comparison (may be unused, depending on the rule). 161 | 162 | If you choose not to use one of the optional variables, set its value to None 163 | 164 | #### Rule Examples 165 | - EXAMPLE 1: If you want to set a requirement that a Super Potion must cost at least as much as (two Potions + 100), then you would set the following: 166 | ``` 167 | Rule( 168 | left_side=value("Super Poiton"), 169 | rule_type=">=", 170 | right_side=value("Potion")*2+100, 171 | ) 172 | ``` 173 | - EXAMPLE 2: If you want to guarantee that a Potion, Elixir, and Revive all cost the same amount, then you would set the following: 174 | ``` 175 | Rule( 176 | left_side=[value("Potion"), value("Elixir"), value("Revive")], 177 | rule_type="=", 178 | right_side=None, 179 | ) 180 | ``` 181 | - EXAMPLE 3: If you have four stats and you want to guarantee that at most (<=) two of them are greater than (>) 100 each, then you would set the following: 182 | ``` 183 | Rule( 184 | left_side=[value("Attack"), value("Defense"), value("Speed"), value("Magic")], 185 | rule_type = "count", 186 | right_side = (">", 100, "<=", 2), 187 | ) 188 | ``` 189 | The Right Side represents: (value rule type, value, count rule type, count), and it must be in parentheses. 190 | 191 | A code example can be found below. If you're using this file as a template, MAKE SURE YOU DELETE THESE RULES! 192 | ``` 193 | Required_Rules = [ 194 | Rule( 195 | description="My Attribute 1 + My Attribute 2 is less than 150", 196 | left_side=value("My Attribute 1") + value("My Attribute 2"), 197 | rule_type="<", 198 | right_side=150, 199 | ), 200 | Rule( 201 | description="My Attribute 2 + My Attribute 3 is at least 20", 202 | left_side=value("My Attribute 2") + value("My Attribute 3"), 203 | rule_type=">=", 204 | right_side=20, 205 | ), 206 | ] 207 | ``` 208 | ## Ruleset 209 | 210 | The third and final component is simpler than the other two because it's basically a set of rules, fittingly named a Ruleset. 211 | 212 | A Ruleset is a collection of optional Rules that may be enabled or disabled by the user. This is useful if you want to add optional user settings without having to create multiple randomizers. You can create up to 14 Rulesets, not counting the required Rules above. 213 | 214 | Rulesets are stored in an array called optional_rulesets. If you don't want any optional rulesets, keep the array as: 215 | `Optional_Rulesets = []` 216 | 217 | A Ruleset has the following variables: 218 | 219 | ##### Name 220 | - The name of the Ruleset. 221 | 222 | ##### Description 223 | (optional) A description of the ruleset. This is what appears when you move your mouse over the ruleset. 224 | 225 | ##### Rules 226 | - An array of Rules that are applied if the Ruleset is enabled. 227 | 228 | ##### Must Be Enabled 229 | - (optional) An array of Ruleset names. This Ruleset can only be enabled if all of the optional Rulesets in this array are also enabled. 230 | 231 | ##### Must Be Disabled 232 | - (optional) An array of Ruleset names. This Ruleset can only be enabled if all of the optional Rulesets in this array are disabled. 233 | 234 | If you choose not to use one of the optional variables, set its value to None 235 | 236 | #### Ruleset Examples (+ More Rule Examples) 237 | A code example can be found below. If you're using this file as a template, MAKE SURE YOU DELETE THESE RULESETS! 238 | ``` 239 | Optional_Rulesets = [ 240 | Ruleset( 241 | name="Basic Rules", 242 | description="A set of basic rules", 243 | rules=[ 244 | Rule( 245 | description="My Attribute 1 and My Attribute 2 are not equal", 246 | left_side=[value("My Attribute 1"), value("My Attribute 2")], 247 | rule_type="!=", 248 | right_side=None, 249 | ), 250 | Rule( 251 | description="My Attribute 1 has more requirements", 252 | left_side=value("My Attribute 1"), 253 | rule_type="<", 254 | right_side=(value("My Attribute 2")+5) - (value("My Attribute 3")/4), 255 | ), 256 | ], 257 | must_be_enabled=None, 258 | must_be_disabled=None, 259 | ), 260 | Ruleset( 261 | name="Advanced Rules", 262 | description="A set of advanced rules", 263 | rules=[ 264 | Rule( 265 | description="The first Attribute is an even number, the other two are odd", 266 | left_side=[(value("My Attribute 1")%2, "==", 0), (value("My Attribute 2")%2, "==", 1), (value("My Attribute 3")%2, "==", 1)], 267 | rule_type="==", 268 | right_side=None, 269 | ), 270 | Rule( 271 | description="The number of attributes that can be less than 70 is at most 2", 272 | left_side=[value("My Attribute 1"), value("My Attribute 2"), value("My Attribute 3")], 273 | rule_type="count", 274 | right_side=("<", 70, "<=", 2), 275 | ), 276 | ], 277 | ), 278 | Ruleset( 279 | name="Special Ruleset", 280 | description="This can only be enabled if My Ruleset 1 is enabled and My Ruleset 2 is disabled.", 281 | rules=[], 282 | must_be_enabled=["My Ruleset 1"], 283 | must_be_disabled=["My Ruleset 2"], 284 | ), 285 | ] 286 | ``` 287 | ## Tips 288 | 289 | That's everything you need to know to make your own randomizer! But here are a few more tips if you want them: 290 | 291 | ##### Advanced Rules 292 | - Look back at Ruleset #2 in the Rulesets section. If you're creative (and have a little bit of coding experience), you can push the boundaries and come up with some interesting rules. You can add OR (or XOR) statements, check if values are divisible by certain numbers, perform bitwise operations on them, and more. 293 | - See the Amazing Mirror Template for an example of an XOR statement. If you want to make something like an OR or XOR statement, remember that a statement returns 1 or 0 if it's True or False respectively. For example, this statement: 294 | `(value("Attribute 1"), ">", 5)` 295 | ... will be equivalent to 1 if Attribute 1 is greater than 5, or 0 if Attribute 1 is not greater than 5. 296 | 297 | ##### No Unnecessary Arrays 298 | - When inputting addresses, left_side, right_side, must_be_enabled, or must_be_disabled, you don't have to use an array if you are only using one value. 299 | 300 | For example, either of these will work the same: 301 | ``` 302 | addresses = [0x12345] 303 | addresses = 0x12345 304 | ``` 305 | 306 | ... or these: 307 | ``` 308 | left_side = [value("A")] 309 | left_side = value("A") 310 | ``` 311 | 312 | ##### Rule Compression 313 | - For most Rule types (everything except "==" and "count"), you can set the Left Side and Right Side as arrays of multiple values each, and comparisons will be performed on every combination of Left Side and Right Side value. For example, this rule: 314 | ``` 315 | Rule( 316 | description="Several comparisons", 317 | left_side=[value("A"), value("B")], 318 | rule_type=">", 319 | right_side=[value("C"), value("D")], 320 | ) 321 | ``` 322 | ... will check all of (A > C), (A > D), (B > C), and (B > D). Nested rules like this are automatically broken down into smaller rules, so in most situations, you won't have to worry about breaking them down yourself. 323 | 324 | ##### Speed/Timeout 325 | - Optimization algorithms are used to speed up seed generation. But if your randomizer is going too slow, see if you have any "count" rules and consider reworking them into something else; "count" doesn't work as well with optimization. If it's still too slow, see if increasing your Timeout by a few seconds solves it. 326 | 327 | ##### Turning Your Randomizer Into An Executable 328 | - When distributing your newly-created randomizer, all you need to do is package your `randomizer.py` file with a copy of the Simple Randomizer Maker executable. But if you want to combine them into one file instead, you can use a program like PyInstaller to package `srm.py` (the main Simple Randomizer Maker script that the EXE runs) with your randomizer file to make a single executable. If you use PyInstaller, just make sure `srm.py`, `gatelib.py`, `classes.py`, and your `randomizer.py` are all in the same directory, make sure you have SRM's dependencies (and PyInstaller) installed, then open a command window in that directory and run `pyinstaller srm.py --onefile --windowed --name "NAME"`, where NAME is the name of your randomizer. Your executable will be saved as `YOUR DIRECTORY/dist/NAME.exe`. 329 | 330 | ##### No Solution? 331 | - In case you run into a situation where your randomizer gives an error that no possible combination of values was found: Look through your Attributes and Rules again and make sure they can actually generate a solution. That includes making sure two enabled optional rulesets don't conflict with each other. You can also try running the randomizer a few more times; maybe you just got a bad seed. Otherwise, read on. 332 | - Backtracking constraint satisfaction is used to heavily speed up seed generation. By "heavily", I mean "a rule that used to take ~10 hours to apply now takes less than a second". If your randomizer gives an error that no solution was found, you can try setting Slow_Mode to True; this will make the randomizer use brute force calculation instead, which has a *very slight* chance of fixing your problem. But again, make sure your Rules/Attributes can actually generate a solution before attempting this. -------------------------------------------------------------------------------- /gatelib.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | from os import path, mkdir, listdir, rmdir 4 | from getpass import getpass as inputHidden 5 | import math 6 | 7 | ############## 8 | # USER INPUT # 9 | ############## 10 | 11 | """ 12 | Asks the user a question and returns the number of the response. If an invalid answer is given, the question is repeated. 13 | 14 | Parameters 15 | ---------- 16 | question : str 17 | The question that is asked. 18 | choices : list (str) 19 | An array of the different possible answers. 20 | allowMultiple : bool 21 | If True, the user may give multiple answers, each separated by a space. An array of these answers is returned. 22 | 23 | Returns 24 | ------- 25 | If allowMultiple is True: 26 | int 27 | The chosen answer. 28 | Else: 29 | list (int) 30 | An array of ints representing chosen answers. 31 | """ 32 | def makeChoice(question, options, allowMultiple=False): 33 | numChoices = len(options) 34 | if numChoices == 0: 35 | print("Warning: A question was asked with no valid answers. Returning None.") 36 | return None 37 | if numChoices == 1: 38 | print("A question was asked with only one valid answer. Returning this answer.") 39 | return 1 40 | print("\n"+question) 41 | for i in range(numChoices): 42 | print(str(i+1)+": "+options[i]) 43 | cInput = input("\n").split(" ") 44 | if not allowMultiple: 45 | try: 46 | assert len(cInput) == 1 47 | choice = int(cInput[0]) 48 | assert choice > 0 and choice <= numChoices 49 | return choice 50 | except: 51 | print("\nInvalid input.") 52 | return makeChoice(question, options, allowMultiple) 53 | else: 54 | try: 55 | choices = [int(c) for c in cInput] 56 | for choice in choices: 57 | assert choice > 0 and choice <= numChoices 58 | return choices 59 | except: 60 | print("\nInvalid input.") 61 | return makeChoice(question, options, allowMultiple) 62 | 63 | """ 64 | Asks the user a question. The answer can be any number between the given minVal and maxVal. If an invalid answer is given, the question is repeated. 65 | 66 | Parameters 67 | ---------- 68 | question : str 69 | The question that is asked. 70 | minVal : float 71 | The minimum allowed value. 72 | maxVal : float 73 | The maximum allowed value. 74 | 75 | Returns 76 | ------- 77 | float 78 | The given value. 79 | """ 80 | def makeChoiceNumInput(question, minVal, maxVal): 81 | while True: 82 | print("\n"+question) 83 | try: 84 | var = float(input()) 85 | assert minVal <= var <= maxVal 86 | return var 87 | except: 88 | print("Invalid input.") 89 | 90 | ########### 91 | # SEEDING # 92 | ########### 93 | 94 | """ 95 | Encodes an array of variable values into a seed according to a given max value array. 96 | 97 | Parameters 98 | ---------- 99 | varArray : list (int) 100 | The array of values 101 | maxValueArray: 102 | An array of the (number of possible values - 1) of each variable. For example, if you have three variables with the possible values... 103 | var1 : [0, 1, 2, 3] 104 | var2 : [0, 1] 105 | var3 : [0, 1, 2, 3, 4] 106 | ... then the maxValueArray should be [4, 2, 5]. 107 | Note that the maxValueArray's implementation assumes that possible values start at 0 and each increment by 1. For example, if a variable is stated to have 4 possible values, it asusmes those values are [0, 1, 2, 3]. 108 | base : int 109 | Between 2 and 36. The numerical base used by the seed (in other words, how many values are possible for each character, such as 0-9 and a-z). 110 | 111 | Returns 112 | ------- 113 | int 114 | The seed in base-10 numerical form. 115 | str 116 | The seed in the given base. 117 | """ 118 | def encodeSeed(varArray, maxValueArray, base=10): 119 | if base > 36: 120 | print("Base must be between 2 and 36. Lowering to 36.") 121 | base = 36 122 | seed = 0 123 | baseShift = 0 124 | for i in range(len(varArray)): 125 | seed += varArray[i]< 36: 154 | print("Base must be between 2 and 36. Lowering to 36.") 155 | base = 36 156 | elif base < 2: 157 | print("Base must be between 2 and 36. Increasing to 2.") 158 | base = 2 159 | seed = int(seed, base) 160 | baseShift = 0 161 | varArray = [] 162 | for i in range(len(maxValueArray)): 163 | bitLength = maxValueArray[i].bit_length() 164 | varArray.append((seed>>baseShift) & ((2**bitLength)-1)) 165 | baseShift += bitLength 166 | return varArray 167 | 168 | """ 169 | Returns whether or not a seed is possible given a maxValueArray and base. 170 | 171 | Parameters 172 | ---------- 173 | seed : str or int 174 | The seed that will be verified. 175 | maxValueArray: 176 | An array of the (number of possible values - 1) of each variable. For example, if you have three variables with the possible values... 177 | var1 : [0, 1, 2, 3] 178 | var2 : [0, 1] 179 | var3 : [0, 1, 2, 3, 4] 180 | ... then the maxValueArray should be [4, 2, 5]. 181 | Note that the maxValueArray's implementation assumes that possible values start at 0 and each increment by 1. For example, if a variable is stated to have 4 possible values, it asusmes those values are [0, 1, 2, 3]. 182 | base : int 183 | Between 2 and 36. The numerical base used by the seed (in other words, how many values are possible for each character, such as 0-9 and a-z). 184 | 185 | Returns 186 | ------- 187 | bool 188 | Whether or not the seed is valid. 189 | list (int) 190 | An array of variable values decoded from the string. For example, if there are 3 variables, the returned array is [var1's value, var2's value, var3's value] 191 | """ 192 | def verifySeed(seed, maxValueArray, base=10): 193 | if base > 36: 194 | print("Base must be between 2 and 36. Lowering to 36.") 195 | base = 36 196 | elif base < 2: 197 | print("Base must be between 2 and 36. Increasing to 2.") 198 | base = 2 199 | if type(seed) is int: 200 | base = 10 201 | seed = dec_to_base(seed,base) 202 | seed = seed.upper().strip() 203 | 204 | try: 205 | maxSeed = 0 206 | baseShift = 0 207 | for i in range(len(maxValueArray)): 208 | maxSeed += maxValueArray[i]<0: 238 | dig = int(num%base) 239 | if dig<10: 240 | base_num += str(dig) 241 | else: 242 | base_num += chr(ord('A')+dig-10) #Using uppercase letters 243 | num //= base 244 | base_num = base_num[::-1] #To reverse the string 245 | return base_num 246 | 247 | ######################## 248 | # FILE/PATH MANAGEMENT # 249 | ######################## 250 | 251 | """ 252 | Writes a value to a file at a given address. Supports multi-byte addresses. 253 | 254 | Parameters 255 | ---------- 256 | file : str 257 | The file that will be modified. 258 | address : int 259 | The value (ideally, a hex value such as 0x12345) that will be modified. 260 | val : int 261 | The value that will be written to this address. 262 | numBytes : int 263 | The number of bytes that this value will take up. 264 | isLittleEndian : bool 265 | If True, bytes are written in reverse order. 266 | 267 | Returns 268 | ------- 269 | False if the value is too large to be written within the given number of bytes; True otherwise. 270 | 271 | Examples 272 | -------- 273 | Example 1 274 | writeToAddress(file.exe, 0x12345, 0x41, 1, False) will write the following value: 275 | 0x12345 = 41 276 | Example 2 277 | writeToAddress(file.exe, 0x12345, 0x6D18, 2, False) will write the following values: 278 | 0x12345 = 6D 279 | 0x12346 = 18 280 | Example 3 281 | writeToAddress(file.exe, 0x12345, 0x1C, 2, False) will write the following values: 282 | 0x12345 = 00 283 | 0x12346 = 1C 284 | Example 4 285 | writeToAddress(file.exe, 0x12345, 0x003119, 3, True) will write the following values: 286 | 0x12345 = 19 287 | 0x12346 = 31 288 | 0x12347 = 00 289 | """ 290 | def writeToAddress(file, address, val, numBytes=1, isLittleEndian=False): 291 | if val.bit_length() > numBytes*8: 292 | print("Given value is greater than "+str(numBytes)+" bytes.") 293 | return False 294 | if not isLittleEndian: 295 | address += (numBytes-1) 296 | increment = -1 297 | else: 298 | increment = 1 299 | for i in range(numBytes): 300 | file.seek(address) 301 | currByte = val & 0xFF 302 | file.write(bytes([currByte])) 303 | address += increment 304 | val = val>>8 305 | return True 306 | 307 | """ 308 | Swaps the endianness (byte order) of a number. 309 | 310 | Parameters 311 | ---------- 312 | num : int 313 | The number whose order will be swapped. 314 | numBytes : int 315 | The number of bytes that this number takes up. 316 | 317 | Returns 318 | ------- 319 | The modified number with swapped endianness. 320 | 321 | Example 322 | ------- 323 | swapEndianness(0x012345) will return: 0x452301 324 | """ 325 | def swapEndianness(num, numBytes): 326 | num2 = 0 327 | for i in range(1, numBytes + 1): 328 | num2 += (num>>(8*(i-1)) & 0xFF)*(256**(numBytes - i)) 329 | return num2 330 | 331 | """ 332 | From https://gist.github.com/jacobtomlinson/9031697 333 | 334 | Removes all empty folders, including nested empty folders, in a directory. 335 | 336 | Parameters 337 | ---------- 338 | p : str 339 | The path of the starting directory; all empty folders that are children (or grandchildren, etc) of this directory are removed. 340 | """ 341 | def removeEmptyFolders(p): 342 | if not path.isdir(p): 343 | return 344 | files = listdir(p) 345 | if len(files): 346 | for f in files: 347 | fullpath = path.join(p, f) 348 | if path.isdir(fullpath): 349 | removeEmptyFolders(fullpath) 350 | files = listdir(p) 351 | if len(files) == 0: 352 | rmdir(p) 353 | 354 | """ 355 | Returns an array of the individual components of a given path. 356 | 357 | Parameters 358 | ---------- 359 | p : str 360 | The path. 361 | 362 | Returns 363 | ------- 364 | list (str) 365 | The path array. 366 | 367 | Example 368 | ------- 369 | Input 370 | "C:/early folder/test2/thing.exe" 371 | Output 372 | ["C:", "early folder", "test2", "thing.exe"] 373 | """ 374 | def getPathArray(p): 375 | p1, p2 = path.split(p) 376 | if p2 == "": 377 | p = p1 378 | pathArray = [] 379 | while True: 380 | p1, p2 = path.split(p) 381 | pathArray = [p2] + pathArray 382 | if p2 == "": 383 | pathArray = [p1] + pathArray 384 | try: 385 | while pathArray[0] == "": 386 | del pathArray[0] 387 | except: 388 | pass 389 | return pathArray 390 | p = p1 391 | 392 | """ 393 | Creates the given directory. Unlike mkdir, this will also create any necessary parent directories that do not already exist. 394 | 395 | Parameters 396 | ---------- 397 | p : str 398 | The path of the folder that will be created. 399 | 400 | Returns 401 | ------- 402 | True if the folder was created, False if it already exists. 403 | """ 404 | def createDir(p): 405 | if path.isdir(p): 406 | return False 407 | pathArray = getPathArray(p) 408 | currPath = pathArray[0] 409 | for i in range(1, len(pathArray)): 410 | currPath = path.join(currPath, pathArray[i]) 411 | if not path.isdir(currPath): 412 | mkdir(currPath) 413 | return True 414 | 415 | """ 416 | Returns the directory containing the current program, regardless of whether it is a standalone script or a wrapped executable. 417 | 418 | Returns 419 | ------- 420 | str 421 | The directory containing the current program. 422 | """ 423 | def getCurrFolder(): 424 | if getattr(sys, 'frozen', False): 425 | mainFolder = path.dirname(sys.executable) # EXE (executable) file 426 | else: 427 | mainFolder = path.dirname(path.realpath(__file__)) # PY (source) file 428 | sys.path.append(mainFolder) 429 | return mainFolder 430 | 431 | """ 432 | Returns the file extension (including the ".") of the first file found in the given folder that matches the given file name. 433 | 434 | Parameters 435 | ---------- 436 | folder : str 437 | The given folder. 438 | fileName : str 439 | The given file name. 440 | 441 | Returns 442 | ------- 443 | str 444 | The file extension (including the ".") of the first file found in folder named fileName (with any extension); if no file with that name is found, return an empty string. 445 | """ 446 | def getFileExt(folder, fileName): 447 | for f in listdir(folder): 448 | fName, fExt = path.splitext(f) 449 | if fName == fileName: 450 | return fExt 451 | return "" 452 | 453 | """ 454 | From https://stackoverflow.com/questions/1392413/calculating-a-directorys-size-using-python 455 | 456 | Returns the total number of bytes taken up by the given directory and its subdirectories. 457 | 458 | Parameters 459 | ---------- 460 | startPath : str 461 | The given directory. 462 | 463 | Returns 464 | ------- 465 | int 466 | The number of bytes taken up by the directory. 467 | """ 468 | def getDirSize(startPath = '.'): 469 | totalSize = 0 470 | for dirpath, dirnames, filenames in os.walk(startPath): 471 | for f in filenames: 472 | fp = os.path.join(dirpath, f) 473 | # skip if it is symbolic link 474 | if not os.path.islink(fp): 475 | totalSize += os.path.getsize(fp) 476 | return totalSize 477 | 478 | #################### 479 | # ARRAY MANAGEMENT # 480 | #################### 481 | 482 | """ 483 | Returns the number of elements (including duplicates) that exist in two different given arrays. 484 | 485 | Parameters 486 | ---------- 487 | arr1 : list 488 | The first array. 489 | arr2 : list 490 | The second array. 491 | 492 | Returns 493 | ------- 494 | int 495 | The number of elements in the overlap 496 | """ 497 | def arrayOverlap(arr1, arr2): 498 | count = 0 499 | for a in arr1: 500 | if a in arr2: 501 | count += 1 502 | return count 503 | 504 | """ 505 | Merges a nested array into a single one-dimensional array. 506 | 507 | Parameters 508 | ---------- 509 | arr : list 510 | The nested array that will be merged. 511 | finalArr : list (str) 512 | Should be ignored (only used in recursion). The created array so far. 513 | 514 | Returns 515 | ------- 516 | list (str): 517 | The merged array. 518 | 519 | Example 520 | ------- 521 | Input 522 | [item1, [item2, item3], item4, [item 5, [item6, item7], item8]] 523 | Output 524 | [item1, item2, item3, item4, item5, item6, item7, item8] 525 | """ 526 | def mergeNestedArray(arr, finalArr=[]): 527 | for val in arr: 528 | if not isinstance(val, list): 529 | finalArr.append(val) 530 | else: 531 | finalArr = mergeNestedArray(val, finalArr) 532 | return finalArr 533 | 534 | """ 535 | From https://www.geeksforgeeks.org/python-find-most-frequent-element-in-a-list/ 536 | 537 | Returns the most common element in a list, along with how many times it occurrs. 538 | 539 | Parameters 540 | ---------- 541 | arr : list 542 | The array. 543 | 544 | Returns 545 | ------- 546 | anything 547 | The most frequently-occurring element. 548 | int 549 | How many instances of this element there are in the array. 550 | """ 551 | def most_frequent(arr): 552 | counter = 0 553 | elem = arr[0] 554 | for i in arr: 555 | curr_frequency = arr.count(i) 556 | if (curr_frequency > counter): 557 | counter = curr_frequency 558 | elem = i 559 | return elem, counter 560 | 561 | """ 562 | Returns whether or not arr1 is an ordered subset of arr2. 563 | 564 | Parameters 565 | ---------- 566 | arr1 : list 567 | The first array. 568 | arr2: list 569 | The second array. 570 | 571 | Returns 572 | ------- 573 | bool 574 | Whether or not arr1 is an ordered subset of arr2. 575 | 576 | Examples 577 | -------- 578 | Input 1 579 | [3, 5], [1, 3, 5, 7, 9] 580 | Output 1 581 | True 582 | Input 2 583 | [3, 5], [1, 2, 3, 4, 5, 6, 7] 584 | Output 2 585 | False 586 | """ 587 | def arrayInArray(arr1, arr2): 588 | for i in range(len(arr2)-len(arr1)+1): 589 | passed = True 590 | for j in range(len(arr1)): 591 | if arr1[j] != arr2[i+j]: 592 | passed = False 593 | break 594 | if passed: 595 | return True 596 | return False 597 | 598 | ############################### 599 | # CONSOLE/TERMINAL MANAGEMENT # 600 | ############################### 601 | 602 | """ 603 | Clears the console screen. 604 | """ 605 | def clearScreen(): 606 | os.system('clear' if os.name =='posix' else 'cls') 607 | 608 | """ 609 | From https://www.quora.com/How-can-I-delete-the-last-printed-line-in-Python-language 610 | 611 | Clears ("backspaces") the last n console lines. 612 | 613 | PARAMETERS 614 | ---------- 615 | n : int 616 | The number of lines to clear. 617 | """ 618 | def delete_last_lines(n=1): 619 | for _ in range(n): 620 | sys.stdout.write('\x1b[1A') 621 | sys.stdout.write('\x1b[2K') 622 | 623 | ####################### 624 | # STRING MANIPULATION # 625 | ####################### 626 | 627 | """ 628 | Prints a title surrounded by a certain character. 629 | 630 | Parameters 631 | ---------- 632 | string : str 633 | The string that is printed. 634 | char : str 635 | The one-character string that surrounds the string. 636 | 637 | Example 638 | ------- 639 | Input 640 | "MY TITLE", "#" 641 | Output 642 | ############ 643 | # MY TITLE # 644 | ############ 645 | """ 646 | def printTitle(string, topBottomChar="#", sideChar="#", cornerChar="#"): 647 | topBottom = cornerChar+(topBottomChar*(len(string)+2))+cornerChar 648 | print(topBottom) 649 | print(sideChar+" "+string+" "+sideChar) 650 | print(topBottom) 651 | 652 | """ 653 | Returns the base string with either the singular or plural suffix depending on the value of num. 654 | 655 | Parameters 656 | ---------- 657 | base : str 658 | The base of the word. 659 | num : int 660 | The quantity of the desired word. 661 | singularSuffix : str 662 | The suffix of the word's singular form 663 | pluralSuffix : str 664 | The suffix of the word's plural form 665 | 666 | Returns 667 | ------- 668 | str 669 | The resulting string 670 | 671 | Examples 672 | -------- 673 | Input 1 674 | pluralize("ind", 1, "ex", "ices") 675 | Output 1 676 | "index" 677 | Input 2 678 | pluralize("ind", 2, "ex", "ices") 679 | Output 2 680 | "indices" 681 | 682 | """ 683 | def pluralize(base, num, singularSuffix="", pluralSuffix="s"): 684 | return base+singularSuffix if num == 1 else base+pluralSuffix 685 | 686 | """ 687 | Creates a copy of a given string, automatically adding line breaks and indenting lines, without splitting any words in two. 688 | A line's length will only exceed the given limit if a single word in the string exceeds it. 689 | 690 | Parameters 691 | ---------- 692 | string : str 693 | The string to be printed. 694 | lineLength : int 695 | The max length of each printed line. 696 | firstLineIndent : str 697 | The start of the first line. 698 | lineIndent : str 699 | The start of all subsequent lines. 700 | 701 | Returns 702 | ------- 703 | The output string. 704 | 705 | Examples 706 | -------- 707 | Input 1 708 | limitedString("Strong Bad's test sentence is as follows: The fish was delish, and it made quite a dish.", 40, "? ", ". ! ") 709 | Output 1 710 | "? Strong Bad's test sentence is as\n. ! follows: The fish was delish, and it\n. ! made quite a dish." 711 | (Which would look like the following when printed): 712 | ? Strong Bad's test sentence is as 713 | . ! follows: The fish was delish, and it 714 | . ! made quite a dish. 715 | Input 2 716 | limitedString("THIS_WORD_IS_VERY_LONG there", 15, "", "") 717 | Output 2: 718 | "THIS_WORD_IS_VERY_LONG\nthere" 719 | (Which would look like the following when printed): 720 | THIS_WORD_IS_VERY_LONG 721 | there 722 | """ 723 | def limitedString(string, lineLength=80, firstLineIndent="", lineIndent=" "): 724 | printArray = string.split(" ") 725 | totalString = "" 726 | currString = firstLineIndent 727 | isStartOfLine = True 728 | while len(printArray) > 0: 729 | if isStartOfLine or (len(printArray[0]) + (not isStartOfLine) <= lineLength - len(currString)): 730 | currString += (" " if not isStartOfLine else "")+printArray.pop(0) 731 | isStartOfLine = False 732 | else: 733 | totalString += currString+"\n" 734 | currString = lineIndent 735 | isStartOfLine = True 736 | totalString += currString 737 | return totalString 738 | 739 | """ 740 | Shortens a string to a maximum length, padding the last few characters with a given character if necessary. 741 | You have the option of whether or not the string can cut off mid-word. 742 | 743 | Parameters 744 | ---------- 745 | string : str 746 | The string to be shortened. 747 | maxLength : int 748 | The maximum length of the output. 749 | suffixChar : str 750 | The character that will pad a long string 751 | suffixLength : int 752 | The length of the padding 753 | cutoff : bool 754 | If True, the string can be cut mid-word; else, it will be cut at the end of the previous word. 755 | 756 | Returns 757 | ------- 758 | The (possibly) shortened string, with spaces stripped from the right side of the pre-padded output. 759 | 760 | Examples 761 | -------- 762 | Input 1 763 | shorten("this string is too long", 20, '.', 3, True) 764 | Output 1 765 | "This string is to..." 766 | Input 2 767 | shorten("this string is too long", 20, '.', 3, False) 768 | Output 2 769 | "This string is..." 770 | Input 3 771 | shorten("this is short", 15, '.', 3, True) 772 | Output 3 773 | "this is short" 774 | """ 775 | def shorten(string, maxLength=10, suffixChar='.', suffixLength=3, cutoff=True): 776 | if len(string) <= maxLength: 777 | return string 778 | if cutoff: 779 | return string[:(maxLength-suffixLength)].rstrip()+(suffixChar*suffixLength) 780 | shortened = string.rstrip() 781 | while len(shortened) > maxLength-suffixLength: 782 | shortened = " ".join(shortened.split(" ")[:-1]).rstrip() 783 | return shortened+(suffixChar*suffixLength) 784 | 785 | """ 786 | Splits a string into multiple parts, with each part being about equal in length, and no words cut off in the middle. 787 | 788 | Parameters 789 | ---------- 790 | string : str 791 | The string to be split. 792 | numParts : int 793 | The number of parts to split the string into. 794 | reverse : bool 795 | Decide if the last part (False) or first part (True) is likely to be the longest part. 796 | 797 | Returns 798 | ------- 799 | list 800 | The split string. 801 | 802 | Examples 803 | -------- 804 | Input 1 805 | splitStringIntoParts("This string is split into three whole parts", 3, True) 806 | Output 1 807 | ['This string is split', 'into three', 'whole parts'] 808 | Input 2 809 | splitStringIntoParts("This string is split into three whole parts", 3, False) 810 | Output 2 811 | ['This string', 'is split into', 'three whole parts'] 812 | """ 813 | def splitStringIntoParts(string, numParts=2, reverse=False): 814 | totalLen = len(string) - (numParts-1) 815 | maxSubStringLength = math.ceil(totalLen/numParts) 816 | stringArray = string.split(" ") 817 | if reverse: 818 | stringArray.reverse() 819 | splitArray = [] 820 | currString = "" 821 | offset = 0 822 | while len(stringArray) > 0: 823 | if len(currString) + (currString != "") + len(stringArray[0]) < maxSubStringLength + offset: 824 | currString += (" " if currString != "" else "")+stringArray.pop(0) 825 | else: 826 | offset = (maxSubStringLength + offset) - (len(currString) + (currString != "")) 827 | splitArray.append(currString) 828 | currString = "" 829 | splitArray[-1] += " "+currString 830 | if reverse: 831 | newSplitArray = [] 832 | while len(splitArray) > 0: 833 | curr = splitArray.pop(-1).split(" ") 834 | curr.reverse() 835 | curr = " ".join(curr) 836 | newSplitArray.append(curr) 837 | return newSplitArray 838 | return splitArray 839 | 840 | """ 841 | Returns a string indicating the input number of bytes in its most significant form, rounding up to the indicated number of decimal places. 842 | For example, if numBytes is at least 1 MB but less than 1 GB, it will be displayed in MB. 843 | 844 | Parameters 845 | ---------- 846 | numBytes : int 847 | The number of bytes. 848 | decimalPlaces : int 849 | The number of decimal places to round to. 850 | 851 | Returns 852 | ------- 853 | str 854 | The number of the most significant data size, along with the data size itself. 855 | 856 | Examples 857 | -------- 858 | Input 1 859 | 5000000, 3 860 | Output 1 861 | 4.769 MB 862 | Input 2 863 | 2048, 1 864 | Output 2 865 | 2 KB 866 | Input 3 867 | 2049, 1 868 | Output 3 869 | 2.1 KB 870 | """ 871 | def simplifyNumBytes(numBytes, decimalPlaces=2): 872 | numBytes = float(numBytes) 873 | byteTypeArray = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] 874 | temp = (10.0**decimalPlaces) 875 | for byteType in byteTypeArray: 876 | if numBytes < 1024: 877 | num = math.ceil(numBytes * temp) / temp 878 | if num == int(num): 879 | num = int(num) 880 | return str(num)+" "+byteType 881 | numBytes /= 1024.0 882 | numBytes *= 1024 883 | num = math.ceil(numBytes * temp) / temp 884 | if num == int(num): 885 | num = int(num) 886 | return str(num)+" YB" 887 | 888 | ######### 889 | # OTHER # 890 | ######### 891 | 892 | """ 893 | Returns an array of a given number of values spaced out by another given value, with offset as the average. 894 | Optionally, a fixed number of decimal places can be defined (to fix float rounding issues). 895 | 896 | Parameters 897 | ---------- 898 | numValues : int 899 | The number of values to be spaced out. 900 | spaceSize : float 901 | The size of the space. 902 | offset : float 903 | The average of all values. 904 | 905 | Returns 906 | ------- 907 | array 908 | The spaced out array. 909 | 910 | Examples 911 | -------- 912 | Input 1 913 | 3, 10, 0 914 | Output 1 915 | [-10, 0, 10] 916 | Input 2 917 | 4, 8, 0 918 | Output 2 919 | [-12, -4, 4, 12] 920 | Input 3 921 | 3, 10, 2 922 | Output 3 923 | [-8, 2, 12] 924 | """ 925 | def spaceOut(numValues, spaceSize, offset=0, numDecimalPlaces=None): 926 | evenNumOfValues = (numValues%2 == 0) 927 | if numDecimalPlaces is None: 928 | return [spaceSize*(i-math.floor(numValues/2) + (0.5 if evenNumOfValues else 0)) + offset for i in range(numValues)] 929 | array = [] 930 | for i in range(numValues): 931 | array.append(round(spaceSize*(i-math.floor(numValues/2) + (0.5 if evenNumOfValues else 0)) + offset, numDecimalPlaces)) 932 | return array 933 | 934 | """ 935 | SOURCES 936 | 937 | dec_to_base 938 | https://www.codespeedy.com/inter-convert-decimal-and-any-base-using-python/ 939 | 940 | removeEmptyFolders 941 | https://gist.github.com/jacobtomlinson/9031697 942 | 943 | getDirSize 944 | https://stackoverflow.com/questions/1392413/calculating-a-directorys-size-using-python 945 | 946 | most_frequent 947 | https://www.geeksforgeeks.org/python-find-most-frequent-element-in-a-list/ 948 | 949 | delete_last_lines 950 | https://www.quora.com/How-can-I-delete-the-last-printed-line-in-Python-language 951 | 952 | All other functions made by GateGuy 953 | """ 954 | -------------------------------------------------------------------------------- /srm.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from os import path, remove, mkdir 3 | import shutil 4 | from time import time 5 | from gatelib import * 6 | import binascii 7 | 8 | # the same folder where this program is stored 9 | if getattr(sys, 'frozen', False): 10 | mainFolder = path.dirname(sys.executable) # EXE (executable) file 11 | else: 12 | mainFolder = path.dirname(path.realpath(__file__)) # PY (source) file 13 | sys.path.append(mainFolder) 14 | outputFolder = path.join(mainFolder, "output") 15 | 16 | # GUI imports 17 | try: 18 | import Tkinter as tk 19 | from Tkinter.filedialog import askopenfilename 20 | from Tkinter import font as tkFont 21 | from Tkinter.messagebox import showinfo, showerror 22 | except ImportError: 23 | import tkinter as tk 24 | from tkinter.filedialog import askopenfilename 25 | from tkinter import font as tkFont 26 | from tkinter.messagebox import showinfo, showerror 27 | try: 28 | import ttk 29 | py3 = False 30 | except ImportError: 31 | import tkinter.ttk as ttk 32 | py3 = True 33 | 34 | import classes 35 | try: 36 | from randomizer import * 37 | except: 38 | tk.Tk().withdraw() 39 | showerror("Randomizer Not Found", "Valid randomizer file not found. Make sure it is named \"randomizer.py\" and does not contain any errors.") 40 | sys.exit() 41 | 42 | stringLen = 5+ceil(len(Optional_Rulesets)/5.0) 43 | 44 | timedOut = False 45 | numAllCombinations = 1 46 | currNumCombinations = 0 47 | currRomIndex = 0 48 | currRulesetPage = 0 49 | 50 | def main(): 51 | initRomInfoVars() 52 | setDefaultRuleNum() 53 | vp_start_gui() 54 | 55 | def initRomInfoVars(): 56 | global Rom_Name, Rom_File_Format, Rom_Hash 57 | 58 | try: 59 | if not isinstance(Rom_Name, list): 60 | Rom_Name = [Rom_Name] 61 | except: 62 | Rom_Name = ["ROM"] 63 | try: 64 | if not isinstance(Rom_File_Format, list): 65 | Rom_File_Format = [Rom_File_Format] 66 | except: 67 | Rom_File_Format = [""] 68 | try: 69 | if not isinstance(Rom_Hash, list): 70 | Rom_Hash = [Rom_Hash] 71 | except: 72 | Rom_Hash = [None] 73 | 74 | # The main randomize function. 75 | def randomize(): 76 | global sourceRoms 77 | global currSeed, seedString 78 | global endTime 79 | global numAllCombinations, currNumCombinations 80 | global Attributes, originalAttributes 81 | 82 | for sr in sourceRoms: 83 | if not path.isfile(sr.get()): 84 | return (False, "Invalid ROM input.") 85 | 86 | numOfSeeds = int(numSeeds.get()) 87 | numSeedsGenerated = 0 88 | for seedNum in range(numOfSeeds): 89 | originalAttributes = copy.copy(Attributes) 90 | if useSeed.get() == "1": 91 | seedString = seedInput.get() 92 | try: 93 | assert len(seedString) == stringLen 94 | assert verifySeed(seedString[:-5], [1]*len(optionalRulesetsList), 36) 95 | except: 96 | print("Invalid seed.") 97 | return (False, "Invalid seed.") 98 | decodedSeedVals = decodeSeed(seedString[:-5], [1]*len(optionalRulesetsList), 36) 99 | for i in range(len(optionalRulesetsList)): 100 | optionalRulesetsList[i] = (optionalRulesetsList[i][0], decodedSeedVals[i]) 101 | currSeed = int(seedString, 36) 102 | else: 103 | varArray = [] 104 | maxValueArray = [] 105 | for ruleset in optionalRulesetsList: 106 | varArray.append(ruleset[1]) 107 | maxValueArray.append(1) 108 | settingsSeed = encodeSeed(varArray, maxValueArray, 36)[0] 109 | maxVal = int("ZZZZZ", 36) 110 | genSeed = random.randint(0, maxVal) 111 | currSeed = (settingsSeed*(maxVal+1)) + genSeed 112 | seedString = str(dec_to_base(currSeed, 36)).upper().zfill(stringLen) 113 | myRules = copy.copy(Required_Rules) 114 | for ruleset in optionalRulesetsList: 115 | if ruleset[1] == 1: 116 | for rule in getFromListByName(Optional_Rulesets, ruleset[0]).rules: 117 | myRules.append(rule) 118 | enabledRulesetsByName = [ruleset[0] for ruleset in optionalRulesetsList if ruleset[1] == 1] 119 | for att in Attributes: 120 | lockFlag = False 121 | for rulesetGroup in att.lock_if_enabled: 122 | if arrayOverlap(rulesetGroup, enabledRulesetsByName) == len(rulesetGroup): 123 | lockFlag = True 124 | break 125 | if not lockFlag: 126 | if len(att.lock_unless_enabled) > 0: 127 | lockFlag = True 128 | for rulesetGroup in att.lock_unless_enabled: 129 | if arrayOverlap(rulesetGroup, enabledRulesetsByName) == len(rulesetGroup): 130 | lockFlag = False 131 | break 132 | if lockFlag: 133 | with open(sourceRoms[att.addresses[0][1] - 1].get(), "rb") as tempFile: 134 | tempFile.seek(att.addresses[0][0]) 135 | defaultValue = 0 136 | for i in range(att.number_of_bytes): 137 | defaultValue += ord(tempFile.read(1)) * (256**i) # little endian by default 138 | if not att.is_little_endian: 139 | defaultValue = swapEndianness(defaultValue, att.number_of_bytes) 140 | if not (defaultValue in att.possible_values): 141 | att.possible_values.append(defaultValue) 142 | myRules.append(Rule( 143 | description="Lock: "+att.name, 144 | left_side=value(att.name), 145 | rule_type="==", 146 | right_side=defaultValue 147 | )) 148 | startTime = time() 149 | endTime = startTime + Timeout 150 | random.seed(currSeed) 151 | random.shuffle(Attributes) 152 | simplifiedRules = [] 153 | for rule in myRules: 154 | rule.simplifyRule(simplifiedRules) 155 | myRules = simplifiedRules 156 | myRules.sort(key=getNumCombinations) 157 | setNumAllCombinations() 158 | try: 159 | result = optimizeAttributes(myRules) 160 | except: 161 | errorMessage = "At least one of your rules is bad (has no possible solution)." 162 | print(errorMessage) 163 | resetAttributesAndSeed() 164 | return (False, errorMessage) 165 | if not result: 166 | errorMessage = "The program timed out (seed generation took longer than "+str(Timeout)+" seconds)." 167 | # \n\nEstimated time for current combination of rules: unknown." 168 | print(errorMessage) 169 | resetAttributesAndSeed() 170 | return (False, errorMessage) 171 | # initialize Attributes 172 | shuffleAllAttributes() 173 | for rule in myRules: 174 | rule.relatedAttributes.sort(key=getShuffledAttributeNum) 175 | print("Generating values...") 176 | if not (shotgunApproach(myRules) 177 | or ((not Slow_Mode) and enforceRulesetBacktracking(myRules)) 178 | or (Slow_Mode and enforceRulesetBruteForce(myRules))): 179 | errorMessage = "" 180 | if timedOut: 181 | errorMessage = "The program timed out (seed generation took longer than "+str(Timeout)+" seconds)." 182 | # The next line only works for brute force, not backtracking 183 | # \n\nEstimated time for current combination of rules given your computer's speed: up to "+str(round(numAllCombinations*Timeout/currNumCombinations, 1))+" seconds." 184 | elif useSeed.get() == "1": 185 | errorMessage = "Invalid seed." 186 | else: 187 | errorMessage = "No combination of values satisfies the given combination of rules. Maybe it's just a bad seed?" 188 | print(errorMessage) 189 | resetAttributesAndSeed() 190 | return (False, errorMessage) 191 | 192 | generatedRom = generateRom() 193 | resetAttributesAndSeed(True) 194 | if generateLog.get() == "1": 195 | generateTextLog() 196 | for att in Attributes: 197 | att.resetToDefault() 198 | if generatedRom[0]: 199 | numSeedsGenerated += 1 200 | else: 201 | return generatedRom 202 | resetAttributesAndSeed() 203 | return (True, "Successfully generated "+str(numSeedsGenerated)+" seed"+("s." if numSeedsGenerated != 1 else ".")) 204 | 205 | # Optimize attributes by checking each rule and attempting to remove any values that are guaranteed to fail. 206 | # For example, if the rule "value("A")>=5" is enabled, this will remove any value of "A"<5. 207 | def optimizeAttributes(ruleset): 208 | global endTime, timedOut 209 | 210 | setNumAllCombinations() 211 | for rule in ruleset: 212 | numRuleCombinations = getNumCombinations(rule) 213 | # only optimize an attribute if it would take a negligible amount of time 214 | if numRuleCombinations < 150000: # 150,000 combinations is what my 3.5 year old laptop checks in about half a second 215 | newPossibleValues = [] 216 | for i in range(len(rule.relatedAttributes)): 217 | rule.relatedAttributes[i].resetToFirstValue() 218 | newPossibleValues.append([False] * len(rule.relatedAttributes[i].possible_values)) 219 | nextValueSet = True 220 | while nextValueSet: 221 | if Timeout > 0 and time() > endTime: 222 | timedOut = True 223 | return False 224 | if rule.rulePasses(): # you could also check if all current values are True, then skip the rule check (but I think this would be less efficient) 225 | for i in range(len(rule.relatedAttributes)): 226 | newPossibleValues[i][rule.relatedAttributes[i].index] = True 227 | nextValueSet = False 228 | for i in range(len(rule.relatedAttributes)): 229 | if rule.relatedAttributes[i].setToNextValue(): 230 | nextValueSet = True 231 | break 232 | for i in range(len(rule.relatedAttributes)): 233 | newVals = [] 234 | for j in range(len(rule.relatedAttributes[i].possible_values)): 235 | if newPossibleValues[i][j] == True: 236 | newVals.append(rule.relatedAttributes[i].possible_values[j]) 237 | rule.relatedAttributes[i].possible_values = newVals 238 | rule.relatedAttributes[i].resetToFirstValue() 239 | numAllCombinationsNew = 1 240 | for att in Attributes: 241 | numAllCombinationsNew *= len(att.possible_values) 242 | try: 243 | print(str(round((1-(numAllCombinationsNew/numAllCombinations))*100, 3))+"% reduction in seed generation time.") 244 | except: 245 | print("Something broke. Close the program, reopen, and try again.") # This shouldn't happen 246 | return False 247 | return True 248 | 249 | # Attempt 50 completely random combinations before attempting a normal approach. 250 | # This is useful for overcoming unlucky RNG without negatively affecting anything else. 251 | def shotgunApproach(ruleset): 252 | ruleNum = 0 253 | numAttempts = 0 254 | 255 | while ruleNum < len(ruleset): 256 | if not ruleset[ruleNum].rulePasses(): 257 | shuffleAllAttributes() 258 | numAttempts += 1 259 | if numAttempts >= 50: 260 | return False 261 | ruleNum = 0 262 | else: 263 | ruleNum += 1 264 | return True 265 | 266 | # Backtracking constraint satisfaction across related Attributes only. 267 | def enforceRulesetBacktracking(ruleset): 268 | global endTime, timedOut 269 | global currNumCombinations 270 | 271 | ruleNum = 0 272 | currNumCombinations = 0 273 | 274 | while ruleNum < len(ruleset): 275 | passedCurrRuleBatch = True 276 | for nestedRuleNum in range(ruleNum+1): 277 | if not ruleset[nestedRuleNum].rulePasses(): 278 | passedCurrRuleBatch = False 279 | break 280 | if not passedCurrRuleBatch: 281 | currNumRelated = len(ruleset[ruleNum].relatedAttributes) - 1 282 | while not ruleset[ruleNum].relatedAttributes[currNumRelated].setToNextValue(): 283 | currNumRelated -= 1 284 | if currNumRelated < 0: 285 | return False 286 | else: 287 | ruleNum += 1 288 | currNumCombinations += 1 289 | return True 290 | 291 | # Brute force constraint satisfaction across all Attributes. 292 | # May work marginally better than backtracking in rare situations (at best), 293 | # but is significantly slower to the point of sometimes being impractical. 294 | def enforceRulesetBruteForce(ruleset): 295 | global endTime 296 | global timedOut 297 | global currNumCombinations 298 | 299 | ruleNum = 0 300 | currNumCombinations = 0 301 | 302 | while ruleNum < len(ruleset): 303 | if not ruleset[ruleNum].rulePasses(): 304 | if Timeout > 0 and time() > endTime: 305 | timedOut = True 306 | return False 307 | nextValueSet = False 308 | for att in Attributes: 309 | if att.setToNextValue(): 310 | nextValueSet = True 311 | break 312 | if not nextValueSet: 313 | return False 314 | ruleNum = 0 315 | currNumCombinations += 1 316 | else: 317 | ruleNum += 1 318 | return True 319 | 320 | # Generate a ROM using the determined Attribute values. 321 | def generateRom(): 322 | global sourceRoms 323 | global seedString 324 | 325 | allNewRoms = [] 326 | success = True 327 | for i in range(len(sourceRoms)): 328 | romName, romExt = path.splitext(path.basename(sourceRoms[i].get())) 329 | newRom = path.join(outputFolder, romName+"-"+seedString+romExt) 330 | allNewRoms.append(newRom) 331 | if not path.isdir(outputFolder): 332 | mkdir(outputFolder) 333 | shutil.copyfile(sourceRoms[i].get(), newRom) 334 | try: 335 | file = open(newRom, "r+b") 336 | for att in Attributes: 337 | for addressTuple in att.addresses: 338 | address, fileNum = addressTuple 339 | if fileNum-1 == i: 340 | writeToAddress(file, address, att.value, att.number_of_bytes, att.is_little_endian) 341 | file.close() 342 | print("Succesfully generated ROM with seed "+seedString) 343 | # return (True, "") 344 | except: 345 | file.close() 346 | success = False 347 | break 348 | if not success: 349 | print("Something went wrong. Deleting generated ROMs.") 350 | for newRom in allNewRoms: 351 | remove(newRom) 352 | return (False, "At least one ROM failed to generate.") 353 | return (True, "") 354 | 355 | # Generate a text log containing the Attribute values. 356 | def generateTextLog(): 357 | global sourceRoms 358 | global seedString 359 | 360 | if len(Rom_Name) > 1: 361 | start = "Log" 362 | else: 363 | start = path.splitext(path.basename(sourceRoms[0].get()))[0] 364 | newLog = path.join(outputFolder, start+"-"+seedString+".txt") 365 | file = open(newLog, "w") 366 | file.writelines(Program_Name+"\nSeed: "+seedString+"\n\nValues:\n") 367 | maxNameLen = max([len(att.name) for att in Attributes]) 368 | maxIntLen = max([len(str(att.value)) for att in Attributes]) 369 | maxHexLen = max([len(str(hex(att.value))) for att in Attributes])-2 370 | for att in Attributes: 371 | nameStr = att.name.ljust(maxNameLen) 372 | intStr = str(att.value).rjust(maxIntLen) 373 | hexStr = "[0x"+str(hex(att.value))[2:].rjust(maxHexLen, "0").upper()+"]" 374 | file.writelines(nameStr+": "+intStr+" "+hexStr+"\n") 375 | file.close() 376 | 377 | def shuffleAllAttributes(): 378 | for att in Attributes: 379 | att.prepare() 380 | 381 | def resetAttributesAndSeed(printAttributes=False): 382 | global Attributes 383 | global originalAttributes 384 | 385 | Attributes.sort(key=getAttributeNum) 386 | if printAttributes: 387 | maxNameLen = max([len(att.name) for att in Attributes]) 388 | maxIntLen = max([len(str(att.value)) for att in Attributes]) 389 | maxHexLen = max([len(str(hex(att.value))) for att in Attributes])-2 390 | for att in Attributes: 391 | nameStr = att.name.ljust(maxNameLen) 392 | intStr = str(att.value).rjust(maxIntLen) 393 | hexStr = "[0x"+str(hex(att.value))[2:].rjust(maxHexLen, "0").upper()+"]" 394 | print(nameStr+": "+intStr+" "+hexStr) 395 | Attributes = copy.copy(originalAttributes) 396 | random.seed(time()) 397 | resetRuleCounter() 398 | 399 | def getNumCombinations(rule): 400 | num = 1 401 | for att in rule.relatedAttributes: 402 | num *= len(att.possible_values) 403 | return num 404 | 405 | def setNumAllCombinations(): 406 | global numAllCombinations 407 | 408 | numAllCombinations = 1 409 | for att in Attributes: 410 | numAllCombinations *= len(att.possible_values) 411 | return numAllCombinations 412 | 413 | def getFromListByName(arr, name): 414 | for a in arr: 415 | if a.name == name: 416 | return a 417 | 418 | def getAttributeNum(att): 419 | return att.attributeNum 420 | 421 | def getShuffledAttributeNum(att): 422 | for i in range(len(Attributes)): 423 | if att is Attributes[i]: 424 | return i 425 | 426 | ####### 427 | # GUI # 428 | ####### 429 | 430 | #! /usr/bin/env python 431 | # -*- coding: utf-8 -*- 432 | # 433 | # GUI module initially created by PAGE version 5.4 434 | # in conjunction with Tcl version 8.6 435 | # platform: Windows NT 436 | 437 | def vp_start_gui(): 438 | '''Starting point when module is the main routine.''' 439 | global val, w, root 440 | root = tk.Tk() 441 | # 1.7 seems to be default scaling 442 | # size = root.winfo_screenheight() 443 | # sizeRatio = 1080/1440 444 | # root.tk.call('tk', 'scaling', 2.0*sizeRatio) 445 | set_Tk_var() 446 | top = TopLevel(root) 447 | init(root, top) 448 | root.mainloop() 449 | 450 | w = None 451 | def create_TopLevel(rt, *args, **kwargs): 452 | '''Starting point when module is imported by another module. 453 | Correct form of call: 'create_TopLevel(root, *args, **kwargs)' .''' 454 | global w, w_win, root 455 | #rt = root 456 | root = rt 457 | w = tk.Toplevel (root) 458 | set_Tk_var() 459 | top = TopLevel (w) 460 | init(w, top, *args, **kwargs) 461 | return (w, top) 462 | 463 | def destroy_TopLevel(): 464 | global w 465 | w.destroy() 466 | w = None 467 | 468 | class TopLevel: 469 | def __init__(self, top=None): 470 | global vMult, changeRomVal 471 | '''This class configures and populates the toplevel window. 472 | top is the toplevel containing window.''' 473 | _bgcolor = '#d9d9d9' # X11 color: 'gray85' 474 | _fgcolor = '#000000' # X11 color: 'black' 475 | _compcolor = '#d9d9d9' # X11 color: 'gray85' 476 | _ana1color = '#d9d9d9' # X11 color: 'gray85' 477 | _ana2color = '#ececec' # Closest X11 color: 'gray92' 478 | self.style = ttk.Style() 479 | if sys.platform == "win32": 480 | self.style.theme_use('winnative') 481 | self.style.configure('.',background=_bgcolor) 482 | self.style.configure('.',foreground=_fgcolor) 483 | self.style.configure('.',font="TkDefaultFont") 484 | self.style.map('.',background= 485 | [('selected', _compcolor), ('active',_ana2color)]) 486 | self.font = tkFont.Font(family='TkDefaultFont') 487 | self.fontHeight = self.font.metrics('linespace') / 500 488 | self.tooltip_font = "TkDefaultFont" 489 | 490 | self.top = top 491 | self.top.geometry(str(750)+"x"+str(450)) 492 | self.top.minsize(750, 450) 493 | # self.top.maxsize(2000, 600) 494 | self.top.resizable(1, 1) 495 | self.top.title(Program_Name) 496 | self.top.configure(background="#d9d9d9") 497 | self.top.configure(highlightbackground="#d9d9d9") 498 | self.top.configure(highlightcolor="black") 499 | 500 | ## Menu Bar 501 | menubar = tk.Menu(self.top, bg=_bgcolor, fg=_fgcolor, tearoff=0) 502 | fileMenu = tk.Menu(menubar, tearoff=0) 503 | fileMenu.add_command(label="Load File...", command=self.setSourceRom) 504 | fileMenu.add_separator() 505 | fileMenu.add_command(label="Exit", command=root.quit) 506 | menubar.add_cascade(label="File", menu=fileMenu) 507 | self.top.config(menu=menubar) 508 | helpMenu = tk.Menu(menubar, tearoff=0) 509 | helpMenu.add_command(label="View Help...", command=self.showHelpPopup) 510 | helpMenu.add_separator() 511 | if About_Page_Text is not None and About_Page_Text != "": 512 | helpMenu.add_command(label="About...", command=self.showAboutPopup) 513 | helpMenu.add_command(label="Simple Randomizer Maker", command=self.showSRMPopup) 514 | menubar.add_cascade(label="Help", menu=helpMenu) 515 | self.top.config(menu=menubar) 516 | 517 | self.style.map('TCheckbutton',background= 518 | [('selected', _bgcolor), ('active', _ana2color)]) 519 | self.style.map('TRadiobutton',background= 520 | [('selected', _bgcolor), ('active', _ana2color)]) 521 | 522 | vMult = 700.0/600 523 | 524 | # Rom Input Label 525 | self.Label_RomInput = ttk.Label(self.top) 526 | self.Label_RomInput.configure(background="#d9d9d9") 527 | self.Label_RomInput.configure(foreground="#000000") 528 | self.Label_RomInput.configure(font="TkDefaultFont") 529 | self.Label_RomInput.configure(relief="flat") 530 | self.Label_RomInput.configure(anchor='w') 531 | self.Label_RomInput.configure(justify='left') 532 | 533 | # Rom Input Entry 534 | self.Entry_RomInput = ttk.Entry(self.top) 535 | self.Entry_RomInput.configure(state='readonly') 536 | self.Entry_RomInput.configure(background="#000000") 537 | self.Entry_RomInput.configure(cursor="ibeam") 538 | 539 | # Change Rom Source Buttons 540 | if len(Rom_Name) > 1: 541 | btnSize = .035 542 | changeRomVal = btnSize*2 + .01 543 | # Previous Source Rom Button 544 | self.Button_PrevSourceRom = ttk.Button(self.top) 545 | self.Button_PrevSourceRom.place(relx=.035, rely=.0365*vMult, relheight=.057*vMult, relwidth=btnSize) 546 | self.Button_PrevSourceRom.configure(command=self.decrementAndSetRomInput) 547 | self.Button_PrevSourceRom.configure(takefocus="") 548 | self.Button_PrevSourceRom.configure(text='<') 549 | 550 | # Next Source Rom Button 551 | self.Button_NextSourceRom = ttk.Button(self.top) 552 | self.Button_NextSourceRom.place(relx=.035+btnSize+.005, rely=.0365*vMult, relheight=.057*vMult, relwidth=btnSize) 553 | self.Button_NextSourceRom.configure(command=self.incrementAndSetRomInput) 554 | self.Button_NextSourceRom.configure(takefocus="") 555 | self.Button_NextSourceRom.configure(text='>') 556 | else: 557 | changeRomVal = 0 558 | 559 | # Rom Input Label and Entry 560 | self.setRomInput() 561 | 562 | # Rom Input Button 563 | self.Button_RomInput = ttk.Button(self.top) 564 | self.Button_RomInput.place(relx=.845, rely=.0365*vMult, relheight=.057*vMult, relwidth=.12) 565 | self.Button_RomInput.configure(command=self.setSourceRom) 566 | self.Button_RomInput.configure(takefocus="") 567 | self.Button_RomInput.configure(text='Load File') 568 | 569 | # Use Settings Radio Button 570 | self.RadioButton_UseSettings = ttk.Radiobutton(self.top) 571 | self.RadioButton_UseSettings.place(relx=.035, rely=.11*vMult, relheight=.05*vMult, relwidth=self.getTextLength('Use Settings')) 572 | self.RadioButton_UseSettings.configure(variable=useSeed) 573 | self.RadioButton_UseSettings.configure(value="0") 574 | self.RadioButton_UseSettings.configure(text='Use Settings') 575 | self.RadioButton_UseSettings.configure(compound='none') 576 | self.RadioButton_UseSettings_tooltip = ToolTip(self.RadioButton_UseSettings, self.tooltip_font, 'Use the settings defined below to create a random seed.') 577 | 578 | # Use Seed Radio Button 579 | self.RadioButton_UseSeed = ttk.Radiobutton(self.top) 580 | self.RadioButton_UseSeed.place(relx=.035-.01+.81-self.getTextLength("W"*(stringLen+1))-self.getTextLength('Use Seed'), rely=.11*vMult, relheight=.057*vMult, relwidth=self.getTextLength('Use Seed')) 581 | self.RadioButton_UseSeed.configure(variable=useSeed) 582 | self.RadioButton_UseSeed.configure(text='''Use Seed''') 583 | self.RadioButton_UseSeed_tooltip = ToolTip(self.RadioButton_UseSeed, self.tooltip_font, 'Recreate a specific set of changes according to a seed.') 584 | 585 | # Seed Input Entry 586 | self.Entry_SeedInput = ttk.Entry(self.top) 587 | # old relx=.37+self.getTextLength('Use Seed') 588 | self.Entry_SeedInput.place(relx=.035-.01+.81-self.getTextLength("W"*(stringLen+1)), rely=.11*vMult, relheight=.05*vMult, relwidth=self.getTextLength("W"*(stringLen+1))) 589 | self.Entry_SeedInput.configure(state='normal') 590 | self.Entry_SeedInput.configure(textvariable=seedInput) 591 | self.Entry_SeedInput.configure(takefocus="") 592 | self.Entry_SeedInput.configure(cursor="ibeam") 593 | self.Entry_SeedInput.bind('',self.keepUpperCharsSeed) 594 | self.Entry_SeedInput.bind('',self.keepUpperCharsSeed) 595 | 596 | # Frame 597 | self.TFrame1 = ttk.Frame(self.top) 598 | self.TFrame1.place(relx=.035, rely=.18*vMult, relheight=.55*vMult, relwidth=.93) 599 | self.TFrame1.configure(relief='groove') 600 | self.TFrame1.configure(borderwidth="2") 601 | self.TFrame1.configure(relief="groove") 602 | 603 | # Change Ruleset Page Buttons 604 | if len(Optional_Rulesets) > 14: 605 | btnSize = .035 606 | # Previous Source Rom Button 607 | self.Button_PrevRulesetPage = ttk.Button(self.top) 608 | self.Button_PrevRulesetPage.place(relx=.93-btnSize-.005, rely=(.18+.55-.057)*vMult, relheight=.057*vMult, relwidth=btnSize) 609 | self.Button_PrevRulesetPage.configure(command=self.decrementAndSetDisplayedRulesets) 610 | self.Button_PrevRulesetPage.configure(takefocus="") 611 | self.Button_PrevRulesetPage.configure(text='<') 612 | 613 | # Next Source Rom Button 614 | self.Button_NextRulesetPage = ttk.Button(self.top) 615 | self.Button_NextRulesetPage.place(relx=.93, rely=(.18+.55-.057)*vMult, relheight=.057*vMult, relwidth=btnSize) 616 | self.Button_NextRulesetPage.configure(command=self.incrementAndSetDisplayedRulesets) 617 | self.Button_NextRulesetPage.configure(takefocus="") 618 | self.Button_NextRulesetPage.configure(text='>') 619 | 620 | # Ruleset Check Buttons, Number of Seeds Label, Number of Seeds Dropdown 621 | self.CheckButtons = [] 622 | self.CheckButtons_tooltips = [] 623 | global optRulesetValues 624 | for i in range(len(Optional_Rulesets)): 625 | self.CheckButtons.append(ttk.Checkbutton(self.top)) # self.TFrame1 to put in frame 626 | self.CheckButtons[i].configure(variable=optRulesetValues[i]) 627 | self.CheckButtons[i].configure(offvalue="0") 628 | self.CheckButtons[i].configure(onvalue="1") 629 | self.CheckButtons[i].configure(takefocus="") 630 | self.CheckButtons[i].configure(text=Optional_Rulesets[i].name) 631 | self.CheckButtons_tooltips.append(ToolTip(self.CheckButtons[i], self.tooltip_font, Optional_Rulesets[i].description)) 632 | self.Label_NumSeeds = ttk.Label(self.top) 633 | self.Label_NumSeeds.configure(background="#d9d9d9") 634 | self.Label_NumSeeds.configure(foreground="#000000") 635 | self.Label_NumSeeds.configure(font="TkDefaultFont") 636 | self.Label_NumSeeds.configure(relief="flat") 637 | self.Label_NumSeeds.configure(anchor='w') 638 | self.Label_NumSeeds.configure(justify='left') 639 | self.Label_NumSeeds.configure(text='# of Seeds') 640 | self.Label_NumSeeds_tooltip = ToolTip(self.Label_NumSeeds, self.tooltip_font, 'How many seeds would you like to generate?') 641 | self.ComboBox_NumSeeds = ttk.Combobox(self.top) 642 | self.ComboBox_NumSeeds.configure(values=['1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16','17','18','19','20']) 643 | self.ComboBox_NumSeeds.configure(state='readonly') 644 | self.ComboBox_NumSeeds.configure(textvariable=numSeeds) 645 | self.setDisplayedRulesets() 646 | 647 | # Text Log Check Button 648 | self.CheckButton_GenerateTextLog = ttk.Checkbutton(self.top) 649 | self.CheckButton_GenerateTextLog.place(relx=.25, rely=.895, relheight=.05*vMult, relwidth=.20) 650 | self.CheckButton_GenerateTextLog.configure(variable=generateLog) 651 | self.CheckButton_GenerateTextLog.configure(takefocus="") 652 | self.CheckButton_GenerateTextLog.configure(text='Generate Text Log') 653 | self.CheckButton_GenerateTextLog_tooltip = ToolTip(self.CheckButton_GenerateTextLog, self.tooltip_font, 'Would you like to generate a text file that details what abilities are tied to each enemy/object in the created seed?') 654 | 655 | # Create Rom Button 656 | self.Button_CreateRom = ttk.Button(self.top) 657 | self.Button_CreateRom.place(relx=.55, rely=.8915, relheight=.057*vMult, relwidth=.144) 658 | self.Button_CreateRom.configure(takefocus="") 659 | self.Label_NumSeeds.configure(anchor='w') 660 | self.Button_CreateRom.configure(text='''Randomize!''') 661 | 662 | # Other 663 | self.RadioButton_UseSettings.configure(command=self.prepareSettingsAndSeed) 664 | self.RadioButton_UseSeed.configure(command=self.prepareSettingsAndSeed) 665 | self.Button_CreateRom.configure(command=self.attemptRandomize) 666 | for i in range(len(Optional_Rulesets)): 667 | self.CheckButtons[i].configure(command=self.prepareSettingsFromDependencies) 668 | self.prepareSettingsFromDependencies() 669 | 670 | def getTextLength(self, text): 671 | return .03+self.font.measure(text)/1000.0 672 | 673 | def getMaxColumnWidth(self, num): 674 | lower = 5 * (num//5) 675 | upper = lower + 5 676 | sizeArr = [] 677 | for i in range(lower, min(upper, len(Optional_Rulesets))): 678 | # sizeArr.append(self.getTextLength(Optional_Rulesets[i].name)-.03) 679 | for j in range(Optional_Rulesets[i].name.count('\n') + 1): 680 | sizeArr.append(self.getTextLength(Optional_Rulesets[i].name.split('\n')[j])-.03) 681 | if len(sizeArr) == 0: 682 | return self.getTextLength("# of Seeds")-.03 683 | return max(sizeArr) 684 | 685 | def prepareSettingsAndSeed(self, unused=None): 686 | if useSeed.get()=="1": 687 | self.Label_NumSeeds.configure(state="disabled") 688 | self.ComboBox_NumSeeds.configure(state="disabled") 689 | for button in self.CheckButtons: 690 | button.configure(state="disabled") 691 | else: 692 | self.Label_NumSeeds.configure(state="normal") 693 | self.ComboBox_NumSeeds.configure(state="readonly") 694 | for button in self.CheckButtons: 695 | button.configure(state="normal") 696 | self.prepareSettingsFromDependencies() 697 | 698 | def prepareSettingsFromDependencies(self): 699 | for i in range(len(self.CheckButtons)): 700 | currCheckButton = self.CheckButtons[i] 701 | firstIndex = currRulesetPage*15 702 | lastIndex = min((currRulesetPage+1)*15, len(Optional_Rulesets)) # last index exclusive 703 | if not (firstIndex <= i < lastIndex): 704 | currCheckButton.configure(state="disabled") 705 | continue 706 | currCheckButton.configure(state="normal") 707 | for j in range(len(self.CheckButtons)): 708 | currRulesetVal = optRulesetValues[j].get() 709 | currRulesetName = Optional_Rulesets[j].originalName 710 | if ((currRulesetVal == "1") and (currRulesetName in Optional_Rulesets[i].must_be_disabled) 711 | ) or ((currRulesetVal == "0") and (currRulesetName in Optional_Rulesets[i].must_be_enabled)): 712 | optRulesetValues[i].set("0") 713 | currCheckButton.configure(state="disabled") 714 | break 715 | 716 | def decrementAndSetDisplayedRulesets(self): 717 | global currRulesetPage 718 | currRulesetPage -= 1 719 | if currRulesetPage < 0: 720 | currRulesetPage = len(Optional_Rulesets) // 15 721 | self.setDisplayedRulesets() 722 | 723 | def incrementAndSetDisplayedRulesets(self): 724 | global currRulesetPage 725 | currRulesetPage += 1 726 | if currRulesetPage > len(Optional_Rulesets) // 15: 727 | currRulesetPage = 0 728 | self.setDisplayedRulesets() 729 | 730 | def setDisplayedRulesets(self): 731 | # Ruleset Check Buttons 732 | firstIndex = currRulesetPage*15 733 | lastIndex = min((currRulesetPage+1)*15, len(Optional_Rulesets)) # last index exclusive 734 | rulesetsOnCurrPage = Optional_Rulesets[currRulesetPage*15:min((currRulesetPage+1)*15, len(Optional_Rulesets))] 735 | numOptRulesets = lastIndex - firstIndex 736 | xShiftArray = spaceOut(min(numOptRulesets//5 + 1, 3), .3, numDecimalPlaces=3) 737 | yShiftArray = spaceOut(min(numOptRulesets+1, 5), .09, numDecimalPlaces=3) 738 | optRulesetNum = 0 739 | for i in range(len(Optional_Rulesets)): 740 | tempMaxLen = 0 741 | tempNumLines = 0 742 | for line in Optional_Rulesets[i].name.split('\n'): 743 | tempMaxLen = max(tempMaxLen, self.getTextLength(line)) 744 | tempNumLines += 1 745 | if firstIndex <= i < lastIndex: 746 | self.CheckButtons[i].place(relx=.475-self.getMaxColumnWidth(optRulesetNum)/2+xShiftArray[optRulesetNum//5], rely=(.40+yShiftArray[optRulesetNum%5])*vMult, relheight=max(self.fontHeight*tempNumLines*vMult, 0.05), relwidth=tempMaxLen+.03) 747 | optRulesetNum += 1 748 | else: 749 | self.CheckButtons[i].place(relx=10, rely=10, relheight=max(self.fontHeight*tempNumLines*vMult, 0.05), relwidth=tempMaxLen+.03) 750 | 751 | if optRulesetNum < 15: 752 | # Number of Seeds Label 753 | seedX = .475-self.getMaxColumnWidth(optRulesetNum)/2+xShiftArray[optRulesetNum//5] 754 | self.Label_NumSeeds.place(relx=seedX, rely=(.40+yShiftArray[optRulesetNum%5])*vMult, relheight=.05*vMult, relwidth=.11) 755 | # Number of Seeds Dropdown 756 | self.ComboBox_NumSeeds.place(relx=seedX, rely=(.45+yShiftArray[optRulesetNum%5])*vMult, relheight=.05*vMult, relwidth=.088) 757 | else: 758 | self.Label_NumSeeds.place(relx=10, rely=10, relheight=.05*vMult, relwidth=.11) 759 | self.ComboBox_NumSeeds.place(relx=10, rely=10, relheight=.05*vMult, relwidth=.088) 760 | self.prepareSettingsAndSeed() 761 | 762 | def decrementAndSetRomInput(self): 763 | global currRomIndex 764 | currRomIndex -= 1 765 | if currRomIndex < 0: 766 | currRomIndex = len(Rom_Name) - 1 767 | self.setRomInput() 768 | 769 | def incrementAndSetRomInput(self): 770 | global currRomIndex 771 | currRomIndex += 1 772 | if currRomIndex >= len(Rom_Name): 773 | currRomIndex = 0 774 | self.setRomInput() 775 | 776 | def setRomInput(self): 777 | romTextLength = self.getTextLength(Rom_Name[currRomIndex]) 778 | self.Label_RomInput.place(relx=.035+changeRomVal, rely=.04*vMult, relheight=.05*vMult, relwidth=romTextLength) 779 | self.Label_RomInput.configure(text=Rom_Name[currRomIndex]) 780 | self.Entry_RomInput.place(relx=.035+changeRomVal+romTextLength-.01, rely=.04*vMult, relheight=.05*vMult, relwidth=.81-romTextLength-changeRomVal) 781 | self.Entry_RomInput.configure(textvariable=sourceRoms[currRomIndex]) 782 | 783 | def setSourceRom(self): 784 | global sourceRoms 785 | currRFF = Rom_File_Format[currRomIndex % len(Rom_File_Format)] 786 | if currRFF is None or currRFF == "": 787 | sourceRoms[currRomIndex].set(askopenfilename()) 788 | else: 789 | sourceRoms[currRomIndex].set(askopenfilename(filetypes=[("Files", "*."+currRFF)])) 790 | if sourceRoms[currRomIndex].get() != "": 791 | with open(sourceRoms[currRomIndex].get(), "rb") as inputFile: 792 | fileBytes = inputFile.read() 793 | currHash = Rom_Hash[currRomIndex % len(Rom_Hash)] 794 | if (currHash is not None) and (currHash != ""): 795 | fileHash = str(hex(binascii.crc32(fileBytes)))[2:].zfill(8).upper() 796 | if currHash.upper() != fileHash: 797 | showerror("Incorrect File", "Incorrect file; CRC32 does not match expected hash.\n\nExpected: "+currHash.upper()+"\nGot: "+fileHash) 798 | sourceRoms[currRomIndex].set("") 799 | 800 | def keepUpperCharsSeed(self, unused): 801 | global seedInput 802 | seedInput.set(''.join(ch.upper() for ch in seedInput.get() if ch.isalpha() or ch.isdigit())) 803 | seedInput.set(seedInput.get()[:stringLen]) 804 | useSeed.set("1") 805 | self.prepareSettingsAndSeed() 806 | 807 | def attemptRandomize(self): 808 | global optionalRulesetsList 809 | global optRulesetValues 810 | 811 | optionalRulesetsList = [("", 0)] * len(optRulesetValues) 812 | for i in range(len(optRulesetValues)): 813 | optionalRulesetsList[i] = (Optional_Rulesets[i].name, int(optRulesetValues[i].get())) 814 | results = randomize() 815 | print("\n") 816 | if results[0]: 817 | showinfo("Success!", results[1]) 818 | else: 819 | showerror("Error", results[1]) 820 | 821 | def showHelpPopup(self): 822 | showinfo("Help", 823 | "To learn more about an option, move your mouse over it." 824 | +"\n"+limitedString("You can generate multiple unique ROMs at once by changing the # of seeds.", 55, "- ") 825 | +"\n"+limitedString("You can also generate a text log that gives information about a created seed.", 55, "- ") 826 | +"\n"+limitedString("Generated ROMs will be placed in an \"output\" folder, which will be in the same folder as this program.", 55, "- ")) 827 | 828 | def showAboutPopup(self): 829 | showinfo("About", About_Page_Text) 830 | 831 | def showSRMPopup(self): 832 | showinfo("Simple Randomizer Maker v1.262", "This was made using\nMips96's Simple Randomizer Maker.\n\nhttps://github.com/Mips96/SimpleRandomizerMaker") 833 | 834 | # ====================================================== 835 | # Support code for Balloon Help (also called tooltips). 836 | # Found the original code at: 837 | # http://code.activestate.com/recipes/576688-tooltip-for-tkinter/ 838 | # Modified by Rozen to remove Tkinter import statements and to receive 839 | # the font as an argument. 840 | # ====================================================== 841 | 842 | from time import localtime, strftime 843 | 844 | class ToolTip(tk.Toplevel): 845 | """ 846 | Provides a ToolTip widget for Tkinter. 847 | To apply a ToolTip to any Tkinter widget, simply pass the widget to the 848 | ToolTip constructor 849 | """ 850 | def __init__(self, wdgt, tooltip_font, msg=None, msgFunc=None, 851 | delay=0.5, follow=True): 852 | """ 853 | Initialize the ToolTip 854 | 855 | Arguments: 856 | wdgt: The widget this ToolTip is assigned to 857 | tooltip_font: Font to be used 858 | msg: A static string message assigned to the ToolTip 859 | msgFunc: A function that retrieves a string to use as the ToolTip text 860 | delay: The delay in seconds before the ToolTip appears(may be float) 861 | follow: If True, the ToolTip follows motion, otherwise hides 862 | """ 863 | self.wdgt = wdgt 864 | # The parent of the ToolTip is the parent of the ToolTips widget 865 | self.parent = self.wdgt.master 866 | # Initalise the Toplevel 867 | tk.Toplevel.__init__(self, self.parent, bg='black', padx=1, pady=1) 868 | # Hide initially 869 | self.withdraw() 870 | # The ToolTip Toplevel should have no frame or title bar 871 | self.overrideredirect(True) 872 | 873 | # The msgVar will contain the text displayed by the ToolTip 874 | self.msgVar = tk.StringVar() 875 | if msg is None: 876 | self.msgVar.set('No message provided') 877 | else: 878 | self.msgVar.set(msg) 879 | self.msgFunc = msgFunc 880 | self.delay = delay 881 | self.follow = follow 882 | self.visible = 0 883 | self.lastMotion = 0 884 | # The text of the ToolTip is displayed in a Message widget 885 | tk.Message(self, textvariable=self.msgVar, bg='#FFFFDD', 886 | font=tooltip_font, 887 | aspect=1000).grid() 888 | 889 | # Add bindings to the widget. This will NOT override 890 | # bindings that the widget already has 891 | self.wdgt.bind('', self.spawn, '+') 892 | self.wdgt.bind('', self.hide, '+') 893 | self.wdgt.bind('', self.move, '+') 894 | 895 | def spawn(self, event=None): 896 | """ 897 | Spawn the ToolTip. This simply makes the ToolTip eligible for display. 898 | Usually this is caused by entering the widget 899 | 900 | Arguments: 901 | event: The event that called this funciton 902 | """ 903 | self.visible = 1 904 | # The after function takes a time argument in milliseconds 905 | self.after(int(self.delay * 1000), self.show) 906 | 907 | def show(self): 908 | """ 909 | Displays the ToolTip if the time delay has been long enough 910 | """ 911 | if self.visible == 1 and time() - self.lastMotion > self.delay: 912 | self.visible = 2 913 | if self.visible == 2: 914 | self.deiconify() 915 | 916 | def move(self, event): 917 | """ 918 | Processes motion within the widget. 919 | Arguments: 920 | event: The event that called this function 921 | """ 922 | self.lastMotion = time() 923 | # If the follow flag is not set, motion within the 924 | # widget will make the ToolTip disappear 925 | # 926 | if self.follow is False: 927 | self.withdraw() 928 | self.visible = 1 929 | 930 | # Offset the ToolTip 10x10 pixes southwest of the pointer 931 | self.geometry('+%i+%i' % (event.x_root+20, event.y_root-10)) 932 | try: 933 | # Try to call the message function. Will not change 934 | # the message if the message function is None or 935 | # the message function fails 936 | self.msgVar.set(self.msgFunc()) 937 | except: 938 | pass 939 | self.after(int(self.delay * 1000), self.show) 940 | 941 | def hide(self, event=None): 942 | """ 943 | Hides the ToolTip. Usually this is caused by leaving the widget 944 | Arguments: 945 | event: The event that called this function 946 | """ 947 | self.visible = 0 948 | self.withdraw() 949 | 950 | def update(self, msg): 951 | """ 952 | Updates the Tooltip with a new message. Added by Rozen 953 | """ 954 | self.msgVar.set(msg) 955 | 956 | # =========================================================== 957 | # End of Class ToolTip 958 | # =========================================================== 959 | 960 | def set_Tk_var(): 961 | global sourceRoms 962 | sourceRoms = [] 963 | for i in range(len(Rom_Name)): 964 | sourceRoms.append(tk.StringVar()) 965 | global optRulesetValues 966 | optRulesetValues = [] 967 | for i in range(len(Optional_Rulesets)): 968 | optRulesetValues.append(tk.StringVar()) 969 | global numSeeds 970 | numSeeds = tk.StringVar() 971 | global useSeed 972 | useSeed = tk.StringVar() 973 | global seedInput 974 | seedInput = tk.StringVar() 975 | global generateLog 976 | generateLog = tk.StringVar() 977 | global message 978 | message = tk.StringVar() 979 | message.set('') 980 | initVars() 981 | 982 | def initVars(): 983 | useSeed.set("0") 984 | numSeeds.set("1") 985 | generateLog.set("1") 986 | for val in optRulesetValues: 987 | val.set("0") 988 | message.set("Move your mouse over a label to learn more about it.") 989 | 990 | def init(top, gui, *args, **kwargs): 991 | global w, top_level, root 992 | w = gui 993 | top_level = top 994 | root = top 995 | 996 | def destroy_window(endProg=False): 997 | # Function which closes the window. 998 | global top_level 999 | top_level.destroy() 1000 | top_level = None 1001 | if endProg: 1002 | sys.exit() 1003 | 1004 | if __name__ == '__main__': 1005 | main() --------------------------------------------------------------------------------