2023-07-28 15:57:21 +09:30
|
|
|
|
extends Node
|
|
|
|
|
|
2023-08-02 20:18:29 +09:30
|
|
|
|
var base_resolution: Vector2 = Vector2(ProjectSettings.get_setting('display/window/size/width'), ProjectSettings.get_setting('display/window/size/height')) # Vector2(640, 360)
|
2023-08-02 19:48:45 +09:30
|
|
|
|
var shrink_timer := Timer.new()
|
2023-08-04 14:36:43 +09:30
|
|
|
|
var last_resolution_pre_shrink := Vector2.ZERO
|
2023-08-02 19:48:45 +09:30
|
|
|
|
|
2023-08-04 18:17:08 +09:30
|
|
|
|
var SNES_PSX_addresses := load_tsv('res://data/SNES_PSX_addresses.tsv')
|
|
|
|
|
|
2023-08-07 19:34:46 +09:30
|
|
|
|
# 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,
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-04 18:17:08 +09:30
|
|
|
|
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)
|
|
|
|
|
|
2023-08-03 17:27:03 +09:30
|
|
|
|
static func load_json(filename: String): # Valid JSON will return Array or Dictionary. int error code for anything else.
|
2023-07-28 15:57:21 +09:30
|
|
|
|
var file := File.new()
|
2023-08-03 17:27:03 +09:30
|
|
|
|
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
|
|
|
|
|
|
2023-08-04 18:17:08 +09:30
|
|
|
|
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:
|
2023-12-10 23:17:06 +10:30
|
|
|
|
output.append(file.get_line().replace('\\n', '\n').replace('’', "'").replace('”', '"')) # TODO: Fix font to accept the special quote marks
|
2023-08-04 18:17:08 +09:30
|
|
|
|
var error:
|
|
|
|
|
print_debug('Failed to open glyph table "%s" - %d' % [filename, error])
|
|
|
|
|
return output
|
|
|
|
|
|
2023-08-03 17:27:03 +09:30
|
|
|
|
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
|
2023-07-31 23:33:05 +09:30
|
|
|
|
|
2023-08-04 18:17:08 +09:30
|
|
|
|
static func limited_eval(token: String):
|
|
|
|
|
# Try int literal
|
|
|
|
|
if token.is_valid_integer():
|
|
|
|
|
return int(token)
|
|
|
|
|
# Try float literal
|
|
|
|
|
if token.is_valid_float():
|
|
|
|
|
return float(token)
|
2023-12-11 00:34:46 +10:30
|
|
|
|
# Try hexadecimal literal
|
|
|
|
|
if token.is_valid_hex_number(true):
|
|
|
|
|
return token.hex_to_int()
|
2023-08-04 18:17:08 +09:30
|
|
|
|
# Try bool literal
|
|
|
|
|
match token.to_lower():
|
|
|
|
|
'true':
|
|
|
|
|
return true
|
|
|
|
|
'false':
|
|
|
|
|
return false
|
|
|
|
|
# It's just a string
|
|
|
|
|
return token
|
|
|
|
|
|
|
|
|
|
|
2023-07-31 23:33:05 +09:30
|
|
|
|
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:
|
2023-08-04 18:17:08 +09:30
|
|
|
|
entry[headers[i]] = limited_eval(line[i])
|
2023-12-10 23:17:06 +10:30
|
|
|
|
else:
|
|
|
|
|
entry[headers[i]] = limited_eval('')
|
2023-07-31 23:33:05 +09:30
|
|
|
|
output[line[0]] = entry
|
|
|
|
|
return output
|
|
|
|
|
print_debug(error)
|
|
|
|
|
return {}
|
|
|
|
|
|
2023-08-02 19:48:45 +09:30
|
|
|
|
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)
|
2023-08-02 20:18:29 +09:30
|
|
|
|
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])
|
2023-08-05 22:11:02 +09:30
|
|
|
|
get_tree().set_screen_stretch(SceneTree.STRETCH_MODE_VIEWPORT, SceneTree.STRETCH_ASPECT_KEEP, base_resolution*scale, scale)
|
2023-08-02 19:48:45 +09:30
|
|
|
|
|
|
|
|
|
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'):
|
2023-08-04 14:36:43 +09:30
|
|
|
|
var target_size := base_resolution * scale
|
2023-08-02 19:48:45 +09:30
|
|
|
|
# OS.window_size = base_resolution*scale
|
2023-08-04 14:36:43 +09:30
|
|
|
|
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:
|
2023-08-05 22:11:02 +09:30
|
|
|
|
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])
|
2023-08-04 14:36:43 +09:30
|
|
|
|
self.last_resolution_pre_shrink = size
|
|
|
|
|
shrink_timer.paused = false
|
2023-08-05 22:11:02 +09:30
|
|
|
|
shrink_timer.start(delay)
|
|
|
|
|
get_tree().set_screen_stretch(SceneTree.STRETCH_MODE_VIEWPORT, SceneTree.STRETCH_ASPECT_KEEP, base_resolution*scale, scale)
|
2023-08-02 19:48:45 +09:30
|
|
|
|
|
2023-08-04 18:17:08 +09:30
|
|
|
|
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]
|
|
|
|
|
|
2023-08-02 19:48:45 +09:30
|
|
|
|
func _ready():
|
2023-08-07 01:15:55 +09:30
|
|
|
|
match shrink_timer.connect('timeout', self, 'shrink_to_integer'):
|
|
|
|
|
OK:
|
|
|
|
|
pass
|
|
|
|
|
_:
|
|
|
|
|
assert(false)
|
2023-08-02 20:18:29 +09:30
|
|
|
|
shrink_timer.one_shot = true
|
2023-08-02 19:48:45 +09:30
|
|
|
|
add_child(shrink_timer)
|
2023-08-07 01:15:55 +09:30
|
|
|
|
match get_tree().connect('screen_resized', self, 'update_window_scale'):
|
|
|
|
|
OK:
|
|
|
|
|
pass
|
|
|
|
|
_:
|
|
|
|
|
assert(false)
|
2023-08-02 19:48:45 +09:30
|
|
|
|
OS.set_min_window_size(base_resolution*2)
|