├── .gitignore ├── KingsRaid.sln ├── README.md ├── krjs.py ├── krng.py └── nox.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyproj -------------------------------------------------------------------------------- /KingsRaid.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27130.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "KingsRaid", "KingsRaid.pyproj", "{FD3498AA-19CE-42A6-B65B-7F7B66C89908}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {FD3498AA-19CE-42A6-B65B-7F7B66C89908}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {FD3498AA-19CE-42A6-B65B-7F7B66C89908}.Release|Any CPU.ActiveCfg = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(SolutionProperties) = preSolution 18 | HideSolutionNode = FALSE 19 | EndGlobalSection 20 | GlobalSection(ExtensibilityGlobals) = postSolution 21 | SolutionGuid = {4A0004E9-45A0-4EEC-A323-C0C8E33F2DE3} 22 | EndGlobalSection 23 | EndGlobal 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KingsRaid 2 | 3 | **About** 4 | 5 | `krng.py` and `nox.py` constitute a program that can generate macros that can be used with Nox. It's important to understand that this program is not -- itself -- a macro. It does not interact with Nox or King's Raid. It does not click anything. Instead, this program *creates* macros that Nox understands. Think of it as doing the equivalent of opening your Macro recorder in Nox, clicking around a bunch of times, and saving the Macro. If you do that in Nox, Nox saves a file to your disk that contains instructions that it processes every time you click the play button to run the macro. All this program is doing is outputting that same file. 6 | 7 | Why do we need this? Why not just use Nox's built-in Macro recorder? 8 | 9 | This is a good question. This is helpful for several reasons. 10 | 11 | 1. Some people are scared of creating Macros, thinking they'll do something wrong, or just don't feel like figuring out how to get it working. 12 | 13 | 2. Some macros are impractical to create manually. Imagine making a Macro to buy and sell 300 items, then go to your inventory and grind them. You would have to click buy and sell 300 times by hand. That's pretty annoying. Furthermore, it would run slowly. It probably takes you a full second between clicks, or 750ms minimum, and after 300 clicks you're going to be getting tired, slowing you down further. An automatically generated macro make all clicks as fast as the game will recognize them. For a Macro that needs to run a long time this could shave literal hours off of your time. 14 | 15 | 3. Some macros are outright *impossible* to create manually. A good example of this is the stamina farming macro included in this script, which will take advantage of natural stamina regen **and** use a stamina potion (but only when necessary) **and** sell your inventory for you periodically so you can run forever. This requires complex sequences of actions and very precise clicks to avoid hitting "bad" locations. You could "cheat" by running a story dungeon 20 times, figuring out how long the longest possible run is, then waiting that long and exiting out to grind, but now that's going to happen every single cycle. Since the point is to farm, you probably want it to happen more efficiently (e.g. in less actual time), so you want each run to be as fast as possible. And even if you did do things that way, that still wouldn't allow you to use the stamina potion only when necessary. You'd have to use 10-20 stamina potions in advance before starting the macro, meaning you couldn't take advantage of natural stamina regen. So such a macro would be impossible to create by hand. 16 | 17 | 4. It's open source, so people can contribute. Instead of you making a macro that works for you only, if someone has a good idea and I can implement it, the entire community benefits, not just 1 person. 18 | 19 | **Instructions** 20 | 21 | 1. Right click these two links: 22 | * [krng.py](https://raw.githubusercontent.com/cppisking/KingsRaid/master/krng.py) 23 | * [nox.py](https://raw.githubusercontent.com/cppisking/KingsRaid/master/nox.py) 24 | 25 | and save them in the same folder on your computer. 26 | 27 | 2. Install [Python 3.5 or higher](https://www.python.org/). Just because 2.7 works now, doesn't mean it will in the future! I plan to break support for Python 2. Use Python 3. 28 | 29 | 3. Double click `krng.py` in Windows Explorer. 30 | 31 | 4. Answer the questions. 32 | 33 | 5. Profit! 34 | 35 | Patches welcome. Do not use the experimental macros unless you know what you're doing, or maybe you want to help me develop them. That's just my testbed for new ideas. 36 | -------------------------------------------------------------------------------- /krjs.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import copy 3 | import itertools 4 | import os 5 | import json 6 | import re 7 | import sys 8 | import traceback 9 | 10 | print('Kings Raid Skill Dumper v1.0') 11 | print('By: cpp (Reddit: u/cpp_is_king, Discord: @cpp#0120)') 12 | print('Paypal: cppisking@gmail.com') 13 | print() 14 | 15 | if sys.version_info < (3,5): 16 | print('Error: This script requires Python 3.5 or higher. Please visit ' 17 | 'www.python.org and install a newer version.') 18 | print('Press any key to exit...') 19 | input() 20 | sys.exit(1) 21 | 22 | args = None 23 | creature_star_grade_data = None 24 | creature_by_index = None 25 | creature_by_name = None 26 | skills_by_index = None 27 | skill_level_factors = None 28 | indent_level = 0 29 | 30 | def print_line(message, add_newline=True, is_continuation=False): 31 | global indent_level 32 | if not is_continuation: 33 | print(' ' * indent_level, end = '') 34 | 35 | if add_newline: 36 | print(message) 37 | else: 38 | print(message, end='') 39 | 40 | def indent(amount): 41 | global indent_level 42 | indent_level = indent_level + amount 43 | 44 | def decode_file(path): 45 | codecs = ['utf-8-sig', 'utf-8'] 46 | for c in codecs: 47 | try: 48 | with open(path, 'r', encoding=c) as fp: 49 | return fp.read() 50 | except: 51 | traceback.print_exc() 52 | pass 53 | return None 54 | 55 | def parse_args(): 56 | parser = argparse.ArgumentParser(description='Kings Raid Json Dumper.') 57 | parser.add_argument('--json_path', type=str, help='The folder containing json files') 58 | parser.add_argument('--star-grade-data', action='store_true', default=False, help='Dump Creature Star Grade Data') 59 | parser.add_argument('--heros', action='store_true', default=False, help='Dump Hero Skills') 60 | parser.add_argument('--auto-attacks', action='store_true', default=True, help='With --heros, dump auto attack info') 61 | parser.add_argument('--skills', action='store_true', default=False, help='With --heros, dump skill info') 62 | parser.add_argument('--hero', type=str, help='When --heros is specified, limit dumping to the specified hero (e.g. Kasel)') 63 | parser.add_argument('--style', type=str, default='verbose', choices=['verbose', 'brief'], help='Level of skill detail to display') 64 | parser.add_argument('--books', type=str, help='Assume skills have the specified book upgrades (e.g. --books=1,3,2,1)') 65 | return parser.parse_args() 66 | 67 | def max_numbered_key(obj, prefix): 68 | indices = set(map(lambda x : int(x[-1]), 69 | filter(lambda x : x.startswith(prefix), obj.keys()))) 70 | if len(indices) == 0: 71 | return None 72 | return max(indices) 73 | 74 | def update_table(json_obj, indexes): 75 | for obj in json_obj: 76 | for key, table in indexes: 77 | value = obj[key] 78 | if value in table: 79 | print('Warning: {0}={1} appears more than once. Ignoring subsequent occurrences...'.format(key, value)) 80 | continue 81 | table[value] = obj 82 | 83 | def load_creatures_table(): 84 | print('Loading creatures...') 85 | global args 86 | global creature_by_index 87 | global creature_by_name 88 | 89 | path = os.path.join(args.json_path, 'CreatureTable.json') 90 | content = decode_file(path) 91 | json_obj = json.loads(content) 92 | creature_by_index = {} 93 | creature_by_name = {} 94 | update_table(json_obj, [('Index', creature_by_index), ('CodeName', creature_by_name)]) 95 | 96 | def load_skills_table(): 97 | print('Loading skills...') 98 | global args 99 | global skills_by_index 100 | index = 0 101 | skills_by_index = {} 102 | path = os.path.join(args.json_path, 'SkillTable.json') 103 | while True: 104 | content = decode_file(path) 105 | json_obj = json.loads(content) 106 | update_table(json_obj, [('Index', skills_by_index)]) 107 | index = index + 1 108 | path = os.path.join(args.json_path, 'SkillTable{0}.json'.format(index)) 109 | if not os.path.exists(path): 110 | break 111 | 112 | def is_playable_hero(creature): 113 | return 'OpenType' in creature 114 | 115 | def load_creature_star_grade_data_table(): 116 | print('Loading creature star grade data...') 117 | global creature_star_grade_data 118 | global creature_by_index 119 | 120 | def short_stat_name(stat): 121 | mapping = { 122 | "PhysicalCriticalChance" : "P. Crit", 123 | "PhysicalDodgeChance" : "P. Dodge", 124 | "PhysicalHitChance" : "P. Acc", 125 | "PhysicalPiercePower" : "P. Pen", 126 | "PhysicalToughness" : "P. Tough", 127 | "MagicalCriticalChance" : "M. Crit", 128 | "MagicalDodgeChance" : "M. Dodge", 129 | "MagicalHitChance" : "M. Acc", 130 | "MagicalPiercePower" : "M. Pen", 131 | "MagicalToughness" : "M. Tough", 132 | "AntiCcChance" : "CC Resist", 133 | "MaxMp" : "MP", 134 | "MpOnDamage" : "MP/DMG", 135 | "MpOnAttack" : "MP/ATK", 136 | "MpOnTime" : "MP/Time", 137 | "MpOnKill" : "MP/Kill", 138 | "MpRegenRatioM" : "MP Regen", 139 | "AttackSpeed" : "ASpd", 140 | "MoveSpeedMms" : "Move Spd", 141 | "LevelStatFactor" : "Lvl Factor", 142 | "MagicalBlockChance" : "M. Block", 143 | "MagicalBlockPower" : "M. Block DEF", 144 | "PhysicalBlockChance" : "P. Block", 145 | "PhysicalBlockPower" : "P. Block DEF", 146 | "PhysicalCriticalPower" : "P. Crit DMG", 147 | "MagicalCriticalPower" : "M. Crit DMG", 148 | "HpStealPower" : "Lifesteal", 149 | "MagicalDefensePower" : "M. DEF", 150 | "PhysicalDefensePower" : "P. DEF", 151 | } 152 | return mapping[stat] 153 | 154 | path = os.path.join(args.json_path, 'CreatureStarGradeStatTable.json') 155 | content = decode_file(path) 156 | json_obj = json.loads(content) 157 | creature_star_grade_data = {} 158 | 159 | for obj in json_obj: 160 | index = obj['CreatureIndex'] 161 | if not index in creature_by_index: 162 | continue 163 | 164 | star = obj['Star'] 165 | transcend = obj.get('Transcended', 0) 166 | effective_star = star + transcend 167 | table = creature_star_grade_data.setdefault(index, {}) 168 | 169 | if effective_star in table: 170 | print('Warning: Creature star grade ({0}, {1}, {2}) appears more than once. Ignoring subsequent occurrences...'.format(index, star, transcend)) 171 | continue 172 | for stat in obj: 173 | if stat == 'CreatureIndex': 174 | continue 175 | if stat == 'Star': 176 | continue 177 | if stat == 'Transcended': 178 | continue 179 | stat_values = None 180 | short_stat = short_stat_name(stat) 181 | stat_values = table.setdefault(short_stat, [None] * 10) 182 | stat_values[effective_star - 1] = obj[stat] 183 | return 184 | 185 | def generate_factors(obj): 186 | max_factor = max_numbered_key(obj, 'Factor') 187 | assert(max_factor is not None) 188 | 189 | result = {} 190 | for x in range(1, max_factor+1): 191 | key_name = 'Factor{0}'.format(x) 192 | if key_name in obj: 193 | result[x] = obj[key_name] 194 | return result 195 | 196 | def generate_ticks(skill_obj, operations): 197 | ticks = skill_obj.get('TickTimeValue', None) 198 | if not ticks: 199 | return [(0, operations)] 200 | tick_expr = re.compile(r'(?P[^\[]*)(?:\[(?P.*)\])?') 201 | def one_tick_operations(t): 202 | match = tick_expr.match(t) 203 | if not match: 204 | print("The tick expression '{0}' for skill '{1}' is in an unrecognized format".format(t, skill_obj['Index'])) 205 | return None 206 | time, op = match.group('t', 'op') 207 | if op is None: 208 | non_null_opers = list(filter(lambda x : x is not None, operations)) 209 | return (int(time), non_null_opers) 210 | 211 | op_indices = list(map(lambda x : int(x) - 1, op.split(':'))) 212 | op_indices = list(filter(lambda x : x < len(operations) and operations[x], op_indices)) 213 | actions = list(map(lambda x : operations[x], op_indices)) 214 | 215 | return (int(time), actions) 216 | return [one_tick_operations(t) for t in ticks] 217 | 218 | def load_skill_level_factors_table(): 219 | print('Loading skill level factors...') 220 | global skill_level_factors 221 | 222 | skill_level_factors = {} 223 | path = os.path.join(args.json_path, 'SkillLevelFactorTable.json') 224 | content = decode_file(path) 225 | json_obj = json.loads(content) 226 | for obj in json_obj: 227 | skill_level_factors[obj['Level']] = generate_factors(obj) 228 | return 229 | 230 | def generate_skill_operations(skill_obj): 231 | def generate_one_operation(index): 232 | op_fields = list(filter(lambda x : x.startswith('Operation') and x.endswith(str(index)), skill_obj.keys())) 233 | d = { x[len('Operation'):-1] : skill_obj[x] for x in op_fields } 234 | return None if len(d) == 0 else d 235 | 236 | max_operation = max_numbered_key(skill_obj, 'Operation') 237 | if max_operation is None: 238 | return [] 239 | 240 | return [generate_one_operation(x) for x in range(1, max_operation+1)] 241 | 242 | def format_skill_operation_key(operation, key, default=None): 243 | if not key in operation: 244 | return default 245 | value = operation[key] 246 | return str(value) 247 | 248 | def dump_default_operation(operation, type): 249 | value_str = format_skill_operation_key(operation, 'Value') 250 | target_type_str = format_skill_operation_key(operation, 'TargetType', default='null target') 251 | value_factors_str = format_skill_operation_key(operation, 'ValueFactor') 252 | 253 | type = type + '({0})'.format(target_type_str) 254 | if value_str: 255 | type = type + ', values = {0}'.format(value_str) 256 | if value_factors_str: 257 | type = type + ', factors = {0}'.format(value_factors_str) 258 | suffix = type 259 | 260 | operation.pop('Value', None) 261 | operation.pop('TargetType', None) 262 | operation.pop('ValueFactor', None) 263 | return suffix 264 | 265 | def get_operation_level_value(creature_index, star, trans, factor1, factor2): 266 | global skill_level_factors 267 | global creature_star_grade_data 268 | 269 | sgd = creature_star_grade_data[creature_index] 270 | lsf = float(sgd['Lvl Factor'][star + trans - 1]) / 1000.0 271 | 272 | level_table = skill_level_factors[80] 273 | 274 | level_factor_index = factor1 275 | skill_scale_factor = factor2 276 | level_scale_factor = level_table.get(level_factor_index, 0) 277 | result = lsf * float(level_scale_factor) * float(skill_scale_factor) 278 | return float(result) 279 | 280 | def get_operation_value(creature_index, star, trans, values, factors, index, default): 281 | if len(values) <= index: 282 | return 0 283 | 284 | # This is an expression, which we don't handle yet. 285 | if values[index][0] == '$': 286 | return values[index] 287 | 288 | if factors is None: 289 | return 0 290 | fi = index*2 291 | fi2 = index*2 + 1 292 | if len(factors) <= fi2: 293 | return 0 294 | 295 | level_value = get_operation_level_value(creature_index, star, trans, int(factors[fi]), int(factors[fi2])) 296 | 297 | return (float(values[index]) + level_value) / 1000.0 298 | 299 | def dump_get_damage_r(creature_index, star, trans, operation, type : str): 300 | global skill_level_factors 301 | values = operation['Value'] 302 | prefix = "physical" if "Physical" in type else "magical" 303 | if not 'ValueFactor' in operation: 304 | formula = 'ATK * {0}'.format(values[0]) 305 | else: 306 | factors = operation['ValueFactor'] 307 | power = get_operation_value(creature_index, star, trans, values, factors, 0, 0) 308 | level_factor = get_operation_value(creature_index, star, trans, values, factors, 1, 0) 309 | 310 | formula = 'Floor[ATK * {0} + {1}]'.format(power, level_factor) 311 | target_type = operation['TargetType'] 312 | operation.pop('TargetType', None) 313 | operation.pop('Value', None) 314 | operation.pop('ValueFactor', None) 315 | return '{0} DMG = {1}'.format(prefix, formula) 316 | 317 | 318 | def dump_one_skill_operation(creature_index, star, trans, index, operation): 319 | global args 320 | prefix = '[{0}]: '.format(index) 321 | suffix = '(null)' 322 | if not operation: 323 | print_line('{0}(null)'.format(prefix)) 324 | return 325 | 326 | op_copy = copy.deepcopy(operation) 327 | 328 | type_str = format_skill_operation_key(op_copy, 'Type') 329 | op_copy.pop('Type', None) 330 | operation_handlers = { 331 | 'GetPhysicalDamageR' : dump_get_damage_r, 332 | 'GetMagicalDamageR' : dump_get_damage_r 333 | } 334 | 335 | if type_str: 336 | if type_str in operation_handlers: 337 | suffix = operation_handlers[type_str](creature_index, star, trans, op_copy, type_str) 338 | else: 339 | suffix = dump_default_operation(op_copy, type_str) 340 | print_line('{0}{1}'.format(prefix, suffix)) 341 | 342 | def should_dump(op_key_value): 343 | k, v = op_key_value 344 | if k == 'ConditionType' and v == 'None': 345 | return False 346 | return True 347 | 348 | op_list = [op_key_value for op_key_value in op_copy.items() if should_dump(op_key_value)] 349 | indent(5) 350 | while len(op_list) > 0: 351 | components = [] 352 | for I in range(0, 3): 353 | if len(op_list) == 0: 354 | break 355 | k, v = op_list.pop(0) 356 | components.append('{0}={1}'.format(k, str(v))) 357 | print_line(', '.join(components)) 358 | indent(-5) 359 | 360 | def format_skill_header(skill_obj): 361 | attr = skill_obj['AttrType'] 362 | target_type = skill_obj['TargetType'] 363 | components = [] 364 | if attr != 'None': 365 | components.append(attr.lower()) 366 | if 'TriggerType' in skill_obj: 367 | trigger = skill_obj['TriggerType'] 368 | if trigger != 'None' and trigger != 'NextSkill': 369 | components.append('trigger = {0}'.format(trigger)) 370 | if 'DurationTimeMs' in skill_obj: 371 | duration = skill_obj['DurationTimeMs'] 372 | components.append('duration = {0}'.format(str(duration))) 373 | if 'TargetCount' in skill_obj: 374 | target_count = skill_obj['TargetCount'] 375 | if target_count != 1: 376 | components.append('target {0} {1}'.format(target_count, target_type)) 377 | else: 378 | components.append('target {0}'.format(target_type)) 379 | else: 380 | components.append('target {0}'.format(target_type)) 381 | return ', '.join(components) 382 | 383 | def dump_one_skill(creature_index, star, trans, name, index=None, book_mods=None): 384 | global skills_by_index 385 | skill = skills_by_index[index] 386 | 387 | def skill_absolute_time(skill): 388 | acting_time = int(skill.get('ActingTimeMs', 0)) 389 | duration_time = sum(map(int, skill.get('DurationTimeMs', [0]))) 390 | return acting_time + duration_time 391 | 392 | print_line('Skill: {0} [Duration = {1}]'.format(name, skill_absolute_time(skill))) 393 | 394 | indent(4) 395 | 396 | damaging_oper_types = ['GetPhysicalDamageR', 'GetMagicalDamageR', 'GetPhysicalDotR', 'GetMagicalDotR', 'GetStateDamageR'] 397 | 398 | def is_aoe(tick): 399 | return 'RadiusMm' in tick 400 | 401 | def damaging_tick_label(tick): 402 | type = tick['Type'] 403 | assert(type in damaging_oper_types) 404 | damage_type = 'P.DMG' if 'Physical' in type else 'M.DMG' 405 | styles = [] 406 | if is_aoe(tick): 407 | s = 'AOE' 408 | if 'ExcludeTarget' in tick.get('Flags', []): 409 | s = s + '-' 410 | styles.append(s) 411 | if 'Dot' in type: 412 | styles.append('DoT') 413 | style_str = '({0})'.format(','.join(styles)) if len(styles) > 0 else '' 414 | return damage_type + style_str 415 | 416 | def tick_scaling_formula(tick): 417 | values = tick['Value'] 418 | if not 'ValueFactor' in tick: 419 | value = values[0] 420 | try: 421 | value = float(value) / 1000.0 422 | except: 423 | pass 424 | return 'ATK*{0}'.format(value) 425 | 426 | factors = tick['ValueFactor'] 427 | power = get_operation_value(creature_index, star, trans, values, factors, 0, 0) 428 | level_factor = get_operation_value(creature_index, star, trans, values, factors, 1, 0) 429 | 430 | return 'ATK*{0}+{1}'.format(power, level_factor) 431 | 432 | def format_one_damaging_tick(tick): 433 | label = damaging_tick_label(tick) 434 | scaling = tick_scaling_formula(tick) 435 | return '{0}[{1}]'.format(label, scaling) 436 | 437 | def format_tick_one_line(tick_operations): 438 | do = list(filter(lambda x : x.get('Type', None) in damaging_oper_types, tick_operations)) 439 | if len(do) == 0: 440 | return '[Non-damaging tick]' 441 | formatted_ticks = [format_one_damaging_tick(x) for x in do] 442 | return ' + '.join(formatted_ticks) 443 | 444 | def dump_skill_attacks(atk_index, skill_obj): 445 | indent(2) 446 | id = skill_obj['Index'] 447 | print_line('Hit {0} (id {1}): '.format(atk_index, id), add_newline=False) 448 | 449 | ilvl = 13 + len(str(id)) 450 | indent(ilvl) 451 | operations = generate_skill_operations(skill_obj) 452 | ticks = generate_ticks(skill_obj, operations) 453 | is_continuation = True 454 | for t in enumerate(ticks): 455 | index, time, ops = t[0], t[1][0], t[1][1] 456 | print_line('[{0}] (t={1}): {2}'.format(index, time, format_tick_one_line(ops)), is_continuation=is_continuation) 457 | is_continuation = False 458 | indent(-ilvl) 459 | indent(-2) 460 | return 461 | 462 | atk_index = 1 463 | dump_skill_attacks(atk_index, skill) 464 | 465 | while 'NextIndex' in skill: 466 | atk_index = atk_index + 1 467 | skill = skills_by_index[skill['NextIndex']] 468 | dump_skill_attacks(atk_index, skill) 469 | 470 | indent(-4) 471 | 472 | def dump_heroes(): 473 | global creature_by_index 474 | global args 475 | for hero in filter(is_playable_hero, creature_by_index.values()): 476 | print_line(hero['CodeName']); 477 | indent(2) 478 | if args.auto_attacks: 479 | dump_one_skill(hero['Index'], 5, 5, 'Auto Attack', index=hero['BaseSkillIndex']); 480 | if args.skills: 481 | for i in range(4): 482 | index_str = 'SkillIndex{0}'.format(i+1) 483 | book_str = 'SkillExtend{0}'.format(i+1) 484 | dump_one_skill(hero['Index'], 5, 5, "S{0}".format(i+1), index=hero[index_str], book_mods=hero[book_str]) 485 | indent(-2) 486 | 487 | def dump_creature_star_grade_table(): 488 | global creature_star_grade_data 489 | global creature_by_index 490 | for index, data in creature_star_grade_data.items(): 491 | creature = creature_by_index[index] 492 | print('Name: {0}'.format(creature['CodeName'])) 493 | print(' | 1* | 2* | 3* | 4* | 5* | T1 | T2 | T3 | T4 | T5 |') 494 | for k, v in data.items(): 495 | if k == 'CreatureIndex': 496 | continue 497 | if k == 'Star': 498 | continue 499 | if k == 'Transcended': 500 | continue 501 | print('{0:>13} | {1:>6} | {2:>6} | {3:>6} | {4:>6} | {5:>6} | {6:>6} | {7:>6} | {8:>6} | {9:>6} | {10:>6} |'.format( 502 | k, str(v[0]), str(v[1]), str(v[2]), str(v[3]), str(v[4]), str(v[5]), str(v[6]), str(v[7]), str(v[8]), str(v[9]))) 503 | pass 504 | 505 | def main(): 506 | load_creatures_table() 507 | load_skills_table() 508 | load_skill_level_factors_table() 509 | load_creature_star_grade_data_table() 510 | 511 | if args.heros: 512 | dump_heroes() 513 | 514 | if args.star_grade_data: 515 | dump_creature_star_grade_table() 516 | 517 | try: 518 | args = parse_args() 519 | main() 520 | except SystemExit as e: 521 | if e.code != 0: 522 | print('An unknown error occurred. Please report this bug.') 523 | traceback.print_exc() 524 | except: 525 | print('An unknown error occurred. Please report this bug.') 526 | traceback.print_exc() -------------------------------------------------------------------------------- /krng.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import argparse 4 | import json 5 | import os 6 | import sys 7 | 8 | import nox 9 | 10 | print('Nox Macro Generator v2.1') 11 | print('By: cpp (Reddit: u/cpp_is_king, Discord: @cpp#0120)') 12 | print('Paypal: cppisking@gmail.com') 13 | print() 14 | 15 | if sys.version_info < (3,5): 16 | print('Error: This script requires Python 3.5 or higher. Please visit ' 17 | 'www.python.org and install a newer version.') 18 | print('Press any key to exit...') 19 | input() 20 | sys.exit(1) 21 | 22 | parser = argparse.ArgumentParser('Nox Macro Generator') 23 | parser.add_argument('--enable-developer-commands', action='store_true', default=False) 24 | args = parser.parse_args() 25 | 26 | macro_name = None 27 | file_path = None 28 | desc = None 29 | 30 | # These coordinate initial values are relative to a 1280x720 resolution, regardless of what 31 | # your actual resolution is. 32 | points = { 33 | 'buy': (965, 652), 34 | 'buy_confirm': (787, 520), 35 | 'exit': (152, 32), 36 | 'inventory' : (178, 644), 37 | 'grind' : (839,629), 38 | 'sell' : (1080,629), 39 | 'grind_all' : (730,629), 40 | 'grind_2' : (732,589), 41 | 'grind_confirm' : (738,531), 42 | 'dismiss_results' : (738,531), 43 | 'enter_node' : (1190,629), 44 | 'use_shop' : (636,561), 45 | 'abandon_raid' : (848, 590), 46 | 'start_raid' : (1183, 624), 47 | 48 | # x coordinate here is very precise so as to click the one unused pixel between 49 | # the hero's S1 and S2 abilities. 50 | 'start_adventure' : (1055, 660), 51 | 'stam_potion_select' : (641,379), 52 | 'stam_potion_confirm' : (635,546), 53 | 'confirm_insufficient_members' : (635,546) 54 | } 55 | 56 | rects = { 57 | 'exit_raid' : ((1171, 596), (1233, 654)), 58 | 'abandon_raid' : ((766, 589), (883, 641)), 59 | 'bid_raid' : ((905, 589), (1025, 641)), 60 | 'start_raid' : ((999, 621), (1182, 671)), 61 | 'raid_hero_lineup' : ((125, 182), (1151, 404)), 62 | 'raid_hero_select' : ((81, 483), (390, 683)), 63 | 'claim_reward' : ((766, 589), (1025, 641)), 64 | 'raid_info' : ((853, 615), (977, 680)), 65 | 'stam_potion' : ((593,292), (686, 387)), 66 | 'stam_potion_raid_5' : ((593,292), (675, 387)), 67 | } 68 | 69 | nox.initialize(points, rects) 70 | 71 | def confirm(properties = None, start_condition = None, notes = []): 72 | global macro_name 73 | global file_path 74 | global desc 75 | 76 | if properties is None: 77 | properties = {} 78 | 79 | print() 80 | if macro_name: 81 | print('Destination Macro Name: {0}'.format(macro_name)) 82 | print('Destination File: {0}'.format(file_path)) 83 | print('Selected Macro: {0}'.format(desc)) 84 | if len(properties) > 0: 85 | print('Properties:') 86 | for (k,v) in properties.items(): 87 | print(' {0}: {1}'.format(k, v)) 88 | if start_condition is not None: 89 | print(' Start Condition: {0}'.format(start_condition)) 90 | 91 | for n in notes: 92 | print('Note: {0}'.format(n)) 93 | 94 | print('Press Enter to confirm or Ctrl+C to cancel. ', end = '') 95 | nox.do_input() 96 | 97 | print('************************************** WARNING *************************************************') 98 | print('* Please watch the macro for the first few cycles to make sure everything is working as *\n' 99 | '* intended. If you are selling or grinding gear, make sure your Sell All and Grind All screen *\n' 100 | '* is pre-configured with the appropriate values. For extra security, make sure all valuable *\n' 101 | '* items are locked. *\n' 102 | '************************************************************************************************') 103 | 104 | nox.wait(500) 105 | 106 | def grind_or_sell_all(is_grind): 107 | # Grind 108 | button = 'grind' if is_grind else 'sell' 109 | 110 | nox.click_button(button, 3500) 111 | 112 | # Grind all 113 | nox.click_button('grind_all', 3500) 114 | 115 | # Click the Grind button on the window that pops up 116 | nox.click_button('grind_2', 3500) 117 | 118 | # Confirmation 119 | nox.click_button('grind_confirm', 3500) 120 | 121 | if is_grind: 122 | # Click on the screen to get rid of the results 123 | nox.click_button('dismiss_results', 3500) 124 | 125 | def gen_grindhouse(): 126 | # The generated macro assumes you are on the Buy screen, Tier 3 is already selected, and an item is 127 | # highlighted. 128 | print() 129 | items_to_buy = nox.prompt_user_for_int( 130 | "How many items should be purchased each cycle?\n" 131 | "If you don't have at least this much inventory space the macro will de-sync.\n" 132 | "Number of items to purchase: ") 133 | 134 | print() 135 | buy_delay = nox.prompt_user_for_int("Enter the number of milliseconds between each click while purchasing\n" 136 | "items. A lower number will make the macro run faster, but could cause\n" 137 | "the macro to get out of sync on slower machines. If the macro doesn't\n" 138 | "register clicks properly while buying items, run the generator again\n" 139 | "and choose a higher number until you find what works.\n\n" 140 | "Milliseconds (Default=325): ", default=325) 141 | 142 | confirm(properties={'Items to buy' : items_to_buy, 'Delay' : buy_delay }, 143 | start_condition='The macro should be started from the forge shop, with an item selected.') 144 | 145 | # Buy 300 items 146 | for i in range(0, items_to_buy): 147 | nox.click_button('buy', buy_delay) 148 | nox.click_button('buy_confirm', buy_delay) 149 | 150 | # Exit twice (to Orvel map) 151 | nox.click_button('exit', 1500) 152 | nox.click_button('exit', 1500) 153 | 154 | # Open inventory 155 | nox.click_button('inventory', 1500) 156 | 157 | grind_or_sell_all(True) 158 | 159 | # Exit back to Orvel world map 160 | nox.click_button('exit', 1500) 161 | 162 | # Re-enter the shop. Delay set to 2500 here since there's an animated transition 163 | # that takes a little extra time 164 | nox.click_button('enter_node', 5000) 165 | 166 | # Click Use Shop button 167 | nox.click_button('use_shop', 1500) 168 | 169 | def gen_raid_experimental(): 170 | 171 | confirm() 172 | nox.click_button('start_raid', 10000) 173 | nox.click_button('stam_potion_confirm', 1000) 174 | nox.click_button('abandon_raid', 1000) 175 | 176 | def gen_raid(): 177 | confirm(start_condition='The macro can be started in a raid lobby or while a raid is in progress.') 178 | 179 | nox.click_button('start_raid', 5000) 180 | nox.click_button('confirm_insufficient_members', 500) 181 | nox.click_button('abandon_raid', 5000) 182 | 183 | def gen_raid_leader(): 184 | confirm(start_condition='The macro can be started in a raid lobby or while a raid is in progress.') 185 | 186 | for i in range(0, 10): 187 | nox.click_button('start_raid', 300) 188 | nox.click_button('confirm_insufficient_members', 500) 189 | nox.click_button('abandon_raid', 5000) 190 | 191 | def manage_inventory(should_grind, should_sell): 192 | if should_grind: 193 | grind_or_sell_all(True) 194 | if should_sell: 195 | grind_or_sell_all(False) 196 | 197 | def prompt_inventory_management_properties(): 198 | choice = nox.prompt_choices( 199 | 'Should I (G)rind All or (S)ell All?', ['G', 'S']) 200 | 201 | if choice.lower() == 'g': 202 | return (True, False) 203 | 204 | return (False, True) 205 | 206 | def do_generate_inventory_management_for_adventure(should_grind, should_sell): 207 | # At this point we're at the victory screen. We need to click the Inventory button on the 208 | # left side. This involves a loading screen and can take quite some time, so wait 15 seconds. 209 | nox.click_loc((80, 230), 15000) 210 | 211 | manage_inventory(should_grind, should_sell) 212 | 213 | # Exit back to Orvel map 214 | nox.click_button('exit', 3500) 215 | 216 | def re_enter_adventure(use_potion): 217 | # Re-enter the map. Since there's a loading transition, this takes a little extra time. 218 | nox.click_button('enter_node', 3500) 219 | 220 | # Prepare battle -> start adventure. 221 | nox.click_button('start_adventure', 3500) 222 | nox.click_button('start_adventure', 3500) 223 | 224 | # The stamina window may have popped up. Use a potion 225 | if use_potion: 226 | nox.click_loc((759, 558), 2000) # Stamina Potion OK 227 | 228 | 229 | def gen_natural_stamina_farm(): 230 | print() 231 | use_pot = nox.prompt_user_yes_no( 232 | "Should the macro automatically use a stamina potion when you run out?") 233 | 234 | inventory_management = nox.prompt_user_for_int( 235 | "Enter the frequency (in minutes) at which to manage inventory.\n" 236 | "To disable inventory management, press Enter without entering a value: ", min=1, 237 | default = -1) 238 | 239 | inv_management_sync = None 240 | properties={'Use Potion': use_pot} 241 | 242 | notes = [] 243 | if inventory_management != -1: 244 | inv_management_sync = nox.prompt_user_for_int( 245 | 'Enter the maximum amount of time (in whole numbers of minutes) it takes your team\n' 246 | 'to complete a story dungeon. (Default = 3): ', default = 3) 247 | (should_grind, should_sell) = prompt_inventory_management_properties() 248 | properties['Inventory Management Sync Time'] = '{0} minutes'.format(inv_management_sync) 249 | s = None 250 | if should_grind and should_sell: 251 | s = "Sell then grind" 252 | elif should_grind: 253 | s = "Grind" 254 | else: 255 | s = "Sell" 256 | properties['Manage Inventory'] = '{0} every {1} minutes'.format(s, inventory_management) 257 | notes=['When the macro is getting ready to transition to the inventory management\n' 258 | ' phase, it may appear the macro is stuck doing nothing on the victory screen.\n' 259 | ' This is intentional, and it can take up to {0} minutes before the transition\n' 260 | ' to the inventory screen happens.'.format(inv_management_sync)] 261 | else: 262 | properties['Manage Inventory'] = 'Never' 263 | 264 | confirm( 265 | properties=properties, 266 | start_condition='The macro should be started while a battle is in progress.', 267 | notes=notes) 268 | 269 | def generate_one_click_cycle(): 270 | # No effect during battle or on victory screen, but if we get stuck in Get Ready 271 | # for Battle screen after inventory management, this starts us again. Make sure 272 | # to do this BEFORE the continue button, otherwise the continue button will click 273 | # one of our heroes and remove them from the lineup. By putting this click first 274 | # it guarantees that we either enter the battle, or get the stamina window (in 275 | # which case the click doesn't go through to the button underneath). 276 | nox.click_button('start_adventure', 500) 277 | 278 | # Be careful with the x coordinate here so that it clicks in between items in the 279 | # inventory if your inventory is full. 280 | nox.click_loc((503, 352), 500) # Continue (game pauses sometimes mid-battle) 281 | 282 | nox.click_loc((1204, 494), 500) # Retry 283 | nox.click_loc((572, 467), 500) # Single Repeat button. Careful not to click the button that 284 | # edits the count of stamina potions to use. 285 | if use_pot: 286 | nox.click_loc((759, 558), 500) # Stamina Potion OK 287 | else: 288 | nox.click_loc((940, 190), 500) # Close stamina pop-up 289 | 290 | if inventory_management == -1: 291 | # If we don't need to manage inventory, just generate a simple macro that can loop forever. 292 | generate_one_click_cycle() 293 | else: 294 | # If we do need to manage inventory, then first generate enough cycles of the normal story 295 | # repeat to fill up the entire specified number of minutes. 296 | nox.repeat_generator_for(generate_one_click_cycle, inventory_management * 60) 297 | 298 | # Then switch to a mode where we just try to get to the victory screen but not initiate a 299 | # repeat. We do this by just clicking the continue button every second for 2 minutes. 300 | # Hopefully 3 minutes is enough to finish any story level. 301 | def get_to_victory_screen(): 302 | # Continue (game pauses sometimes mid-battle) 303 | nox.click_loc((503, 352), 1000) 304 | 305 | # Need to make sure to click below the loot results so they get dismissed properly 306 | nox.click_loc((503, 500), 1000) 307 | 308 | nox.repeat_generator_for(get_to_victory_screen, inv_management_sync * 60) 309 | 310 | # At this point the Inventory button on the top left side of the victory should be clickable. 311 | # so initiate the process of clicking, grinding/selling, and getting back into the battle. 312 | do_generate_inventory_management_for_adventure(should_grind, should_sell) 313 | 314 | # Re-enter the battle from the world map, using a potion if necessary 315 | re_enter_adventure(use_pot) 316 | 317 | try: 318 | macro_generators = [ 319 | ("NPC Gear Purchasing and Grinding", gen_grindhouse), 320 | ("AFK Raid (Member)", gen_raid), 321 | ("AFK Raid (Leader)", gen_raid_leader), 322 | ("Story Repeat w/ Natural Stamina Regen", gen_natural_stamina_farm), 323 | ] 324 | if args.enable_developer_commands: 325 | macro_generators.extend([ 326 | ("**DEV** Natural Stamina Regen Raid Farming (Non-Leader)", gen_raid_experimental), 327 | ("**DEV** Re-enter adventure (potion)", lambda : re_enter_adventure(True)), 328 | ("**DEV** Re-enter adventure (no potion)", lambda : re_enter_adventure(False)), 329 | ]) 330 | 331 | print() 332 | for (n,(desc,fn)) in enumerate(macro_generators): 333 | print('{0}) {1}'.format(n+1, desc)) 334 | 335 | macro_number = nox.prompt_user_for_int('Enter the macro you wish to generate: ', 336 | min=1, max=len(macro_generators)) 337 | 338 | (macro_name, file_path) = nox.load_macro_file() 339 | 340 | (desc, fn) = macro_generators[macro_number - 1] 341 | 342 | # Generate the macro 343 | fn() 344 | 345 | # At this point we're back where we started and the macro can loop. 346 | nox.close() 347 | 348 | print('File {0} successfully written.'.format(file_path)) 349 | except SystemExit: 350 | pass 351 | except: 352 | print('Something happened. Please report this and paste the below text.') 353 | import traceback 354 | traceback.print_exc() 355 | print('Press any key to exit') 356 | nox.do_input() -------------------------------------------------------------------------------- /nox.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import json 4 | import os 5 | import re 6 | import sys 7 | 8 | # Don't change anything in this file unless you know what you're doing. 9 | # ================================================================================================================== 10 | 11 | file = None 12 | button_points = {} 13 | button_rects = {} 14 | resolution = (1280,720) 15 | time = 0 16 | 17 | def do_input(): 18 | return input() 19 | 20 | def wait(amount): 21 | global time 22 | time = time + amount 23 | 24 | def error(message): 25 | print(message) 26 | do_input() 27 | sys.exit(1) 28 | 29 | def repeat_generator_for(fn, seconds): 30 | global time 31 | initial = time 32 | milliseconds = seconds * 1000 33 | 34 | while time - initial < milliseconds: 35 | fn() 36 | 37 | def click_loc(loc, wait_milliseconds): 38 | global file 39 | global resolution 40 | global time 41 | 42 | def scale(xy): 43 | global resolution 44 | return (int(xy[0]*resolution[0]/1280), 45 | int(xy[1]*resolution[1]/720)) 46 | 47 | x, y = scale(loc) 48 | file.write("0ScRiPtSePaRaToR{0}|{1}|MULTI:1:0:{2}:{3}ScRiPtSePaRaToR{4}\n".format( 49 | resolution[0], resolution[1], x, y, time)) 50 | 51 | # This is the delay between pressing the button and releasing the button. If you set it to be too fast, 52 | # the device won't register a click properly. In my experience 100ms is about as fast as you can get 53 | # to have all clicks properly registered. 54 | wait(100) 55 | file.write("0ScRiPtSePaRaToR{0}|{1}|MULTI:0:6ScRiPtSePaRaToR{2}\n".format(resolution[0], resolution[1], time)) 56 | file.write("0ScRiPtSePaRaToR{0}|{1}|MULTI:0:6ScRiPtSePaRaToR{2}\n".format(resolution[0], resolution[1], time)) 57 | file.write("0ScRiPtSePaRaToR{0}|{1}|MULTI:0:1ScRiPtSePaRaToR{2}\n".format(resolution[0], resolution[1], time)) 58 | file.write("0ScRiPtSePaRaToR{0}|{1}|MSBRL:-1158647:599478ScRiPtSePaRaToR{2}\n".format(resolution[0], resolution[1], time)) 59 | 60 | # This is the delay between finishing one click and beginning the next click. This needs to account 61 | # for how fast the game can transition from one screen to the next. For example, if you're repeatedly 62 | # clicking a buy button with the game not really doing anything between each click, this can be very 63 | # low. On the other hand, if a click causes the game to transition from one screen to another (e.g. 64 | # using a portal and the game having to load into Orvel and load an entirely new area) then it should 65 | # be fairly high. 66 | wait(wait_milliseconds) 67 | 68 | def click_button(button, wait_milliseconds): 69 | global button_points 70 | loc = button_points[button] 71 | return click_loc(loc, wait_milliseconds) 72 | 73 | def click_rect(rect, wait_milliseconds, dont_click = None): 74 | '''Click a single rectangle, optionally *not* clicking in any one of a list of rectangles''' 75 | 76 | global button_rects 77 | coords = button_rects[rect] 78 | centerx = int((coords[0][0] + coords[1][0]) / 2) 79 | centery = int((coords[0][1] + coords[1][1]) / 2) 80 | return click_loc((centerx, centery), wait_milliseconds) 81 | 82 | def click_rects(rect_list, wait_milliseconds, dont_click = None): 83 | '''Click a list of rectangles, one after the other with a specified delay between each click. 84 | By passing a list for the `dont_click` argument, the algorithm will guarantee *not* to click 85 | any point in the specified list of rectangles.''' 86 | for r in rect_list: 87 | click_rect(r, wait_milliseconds, dont_click=dont_click) 88 | 89 | def prompt_user_for_int(message, default=None, min=None, max=None): 90 | result = None 91 | while True: 92 | print(message, end='') 93 | result = do_input() 94 | 95 | if default is not None and len(result) == 0: 96 | result = default 97 | break 98 | 99 | if not is_integer(result): 100 | print('Value is not an integer.') 101 | continue 102 | 103 | result = int(result) 104 | if min is not None and result < min: 105 | print('Invalid value. Must be at least {0}'.format(min)) 106 | continue 107 | 108 | if max is not None and result > max: 109 | print('Invalid value. Must be no larger than {0}'.format(max)) 110 | continue 111 | 112 | break 113 | return int(result) 114 | 115 | def prompt_choices(message, choices, default=None): 116 | result = default 117 | choice_str = '/'.join(choices) 118 | default_str = ' (default={0})'.format(default) if default else '' 119 | 120 | lower_choices = [x.lower() for x in choices] 121 | 122 | message = '{0} ({1}){2}: '.format(message, choice_str, default_str) 123 | while True: 124 | print(message, end='') 125 | input = do_input() 126 | if len(input) == 0: 127 | if default is not None: 128 | return default 129 | continue 130 | 131 | input = input.lower() 132 | if input in lower_choices: 133 | return input 134 | 135 | return None 136 | 137 | def prompt_user_yes_no(message, default=False): 138 | result = default 139 | message = "{0} (Y/N) (default={1}): ".format(message, "Y" if default else "N") 140 | while True: 141 | print(message, end='') 142 | input = do_input() 143 | if len(input) == 0: 144 | result = default 145 | break 146 | input = input.lower() 147 | if input == 'n': 148 | result = False 149 | break 150 | if input == 'y': 151 | result = True 152 | break 153 | return result 154 | 155 | def find_nox_install(): 156 | app_data = None 157 | if sys.platform == 'darwin': 158 | user_dir = os.path.expanduser('~') 159 | app_data = os.path.join(user_dir, 'Library', 'Application Support') 160 | elif sys.platform == 'win32': 161 | app_data = os.environ.get('LOCALAPPDATA', None) 162 | 163 | if not app_data: 164 | error('Could not get local app data folder. Exiting...') 165 | 166 | nox_folder = os.path.join(app_data, 'Nox') 167 | if not os.path.exists(nox_folder): 168 | nox_folder = os.path.join(app_data, 'Nox App Player') 169 | 170 | if not os.path.exists(nox_folder): 171 | error('Could not find Nox local app data folder. Exiting...') 172 | 173 | nox_record_folder = os.path.join(nox_folder, 'record') 174 | nox_records_file = os.path.join(nox_record_folder, 'records') 175 | if not os.path.exists(nox_records_file): 176 | error('Missing or invalid Nox macro folder. Record an empty macro via the Nox UI then run this script again.') 177 | 178 | return nox_record_folder 179 | 180 | def is_integer(s): 181 | try: 182 | n = int(s) 183 | return True 184 | except: 185 | pass 186 | 187 | return False 188 | 189 | def select_macro_interactive(json_obj): 190 | if len(json_obj) == 0: 191 | error('The records file contains no macros. Record a dummy macro in Nox and try again.') 192 | index = 0 193 | keys = list(json_obj.keys()) 194 | if len(json_obj) > 1: 195 | print() 196 | for (n,key) in enumerate(keys): 197 | print('{0}) {1}'.format(n+1, json_obj[key]['name'])) 198 | value = prompt_user_for_int('Enter the macro you wish to overwrite: ', min=1, max=len(json_obj)) 199 | index = value - 1 200 | key = keys[index] 201 | return key 202 | 203 | def select_resolution_interactive(): 204 | global resolution 205 | while True: 206 | print('Enter your emulator resolution (or press Enter for 1280x720): ', end = '') 207 | res = do_input().strip() 208 | if len(res) == 0: 209 | resolution = (1280, 720) 210 | return 211 | else: 212 | match = re.fullmatch(r'(\d+)x(\d+)', res) 213 | if match is not None and is_integer(match[1]) and is_integer(match[2]): 214 | resolution = (int(match[1]), int(match[2])) 215 | return 216 | 217 | def get_nox_macro_interactive(): 218 | nox_folder = find_nox_install() 219 | records_file = os.path.join(nox_folder, 'records') 220 | fp = open(records_file, 'r') 221 | json_obj = json.load(fp) 222 | macro_key = select_macro_interactive(json_obj) 223 | macro_file = os.path.join(nox_folder, macro_key) 224 | 225 | name = json_obj[macro_key]['name'] 226 | 227 | fp.close() 228 | return (name, macro_file) 229 | 230 | def initialize(points, rects): 231 | global button_points 232 | global button_rects 233 | select_resolution_interactive() 234 | 235 | button_points = points 236 | button_rects = rects 237 | 238 | def load_macro_file(): 239 | global file 240 | name = None 241 | file_path = None 242 | (name, file_path) = get_nox_macro_interactive() 243 | 244 | file = open(file_path, 'w') 245 | return (name, file_path) 246 | 247 | def close(): 248 | global file 249 | file.close() 250 | --------------------------------------------------------------------------------