Initial Void Bastion 0.20 prototype

This commit is contained in:
Emporex committed 2026-10-03 21:28:56 +02:00
commit 7362ca0f02
251 files changed
+29688

No files matched your search

File diff suppressed because it is too large. Load diff
+1
View File
@@ -0,0 +1 @@
uid://dssu2uimllcg6
+374
View File
@@ -0,0 +1,374 @@
#
# © 2024-present https://github.com/cengiz-pz
#
@tool
extends EditorPlugin
const PLUGIN_NAME: String = "AdmobPlugin"
const ANDROID_DEPENDENCIES: Array = [ "androidx.appcompat:appcompat:1.7.1", "androidx.lifecycle:lifecycle-process:2.8.3", "com.google.android.gms:play-services-ads:24.9.0" ]
const IOS_PLATFORM_VERSION: String = "14.3"
const IOS_FRAMEWORKS: Array = [ "Foundation.framework", "AppTrackingTransparency.framework" ]
const IOS_EMBEDDED_FRAMEWORKS: Array = [ ]
const IOS_LINKER_FLAGS: Array = [ "-ObjC" ]
const IOS_BUNDLE_FILES: Array = [ ]
const SPM_DEPENDENCIES: Array = [ {&"url": "https://github.com/googleads/swift-package-manager-google-mobile-ads.git", &"version": "12.14.0", &"products": ["GoogleMobileAds"]} ]
var android_export_plugin: AndroidExportPlugin
var ios_export_plugin: IosExportPlugin
func _enter_tree() -> void:
android_export_plugin = AndroidExportPlugin.new()
add_export_plugin(android_export_plugin)
ios_export_plugin = IosExportPlugin.new()
add_export_plugin(ios_export_plugin)
func _exit_tree() -> void:
remove_export_plugin(android_export_plugin)
android_export_plugin = null
remove_export_plugin(ios_export_plugin)
ios_export_plugin = null
class AndroidExportPlugin extends EditorExportPlugin:
const APP_ID_META_TAG = """
<meta-data
tools:replace="android:value"
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="%s"/>
"""
var _plugin_name = PLUGIN_NAME
var _export_config: AdmobAndroidExportConfig
func _supports_platform(platform: EditorExportPlatform) -> bool:
return platform is EditorExportPlatformAndroid
func _get_android_libraries(platform: EditorExportPlatform, debug: bool) -> PackedStringArray:
if debug:
return PackedStringArray(["%s/bin/debug/%s-debug.aar" % [_plugin_name, _plugin_name]])
else:
return PackedStringArray(["%s/bin/release/%s-release.aar" % [_plugin_name, _plugin_name]])
func _get_name() -> String:
return _plugin_name
func _export_begin(features: PackedStringArray, is_debug: bool, path: String, flags: int) -> void:
if _supports_platform(get_export_platform()):
_export_config = AdmobAndroidExportConfig.new()
if not _export_config.export_config_file_exists() or _export_config.load_export_config_from_file() != OK:
_export_config.load_export_config_from_node()
func _get_android_dependencies(platform: EditorExportPlatform, debug: bool) -> PackedStringArray:
var deps: PackedStringArray = PackedStringArray(ANDROID_DEPENDENCIES)
if _export_config and _export_config.enabled_mediation_networks.size() > 0:
for __network in _export_config.enabled_mediation_networks:
for __dependency in __network.android_dependencies:
deps.append(__dependency)
GmpLogger.log_info("Android dependencies: %s" % str(deps))
return deps
func _get_android_dependencies_maven_repos(platform: EditorExportPlatform, debug: bool) -> PackedStringArray:
var __custom_repos: PackedStringArray = []
if _export_config and _export_config.enabled_mediation_networks.size() > 0:
for network in _export_config.enabled_mediation_networks:
if network.android_custom_maven_repo and not network.android_custom_maven_repo.is_empty():
__custom_repos.append(network.android_custom_maven_repo)
GmpLogger.log_info(
"Added custom Maven repo for %s mediation: %s" %
[network.tag, network.android_custom_maven_repo],
)
return __custom_repos
func _get_android_manifest_application_element_contents(platform: EditorExportPlatform, debug: bool) -> String:
var __contents: String
if _export_config:
__contents = APP_ID_META_TAG % (_export_config.real_application_id if _export_config.is_real \
else _export_config.debug_application_id )
else:
GmpLogger.log_warn("Export config not found for %s!" % _plugin_name)
__contents = ""
return __contents
class IosExportPlugin extends EditorExportPlugin:
const NS_APP_TRANSPORT_SECURITY: String = """
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSAllowsArbitraryLoadsInWebContent</key>
<true/>
</dict>
"""
var _plugin_name = PLUGIN_NAME
var _spm_dependencies = []
var _export_config: AdmobIosExportConfig
var _export_path: String
func _supports_platform(platform: EditorExportPlatform) -> bool:
return platform is EditorExportPlatformIOS
func _get_name() -> String:
return _plugin_name
func _export_begin(features: PackedStringArray, is_debug: bool, path: String, flags: int) -> void:
if _supports_platform(get_export_platform()):
_export_path = path.simplify_path()
_export_config = AdmobIosExportConfig.new()
if not _export_config.export_config_file_exists() or _export_config.load_export_config_from_file() != OK:
_export_config.load_export_config_from_node()
add_apple_embedded_platform_plist_content("<key>GADApplicationIdentifier</key>")
add_apple_embedded_platform_plist_content(
"\t<string>%s</string>" % (_export_config.real_application_id
if _export_config.is_real else _export_config.debug_application_id ),
)
if _export_config.att_enabled and _export_config.att_text and not _export_config.att_text.is_empty():
add_apple_embedded_platform_plist_content("<key>NSUserTrackingUsageDescription</key>")
add_apple_embedded_platform_plist_content("<string>%s</string>" % _export_config.att_text)
add_apple_embedded_platform_plist_content(
MediationNetwork.generate_sk_ad_network_plist(
_export_config.enabled_mediation_networks,
),
)
add_apple_embedded_platform_plist_content(NS_APP_TRANSPORT_SECURITY)
for __framework in IOS_FRAMEWORKS:
add_apple_embedded_platform_framework(__framework)
for __framework in IOS_EMBEDDED_FRAMEWORKS:
add_apple_embedded_platform_embedded_framework(__framework)
for __flag in IOS_LINKER_FLAGS:
add_apple_embedded_platform_linker_flags(__flag)
for __bundle_file in IOS_BUNDLE_FILES:
add_apple_embedded_platform_bundle_file(__bundle_file)
for __spm_dep in SPM_DEPENDENCIES:
_spm_dependencies.append(SpmDependency.new(__spm_dep))
func _end_generate_apple_embedded_project(path: String, will_build_archive: bool) -> void:
GmpLogger.log_info("Apple export project generated at: %s. Will build archive: %s"
% [path, str(will_build_archive)])
if _supports_platform(get_export_platform()):
_spm_dependencies.append_array(_get_extra_dependencies())
if _spm_dependencies.is_empty():
GmpLogger.log_info("No SPM dependencies to install. Skipping.")
else:
GmpLogger.log_info("Installing %d SPM dependencies." % _spm_dependencies.size())
_install_dependencies(path.get_base_dir(), path.get_file().get_basename())
func _get_extra_dependencies() -> Array[SpmDependency]:
var __extra_dependencies:= [] as Array[SpmDependency]
__extra_dependencies.append_array(
MediationNetwork.get_spm_dependencies(_export_config.enabled_mediation_networks))
return __extra_dependencies
func _install_dependencies(a_base_dir: String, a_project_name: String) -> void:
var __project_file_name:= "%s.xcodeproj" % a_project_name
var __project_file_path:= a_base_dir.path_join(__project_file_name)
if not DirAccess.dir_exists_absolute(__project_file_path):
GmpLogger.log_error("Xcode project '%s' does not exist! Can't install SPM dependencies."
% __project_file_path)
return
var __script_name = "add_dependency.rb"
var __add_dependency_script_path = a_base_dir.path_join(__script_name)
var __result = _generate_add_dependency_script(__add_dependency_script_path)
if __result != Error.OK:
GmpLogger.log_error("Failed to generate '%s' script with error %d!" % [__script_name, __result])
return
GmpLogger.log_info("Adding SPM dependencies to %s..." % __project_file_path)
for __spm_dep: SpmDependency in _spm_dependencies:
for __spm_dep_product: String in __spm_dep.get_products():
var exec_output: Array = []
var exec_code = OS.execute("ruby", [
__add_dependency_script_path,
__project_file_path,
__spm_dep.get_url(),
__spm_dep.get_version(),
__spm_dep_product,
], exec_output, true, false)
if exec_code == 0:
GmpLogger.log_info("Product %s for SPM dependency %s added successfully!"
% [__spm_dep_product, __spm_dep.format_to_string()])
for line in exec_output:
GmpLogger.log_info("SPM: %s" % line)
else:
GmpLogger.log_info("Failed to add product %s for SPM dependency %s !"
% [__spm_dep_product, __spm_dep.format_to_string()])
for line in exec_output:
GmpLogger.log_error("SPM: %s" % line)
GmpLogger.log_info("Resolving SPM dependencies...")
__script_name = "resolve_dependencies.sh"
var __resolve_dependencies_script_path = a_base_dir.path_join(__script_name)
__result = _generate_resolve_dependencies_script(__resolve_dependencies_script_path, a_base_dir, a_project_name)
if __result != Error.OK:
GmpLogger.log_error("Failed to generate '%s' script with error %d!" % [__script_name, __result])
return
var exec_output: Array = []
var exec_code = OS.execute(__resolve_dependencies_script_path, [], exec_output, true, false)
if exec_code == 0:
for line in exec_output:
GmpLogger.log_info("SPM: %s" % line)
GmpLogger.log_info("Resolved dependencies successfully!")
else:
for line in exec_output:
GmpLogger.log_error("SPM: %s" % line)
GmpLogger.log_info("Failed to resolve dependencies! Try manually in Xcode.")
const ADD_DEPENDENCY_RUBY_SCRIPT = """
require 'xcodeproj'
project_path = ARGV[0]
url = ARGV[1].strip
version = ARGV[2].strip
product_name = ARGV[3].strip
unless File.exist?(project_path)
puts "Error: Xcode project not found at #{project_path}"
exit 1
end
if url.empty? || version.empty? || product_name.empty?
puts "Error: url, version, and product_name must all be non-empty."
exit 1
end
begin
project = Xcodeproj::Project.open(project_path)
target = project.targets.first
if target.nil?
puts "Error: No targets found in the Xcode project."
exit 1
end
existing_dep = target.package_product_dependencies.find do |dep|
dep.product_name == product_name
end
if existing_dep
puts "Warning: Product dependency '#{product_name}' already exists in the project. Skipping add.\n\n"
else
# Reuse an existing package reference for the same URL, or create a new one
pkg = project.root_object.package_references.find do |p|
p.repositoryURL == url
end
if pkg
puts "Reusing existing package reference for '#{url}'."
else
pkg = project.new(Xcodeproj::Project::Object::XCRemoteSwiftPackageReference)
pkg.repositoryURL = url
pkg.requirement = {
'kind' => 'upToNextMajorVersion',
'minimumVersion' => version
}
project.root_object.package_references << pkg
end
# Create the product dependency and link it to the shared package reference
ref = project.new(Xcodeproj::Project::Object::XCSwiftPackageProductDependency)
ref.product_name = product_name
ref.package = pkg
target.package_product_dependencies << ref
puts "Successfully added SPM dependency '#{product_name}' " \
"(#{url} @ #{version}) to #{File.basename(project_path)}\n\n"
end
project.save
rescue => e
puts "An error occurred: #{e.message}\n\n"
exit 1
end
"""
func _generate_add_dependency_script(a_script_path: String) -> Error:
var __result = Error.OK
var __script_content = ADD_DEPENDENCY_RUBY_SCRIPT
__result = _create_script(a_script_path, __script_content)
return __result
const RESOLVE_DEPENDENCIES_BASH_SCRIPT = """
#!/bin/bash
set -e # Exit on error
xcodebuild -resolvePackageDependencies \
-project "%s.xcodeproj" \
-scheme "%s"
"""
func _generate_resolve_dependencies_script(a_script_path: String, a_base_dir: String,
a_project_name: String) -> Error:
var __result: Error = Error.OK
var __script_content = RESOLVE_DEPENDENCIES_BASH_SCRIPT \
% [ ProjectSettings.globalize_path(a_base_dir.path_join(a_project_name)), a_project_name ]
__result = _create_script(a_script_path, __script_content)
return __result
func _create_script(a_script_path: String, a_script_content: String) -> Error:
var __result: Error = Error.OK
var __script_file = FileAccess.open(a_script_path, FileAccess.WRITE)
if __script_file:
__script_file.store_string(a_script_content)
__script_file.close()
else:
__result = Error.ERR_FILE_CANT_WRITE
var chmod_output: Array = []
var chmod_code = OS.execute("chmod", ["+x", a_script_path], chmod_output, true, false)
if chmod_code != 0:
GmpLogger.log_error("Failed to chmod %s script: %s"
% [a_script_path, (chmod_output if chmod_output.size() > 0 else "Unknown error")])
__result = Error.ERR_FILE_NO_PERMISSION
return __result
+1
View File
@@ -0,0 +1 @@
uid://ombwj85k4mvh
+350
View File
@@ -0,0 +1,350 @@
#
# © 2024-present https://github.com/cengiz-pz
#
@icon("icon.png")
class_name InlineAdaptiveBanner
extends Control
const MINIMUM_VALID_WIDTH: float = 50.0
const MINIMUM_CLIP_THRESHOLD: float = 0.05
const MAXIMUM_CLIP_THRESHOLD: float = 1.0
const MINIMUM_RESIZE_THRESHOLD: float = 10.0
const MAXIMUM_RESIZE_THRESHOLD: float = 500.0
## Maximum pixel height of the requested ad. If set to -1, height will be automatically determined.
@export var max_ad_height: int = -1
## Minimum portion of the banner’s area (as a percentage) that must be clipped before the banner is considered hidden.
@export_range(MINIMUM_CLIP_THRESHOLD, MAXIMUM_CLIP_THRESHOLD, 0.05) var clip_threshold: float = 0.1:
set(a_value):
clip_threshold = clampf(a_value, MINIMUM_CLIP_THRESHOLD, MAXIMUM_CLIP_THRESHOLD)
## Width-change threshold (in pixels) that triggers a banner reload when the ad container is resized by this amount
## or more.
@export_range(MINIMUM_RESIZE_THRESHOLD, MAXIMUM_RESIZE_THRESHOLD, 5.0) var resize_threshold: float = 50.0:
set(a_value):
resize_threshold = clampf(a_value, MINIMUM_RESIZE_THRESHOLD, MAXIMUM_RESIZE_THRESHOLD)
## Path to Admob node.
@export_node_path("Admob") var admob_path: NodePath
## Whether the inline-adaptive banner ad should be automatically loaded.
@export var auto_load: bool = false
@export_category("Ad Unit Identifier")
## Use ad unit identifier value defined in the Admob node.
@export var inherit_from_admob_node: bool = false
@export_group("Android", "ad_unit_id_android")
## Ad unit ID of the inline adaptive banner ad to be loaded on the Android platform.
@export var ad_unit_id_android_debug: String = "ca-app-pub-3940256099942544/2014213617"
## Ad unit ID of the inline adaptive banner ad to be loaded on the Android platform.
@export var ad_unit_id_android_real: String = ""
@export_group("iOS", "ad_unit_id_ios")
## Ad unit ID of the inline adaptive banner ad to be loaded on the iOS platform.
@export var ad_unit_id_ios_debug: String = "ca-app-pub-3940256099942544/2934735716"
## Ad unit ID of the inline adaptive banner ad to be loaded on the iOS platform.
@export var ad_unit_id_ios_real: String = ""
var admob: Admob
var _ad_unit_id: String
var ad_id: String = ""
var last_width: float
var pending_load: bool = false
var is_position_dirty: bool = false
var is_banner_visible: bool = false
var do_ignore_resize: bool = false
func _enter_tree() -> void:
var __admob: Admob
if admob_path:
__admob = get_node(admob_path)
if __admob:
initialize(__admob)
else:
push_warning("Admob node not found")
func _exit_tree() -> void:
if ad_id != "":
remove_ad()
func _ready() -> void:
if Engine.is_editor_hint():
return
size_flags_vertical = Control.SIZE_SHRINK_CENTER
last_width = size.x
resized.connect(_on_resized)
func initialize(a_admob_node: Admob) -> void:
admob = a_admob_node
visibility_changed.connect(_on_visibility_changed)
admob.banner_ad_loaded.connect(_on_banner_ad_loaded)
admob.banner_ad_failed_to_load.connect(_on_banner_ad_failed_to_load)
admob.banner_ad_refreshed.connect(_on_banner_ad_refreshed)
admob.banner_ad_size_measured.connect(_on_banner_ad_size_measured)
if admob.is_initialization_completed:
if auto_load:
call_deferred("_deferred_load_ad")
else:
admob.initialization_completed.connect(_on_Admob_initialization_completed)
if inherit_from_admob_node:
_ad_unit_id = admob._banner_id
else:
if OS.has_feature("ios"):
if admob.is_real:
_ad_unit_id = ad_unit_id_ios_real
else:
_ad_unit_id = ad_unit_id_ios_debug
else:
if admob.is_real:
_ad_unit_id = ad_unit_id_android_real
else:
_ad_unit_id = ad_unit_id_android_debug
func _deferred_load_ad() -> void:
load_ad()
_mark_position_dirty()
# --------------------------------------------------------------
# AD MANIPULATION
# --------------------------------------------------------------
func load_ad() -> void:
if _ad_unit_id == "":
return
var req := LoadAdRequest.new()
req.set_ad_unit_id(_ad_unit_id)
req.set_ad_size(LoadAdRequest.RequestedAdSize.INLINE_ADAPTIVE)
if custom_minimum_size.x < MINIMUM_VALID_WIDTH:
GmpLogger.log_warn("Can't load ad. Invalid inline adaptive banner width: %.1f" % custom_minimum_size.x)
return
req.set_adaptive_width(round(custom_minimum_size.x))
if max_ad_height != -1:
req.set_adaptive_max_height(max_ad_height)
req.set_ad_position(LoadAdRequest.AdPosition.CUSTOM)
pending_load = true
admob.load_banner_ad(req)
func show_ad() -> void:
if not is_banner_visible:
admob.show_banner_ad(ad_id)
is_banner_visible = true
func hide_ad() -> void:
if is_banner_visible:
admob.hide_banner_ad(ad_id)
is_banner_visible = false
func move_ad(a_physical_position: Vector2) -> void:
admob.move_banner_ad(ad_id, a_physical_position.x, a_physical_position.y)
func remove_ad() -> void:
admob.remove_banner_ad(ad_id)
is_banner_visible = false
ad_id = ""
# --------------------------------------------------------------
# POSITIONING
# --------------------------------------------------------------
func _process(_delta: float) -> void:
if is_position_dirty:
_deferred_update_position()
is_position_dirty = false
else:
update_banner_position()
func _mark_position_dirty() -> void:
is_position_dirty = true
call_deferred("_deferred_update_position")
func _deferred_update_position() -> void:
update_banner_position()
func update_banner_position() -> void:
if ad_id == "":
return
# Check basic tree visibility (Tabs, hidden parents)
if not is_visible_in_tree():
hide_ad()
return
# Smart clipping (visibility logic)
var is_clipped_out: bool = false
if size.x > 1.0 and size.y > 1.0:
var virtual_rect := get_global_rect()
var original_area := virtual_rect.get_area()
var visible_region := virtual_rect
var parent = get_parent()
# Walk up the tree to find ScrollContainers or other clippers
while parent is Control:
if parent.clip_contents:
var parent_rect: Rect2 = parent.get_global_rect()
var intersection := visible_region.intersection(parent_rect)
# No intersection -> fully clipped (100%)
if intersection.get_area() <= 0:
is_clipped_out = true
break
visible_region = intersection
parent = parent.get_parent()
# Final check
if not get_viewport_rect().intersects(virtual_rect):
is_clipped_out = true
else:
# Calculate the percentage clipped
var visible_area := visible_region.get_area()
var clipped_ratio := 1.0 - (visible_area / original_area)
if clipped_ratio >= clip_threshold:
is_clipped_out = true
if is_clipped_out:
if is_banner_visible:
hide_ad()
else:
if not is_banner_visible:
show_ad()
move_ad(_godot_to_physical(global_position))
# --------------------------------------------------------------
# SIGNAL HANDLERS
# --------------------------------------------------------------
func _on_Admob_initialization_completed(_status: InitializationStatus) -> void:
if auto_load:
call_deferred("_deferred_load_ad")
func _on_resized() -> void:
if ad_id == "":
return
if do_ignore_resize:
do_ignore_resize = false
return
if abs(size.x - last_width) < resize_threshold:
return
last_width = size.x
remove_ad()
call_deferred("_deferred_load_ad")
_mark_position_dirty()
func _on_visibility_changed() -> void:
if ad_id != "":
if is_visible_in_tree():
show_ad()
else:
hide_ad()
func _on_banner_ad_loaded(loaded_ad_info: AdInfo, _response: ResponseInfo) -> void:
if !pending_load:
return
if loaded_ad_info.get_load_ad_request().get_ad_size() != LoadAdRequest.RequestedAdSize.INLINE_ADAPTIVE:
return
if loaded_ad_info.get_load_ad_request().get_ad_unit_id().casecmp_to(_ad_unit_id) != 0:
return
pending_load = false
ad_id = loaded_ad_info.get_ad_id()
_apply_loaded_size(Vector2(loaded_ad_info.get_measured_width(), loaded_ad_info.get_measured_height()))
_mark_position_dirty()
func _on_banner_ad_failed_to_load(failed_ad_info: AdInfo, _err: LoadAdError) -> void:
if failed_ad_info.get_load_ad_request().get_ad_size() != LoadAdRequest.RequestedAdSize.INLINE_ADAPTIVE:
return
if failed_ad_info.get_load_ad_request().get_ad_unit_id().casecmp_to(_ad_unit_id) != 0:
return
if pending_load:
pending_load = false
func _on_banner_ad_size_measured(ad_info: AdInfo) -> void:
if ad_info.get_ad_id().casecmp_to(ad_id) == 0:
_apply_loaded_size(Vector2(ad_info.get_measured_width(), ad_info.get_measured_height()))
func _on_banner_ad_refreshed(refreshed_ad_info: AdInfo, _response: ResponseInfo) -> void:
if refreshed_ad_info.get_ad_id().casecmp_to(ad_id) == 0:
hide_ad() # Prevent ad from briefly displaying out of position
_apply_loaded_size(Vector2(refreshed_ad_info.get_measured_width(), refreshed_ad_info.get_measured_height()))
_mark_position_dirty()
# --------------------------------------------------------------
# UTILITY
# --------------------------------------------------------------
func _apply_loaded_size(a_size: Vector2) -> void:
do_ignore_resize = true
custom_minimum_size = a_size
size = a_size
func _calculate_scale_factor() -> Vector2:
var screen_size: Vector2 = DisplayServer.window_get_size() as Vector2
var virtual_size: Vector2 = get_viewport().get_visible_rect().size
return screen_size / virtual_size
func _physical_to_godot(a_physical_size: Vector2) -> Vector2:
return a_physical_size / _calculate_scale_factor()
func _godot_to_physical(a_godot_size: Vector2) -> Vector2:
return a_godot_size * _calculate_scale_factor()
@@ -0,0 +1 @@
uid://dq4pb7uer2kjh
+8
View File
@@ -0,0 +1,8 @@
[General]
is_real=false
[Debug]
app_id="ca-app-pub-3940256099942544~3347511713"
[Release]
app_id=""
Binary file not shown.
+72
View File
@@ -0,0 +1,72 @@
class_name AdCache
extends RefCounted
var limit: int
var label: String
var remove_ad_callable: Callable
var _ads: Dictionary[String, Object]
var _ad_queue: Array[String]
func _init(a_limit: int, a_label: String, a_callable: Callable) -> void:
self.limit = a_limit
self.label = a_label
self.remove_ad_callable = a_callable
_ads = {}
_ad_queue = []
func is_empty() -> bool:
return _ads.is_empty()
func has_key(a_key: String) -> bool:
return _ads.has(a_key)
func last_key() -> String:
return "" if _ad_queue.is_empty() else _ad_queue[0]
func all_keys() -> Array[String]:
return _ads.keys()
func cache(a_ad_id: String, a_ad_data: Object) -> void:
_ads[a_ad_id] = a_ad_data
_ad_queue.push_front(a_ad_id)
while _ads.size() > limit:
(
GmpLogger
. log_warn(
(
"%s: %s cache size (%d) has exceeded maximum (%d)"
% [
Admob.PLUGIN_SINGLETON_NAME,
label,
_ads.size(),
limit,
]
),
)
)
remove_ad_callable.call(_erase_oldest())
func erase(a_ad_id: String) -> void:
_ad_queue.erase(a_ad_id)
_ads.erase(a_ad_id)
func erase_last() -> String:
var __removed_ad_id: String = _ad_queue.pop_front()
_ads.erase(__removed_ad_id)
return __removed_ad_id
func _erase_oldest() -> String:
var __removed_ad_id: String = _ad_queue.pop_back()
_ads.erase(__removed_ad_id)
return __removed_ad_id
+1
View File
@@ -0,0 +1 @@
uid://c8k2i5b2rg7qx
@@ -0,0 +1,24 @@
#
# © 2024-present https://github.com/cengiz-pz
#
class_name AdmobAndroidExportConfig
extends AdmobExportConfig
const ANDROID_CONFIG_FILE_PATH: String = "res://addons/" + PLUGIN_NAME + "/android_export.cfg"
func get_config_file_path() -> String:
return ANDROID_CONFIG_FILE_PATH
func load_platform_specific_export_config_from_node(a_node: Admob) -> Error:
debug_application_id = a_node.android_debug_application_id
real_application_id = a_node.android_real_application_id
return Error.OK
func print_loaded_config() -> void:
super.print_loaded_config()
GmpLogger.log_info("... debug_application_id: %s" % debug_application_id)
GmpLogger.log_info("... real_application_id: %s" % real_application_id)
@@ -0,0 +1 @@
uid://bd7jn56dvb8v4
@@ -0,0 +1,190 @@
#
# © 2024-present https://github.com/cengiz-pz
#
class_name AdmobExportConfig
extends RefCounted
const PLUGIN_NODE_TYPE_NAME = "Admob"
const PLUGIN_NAME: String = "AdmobPlugin"
const CONFIG_FILE_SECTION_GENERAL: String = "General"
const CONFIG_FILE_SECTION_DEBUG: String = "Debug"
const CONFIG_FILE_SECTION_RELEASE: String = "Release"
const CONFIG_FILE_SECTION_MEDIATION: String = "Mediation"
const CONFIG_FILE_KEY_IS_REAL: String = "is_real"
const CONFIG_FILE_KEY_APP_ID: String = "app_id"
const CONFIG_FILE_KEY_ENABLED_NETWORKS: String = "enabled_networks"
var is_real: bool
var debug_application_id: String
var real_application_id: String
var enabled_mediation_networks: Array[MediationNetwork] = []
func get_config_file_path() -> String:
return ""
func export_config_file_exists() -> bool:
return FileAccess.file_exists(get_config_file_path())
func load_export_config_from_file() -> Error:
GmpLogger.log_info("Loading export config from file!")
var __result = Error.OK
var __config_file_path = get_config_file_path()
var __config_file = ConfigFile.new()
var __load_result = __config_file.load(__config_file_path)
if __load_result == Error.OK:
is_real = __config_file.get_value(CONFIG_FILE_SECTION_GENERAL, CONFIG_FILE_KEY_IS_REAL)
debug_application_id = __config_file.get_value(CONFIG_FILE_SECTION_DEBUG, CONFIG_FILE_KEY_APP_ID)
real_application_id = __config_file.get_value(CONFIG_FILE_SECTION_RELEASE, CONFIG_FILE_KEY_APP_ID)
if __config_file.has_section(CONFIG_FILE_SECTION_MEDIATION):
if __config_file.has_section_key(CONFIG_FILE_SECTION_MEDIATION, CONFIG_FILE_KEY_ENABLED_NETWORKS):
var __network_array := (
Array(
__config_file.get_value(CONFIG_FILE_SECTION_MEDIATION, CONFIG_FILE_KEY_ENABLED_NETWORKS, []),
TYPE_STRING,
&"",
null,
)
as Array[String]
)
for __network in __network_array:
if MediationNetwork.is_valid_tag(__network):
enabled_mediation_networks.append(MediationNetwork.get_by_tag(__network))
else:
GmpLogger.log_error("Invalid network tag '%s' in file %s!" % [__network, __config_file_path])
else:
(
GmpLogger
. log_error(
(
"Missing key %s in section %s of %s!"
% [CONFIG_FILE_KEY_ENABLED_NETWORKS, CONFIG_FILE_SECTION_MEDIATION, __config_file_path]
),
)
)
if is_real == null or debug_application_id == null or real_application_id == null:
__result = Error.ERR_INVALID_DATA
else:
__result = load_platform_specific_export_config_from_file(__config_file)
if __result != Error.OK:
GmpLogger.log_error("Invalid export config file %s!" % __config_file_path)
else:
__result = Error.ERR_CANT_OPEN
GmpLogger.log_error("Failed to open export config file %s!" % __config_file_path)
if __result == Error.OK:
print_loaded_config()
return __result
func load_platform_specific_export_config_from_file(a_config_file: ConfigFile) -> Error:
return Error.OK
func load_export_config_from_node() -> Error:
GmpLogger.log_info("Loading export config from node!")
var __result = Error.OK
var __admob_node: Admob
GmpLogger.log_info("Searching edited scene for %s node..." % PLUGIN_NODE_TYPE_NAME)
var __edited_scene_root: Node = EditorInterface.get_edited_scene_root()
if __edited_scene_root:
__admob_node = get_plugin_node(__edited_scene_root)
else:
GmpLogger.log_info("No edited scene found")
if not __admob_node:
GmpLogger.log_info("Searching main scene for %s node..." % PLUGIN_NODE_TYPE_NAME)
var __main_scene_path: String = ProjectSettings.get_setting("application/run/main_scene")
if __main_scene_path and __main_scene_path != "":
var __packed_main: PackedScene = load(__main_scene_path)
if __packed_main:
var __main_scene = __packed_main.instantiate()
__admob_node = get_plugin_node(__main_scene)
else:
GmpLogger.log_info("Main scene path not defined in the project settings")
if not __admob_node:
GmpLogger.log_info("Searching all project scenes for %s node..." % PLUGIN_NODE_TYPE_NAME)
var collect_scene_paths: Callable = func(
a_dir_path: String,
a_collected_paths: Array[String],
a_recursive_func: Callable,
) -> void:
for __file in DirAccess.get_files_at(a_dir_path):
if __file.ends_with(".tscn") or __file.ends_with(".scn"):
a_collected_paths.append(a_dir_path.path_join(__file))
for __dir in DirAccess.get_directories_at(a_dir_path):
a_recursive_func.call(a_dir_path.path_join(__dir), a_collected_paths, a_recursive_func)
var __scene_paths: Array[String] = []
collect_scene_paths.call("res://", __scene_paths, collect_scene_paths)
for __scene_path in __scene_paths:
var packed: PackedScene = load(__scene_path)
if packed:
var __instance = packed.instantiate()
var __found_node: Admob = get_plugin_node(__instance)
if __found_node:
GmpLogger.log_info("Found %s node in scene: %s" % [PLUGIN_NODE_TYPE_NAME, __scene_path])
__admob_node = __found_node
break
# Load configuration if node found
if __admob_node:
is_real = __admob_node.is_real
enabled_mediation_networks = MediationNetwork.get_all_enabled(__admob_node.enabled_networks)
__result = load_platform_specific_export_config_from_node(__admob_node)
if __result == Error.OK:
print_loaded_config()
else:
GmpLogger.log_error("Invalid %s node for %s!" % [PLUGIN_NODE_TYPE_NAME, PLUGIN_NAME])
else:
GmpLogger.log_error("%s failed to find %s node!" % [PLUGIN_NAME, PLUGIN_NODE_TYPE_NAME])
__result = Error.ERR_UNCONFIGURED
return __result
func load_platform_specific_export_config_from_node(a_node: Admob) -> Error:
return Error.OK
func print_loaded_config() -> void:
GmpLogger.log_info("Loaded export configuration settings:")
GmpLogger.log_info(" is_real: %s" % ("true" if is_real else "false"))
GmpLogger.log_info(
" enabled_mediation_networks: %s" % MediationNetwork.generate_tag_list(enabled_mediation_networks)
)
func get_plugin_node(a_node: Node) -> Admob:
var __result: Admob
if a_node is Admob:
__result = a_node
elif a_node.get_child_count() > 0:
for __child in a_node.get_children():
var __child_result = get_plugin_node(__child)
if __child_result is Admob:
__result = __child_result
break
return __result
@@ -0,0 +1 @@
uid://byw0f6m12b2s2
@@ -0,0 +1,43 @@
#
# © 2024-present https://github.com/cengiz-pz
#
class_name AdmobIosExportConfig
extends AdmobExportConfig
const IOS_CONFIG_FILE_PATH: String = "res://addons/" + PLUGIN_NAME + "/ios_export.cfg"
const CONFIG_FILE_SECTION_ATT: String = "ATT"
const CONFIG_FILE_KEY_ATT_ENABLED: String = "att_enabled"
const CONFIG_FILE_KEY_ATT_TEXT: String = "att_text"
var att_enabled: bool
var att_text: String
func get_config_file_path() -> String:
return IOS_CONFIG_FILE_PATH
func load_platform_specific_export_config_from_file(a_config_file: ConfigFile) -> Error:
att_enabled = a_config_file.get_value(CONFIG_FILE_SECTION_ATT, CONFIG_FILE_KEY_ATT_ENABLED, false)
att_text = a_config_file.get_value(CONFIG_FILE_SECTION_ATT, CONFIG_FILE_KEY_ATT_TEXT)
return Error.OK
func load_platform_specific_export_config_from_node(a_node: Admob) -> Error:
debug_application_id = a_node.ios_debug_application_id
real_application_id = a_node.ios_real_application_id
att_enabled = a_node.att_enabled
att_text = a_node.att_text
return Error.OK
func print_loaded_config() -> void:
super.print_loaded_config()
GmpLogger.log_info("... debug_application_id: %s" % debug_application_id)
GmpLogger.log_info("... real_application_id: %s" % real_application_id)
GmpLogger.log_info("... att_enabled: %s" % ("true" if att_enabled else "false"))
GmpLogger.log_info("... att_text: %s" % att_text)
@@ -0,0 +1 @@
uid://hamb6dlvl4bc
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cmkpgeo3anvuw"
path="res://.godot/imported/icon.png-928f79d906c5a286de384358dfb1ba26.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/AdmobPlugin/icon.png"
dest_files=["res://.godot/imported/icon.png-928f79d906c5a286de384358dfb1ba26.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
+12
View File
@@ -0,0 +1,12 @@
[General]
is_real=false
[Debug]
app_id="ca-app-pub-3940256099942544~1458002511"
[Release]
app_id=""
[ATT]
att_enabled=false
att_text="Deine Erlaubnis hilft uns, Werbung zu messen und das Spiel zu verbessern."
+32
View File
@@ -0,0 +1,32 @@
#
# © 2024-present https://github.com/cengiz-pz
#
class_name AdError
extends RefCounted
const CODE_PROPERTY := &"code"
const DOMAIN_PROPERTY := &"domain"
const MESSAGE_PROPERTY := &"message"
const CAUSE_PROPERTY := &"cause"
var _data: Dictionary
func _init(a_data: Dictionary):
_data = a_data
func get_code() -> int:
return _data[CODE_PROPERTY]
func get_domain() -> String:
return _data[DOMAIN_PROPERTY]
func get_message() -> String:
return _data[MESSAGE_PROPERTY]
func get_cause() -> AdError:
return AdError.new(_data[CAUSE_PROPERTY]) if _data.has(CAUSE_PROPERTY) else null
+1
View File
@@ -0,0 +1 @@
uid://0att8tjqjm8t
+37
View File
@@ -0,0 +1,37 @@
#
# © 2024-present https://github.com/cengiz-pz
#
class_name AdInfo
extends RefCounted
const AD_ID_PROPERTY := &"ad_id"
const MEASURED_WIDTH_PROPERTY := &"measured_width"
const MEASURED_HEIGHT_PROPERTY := &"measured_height"
const IS_COLLAPSIBLE_PROPERTY := &"is_collapsible"
const LOAD_AD_REQUEST_PROPERTY := &"load_ad_request"
var _data: Dictionary
func _init(a_data: Dictionary):
_data = a_data
func get_ad_id() -> String:
return _data[AD_ID_PROPERTY] if _data.has(AD_ID_PROPERTY) else ""
func get_measured_width() -> int:
return _data[MEASURED_WIDTH_PROPERTY] if _data.has(MEASURED_WIDTH_PROPERTY) else 0
func get_measured_height() -> int:
return _data[MEASURED_HEIGHT_PROPERTY] if _data.has(MEASURED_HEIGHT_PROPERTY) else 0
func get_is_collapsible() -> bool:
return _data[IS_COLLAPSIBLE_PROPERTY] if _data.has(IS_COLLAPSIBLE_PROPERTY) else false
func get_load_ad_request() -> LoadAdRequest:
return LoadAdRequest.new(_data[LOAD_AD_REQUEST_PROPERTY]) if _data.has(LOAD_AD_REQUEST_PROPERTY) else null
+1
View File
@@ -0,0 +1 @@
uid://pm7kx5541j4
+22
View File
@@ -0,0 +1,22 @@
#
# © 2024-present https://github.com/cengiz-pz
#
class_name AdSize
extends RefCounted
const WIDTH_PROPERTY := &"width"
const HEIGHT_PROPERTY := &"height"
var _data: Dictionary
func _init(a_data: Dictionary):
_data = a_data if a_data else {}
func get_width() -> int:
return _data[WIDTH_PROPERTY] if _data.has(WIDTH_PROPERTY) else 0
func get_height() -> int:
return _data[HEIGHT_PROPERTY] if _data.has(HEIGHT_PROPERTY) else 0
+1
View File
@@ -0,0 +1 @@
uid://1y453yingj1w
@@ -0,0 +1,52 @@
#
# © 2024-present https://github.com/cengiz-pz
#
class_name AdapterResponseInfo
extends RefCounted
const AD_ERROR_PROPERTY := &"ad_error"
const AD_SOURCE_ID_PROPERTY := &"ad_source_id"
const AD_SOURCE_INSTANCE_ID_PROPERTY := &"ad_source_instance_id"
const AD_SOURCE_INSTANCE_NAME_PROPERTY := &"ad_source_instance_name"
const AD_SOURCE_NAME_PROPERTY := &"ad_source_name"
const ADAPTER_CLASS_NAME_PROPERTY := &"adapter_class_name"
const NETWORK_TAG_PROPERTY := &"network_tag"
const LATENCY_PROPERTY := &"latency"
var _data: Dictionary
func _init(a_data: Dictionary):
_data = a_data
func get_ad_error() -> AdError:
return AdError.new(_data[AD_ERROR_PROPERTY]) if _data.has(AD_ERROR_PROPERTY) else null
func get_ad_source_id() -> String:
return _data[AD_SOURCE_ID_PROPERTY]
func get_ad_source_instance_id() -> String:
return _data[AD_SOURCE_INSTANCE_ID_PROPERTY]
func get_ad_source_instance_name() -> String:
return _data[AD_SOURCE_INSTANCE_NAME_PROPERTY]
func get_ad_source_name() -> String:
return _data[AD_SOURCE_NAME_PROPERTY]
func get_adapter_class_name() -> String:
return _data[ADAPTER_CLASS_NAME_PROPERTY]
func get_network_tag() -> String:
return _data[NETWORK_TAG_PROPERTY]
func get_latency() -> int:
return _data[LATENCY_PROPERTY]
@@ -0,0 +1 @@
uid://kwsy2dkniyn8
+32
View File
@@ -0,0 +1,32 @@
#
# © 2024-present https://github.com/cengiz-pz
#
class_name AdapterStatus
extends RefCounted
const ADAPTER_CLASS_PROPERTY := &"adapter_class"
const LATENCY_PROPERTY := &"latency"
const INITIALIZATION_STATE_PROPERTY := &"initialization_state"
const DESCRIPTION_PROPERTY := &"description"
var _data: Dictionary
func _init(a_data: Dictionary):
_data = a_data
func get_adapter_class() -> String:
return _data[ADAPTER_CLASS_PROPERTY]
func get_latency() -> int:
return _data[LATENCY_PROPERTY]
func get_initialization_state() -> String:
return _data[INITIALIZATION_STATE_PROPERTY]
func get_description() -> String:
return _data[DESCRIPTION_PROPERTY]
@@ -0,0 +1 @@
uid://scnfdeiyquod
+105
View File
@@ -0,0 +1,105 @@
#
# © 2024-present https://github.com/cengiz-pz
#
class_name AdmobConfig
extends RefCounted
enum ContentRating {
G, ## G means "General Audiences" and is suitable for all ages, containing no objectionable material
## (minimal violence, no nudity or sex scenes, and no drug use).
PG, ## PG stands for "Parental Guidance Suggested," meaning that some material may not be suitable for young
## children (may include themes, language, or mild violence).
T, ## T means Teen, and indicates content suitable for ages 13 and older (can include violence, suggestive themes,
## crude humor, simulated gambling, and/or infrequent strong language).
MA, ## MA stands for "Mature Audience", signifying that the material is intended for adults (may include graphic
## violence, strong language, or explicit sexual activity).
}
enum TagForChildDirectedTreatment {
UNSPECIFIED = -1, ## Means that you have not provided a specific instruction for your app's content to be treated
## as child-directed for COPPA purposes.
FALSE = 0, ## Means you are indicating that your app's content is not child-directed for the purposes of the
## Children's Online Privacy Protection Act (COPPA).
TRUE = 1, ## Tells ad networks to treat ad requests as if the content is directed towards children, which is
## important for complying with regulations like COPPA.
}
enum TagForUnderAgeOfConsent {
UNSPECIFIED = -1, ## Means you have not provided any information on whether the user is under the age of consent,
## so the system does not apply any specific treatment for these users based on this tag alone.
FALSE = 0, ## Indicates that you do not want the specific ad request to receive the special handling suitable for
## users under the age of consent in the EEA, the UK, and Switzerland.
TRUE = 1, ## Indicates that you want the ad request to be handled in a manner suitable for users under the age of
## consent within the EEA, the UK, and Switzerland.
}
enum PersonalizationState {
DEFAULT = 0, ## By default, the SDK will attempt to serve personalized ads based on the user's past behavior and
## interests.
ENABLED = 1, ## Indicates that the Google Mobile Ads SDK is authorized to serve ads that are tailored to the user
## based on past activity and collected data.
DISABLED = 2, ## Means that ad requests are set to serve non-personalized ads (NPA) only.
}
const DATA_KEY_IS_REAL := &"is_real"
const DATA_KEY_MAX_AD_CONTENT_RATING := &"max_ad_content_rating"
const DATA_KEY_CHILD_DIRECTED_TREATMENT := &"tag_for_child_directed_treatment"
const DATA_KEY_UNDER_AGE_OF_CONSENT := &"tag_for_under_age_of_consent"
const DATA_KEY_FIRST_PARTY_ID_ENABLED := &"first_party_id_enabled"
const DATA_KEY_PERSONALIZATION_STATE := &"personalization_state"
const DATA_KEY_TEST_DEVICE_IDS := &"test_device_ids"
var _data: Dictionary
func _init():
_data = {
DATA_KEY_TEST_DEVICE_IDS: [],
}
func set_is_real(a_value: bool) -> AdmobConfig:
_data[DATA_KEY_IS_REAL] = a_value
return self
func set_max_ad_content_rating(a_value: ContentRating) -> AdmobConfig:
_data[DATA_KEY_MAX_AD_CONTENT_RATING] = ContentRating.keys()[a_value]
return self
func set_child_directed_treatment(a_value: TagForChildDirectedTreatment) -> AdmobConfig:
_data[DATA_KEY_CHILD_DIRECTED_TREATMENT] = a_value
return self
func set_under_age_of_consent(a_value: TagForUnderAgeOfConsent) -> AdmobConfig:
_data[DATA_KEY_UNDER_AGE_OF_CONSENT] = a_value
return self
func set_first_party_id_enabled(a_value: bool) -> AdmobConfig:
_data[DATA_KEY_FIRST_PARTY_ID_ENABLED] = a_value
return self
func set_personalization_state(a_value: PersonalizationState) -> AdmobConfig:
_data[DATA_KEY_PERSONALIZATION_STATE] = a_value
return self
func set_test_device_ids(a_value: Array) -> AdmobConfig:
if a_value == null:
_data[DATA_KEY_TEST_DEVICE_IDS] = []
else:
_data[DATA_KEY_TEST_DEVICE_IDS] = a_value
return self
func add_test_device_id(a_value: String) -> AdmobConfig:
_data[DATA_KEY_TEST_DEVICE_IDS].append(a_value)
return self
func get_raw_data() -> Dictionary:
return _data
@@ -0,0 +1 @@
uid://ihpl6qb5lkw2
+46
View File
@@ -0,0 +1,46 @@
#
# © 2026-present https://github.com/cengiz-pz
#
class_name AdmobSettings
extends RefCounted
const DATA_KEY_AD_VOLUME := &"ad_volume"
const DATA_KEY_ADS_MUTED := &"ads_muted"
const DATA_KEY_APPLY_AT_STARTUP := &"apply_at_startup"
var _data: Dictionary
func _init(a_data: Dictionary = {}) -> void:
_data = a_data
func set_ad_volume(a_value: float) -> AdmobSettings:
_data[DATA_KEY_AD_VOLUME] = a_value
return self
func get_ad_volume() -> float:
return _data[DATA_KEY_AD_VOLUME]
func set_ads_muted(a_value: bool) -> AdmobSettings:
_data[DATA_KEY_ADS_MUTED] = a_value
return self
func are_ads_muted() -> bool:
return _data[DATA_KEY_ADS_MUTED]
func set_apply_at_startup(a_value: bool) -> AdmobSettings:
_data[DATA_KEY_APPLY_AT_STARTUP] = a_value
return self
func get_apply_at_startup() -> bool:
return _data[DATA_KEY_APPLY_AT_STARTUP]
func get_raw_data() -> Dictionary:
return _data
@@ -0,0 +1 @@
uid://u1bg67vtlhd8
@@ -0,0 +1,63 @@
#
# © 2024-present https://github.com/cengiz-pz
#
class_name ConsentRequestParameters
extends RefCounted
enum DebugGeography {
NOT_SET = -1, ## Don't specify location. (Actual location will be used.)
DISABLED = 0, ## Use actual location.
EEA = 1, ## Use European Economic Area.
NOT_EEA = 2, ## Deprecated.
REGULATED_US_STATE = 3, ## Use a regulated state of USA. (ie. California)
OTHER = 4, ## Use any non-regulated location.
}
const IS_REAL_PROPERTY := &"is_real"
const TAG_FOR_UNDER_AGE_OF_CONSENT_PROPERTY := &"tag_for_under_age_of_consent"
const DEBUG_GEOGRAPHY_PROPERTY := &"debug_geography"
const TEST_DEVICE_HASHED_IDS_PROPERTY := &"test_device_hashed_ids"
var _data: Dictionary
func _init():
_data = {
TEST_DEVICE_HASHED_IDS_PROPERTY: [],
}
func set_is_real(a_value: bool) -> ConsentRequestParameters:
_data[IS_REAL_PROPERTY] = a_value
return self
func set_tag_for_under_age_of_consent(a_value: AdmobConfig.TagForUnderAgeOfConsent) -> ConsentRequestParameters:
match a_value:
AdmobConfig.TagForChildDirectedTreatment.TRUE:
_data[TAG_FOR_UNDER_AGE_OF_CONSENT_PROPERTY] = true
AdmobConfig.TagForChildDirectedTreatment.FALSE:
_data[TAG_FOR_UNDER_AGE_OF_CONSENT_PROPERTY] = false
return self
func set_debug_geography(a_value: DebugGeography) -> ConsentRequestParameters:
_data[DEBUG_GEOGRAPHY_PROPERTY] = a_value
return self
func add_test_device_hashed_id(a_value: String) -> ConsentRequestParameters:
_data[TEST_DEVICE_HASHED_IDS_PROPERTY].append(a_value)
return self
func get_device_ids() -> Array:
return _data[TEST_DEVICE_HASHED_IDS_PROPERTY]
func get_raw_data() -> Dictionary:
return _data
@@ -0,0 +1 @@
uid://5nuqawjs80oi
+22
View File
@@ -0,0 +1,22 @@
#
# © 2024-present https://github.com/cengiz-pz
#
class_name FormError
extends RefCounted
const CODE_PROPERTY := &"code"
const MESSAGE_PROPERTY := &"message"
var _data: Dictionary
func _init(a_data: Dictionary):
_data = a_data if a_data else {}
func get_code() -> int:
return _data[CODE_PROPERTY] if _data.has(CODE_PROPERTY) else 0
func get_message() -> String:
return _data[MESSAGE_PROPERTY] if _data.has(MESSAGE_PROPERTY) else ""
@@ -0,0 +1 @@
uid://f4ywykxwjpal
@@ -0,0 +1,22 @@
#
# © 2024-present https://github.com/cengiz-pz
#
class_name InitializationStatus
extends RefCounted
var _data: Dictionary
func _init(a_data: Dictionary):
if a_data == null:
_data = {}
else:
_data = a_data
func get_network_tags() -> Array:
return _data.keys()
func get_adapter_status(a_network_tag: String) -> AdapterStatus:
return AdapterStatus.new(_data[a_network_tag]) if _data.has(a_network_tag) else null
@@ -0,0 +1 @@
uid://b0oie3ngw68ha
+15
View File
@@ -0,0 +1,15 @@
#
# © 2024-present https://github.com/cengiz-pz
#
class_name LoadAdError
extends AdError
const RESPONSE_INFO_PROPERTY := &"response_info"
func _init(a_data: Dictionary):
super(a_data)
func get_response_info() -> ResponseInfo:
return ResponseInfo.new(_data[RESPONSE_INFO_PROPERTY]) if _data.has(RESPONSE_INFO_PROPERTY) else null
@@ -0,0 +1 @@
uid://ddnpjk4ksplb5
+332
View File
@@ -0,0 +1,332 @@
#
# © 2024-present https://github.com/cengiz-pz
#
class_name LoadAdRequest
extends RefCounted
enum AdPosition {
TOP, ## Ad will be anchored at the top of the screen.
BOTTOM, ## Ad will be anchored at the bottom of the screen.
LEFT, ## Ad will be anchored to the left of the screen.
RIGHT, ## Ad will be anchored to the right of the screen.
TOP_LEFT, ## Ad will be anchored at the top-left of the screen.
TOP_RIGHT, ## Ad will be anchored at the top-right of the screen.
BOTTOM_LEFT, ## Ad will be anchored at the bottom-left of the screen.
BOTTOM_RIGHT, ## Ad will be anchored at the bottom-right of the screen.
CENTER, ## Ad will be anchored at the center of the screen.
CUSTOM, ## Ad position will be set dynamically.
}
enum RequestedAdSize {
BANNER, ## A standard ad size that refers to a rectangular ad unit, most commonly 320x50 density-independent pixels
## (dp).
LARGE_BANNER, ## A fixed-size banner with dimensions of 320x100 density-independent pixels (dp).
MEDIUM_RECTANGLE, ## A standard 300x250 pixel ad, often referred to as the Interactive Advertising Bureau (IAB)
## medium rectangle.
FULL_BANNER, ## An ad size constant for an Interactive Advertising Bureau (IAB) full-size banner, which has a fixed
## size of 468x60 density-independent pixels (dp).
LEADERBOARD, ## A banner ad with dimensions of 728 pixels wide by 90 pixels tall. This is a standard display ad
## size that is designed for tablet screens.
ADAPTIVE, ## An ad that automatically adjusts its width and height to fit the device's available screen space,
## providing the best-performing banner size for that specific layout.
INLINE_ADAPTIVE, ## An ad that dynamically optimizes its height for the available inline content width, ensuring a
## smoothly integrated banner that fits naturally within scrollable or in-feed layouts.
SKYSCRAPER, ## Deprecated
FLUID, ## Deprecated
}
enum CollapsiblePosition {
DISABLED, ## The banner ad will not be collapsible.
TOP, ## The banner ad will be collapsible from bottom to top.
BOTTOM, ## The banner ad will be collapsible from top to bottom.
}
## Controls which aspect ratios the SDK should prefer when requesting native ad media.
## Maps to NativeAdOptions.NATIVE_MEDIA_ASPECT_RATIO_* constants.
enum NativeMediaAspectRatio {
UNKNOWN = 0, ## No preference for aspect ratio (SDK default).
ANY = 1, ## Any aspect ratio is acceptable.
LANDSCAPE = 2, ## Prefer landscape (wider than tall) media.
PORTRAIT = 3, ## Prefer portrait (taller than wide) media.
SQUARE = 4, ## Prefer square (1:1) media.
}
## Controls where the AdChoices icon is placed within the native ad view.
## Maps to NativeAdOptions.ADCHOICES_* constants.
enum NativeAdChoicesPlacement {
TOP_LEFT = 0, ## AdChoices icon in the top-left corner.
TOP_RIGHT = 1, ## AdChoices icon in the top-right corner (SDK default).
BOTTOM_RIGHT = 2, ## AdChoices icon in the bottom-right corner.
BOTTOM_LEFT = 3, ## AdChoices icon in the bottom-left corner.
}
## Controls how icon/image assets are scaled inside their ImageView.
## Maps directly to Android's ImageView.ScaleType enum.
enum NativeImageScaleType {
MATRIX = 0, ## Scale using the image Matrix.
FIT_XY = 1, ## Scale to fill the view bounds, ignoring aspect ratio.
FIT_START = 2, ## Scale to fit within bounds, aligned to top-left.
FIT_CENTER = 3, ## Scale to fit within bounds, centered (Android default).
FIT_END = 4, ## Scale to fit within bounds, aligned to bottom-right.
CENTER = 5, ## Center image without scaling.
CENTER_CROP = 6, ## Scale so shorter dimension fits; crop the longer one.
CENTER_INSIDE = 7, ## Scale to fit within bounds if larger; center otherwise.
}
const COLLAPSIBLE_POSITION_NAMES: Dictionary = {
CollapsiblePosition.TOP: "top",
CollapsiblePosition.BOTTOM: "bottom",
}
const DATA_KEY_AD_UNIT_ID := &"ad_unit_id"
const DATA_KEY_REQUEST_AGENT := &"request_agent"
const DATA_KEY_AD_SIZE := &"ad_size"
const DATA_KEY_ADAPTIVE_WIDTH := &"adaptive_width"
const DATA_KEY_ADAPTIVE_MAX_HEIGHT := &"adaptive_max_height"
const DATA_KEY_AD_POSITION := &"ad_position"
const DATA_KEY_COLLAPSIBLE_POSITION := &"collapsible_position"
const DATA_KEY_ANCHOR_TO_SAFE_AREA := &"anchor_to_safe_area"
const DATA_KEY_KEYWORDS := &"keywords"
const DATA_KEY_USER_ID := &"user_id"
const DATA_KEY_CUSTOM_DATA := &"custom_data"
const DATA_KEY_NETWORK_EXTRAS := &"network_extras"
# Native ad option keys — sent to the Java layer inside the ad-request dictionary.
const DATA_KEY_NATIVE_MEDIA_ASPECT_RATIO := &"native_media_aspect_ratio"
const DATA_KEY_NATIVE_RETURN_URLS_FOR_IMAGE_ASSETS := &"native_return_urls_for_image_assets"
const DATA_KEY_NATIVE_REQUEST_MULTIPLE_IMAGES := &"native_request_multiple_images"
const DATA_KEY_NATIVE_AD_CHOICES_PLACEMENT := &"native_ad_choices_placement"
const DATA_KEY_NATIVE_IMAGE_SCALE_TYPE := &"native_image_scale_type"
const DATA_KEY_NATIVE_DISABLE_VALIDATOR := &"native_disable_validator"
const DEFAULT_DATA: Dictionary = {
DATA_KEY_KEYWORDS: [],
DATA_KEY_NETWORK_EXTRAS: [],
}
var _data: Dictionary
func _init(a_data: Dictionary = DEFAULT_DATA.duplicate()) -> void:
_data = a_data
func set_ad_unit_id(a_value: String) -> LoadAdRequest:
_data[DATA_KEY_AD_UNIT_ID] = a_value
return self
func get_ad_unit_id() -> String:
return _data[DATA_KEY_AD_UNIT_ID]
func set_request_agent(a_value: String) -> LoadAdRequest:
_data[DATA_KEY_REQUEST_AGENT] = a_value
return self
func get_request_agent() -> String:
return _data[DATA_KEY_REQUEST_AGENT]
func set_ad_size(a_value: RequestedAdSize) -> LoadAdRequest:
_data[DATA_KEY_AD_SIZE] = RequestedAdSize.keys()[a_value]
return self
func get_ad_size() -> RequestedAdSize:
return RequestedAdSize[_data[DATA_KEY_AD_SIZE]] if _data.has(DATA_KEY_AD_SIZE) else RequestedAdSize.BANNER
func set_adaptive_width(a_value: int) -> LoadAdRequest:
_data[DATA_KEY_ADAPTIVE_WIDTH] = a_value
return self
func get_adaptive_width() -> int:
return _data[DATA_KEY_ADAPTIVE_WIDTH]
func set_adaptive_max_height(a_value: int) -> LoadAdRequest:
_data[DATA_KEY_ADAPTIVE_MAX_HEIGHT] = a_value
return self
func get_adaptive_max_height() -> int:
return _data[DATA_KEY_ADAPTIVE_MAX_HEIGHT]
func set_ad_position(a_value: AdPosition) -> LoadAdRequest:
_data[DATA_KEY_AD_POSITION] = AdPosition.keys()[a_value]
return self
func get_ad_position() -> AdPosition:
return AdPosition[_data[DATA_KEY_AD_POSITION]] if _data.has(DATA_KEY_AD_POSITION) else AdPosition.CUSTOM
func set_collapsible_position(a_value: CollapsiblePosition) -> LoadAdRequest:
if a_value != CollapsiblePosition.DISABLED:
_data[DATA_KEY_COLLAPSIBLE_POSITION] = COLLAPSIBLE_POSITION_NAMES[a_value]
return self
func get_collapsible_position() -> CollapsiblePosition:
return (
CollapsiblePosition[_data[DATA_KEY_COLLAPSIBLE_POSITION]]
if _data.has(DATA_KEY_COLLAPSIBLE_POSITION)
else CollapsiblePosition.DISABLED
)
func set_anchor_to_safe_area(a_value: bool) -> LoadAdRequest:
_data[DATA_KEY_ANCHOR_TO_SAFE_AREA] = a_value
return self
func get_anchor_to_safe_area() -> bool:
return _data[DATA_KEY_ANCHOR_TO_SAFE_AREA] if _data.has(DATA_KEY_ANCHOR_TO_SAFE_AREA) else false
func set_keywords(a_value: Array) -> LoadAdRequest:
if a_value == null:
_data[DATA_KEY_KEYWORDS] = []
else:
_data[DATA_KEY_KEYWORDS] = a_value
return self
func add_keyword(a_value: String) -> LoadAdRequest:
_data[DATA_KEY_KEYWORDS].append(a_value)
return self
func get_keywords() -> Array:
return _data[DATA_KEY_KEYWORDS] if _data.has(DATA_KEY_KEYWORDS) else []
func set_user_id(a_value: String) -> LoadAdRequest:
_data[DATA_KEY_USER_ID] = a_value
return self
func get_user_id() -> String:
return _data[DATA_KEY_USER_ID] if _data.has(DATA_KEY_USER_ID) else ""
func set_custom_data(a_value: String) -> LoadAdRequest:
_data[DATA_KEY_CUSTOM_DATA] = a_value
return self
func get_custom_data() -> String:
return _data[DATA_KEY_CUSTOM_DATA] if _data.has(DATA_KEY_CUSTOM_DATA) else ""
func set_network_extras(a_value: Array) -> LoadAdRequest:
if a_value == null:
_data[DATA_KEY_NETWORK_EXTRAS] = []
else:
_data[DATA_KEY_NETWORK_EXTRAS] = a_value
return self
func get_network_extras() -> Array:
return _data[DATA_KEY_NETWORK_EXTRAS] if _data.has(DATA_KEY_NETWORK_EXTRAS) else []
# ---------------------------------------------------------------------------
# Native Ad Options
# ---------------------------------------------------------------------------
## Sets a preferred media aspect ratio for native ads.
## Has no effect when used for non-native ad types.
func set_native_media_aspect_ratio(a_value: NativeMediaAspectRatio) -> LoadAdRequest:
_data[DATA_KEY_NATIVE_MEDIA_ASPECT_RATIO] = NativeMediaAspectRatio.keys()[a_value]
return self
func get_native_media_aspect_ratio() -> NativeMediaAspectRatio:
return (
NativeMediaAspectRatio[_data[DATA_KEY_NATIVE_MEDIA_ASPECT_RATIO]]
if _data.has(DATA_KEY_NATIVE_MEDIA_ASPECT_RATIO)
else NativeMediaAspectRatio.UNKNOWN
)
## When true the SDK returns image asset URLs instead of pre-fetched Drawable
## objects, allowing the app to download and manage images itself.
## Has no effect when used for non-native ad types.
func set_native_return_urls_for_image_assets(a_value: bool) -> LoadAdRequest:
_data[DATA_KEY_NATIVE_RETURN_URLS_FOR_IMAGE_ASSETS] = a_value
return self
func get_native_return_urls_for_image_assets() -> bool:
return (
_data[DATA_KEY_NATIVE_RETURN_URLS_FOR_IMAGE_ASSETS]
if _data.has(DATA_KEY_NATIVE_RETURN_URLS_FOR_IMAGE_ASSETS)
else false
)
## When true the SDK may return ads that contain multiple images for a single
## asset slot. By default only one image per slot is returned.
## Has no effect when used for non-native ad types.
func set_native_request_multiple_images(a_value: bool) -> LoadAdRequest:
_data[DATA_KEY_NATIVE_REQUEST_MULTIPLE_IMAGES] = a_value
return self
func get_native_request_multiple_images() -> bool:
return (
_data[DATA_KEY_NATIVE_REQUEST_MULTIPLE_IMAGES] if _data.has(DATA_KEY_NATIVE_REQUEST_MULTIPLE_IMAGES) else false
)
## Sets where the AdChoices icon is placed inside the native ad view.
## Has no effect when used for non-native ad types.
func set_native_ad_choices_placement(a_value: NativeAdChoicesPlacement) -> LoadAdRequest:
_data[DATA_KEY_NATIVE_AD_CHOICES_PLACEMENT] = NativeAdChoicesPlacement.keys()[a_value]
return self
func get_native_ad_choices_placement() -> NativeAdChoicesPlacement:
return (
NativeAdChoicesPlacement[_data[DATA_KEY_NATIVE_AD_CHOICES_PLACEMENT]]
if _data.has(DATA_KEY_NATIVE_AD_CHOICES_PLACEMENT)
else NativeAdChoicesPlacement.TOP_RIGHT
)
## Sets how icon and image assets are scaled inside their Android ImageView.
## Has no effect when used for non-native ad types.
func set_native_image_scale_type(a_value: NativeImageScaleType) -> LoadAdRequest:
_data[DATA_KEY_NATIVE_IMAGE_SCALE_TYPE] = NativeImageScaleType.keys()[a_value]
return self
func get_native_image_scale_type() -> NativeImageScaleType:
return (
NativeImageScaleType[_data[DATA_KEY_NATIVE_IMAGE_SCALE_TYPE]]
if _data.has(DATA_KEY_NATIVE_IMAGE_SCALE_TYPE)
else NativeImageScaleType.FIT_CENTER
)
## When true the SDK's native ad validator is disabled. The validator normally
## logs warnings when required view bindings are missing; disabling it suppresses
## those warnings in test / custom-layout scenarios.
## Has no effect when used for non-native ad types.
func set_native_disable_validator(a_value: bool) -> LoadAdRequest:
_data[DATA_KEY_NATIVE_DISABLE_VALIDATOR] = a_value
return self
func get_native_disable_validator() -> bool:
return _data[DATA_KEY_NATIVE_DISABLE_VALIDATOR] if _data.has(DATA_KEY_NATIVE_DISABLE_VALIDATOR) else false
func get_raw_data() -> Dictionary:
return _data
@@ -0,0 +1 @@
uid://c86msp23213bn
@@ -0,0 +1,322 @@
#
# © 2024-present https://github.com/cengiz-pz
#
class_name MediationNetwork
extends RefCounted
enum Flag {
APPLOVIN = 1 << 0, ## AppLovin
CHARTBOOST = 1 << 1, ## Chartboost
DTEXCHANGE = 1 << 2, ## DT Exchange (previously Fyber)
IMOBILE = 1 << 3, ## i-mobile
INMOBI = 1 << 4, ## InMobi
IRONSOURCE = 1 << 5, ## ironSource
LIFTOFF = 1 << 6, ## Liftoff Monetize (previously Vungle)
LINE = 1 << 7, ## LINE Ads Network
MAIO = 1 << 8, ## maio
META = 1 << 9, ## Meta Audience Network (previously Facebook)
MINTEGRAL = 1 << 10, ## Mintegral
MOLOCO = 1 << 11, ## Moloco
MYTARGET = 1 << 12, ## myTarget
PANGLE = 1 << 13, ## Pangle
UNITY = 1 << 14, ## Unity Ads
}
const FLAG_PROPERTY := &"flag"
const TAG_PROPERTY := &"tag"
const DEPENDENCIES_PROPERTY := &"dependencies"
const MAVEN_REPO_PROPERTY := &"maven_repo"
const PACKAGE_PROPERTY := &"package"
const PACKAGE_URL_PROPERTY := &"package_url"
const PACKAGE_VERSION_PROPERTY := &"package_version"
const SK_AD_NETWORK_IDS_PROPERTY := &"sk_ad_network_ids"
const GOOGLE_SK_AD_NETWORK_IDS = [ "cstr6suwn9", "4fzdc2evr5", "2fnua5tdw4", "ydx93a7ass", "p78axxw29g", "v72qych5uu", "ludvb6z3bs", "cp8zw746q7", "3sh42y64q3", "c6k4g5qg8m", "s39g8k73mm", "3qy4746246", "f38h382jlk", "hs6bdukanm", "mlmmfzh3r3", "v4nxqhlyqp", "wzmmz9fp6w", "su67r6k2v3", "yclnxrl5pm", "t38b2kh725", "7ug5zh24hu", "gta9lk7p23", "vutu7akeur", "y5ghdn5j9k", "v9wttpbfk9", "n38lu8286q", "47vhws6wlr", "kbd757ywx3", "9t245vhmpl", "a2p9lx4jpn", "22mmun2rn5", "44jx6755aq", "k674qkevps", "4468km3ulz", "2u9pt9hc89", "8s468mfl3y", "klf5c3l5u5", "ppxm28t8ap", "kbmxgpxpgc", "uw77j35x4d", "578prtvx9j", "4dzt52r2t5", "tl55sbb4fm", "c3frkrj4fj", "e5fvkxwrpn", "8c4e2ghe7u", "3rd42ekr43", "97r2b46745", "3qcr597p9d" ]
const SK_AD_NETWORK_ITEM_LIST_FORMAT: String = """
<key>SKAdNetworkItems</key>
<array>
%s
</array>
"""
const SK_AD_NETWORK_ITEM_FORMAT: String = """
<dict>
<key>SKAdNetworkIdentifier</key>
<string>%s.skadnetwork</string>
</dict>
"""
const MEDIATION_NETWORKS: Dictionary = {
Flag.APPLOVIN: {
FLAG_PROPERTY: Flag.APPLOVIN,
TAG_PROPERTY: "applovin",
DEPENDENCIES_PROPERTY: ["com.google.ads.mediation:applovin:13.4.0.1"],
MAVEN_REPO_PROPERTY: "",
PACKAGE_PROPERTY: "AppLovinAdapterTarget",
PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-applovin.git",
PACKAGE_VERSION_PROPERTY: "13.4.0",
SK_AD_NETWORK_IDS_PROPERTY: ["22mmun2rn5", "238da6jt44", "24t9a8vw3c", "24zw6aqk47", "252b5q8x7y", "275upjj5gd", "294l99pt4k", "2fnua5tdw4", "2u9pt9hc89", "32z4fx6l9h", "3l6bd9hu43", "3qcr597p9d", "3qy4746246", "3rd42ekr43", "3sh42y64q3", "424m5254lk", "4468km3ulz", "44jx6755aq", "44n7hlldy6", "47vhws6wlr", "488r3q3dtq", "4dzt52r2t5", "4fzdc2evr5", "4mn522wn87", "4pfyvq9l8r", "4w7y6s5ca2", "523jb4fst2", "52fl2v3hgk", "54nzkqm89y", "578prtvx9j", "5a6flpkh64", "5l3tpt7t6e", "5lm9lj6jb7", "5tjdwbrq8w", "6964rsfnh4", "6g9af3uyq4", "6p4ks3rnbw", "6v7lgmsu45", "6xzpu9s2p8", "737z793b9f", "74b6s63p6l", "79pbpufp6p", "7fmhfwg9en", "7rz58n8ntl", "7ug5zh24hu", "84993kbrcf", "89z7zv988g", "8c4e2ghe7u", "8m87ys6875", "8r8llnkz5a", "8s468mfl3y", "97r2b46745", "9b89h5y424", "9nlqeag3gk", "9rd848q2bz", "9t245vhmpl", "9vvzujtq5s", "9yg77x724h", "a2p9lx4jpn", "a7xqa6mtl2", "a8cz6cu7e5", "av6w8kgt66", "b9bk5wbcq9", "bxvub5ada5", "c3frkrj4fj", "c6k4g5qg8m", "cg4yq2srnc", "cj5566h2ga", "cp8zw746q7", "cs644xg564", "cstr6suwn9", "dbu4b84rxf", "dkc879ngq3", "dzg6xy7pwj", "e5fvkxwrpn", "ecpz2srf59", "eh6m2bh4zr", "ejvt5qm6ak", "f38h382jlk", "f73kdq92p3", "f7s53z58qe", "feyaarzu9v", "g28c52eehv", "g2y4y55b64", "g6gcrrvk4p", "ggvn48r87g", "glqzh8vgby", "gta8lk7p23", "gta9lk7p23", "hb56zgv37p", "hdw39hrw9y", "hs6bdukanm", "k674qkevps", "kbd757ywx3", "kbmxgpxpgc", "klf5c3l5u5", "krvm3zuq6h", "lr83yxwka7", "ludvb6z3bs", "m297p6643m", "m5mvw97r93", "m8dbw4sv7c", "mj797d8u6f", "mlmmfzh3r3", "mls7yz5dvl", "mp6xlyr22a", "mqn7fxpca7", "mtkv5xtk9e", "n38lu8286q", "n66cz3y3bx", "n6fk4nfna4", "n9x2a789qt", "nzq8sh4pbs", "p78axxw29g", "ppxm28t8ap", "prcb7njmu6", "pwa73g5rt2", "pwdxu55a5a", "qqp299437r", "qu637u8glc", "r45fhb6rf7", "rvh3l7un93", "rx5hdcabgc", "s39g8k73mm", "s69wq72ugq", "su67r6k2v3", "t38b2kh725", "tl55sbb4fm", "u679fj5vs4", "uw77j35x4d", "v4nxqhlyqp", "v72qych5uu", "v79kvwwj4g", "v9wttpbfk9", "vcra2ehyfk", "vhf287vqwu", "vutu7akeur", "w9q455wk68", "wg4vff78zm", "wzmmz9fp6w", "x44k69ngh6", "x5l83yy675", "x8jxxk4ff5", "x8uqf25wch", "xga6mpmplv", "xy9t38ct57", "y45688jllp", "y5ghdn5j9k", "yclnxrl5pm", "ydx93a7ass", "zmvfpc5aq8", "zq492l623r"],
},
Flag.CHARTBOOST: {
FLAG_PROPERTY: Flag.CHARTBOOST,
TAG_PROPERTY: "chartboost",
DEPENDENCIES_PROPERTY: ["com.google.ads.mediation:chartboost:9.10.0.1"],
MAVEN_REPO_PROPERTY: "https://cboost.jfrog.io/artifactory/chartboost-ads/",
PACKAGE_PROPERTY: "ChartboostAdapterTarget",
PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-chartboost.git",
PACKAGE_VERSION_PROPERTY: "9.11.001",
SK_AD_NETWORK_IDS_PROPERTY: ["f38h382jlk"],
},
Flag.DTEXCHANGE: {
FLAG_PROPERTY: Flag.DTEXCHANGE,
TAG_PROPERTY: "dtexchange",
DEPENDENCIES_PROPERTY: ["com.google.ads.mediation:fyber:8.4.0.0"],
MAVEN_REPO_PROPERTY: "",
PACKAGE_PROPERTY: "DTExchangeAdapterTarget",
PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-dtexchange.git",
PACKAGE_VERSION_PROPERTY: "8.4.600",
SK_AD_NETWORK_IDS_PROPERTY: ["ydx93a7ass", "t6d3zquu66", "4fzdc2evr5", "a8cz6cu7e5", "9nlqeag3gk", "a2p9lx4jpn", "k674qkevps", "c3frkrj4fj", "n6fk4nfna4", "mtkv5xtk9e", "p78axxw29g", "n9x2a789qt", "kbmxgpxpgc", "s39g8k73mm", "krvm3zuq6h", "294l99pt4k", "3rd42ekr43", "ggvn48r87g", "4pfyvq9l8r", "cg4yq2srnc", "5a6flpkh64", "v72qych5uu", "x8uqf25wch", "c6k4g5qg8m", "wg4vff78zm", "rx5hdcabgc", "32z4fx6l9h", "3qy4746246", "252b5q8x7y", "f38h382jlk", "24t9a8vw3c", "hs6bdukanm", "9rd848q2bz", "prcb7njmu6", "m8dbw4sv7c", "9g2aggbj52", "wzmmz9fp6w", "yclnxrl5pm", "t38b2kh725", "7ug5zh24hu", "5lm9lj6jb7", "ejvt5qm6ak", "7rz58n8ntl", "kbd757ywx3", "9t245vhmpl", "44jx6755aq", "tl55sbb4fm", "8s468mfl3y", "4468km3ulz", "2u9pt9hc89", "av6w8kgt66", "klf5c3l5u5", "hdw39hrw9y", "y45688jllp", "dzg6xy7pwj", "3sh42y64q3", "ppxm28t8ap", "f73kdq92p3", "5l3tpt7t6e", "uw77j35x4d", "mlmmfzh3r3", "6g9af3uyq4", "su67r6k2v3", "g28c52eehv", "pwa73g5rt2", "zq492l623r", "22mmun2rn5", "8r8llnkz5a", "47vhws6wlr", "5tjdwbrq8w", "h65wbv5k3f", "2fnua5tdw4", "7fmhfwg9en", "tvvz7th9br", "x44k69ngh6", "vhf287vqwu", "4dzt52r2t5", "x8yj322td6", "tskbem2b5g", "cp8zw746q7", "cj5566h2ga", "578prtvx9j", "e5fvkxwrpn", "r45fhb6rf7", "mqn7fxpca7", "g6gcrrvk4p", "xga6mpmplv", "qu637u8glc", "w9q455wk68", "275upjj5gd", "97r2b46745", "cstr6suwn9"],
},
Flag.IMOBILE: {
FLAG_PROPERTY: Flag.IMOBILE,
TAG_PROPERTY: "imobile",
DEPENDENCIES_PROPERTY: ["com.google.ads.mediation:imobile:2.3.2.1"],
MAVEN_REPO_PROPERTY: "https://imobile.github.io/adnw-sdk-android",
PACKAGE_PROPERTY: "IMobileAdapterTarget",
PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-imobile.git",
PACKAGE_VERSION_PROPERTY: "2.3.402",
SK_AD_NETWORK_IDS_PROPERTY: ["v4nxqhlyqp"],
},
Flag.INMOBI: {
FLAG_PROPERTY: Flag.INMOBI,
TAG_PROPERTY: "inmobi",
DEPENDENCIES_PROPERTY: ["com.google.ads.mediation:inmobi:10.8.8.1"],
MAVEN_REPO_PROPERTY: "",
PACKAGE_PROPERTY: "InMobiAdapterTarget",
PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-inmobi.git",
PACKAGE_VERSION_PROPERTY: "10.8.600",
SK_AD_NETWORK_IDS_PROPERTY: ["uw77j35x4d", "7ug5zh24hu", "hs6bdukanm", "4fzdc2evr5", "ggvn48r87g", "5lm9lj6jb7", "9rd848q2bz", "c6k4g5qg8m", "wzmmz9fp6w", "3sh42y64q3", "yclnxrl5pm", "kbd757ywx3", "f73kdq92p3", "ydx93a7ass", "w9q455wk68", "prcb7njmu6", "wg4vff78zm", "mlmmfzh3r3", "tl55sbb4fm", "4pfyvq9l8r", "t38b2kh725", "5l3tpt7t6e", "7rz58n8ntl", "klf5c3l5u5", "cg4yq2srnc", "av6w8kgt66", "9t245vhmpl", "v72qych5uu", "2u9pt9hc89", "44jx6755aq", "8s468mfl3y", "p78axxw29g", "ppxm28t8ap", "424m5254lk", "5a6flpkh64", "pwa73g5rt2", "e5fvkxwrpn", "9nlqeag3gk", "k674qkevps", "cj5566h2ga", "mtkv5xtk9e", "3rd42ekr43", "g28c52eehv", "2fnua5tdw4", "4468km3ulz", "97r2b46745", "glqzh8vgby", "3qcr597p9d", "578prtvx9j", "n6fk4nfna4", "b9bk5wbcq9", "kbmxgpxpgc", "294l99pt4k", "523jb4fst2", "mls7yz5dvl", "74b6s63p6l", "22mmun2rn5", "52fl2v3hgk", "6g9af3uyq4", "x5l83yy675", "mqn7fxpca7", "g6gcrrvk4p", "r45fhb6rf7", "c3frkrj4fj", "ejvt5qm6ak", "f38h382jlk", "488r3q3dtq", "55644vm79v", "5tjdwbrq8w", "nzq8sh4pbs", "m8dbw4sv7c", "7fmhfwg9en", "6v7lgmsu45", "s39g8k73mm", "6yxyv74ff7", "55y65gfgn7", "mqn7fxpca7", "qqp299437r", "32z4fx6l9h", "feyaarzu9v", "cp8zw746q7", "252b5q8x7y", "qu637u8glc", "vhf287vqwu", "a8cz6cu7e5", "cwn433xbcr", "fq6vru337s", "87u5trcl3r", "mj797d8u6f", "dbu4b84rxf", "238da6jt44", "4dzt52r2t5", "a2p9lx4jpn", "t6d3zquu66", "zq492l623r", "cstr6suwn9", "xga6mpmplv", "tmhh9296z4", "6xzpu9s2p8", "ludvb6z3bs", "v9wttpbfk9", "n38lu8286q", "fz2k2k5tej", "g2y4y55b64", "su67r6k2v3", "v79kvwwj4g", "275upjj5gd", "24zw6aqk47", "44n7hlldy6", "ecpz2srf59", "bvpn9ufa9b", "gta9lk7p23"],
},
Flag.IRONSOURCE: {
FLAG_PROPERTY: Flag.IRONSOURCE,
TAG_PROPERTY: "ironsource",
DEPENDENCIES_PROPERTY: ["com.google.ads.mediation:ironsource:9.0.0.1"],
MAVEN_REPO_PROPERTY: "",
PACKAGE_PROPERTY: "IronSourceAdapterTarget",
PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-ironsource.git",
PACKAGE_VERSION_PROPERTY: "8.10.0",
SK_AD_NETWORK_IDS_PROPERTY: ["su67r6k2v3"],
},
Flag.LIFTOFF: {
FLAG_PROPERTY: Flag.LIFTOFF,
TAG_PROPERTY: "liftoff",
DEPENDENCIES_PROPERTY: ["com.google.ads.mediation:vungle:7.6.0.0"],
MAVEN_REPO_PROPERTY: "",
PACKAGE_PROPERTY: "LiftoffMonetizeAdapterTarget",
PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-liftoffmonetize.git",
PACKAGE_VERSION_PROPERTY: "7.6.0",
SK_AD_NETWORK_IDS_PROPERTY: ["22mmun2rn5", "238da6jt44", "24t9a8vw3c", "24zw6aqk47", "275upjj5gd", "294l99pt4k", "2fnua5tdw4", "2u9pt9hc89", "32z4fx6l9h", "3l6bd9hu43", "3qcr597p9d", "3qy4746246", "3rd42ekr43", "3sh42y64q3", "424m5254lk", "4468km3ulz", "44jx6755aq", "44n7hlldy6", "488r3q3dtq", "4dzt52r2t5", "4fzdc2evr5", "4pfyvq9l8r", "4w7y6s5ca2", "523jb4fst2", "52fl2v3hgk", "54nzkqm89y", "578prtvx9j", "5a6flpkh64", "5l3tpt7t6e", "5lm9lj6jb7", "5tjdwbrq8w", "6964rsfnh4", "6g9af3uyq4", "6lz2ygh3q6", "6p4ks3rnbw", "6rd35atwn8", "6v7lgmsu45", "6xzpu9s2p8", "6yxyv74ff7", "737z793b9f", "74b6s63p6l", "79pbpufp6p", "7fmhfwg9en", "7rz58n8ntl", "7ug5zh24hu", "84993kbrcf", "87u5trcl3r", "89z7zv988g", "8m87ys6875", "8s468mfl3y", "97r2b46745", "9b89h5y424", "9nlqeag3gk", "9rd848q2bz", "9t245vhmpl", "9vvzujtq5s", "a2p9lx4jpn", "a7xqa6mtl2", "a8cz6cu7e5", "apzhy3va96", "av6w8kgt66", "b9bk5wbcq9", "bvpn9ufa9b", "bxvub5ada5", "c3frkrj4fj", "c6k4g5qg8m", "cg4yq2srnc", "cj5566h2ga", "cp8zw746q7", "cs644xg564", "cstr6suwn9", "cwn433xbcr", "dbu4b84rxf", "dkc879ngq3", "dzg6xy7pwj", "e5fvkxwrpn", "ecpz2srf59", "ejvt5qm6ak", "f2zub97jtl", "f38h382jlk", "f73kdq92p3", "f7s53z58qe", "feyaarzu9v", "fq6vru337s", "fz2k2k5tej", "g28c52eehv", "g2y4y55b64", "g6gcrrvk4p", "ggvn48r87g", "glqzh8vgby", "gta9lk7p23", "hb56zgv37p", "hdw39hrw9y", "hs6bdukanm", "k674qkevps", "k6y4y55b64", "kbd757ywx3", "kbmxgpxpgc", "klf5c3l5u5", "krvm3zuq6h", "ln5gz23vtd", "lr83yxwka7", "ludvb6z3bs", "m297p6643m", "m2jqnlggk3", "m5mvw97r93", "m8dbw4sv7c", "mj797d8u6f", "mlmmfzh3r3", "mls7yz5dvl", "mp6xlyr22a", "mqn7fxpca7", "mtkv5xtk9e", "n38lu8286q", "n6fk4nfna4", "n9x2a789qt", "ns5j362hk7", "p78axxw29g", "pg7ctvrt6f", "ppxm28t8ap", "prcb7njmu6", "pwa73g5rt2", "pwdxu55a5a", "qqp299437r", "qu637u8glc", "qwpu75vrh2", "r45fhb6rf7", "raa6f494kr", "rvh3l7un93", "rx5hdcabgc", "s39g8k73mm", "sczv5946wb", "su67r6k2v3", "t38b2kh725", "t6d3zquu66", "thzdn4h5nc", "tl55sbb4fm", "tmhh9296z4", "u679fj5vs4", "uw77j35x4d", "v72qych5uu", "v79kvwwj4g", "v9wttpbfk9", "vcra2ehyfk", "vhf287vqwu", "w9q455wk68", "wg4vff78zm", "wzmmz9fp6w", "x44k69ngh6", "x5l83yy675", "x8uqf25wch", "xga6mpmplv", "xy9t38ct57", "y45688jllp", "y5ghdn5j9k", "yclnxrl5pm", "ydx93a7ass", "z959bm4gru", "zmvfpc5aq8", "zq492l623r"],
},
Flag.LINE: {
FLAG_PROPERTY: Flag.LINE,
TAG_PROPERTY: "line",
DEPENDENCIES_PROPERTY: ["com.google.ads.mediation:line:2.9.20250924.1"],
MAVEN_REPO_PROPERTY: "",
PACKAGE_PROPERTY: "LineAdapterTarget",
PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-line.git",
PACKAGE_VERSION_PROPERTY: "2.9.2025111900",
SK_AD_NETWORK_IDS_PROPERTY: ["vutu7akeur", "eh6m2bh4zr", "cstr6suwn9", "578prtvx9j", "9t245vhmpl", "v72qych5uu", "x8uqf25wch", "7ug5zh24hu", "hs6bdukanm", "dbu4b84rxf", "8c4e2ghe7u"],
},
Flag.MAIO: {
FLAG_PROPERTY: Flag.MAIO,
TAG_PROPERTY: "maio",
DEPENDENCIES_PROPERTY: ["com.google.ads.mediation:maio:2.0.6.0"],
MAVEN_REPO_PROPERTY: "https://imobile-maio.github.io/maven",
PACKAGE_PROPERTY: "MaioAdapterTarget",
PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-maio.git",
PACKAGE_VERSION_PROPERTY: "2.2.0",
SK_AD_NETWORK_IDS_PROPERTY: ["v4nxqhlyqp"],
},
Flag.META: {
FLAG_PROPERTY: Flag.META,
TAG_PROPERTY: "meta",
DEPENDENCIES_PROPERTY: ["com.google.ads.mediation:facebook:6.20.0.2"],
MAVEN_REPO_PROPERTY: "",
PACKAGE_PROPERTY: "MetaAdapterTarget",
PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-meta.git",
PACKAGE_VERSION_PROPERTY: "6.20.100",
SK_AD_NETWORK_IDS_PROPERTY: ["v9wttpbfk9", "n38lu8286q"],
},
Flag.MINTEGRAL: {
FLAG_PROPERTY: Flag.MINTEGRAL,
TAG_PROPERTY: "mintegral",
DEPENDENCIES_PROPERTY: ["com.google.ads.mediation:mintegral:16.9.91.2"],
MAVEN_REPO_PROPERTY: "https://dl-maven-android.mintegral.com/repository/mbridge_android_sdk_oversea",
PACKAGE_PROPERTY: "MintegralAdapterTarget",
PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-mintegral.git",
PACKAGE_VERSION_PROPERTY: "7.7.901",
SK_AD_NETWORK_IDS_PROPERTY: ["kbd757ywx3"],
},
Flag.MOLOCO: {
FLAG_PROPERTY: Flag.MOLOCO,
TAG_PROPERTY: "moloco",
DEPENDENCIES_PROPERTY: ["com.google.ads.mediation:moloco:4.2.0.0"],
MAVEN_REPO_PROPERTY: "",
PACKAGE_PROPERTY: "MolocoAdapterTarget",
PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-moloco.git",
PACKAGE_VERSION_PROPERTY: "4.1.0",
SK_AD_NETWORK_IDS_PROPERTY: ["9t245vhmpl"],
},
Flag.MYTARGET: {
FLAG_PROPERTY: Flag.MYTARGET,
TAG_PROPERTY: "mytarget",
DEPENDENCIES_PROPERTY: ["com.google.ads.mediation:mytarget:5.27.3.0"],
MAVEN_REPO_PROPERTY: "",
PACKAGE_PROPERTY: "MyTargetAdapterTarget",
PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-mytarget.git",
PACKAGE_VERSION_PROPERTY: "5.36.0",
SK_AD_NETWORK_IDS_PROPERTY: ["n9x2a789qt", "r26jy69rpl"],
},
Flag.PANGLE: {
FLAG_PROPERTY: Flag.PANGLE,
TAG_PROPERTY: "pangle",
DEPENDENCIES_PROPERTY: ["com.google.ads.mediation:pangle:7.6.0.5.0"],
MAVEN_REPO_PROPERTY: "https://artifact.bytedance.com/repository/pangle/",
PACKAGE_PROPERTY: "PangleAdapterTarget",
PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-pangle.git",
PACKAGE_VERSION_PROPERTY: "7.6.600",
SK_AD_NETWORK_IDS_PROPERTY: ["22mmun2rn5"],
},
Flag.UNITY: {
FLAG_PROPERTY: Flag.UNITY,
TAG_PROPERTY: "unity",
DEPENDENCIES_PROPERTY: ["com.unity3d.ads:unity-ads:4.16.2", "com.google.ads.mediation:unity:4.16.3.0"],
MAVEN_REPO_PROPERTY: "",
PACKAGE_PROPERTY: "UnityAdapterTarget",
PACKAGE_URL_PROPERTY: "https://github.com/googleads/googleads-mobile-ios-mediation-unity.git",
PACKAGE_VERSION_PROPERTY: "4.16.4",
SK_AD_NETWORK_IDS_PROPERTY: ["9nlqeag3gk", "hs6bdukanm", "9rd848q2bz", "22mmun2rn5", "f73kdq92p3", "tl55sbb4fm", "prcb7njmu6", "n9x2a789qt", "97r2b46745", "zmvfpc5aq8", "av6w8kgt66", "uw77j35x4d", "pwa73g5rt2", "e5fvkxwrpn", "klf5c3l5u5", "xga6mpmplv", "wg4vff78zm", "lr83yxwka7", "5tjdwbrq8w", "7ug5zh24hu", "glqzh8vgby", "feyaarzu9v", "6yxyv74ff7", "mqn7fxpca7", "vhf287vqwu", "g6gcrrvk4p", "v79kvwwj4g", "32z4fx6l9h", "2fnua5tdw4", "a8cz6cu7e5", "v72qych5uu", "a2p9lx4jpn", "5f5u5tfb26", "4fzdc2evr5", "5lm9lj6jb7", "4w7y6s5ca2", "3sh42y64q3", "4468km3ulz", "mj797d8u6f", "488r3q3dtq", "mlmmfzh3r3", "9t245vhmpl", "x44k69ngh6", "kbd757ywx3", "4dzt52r2t5", "k674qkevps", "294l99pt4k", "m8dbw4sv7c", "2u9pt9hc89", "p78axxw29g", "424m5254lk", "238da6jt44", "ydx93a7ass", "s39g8k73mm", "v9wttpbfk9", "44jx6755aq", "578prtvx9j", "yclnxrl5pm", "k6y4y55b64", "5a6flpkh64", "3rd42ekr43", "f38h382jlk", "cstr6suwn9", "ppxm28t8ap", "f7s53z58qe", "zq492l623r", "t38b2kh725", "mp6xlyr22a", "3qy4746246", "5l3tpt7t6e", "c6k4g5qg8m", "w9q455wk68", "4pfyvq9l8r", "wzmmz9fp6w", "8s468mfl3y"],
},
}
const MEDIATION_NETWORK_TAGS: Dictionary = {
"applovin": Flag.APPLOVIN,
"chartboost": Flag.CHARTBOOST,
"dtexchange": Flag.DTEXCHANGE,
"imobile": Flag.IMOBILE,
"inmobi": Flag.INMOBI,
"ironsource": Flag.IRONSOURCE,
"liftoff": Flag.LIFTOFF,
"line": Flag.LINE,
"maio": Flag.MAIO,
"meta": Flag.META,
"mintegral": Flag.MINTEGRAL,
"moloco": Flag.MOLOCO,
"mytarget": Flag.MYTARGET,
"pangle": Flag.PANGLE,
"unity": Flag.UNITY,
}
var flag: Flag
var tag: String
var android_dependencies: Array
var android_custom_maven_repo: String
var swift_package: String
var swift_package_url: String
var swift_package_version: String
var sk_ad_network_ids: PackedStringArray
func _init(a_data: Dictionary) -> void:
flag = a_data[FLAG_PROPERTY]
tag = a_data[TAG_PROPERTY]
android_dependencies = a_data[DEPENDENCIES_PROPERTY]
android_custom_maven_repo = a_data[MAVEN_REPO_PROPERTY]
swift_package = a_data[PACKAGE_PROPERTY]
swift_package_url = a_data[PACKAGE_URL_PROPERTY]
swift_package_version = a_data[PACKAGE_VERSION_PROPERTY]
sk_ad_network_ids = a_data[SK_AD_NETWORK_IDS_PROPERTY]
func get_spm_dependency() -> SpmDependency:
return SpmDependency.new({
SpmDependency.URL_PROPERTY: swift_package_url,
SpmDependency.VERSION_PROPERTY: swift_package_version,
SpmDependency.PRODUCTS_PROPERTY: [swift_package],
})
static func is_flag_enabled(a_value: int, a_flag: Flag) -> bool:
return a_value & a_flag
static func is_valid_tag(a_tag: String) -> bool:
return MEDIATION_NETWORK_TAGS.has(a_tag)
static func get_by_flag(a_flag: Flag) -> MediationNetwork:
return MediationNetwork.new(MEDIATION_NETWORKS[a_flag])
static func get_by_tag(a_tag: String) -> MediationNetwork:
return get_by_flag(MEDIATION_NETWORK_TAGS[a_tag])
static func get_all_enabled(a_value: int) -> Array[MediationNetwork]:
var __enabled_networks: Array[MediationNetwork] = []
for __flag in Flag.values():
if is_flag_enabled(a_value, __flag):
__enabled_networks.append(get_by_flag(__flag))
return __enabled_networks
static func get_all_enabled_tags(a_value: int) -> Array[String]:
var __enabled_network_tags: Array[String] = []
for __flag in Flag.values():
if is_flag_enabled(a_value, __flag):
var __network: MediationNetwork = get_by_flag(__flag)
__enabled_network_tags.append(__network.tag)
return __enabled_network_tags
static func generate_sk_ad_network_plist(a_networks: Array[MediationNetwork]) -> String:
var __sk_ad_ids_plist_content: String
var __unique_sk_network_ad_ids: Dictionary = { }
# Add Google-required SK Ad Network IDs
for __network_id in GOOGLE_SK_AD_NETWORK_IDS:
if not __unique_sk_network_ad_ids.has(__network_id):
__unique_sk_network_ad_ids.set(__network_id, null)
# Add SK Ad Network IDs required by other networks
for __network in a_networks:
for __network_id in __network.sk_ad_network_ids:
if not __unique_sk_network_ad_ids.has(__network_id):
__unique_sk_network_ad_ids.set(__network_id, null)
for __network_id in __unique_sk_network_ad_ids.keys():
__sk_ad_ids_plist_content += SK_AD_NETWORK_ITEM_FORMAT % __network_id
return SK_AD_NETWORK_ITEM_LIST_FORMAT % __sk_ad_ids_plist_content
static func get_spm_dependencies(a_networks: Array[MediationNetwork]) -> Array[SpmDependency]:
var __spm_dependencies: Array[SpmDependency] = []
for __network in a_networks:
__spm_dependencies.append(__network.get_spm_dependency())
return __spm_dependencies
static func generate_tag_list(a_networks: Array[MediationNetwork]) -> String:
var __enabled_network_tags: PackedStringArray = []
for __network in a_networks:
__enabled_network_tags.append(__network.tag)
return ",".join(__enabled_network_tags)
@@ -0,0 +1 @@
uid://devoemkicbhtc
+32
View File
@@ -0,0 +1,32 @@
#
# © 2024-present https://github.com/cengiz-pz
#
class_name NetworkExtras
extends Resource
const NETWORK_TAG_PROPERTY := &"network_tag"
const EXTRAS_PROPERTY := &"extras"
## Identifies the target ad network for the extras.
@export var network_flag: MediationNetwork.Flag = MediationNetwork.Flag.APPLOVIN
## Dictionary holding one or more extras. Only keys of type String are allowed. Values can be String, int, or bool.
@export var extras: Dictionary
func get_raw_data() -> Dictionary:
var __network: MediationNetwork = MediationNetwork.get_by_flag(network_flag)
var __raw_data: Dictionary
__raw_data = {NETWORK_TAG_PROPERTY: __network.tag, EXTRAS_PROPERTY: extras}
return __raw_data
static func build_raw_data_array(a_network_extras: Array[NetworkExtras]) -> Array:
var __raw_data_array: Array[Dictionary] = []
for __network_extras in a_network_extras:
__raw_data_array.append(__network_extras.get_raw_data())
return __raw_data_array
@@ -0,0 +1 @@
uid://cxw2idjnk2d72
@@ -0,0 +1,45 @@
#
# © 2024-present https://github.com/cengiz-pz
#
class_name NetworkPrivacySettings
extends RefCounted
const HAS_GDPR_CONSENT_PROPERTY := &"has_gdpr_consent"
const IS_AGE_RESTRICTED_USER_PROPERTY := &"is_age_restricted_user"
const HAS_CCPA_SALE_CONSENT_PROPERTY := &"has_ccpa_sale_consent"
const ENABLED_NETWORKS_PROPERTY := &"enabled_networks"
var _data: Dictionary
func _init() -> void:
_data = {
ENABLED_NETWORKS_PROPERTY: [],
}
func set_has_gdpr_consent(a_value: bool) -> NetworkPrivacySettings:
_data[HAS_GDPR_CONSENT_PROPERTY] = a_value
return self
func set_is_age_restricted_user(a_value: bool) -> NetworkPrivacySettings:
_data[IS_AGE_RESTRICTED_USER_PROPERTY] = a_value
return self
func set_has_ccpa_sale_consent(a_value: bool) -> NetworkPrivacySettings:
_data[HAS_CCPA_SALE_CONSENT_PROPERTY] = a_value
return self
func set_enabled_networks(a_value: Array[String]) -> NetworkPrivacySettings:
if a_value == null:
_data[ENABLED_NETWORKS_PROPERTY] = []
else:
_data[ENABLED_NETWORKS_PROPERTY] = a_value
return self
func get_raw_data() -> Dictionary:
return _data
@@ -0,0 +1 @@
uid://b42fjtxon0ynl
+46
View File
@@ -0,0 +1,46 @@
#
# © 2024-present https://github.com/cengiz-pz
#
class_name ResponseInfo
extends RefCounted
const ADAPTER_RESPONSES_PROPERTY := &"adapter_responses"
const LOADED_ADAPTER_RESPONSE_PROPERTY := &"loaded_adapter_response"
const ADAPTER_CLASS_NAME_PROPERTY := &"adapter_class_name"
const NETWORK_TAG_PROPERTY := &"network_tag"
const RESPONSE_ID_PROPERTY := &"response_id"
var _data: Dictionary
func _init(a_data: Dictionary):
_data = a_data
func get_adapter_responses() -> Array[AdapterResponseInfo]:
var __responses: Array[AdapterResponseInfo] = []
for __response_dict in _data[ADAPTER_RESPONSES_PROPERTY]:
__responses.append(AdapterResponseInfo.new(__response_dict))
return __responses
func get_loaded_adapter_response() -> AdapterResponseInfo:
return (
AdapterResponseInfo.new(_data[LOADED_ADAPTER_RESPONSE_PROPERTY])
if _data.has(LOADED_ADAPTER_RESPONSE_PROPERTY)
else null
)
func get_adapter_class_name() -> String:
return _data[ADAPTER_CLASS_NAME_PROPERTY] if _data.has(ADAPTER_CLASS_NAME_PROPERTY) else ""
func get_network_tag() -> String:
return _data[NETWORK_TAG_PROPERTY] if _data.has(NETWORK_TAG_PROPERTY) else ""
func get_response_id() -> String:
return _data[RESPONSE_ID_PROPERTY] if _data.has(RESPONSE_ID_PROPERTY) else ""
@@ -0,0 +1 @@
uid://b3jib8n1rbnyv
+22
View File
@@ -0,0 +1,22 @@
#
# © 2024-present https://github.com/cengiz-pz
#
class_name RewardItem
extends RefCounted
const AMOUNT_PROPERTY := &"amount"
const TYPE_PROPERTY := &"type"
var _data: Dictionary
func _init(a_data: Dictionary):
_data = a_data
func get_amount() -> int:
return _data[AMOUNT_PROPERTY]
func get_type() -> String:
return _data[TYPE_PROPERTY]
@@ -0,0 +1 @@
uid://cf0fkl05bgokb
+33
View File
@@ -0,0 +1,33 @@
#
# © 2024-present https://github.com/cengiz-pz
#
class_name UserConsent
extends RefCounted
enum Status {
UNKNOWN,
NOT_REQUIRED,
REQUIRED,
OBTAINED,
}
var status: Status
func _init(a_status_string: String = "") -> void:
if a_status_string == "":
status = Status.UNKNOWN
else:
status = string_to_status(a_status_string)
func to_status_string() -> String:
return Status.keys()[status]
static func status_to_string(a_status: Status) -> String:
return Status.keys()[a_status]
static func string_to_status(a_string: String) -> Status:
return Status.get(a_string)
@@ -0,0 +1 @@
uid://dcbs2sidgjetb
+10
View File
@@ -0,0 +1,10 @@
;
; © 2024-present https://github.com/cengiz-pz
;
[plugin]
name="Admob"
description="Allows displaying of AdMob ads."
author="Cengiz"
version="7.0"
script="AdmobPlugin.gd"
+29
View File
@@ -0,0 +1,29 @@
#
# © 2026-present https://github.com/<<GitHubUsername>>
#
class_name GmpLogger extends Object
static func log_error(a_description: String) -> void:
push_error("%s" % [a_description])
static func log_warn(a_description: String) -> void:
push_warning("%s" % [a_description])
static func log_info(a_description: String) -> void:
print_rich("[color=lime]INFO: %s[/color]" % [a_description])
static func error(a_plugin_name: String, a_description: String) -> void:
log_error("%s: %s" % [a_plugin_name, a_description])
static func warn(a_plugin_name: String, a_description: String) -> void:
log_warn("%s: %s" % [a_plugin_name, a_description])
static func info(a_plugin_name: String, a_description: String) -> void:
log_info("%s: %s" % [a_plugin_name, a_description])
+1
View File
@@ -0,0 +1 @@
uid://78axiycs2dmy
+48
View File
@@ -0,0 +1,48 @@
#
# © 2026-present https://github.com/<<GitHubUsername>>
#
class_name SpmDependency extends RefCounted
const URL_PROPERTY := &"url"
const VERSION_PROPERTY := &"version"
const PRODUCTS_PROPERTY := &"products"
var _data: Dictionary
func _init(a_data: Dictionary = {}):
_data = a_data
func get_url() -> String:
return _data[URL_PROPERTY]
func set_url(a_url: String) -> void:
_data[URL_PROPERTY] = a_url
func get_version() -> String:
return _data[VERSION_PROPERTY]
func set_version(a_version: String) -> void:
_data[VERSION_PROPERTY] = a_version
func get_products() -> Array:
return _data[PRODUCTS_PROPERTY]
func set_products(a_products: Array) -> void:
_data[PRODUCTS_PROPERTY] = a_products
func format_to_string() -> String:
var __result: String = ""
for __product in get_products():
__result += "%s@%s (%s) " % [__product, get_version(), get_url()]
return __result
+1
View File
@@ -0,0 +1 @@
uid://dxpvam6fnixki
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Blake Bukowsky
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+43
View File
@@ -0,0 +1,43 @@
# Agent Tools
Godot editor plugin that exposes **43 tools across 12 namespaces** (scene edits, signal wiring, resource creation, reference validation, animation, headless test runs, etc.) over MCP so coding agents can operate through the editor's real APIs instead of hand-editing `.tscn` / `.tres` / `project.godot` as text.
Works with any MCP-capable client — Claude Code, Claude Desktop, Cursor, Cline, Windsurf, Continue, Zed.
## Quick start
1. Enable this plugin: *Project → Project Settings → Plugins* → tick **Agent Tools**.
2. Confirm the Output panel shows `[agent_tools] listening on 127.0.0.1:9920`.
3. Configure your MCP client to run the bridge — see the [full setup guide](https://github.com/BlakeBukowsky/GodotTools#configure-your-agent) for per-client config paths.
Simplest user-scoped config (for Claude Code, Cursor, Windsurf, etc. — different config file per client):
```json
{
"mcpServers": {
"godot-agent-tools": {
"command": "npx",
"args": ["-y", "godot-agent-tools-mcp"]
}
}
}
```
Full documentation, troubleshooting, and tool catalog: **https://github.com/BlakeBukowsky/GodotTools**
## Raw TCP protocol (if you want to skip MCP)
Line-delimited JSON-RPC on `127.0.0.1:9920`:
```json
{"id": 1, "method": "scene.current", "params": {}}
```
Response:
```json
{"id": 1, "result": {"open": false}}
```
## License
MIT — see [LICENSE](LICENSE).
+187
View File
@@ -0,0 +1,187 @@
# Auto-run driver for run.scene_headless (input simulation, multi-screenshot,
# state dump, deterministic seed). Reads a per-call config from
# res://.godot/agent_tools/headless/config.json, instances the target scene,
# injects scripted input per frame, captures screenshots at specified frames,
# optionally writes a final-state JSON dump, then quits.
@tool
extends Node
const CONFIG_PATH := "res://.godot/agent_tools/headless/config.json"
const Coerce := preload("res://addons/agent_tools/tools/_coerce.gd")
var _input_script: Array = []
var _screenshots: Array = [] # [{frame, path}, ...]
var _state_dump: bool = false
var _state_dump_path: String = ""
var _quit_frame: int = 120
var _frame: int = 0
var _target_inst: Node = null
func _ready() -> void:
var f := FileAccess.open(CONFIG_PATH, FileAccess.READ)
if f == null:
push_error("[headless_driver] config not found at %s" % CONFIG_PATH)
get_tree().quit(2)
return
var parsed = JSON.parse_string(f.get_as_text())
f.close()
if typeof(parsed) != TYPE_DICTIONARY:
push_error("[headless_driver] malformed config")
get_tree().quit(2)
return
# Seed the global RNG before instancing so the target scene's randomness
# (randi/randf in _ready, etc.) is deterministic per-seed.
if parsed.has("seed"):
seed(int(parsed.seed))
_input_script = parsed.get("input_script", [])
_screenshots = parsed.get("screenshots", [])
_state_dump = bool(parsed.get("state_dump", false))
_state_dump_path = str(parsed.get("state_dump_path", ""))
_quit_frame = int(parsed.get("quit_frame", 120))
var target_scene_path: String = parsed.get("target_scene", "")
if target_scene_path == "":
push_error("[headless_driver] config missing 'target_scene'")
get_tree().quit(2)
return
var packed = load(target_scene_path)
if packed == null:
push_error("[headless_driver] failed to load target %s" % target_scene_path)
get_tree().quit(2)
return
_target_inst = packed.instantiate()
add_child(_target_inst)
print("[headless_driver] driving %s for %d frames, %d events, %d screenshots%s" %
[target_scene_path, _quit_frame, _input_script.size(), _screenshots.size(),
", state_dump=on" if _state_dump else ""])
func _process(_delta: float) -> void:
# 1. Inject input events scheduled for this frame.
for spec in _input_script:
if int(spec.get("frame", -1)) == _frame:
_inject(spec)
# 2. Capture screenshots scheduled for this frame.
for shot in _screenshots:
if int(shot.get("frame", -1)) == _frame:
_capture(str(shot.get("path", "")))
if _frame >= _quit_frame:
if _state_dump and _state_dump_path != "":
_write_state_dump()
print("[headless_driver] done at frame %d" % _frame)
get_tree().quit(0)
return
_frame += 1
func _inject(spec: Dictionary) -> void:
match spec.get("type", ""):
"action_press":
Input.action_press(spec.get("action", ""), float(spec.get("strength", 1.0)))
"action_release":
Input.action_release(spec.get("action", ""))
"action_tap":
Input.action_press(spec.get("action", ""), float(spec.get("strength", 1.0)))
Input.action_release(spec.get("action", ""))
"key":
var ek := InputEventKey.new()
var kc = spec.get("keycode", 0)
if kc is String:
ek.physical_keycode = OS.find_keycode_from_string(kc)
else:
ek.physical_keycode = int(kc)
ek.pressed = bool(spec.get("pressed", true))
Input.parse_input_event(ek)
"mouse_click":
var pos = spec.get("position", [0, 0])
var v := Vector2(pos[0], pos[1])
var button := int(spec.get("button", 1))
var down := InputEventMouseButton.new()
down.button_index = button
down.pressed = true
down.position = v
down.global_position = v
Input.parse_input_event(down)
var up := InputEventMouseButton.new()
up.button_index = button
up.pressed = false
up.position = v
up.global_position = v
Input.parse_input_event(up)
"mouse_motion":
var pos = spec.get("position", [0, 0])
var em := InputEventMouseMotion.new()
em.position = Vector2(pos[0], pos[1])
em.global_position = em.position
Input.parse_input_event(em)
_:
push_warning("[headless_driver] unknown event type: %s" % spec.get("type", "<missing>"))
func _capture(path: String) -> void:
if path == "":
return
var vp := get_tree().root.get_viewport()
if vp == null:
push_warning("[headless_driver] no root viewport")
return
var tex := vp.get_texture()
if tex == null:
push_warning("[headless_driver] viewport has no texture (running under dummy renderer?)")
return
var img := tex.get_image()
if img == null or img.is_empty():
push_warning("[headless_driver] failed to capture image at frame %d" % _frame)
return
var dir := path.get_base_dir()
if not DirAccess.dir_exists_absolute(dir):
DirAccess.make_dir_recursive_absolute(dir)
var err := img.save_png(path)
if err != OK:
push_warning("[headless_driver] save_png failed: %d" % err)
else:
print("[headless_driver] frame %d -> %s" % [_frame, path])
func _write_state_dump() -> void:
if _target_inst == null:
return
var snapshot := _node_snapshot(_target_inst, _target_inst)
var f := FileAccess.open(_state_dump_path, FileAccess.WRITE)
if f == null:
push_warning("[headless_driver] failed to open state dump path: %s" % _state_dump_path)
return
f.store_string(JSON.stringify(snapshot, " "))
f.close()
print("[headless_driver] state dump -> %s" % _state_dump_path)
# Mirrors scene.inspect's shape, plus a small selection of commonly-useful
# properties. Kept narrow on purpose — full get_property_list() dumps balloon.
static func _node_snapshot(node: Node, scene_root: Node) -> Dictionary:
var d := {
"name": String(node.name),
"class": node.get_class(),
"node_path": "." if node == scene_root else String(scene_root.get_path_to(node)),
"script": "",
"properties": {},
"children": [],
}
var scr := node.get_script() as Script
if scr:
d.script = scr.resource_path
# Common props worth verifying, if present on this node type.
for prop_name in ["visible", "position", "global_position", "size", "text",
"value", "modulate", "color", "rotation", "scale", "disabled", "pressed"]:
if prop_name in node:
d.properties[prop_name] = Coerce.to_json(node.get(prop_name))
for child in node.get_children():
if child.owner == scene_root or child == scene_root:
d.children.append(_node_snapshot(child, scene_root))
return d
@@ -0,0 +1 @@
uid://iko41fhoo3mm
+6
View File
@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://addons/agent_tools/headless/driver.gd" id="1"]
[node name="HeadlessDriver" type="Node"]
script = ExtResource("1")
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d2wklut1fj5ed"
path="res://.godot/imported/icon.png-381bc5920e04d060db359f1234d8c8b0.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/agent_tools/icon.png"
dest_files=["res://.godot/imported/icon.png-381bc5920e04d060db359f1234d8c8b0.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
+9
View File
@@ -0,0 +1,9 @@
[plugin]
name="Agent Tools"
description="70 tools + 6 MCP Resources for coding agents to edit scenes, wire signals, create resources, and more through Godot's real APIs instead of hand-editing .tscn/.tres files. If you haven't already, configure your MCP client (Claude Code, Cursor, Cline, etc.) per the README: https://github.com/BlakeBukowsky/GodotTools"
author="Blake Bukowsky"
version="0.3.1"
license="MIT"
script="plugin.gd"
icon="icon.png"
+132
View File
@@ -0,0 +1,132 @@
@tool
extends EditorPlugin
const Server := preload("res://addons/agent_tools/server.gd")
const Registry := preload("res://addons/agent_tools/registry.gd")
const DEFAULT_PORT := 9920
const PORT_RANGE_END := 9929 # scan 9920..9929 inclusive
const PORT_SETTING := "agent_tools/port"
const BRIDGE_AUTOLOAD_NAME := "_MCPGameBridge"
const BRIDGE_AUTOLOAD_PATH := "res://addons/agent_tools/runtime/game_bridge.gd"
const DEFAULT_INTERFACE := "127.0.0.1"
const INTERFACE_SETTING := "agent_tools/interface"
var _server: Server
var _registry: Registry
var _bound_port: int = -1
var _session_file_path: String = ""
func _enter_tree() -> void:
_registry = Registry.new()
_server = Server.new(_registry)
add_child(_server)
# Interface resolution
# Defaults to 127.0.0.1, override with project.godot agent_tools/interface
var forced_interface := DEFAULT_INTERFACE
if ProjectSettings.has_setting(INTERFACE_SETTING):
forced_interface = ProjectSettings.get_setting(INTERFACE_SETTING)
# Port resolution:
# 1. If 'agent_tools/port' is set in project.godot, honor it strictly (fail if busy).
# 2. Otherwise try DEFAULT_PORT..PORT_RANGE_END and use the first free one.
# This lets multiple Godot editors coexist: first-to-start takes 9920, second takes 9921, etc.
var forced_port := -1
if ProjectSettings.has_setting(PORT_SETTING):
forced_port = int(ProjectSettings.get_setting(PORT_SETTING))
if forced_port != -1:
if _server.start(forced_port, forced_interface):
_bound_port = forced_port
else:
push_error("[agent_tools] configured port %s:%d is busy. Either close the other process or change 'agent_tools/port' in project.godot." % [forced_interface, forced_port])
else:
for p in range(DEFAULT_PORT, PORT_RANGE_END + 1):
if _server.start(p, forced_interface):
_bound_port = p
break
if _bound_port == -1:
push_error("[agent_tools] all ports %d-%d are busy on %s. Close other Godot editors running agent_tools, or set 'agent_tools/port' in project.godot." % [DEFAULT_PORT, PORT_RANGE_END, forced_interface])
if _bound_port != -1:
print("[agent_tools] listening on %s:%d" % [forced_interface, _bound_port])
print("[agent_tools] If you haven't already, configure your MCP client (Claude Code, Cursor, Cline, etc.) to use these tools.")
print("[agent_tools] Setup guide: https://github.com/BlakeBukowsky/GodotTools#configure-your-agent")
_write_session_registry()
# Register the runtime bridge autoload so editor.game_screenshot / logs.read
# can reach the running game. add_autoload_singleton() writes an entry to
# project.godot; remove_autoload_singleton() in _exit_tree cleans it up.
if not ProjectSettings.has_setting("autoload/" + BRIDGE_AUTOLOAD_NAME):
add_autoload_singleton(BRIDGE_AUTOLOAD_NAME, BRIDGE_AUTOLOAD_PATH)
func _exit_tree() -> void:
_remove_session_registry()
if _server:
_server.stop()
_server.queue_free()
_server = null
_registry = null
_bound_port = -1
if ProjectSettings.has_setting("autoload/" + BRIDGE_AUTOLOAD_NAME):
remove_autoload_singleton(BRIDGE_AUTOLOAD_NAME)
# Session registry is a per-PID JSON file under a cross-project, user-scoped
# directory. The MCP shim reads these on startup so `session.list` can discover
# every running Godot editor + which port to talk to each.
func _write_session_registry() -> void:
var dir_path := _session_registry_dir()
if dir_path == "":
return
if not DirAccess.dir_exists_absolute(dir_path):
var derr := DirAccess.make_dir_recursive_absolute(dir_path)
if derr != OK:
push_warning("[agent_tools] could not create session registry dir: %s" % dir_path)
return
var pid := OS.get_process_id()
_session_file_path = dir_path.path_join("%d.json" % pid)
var project_name := ""
if ProjectSettings.has_setting("application/config/name"):
project_name = str(ProjectSettings.get_setting("application/config/name"))
var version := Engine.get_version_info()
var entry := {
"pid": pid,
"port": _bound_port,
"project_path": ProjectSettings.globalize_path("res://"),
"project_name": project_name,
"godot_version": "%d.%d.%d-%s" % [version.major, version.minor, version.patch, version.status],
"started_at_unix": int(Time.get_unix_time_from_system()),
}
var f := FileAccess.open(_session_file_path, FileAccess.WRITE)
if f == null:
push_warning("[agent_tools] could not write session file: %s" % _session_file_path)
return
f.store_string(JSON.stringify(entry, " "))
f.close()
func _remove_session_registry() -> void:
if _session_file_path == "":
return
if FileAccess.file_exists(_session_file_path):
@warning_ignore("return_value_discarded")
DirAccess.remove_absolute(_session_file_path)
_session_file_path = ""
# Cross-project location: <user home>/.godot-agent-tools/sessions/. Chosen to
# sit outside any single project's res:// or user:// tree.
func _session_registry_dir() -> String:
var home := OS.get_environment("USERPROFILE")
if home == "":
home = OS.get_environment("HOME")
if home == "":
return ""
return home.path_join(".godot-agent-tools").path_join("sessions")
+1
View File
@@ -0,0 +1 @@
uid://bkvb64lq51xyg
+129
View File
@@ -0,0 +1,129 @@
@tool
extends RefCounted
const SceneTools := preload("res://addons/agent_tools/tools/scene_tools.gd")
const SignalTools := preload("res://addons/agent_tools/tools/signal_tools.gd")
const ScriptTools := preload("res://addons/agent_tools/tools/script_tools.gd")
const ResourceTools := preload("res://addons/agent_tools/tools/resource_tools.gd")
const RefsTools := preload("res://addons/agent_tools/tools/refs_tools.gd")
const ProjectTools := preload("res://addons/agent_tools/tools/project_tools.gd")
const EditorTools := preload("res://addons/agent_tools/tools/editor_tools.gd")
const DocsTools := preload("res://addons/agent_tools/tools/docs_tools.gd")
const InputTools := preload("res://addons/agent_tools/tools/input_tools.gd")
const RunTools := preload("res://addons/agent_tools/tools/run_tools.gd")
const FsTools := preload("res://addons/agent_tools/tools/fs_tools.gd")
const AnimationTools := preload("res://addons/agent_tools/tools/animation_tools.gd")
const UserFsTools := preload("res://addons/agent_tools/tools/user_fs_tools.gd")
const PerformanceTools := preload("res://addons/agent_tools/tools/performance_tools.gd")
const TestTools := preload("res://addons/agent_tools/tools/test_tools.gd")
const ClientTools := preload("res://addons/agent_tools/tools/client_tools.gd")
const PhysicsTools := preload("res://addons/agent_tools/tools/physics_tools.gd")
const ThemeTools := preload("res://addons/agent_tools/tools/theme_tools.gd")
# Returns {"data": <any>} on success, {"error": {"code": int, "message": str}} on failure.
func dispatch(method: String, params: Dictionary) -> Dictionary:
match method:
"scene.new": return SceneTools.new_scene(params)
"scene.add_node": return SceneTools.add_node(params)
"scene.instance_packed": return SceneTools.instance_packed(params)
"scene.duplicate_node": return SceneTools.duplicate_node(params)
"scene.remove_node": return SceneTools.remove_node(params)
"scene.reparent": return SceneTools.reparent(params)
"scene.set_property": return SceneTools.set_property(params)
"scene.get_property": return SceneTools.get_property(params)
"scene.call_method": return SceneTools.call_method(params)
"scene.build_tree": return SceneTools.build_tree(params)
"scene.open": return SceneTools.open_scene(params)
"scene.save": return SceneTools.save_scene(params)
"scene.current": return SceneTools.current(params)
"scene.inspect": return SceneTools.inspect(params)
"scene.capture_screenshot": return SceneTools.capture_screenshot(params)
"signal.connect": return SignalTools.connect_signal(params)
"signal.disconnect": return SignalTools.disconnect_signal(params)
"signal.list": return SignalTools.list_signals(params)
"script.create": return ScriptTools.create(params)
"script.attach": return ScriptTools.attach(params)
"script.patch": return ScriptTools.patch(params)
"resource.create": return ResourceTools.create(params)
"resource.set_property": return ResourceTools.set_property(params)
"resource.call_method": return ResourceTools.call_method(params)
"refs.validate_project": return RefsTools.validate_project(params)
"refs.find_usages": return RefsTools.find_usages(params)
"refs.rename": return RefsTools.rename(params)
"refs.rename_class": return RefsTools.rename_class(params)
"project.get_setting": return ProjectTools.get_setting(params)
"project.set_setting": return ProjectTools.set_setting(params)
"autoload.add": return ProjectTools.autoload_add(params)
"autoload.remove": return ProjectTools.autoload_remove(params)
"autoload.list": return ProjectTools.autoload_list(params)
"editor.reload_filesystem": return EditorTools.reload_filesystem(params)
"editor.save_all_scenes": return EditorTools.save_all_scenes(params)
"editor.state": return EditorTools.state(params)
"editor.selection_get": return EditorTools.selection_get(params)
"editor.selection_set": return EditorTools.selection_set(params)
"editor.game_screenshot": return EditorTools.game_screenshot(params)
"logs.read": return EditorTools.logs_read(params)
"logs.clear": return EditorTools.logs_clear(params)
"docs.class_ref": return DocsTools.class_ref(params)
"input_map.add_action": return InputTools.add_action(params)
"input_map.add_event": return InputTools.add_event(params)
"input_map.list": return InputTools.list_actions(params)
"input_map.remove_action": return InputTools.remove_action(params)
"input_map.remove_event": return InputTools.remove_event(params)
"run.scene_headless": return RunTools.scene_headless(params)
"fs.list": return FsTools.list(params)
"fs.read_text": return FsTools.read_text(params)
"fs.write_text": return FsTools.write_text(params)
"user_fs.read": return UserFsTools.read(params)
"user_fs.list": return UserFsTools.list(params)
"animation.list": return AnimationTools.list_animations(params)
"animation.add_animation": return AnimationTools.add_animation(params)
"animation.remove_animation": return AnimationTools.remove_animation(params)
"animation.add_value_track": return AnimationTools.add_value_track(params)
"performance.monitors": return PerformanceTools.monitors(params)
"test.run": return TestTools.run(params)
"client.list": return ClientTools.list_clients(params)
"client.configure": return ClientTools.configure(params)
"client.remove": return ClientTools.remove(params)
"physics.autofit_collision_shape_2d": return PhysicsTools.autofit_collision_shape_2d(params)
"theme.set_color": return ThemeTools.set_color(params)
"theme.set_constant": return ThemeTools.set_constant(params)
"theme.set_font_size": return ThemeTools.set_font_size(params)
"theme.set_stylebox_flat": return ThemeTools.set_stylebox_flat(params)
"batch.execute": return batch_execute(params)
_:
return {"error": {"code": -32601, "message": "method not found: %s" % method}}
# batch.execute — run multiple tool calls in one round trip.
# Params: {calls: [{method, params?}, ...], stop_on_error?: false}
# Returns: {results: [{method, ok, data|error, index}, ...]}
func batch_execute(params: Dictionary) -> Dictionary:
var calls: Array = params.get("calls", [])
var stop_on_error: bool = params.get("stop_on_error", false)
if calls.is_empty():
return {"error": {"code": -32602, "message": "missing 'calls' (array of {method, params})"}}
var results: Array = []
for i in calls.size():
var entry = calls[i]
if typeof(entry) != TYPE_DICTIONARY or not entry.has("method"):
results.append({"index": i, "ok": false, "error": "each call needs {method, params?}"})
if stop_on_error:
break
continue
var sub_method: String = entry.method
var sub_params: Dictionary = entry.get("params", {})
var sub_result: Dictionary = dispatch(sub_method, sub_params)
var row := {"index": i, "method": sub_method}
if sub_result.has("error"):
row["ok"] = false
row["error"] = sub_result.error
else:
row["ok"] = true
row["data"] = sub_result.get("data")
results.append(row)
if stop_on_error and not row.ok:
break
return {"data": {"results": results, "count": results.size(), "requested": calls.size()}}
+1
View File
@@ -0,0 +1 @@
uid://b5pe4omwjkjgo
@@ -0,0 +1,31 @@
# Custom Logger that forwards every captured message to the game bridge's
# log buffer. Has to live in its own top-level file — nested classes don't
# reliably register Logger's virtual overrides (_log_error / _log_message).
extends Logger
var _bridge: Node
func _init(bridge: Node) -> void:
_bridge = bridge
func _log_error(function: String, file: String, line: int, code: String, rationale: String, editor_notify: bool, error_type: int, script_backtraces: Array) -> void:
# error_type is Logger.ErrorType: 0=ERROR, 1=WARNING, 2=SCRIPT, 3=SHADER.
var level: String
match error_type:
1: level = "WARNING"
2: level = "SCRIPT"
3: level = "SHADER"
_: level = "ERROR"
var msg: String = rationale if rationale != "" else code
if file != "" and line > 0:
msg += " (at %s:%d in %s)" % [file, line, function]
if _bridge and _bridge.has_method("log_entry"):
_bridge.log_entry(level, msg)
func _log_message(message: String, error: bool) -> void:
if _bridge and _bridge.has_method("log_entry"):
_bridge.log_entry("ERROR" if error else "INFO", message.strip_edges())
@@ -0,0 +1 @@
uid://wosqs8dlh1vx
+134
View File
@@ -0,0 +1,134 @@
# MCPGameBridge — runtime autoload registered by the Agent Tools plugin.
#
# Bridges the editor-side agent tools to the *running* game via a file-based
# request/response channel under res://.godot/agent_tools/bridge/. Currently:
# - screenshot: capture the running viewport, save to a path
# - logs: append print/push_error/push_warning output to a buffer
#
# This script ONLY runs in the game process (checks Engine.is_editor_hint()
# on startup and disables itself in the editor). The editor plugin registers
# it via add_autoload_singleton() on _enter_tree and removes it on _exit_tree.
extends Node
const BridgeLogger := preload("res://addons/agent_tools/runtime/bridge_logger.gd")
const BRIDGE_DIR := "res://.godot/agent_tools/bridge"
const REQUEST_PATH := BRIDGE_DIR + "/request.json"
const RESPONSE_PATH := BRIDGE_DIR + "/response.json"
const LOGS_PATH := BRIDGE_DIR + "/logs.json"
const POLL_INTERVAL_SEC := 0.05 # 20 Hz — cheap enough, responsive enough
const LOG_BUFFER_MAX := 500
var _timer: Timer
var _log_buffer: Array = []
var _initialized := false
func _ready() -> void:
if Engine.is_editor_hint():
# Tool mode — don't do anything inside the editor. The bridge only
# makes sense in the running game.
return
_initialized = true
_ensure_bridge_dir()
# Poll for editor requests on a timer so we don't eat a _process callback.
_timer = Timer.new()
_timer.wait_time = POLL_INTERVAL_SEC
_timer.one_shot = false
_timer.autostart = true
_timer.timeout.connect(_poll_request)
add_child(_timer)
# Attach our custom logger to capture print / push_error / push_warning
# from the game and mirror them to the log buffer file so logs.read can
# pick them up from the editor side.
var logger := BridgeLogger.new(self)
OS.add_logger(logger)
func _ensure_bridge_dir() -> void:
if not DirAccess.dir_exists_absolute(BRIDGE_DIR):
@warning_ignore("return_value_discarded")
DirAccess.make_dir_recursive_absolute(BRIDGE_DIR)
# Called by MCPBridgeLogger whenever the game writes to stdout/stderr.
func log_entry(level: String, message: String) -> void:
_log_buffer.append({
"level": level,
"message": message,
"time_ms": Time.get_ticks_msec(),
})
if _log_buffer.size() > LOG_BUFFER_MAX:
_log_buffer = _log_buffer.slice(_log_buffer.size() - LOG_BUFFER_MAX)
# Flush to disk so editor-side logs.read sees it. Debounced by poll interval
# in practice because each log call flushes but writes are cheap.
_flush_logs()
func _flush_logs() -> void:
var f := FileAccess.open(LOGS_PATH, FileAccess.WRITE)
if f == null:
return
f.store_string(JSON.stringify(_log_buffer))
f.close()
func _poll_request() -> void:
if not FileAccess.file_exists(REQUEST_PATH):
return
var f := FileAccess.open(REQUEST_PATH, FileAccess.READ)
if f == null:
return
var body := f.get_as_text()
f.close()
@warning_ignore("return_value_discarded")
DirAccess.remove_absolute(REQUEST_PATH)
var parsed = JSON.parse_string(body)
if typeof(parsed) != TYPE_DICTIONARY:
_write_response({"id": "", "error": "malformed request"})
return
var req_id: String = parsed.get("id", "")
match parsed.get("type", ""):
"screenshot":
_handle_screenshot(req_id, parsed)
_:
_write_response({"id": req_id, "error": "unknown request type: %s" % parsed.get("type", "<missing>")})
func _handle_screenshot(req_id: String, req: Dictionary) -> void:
var output_path: String = req.get("output", "")
if output_path == "":
_write_response({"id": req_id, "error": "missing 'output'"})
return
var vp := get_viewport()
if vp == null:
_write_response({"id": req_id, "error": "no viewport available"})
return
var tex := vp.get_texture()
if tex == null:
_write_response({"id": req_id, "error": "viewport has no texture"})
return
var img := tex.get_image()
if img == null or img.is_empty():
_write_response({"id": req_id, "error": "failed to capture image"})
return
var dir := output_path.get_base_dir()
if not DirAccess.dir_exists_absolute(dir):
@warning_ignore("return_value_discarded")
DirAccess.make_dir_recursive_absolute(dir)
var save_err := img.save_png(output_path)
if save_err != OK:
_write_response({"id": req_id, "error": "save_png failed: %d" % save_err})
return
_write_response({"id": req_id, "path": output_path})
func _write_response(payload: Dictionary) -> void:
var f := FileAccess.open(RESPONSE_PATH, FileAccess.WRITE)
if f == null:
return
f.store_string(JSON.stringify(payload))
f.close()
@@ -0,0 +1 @@
uid://cds186n8uv4f0
+125
View File
@@ -0,0 +1,125 @@
@tool
extends Node
# Line-delimited JSON-RPC over TCP on localhost.
# One request per line: {"id": <any>, "method": "<name>", "params": {...}}
# One response per line: {"id": <echoed>, "result": <data>} OR {"id": ..., "error": {"code": int, "message": str, "data"?: any}}
const MAX_MESSAGE_BYTES := 1_048_576 # 1 MiB per line — safety cap
var _tcp: TCPServer
var _clients: Array = [] # Array of {peer: StreamPeerTCP, buf: PackedByteArray}
var _registry
func _init(registry) -> void:
_registry = registry
func start(port: int, interface: String = "127.0.0.1") -> bool:
_tcp = TCPServer.new()
var err := _tcp.listen(port, interface)
if err != OK:
_tcp = null
return false
set_process(true)
return true
func stop() -> void:
set_process(false)
for c in _clients:
(c.peer as StreamPeerTCP).disconnect_from_host()
_clients.clear()
if _tcp:
_tcp.stop()
_tcp = null
func _process(_delta: float) -> void:
if _tcp == null:
return
while _tcp.is_connection_available():
var peer := _tcp.take_connection()
_clients.append({"peer": peer, "buf": PackedByteArray()})
var still_alive: Array = []
for c in _clients:
var peer: StreamPeerTCP = c.peer
peer.poll()
if peer.get_status() != StreamPeerTCP.STATUS_CONNECTED:
continue
var available := peer.get_available_bytes()
if available > 0:
var chunk := peer.get_data(available)
if chunk[0] == OK:
c.buf.append_array(chunk[1])
_drain_messages(c)
if (c.buf as PackedByteArray).size() > MAX_MESSAGE_BYTES:
_send_error(peer, null, -32700, "message exceeds %d bytes" % MAX_MESSAGE_BYTES)
peer.disconnect_from_host()
continue
still_alive.append(c)
_clients = still_alive
func _drain_messages(c: Dictionary) -> void:
while true:
var buf: PackedByteArray = c.buf
var nl := buf.find(0x0A) # \n
if nl < 0:
return
var line_bytes := buf.slice(0, nl)
c.buf = buf.slice(nl + 1)
var line := line_bytes.get_string_from_utf8().strip_edges()
if line == "":
continue
_handle_line(c.peer, line)
func _handle_line(peer: StreamPeerTCP, line: String) -> void:
var parsed = JSON.parse_string(line)
if typeof(parsed) != TYPE_DICTIONARY:
_send_error(peer, null, -32700, "parse error — expected JSON object")
return
var id = parsed.get("id")
var method: String = parsed.get("method", "")
var params: Dictionary = parsed.get("params", {})
if method == "":
_send_error(peer, id, -32600, "missing 'method'")
return
var result: Dictionary = _registry.dispatch(method, params)
# Guard against silent tool-module failures: when a tool's .gd file has a parse
# error, preload() returns null, calls on it produce no output, and dispatch
# returns its default {}. Surface that instead of sending {"result": null}.
if result.is_empty():
_send_error(peer, id, -32000,
"tool returned empty response — likely a parse error in the tool module. Check Godot's Output panel for the actual error.",
{"method": method})
elif result.has("error"):
var e: Dictionary = result.error
_send_error(peer, id, e.get("code", -32000), e.get("message", ""), e.get("data"))
elif result.has("data"):
_send_result(peer, id, result.data)
else:
_send_error(peer, id, -32000,
"tool returned malformed response (missing both 'data' and 'error')",
{"method": method})
func _send_result(peer: StreamPeerTCP, id, data) -> void:
_write(peer, {"id": id, "result": data})
func _send_error(peer: StreamPeerTCP, id, code: int, msg: String, data = null) -> void:
var err := {"code": code, "message": msg}
if data != null:
err["data"] = data
_write(peer, {"id": id, "error": err})
func _write(peer: StreamPeerTCP, obj: Dictionary) -> void:
var line := JSON.stringify(obj) + "\n"
peer.put_data(line.to_utf8_buffer())
+1
View File
@@ -0,0 +1 @@
uid://pcfsf0ijlm8x
+306
View File
@@ -0,0 +1,306 @@
@tool
extends RefCounted
# Shared JSON-to-Godot type coercion for scene.set_property, resource.set_property,
# and any other tool that assigns a user-provided value into a typed property slot.
#
# Contract:
# coerce(value, target_type: int) -> Variant
#
# Returns the coerced value on success. On mismatch, returns a Dictionary with
# a single "_error" key describing the failure — callers check `is Dictionary`
# and forward the message as a tool-level error.
#
# New types go here. Don't re-add `_coerce` to individual tool files; route
# through Coerce.coerce() instead.
static func coerce(value, target_type: int):
match target_type:
TYPE_BOOL:
# Godot 4 has no `bool()` constructor (it errors at runtime). Convert via
# truthiness, but parse strings explicitly so "false"/"0" don't read as
# truthy the way any non-empty string otherwise would.
if value is String:
return value.strip_edges().to_lower() in ["true", "1", "yes", "on"]
return true if value else false
TYPE_INT:
return int(value)
TYPE_FLOAT:
return float(value)
TYPE_STRING, TYPE_STRING_NAME:
return String(value)
TYPE_NODE_PATH:
return NodePath(String(value))
TYPE_VECTOR2:
if value is Array and value.size() == 2:
return Vector2(value[0], value[1])
return {"_error": "Vector2 expects [x, y]"}
TYPE_VECTOR2I:
if value is Array and value.size() == 2:
return Vector2i(int(value[0]), int(value[1]))
return {"_error": "Vector2i expects [x, y]"}
TYPE_VECTOR3:
if value is Array and value.size() == 3:
return Vector3(value[0], value[1], value[2])
return {"_error": "Vector3 expects [x, y, z]"}
TYPE_VECTOR3I:
if value is Array and value.size() == 3:
return Vector3i(int(value[0]), int(value[1]), int(value[2]))
return {"_error": "Vector3i expects [x, y, z]"}
TYPE_VECTOR4:
if value is Array and value.size() == 4:
return Vector4(value[0], value[1], value[2], value[3])
return {"_error": "Vector4 expects [x, y, z, w]"}
TYPE_VECTOR4I:
if value is Array and value.size() == 4:
return Vector4i(int(value[0]), int(value[1]), int(value[2]), int(value[3]))
return {"_error": "Vector4i expects [x, y, z, w]"}
TYPE_RECT2:
if value is Array and value.size() == 4:
return Rect2(value[0], value[1], value[2], value[3])
return {"_error": "Rect2 expects [x, y, width, height]"}
TYPE_RECT2I:
if value is Array and value.size() == 4:
return Rect2i(int(value[0]), int(value[1]), int(value[2]), int(value[3]))
return {"_error": "Rect2i expects [x, y, width, height]"}
TYPE_QUATERNION:
if value is Array and value.size() == 4:
return Quaternion(value[0], value[1], value[2], value[3])
return {"_error": "Quaternion expects [x, y, z, w]"}
TYPE_TRANSFORM2D:
# TRS form: {origin: [x,y], rotation: radians, scale: [x,y]} — all optional.
if value is Dictionary:
var origin = value.get("origin", [0, 0])
var rotation: float = float(value.get("rotation", 0.0))
var scale_v = value.get("scale", [1, 1])
var skew: float = float(value.get("skew", 0.0))
if not (origin is Array and origin.size() == 2 and scale_v is Array and scale_v.size() == 2):
return {"_error": "Transform2D origin/scale expect [x, y]"}
return Transform2D(
rotation,
Vector2(scale_v[0], scale_v[1]),
skew,
Vector2(origin[0], origin[1])
)
return {"_error": "Transform2D expects {origin: [x,y], rotation: radians, scale: [x,y], skew?}"}
TYPE_TRANSFORM3D:
# TRS form: {origin: [x,y,z], rotation: [x,y,z] euler radians, scale: [x,y,z]}.
if value is Dictionary:
var origin = value.get("origin", [0, 0, 0])
var rotation = value.get("rotation", [0, 0, 0])
var scale_v = value.get("scale", [1, 1, 1])
if not (origin is Array and origin.size() == 3
and rotation is Array and rotation.size() == 3
and scale_v is Array and scale_v.size() == 3):
return {"_error": "Transform3D origin/rotation/scale expect [x, y, z]"}
var basis := Basis.from_euler(Vector3(rotation[0], rotation[1], rotation[2]))
basis = basis.scaled(Vector3(scale_v[0], scale_v[1], scale_v[2]))
return Transform3D(basis, Vector3(origin[0], origin[1], origin[2]))
return {"_error": "Transform3D expects {origin: [x,y,z], rotation: [x,y,z] (euler rad), scale: [x,y,z]}"}
TYPE_BASIS:
if value is Dictionary:
var rotation = value.get("rotation", [0, 0, 0])
var scale_v = value.get("scale", [1, 1, 1])
if not (rotation is Array and rotation.size() == 3
and scale_v is Array and scale_v.size() == 3):
return {"_error": "Basis rotation/scale expect [x, y, z]"}
return Basis.from_euler(Vector3(rotation[0], rotation[1], rotation[2])).scaled(
Vector3(scale_v[0], scale_v[1], scale_v[2]))
return {"_error": "Basis expects {rotation: [x,y,z] (euler rad), scale: [x,y,z]}"}
TYPE_AABB:
if value is Dictionary:
var pos = value.get("position", [0, 0, 0])
var size = value.get("size", [0, 0, 0])
if not (pos is Array and pos.size() == 3 and size is Array and size.size() == 3):
return {"_error": "AABB position/size expect [x, y, z]"}
return AABB(Vector3(pos[0], pos[1], pos[2]), Vector3(size[0], size[1], size[2]))
return {"_error": "AABB expects {position: [x,y,z], size: [x,y,z]}"}
TYPE_PLANE:
if value is Dictionary:
var normal = value.get("normal", [0, 1, 0])
var d: float = float(value.get("d", 0.0))
if not (normal is Array and normal.size() == 3):
return {"_error": "Plane normal expects [x, y, z]"}
return Plane(Vector3(normal[0], normal[1], normal[2]), d)
return {"_error": "Plane expects {normal: [x,y,z], d: float}"}
TYPE_COLOR:
if value is Array and (value.size() == 3 or value.size() == 4):
var a := float(value[3]) if value.size() == 4 else 1.0
return Color(value[0], value[1], value[2], a)
if value is String:
return Color(value)
return {"_error": "Color expects [r,g,b(,a)] or '#rrggbb(aa)'"}
TYPE_PACKED_STRING_ARRAY:
if value is Array:
var out := PackedStringArray()
for s in value:
out.append(String(s))
return out
return {"_error": "PackedStringArray expects an array of strings"}
TYPE_PACKED_INT32_ARRAY:
if value is Array:
var out := PackedInt32Array()
for i in value:
out.append(int(i))
return out
return {"_error": "PackedInt32Array expects an array of numbers"}
TYPE_PACKED_INT64_ARRAY:
if value is Array:
var out := PackedInt64Array()
for i in value:
out.append(int(i))
return out
return {"_error": "PackedInt64Array expects an array of numbers"}
TYPE_PACKED_FLOAT32_ARRAY:
if value is Array:
var out := PackedFloat32Array()
for f in value:
out.append(float(f))
return out
return {"_error": "PackedFloat32Array expects an array of numbers"}
TYPE_PACKED_FLOAT64_ARRAY:
if value is Array:
var out := PackedFloat64Array()
for f in value:
out.append(float(f))
return out
return {"_error": "PackedFloat64Array expects an array of numbers"}
TYPE_PACKED_VECTOR2_ARRAY:
if value is Array:
var out := PackedVector2Array()
for v in value:
if v is Array and v.size() == 2:
out.append(Vector2(v[0], v[1]))
else:
return {"_error": "PackedVector2Array expects [[x, y], ...]"}
return out
return {"_error": "PackedVector2Array expects an array of [x, y] pairs"}
TYPE_PACKED_VECTOR3_ARRAY:
if value is Array:
var out := PackedVector3Array()
for v in value:
if v is Array and v.size() == 3:
out.append(Vector3(v[0], v[1], v[2]))
else:
return {"_error": "PackedVector3Array expects [[x, y, z], ...]"}
return out
return {"_error": "PackedVector3Array expects an array of [x, y, z] triples"}
TYPE_PACKED_COLOR_ARRAY:
if value is Array:
var out := PackedColorArray()
for c in value:
if c is Array and (c.size() == 3 or c.size() == 4):
var a := float(c[3]) if c.size() == 4 else 1.0
out.append(Color(c[0], c[1], c[2], a))
elif c is String:
out.append(Color(c))
else:
return {"_error": "PackedColorArray expects [[r,g,b(,a)], ...] or ['#hex', ...]"}
return out
return {"_error": "PackedColorArray expects an array of colors"}
TYPE_OBJECT:
# Auto-load a Resource when the caller passes a "res://..." or "uid://..." path string.
# Without this, Godot silently drops the assignment and leaves the slot null.
if value is String and (value.begins_with("res://") or value.begins_with("uid://")):
var loaded := ResourceLoader.load(value)
if loaded == null:
return {"_error": "failed to load resource: %s" % value}
return loaded
return value
_:
return value
# Inverse of coerce(): convert a Godot value back to a JSON-native form so the
# response echo is unambiguous data rather than Godot's str() stringification
# (which turns Vector2 into "(x, y)", Color into "(r, g, b, a)", null Object
# into "<Object#null>", etc., all of which look like success strings).
static func to_json(value):
match typeof(value):
TYPE_VECTOR2:
return [value.x, value.y]
TYPE_VECTOR2I:
return [int(value.x), int(value.y)]
TYPE_VECTOR3:
return [value.x, value.y, value.z]
TYPE_VECTOR3I:
return [int(value.x), int(value.y), int(value.z)]
TYPE_VECTOR4:
return [value.x, value.y, value.z, value.w]
TYPE_VECTOR4I:
return [int(value.x), int(value.y), int(value.z), int(value.w)]
TYPE_RECT2:
return [value.position.x, value.position.y, value.size.x, value.size.y]
TYPE_RECT2I:
return [int(value.position.x), int(value.position.y), int(value.size.x), int(value.size.y)]
TYPE_COLOR:
return [value.r, value.g, value.b, value.a]
TYPE_QUATERNION:
return [value.x, value.y, value.z, value.w]
TYPE_NODE_PATH:
return String(value)
TYPE_STRING_NAME:
return String(value)
TYPE_TRANSFORM2D:
return {
"origin": [value.origin.x, value.origin.y],
"x": [value.x.x, value.x.y],
"y": [value.y.x, value.y.y],
}
TYPE_TRANSFORM3D:
return {
"origin": [value.origin.x, value.origin.y, value.origin.z],
"basis": to_json(value.basis),
}
TYPE_BASIS:
return [
[value.x.x, value.x.y, value.x.z],
[value.y.x, value.y.y, value.y.z],
[value.z.x, value.z.y, value.z.z],
]
TYPE_AABB:
return {
"position": [value.position.x, value.position.y, value.position.z],
"size": [value.size.x, value.size.y, value.size.z],
}
TYPE_PLANE:
return {
"normal": [value.normal.x, value.normal.y, value.normal.z],
"d": value.d,
}
TYPE_OBJECT:
if value == null:
return null
if value is Resource and value.resource_path != "":
return {"class": value.get_class(), "resource_path": value.resource_path}
return {"class": (value as Object).get_class()}
TYPE_PACKED_BYTE_ARRAY, TYPE_PACKED_INT32_ARRAY, TYPE_PACKED_INT64_ARRAY, \
TYPE_PACKED_FLOAT32_ARRAY, TYPE_PACKED_FLOAT64_ARRAY, TYPE_PACKED_STRING_ARRAY:
return Array(value)
TYPE_PACKED_VECTOR2_ARRAY:
var out_v2: Array = []
for v in value:
out_v2.append([v.x, v.y])
return out_v2
TYPE_PACKED_VECTOR3_ARRAY:
var out_v3: Array = []
for v in value:
out_v3.append([v.x, v.y, v.z])
return out_v3
TYPE_PACKED_COLOR_ARRAY:
var out_c: Array = []
for c in value:
out_c.append([c.r, c.g, c.b, c.a])
return out_c
TYPE_ARRAY:
var out_a: Array = []
for item in value:
out_a.append(to_json(item))
return out_a
TYPE_DICTIONARY:
var out_d: Dictionary = {}
for k in value:
out_d[str(k)] = to_json(value[k])
return out_d
_:
return value
+1
View File
@@ -0,0 +1 @@
uid://cncjf8g7jufrc
+210
View File
@@ -0,0 +1,210 @@
@tool
extends RefCounted
# Tools for working with AnimationPlayer nodes in the currently-edited scene.
# Animations live in AnimationLibrary resources attached to the player; the "" library
# is the default, named libraries prefix animations as "lib/anim".
# animation.list — list animations on an AnimationPlayer with their tracks.
# Params: {node_path}
static func list_animations(params: Dictionary) -> Dictionary:
var node_path: String = params.get("node_path", "")
if node_path == "":
return _err(-32602, "missing 'node_path'")
var root := EditorInterface.get_edited_scene_root()
if root == null:
return _err(-32001, "no scene open")
var node: Node = root if node_path == "." else root.get_node_or_null(node_path)
if node == null:
return _err(-32001, "node not found: %s" % node_path)
if not (node is AnimationPlayer):
return _err(-32602, "node is not an AnimationPlayer: %s (%s)" % [node_path, node.get_class()])
var ap: AnimationPlayer = node
var out: Array = []
for lib_name in ap.get_animation_library_list():
var lib: AnimationLibrary = ap.get_animation_library(lib_name)
for anim_name in lib.get_animation_list():
var anim: Animation = lib.get_animation(anim_name)
var full_name: String
if String(lib_name) == "":
full_name = String(anim_name)
else:
full_name = "%s/%s" % [lib_name, anim_name]
var tracks: Array = []
for i in anim.get_track_count():
tracks.append({
"index": i,
"type": _track_type_name(anim.track_get_type(i)),
"path": String(anim.track_get_path(i)),
"key_count": anim.track_get_key_count(i),
})
out.append({
"name": full_name,
"length": anim.length,
"loop_mode": anim.loop_mode,
"step": anim.step,
"tracks": tracks,
})
return _ok({"node_path": node_path, "animations": out})
# animation.add_animation — create an empty animation in the player's library.
# Params: {node_path, name, length?: 1.0, library?: ""}
static func add_animation(params: Dictionary) -> Dictionary:
var node_path: String = params.get("node_path", "")
var anim_name: String = params.get("name", "")
var length: float = float(params.get("length", 1.0))
var library_name: String = params.get("library", "")
if node_path == "" or anim_name == "":
return _err(-32602, "missing 'node_path' or 'name'")
var root := EditorInterface.get_edited_scene_root()
if root == null:
return _err(-32001, "no scene open")
var node: Node = root if node_path == "." else root.get_node_or_null(node_path)
if node == null:
return _err(-32001, "node not found: %s" % node_path)
if not (node is AnimationPlayer):
return _err(-32602, "node is not an AnimationPlayer: %s (%s)" % [node_path, node.get_class()])
var ap: AnimationPlayer = node
var lib: AnimationLibrary
if ap.has_animation_library(library_name):
lib = ap.get_animation_library(library_name)
else:
lib = AnimationLibrary.new()
var add_err: int = ap.add_animation_library(library_name, lib)
if add_err != OK:
return _err(-32001, "add_animation_library failed: %d" % add_err)
if lib.has_animation(anim_name):
return _err(-32602, "animation already exists: %s" % anim_name)
var anim := Animation.new()
anim.length = length
@warning_ignore("return_value_discarded")
lib.add_animation(anim_name, anim)
EditorInterface.mark_scene_as_unsaved()
return _ok({"name": anim_name, "library": library_name, "length": length})
# animation.remove_animation — delete an animation from the player.
# Params: {node_path, name, library?: ""}
static func remove_animation(params: Dictionary) -> Dictionary:
var node_path: String = params.get("node_path", "")
var anim_name: String = params.get("name", "")
var library_name: String = params.get("library", "")
if node_path == "" or anim_name == "":
return _err(-32602, "missing 'node_path' or 'name'")
var root := EditorInterface.get_edited_scene_root()
if root == null:
return _err(-32001, "no scene open")
var node: Node = root if node_path == "." else root.get_node_or_null(node_path)
if node == null:
return _err(-32001, "node not found: %s" % node_path)
if not (node is AnimationPlayer):
return _err(-32602, "node is not an AnimationPlayer")
var ap: AnimationPlayer = node
if not ap.has_animation_library(library_name):
return _err(-32001, "library not found: '%s'" % library_name)
var lib: AnimationLibrary = ap.get_animation_library(library_name)
if not lib.has_animation(anim_name):
return _err(-32001, "animation not found: %s" % anim_name)
lib.remove_animation(anim_name)
EditorInterface.mark_scene_as_unsaved()
return _ok({"removed": anim_name, "library": library_name})
# animation.add_value_track — add a value track that animates a property over time.
# target_node is resolved relative to the scene root (simpler than the
# AnimationPlayer.root_node-relative resolution, which is fiddly).
# Params: {node_path, animation, target_node, property, keyframes: [{time, value, easing?}]}
static func add_value_track(params: Dictionary) -> Dictionary:
var node_path: String = params.get("node_path", "")
var anim_name: String = params.get("animation", "")
var target_node: String = params.get("target_node", "")
var property_name: String = params.get("property", "")
var keyframes: Array = params.get("keyframes", [])
if node_path == "" or anim_name == "" or target_node == "" or property_name == "":
return _err(-32602, "missing 'node_path', 'animation', 'target_node', or 'property'")
if keyframes.is_empty():
return _err(-32602, "'keyframes' must contain at least one entry")
var root := EditorInterface.get_edited_scene_root()
if root == null:
return _err(-32001, "no scene open")
var node: Node = root if node_path == "." else root.get_node_or_null(node_path)
if node == null:
return _err(-32001, "node not found: %s" % node_path)
if not (node is AnimationPlayer):
return _err(-32602, "node is not an AnimationPlayer")
var ap: AnimationPlayer = node
if not ap.has_animation(anim_name):
return _err(-32001, "animation not found on this player: %s" % anim_name)
var anim: Animation = ap.get_animation(anim_name)
# Validate that the target resolves from the scene root (a simpler contract
# than AP.root_node-relative — callers think in scene-tree terms already).
if root.get_node_or_null(target_node) == null:
return _err(-32001, "target_node does not resolve from scene root: %s" % target_node)
var track_idx: int = anim.add_track(Animation.TYPE_VALUE)
anim.track_set_path(track_idx, NodePath("%s:%s" % [target_node, property_name]))
var max_time: float = 0.0
for kf in keyframes:
if typeof(kf) != TYPE_DICTIONARY or not kf.has("time") or not kf.has("value"):
return _err(-32602, "each keyframe needs {time, value}")
var t: float = float(kf.time)
var transition: float = float(kf.get("easing", 1.0))
anim.track_insert_key(track_idx, t, kf.value, transition)
if t > max_time:
max_time = t
if max_time > anim.length:
anim.length = max_time
EditorInterface.mark_scene_as_unsaved()
return _ok({
"animation": anim_name,
"track_index": track_idx,
"target": "%s:%s" % [target_node, property_name],
"keyframes_added": keyframes.size(),
})
static func _track_type_name(t: int) -> String:
match t:
Animation.TYPE_VALUE:
return "value"
Animation.TYPE_POSITION_3D:
return "position_3d"
Animation.TYPE_ROTATION_3D:
return "rotation_3d"
Animation.TYPE_SCALE_3D:
return "scale_3d"
Animation.TYPE_BLEND_SHAPE:
return "blend_shape"
Animation.TYPE_METHOD:
return "method"
Animation.TYPE_BEZIER:
return "bezier"
Animation.TYPE_AUDIO:
return "audio"
Animation.TYPE_ANIMATION:
return "animation"
_:
return "unknown(%d)" % t
static func _ok(data) -> Dictionary:
return {"data": data}
static func _err(code: int, msg: String) -> Dictionary:
return {"error": {"code": code, "message": msg}}
@@ -0,0 +1 @@
uid://kurfdxg67ggk
+296
View File
@@ -0,0 +1,296 @@
@tool
extends RefCounted
# client.* — manage MCP client config files so users don't have to hand-edit them.
# Each client has its own config schema; we handle three shapes:
# "mcp_servers" → {"mcpServers": {"<name>": {command, args}}}
# Used by Claude Code, Claude Desktop, Cursor, Windsurf.
# "vscode_servers" → {"servers": {"<name>": {command, args}}}
# VS Code's native MCP config.
# "continue_experimental" → {"experimental": {"modelContextProtocolServers":
# [{"transport": {"type": "stdio", command, args}}]}}
# Continue.dev's MCP config shape (array, not dict).
#
# Clients requiring JSONC (with comments) or non-file storage — Zed, VS Code
# user-scope settings.json, Cline — aren't supported yet. Use manual config
# for those; see the README per-client section.
const SERVER_NAME := "godot-agent-tools"
const SERVER_COMMAND := "npx"
const SERVER_ARGS := ["-y", "godot-agent-tools-mcp"]
static func list_clients(_params: Dictionary) -> Dictionary:
var out: Array = []
for client_id in _client_ids():
var info := _client_info(client_id)
if info.has("_error"):
out.append({"client": client_id, "error": info._error})
continue
var res := _read_config(info.path)
var cfg: Dictionary = res.cfg
var installed := _has_server_entry(cfg, info.format)
out.append({
"client": client_id,
"config_path": info.path,
"exists": _path_exists(info.path),
"configured": installed,
"unparseable": res.unparseable,
})
return _ok({"clients": out})
static func configure(params: Dictionary) -> Dictionary:
var client_id: String = params.get("client", "")
var overwrite: bool = params.get("overwrite", false)
if client_id == "":
return _err(-32602, "missing 'client' (supported: %s)" % ", ".join(_client_ids()))
var info := _client_info(client_id)
if info.has("_error"):
return _err(-32602, info._error)
var res := _read_config(info.path)
if res.unparseable:
return _err(-32001, "existing config is not valid JSON — refusing to overwrite (fix or remove it first): %s" % info.path)
var cfg: Dictionary = res.cfg
if _has_server_entry(cfg, info.format) and not overwrite:
return _ok({"client": client_id, "path": info.path, "status": "already_configured"})
_set_server_entry(cfg, info.format)
var write_err := _write_config(info.path, cfg)
if write_err != "":
return _err(-32001, write_err)
return _ok({"client": client_id, "path": info.path, "status": "configured"})
static func remove(params: Dictionary) -> Dictionary:
var client_id: String = params.get("client", "")
if client_id == "":
return _err(-32602, "missing 'client'")
var info := _client_info(client_id)
if info.has("_error"):
return _err(-32602, info._error)
var res := _read_config(info.path)
if res.unparseable:
return _err(-32001, "existing config is not valid JSON — refusing to modify (fix or remove it first): %s" % info.path)
var cfg: Dictionary = res.cfg
if not _has_server_entry(cfg, info.format):
return _ok({"client": client_id, "path": info.path, "status": "not_present"})
_clear_server_entry(cfg, info.format)
var write_err := _write_config(info.path, cfg)
if write_err != "":
return _err(-32001, write_err)
return _ok({"client": client_id, "path": info.path, "status": "removed"})
static func _client_ids() -> Array:
return [
"claude_code_project",
"claude_code_user",
"claude_desktop",
"cursor_project",
"cursor_user",
"windsurf_user",
"continue_user",
"vscode_project",
]
static func _client_info(client_id: String) -> Dictionary:
var home := OS.get_environment("USERPROFILE")
if home == "":
home = OS.get_environment("HOME")
if home == "":
return {"_error": "cannot resolve home directory (neither USERPROFILE nor HOME set)"}
match client_id:
"claude_code_project":
return {"path": ProjectSettings.globalize_path("res://.mcp.json"), "format": "mcp_servers"}
"claude_code_user":
return {"path": home.path_join(".claude.json"), "format": "mcp_servers"}
"cursor_project":
return {"path": ProjectSettings.globalize_path("res://.cursor/mcp.json"), "format": "mcp_servers"}
"cursor_user":
return {"path": home.path_join(".cursor").path_join("mcp.json"), "format": "mcp_servers"}
"windsurf_user":
return {"path": home.path_join(".codeium").path_join("windsurf").path_join("mcp_config.json"), "format": "mcp_servers"}
"continue_user":
return {"path": home.path_join(".continue").path_join("config.json"), "format": "continue_experimental"}
"vscode_project":
return {"path": ProjectSettings.globalize_path("res://.vscode/mcp.json"), "format": "vscode_servers"}
"claude_desktop":
var os_name := OS.get_name()
var path: String
if os_name == "macOS":
path = home.path_join("Library").path_join("Application Support").path_join("Claude").path_join("claude_desktop_config.json")
elif os_name == "Windows":
var appdata := OS.get_environment("APPDATA")
if appdata == "":
return {"_error": "APPDATA env var not set on Windows"}
path = appdata.path_join("Claude").path_join("claude_desktop_config.json")
else:
path = home.path_join(".config").path_join("Claude").path_join("claude_desktop_config.json")
return {"path": path, "format": "mcp_servers"}
_:
return {"_error": "unknown client: %s (supported: %s)" % [client_id, ", ".join(_client_ids())]}
static func _path_exists(path: String) -> bool:
return FileAccess.file_exists(path)
# Returns {"cfg": Dictionary, "unparseable": bool}. unparseable=true means the
# file exists with non-empty content that is NOT a valid JSON object — callers
# that go on to WRITE must abort, since merging into {} would wipe the user's
# data (e.g. a large ~/.claude.json with a transient syntax error, or a JSONC
# file with comments that JSON.parse_string can't handle).
static func _read_config(path: String) -> Dictionary:
var content := ""
if _path_exists(path):
var f := FileAccess.open(path, FileAccess.READ)
if f:
content = f.get_as_text()
f.close()
if content.strip_edges() == "":
return {"cfg": {}, "unparseable": false}
var parsed = JSON.parse_string(content)
if typeof(parsed) != TYPE_DICTIONARY:
return {"cfg": {}, "unparseable": true}
return {"cfg": parsed, "unparseable": false}
static func _write_config(path: String, cfg: Dictionary) -> String:
var dir_path := path.get_base_dir()
if not DirAccess.dir_exists_absolute(dir_path):
var derr := DirAccess.make_dir_recursive_absolute(dir_path)
if derr != OK:
return "mkdir failed for %s (%d)" % [dir_path, derr]
# Back up any existing non-empty file before overwriting, so a mistaken merge
# or an interrupted write is recoverable.
if FileAccess.file_exists(path):
var prev := FileAccess.open(path, FileAccess.READ)
if prev:
var prev_content := prev.get_as_text()
prev.close()
if prev_content.strip_edges() != "":
var bak := FileAccess.open(path + ".bak", FileAccess.WRITE)
if bak:
bak.store_string(prev_content)
bak.close()
var f := FileAccess.open(path, FileAccess.WRITE)
if f == null:
return "could not open for write: %s (error %d)" % [path, FileAccess.get_open_error()]
f.store_string(JSON.stringify(cfg, " "))
f.close()
return ""
# --- Format-specific readers/writers -----------------------------------------
static func _has_server_entry(cfg: Dictionary, format: String) -> bool:
match format:
"mcp_servers":
var servers = cfg.get("mcpServers")
return typeof(servers) == TYPE_DICTIONARY and (servers as Dictionary).has(SERVER_NAME)
"vscode_servers":
var servers = cfg.get("servers")
return typeof(servers) == TYPE_DICTIONARY and (servers as Dictionary).has(SERVER_NAME)
"continue_experimental":
var exp = cfg.get("experimental")
if typeof(exp) != TYPE_DICTIONARY:
return false
var arr = exp.get("modelContextProtocolServers")
if typeof(arr) != TYPE_ARRAY:
return false
for entry in arr:
if typeof(entry) == TYPE_DICTIONARY and _entry_matches_continue(entry):
return true
return false
_:
return false
static func _set_server_entry(cfg: Dictionary, format: String) -> void:
match format:
"mcp_servers":
if typeof(cfg.get("mcpServers")) != TYPE_DICTIONARY:
cfg["mcpServers"] = {}
(cfg["mcpServers"] as Dictionary)[SERVER_NAME] = {
"command": SERVER_COMMAND,
"args": SERVER_ARGS.duplicate(),
}
"vscode_servers":
if typeof(cfg.get("servers")) != TYPE_DICTIONARY:
cfg["servers"] = {}
(cfg["servers"] as Dictionary)[SERVER_NAME] = {
"command": SERVER_COMMAND,
"args": SERVER_ARGS.duplicate(),
}
"continue_experimental":
if typeof(cfg.get("experimental")) != TYPE_DICTIONARY:
cfg["experimental"] = {}
var exp: Dictionary = cfg["experimental"]
if typeof(exp.get("modelContextProtocolServers")) != TYPE_ARRAY:
exp["modelContextProtocolServers"] = []
var arr: Array = exp["modelContextProtocolServers"]
# Remove any existing match so we don't duplicate.
var kept: Array = []
for entry in arr:
if typeof(entry) != TYPE_DICTIONARY or not _entry_matches_continue(entry):
kept.append(entry)
kept.append({
"transport": {
"type": "stdio",
"command": SERVER_COMMAND,
"args": SERVER_ARGS.duplicate(),
},
})
exp["modelContextProtocolServers"] = kept
static func _clear_server_entry(cfg: Dictionary, format: String) -> void:
match format:
"mcp_servers":
var servers = cfg.get("mcpServers")
if typeof(servers) == TYPE_DICTIONARY:
(servers as Dictionary).erase(SERVER_NAME)
"vscode_servers":
var servers = cfg.get("servers")
if typeof(servers) == TYPE_DICTIONARY:
(servers as Dictionary).erase(SERVER_NAME)
"continue_experimental":
var exp = cfg.get("experimental")
if typeof(exp) != TYPE_DICTIONARY:
return
var arr = exp.get("modelContextProtocolServers")
if typeof(arr) != TYPE_ARRAY:
return
var kept: Array = []
for entry in arr:
if typeof(entry) != TYPE_DICTIONARY or not _entry_matches_continue(entry):
kept.append(entry)
exp["modelContextProtocolServers"] = kept
# Continue's config uses a flat array with no `name` field — identify our entry
# by the transport command+args combo.
static func _entry_matches_continue(entry: Dictionary) -> bool:
var transport = entry.get("transport")
if typeof(transport) != TYPE_DICTIONARY:
return false
if transport.get("command") != SERVER_COMMAND:
return false
var a = transport.get("args")
if typeof(a) != TYPE_ARRAY or (a as Array).size() < 2:
return false
return a[1] == SERVER_ARGS[1]
static func _ok(data) -> Dictionary:
return {"data": data}
static func _err(code: int, msg: String) -> Dictionary:
return {"error": {"code": code, "message": msg}}
@@ -0,0 +1 @@
uid://b3u1el0113l81
+72
View File
@@ -0,0 +1,72 @@
@tool
extends RefCounted
# docs.class_ref — return the public API of a Godot class so the agent can plan
# work without guessing method/property/signal names.
# Params: {class_name, include_inherited?: false}
# Returns: {class, parent, methods: [{name, args, return}], properties, signals, constants}
static func class_ref(params: Dictionary) -> Dictionary:
var cname: String = params.get("class_name", "")
var include_inherited: bool = params.get("include_inherited", false)
if cname == "":
return _err(-32602, "missing 'class_name'")
if not ClassDB.class_exists(cname):
return _err(-32602, "unknown class: %s" % cname)
var no_inheritance := not include_inherited
var methods: Array = []
for m in ClassDB.class_get_method_list(cname, no_inheritance):
# Skip internal / editor-only methods — they're noise for an agent.
if (m.flags as int) & METHOD_FLAG_OBJECT_CORE:
continue
var args: Array = []
for a in m.args:
args.append({"name": a.name, "type": type_string(a.type)})
methods.append({
"name": m.name,
"args": args,
"return": type_string(m.return.type) if m.has("return") else "void",
"virtual": bool((m.flags as int) & METHOD_FLAG_VIRTUAL),
"static": bool((m.flags as int) & METHOD_FLAG_STATIC),
})
var properties: Array = []
for p in ClassDB.class_get_property_list(cname, no_inheritance):
# Only user-facing properties (storage + editor). Internal group markers have usage=0.
if not ((p.usage as int) & PROPERTY_USAGE_EDITOR):
continue
properties.append({
"name": p.name,
"type": type_string(p.type),
"hint": p.get("hint_string", ""),
})
var signals: Array = []
for s in ClassDB.class_get_signal_list(cname, no_inheritance):
var args: Array = []
for a in s.args:
args.append({"name": a.name, "type": type_string(a.type)})
signals.append({"name": s.name, "args": args})
var constants: Array = []
for c in ClassDB.class_get_integer_constant_list(cname, no_inheritance):
constants.append({"name": c, "value": ClassDB.class_get_integer_constant(cname, c)})
return _ok({
"class": cname,
"parent": ClassDB.get_parent_class(cname),
"inherited_included": include_inherited,
"methods": methods,
"properties": properties,
"signals": signals,
"constants": constants,
})
static func _ok(data) -> Dictionary:
return {"data": data}
static func _err(code: int, msg: String) -> Dictionary:
return {"error": {"code": code, "message": msg}}
@@ -0,0 +1 @@
uid://dwarklg56vsim
+201
View File
@@ -0,0 +1,201 @@
@tool
extends RefCounted
# editor.reload_filesystem — trigger an editor filesystem rescan.
# Run this after external file changes (created/moved/deleted by tools outside the editor)
# so load() and the FileSystem dock reflect reality.
static func reload_filesystem(_params: Dictionary) -> Dictionary:
EditorInterface.get_resource_filesystem().scan()
return {"data": {"scanned": true}}
# editor.save_all_scenes — save every open edited scene.
static func save_all_scenes(_params: Dictionary) -> Dictionary:
EditorInterface.save_all_scenes()
return {"data": {"saved": true}}
# editor.state — consolidated editor + project status in one call.
# Replaces piecewise scene.current + project.get_setting + etc.
static func state(_params: Dictionary) -> Dictionary:
var version := Engine.get_version_info()
var version_str: String = "%d.%d.%d-%s" % [version.major, version.minor, version.patch, version.status]
var current_scene_path := ""
var current_scene_class := ""
var current_scene_name := ""
var root := EditorInterface.get_edited_scene_root()
if root != null:
current_scene_path = root.scene_file_path
current_scene_class = root.get_class()
current_scene_name = String(root.name)
var open_scenes: Array = []
for s in EditorInterface.get_open_scenes():
open_scenes.append(s)
var project_name := ""
if ProjectSettings.has_setting("application/config/name"):
project_name = str(ProjectSettings.get_setting("application/config/name"))
return {"data": {
"godot_version": version_str,
"project_name": project_name,
"current_scene": {
"path": current_scene_path,
"root_name": current_scene_name,
"root_class": current_scene_class,
"open": root != null,
},
"open_scenes": open_scenes,
"playing_scene": EditorInterface.is_playing_scene(),
"playing_scene_path": EditorInterface.get_playing_scene() if EditorInterface.is_playing_scene() else "",
}}
# editor.selection_get — return the currently-selected nodes in the editor tree dock.
# Useful for "operate on whatever I clicked" workflows.
static func selection_get(_params: Dictionary) -> Dictionary:
var selection := EditorInterface.get_selection()
var root := EditorInterface.get_edited_scene_root()
var out: Array = []
for node in selection.get_selected_nodes():
var path_str := ""
if root != null and (node == root or root.is_ancestor_of(node)):
path_str = String(root.get_path_to(node))
out.append({
"name": String(node.name),
"class": node.get_class(),
"node_path": path_str,
})
return {"data": {"selected": out, "count": out.size()}}
# editor.selection_set — select a specific set of nodes in the editor tree dock.
# Params: {node_paths: ["Player", "Enemy"]}
static func selection_set(params: Dictionary) -> Dictionary:
var paths: Array = params.get("node_paths", [])
var root := EditorInterface.get_edited_scene_root()
if root == null:
return {"error": {"code": -32001, "message": "no scene open"}}
var selection := EditorInterface.get_selection()
selection.clear()
var resolved: Array = []
var missing: Array = []
for p in paths:
var node: Node = root if p == "." else root.get_node_or_null(p)
if node == null:
missing.append(p)
continue
selection.add_node(node)
resolved.append(p)
if not missing.is_empty():
return {"error": {"code": -32001, "message": "nodes not found: %s" % str(missing)}}
return {"data": {"selected": resolved}}
# editor.game_screenshot — capture the viewport of the currently-running game.
# Works when the user has pressed F5 (or equivalent) and a game process is running
# with our runtime bridge autoload registered. Writes a request file, polls for
# the bridge to produce the PNG, returns the path.
#
# Params: {output?: "res://.godot/agent_tools/game.png", timeout_ms?: 5000}
static func game_screenshot(params: Dictionary) -> Dictionary:
var output_path: String = params.get("output", "res://.godot/agent_tools/game_screenshot.png")
var timeout_ms: int = int(params.get("timeout_ms", 5000))
if not EditorInterface.is_playing_scene():
return {"error": {"code": -32001,
"message": "no game running — press F5 (or equivalent) to start the game, then call again. Alternative for arbitrary scenes: run.scene_headless with 'screenshot'."}}
var bridge_dir := "res://.godot/agent_tools/bridge"
if not DirAccess.dir_exists_absolute(bridge_dir):
var derr := DirAccess.make_dir_recursive_absolute(bridge_dir)
if derr != OK:
return {"error": {"code": -32001, "message": "mkdir failed: %d" % derr}}
# Wipe stale output so we can detect when the bridge has written fresh data.
if FileAccess.file_exists(output_path):
@warning_ignore("return_value_discarded")
DirAccess.remove_absolute(output_path)
var request_path := bridge_dir + "/request.json"
var request_id := str(Time.get_ticks_msec())
var req := {
"id": request_id,
"type": "screenshot",
"output": output_path,
}
var f := FileAccess.open(request_path, FileAccess.WRITE)
if f == null:
return {"error": {"code": -32001, "message": "could not write bridge request"}}
f.store_string(JSON.stringify(req))
f.close()
# Poll for response — bridge writes response.json with matching id when done.
var response_path := bridge_dir + "/response.json"
var deadline_usec: int = Time.get_ticks_usec() + timeout_ms * 1000
while Time.get_ticks_usec() < deadline_usec:
if FileAccess.file_exists(response_path):
var rf := FileAccess.open(response_path, FileAccess.READ)
if rf:
var body := rf.get_as_text()
rf.close()
var parsed = JSON.parse_string(body)
if typeof(parsed) == TYPE_DICTIONARY and parsed.get("id", "") == request_id:
@warning_ignore("return_value_discarded")
DirAccess.remove_absolute(response_path)
if parsed.has("error"):
return {"error": {"code": -32001, "message": "bridge: %s" % parsed.error}}
return {"data": {
"path": output_path,
"captured": FileAccess.file_exists(output_path),
}}
OS.delay_msec(50)
# Clean up stale request if bridge never picked it up.
if FileAccess.file_exists(request_path):
@warning_ignore("return_value_discarded")
DirAccess.remove_absolute(request_path)
return {"error": {"code": -32001,
"message": "bridge timed out after %dms — is the running game using the _MCPGameBridge autoload? Re-enable the plugin to re-register it, then press F5 again." % timeout_ms}}
# logs.read — pull recent log lines captured by the game-bridge autoload.
# Params: {clear?: false, max_lines?: 200}
static func logs_read(params: Dictionary) -> Dictionary:
var clear: bool = params.get("clear", false)
var max_lines: int = int(params.get("max_lines", 200))
var logs_path := "res://.godot/agent_tools/bridge/logs.json"
if not FileAccess.file_exists(logs_path):
return {"data": {
"entries": [],
"count": 0,
"note": "no log buffer found — the game may not be running or the autoload isn't initialized. Press F5 and try again.",
}}
var f := FileAccess.open(logs_path, FileAccess.READ)
if f == null:
return {"error": {"code": -32001, "message": "could not read logs"}}
var parsed = JSON.parse_string(f.get_as_text())
f.close()
if typeof(parsed) != TYPE_ARRAY:
parsed = []
var entries: Array = parsed
if entries.size() > max_lines:
entries = entries.slice(entries.size() - max_lines)
if clear:
@warning_ignore("return_value_discarded")
DirAccess.remove_absolute(logs_path)
return {"data": {"entries": entries, "count": entries.size()}}
# logs.clear — drop the log buffer regardless of read state.
static func logs_clear(_params: Dictionary) -> Dictionary:
var logs_path := "res://.godot/agent_tools/bridge/logs.json"
if FileAccess.file_exists(logs_path):
@warning_ignore("return_value_discarded")
DirAccess.remove_absolute(logs_path)
return {"data": {"cleared": true}}
@@ -0,0 +1 @@
uid://bgoufu3l40rvx
+144
View File
@@ -0,0 +1,144 @@
@tool
extends RefCounted
# fs.list — enumerate project files by type, with optional glob filter.
# Params: {type?: "all"|"scene"|"script"|"resource"|"shader"|"image"|"audio",
# glob?: "res://scenes/**/*.tscn",
# include_addons?: false}
# Returns: {type, count, files: [paths]}
static func list(params: Dictionary) -> Dictionary:
var type_filter: String = params.get("type", "all")
var glob: String = params.get("glob", "")
var include_addons: bool = params.get("include_addons", false)
var exts: Array
match type_filter:
"", "all":
exts = ["gd", "cs", "tscn", "tres", "res", "gdshader", "gdshaderinc",
"png", "jpg", "jpeg", "svg", "webp",
"ogg", "wav", "mp3",
"json", "cfg"]
"scene":
exts = ["tscn"]
"script":
exts = ["gd", "cs"]
"resource":
exts = ["tres", "res"]
"shader":
exts = ["gdshader", "gdshaderinc"]
"image":
exts = ["png", "jpg", "jpeg", "svg", "webp"]
"audio":
exts = ["ogg", "wav", "mp3"]
_:
return _err(-32602, "unknown type: %s (use all|scene|script|resource|shader|image|audio)" % type_filter)
var files: Array = []
_walk("res://", exts, files, include_addons)
if glob != "":
var filtered: Array = []
for f in files:
if f.matchn(glob): # case-insensitive glob match
filtered.append(f)
files = filtered
files.sort()
return _ok({
"type": type_filter,
"count": files.size(),
"files": files,
})
# fs.read_text — read a text file under res://. Complement to user_fs.read.
# Params: {path}
static func read_text(params: Dictionary) -> Dictionary:
var path: String = params.get("path", "")
if path == "":
return _err(-32602, "missing 'path'")
if not path.begins_with("res://"):
return _err(-32602, "path must begin with 'res://' (use user_fs.read for user:// files)")
if not _within_sandbox(path, "res://"):
return _err(-32602, "path escapes the res:// project sandbox ('..' traversal not allowed): %s" % path)
var f := FileAccess.open(path, FileAccess.READ)
if f == null:
return _err(-32001, "failed to open %s (error %d)" % [path, FileAccess.get_open_error()])
var size_bytes := f.get_length()
var content := f.get_as_text()
f.close()
return _ok({"path": path, "content": content, "size_bytes": size_bytes})
# fs.write_text — write a text file under res://. Creates parent directories
# if needed. Triggers the editor's filesystem rescan so the new file shows up
# in the FileSystem dock.
# Params: {path, content, overwrite?: false}
static func write_text(params: Dictionary) -> Dictionary:
var path: String = params.get("path", "")
var content: String = str(params.get("content", ""))
var overwrite: bool = params.get("overwrite", false)
if path == "":
return _err(-32602, "missing 'path'")
if not path.begins_with("res://"):
return _err(-32602, "path must begin with 'res://'")
if not _within_sandbox(path, "res://"):
return _err(-32602, "path escapes the res:// project sandbox ('..' traversal not allowed): %s" % path)
if FileAccess.file_exists(path) and not overwrite:
return _err(-32602, "file exists (pass overwrite:true to replace): %s" % path)
var dir_path := path.get_base_dir()
if not DirAccess.dir_exists_absolute(dir_path):
var derr := DirAccess.make_dir_recursive_absolute(dir_path)
if derr != OK:
return _err(-32001, "mkdir failed (%d): %s" % [derr, dir_path])
var f := FileAccess.open(path, FileAccess.WRITE)
if f == null:
return _err(-32001, "could not open for write: %s" % path)
f.store_string(content)
f.close()
EditorInterface.get_resource_filesystem().update_file(path)
return _ok({"path": path, "bytes_written": content.length()})
static func _walk(dir_path: String, exts: Array, out: Array, include_addons: bool) -> void:
var d := DirAccess.open(dir_path)
if d == null:
return
d.list_dir_begin()
while true:
var name := d.get_next()
if name == "":
break
if name.begins_with("."):
continue
var full := dir_path.path_join(name)
if d.current_is_dir():
if not include_addons and full == "res://addons/agent_tools":
continue # always skip our own addon
_walk(full, exts, out, include_addons)
else:
for e in exts:
if name.ends_with("." + e):
out.append(full)
break
d.list_dir_end()
# Path-traversal guard. `begins_with("res://")` is NOT a sandbox — Godot resolves
# `..` segments against the real filesystem, so "res://../../etc/x" escapes the
# project. Globalize to an absolute path, collapse `..`, and confirm the result
# stays inside the (globalized, collapsed) prefix root.
static func _within_sandbox(path: String, prefix: String) -> bool:
var root := ProjectSettings.globalize_path(prefix).simplify_path().trim_suffix("/")
var abs := ProjectSettings.globalize_path(path).simplify_path()
return abs == root or abs.begins_with(root + "/")
static func _ok(data) -> Dictionary:
return {"data": data}
static func _err(code: int, msg: String) -> Dictionary:
return {"error": {"code": code, "message": msg}}
+1
View File
@@ -0,0 +1 @@
uid://dxq0mo7kx65wd
+243
View File
@@ -0,0 +1,243 @@
@tool
extends RefCounted
# input_map.add_action — register a new input action in project.godot.
# Params: {name, deadzone?: 0.5}
static func add_action(params: Dictionary) -> Dictionary:
var action_name: String = params.get("name", "")
if action_name == "":
return _err(-32602, "missing 'name'")
var deadzone: float = float(params.get("deadzone", 0.5))
var key := "input/" + action_name
if ProjectSettings.has_setting(key):
return _err(-32602, "action already exists: %s" % action_name)
ProjectSettings.set_setting(key, {
"deadzone": deadzone,
"events": [],
})
var err := ProjectSettings.save()
if err != OK:
return _err(-32001, "save failed: %d" % err)
# Keep the runtime InputMap in sync with the on-disk project settings —
# ProjectSettings.save() only writes the file; it doesn't register actions
# with InputMap until editor restart.
if not InputMap.has_action(action_name):
InputMap.add_action(action_name, deadzone)
return _ok({"added": action_name})
# input_map.add_event — attach an input event to an existing action.
# Supported event shapes (pick one via 'type'):
# {type: "key", keycode: "A" | "Space" | "F1" ..., physical?: true}
# {type: "mouse_button", button_index: 1 (left) | 2 (right) | 3 (middle)}
# {type: "joy_button", button_index: 0..}
# Params: {action, event: <shape above>}
static func add_event(params: Dictionary) -> Dictionary:
var action_name: String = params.get("action", "")
var event_spec: Dictionary = params.get("event", {})
if action_name == "":
return _err(-32602, "missing 'action'")
if event_spec.is_empty():
return _err(-32602, "missing 'event'")
var key := "input/" + action_name
if not ProjectSettings.has_setting(key):
return _err(-32001, "action not found: %s" % action_name)
var event := _build_event(event_spec)
if event == null:
return _err(-32602, "invalid event spec — see tool description for supported shapes")
var current: Dictionary = ProjectSettings.get_setting(key)
var events_arr: Array = current.get("events", [])
events_arr.append(event)
current["events"] = events_arr
ProjectSettings.set_setting(key, current)
var err := ProjectSettings.save()
if err != OK:
return _err(-32001, "save failed: %d" % err)
# Sync InputMap so Input.is_action_pressed() etc. see the event without restart.
if InputMap.has_action(action_name):
InputMap.action_add_event(action_name, event)
return _ok({"action": action_name, "event": event_spec})
# input_map.list — return registered input actions with their events.
# Params: {include_builtins?: false}
# Defaults to user-defined only (parses the [input] section of project.godot,
# which contains only user overrides — Godot's ~90 ui_* builtins live in-memory).
# When include_builtins, falls back to InputMap which has the full union.
static func list_actions(params: Dictionary) -> Dictionary:
var include_builtins: bool = params.get("include_builtins", false)
var items: Array = []
if include_builtins:
for a in InputMap.get_actions():
var described: Array = []
for e in InputMap.action_get_events(a):
described.append(_describe_event(e))
items.append({
"name": String(a),
"deadzone": InputMap.action_get_deadzone(a),
"events": described,
})
else:
# Read straight from project.godot so newly-added actions show up even
# if they haven't been registered with the runtime InputMap yet.
var cf := ConfigFile.new()
var err := cf.load("res://project.godot")
if err == OK and cf.has_section("input"):
for action_name in cf.get_section_keys("input"):
var value: Dictionary = cf.get_value("input", action_name)
var described: Array = []
for e in value.get("events", []):
described.append(_describe_event(e))
items.append({
"name": action_name,
"deadzone": value.get("deadzone", 0.5),
"events": described,
})
return _ok({"actions": items})
# input_map.remove_event — remove an event from an action by index.
# Call input_map.list first to see indices; events are listed in the order they
# were added.
# Params: {action, event_index}
static func remove_event(params: Dictionary) -> Dictionary:
var action_name: String = params.get("action", "")
if action_name == "":
return _err(-32602, "missing 'action'")
if not params.has("event_index"):
return _err(-32602, "missing 'event_index'")
var idx: int = int(params.event_index)
var key := "input/" + action_name
if not ProjectSettings.has_setting(key):
return _err(-32001, "action not found: %s" % action_name)
var current: Dictionary = ProjectSettings.get_setting(key)
var events_arr: Array = current.get("events", [])
if idx < 0 or idx >= events_arr.size():
return _err(-32602, "event_index %d out of range (0..%d)" % [idx, events_arr.size() - 1])
var removed_event = events_arr[idx]
events_arr.remove_at(idx)
current["events"] = events_arr
ProjectSettings.set_setting(key, current)
var err := ProjectSettings.save()
if err != OK:
return _err(-32001, "save failed: %d" % err)
if InputMap.has_action(action_name) and removed_event != null:
InputMap.action_erase_event(action_name, removed_event)
return _ok({"action": action_name, "removed_index": idx})
# input_map.remove_action — delete a user-registered action.
static func remove_action(params: Dictionary) -> Dictionary:
var action_name: String = params.get("name", "")
if action_name == "":
return _err(-32602, "missing 'name'")
var key := "input/" + action_name
if not ProjectSettings.has_setting(key):
return _err(-32001, "action not found: %s" % action_name)
ProjectSettings.clear(key)
var err := ProjectSettings.save()
if err != OK:
return _err(-32001, "save failed: %d" % err)
if InputMap.has_action(action_name):
InputMap.erase_action(action_name)
return _ok({"removed": action_name})
static func _build_event(spec: Dictionary) -> InputEvent:
# device defaults to -1 (ALL_DEVICES) matching the editor's "All Devices" default.
# For local multiplayer bind device=0, device=1, etc. to distinguish controllers.
var device: int = int(spec.get("device", -1))
match spec.get("type", ""):
"key":
var e := InputEventKey.new()
e.device = device
var kc_raw = spec.get("keycode", "")
var keycode := 0
if kc_raw is int:
keycode = kc_raw
elif kc_raw is String:
keycode = OS.find_keycode_from_string(kc_raw)
if keycode == 0:
return null
if spec.get("physical", true):
e.physical_keycode = keycode
else:
e.keycode = keycode
return e
"mouse_button":
var e := InputEventMouseButton.new()
e.device = device
e.button_index = int(spec.get("button_index", 1))
return e
"joy_button":
var e := InputEventJoypadButton.new()
e.device = device
e.button_index = int(spec.get("button_index", 0))
return e
"joy_motion":
# axis accepts int 0..5 or one of:
# "left_x" / "left_y" / "right_x" / "right_y" / "trigger_left" / "trigger_right"
# axis_value: -1.0 (full negative) to 1.0 (full positive); sign picks the direction
# that triggers the action.
var e := InputEventJoypadMotion.new()
e.device = device
e.axis = _coerce_axis(spec.get("axis", 0))
e.axis_value = float(spec.get("axis_value", 1.0))
return e
_:
return null
static func _coerce_axis(v) -> int:
if v is int:
return v
if v is String:
match v:
"left_x":
return JOY_AXIS_LEFT_X
"left_y":
return JOY_AXIS_LEFT_Y
"right_x":
return JOY_AXIS_RIGHT_X
"right_y":
return JOY_AXIS_RIGHT_Y
"trigger_left":
return JOY_AXIS_TRIGGER_LEFT
"trigger_right":
return JOY_AXIS_TRIGGER_RIGHT
return 0
static func _describe_event(e) -> Dictionary:
if e is InputEventKey:
var kc: int = e.physical_keycode if e.physical_keycode != 0 else e.keycode
return {
"type": "key",
"keycode": OS.get_keycode_string(kc),
"physical": e.physical_keycode != 0,
"device": e.device,
}
if e is InputEventMouseButton:
return {"type": "mouse_button", "button_index": e.button_index, "device": e.device}
if e is InputEventJoypadButton:
return {"type": "joy_button", "button_index": e.button_index, "device": e.device}
if e is InputEventJoypadMotion:
return {
"type": "joy_motion",
"axis": e.axis,
"axis_value": e.axis_value,
"device": e.device,
}
return {"type": "unknown", "class": (e as Object).get_class() if e else "null"}
static func _ok(data) -> Dictionary:
return {"data": data}
static func _err(code: int, msg: String) -> Dictionary:
return {"error": {"code": code, "message": msg}}
@@ -0,0 +1 @@
uid://dp1m17i4hu7t1
@@ -0,0 +1,56 @@
@tool
extends RefCounted
# performance.monitors — read Godot's built-in performance monitors.
# Params: {monitors?: [string]} — specific monitor names, default = common set
# Returns: {fps, frame_time_ms, process_time_ms, physics_time_ms, mem_static_mb,
# mem_dynamic_mb, objects, resources, nodes, draw_calls, ...}
static func monitors(params: Dictionary) -> Dictionary:
var requested: Array = params.get("monitors", [])
var out: Dictionary = {}
# Default set — the monitors agents actually want for quick perf snapshots.
if requested.is_empty():
out["fps"] = Performance.get_monitor(Performance.TIME_FPS)
out["frame_time_ms"] = Performance.get_monitor(Performance.TIME_PROCESS) * 1000.0
out["physics_time_ms"] = Performance.get_monitor(Performance.TIME_PHYSICS_PROCESS) * 1000.0
out["mem_static_mb"] = Performance.get_monitor(Performance.MEMORY_STATIC) / 1048576.0
out["mem_static_max_mb"] = Performance.get_monitor(Performance.MEMORY_STATIC_MAX) / 1048576.0
out["object_count"] = int(Performance.get_monitor(Performance.OBJECT_COUNT))
out["resource_count"] = int(Performance.get_monitor(Performance.OBJECT_RESOURCE_COUNT))
out["node_count"] = int(Performance.get_monitor(Performance.OBJECT_NODE_COUNT))
out["orphan_node_count"] = int(Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT))
out["draw_calls"] = int(Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME))
else:
for name in requested:
var enum_value := _resolve_monitor_name(String(name))
if enum_value == -1:
out[name] = null
else:
out[name] = Performance.get_monitor(enum_value)
return _ok({"monitors": out})
# Map friendly strings to Performance.* enum values so callers don't have to
# remember the integer IDs.
static func _resolve_monitor_name(name: String) -> int:
match name:
"fps": return Performance.TIME_FPS
"frame_time": return Performance.TIME_PROCESS
"physics_time": return Performance.TIME_PHYSICS_PROCESS
"mem_static": return Performance.MEMORY_STATIC
"mem_static_max": return Performance.MEMORY_STATIC_MAX
"objects": return Performance.OBJECT_COUNT
"resources": return Performance.OBJECT_RESOURCE_COUNT
"nodes": return Performance.OBJECT_NODE_COUNT
"orphan_nodes": return Performance.OBJECT_ORPHAN_NODE_COUNT
"draw_calls": return Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME
"audio_latency": return Performance.AUDIO_OUTPUT_LATENCY
"physics_2d_active_objects": return Performance.PHYSICS_2D_ACTIVE_OBJECTS
"physics_3d_active_objects": return Performance.PHYSICS_3D_ACTIVE_OBJECTS
_: return -1
static func _ok(data) -> Dictionary:
return {"data": data}
@@ -0,0 +1 @@
uid://bijue5qgitvnk
+139
View File
@@ -0,0 +1,139 @@
@tool
extends RefCounted
# physics.autofit_collision_shape_2d — compute a CollisionShape2D sized to a
# sibling/child Sprite2D or AnimatedSprite2D's visual bounds. Common pattern
# that's tedious to set up by hand: you have a sprite, you want a rectangle or
# circle collider that roughly matches it.
#
# Params: {
# node_path: CollisionShape2D to fit (created if missing when create:true)
# source: NodePath to a Sprite2D / AnimatedSprite2D (auto-detected
# among siblings if omitted)
# shape: "rectangle" | "circle" | "capsule" (default "rectangle")
# margin: float pixels to shrink by (default 0)
# create: bool — if true and node_path doesn't exist, create it as
# a child of the source's parent. Default false.
# }
static func autofit_collision_shape_2d(params: Dictionary) -> Dictionary:
var node_path: String = params.get("node_path", "")
var source_path: String = params.get("source", "")
var shape_type: String = params.get("shape", "rectangle")
var margin: float = float(params.get("margin", 0.0))
var create: bool = params.get("create", false)
if node_path == "":
return _err(-32602, "missing 'node_path'")
var root := EditorInterface.get_edited_scene_root()
if root == null:
return _err(-32001, "no scene open")
var target: Node = root if node_path == "." else root.get_node_or_null(node_path)
if target == null and not create:
return _err(-32001, "node not found: %s (pass create:true to auto-create)" % node_path)
# Resolve the visual source.
var source: Node = null
if source_path != "":
source = root.get_node_or_null(source_path)
if source == null:
return _err(-32001, "source not found: %s" % source_path)
elif target != null:
# Look among siblings for a Sprite2D / AnimatedSprite2D.
var parent := target.get_parent()
if parent:
for sib in parent.get_children():
if sib is Sprite2D or sib is AnimatedSprite2D:
source = sib
break
if source == null:
return _err(-32001, "no Sprite2D/AnimatedSprite2D source found — pass 'source'")
# Compute visual bounds (untransformed; CollisionShape2D inherits parent's transform).
var bounds := _sprite_bounds(source)
if bounds.size.x <= 0 or bounds.size.y <= 0:
return _err(-32001, "source has no visible extents (%s)" % source.get_class())
# Create the CollisionShape2D if needed.
if target == null and create:
var inst := CollisionShape2D.new()
inst.name = node_path.get_file() if "/" in node_path else node_path
var parent := source.get_parent()
parent.add_child(inst)
inst.owner = root
target = inst
if not target is CollisionShape2D:
return _err(-32602, "target is not a CollisionShape2D: %s (%s)" % [node_path, target.get_class()])
var cs: CollisionShape2D = target
# Build the requested shape sized to bounds.
var shape_res: Shape2D
match shape_type:
"rectangle":
var r := RectangleShape2D.new()
r.size = Vector2(
max(1.0, bounds.size.x - margin * 2.0),
max(1.0, bounds.size.y - margin * 2.0)
)
shape_res = r
"circle":
var c := CircleShape2D.new()
c.radius = max(1.0, min(bounds.size.x, bounds.size.y) / 2.0 - margin)
shape_res = c
"capsule":
var cap := CapsuleShape2D.new()
cap.radius = max(1.0, min(bounds.size.x, bounds.size.y) / 2.0 - margin)
cap.height = max(cap.radius * 2.0 + 1.0, bounds.size.y - margin * 2.0)
shape_res = cap
_:
return _err(-32602, "unknown shape '%s' (supported: rectangle, circle, capsule)" % shape_type)
cs.shape = shape_res
# Center the CollisionShape2D on the sprite's center.
cs.position = bounds.position + bounds.size / 2.0
EditorInterface.mark_scene_as_unsaved()
return _ok({
"node_path": String(root.get_path_to(cs)),
"shape": shape_type,
"size": [bounds.size.x - margin * 2.0, bounds.size.y - margin * 2.0],
"center": [cs.position.x, cs.position.y],
"source": String(root.get_path_to(source)),
})
static func _sprite_bounds(source: Node) -> Rect2:
if source is Sprite2D:
var s: Sprite2D = source
if s.texture == null:
return Rect2(Vector2.ZERO, Vector2.ZERO)
var tex_size: Vector2 = s.texture.get_size()
if s.region_enabled:
tex_size = s.region_rect.size
var size: Vector2 = tex_size * Vector2(abs(s.scale.x), abs(s.scale.y))
# For centered sprites, bounds are at (-size/2, -size/2).
var offset: Vector2 = -size / 2.0 if s.centered else Vector2.ZERO
offset += s.offset
return Rect2(offset, size)
if source is AnimatedSprite2D:
var a: AnimatedSprite2D = source
if a.sprite_frames == null or a.animation == "":
return Rect2(Vector2.ZERO, Vector2.ZERO)
var tex := a.sprite_frames.get_frame_texture(a.animation, a.frame)
if tex == null:
return Rect2(Vector2.ZERO, Vector2.ZERO)
var size: Vector2 = tex.get_size() * Vector2(abs(a.scale.x), abs(a.scale.y))
var offset: Vector2 = -size / 2.0 if a.centered else Vector2.ZERO
offset += a.offset
return Rect2(offset, size)
return Rect2(Vector2.ZERO, Vector2.ZERO)
static func _ok(data) -> Dictionary:
return {"data": data}
static func _err(code: int, msg: String) -> Dictionary:
return {"error": {"code": code, "message": msg}}
@@ -0,0 +1 @@
uid://c26p52t0pcj6q
+89
View File
@@ -0,0 +1,89 @@
@tool
extends RefCounted
# project.get_setting — read a value from project.godot. Returns {exists: false} if unset.
# Params: {key: "application/config/name"}
static func get_setting(params: Dictionary) -> Dictionary:
var key: String = params.get("key", "")
if key == "":
return _err(-32602, "missing 'key'")
if not ProjectSettings.has_setting(key):
return _ok({"key": key, "exists": false})
return _ok({"key": key, "exists": true, "value": ProjectSettings.get_setting(key)})
# project.set_setting — write a value to project.godot and save.
# DESTRUCTIVE: mutates project.godot. Prefer targeted tools (autoload.add, etc.) when available.
# Params: {key, value}
static func set_setting(params: Dictionary) -> Dictionary:
var key: String = params.get("key", "")
if key == "":
return _err(-32602, "missing 'key'")
if not params.has("value"):
return _err(-32602, "missing 'value'")
ProjectSettings.set_setting(key, params["value"])
var err := ProjectSettings.save()
if err != OK:
return _err(-32001, "ProjectSettings.save() failed: %d" % err)
return _ok({"key": key, "value": params["value"]})
# autoload.add — register an autoload (singleton by default).
# Params: {name, path, singleton?: true}
static func autoload_add(params: Dictionary) -> Dictionary:
var autoload_name: String = params.get("name", "")
var path: String = params.get("path", "")
var singleton: bool = params.get("singleton", true)
if autoload_name == "" or path == "":
return _err(-32602, "missing 'name' or 'path'")
if not ResourceLoader.exists(path):
return _err(-32001, "path does not exist: %s" % path)
var value := ("*" + path) if singleton else path
ProjectSettings.set_setting("autoload/" + autoload_name, value)
var err := ProjectSettings.save()
if err != OK:
return _err(-32001, "save failed: %d" % err)
return _ok({"name": autoload_name, "path": path, "singleton": singleton})
# autoload.remove — unregister an autoload.
# Params: {name}
static func autoload_remove(params: Dictionary) -> Dictionary:
var autoload_name: String = params.get("name", "")
if autoload_name == "":
return _err(-32602, "missing 'name'")
var key := "autoload/" + autoload_name
if not ProjectSettings.has_setting(key):
return _err(-32001, "autoload not found: %s" % autoload_name)
ProjectSettings.clear(key)
var err := ProjectSettings.save()
if err != OK:
return _err(-32001, "save failed: %d" % err)
return _ok({"removed": autoload_name})
# autoload.list — enumerate all registered autoloads.
static func autoload_list(_params: Dictionary) -> Dictionary:
var items: Array = []
# ProjectSettings.get_property_list() contains our autoload entries under "autoload/*".
for p in ProjectSettings.get_property_list():
var name: String = p.name
if not name.begins_with("autoload/"):
continue
var value: String = str(ProjectSettings.get_setting(name))
var singleton := value.begins_with("*")
items.append({
"name": name.trim_prefix("autoload/"),
"path": value.trim_prefix("*") if singleton else value,
"singleton": singleton,
})
return _ok({"autoloads": items})
static func _ok(data) -> Dictionary:
return {"data": data}
static func _err(code: int, msg: String) -> Dictionary:
return {"error": {"code": code, "message": msg}}
@@ -0,0 +1 @@
uid://bsfbwkk5nas36
+371
View File
@@ -0,0 +1,371 @@
@tool
extends RefCounted
# refs.validate_project — project-wide sweep. No params.
# Returns {checked: int, issues: [{path, kind, ...}]}.
# Kinds: script_parse_error, scene_load_failed, scene_instantiate_failed,
# resource_load_failed, missing_ext_resource, missing_uid,
# signal_method_missing, signal_target_missing.
static func validate_project(_params: Dictionary) -> Dictionary:
var issues: Array = []
var files: Array = []
_walk("res://", ["gd", "tscn", "tres"], files)
for path in files:
if path.begins_with("res://addons/agent_tools/"):
continue # don't validate ourselves
if path.ends_with(".gd"):
_check_script(path, issues)
elif path.ends_with(".tscn"):
_check_scene(path, issues)
elif path.ends_with(".tres"):
_check_resource(path, issues)
return {"data": {"checked": files.size(), "issues": issues}}
static func _walk(dir_path: String, exts: Array, out: Array) -> void:
var d := DirAccess.open(dir_path)
if d == null:
return
d.list_dir_begin()
while true:
var name := d.get_next()
if name == "":
break
if name.begins_with("."):
continue
var full := dir_path.path_join(name)
if d.current_is_dir():
_walk(full, exts, out)
else:
for e in exts:
if name.ends_with("." + e):
out.append(full)
break
d.list_dir_end()
static func _check_script(path: String, issues: Array) -> void:
# Go through the normal loader so class_name / preload chains resolve against
# the real global registry. A detached GDScript.new() + reload() would false-positive
# on any script with class_name (duplicate global class registration).
var script := ResourceLoader.load(path, "Script")
if script == null:
issues.append({"path": path, "kind": "script_load_failed"})
static func _check_scene(path: String, issues: Array) -> void:
_check_ext_resources_text(path, issues)
var packed := ResourceLoader.load(path, "PackedScene") as PackedScene
if packed == null:
issues.append({"path": path, "kind": "scene_load_failed"})
return
var root := packed.instantiate(PackedScene.GEN_EDIT_STATE_DISABLED)
if root == null:
issues.append({"path": path, "kind": "scene_instantiate_failed"})
return
_walk_scene_signals(root, root, path, issues)
root.free()
static func _walk_scene_signals(node: Node, scene_root: Node, scene_path: String, issues: Array) -> void:
for sig in node.get_signal_list():
var sig_name: String = sig.name
for c in node.get_signal_connection_list(sig_name):
var flags: int = c.flags
if not (flags & CONNECT_PERSIST):
continue # ignore runtime-only connections
var cb: Callable = c.callable
var target: Object = cb.get_object()
var method: String = cb.get_method()
if target == null:
issues.append({
"path": scene_path,
"kind": "signal_target_missing",
"node": String(scene_root.get_path_to(node)),
"signal": sig_name,
})
elif not target.has_method(method):
issues.append({
"path": scene_path,
"kind": "signal_method_missing",
"node": String(scene_root.get_path_to(node)),
"signal": sig_name,
"method": method,
})
for child in node.get_children():
if child.owner == scene_root or child == scene_root:
_walk_scene_signals(child, scene_root, scene_path, issues)
static func _check_resource(path: String, issues: Array) -> void:
var res := ResourceLoader.load(path)
if res == null:
issues.append({"path": path, "kind": "resource_load_failed"})
# refs.find_usages — find every file that references a given resource.
# Target may be either a path ("res://player.gd") or a uid ("uid://abc123").
# Searches for both forms so you catch uid-indirected references too.
# Params: {target: "res://..." | "uid://..."}
static func find_usages(params: Dictionary) -> Dictionary:
var target: String = params.get("target", "")
if target == "":
return _err(-32602, "missing 'target'")
var terms: Array = [target]
if target.begins_with("uid://"):
var id := ResourceUID.text_to_id(target)
if id != -1 and ResourceUID.has_id(id):
var p := ResourceUID.get_id_path(id)
if p != "" and p != target:
terms.append(p)
else:
var id := ResourceLoader.get_resource_uid(target)
if id != -1:
terms.append(ResourceUID.id_to_text(id))
var files: Array = []
_walk("res://", ["gd", "tscn", "tres", "cfg", "gdshader", "gdshaderinc", "cs", "godot"], files)
var matches: Array = []
for path in files:
if path.begins_with("res://addons/agent_tools/"):
continue
if path == target:
continue # skip the file itself
var f := FileAccess.open(path, FileAccess.READ)
if f == null:
continue
var line_num := 0
while not f.eof_reached():
line_num += 1
var line := f.get_line()
for term in terms:
if term in line:
matches.append({
"path": path,
"line": line_num,
"matched": term,
"text": line.strip_edges(),
})
break
return _ok({"target": target, "terms_searched": terms, "matches": matches})
# refs.rename — move a file and rewrite all references to it.
# Path-form references are text-replaced; uid-form references keep working because
# the .uid sidecar moves with the file (preserving the UID → new path mapping).
# Params: {from, to, overwrite?: false, dry_run?: false}
static func rename(params: Dictionary) -> Dictionary:
var from_path: String = params.get("from", "")
var to_path: String = params.get("to", "")
var overwrite: bool = params.get("overwrite", false)
var dry_run: bool = params.get("dry_run", false)
if from_path == "" or to_path == "":
return _err(-32602, "missing 'from' or 'to'")
if not (from_path.begins_with("res://") and to_path.begins_with("res://")):
return _err(-32602, "paths must be 'res://...'")
if from_path == to_path:
return _err(-32602, "'from' and 'to' are the same")
if not FileAccess.file_exists(from_path):
return _err(-32001, "source not found: %s" % from_path)
if FileAccess.file_exists(to_path) and not overwrite:
return _err(-32602, "destination exists (pass overwrite:true): %s" % to_path)
# Collect path-form usages. UID-form references are preserved automatically when
# the .uid sidecar moves with the file, so we don't need to rewrite them.
var usage := find_usages({"target": from_path})
if usage.has("error"):
return usage
var edits: Dictionary = {}
for m in usage.data.matches:
if m.matched != from_path:
continue # uid-form match — no rewrite needed
if not edits.has(m.path):
edits[m.path] = true
if dry_run:
return _ok({
"would_move": {"from": from_path, "to": to_path},
"would_update": edits.keys(),
})
var dir_path := to_path.get_base_dir()
if not DirAccess.dir_exists_absolute(dir_path):
var derr := DirAccess.make_dir_recursive_absolute(dir_path)
if derr != OK:
return _err(-32001, "mkdir failed (%d): %s" % [derr, dir_path])
var move_err := DirAccess.rename_absolute(from_path, to_path)
if move_err != OK:
return _err(-32001, "move failed (%d)" % move_err)
# Move .uid sidecar so UIDs continue resolving to the new path. Best-effort —
# sidecar failures don't block the main rename that already succeeded.
var from_uid := from_path + ".uid"
if FileAccess.file_exists(from_uid):
@warning_ignore("return_value_discarded")
DirAccess.rename_absolute(from_uid, to_path + ".uid")
# Move .import sidecar for imported assets (textures, audio, etc.).
var from_import := from_path + ".import"
if FileAccess.file_exists(from_import):
@warning_ignore("return_value_discarded")
DirAccess.rename_absolute(from_import, to_path + ".import")
var files_updated: Array = []
for ref_path in edits.keys():
var rf := FileAccess.open(ref_path, FileAccess.READ)
if rf == null:
continue
var content := rf.get_as_text()
rf.close()
var new_content := content.replace(from_path, to_path)
if new_content == content:
continue
var wf := FileAccess.open(ref_path, FileAccess.WRITE)
if wf == null:
continue
wf.store_string(new_content)
wf.close()
files_updated.append(ref_path)
EditorInterface.get_resource_filesystem().scan()
return _ok({
"moved": {"from": from_path, "to": to_path},
"files_updated": files_updated,
})
# refs.rename_class — rename `class_name X` to `class_name Y` and rewrite every
# word-boundary reference to X across .gd / .tscn / .tres files. Best-effort:
# won't rewrite an X that happens to be a local variable name (word boundary
# alone isn't semantic analysis). Use dry_run first on anything non-trivial.
# Params: {from, to, dry_run?: false}
static func rename_class(params: Dictionary) -> Dictionary:
var from_name: String = params.get("from", "")
var to_name: String = params.get("to", "")
var dry_run: bool = params.get("dry_run", false)
if from_name == "" or to_name == "":
return _err(-32602, "missing 'from' or 'to'")
if not _is_valid_identifier(from_name):
return _err(-32602, "'from' is not a valid identifier: %s" % from_name)
if not _is_valid_identifier(to_name):
return _err(-32602, "'to' is not a valid identifier: %s" % to_name)
if from_name == to_name:
return _err(-32602, "'from' and 'to' are identical")
var defining_script := _find_class_definition(from_name)
if defining_script == "":
return _err(-32001, "no script found declaring class_name %s" % from_name)
if _find_class_definition(to_name) != "":
return _err(-32602, "class_name %s already exists" % to_name)
var files: Array = []
_walk("res://", ["gd", "tscn", "tres"], files)
var regex := RegEx.new()
regex.compile("\\b%s\\b" % from_name)
var edits: Array = []
for path in files:
if path.begins_with("res://addons/agent_tools/"):
continue
var f := FileAccess.open(path, FileAccess.READ)
if f == null:
continue
var text := f.get_as_text()
f.close()
if regex.search(text) != null:
edits.append(path)
if dry_run:
return _ok({
"from": from_name,
"to": to_name,
"defining_script": defining_script,
"would_update": edits,
})
var updated: Array = []
for path in edits:
var f := FileAccess.open(path, FileAccess.READ)
if f == null:
continue
var text := f.get_as_text()
f.close()
var new_text := regex.sub(text, to_name, true)
if new_text == text:
continue
var wf := FileAccess.open(path, FileAccess.WRITE)
if wf == null:
continue
wf.store_string(new_text)
wf.close()
updated.append(path)
EditorInterface.get_resource_filesystem().scan()
return _ok({
"from": from_name,
"to": to_name,
"defining_script": defining_script,
"updated": updated,
})
static func _is_valid_identifier(s: String) -> bool:
var re := RegEx.new()
re.compile("^[A-Za-z_][A-Za-z0-9_]*$")
return re.search(s) != null
static func _find_class_definition(class_name_val: String) -> String:
var files: Array = []
_walk("res://", ["gd"], files)
var re := RegEx.new()
re.compile("^\\s*class_name\\s+%s\\b" % class_name_val)
for path in files:
if path.begins_with("res://addons/agent_tools/"):
continue
var f := FileAccess.open(path, FileAccess.READ)
if f == null:
continue
var text := f.get_as_text()
f.close()
for line in text.split("\n"):
if re.search(line) != null:
return path
return ""
static func _ok(data) -> Dictionary:
return {"data": data}
static func _err(code: int, msg: String) -> Dictionary:
return {"error": {"code": code, "message": msg}}
# Parse the raw .tscn/.tres text for [ext_resource ... path="..."] and verify each target resolves.
# Catches references to files that were deleted or moved without updating the scene.
static func _check_ext_resources_text(path: String, issues: Array) -> void:
var f := FileAccess.open(path, FileAccess.READ)
if f == null:
return
var text := f.get_as_text()
var regex := RegEx.new()
regex.compile("\\[ext_resource[^\\]]*path=\"([^\"]+)\"")
for m in regex.search_all(text):
var ref: String = m.get_string(1)
if ref.begins_with("uid://"):
var id := ResourceUID.text_to_id(ref)
if id == -1 or not ResourceUID.has_id(id):
issues.append({"path": path, "kind": "missing_uid", "uid": ref})
continue
if not ResourceLoader.exists(ref):
issues.append({"path": path, "kind": "missing_ext_resource", "target": ref})
Loaded 100 of 251 files, more files were not shown because too many files have changed in this diff. Show more