extends Node var base_resolution: Vector2 = Vector2(ProjectSettings.get_setting('display/window/size/width'), ProjectSettings.get_setting('display/window/size/height')) # Vector2(640, 360) var shrink_timer := Timer.new() var last_resolution_pre_shrink := Vector2.ZERO var SNES_PSX_addresses := load_tsv('res://data/SNES_PSX_addresses.tsv') # Canonicalize english stat names within the codebase # This is based off of FF8, and is more or less RPGe but with Speed instead of Agility # I don't like Vigor or Stamina, Agility could go either way but I side with FF8 and FF9 rather than FF10 enum { STAT_STRENGTH = 0, STAT_STR = 0, STAT_SPEED = 1, STAT_SPD = 1, STAT_VITALITY = 2, STAT_VIT = 2, STAT_MAGIC = 3, STAT_MAG = 3, } static func eval(expression: String): var ex = Expression.new() match ex.parse(expression): OK: return ex.execute() var error: print_debug(ex.get_error_text(), error) static func load_json(filename: String): # Valid JSON will return Array or Dictionary. int error code for anything else. var file := File.new() match file.open(filename, File.READ): OK: var result = JSON.parse(file.get_as_text()) match result.error: OK: return result.result var error: print_debug(result.error_string) print_debug(result.error_line) return error var error: return error static func _to_json(data, current_indent: int = 0) -> String: match typeof(data): TYPE_RAW_ARRAY: # XX: add more as required return str(data) TYPE_ARRAY: var contents := PoolStringArray() for value in data: contents.append(_to_json(value, current_indent + 1)) return '[' + ', '.join(contents) + ']\n' TYPE_DICTIONARY: var contents := PoolStringArray() for k in data: contents.append('"%s": %s' % [k, _to_json(data[k], current_indent + 1)]) return '{' + ', '.join(contents) + '}\n' _: return JSON.print(data) static func save_json(filename: String, data) -> int: # Returns error code but Error is not usable as return signature # Oh cool, 3.6.beta2's json serialization is borked on PoolByteArray and PoolVector2Array # print(JSON.print([PoolByteArray([0,1]), PoolIntArray([0,1]), PoolRealArray([0,1]), PoolVector2Array([Vector2.ONE, Vector2.LEFT]), PoolStringArray(['hello', 'world'])])) # ["[0, 1]",[0,1],[0,1],"[(1, 1), (-1, 0)]",["hello","world"]] var file := File.new() var error := file.open(filename, File.WRITE) if error != OK: return error # file.store_line(to_json(data)) # file.store_line(JSON.print(data)) file.store_line(_to_json(data, 0)) return OK static func load_glyph_table(filename: String) -> PoolStringArray: var output = PoolStringArray() var file = File.new() match file.open(filename, File.READ): OK: var l: int = file.get_len() while file.get_position() < l: output.append(file.get_line()) var error: print_debug('Failed to open glyph table "%s" - %d' % [filename, error]) return output static func genericize_type(type: int) -> int: match type: TYPE_RAW_ARRAY, TYPE_INT_ARRAY, TYPE_REAL_ARRAY, TYPE_STRING_ARRAY: return TYPE_ARRAY TYPE_INT: return TYPE_REAL _: return type static func are_values_equal(v1, v2, fuzzy_types: bool = true) -> bool: var t1 := typeof(v1) var t2 := typeof(v2) if fuzzy_types: t1 = genericize_type(t1) t2 = genericize_type(t2) if t1 != t2: print_debug('Type mismatch: %s vs %s' % [t1, t2]) return false match t1: TYPE_ARRAY: return are_arrays_equal(v1, v2, fuzzy_types) TYPE_DICTIONARY: return are_dictionaries_equal(v1, v2, fuzzy_types) _: return v1 == v2 static func are_dictionaries_equal(d1: Dictionary, d2: Dictionary, fuzzy_types: bool = true) -> bool: if len(d1) != len(d2): print_debug('Array lengths not equal: %s vs %s' % [len(d1), len(d2)]) return false for k in d1: if not (k in d2): print_debug('d1 has key "%s" that d2 does not' % k) return false if not are_values_equal(d1[k], d2[k], fuzzy_types): print_debug('d1["%s"] != d2["%s"]' % [k, k]) print_debug(d1[k]) print_debug(d2[k]) return false return true static func are_arrays_equal(a1: Array, a2: Array, fuzzy_types: bool = true) -> bool: var l1 := len(a1) if l1 != len(a2): print_debug('Array lengths not equal: %s vs %s' % [l1, len(a2)]) return false for i in l1: if not are_values_equal(a1[i], a2[i], fuzzy_types): print_debug('a1[%d] != a2[%d]' % [i, i]) print_debug(a1[i]) print_debug(a2[i]) return false return true static func limited_eval(token: String): # Try hexadecimal literal if token.begins_with('0x'): var hex := token.hex_to_int() if hex > 0: return hex # Try int literal if token.is_valid_integer(): return int(token) # Try float literal if token.is_valid_float(): return float(token) # Try bool literal match token.to_lower(): 'true': return true 'false': return false # It's just a string return token static func load_tsv(filename: String, delimiter: String = '\t') -> Dictionary: var file := File.new() var error := file.open(filename, File.READ) if error == OK: var headers := file.get_csv_line(delimiter) var n := headers.size() var output = {} while file.get_position() < file.get_len(): var line := file.get_csv_line(delimiter) var entry := {} for i in range(1, n): if line.size() > i: entry[headers[i]] = limited_eval(line[i]) output[line[0]] = entry return output print_debug(error) return {} func shrink_to_integer(): var size := OS.get_window_size() var scale_vec := size / base_resolution var scale_f := max(min(scale_vec.x, scale_vec.y), 1) var scale := int(scale_f) var new_window_size := base_resolution*scale if OS.get_window_size() != new_window_size: # avoid retriggering this event forever OS.set_window_size(new_window_size) print('resized to scale %d %s' % [scale, new_window_size]) get_tree().set_screen_stretch(SceneTree.STRETCH_MODE_VIEWPORT, SceneTree.STRETCH_ASPECT_KEEP, base_resolution*scale, scale) func update_window_scale(): if OS.window_size < base_resolution*2: OS.window_size = base_resolution*2 var size := OS.get_window_size() var scale_vec := size / base_resolution var scale_f := max(min(scale_vec.x, scale_vec.y), 1) var scale := int(scale_f) if ProjectSettings.get_setting('display/window/size/snap_to_integer'): var target_size := base_resolution * scale # OS.window_size = base_resolution*scale if size == target_size: print_debug('Config is to shrink down to integer scale, but %s is already a perfect %dx scale' % [size, scale]) elif size == self.last_resolution_pre_shrink: # Don't fight the WM if it sets the window size back again print_debug("Config is to shrink down to integer scale, but %s was the last external resize event, so we won't fight the window manager" % size) else: var delay = 1.0 - min((size - self.last_resolution_pre_shrink).length_squared()/1000, 0.95) print_debug('Setting lastres to %s, will resize to %dx == %s in %f seconds' % [self.last_resolution_pre_shrink, scale, target_size, delay]) self.last_resolution_pre_shrink = size shrink_timer.paused = false shrink_timer.start(delay) get_tree().set_screen_stretch(SceneTree.STRETCH_MODE_VIEWPORT, SceneTree.STRETCH_ASPECT_KEEP, base_resolution*scale, scale) static func game_time_frames_to_hhmm(game_time_frames: int) -> String: var game_seconds = game_time_frames / 60 var game_minutes = game_seconds / 60 var game_hours = game_minutes / 60 return '%d:%02d' % [game_hours, game_minutes % 60] func _ready(): match shrink_timer.connect('timeout', self, 'shrink_to_integer'): OK: pass _: assert(false) shrink_timer.one_shot = true add_child(shrink_timer) match get_tree().connect('screen_resized', self, 'update_window_scale'): OK: pass _: assert(false) OS.set_min_window_size(base_resolution*2)