Skip to content

Commit

Permalink
Release v1.5
Browse files Browse the repository at this point in the history
* Fix vanilla Cassette Beasts v1.1.3 bootleg partner bug
* Add super-early class patcher mechanism
* Fix eye color in Redkrab's world sprite
* Add glass resource, as a common shared mod currency item
* Replace post_init callbacks with preloader singleton_setup_completed
* Replace Magikrab scene override with instance editing in callback
* Add a safe and reasonable mod updates notification system
  • Loading branch information
Yukitty committed Jun 16, 2023
1 parent bbc7512 commit d748149
Show file tree
Hide file tree
Showing 26 changed files with 835 additions and 151 deletions.
9 changes: 5 additions & 4 deletions .github/README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
# Mod Utils

This is a support library that provides several high level functions for other mods, to make modding easier, more consistent, and to resolve issues with multiple mods accessing the same resource, such as scenes, translation strings, or class scripts.
This is a support library that provides several high level functions for other mods, to make modding easier, more consistent, and to resolve issues with multiple mods accessing the same resource, such as instanced scenes, vanilla translation strings, or class scripts.

If you're a mod author, you're absolutely welcome to use Mod Utils if it can help you in any way.<br>[Read the documentation.](/../../wiki)

## How to download and install

On Windows open `%AppData%\Roaming\CassetteBeasts\` and create a `mods` folder.<br>
On Linux or SteamDeck open `~/.local/share/CassetteBeasts/` and create a `mods` folder.
On Windows (Steam) open `%AppData%\CassetteBeasts\` and create a `mods` folder.<br>
On Windows (Game Pass) open `%LocalAppData%\Packages\RawFury.CassetteBeasts_9s0pnehqffj7t\SystemAppData\wgs\` and create a `mods` folder.<br>
On Linux or SteamDeck open `~/.local/share/CassetteBeasts/` and create a `mods` folder. No need to use Proton!

1. Download the [latest release](https://github.com/Yukitty/CassetteBeasts-cat-modutils/releases/latest).
2. Extract "cat_modutils.pck" to your mods folder.
2. Extract "cat_modutils.pck" and put it directly in your mods folder.
3. Open Cassette Beasts and verify the installed mods on the title screen.<br>Do not load your save file if any mod title looks wrong!

Please see the [official Cassette Beasts wiki Mod User Guide](https://wiki.cassettebeasts.com/wiki/Modding/Mod_User_Guide) for more information.
Expand Down
25 changes: 25 additions & 0 deletions mods/cat_modutils/bugfix/Party.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
extends "res://global/save_state/Party.gd"


func remap_partner_tapes() -> void:
for partner in partners:
if is_partner_unlocked(partner.partner_id):
continue

# BUGFIX: Set partner levels for new custom partners
if initial_partner_levels.has(partner.partner_id):
partner.level = initial_partner_levels[partner.partner_id]

assert (partner.tapes.size() == 1)
if partner.tapes.size() != 1:
continue
var tape = partner.tapes[0]
if tape.form == partner.partner_signature_species:
tape.form = MonsterForms.get_species_mapping(tape.form)

# BUGFIX: Only clear bootleg in type randomizer runs
if MonsterForms.type_rand_seed != null:
tape.type_override = []

tape.type_native = MonsterForms.get_type_mapping(tape.form)
tape.assign_initial_stickers(true)
6 changes: 6 additions & 0 deletions mods/cat_modutils/callbacks.gd
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ var _scene_ready: Dictionary
var _chunk_ready: Dictionary
var _level_streamer: LevelStreamer


func _init() -> void:
DLC.get_tree().connect("node_added", self, "_on_node_added")
connect_class_ready("res://addons/level_streamer/LevelStreamer.gd", self, "_on_LevelStreamer_ready")


func _on_LevelStreamer_ready(level_streamer: Node) -> void:
_level_streamer = level_streamer


func _on_node_added(node: Node) -> void:
var scene_path: String = node.filename
var script_path: String
Expand Down Expand Up @@ -43,6 +46,7 @@ func _on_node_added(node: Node) -> void:
for callback in _scene_ready[scene_path]:
node.connect("ready", callback.owner, callback.function, [node] + callback.binds, CONNECT_DEFERRED)


# Class callbacks can be used to mass-edit many scenes that share a common ancestor
func connect_class_ready(script, callback_owner: Object, callback_function: String, callback_binds: Array = []) -> void:
if script is Resource:
Expand All @@ -56,6 +60,7 @@ func connect_class_ready(script, callback_owner: Object, callback_function: Stri
"binds": callback_binds,
})


# Scene instance callbacks can be used to inject content into an existing scene
# when it spawns, even for sub-scenes that aren't typically the root.
func connect_scene_ready(scene: String, callback_owner: Object, callback_function: String, callback_binds: Array = []) -> void:
Expand All @@ -67,6 +72,7 @@ func connect_scene_ready(scene: String, callback_owner: Object, callback_functio
"binds": callback_binds,
})


# This is for chunked maps like the Overworld.
func connect_chunk_setup(scene: String, chunk_index: Vector2, callback_owner: Object, callback_function: String, callback_binds: Array = []) -> void:
chunk_index = Vector2(floor(chunk_index.x), floor(chunk_index.y))
Expand Down
9 changes: 4 additions & 5 deletions mods/cat_modutils/cheat_mod.gd
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ extends Reference
var _file_button: PackedScene


func _init(modutils: Reference) -> void:
modutils.connect("post_init", self, "_on_post_init")
func _init() -> void:
# Finish init later
assert(not SceneManager.preloader.singleton_setup_complete)
yield(SceneManager.preloader, "singleton_setup_completed")


func _on_post_init() -> void:
# Index all mods looking for a `MODUTILS.cheat_mod` flag
var enabled: bool = false
for mod in DLC.mods:
Expand All @@ -25,6 +25,5 @@ func _on_post_init() -> void:
SaveSystem.connect("file_loaded", self, "_on_SaveSystem_file_loaded")



func _on_SaveSystem_file_loaded() -> void:
SaveState.has_cheated = true
56 changes: 56 additions & 0 deletions mods/cat_modutils/class_patch.gd
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,52 @@ extends Reference

var processed_code: Dictionary = {}


func _init() -> void:
# Run super-early callback for class patcher
# This is basically a mid-execution pre-emption of the DLC singleton
# and I hate it.

# Fetch ALL mod metadata resources
# except for our own, which is currently being initialized
var dir := Directory.new()
var err: int = dir.open("res://mods/")
assert(err == OK)
dir.list_dir_begin(true, true)
assert(err == OK)
var mods: Array = []
while true:
var file = dir.get_next()
if file.empty():
break
if file == "cat_modutils":
continue
file = "res://mods/%s/metadata.tres" % file
if dir.file_exists(file):
var meta: ContentInfo = load(file) as ContentInfo
if meta:
mods.push_back(meta)
dir.list_dir_end()

# Iterate all mod metadata
# and apply patches from the MODUTILS global const, if present.
for meta in mods:
if "MODUTILS" in meta and meta.MODUTILS is Dictionary and "class_patch" in meta.MODUTILS:
assert(meta.MODUTILS.class_patch is Array and not meta.MODUTILS.class_patch.empty())
for def in meta.MODUTILS.class_patch:
assert(def is Dictionary and not def.empty())
assert("patch" in def and def.patch is String and not def.patch.empty())
assert("target" in def and def.target is String and not def.target.empty())
patch(def.patch, def.target)

# Now that we've initialized all of the mods that weren't loaded yet,
# we need to hold the references until DLC is finished taking them.
# This will prevent headaches from repeated _init calls.
# A simple yield should do nicely here.
assert(not SceneManager.preloader.singleton_setup_complete)
yield(SceneManager.preloader, "singleton_setup_completed")


func get_class_script(script: GDScript) -> String:
# First, check if plain text source_code is already available.
# That would be a sign someone else has edited the file already.
Expand All @@ -28,10 +74,12 @@ func get_class_script(script: GDScript) -> String:

return source_code


func set_class_script(script: GDScript, source_code: String) -> void:
script.source_code = source_code
script.reload()


func patch_process_code(path: String, code: String) -> void:
var currentfunc: String = "global"
var line: String
Expand All @@ -49,6 +97,7 @@ func patch_process_code(path: String, code: String) -> void:
else:
processed_code[path][currentfunc] = line


func patch(patch_path: String, target_path: String, toprint: bool = false) -> void:
# As a first step, we grab the file that is to replace, and decompile its variables and functions
var source_code: String = get_class_script(load(target_path))
Expand Down Expand Up @@ -219,6 +268,7 @@ func patch(patch_path: String, target_path: String, toprint: bool = false) -> vo
print(finalcode)
set_class_script(load(target_path), finalcode)


func patch_removelines(what: String, function: String, code: String) -> void:
var string: String = processed_code[code][function]
var pos: int = processed_code[code][function].find(what)
Expand All @@ -228,9 +278,11 @@ func patch_removelines(what: String, function: String, code: String) -> void:

processed_code[code][function] = string


func patch_removefunc(what: String, code: String) -> void:
processed_code[code].erase(what)


func patch_addlines(what: String, before: bool, function: String, code: String) -> void:
var string: String = processed_code[code][function]
var firstline: String = string.get_slice("\n", 0)
Expand All @@ -241,20 +293,24 @@ func patch_addlines(what: String, before: bool, function: String, code: String)
else:
processed_code[code][function] = string + "\n" + what


func patch_addlineshere(what: String, where: String, function: String, code: String) -> void:
var string: String = processed_code[code][function]
var pos: int = processed_code[code][function].find(where)

if pos != -1:
processed_code[code][function] = string.replace(where, where + "\n" + what)


func patch_addfunc(what: String, function: String, code: String) -> void:
processed_code[code][function] = what


func patch_replacelines(what: String, forwhat: String, function: String, code: String) -> void:
var string: String = processed_code[code][function]
processed_code[code][function] = string.replace(what, forwhat)


func patch_replacetext(what: String, forwhat: String, where: String, function: String, code: String) -> void:
var string: String = processed_code[code][function]
var line: String = string.substr(string.find(where), where.length())
Expand Down
21 changes: 21 additions & 0 deletions mods/cat_modutils/items.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
extends Reference


func _init() -> void:
var dt: Dictionary = Datatables.load("res://data/items/").table
var item: BaseItem = load("res://mods/cat_modutils/items/modutils_glass.tres")
dt[Datatables.get_db_key(item)] = item

# Finish init later
assert(not SceneManager.preloader.singleton_setup_complete)
yield(SceneManager.preloader, "singleton_setup_completed")

var item_desc: Translation
var locale: String = TranslationServer.get_locale()
for l in TranslationServer.get_loaded_locales():
item_desc = Translation.new()
item_desc.locale = l
item_desc.add_message("MODUTILS_ITEM_GLASS_DESCRIPTION",
tr("MODUTILS_ITEM_DESCRIPTION_FOOTER") % tr("ITEM_PLASTIC_DESCRIPTION"))
TranslationServer.add_translation(item_desc)
TranslationServer.set_locale(locale)
Binary file added mods/cat_modutils/items/glass.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 35 additions & 0 deletions mods/cat_modutils/items/glass.png.import
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[remap]

importer="texture"
type="StreamTexture"
path="res://.import/glass.png-3c3eef8883d3b7734f8a1b6656eb899b.stex"
metadata={
"vram_texture": false
}

[deps]

source_file="res://mods/cat_modutils/items/glass.png"
dest_files=[ "res://.import/glass.png-3c3eef8883d3b7734f8a1b6656eb899b.stex" ]

[params]

compress/mode=0
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/normal_map=0
flags/repeat=0
flags/filter=false
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=0
process/fix_alpha_border=true
process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=false
svg/scale=1.0
20 changes: 20 additions & 0 deletions mods/cat_modutils/items/modutils_glass.tres
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[gd_resource type="Resource" load_steps=3 format=2]

[ext_resource path="res://data/BaseItem.gd" type="Script" id=1]
[ext_resource path="res://mods/cat_modutils/items/glass.png" type="Texture" id=2]

[resource]
script = ExtResource( 1 )
name = "MODUTILS_ITEM_GLASS_NAME"
aux_name = ""
icon = ExtResource( 2 )
description = "MODUTILS_ITEM_GLASS_DESCRIPTION"
category = "resources"
base_stack_limit = 0
stack_limit_category = ""
consume_on_use = false
discardable = true
value = 500
usable_contexts = 0
battle_usage = 0
sort_order = 0
4 changes: 2 additions & 2 deletions mods/cat_modutils/metadata.tres
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
script = ExtResource( 1 )
id = "cat_modutils"
name = "Mod Utils"
version_code = 8
version_string = "1.4.1"
version_code = 9
version_string = "1.5"
author = "cat"
save_file_format_tag = ""
save_file_format_tag_version = 0
Expand Down
27 changes: 23 additions & 4 deletions mods/cat_modutils/mod.gd
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,54 @@ const MOD_STRINGS: Array = [
preload("mod_strings.en.translation"),
]

const MODUTILS = {
"updates": "https://gist.githubusercontent.com/Yukitty/f113b1e2c11faad763a47ebc0a867643/raw/updates.json"
}

# Submodules
var callbacks: Reference
var trans_patch: Reference
var settings: Reference
var class_patch: Reference
var cheat_mod: Reference
var world: Reference
var items: Reference
var updates: Reference

# Modified Resource references
var _save_state_party: Resource


func _init() -> void:
# Load submodules ASAP so we're ready for other mods
callbacks = preload("callbacks.gd").new()
trans_patch = preload("trans_patch.gd").new(self)
trans_patch = preload("trans_patch.gd").new()
settings = preload("settings.gd").new(self)
class_patch = preload("class_patch.gd").new()
cheat_mod = preload("cheat_mod.gd").new(self)
cheat_mod = preload("cheat_mod.gd").new()
world = preload("world.gd").new(self)
items = preload("items.gd").new()
updates = preload("updates.gd").new()


func init_content() -> void:
# Add translation strings
for translation in MOD_STRINGS:
TranslationServer.add_translation(translation)

# Call post_init deferred, to work around init_content oversight in v1.1.2
# Extend SaveState.party (bugfixes)
_save_state_party = load("res://mods/cat_modutils/bugfix/Party.gd")
_save_state_party.take_over_path("res://global/save_state/Party.gd")

# Call post_init deferred, to work around init_content oversight in
# Cassette Beasts v1.1.2 and earlier
call_deferred("_on_post_init")


func _on_post_init() -> void:
emit_signal("post_init")

# DEPRECIATED
# DEPRECIATED: Use init_content in Cassette Beasts v1.1.3 and later
for mod in DLC.mods:
if mod.has_method("modutils_post_init"):
mod.modutils_post_init(self)
7 changes: 7 additions & 0 deletions mods/cat_modutils/mod_strings.csv
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,10 @@ MODUTILS_REDKRAB_VERLINE3,"Which is to say, please have patience, and fantastic
UI_PAUSE_MODUTILS_RETURN_BTN,Return to Mod Club Station
MODUTILS_REDKRAB_RETURN,My fellows! Have you grown weary of this place? Shall we retire to Mod Club Station?
MODUTILS_REDKRAB_RETURN_NO1,Adventure here as long as you wish! I shall await your triumphant return with baited breath.
MODUTILS_ITEM_GLASS_NAME,Glass
MODUTILS_ITEM_DESCRIPTION_FOOTER,%s\n\n[color=#ab75e8]Provided by Mod Utils for general use.[/color]
MODUTILS_TITLE_MOD_UPDATE,An update is available for {content_name}.\nPlease check the mod's release page for updates.
MODUTILS_TITLE_MOD_UPDATE_WITH_URL1,An update is available for {content_name}.\nThe mod has provided a link to its home page.
MODUTILS_TITLE_MOD_UPDATE_WITH_URL2,Open the following address in your web browser?\n{content_address}
MODUTILS_TITLE_MOD_ERROR1,There was an error downloading or parsing update info for {content_name}.
MODUTILS_TITLE_MOD_ERROR2,"The mod may have updated, or this may be a temporary error.\nThe mod is still loaded and ready to use."
Loading

0 comments on commit d748149

Please sign in to comment.