maxgame/addons/popochiu/popochiu_resources.gd
2025-07-17 01:49:18 -04:00

538 lines
16 KiB
GDScript

class_name PopochiuResources
extends Node
enum Types {
ROOM,
CHARACTER,
INVENTORY_ITEM,
DIALOG,
# Room object types
PROP,
HOTSPOT,
REGION,
MARKER,
WALKABLE_AREA
}
enum CursorType {
NONE,
ACTIVE,
DOWN,
IDLE,
LEFT,
LOOK,
RIGHT,
SEARCH,
TALK,
UP,
USE,
WAIT,
}
enum AudioTypes {
NONE = -1,
MUSIC,
SOUND_EFFECT,
VOICE,
UI
}
# PLUGIN -------------------------------------------------------------------------------------------
const MAIN_DOCK_PATH = "res://addons/popochiu/editor/main_dock/popochiu_dock.tscn"
const MAIN_TYPES = [
Types.ROOM, Types.CHARACTER, Types.INVENTORY_ITEM, Types.DIALOG
]
const ROOM_TYPES = [Types.PROP, Types.HOTSPOT, Types.REGION, Types.MARKER, Types.WALKABLE_AREA]
const DOCUMENTATION = "https://carenalgas.github.io/popochiu/"
const CFG = "res://addons/popochiu/plugin.cfg"
const GUI_ADDON_FOLDER = "res://addons/popochiu/engine/objects/gui/"
const GUI_TEMPLATES_FOLDER = GUI_ADDON_FOLDER + "templates/"
const GUI_SCRIPT_TEMPLATES_FOLDER = "res://addons/popochiu/engine/templates/gui/"
const RETRO_RESOLUTION = Vector2(356.0, 200.0)
# SINGLETONS ---------------------------------------------------------------------------------------
const GLOBALS_SNGL = "res://game/popochiu_globals.gd"
const UTILS_SNGL = "res://addons/popochiu/engine/others/popochiu_utils.gd"
const CURSOR_SNGL = "res://addons/popochiu/engine/cursor/cursor.tscn"
const POPOCHIU_SNGL = "res://addons/popochiu/engine/popochiu.tscn"
const IROOM = "res://addons/popochiu/engine/interfaces/i_room.gd"
const ICHARACTER = "res://addons/popochiu/engine/interfaces/i_character.gd"
const IINVENTORY = "res://addons/popochiu/engine/interfaces/i_inventory.gd"
const IDIALOG = "res://addons/popochiu/engine/interfaces/i_dialog.gd"
const IGRAPHIC_INTERFACE_SNGL = "res://addons/popochiu/engine/interfaces/i_graphic_interface.gd"
const IAUDIO = "res://addons/popochiu/engine/interfaces/i_audio.gd"
const R_SNGL = "res://game/autoloads/r.gd"
const C_SNGL = "res://game/autoloads/c.gd"
const I_SNGL = "res://game/autoloads/i.gd"
const D_SNGL = "res://game/autoloads/d.gd"
const A_SNGL = "res://game/autoloads/a.gd"
const G_SNGL = "res://game/autoloads/g.gd"
# FIRST INSTALL ------------------------------------------------------------------------------------
const GI = 0
const TL = 1
const TRANSITION_LAYER_ADDON =\
"res://addons/popochiu/engine/objects/transition_layer/transition_layer.tscn"
# ENGINE -------------------------------------------------------------------------------------------
const POPOCHIU_SCENE = "res://addons/popochiu/engine/popochiu.tscn"
const AUDIO_MANAGER =\
"res://addons/popochiu/engine/audio_manager/audio_manager.tscn"
const CURSOR_TYPE =\
preload("res://addons/popochiu/engine/cursor/cursor.gd").Type
const DATA = "res://game//popochiu_data.cfg"
const ROOM_CHILDREN = ["props", "hotspots", "walkable_areas", "regions"]
const VALID_TYPES = [
TYPE_BOOL, TYPE_INT, TYPE_FLOAT, TYPE_STRING,
TYPE_ARRAY, TYPE_PACKED_STRING_ARRAY,
TYPE_PACKED_INT32_ARRAY, TYPE_PACKED_INT64_ARRAY,
TYPE_PACKED_FLOAT32_ARRAY, TYPE_PACKED_FLOAT64_ARRAY
]
const PROPS_IGNORE = [
"description",
"baseline",
"clickable",
"cursor",
"always_on_top",
"frames",
"v_frames",
"link_to_item",
"_description_code",
"last_click_button",
"_double_click_delay",
"_has_double_click",
]
const HOTSPOTS_IGNORE = [
"description",
"baseline",
"clickable",
"cursor",
"always_on_top",
"_description_code",
"last_click_button",
"_double_click_delay",
"_has_double_click",
]
const WALKABLE_AREAS_IGNORE = [
"description",
"tint"
]
const REGIONS_IGNORE = [
"description",
"tint",
"scaling",
"scale_top",
"scale_bottom"
]
const SNGL_TEMPLATE = "@tool\n" +\
"extends \"%s\"\n\n" +\
"# classes ----\n" +\
"# ---- classes\n" +\
"\n" +\
"# nodes ----\n" +\
"# ---- nodes\n" +\
"\n" +\
"# functions ----\n" +\
"# ---- functions\n" +\
"\n"
const SNGL_SETUP = {
R_SNGL : {
interface = IROOM,
section = "rooms",
"class" = "res://game/rooms/%s/room_%s.gd",
"const" = "const PR%s := preload(\"%s\")\n",
node = "var %s: PR%s : get = get_%s\n",
"func" = "func get_%s() -> PR%s: return get_runtime_room(\"%s\")\n",
prefix = "R",
},
C_SNGL : {
interface = ICHARACTER,
section = "characters",
"class" = "res://game/characters/%s/character_%s.gd",
"const" = "const PC%s := preload(\"%s\")\n",
node = "var %s: PC%s : get = get_%s\n",
"func" = "func get_%s() -> PC%s: return get_runtime_character(\"%s\")\n",
prefix = "C",
},
I_SNGL : {
interface = IINVENTORY,
section = "inventory_items",
"class" = "res://game/inventory_items/%s/inventory_item_%s.gd",
"const" = "const PII%s := preload(\"%s\")\n",
node = "var %s: PII%s : get = get_%s\n",
"func" = "func get_%s() -> PII%s: return get_item_instance(\"%s\")\n",
prefix = "I",
},
D_SNGL : {
interface = IDIALOG,
section = "dialogs",
"class" = "res://game/dialogs/%s/dialog_%s.gd",
"const" = "const PD%s := preload(\"%s\")\n",
node = "var %s: PD%s : get = get_%s\n",
"func" = "func get_%s() -> PD%s: return get_instance(\"%s\")\n",
prefix = "D",
}
}
const A_TEMPLATE = "@tool\n" +\
"extends \"%s\"\n\n" +\
"# cues ----\n" +\
"# ---- cues\n" +\
"\n"
const AUDIO_CUE_SOUND =\
"res://addons/popochiu/engine/audio_manager/audio_cue_sound.gd"
const AUDIO_CUE_MUSIC =\
"res://addons/popochiu/engine/audio_manager/audio_cue_music.gd"
const VAR_AUDIO_CUE_SOUND = "var %s: AudioCueSound = load(\"%s\")\n"
const VAR_AUDIO_CUE_MUSIC = "var %s: AudioCueMusic = load(\"%s\")\n"
# GODOT PROJECT SETTINGS ---------------------------------------------------------------------------
const DISPLAY_WIDTH = "display/window/size/viewport_width"
const DISPLAY_HEIGHT = "display/window/size/viewport_height"
const MAIN_SCENE = "application/run/main_scene"
const TEST_WIDTH = "display/window/size/window_width_override"
const TEST_HEIGHT = "display/window/size/window_height_override"
const STRETCH_MODE = "display/window/stretch/mode"
const STRETCH_ASPECT = "display/window/stretch/aspect"
# GUI TEMPLATES ------------------------------------------------------------------------------------
const GUI_CUSTOM = "custom"
const GUI_CUSTOM_SCENE = GUI_ADDON_FOLDER + "popochiu_gui.tscn"
const GUI_CUSTOM_TEMPLATE = GUI_SCRIPT_TEMPLATES_FOLDER + "custom_commands_template.gd"
# GAME ---------------------------------------------------------------------------------------------
const GAME_PATH = "res://game/"
const ROOMS_PATH = GAME_PATH + "rooms/"
const CHARACTERS_PATH = GAME_PATH + "characters/"
const INVENTORY_ITEMS_PATH = GAME_PATH + "inventory_items/"
const DIALOGS_PATH = GAME_PATH + "dialogs/"
const GUI_GAME_FOLDER = GAME_PATH + "gui/"
const GUI_GAME_SCENE = GUI_GAME_FOLDER + "gui.tscn"
const GUI_COMMANDS = GUI_GAME_FOLDER + "gui_commands.gd"
const TRANSITION_LAYER = GAME_PATH + "transition_layer/transition_layer.tscn"
#region Public #####################################################################################
# Verify if the folders (where Popochiu's objects will be) exists
static func init_file_structure() -> bool:
var is_first_install := !DirAccess.dir_exists_absolute(GAME_PATH)
# Create the folders that does not exist
for d in _get_directories().values():
if not DirAccess.dir_exists_absolute(d):
DirAccess.make_dir_recursive_absolute(d)
# ---- Create config files ---------------------------------------------------------------------
# Create .cfg file
if not FileAccess.file_exists(DATA):
_create_empty_file(DATA)
# Create Globals file
if not FileAccess.file_exists(GLOBALS_SNGL):
var globals_file = FileAccess.open(GLOBALS_SNGL, FileAccess.WRITE)
globals_file.store_string("extends Node")
globals_file.close()
# ---- Create autoload files -------------------------------------------------------------------
create_auto_loads()
return is_first_install
static func create_auto_loads() -> void:
for key in SNGL_SETUP:
if not FileAccess.file_exists(key):
var file := FileAccess.open(key, FileAccess.WRITE)
file.store_string(SNGL_TEMPLATE % SNGL_SETUP[key].interface)
file.close()
if not FileAccess.file_exists(A_SNGL):
var file := FileAccess.open(A_SNGL, FileAccess.WRITE)
file.store_string(A_TEMPLATE % IAUDIO)
file.close()
static func update_autoloads(save := false) -> void:
# ---- Create autoload files -------------------------------------------------------------------
create_auto_loads()
# ---- Update autoload files -------------------------------------------------------------------
for id in SNGL_SETUP:
if FileAccess.file_exists(id):
var s: Script = load(id)
var code := s.source_code
var modified := false
var sngl_setup: Dictionary = SNGL_SETUP[id]
if not get_data_cfg().has_section(sngl_setup.section):
continue
for key in get_data_cfg().get_section_keys(sngl_setup.section):
var var_name: String = key
var snake_name := key.to_snake_case()
if var_name[0].is_valid_int():
var_name = var_name.insert(0, sngl_setup.prefix)
if not ("var %s" % var_name) in code:
var classes_idx := code.find("# ---- classes")
var class_path: String = sngl_setup["class"] % [
snake_name, snake_name
]
code = code.insert(
classes_idx,
sngl_setup["const"] % [key, class_path]
)
var nodes_idx := code.find("# ---- nodes")
code = code.insert(
nodes_idx,
sngl_setup.node % [var_name, key, key]
)
var functions_idx := code.find("# ---- functions")
code = code.insert(
functions_idx,
sngl_setup["func"] % [key, key, key]
)
modified = true
if modified:
s.source_code = code
if save: ResourceSaver.save(s, id)
# ---- Populate the A singleton ----------------------------------------------------------------
if not get_data_cfg().has_section("audio")\
or not FileAccess.file_exists(A_SNGL):
return
# [mx_cues, sfx_cues, vo_cues, ui_cues]
var audio_groups := get_data_cfg().get_section_keys("audio")
var s: Script = load(A_SNGL)
var code := s.source_code
var modified := false
var old_audio_cues := []
# Add all the AudioCues as variables
for group in audio_groups:
for path in get_data_value("audio", group, []):
# Check if the AudioCue is of a valid type
var audio_cue: Resource = load(path)
var script_path: String = audio_cue.get_script().resource_path
if not script_path in [AUDIO_CUE_MUSIC, AUDIO_CUE_SOUND]:
# Backup the properties of the AudioCue
var values = audio_cue.get_values()
if group == "mx_cues":
audio_cue.set_script(load(AUDIO_CUE_MUSIC))
else:
audio_cue.set_script(load(AUDIO_CUE_SOUND))
# Restore the properties of the AudioCue
audio_cue.set_values(values)
old_audio_cues.append(audio_cue)
ResourceSaver.save(audio_cue, path)
var var_name := audio_cue.resource_name
if ("var %s" % var_name) in code:
continue
var cues_idx := code.find("# ---- cues")
if group == "mx_cues":
code = code.insert(
cues_idx, VAR_AUDIO_CUE_MUSIC % [var_name, path]
)
else:
code = code.insert(
cues_idx, VAR_AUDIO_CUE_SOUND % [var_name, path]
)
modified = true
if modified:
s.source_code = code
if save: ResourceSaver.save(s, A_SNGL)
# Save the script changes in the AudioCues
for cue in old_audio_cues:
ResourceSaver.call_deferred("save", cue.resource_path, cue)
static func remove_autoload_obj(id: String, script_name: String) -> void:
var sngl_setup: Dictionary = SNGL_SETUP[id]
var snake_name := script_name.to_snake_case()
var class_path: String = sngl_setup["class"] % [snake_name, snake_name]
var s: Script = load(id)
var code := s.source_code
code = code.replace(sngl_setup["const"] % [script_name, class_path], "")
code = code.replace(sngl_setup.node % [script_name, script_name, script_name], "")
code = code.replace(sngl_setup["func"] % [script_name, script_name, script_name], "")
s.source_code = code
ResourceSaver.save(s, id)
static func remove_audio_autoload(type: String, var_name: String, path: String) -> void:
var s: Script = load(A_SNGL)
var code := s.source_code
if type == "mx_cues":
code = code.replace(VAR_AUDIO_CUE_MUSIC % [var_name, path], "")
else:
code = code.replace(VAR_AUDIO_CUE_SOUND % [var_name, path], "")
s.source_code = code
ResourceSaver.save(s, A_SNGL)
# ---- GAME DATA -----------------------------------------------------------------------------------
static func get_data_cfg() -> ConfigFile:
var config := ConfigFile.new()
var err: int = config.load(DATA)
if err == OK:
return config
PopochiuUtils.print_error("Couldn't load popochiu_data.cfg")
return null
static func set_data_value(section: String, key: String, value) -> int:
var config := get_data_cfg()
config.set_value(section, key, value)
return config.save(DATA)
static func has_data_value(section: String, key: String) -> bool:
return get_data_cfg().has_section_key(section, key)
static func get_data_value(section: String, key: String, default):
var config := get_data_cfg()
if not config.has_section(section):
return default
return config.get_value(section, key, default)
static func erase_data_value(section: String, key: String) -> void:
var config := get_data_cfg()
if config.has_section_key(section, key):
config.erase_section_key(section, key)
config.save(DATA)
else:
PopochiuUtils.print_error("Can't delete %s key from %s section" % [key, section])
static func get_section(section: String) -> Array:
var config := get_data_cfg()
var resource_paths := []
if config.has_section(section):
for key in config.get_section_keys(section):
resource_paths.append(config.get_value(section, key))
return resource_paths
static func store_properties(
target: Dictionary, source: Object, ignore_too := []
) -> void:
copy_popochiu_object_properties(target, source, ignore_too)
# ---- Call custom function to store extra data ------------------------------------------------
if source.has_method("on_save"):
target.custom_data = source.on_save()
if target.custom_data.is_empty(): target.erase("custom_data")
static func copy_popochiu_object_properties(target, source: Object, ignore_too := []) -> void:
var properties_to_ignore := ["script_name", "scene"]
if not ignore_too.is_empty():
properties_to_ignore.append_array(ignore_too)
# ---- Store basic type properties -------------------------------------------------------------
# prop = {class_name, hint, hint_string, name, type, usage}
for prop in source.get_script().get_script_property_list():
if prop.name in properties_to_ignore: continue
if not prop.type in VALID_TYPES: continue
# Check if the property is a script variable (8192)
# or a export variable (8199)
if prop.usage == PROPERTY_USAGE_SCRIPT_VARIABLE or prop.usage == (
PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SCRIPT_VARIABLE
):
target[prop.name] = source[prop.name]
static func has_property(source: Object, property: String) -> bool:
for prop in source.get_script().get_script_property_list():
if prop.name == property:
return true
return false
static func get_section_keys(section: String) -> Array:
var keys := []
var config := get_data_cfg()
if config.has_section(section):
keys = config.get_section_keys(section)
return keys
# ---- PLUGIN --------------------------------------------------------------------------------------
static func get_plugin_cfg() -> ConfigFile:
var config := ConfigFile.new()
var err: int = config.load(CFG)
if err == OK:
return config
PopochiuUtils.print_error("Couldn't load plugin config.")
return null
static func get_version() -> String:
return get_plugin_cfg().get_value("plugin", "version")
static func is_setup_done() -> bool:
return get_data_value("setup", "done", false)
static func is_gui_set() -> bool:
return (
!get_data_value("ui", "template", "").is_empty()
and DirAccess.dir_exists_absolute(GUI_GAME_FOLDER)
)
#endregion
#region Private ####################################################################################
static func _create_empty_file(path):
var file = FileAccess.open(path, FileAccess.WRITE)
file.store_string("")
file.close()
static func _get_directories() -> Dictionary:
return {
BASE = GAME_PATH,
AUTOLOADS = GAME_PATH + "/autoloads/",
ROOMS = ROOMS_PATH,
CHARACTERS = CHARACTERS_PATH,
INVENTORY_ITEMS = INVENTORY_ITEMS_PATH,
DIALOGS = DIALOGS_PATH,
}
#endregion