@tool extends RefCounted const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd") ## Handles Material authoring: creating .tres files, setting BaseMaterial3D ## properties / shader uniforms, assigning to nodes, high-level presets. ## ## File-resource lifecycle mirrors ThemeHandler (create/load/mutate/save). ## Undo pattern mirrors AnimationHandler (single create_action bundles ## every dependency spawn). const MaterialValues := preload("res://addons/godot_ai/handlers/material_values.gd") const MaterialPresets := preload("res://addons/godot_ai/handlers/material_presets.gd") const _TYPE_TO_CLASS := { "standard": "StandardMaterial3D", "orm": "ORMMaterial3D", "canvas_item": "CanvasItemMaterial", "shader": "ShaderMaterial", } const _SUPPORTED_SUFFIXES := [".tres", ".material", ".res"] var _undo_redo: EditorUndoRedoManager func _init(undo_redo: EditorUndoRedoManager) -> void: _undo_redo = undo_redo # ============================================================================ # material_create # ============================================================================ func create_material(params: Dictionary) -> Dictionary: var path: String = params.get("path", "") var type_str: String = params.get("type", "standard") var shader_path: String = params.get("shader_path", "") var overwrite: bool = params.get("overwrite", false) var err := _validate_material_path(path, "path", true) if err != null: return err if not _TYPE_TO_CLASS.has(type_str): return ErrorCodes.make( ErrorCodes.VALUE_OUT_OF_RANGE, "Invalid material type '%s'. Valid: %s" % [type_str, ", ".join(_TYPE_TO_CLASS.keys())] ) var existed_before := FileAccess.file_exists(path) if existed_before and not overwrite: return ErrorCodes.make( ErrorCodes.INVALID_PARAMS, "Material already exists at %s (pass overwrite=true to replace)" % path ) var mat := _instantiate_material(type_str) if mat == null: return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to instantiate material") if type_str == "shader": if shader_path.is_empty(): return ErrorCodes.make( ErrorCodes.INVALID_PARAMS, "ShaderMaterial requires shader_path (res:// / uid:// / user:// path to a .gdshader)" ) var shader_path_err = McpPathValidator.loadable_error(shader_path, "shader_path") if shader_path_err != null: return shader_path_err if not ResourceLoader.exists(shader_path): return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Shader not found: %s" % shader_path) var shader_res := ResourceLoader.load(shader_path) if not (shader_res is Shader): return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "Resource at %s is not a Shader" % shader_path) (mat as ShaderMaterial).shader = shader_res var dir_path := path.get_base_dir() var mkdir_err := DirAccess.make_dir_recursive_absolute(dir_path) if mkdir_err != OK and mkdir_err != ERR_ALREADY_EXISTS: return ErrorCodes.make( ErrorCodes.INTERNAL_ERROR, "Failed to create directory: %s (error %d)" % [dir_path, mkdir_err] ) var save_err := ResourceSaver.save(mat, path) if save_err != OK: return ErrorCodes.make( ErrorCodes.INTERNAL_ERROR, "Failed to save material to %s (error %d)" % [path, save_err] ) var efs := EditorInterface.get_resource_filesystem() if efs != null: efs.update_file(path) return { "data": { "path": path, "type": type_str, "class": mat.get_class(), "shader_path": shader_path, "overwritten": existed_before, "undoable": false, "reason": "File creation is persistent; delete the file manually to revert", } } # ============================================================================ # material_set_param # ============================================================================ func set_param(params: Dictionary) -> Dictionary: var load_result := _load_material_from_path(params.get("path", ""), true) if load_result.has("error"): return load_result var mat: Material = load_result.material var mat_path: String = load_result.path var property: String = params.get("param", "") if property.is_empty(): return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: param") if not ("value" in params): return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: value") var raw_value = params.get("value") # Probe the property. We allow any property present in get_property_list, # plus `shader` on ShaderMaterial. var prop_type: int = TYPE_NIL var property_exists := false for prop in mat.get_property_list(): if prop.name == property: property_exists = true prop_type = prop.get("type", TYPE_NIL) break if not property_exists: return ErrorCodes.make( ErrorCodes.PROPERTY_NOT_ON_CLASS, McpPropertyErrors.build_message(mat, property) ) var coerced := MaterialValues.coerce_material_value(property, raw_value, prop_type) if not coerced.ok: return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, String(coerced.error)) var new_value = coerced.value var old_value = mat.get(property) _undo_redo.create_action("MCP: Set material %s.%s" % [mat_path.get_file(), property]) _undo_redo.add_do_method(self, "_apply_param", mat_path, property, new_value, false) _undo_redo.add_undo_method(self, "_apply_param", mat_path, property, old_value, false) _undo_redo.commit_action() return { "data": { "path": mat_path, "property": property, "value": MaterialValues.serialize_value(new_value), "previous_value": MaterialValues.serialize_value(old_value), "undoable": true, } } # ============================================================================ # material_set_shader_param # ============================================================================ func set_shader_param(params: Dictionary) -> Dictionary: var load_result := _load_material_from_path(params.get("path", ""), true) if load_result.has("error"): return load_result var mat: Material = load_result.material var mat_path: String = load_result.path if not (mat is ShaderMaterial): return ErrorCodes.make( ErrorCodes.WRONG_TYPE, "Material at %s is %s, not ShaderMaterial" % [mat_path, mat.get_class()] ) var shader_mat := mat as ShaderMaterial if shader_mat.shader == null: return ErrorCodes.make( ErrorCodes.WRONG_TYPE, "ShaderMaterial at %s has no shader assigned" % mat_path ) var param_name: String = params.get("param", "") if param_name.is_empty(): return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: param") if not ("value" in params): return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: value") # Verify the uniform exists in the shader. var uniform_type := _shader_uniform_type(shader_mat.shader, param_name) if uniform_type == TYPE_NIL: return ErrorCodes.make( ErrorCodes.PROPERTY_NOT_ON_CLASS, "Shader uniform '%s' not declared on shader at %s" % [param_name, shader_mat.shader.resource_path] ) var raw_value = params.get("value") var coerced := MaterialValues.coerce_material_value(param_name, raw_value, uniform_type) if not coerced.ok: return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, String(coerced.error)) var new_value = coerced.value var old_value = shader_mat.get_shader_parameter(param_name) _undo_redo.create_action("MCP: Set shader param %s.%s" % [mat_path.get_file(), param_name]) _undo_redo.add_do_method(self, "_apply_shader_param", mat_path, param_name, new_value) _undo_redo.add_undo_method(self, "_apply_shader_param", mat_path, param_name, old_value) _undo_redo.commit_action() return { "data": { "path": mat_path, "param": param_name, "value": MaterialValues.serialize_value(new_value), "previous_value": MaterialValues.serialize_value(old_value), "undoable": true, } } # ============================================================================ # material_get # ============================================================================ func get_material(params: Dictionary) -> Dictionary: var load_result := _load_material_from_path(params.get("path", "")) if load_result.has("error"): return load_result var mat: Material = load_result.material var mat_path: String = load_result.path var properties: Array[Dictionary] = [] for prop in mat.get_property_list(): var usage: int = prop.get("usage", 0) if not (usage & PROPERTY_USAGE_EDITOR): continue var name: String = prop.name if name.begins_with("shader_parameter/"): continue # handled below var value = mat.get(name) if value == null and prop.type != TYPE_NIL: continue properties.append({ "name": name, "type": type_string(prop.type), "value": MaterialValues.serialize_value(value), }) var shader_params: Array[Dictionary] = [] if mat is ShaderMaterial: var shader_mat := mat as ShaderMaterial if shader_mat.shader != null: for u in shader_mat.shader.get_shader_uniform_list(): var u_name: String = u.get("name", "") if u_name.is_empty(): continue shader_params.append({ "name": u_name, "type": type_string(u.get("type", TYPE_NIL)), "value": MaterialValues.serialize_value(shader_mat.get_shader_parameter(u_name)), }) var reverse_type_map := _reverse_type_map() var shader_path_str := "" if mat is ShaderMaterial: var sm := mat as ShaderMaterial if sm.shader != null: shader_path_str = sm.shader.resource_path return { "data": { "path": mat_path, "class": mat.get_class(), "type": reverse_type_map.get(mat.get_class(), ""), "properties": properties, "property_count": properties.size(), "shader_parameters": shader_params, "shader_path": shader_path_str, } } # ============================================================================ # material_list # ============================================================================ func list_materials(params: Dictionary) -> Dictionary: var root: String = params.get("root", "res://") var type_filter: String = params.get("type", "") var root_err = McpPathValidator.path_error(root, "root") if root_err != null: return root_err var efs := EditorInterface.get_resource_filesystem() if efs == null: return ErrorCodes.make(ErrorCodes.EDITOR_NOT_READY, "EditorFileSystem not available") var results: Array[Dictionary] = [] var start_dir := efs.get_filesystem_path(root) if start_dir == null: start_dir = efs.get_filesystem() _scan_materials(start_dir, type_filter, root, results) return {"data": {"materials": results, "count": results.size()}} func _scan_materials(dir: EditorFileSystemDirectory, type_filter: String, root: String, out: Array[Dictionary]) -> void: if dir == null: return for i in dir.get_file_count(): var file_path := dir.get_file_path(i) if not file_path.begins_with(root): continue var file_type := dir.get_file_type(i) var is_material := file_type == "Material" or ClassDB.is_parent_class(file_type, "Material") if not is_material: # Some material variants serialize as specific classes. if not (file_type in _TYPE_TO_CLASS.values()): continue if not type_filter.is_empty(): if file_type != type_filter and not ClassDB.is_parent_class(file_type, type_filter): continue out.append({"path": file_path, "class": file_type}) for i in dir.get_subdir_count(): _scan_materials(dir.get_subdir(i), type_filter, root, out) # ============================================================================ # material_assign # ============================================================================ func assign_material(params: Dictionary) -> Dictionary: var node_path: String = params.get("node_path", "") if node_path.is_empty(): return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: node_path") var _resolved := McpNodeValidator.resolve_or_error(node_path, "node_path") if _resolved.has("error"): return _resolved var node: Node = _resolved.node var _scene_root: Node = _resolved.scene_root var slot: String = params.get("slot", "override") var resource_path: String = params.get("resource_path", "") var create_if_missing: bool = params.get("create_if_missing", false) var type_str: String = params.get("type", "standard") var slot_result := _resolve_slot_property(node, slot) if slot_result.has("error"): return slot_result var property: String = slot_result.property # Load or create the material. var mat: Material = null var material_created := false if not resource_path.is_empty(): var rpath_err = McpPathValidator.loadable_error(resource_path, "resource_path") if rpath_err != null: return rpath_err if not ResourceLoader.exists(resource_path): if create_if_missing: # We'd need to create a new file here — refuse; callers should # use material_create first or omit resource_path to get an # inline material. return ErrorCodes.make( ErrorCodes.RESOURCE_NOT_FOUND, "Resource not found: %s. Create it first with material_create or omit resource_path for an inline material." % resource_path ) return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Resource not found: %s" % resource_path) var loaded := ResourceLoader.load(resource_path) if not (loaded is Material): var loaded_class := "null" if loaded != null: loaded_class = loaded.get_class() return ErrorCodes.make( ErrorCodes.WRONG_TYPE, "Resource at %s is not a Material (got %s)" % [resource_path, loaded_class] ) mat = loaded else: if not create_if_missing: return ErrorCodes.make( ErrorCodes.INVALID_PARAMS, "Missing resource_path (pass create_if_missing=true to create a new inline material)" ) if not _TYPE_TO_CLASS.has(type_str): return ErrorCodes.make( ErrorCodes.VALUE_OUT_OF_RANGE, "Invalid material type '%s'" % type_str ) mat = _instantiate_material(type_str) material_created = true var old_value = node.get(property) _undo_redo.create_action("MCP: Assign material to %s.%s" % [node.name, property]) _undo_redo.add_do_property(node, property, mat) _undo_redo.add_undo_property(node, property, old_value) if material_created: _undo_redo.add_do_reference(mat) _undo_redo.commit_action() return { "data": { "node_path": node_path, "property": property, "slot": slot, "resource_path": resource_path, "material_class": mat.get_class(), "material_created": material_created, "undoable": true, } } # ============================================================================ # material_apply_to_node # ============================================================================ func apply_to_node(params: Dictionary) -> Dictionary: var node_path: String = params.get("node_path", "") if node_path.is_empty(): return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: node_path") var type_str: String = params.get("type", "standard") if not _TYPE_TO_CLASS.has(type_str): return ErrorCodes.make( ErrorCodes.VALUE_OUT_OF_RANGE, "Invalid material type '%s'. Valid: %s" % [type_str, ", ".join(_TYPE_TO_CLASS.keys())] ) var _resolved := McpNodeValidator.resolve_or_error(node_path, "node_path") if _resolved.has("error"): return _resolved var node: Node = _resolved.node var _scene_root: Node = _resolved.scene_root var slot: String = params.get("slot", "override") var slot_result := _resolve_slot_property(node, slot) if slot_result.has("error"): return slot_result var property: String = slot_result.property var mat := _instantiate_material(type_str) var props_to_set: Dictionary = params.get("params", {}) var applied: Array[String] = [] for prop_name in props_to_set: var apply_err := _apply_one_param_on_instance(mat, String(prop_name), props_to_set[prop_name]) if apply_err != null: return apply_err applied.append(String(prop_name)) var save_to: String = params.get("save_to", "") var saved := false if not save_to.is_empty(): var save_err_validation := _validate_material_path(save_to, "save_to", true) if save_err_validation != null: return save_err_validation var dir_path := save_to.get_base_dir() var mkdir_err := DirAccess.make_dir_recursive_absolute(dir_path) if mkdir_err != OK and mkdir_err != ERR_ALREADY_EXISTS: return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to create directory: %s" % dir_path) var save_err := ResourceSaver.save(mat, save_to) if save_err != OK: return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to save material to %s (error %d)" % [save_to, save_err]) var efs := EditorInterface.get_resource_filesystem() if efs != null: efs.update_file(save_to) # Prefer the on-disk reference (keeps the scene ref small), but fall # back to the in-memory material if the reload fails — otherwise a null # would clear the slot and crash mat.get_class() below. var reloaded := ResourceLoader.load(save_to) if reloaded != null: mat = reloaded saved = true var old_value = node.get(property) _undo_redo.create_action("MCP: Apply %s material to %s" % [type_str, node.name]) _undo_redo.add_do_property(node, property, mat) _undo_redo.add_undo_property(node, property, old_value) _undo_redo.add_do_reference(mat) _undo_redo.commit_action() return { "data": { "node_path": node_path, "property": property, "slot": slot, "type": type_str, "class": mat.get_class(), "applied_params": applied, "material_created": true, "saved_to": save_to if saved else "", "undoable": true, } } # ============================================================================ # material_apply_preset # ============================================================================ func apply_preset(params: Dictionary) -> Dictionary: var preset_name: String = params.get("preset", "") if preset_name.is_empty(): return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: preset") var overrides: Dictionary = params.get("overrides", {}) var blueprint = MaterialPresets.build(preset_name, overrides) if blueprint == null: return ErrorCodes.make( ErrorCodes.VALUE_OUT_OF_RANGE, "Unknown preset '%s'. Valid: %s" % [preset_name, ", ".join(MaterialPresets.list())] ) var type_str: String = blueprint.get("type", "standard") var preset_params: Dictionary = blueprint.get("params", {}) var path: String = params.get("path", "") var node_path: String = params.get("node_path", "") if path.is_empty() and node_path.is_empty(): return ErrorCodes.make( ErrorCodes.MISSING_REQUIRED_PARAM, "Pass at least one of: path (save to disk), node_path (assign to node)" ) # If both path and node_path, save to disk, then assign the saved resource. # If only path, save to disk. # If only node_path, inline material via apply_to_node. if not node_path.is_empty() and path.is_empty(): # Inline var inline_result := apply_to_node({ "node_path": node_path, "type": type_str, "params": preset_params, "slot": params.get("slot", "override"), }) if inline_result.has("data"): inline_result.data["preset"] = preset_name inline_result.data["assigned"] = true inline_result.data["path"] = "" inline_result.data["saved_to_disk"] = false inline_result.data["reason"] = "Inline material assigned to node" return inline_result # Save-to-disk path. var existed_before := FileAccess.file_exists(path) if existed_before and not params.get("overwrite", false): return ErrorCodes.make( ErrorCodes.INVALID_PARAMS, "Material already exists at %s (pass overwrite=true to replace)" % path ) var path_err := _validate_material_path(path, "path", true) if path_err != null: return path_err var mat := _instantiate_material(type_str) for prop_name in preset_params: var apply_err := _apply_one_param_on_instance(mat, String(prop_name), preset_params[prop_name]) if apply_err != null: return apply_err var dir_path := path.get_base_dir() var mkdir_err := DirAccess.make_dir_recursive_absolute(dir_path) if mkdir_err != OK and mkdir_err != ERR_ALREADY_EXISTS: return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to create directory: %s" % dir_path) var save_err := ResourceSaver.save(mat, path) if save_err != OK: return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Failed to save material: %s" % path) var efs := EditorInterface.get_resource_filesystem() if efs != null: efs.update_file(path) var assigned := false if not node_path.is_empty(): var _resolved := McpNodeValidator.resolve_or_error(node_path, "node_path") if _resolved.has("error"): return _resolved var node: Node = _resolved.node var _scene_root: Node = _resolved.scene_root var slot_result := _resolve_slot_property(node, params.get("slot", "override")) if slot_result.has("error"): return slot_result var property: String = slot_result.property var saved_mat := ResourceLoader.load(path) var old_value = node.get(property) _undo_redo.create_action("MCP: Apply preset %s to %s" % [preset_name, node.name]) _undo_redo.add_do_property(node, property, saved_mat) _undo_redo.add_undo_property(node, property, old_value) _undo_redo.commit_action() assigned = true return { "data": { "preset": preset_name, "type": type_str, "path": path, "node_path": node_path, "material_created": true, "assigned": assigned, "saved_to_disk": true, "undoable": assigned, # assign is undoable; save is not "reason": "" if assigned else "File save is not undoable", } } # ============================================================================ # Undo-callable: applies a param on the loaded resource and saves. # ============================================================================ func _apply_param(mat_path: String, property: String, value: Variant, _is_shader: bool) -> void: var mat: Material = ResourceLoader.load(mat_path) if mat == null: return mat.set(property, value) ResourceSaver.save(mat, mat_path) func _apply_shader_param(mat_path: String, param_name: String, value: Variant) -> void: var mat: Material = ResourceLoader.load(mat_path) if mat == null or not (mat is ShaderMaterial): return (mat as ShaderMaterial).set_shader_parameter(param_name, value) ResourceSaver.save(mat, mat_path) # ============================================================================ # Helpers # ============================================================================ static func _instantiate_material(type_str: String) -> Material: match type_str: "standard": return StandardMaterial3D.new() "orm": return ORMMaterial3D.new() "canvas_item": return CanvasItemMaterial.new() "shader": return ShaderMaterial.new() return null static func _reverse_type_map() -> Dictionary: var out := {} for k in _TYPE_TO_CLASS: out[_TYPE_TO_CLASS[k]] = k return out static func _validate_material_path(path: String, param_name: String, for_write: bool = false) -> Variant: if path.is_empty(): return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: %s" % param_name) var path_err := McpPathValidator.validate_resource_path(path, for_write) if not path_err.is_empty(): return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "%s: %s" % [param_name, path_err]) var has_suffix := false for s in _SUPPORTED_SUFFIXES: if path.ends_with(s): has_suffix = true break if not has_suffix: return ErrorCodes.make( ErrorCodes.VALUE_OUT_OF_RANGE, "%s must end with one of %s (got %s)" % [param_name, ", ".join(_SUPPORTED_SUFFIXES), path] ) return null func _load_material_from_path(path: String, for_write: bool = false) -> Dictionary: var err := _validate_material_path(path, "path", for_write) if err != null: return err if not ResourceLoader.exists(path): return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Material not found: %s" % path) var res := ResourceLoader.load(path) if res == null or not (res is Material): return ErrorCodes.make(ErrorCodes.WRONG_TYPE, "Resource at %s is not a Material" % path) return {"material": res, "path": path} ## Map a slot name to a Godot property name on the given node. ## Returns {property: "..."} or an error dict. func _resolve_slot_property(node: Node, slot: String) -> Dictionary: if slot == "override": if node is MeshInstance3D or node is CSGShape3D: return {"property": "material_override"} if node is CanvasItem: return {"property": "material"} if node is GPUParticles3D or node is GPUParticles2D or node is CPUParticles3D or node is CPUParticles2D: return {"property": "material_override"} if node is GeometryInstance3D else {"property": "material"} return ErrorCodes.make( ErrorCodes.PROPERTY_NOT_ON_CLASS, "Slot 'override' not supported on %s" % node.get_class() ) if slot == "canvas": if node is CanvasItem: return {"property": "material"} return ErrorCodes.make( ErrorCodes.WRONG_TYPE, "Slot 'canvas' requires a CanvasItem (got %s)" % node.get_class() ) if slot == "process": if node is GPUParticles3D or node is GPUParticles2D: return {"property": "process_material"} return ErrorCodes.make( ErrorCodes.WRONG_TYPE, "Slot 'process' requires a GPUParticles2D/3D (got %s)" % node.get_class() ) if slot.begins_with("surface_"): if not (node is MeshInstance3D): return ErrorCodes.make( ErrorCodes.WRONG_TYPE, "Slot '%s' requires a MeshInstance3D (got %s)" % [slot, node.get_class()] ) var idx_str := slot.substr(len("surface_")) if not idx_str.is_valid_int(): return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Invalid surface slot: %s" % slot) var idx := int(idx_str) var mi := node as MeshInstance3D var surf_count := mi.mesh.get_surface_count() if mi.mesh != null else 0 if idx < 0 or idx >= surf_count: return ErrorCodes.make( ErrorCodes.INVALID_PARAMS, "Surface index %d out of range (mesh has %d surfaces)" % [idx, surf_count] ) return {"property": "surface_material_override/%d" % idx} return ErrorCodes.make( ErrorCodes.VALUE_OUT_OF_RANGE, "Unknown slot '%s'. Valid: override, canvas, process, surface_N" % slot ) ## Apply one property to an in-memory material instance; returns null on ## success or an error dict on failure. func _apply_one_param_on_instance(mat: Material, property: String, raw_value: Variant) -> Variant: var prop_type: int = TYPE_NIL var property_exists := false for prop in mat.get_property_list(): if prop.name == property: property_exists = true prop_type = prop.get("type", TYPE_NIL) break if not property_exists: return ErrorCodes.make( ErrorCodes.PROPERTY_NOT_ON_CLASS, McpPropertyErrors.build_message(mat, property) ) var coerced := MaterialValues.coerce_material_value(property, raw_value, prop_type) if not coerced.ok: return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, String(coerced.error)) mat.set(property, coerced.value) return null ## Inspect a shader to get the Variant type of a uniform. Returns TYPE_NIL if ## the uniform is not declared. static func _shader_uniform_type(shader: Shader, name: String) -> int: if shader == null: return TYPE_NIL for u in shader.get_shader_uniform_list(): if u.get("name", "") == name: return int(u.get("type", TYPE_NIL)) return TYPE_NIL