Godot Version
v4.7.1.stable.official [a13da4feb]
Question
I am creating a mod for a game (Pathogenic) by extending one of the existing classes. I used the included Mod Tool plugin to create the extension file. I tried various solutions from other existing posts, but none of them worked:
- I renamed the extension script to be different from the super script
- I gave it a distinct class_name as opposed to just using
extends “res://…" - I re-attached the script to the .tscn file (which would not be a viable solution for creating a mod anyway)
When manually ignoring the error (by resuming execution with F12 in the IDE), the program works as intended and my mod is loaded correctly.
My extension script:
class_name PlayerAmoebaExtension
extends "res://scn/player/player_amoeba/player_amoeba.gd"
## Flag indicating that the user is currently selecting the desired connections.
## Used by _unhandled_input
var _selection_active = false
## Dictionary containing possible slots as keys and and Array of their original
## scale and color as values. Set in _highlight_connections and used reset key
## slots if placement is cancelled
var _scale_color_store: Dictionary = {} # Slot -> Array[scale,color]
## The color for highlighted but not selected slots
var _highlight_color: Color = Color(0.6, 0.6, 0, 1)
## The color for highlighted and selected slots
var _selected_color: Color = Color(0.075, 0.654, 0.149, 1.0)
## Defines by how much the highlighted slots will be enlarged
var _highlight_scale_factor: Vector2 = Vector2(0.375, 0.375)
## Holds the position at which the new slot was placed
var _new_slot_position: Vector2
func is_placing() -> bool:
return _placement_active or _selection_active
func _commit_placement() -> void:
composer.hide_preview()
_hide_preview_overlay()
_hide_slot_previews()
_placement_active = false
_selection_active = false
_revert_highlights()
should_listen_to_events = true
release_attack_actions()
var mirror_pos := _mirror_of(_new_slot_position)
var positions := [_new_slot_position]
if mirror_pos != _new_slot_position:
positions.append(mirror_pos)
await add_blobs(positions, _placement_radius, _placement_type)
blob_placement_finished.emit(true)
_pop_editor_mouse_filter()
func _preview_candidates_within(slot_local: Vector2, inv_anchor: Transform2D, radius: float, max_conn: int) -> Array[Slot]:
var radius_sq: float = radius * radius
var result: Array[Slot] = []
for s_node in get_physical_slots():
var s := s_node as Slot
if not is_instance_valid(s):
continue
if not _placement_slot_internal and not s.internal:
continue
#side effect without which the game crashes!
AmoebaSlotEvolution.gather_connection_segments(self, inv_anchor)
var existing_local: Vector2 = AmoebaSlotEvolution.slot_composer_local(s, inv_anchor)
var d_sq := existing_local.distance_squared_to(slot_local)
if d_sq > radius_sq:
continue
result.append(s)
return result
func _unhandled_input(event: InputEvent) -> void:
if _placement_active:
if event.is_action_pressed(&"accept"):
_new_slot_position = _placement_cursor_local()
_check_placement()
get_viewport().set_input_as_handled()
elif event.is_action_pressed(&"ui_cancel") or (event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_RIGHT):
if _placement_cancellable:
# Draft pick: emitting placed=false makes apply() return false, so
# add_mutation refunds the pick and the editor reopens the draft.
_cancel_placement()
# World pickup: aborting would silently waste the already-consumed reward,
# so just swallow ESC / right-click — the blob must be placed.
get_viewport().set_input_as_handled()
elif _selection_active:
if event.is_action_pressed(&"accept"):
_select_connection()
get_viewport().set_input_as_handled()
elif event.is_action_pressed(&"ui_cancel") or (event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_RIGHT):
_placement_active = true
_selection_active = false
_revert_highlights()
get_viewport().set_input_as_handled()
func _check_placement():
var key = _preview_connections_by_blob.keys()[0]
if _preview_connections_by_blob[key].size() <= _PREVIEW_MAX_CONNECTIONS:
pending_slot_connections = _preview_connections_by_blob.duplicate()
_commit_placement()
else:
_placement_active = false
_selection_active = true
_highlight_connections()
return
## Changes size and color of all slots that may be connected to the newly placed
## slot. Previous colors and scale is saved in _candidates
func _highlight_connections() -> void:
pending_slot_connections = {}
for slot_array: Array in _preview_connections_by_blob.values():
for slot: Slot in slot_array:
for child: Node in slot.visual.get_children():
if child is Sprite2D:
var sprite := child as Sprite2D
if !_scale_color_store.has(slot):
_scale_color_store[slot] = [sprite.scale, sprite.modulate]
sprite.scale = _highlight_scale_factor
sprite.modulate = _highlight_color
# we only want one side to allow unambiguous cross-axis selection
break
## Restores original size and color for all candidates highlighted by _highlight_connections
func _revert_highlights() -> void:
for slot: Slot in _scale_color_store.keys():
for child in slot.visual.get_children():
if child is Sprite2D:
var sprite := child as Sprite2D
sprite.scale = _scale_color_store[slot][0]
sprite.modulate = _scale_color_store[slot][1]
_scale_color_store.clear()
## Marks already highlighted candidates
func _select_connection():
var mouse_pos = get_global_mouse_position()
var slot_hit = false
for slot_array: Array in _preview_connections_by_blob.values():
for slot: Slot in slot_array:
var local_mouse := slot.to_local(mouse_pos)
var hit_radius = 0
for child in slot.visual.get_children():
if child is Sprite2D:
var sprite := child as Sprite2D
if !sprite.is_visible_in_tree():
continue
hit_radius = sprite.texture.get_size().x * sprite.global_scale.x * 0.5
if local_mouse.distance_to(Vector2(0,0)) < hit_radius:
slot_hit = true
if not pending_slot_connections.has(_new_slot_position):
pending_slot_connections[_new_slot_position] = []
if !pending_slot_connections[_new_slot_position].has(slot):
pending_slot_connections[_new_slot_position].append(slot)
sprite.modulate = _selected_color
else:
pending_slot_connections[_new_slot_position].erase(slot)
sprite.modulate = _highlight_color
break
if slot_hit:
break
if slot_hit:
break
if slot_hit:
break
if pending_slot_connections[_new_slot_position].size() == _PREVIEW_MAX_CONNECTIONS:
_commit_placement()
My mod script:
extends Node
# This mod changes how the slots of the amoeba connect to each other.
# The user may now chose any 3 existing slots that are within range when placing a new slot.
# External slots still cannot be connected to each other.
# Cross-axis connections are possible.
# This mod extends player_amoeba.gd and changes how inputs in the editor are handled.
const MOD_DIR := "unknown_archetype-amoeba_slot_rework" # Name of the directory that this file is in
const LOG_NAME := "unknown_archetype-amoeba_slot_rework:Main" # Full ID of the mod (AuthorName-ModName)
var mod_dir_path := ""
var extensions_dir_path := ""
var translations_dir_path := ""
func _ready() -> void:
ModLoaderLog.info("Init", LOG_NAME)
mod_dir_path = ModLoaderMod.get_unpacked_dir().path_join(MOD_DIR)
# Add extensions
var script_path = "res://mods-unpacked/unknown_archetype-amoeba_slot_rework/extensions/scn/player/player_amoeba/player_amoeba_extension.gd"
ModLoaderMod.install_script_extension(script_path)
ModLoaderLog.info("Done", LOG_NAME)
My console output at the point of the error:
Godot Engine v4.7.1.stable.official.a13da4feb - https://godotengine.org
OpenGL API 3.3.0 NVIDIA 560.94 - Compatibility - Using Device: NVIDIA - NVIDIA GeForce GTX 1060 6GB
INFO ModLoader:Store: Applying options override with feature tag "editor".
INFO ModLoader:Store: Applying options override with feature tag "steam".
INFO ModLoader: game_install_directory: res://
INFO ModLoader:Path: The directory for mods at path "res://mods" does not exist.
INFO ModLoader:ThirdParty:Steam: Checking workshop items, with path: "workshop/content/3808690"
INFO ModLoader:ThirdParty:Steam: The directory for mods at path "workshop/content/3808690" does not exist.
SUCCESS ModLoader: res://mods-unpacked/unknown_archetype-amoeba_slot_rework loaded.
SUCCESS ModLoader: DONE: Loaded 1 mod files into the virtual filesystem
SUCCESS ModLoader: DONE: Loaded all mod configs
INFO ModLoader: mod_load_order -> 1) unknown_archetype-amoeba_slot_rework
INFO ModLoader: Initializing -> unknown_archetype-amoeba_slot_rework
SUCCESS ModLoader: DONE: Completely finished loading mods
SUCCESS ModLoader: DONE: Installed all script extensions
SUCCESS ModLoader: DONE: Applied all scene extensions
<Game specific logs>
INFO unknown_archetype-amoeba_slot_rework:Main: Init
After resuming the execution manually, the console gives slightly more information:
E 0:04:22:593 script_extension.gd:126 @ apply_extension(): Parse Error: Class "PlayerAmoeba" hides a global script class.
<GDScript Source>gdscript://-9223371045589737769.gd:1 @ GDScript::reload()
<Stack Trace> script_extension.gd:126 @ apply_extension()
mod.gd:52 @ install_script_extension()
mod_main.gd:22 @ _ready()
I am new to Godot, so any help would be greatly appreciated!
