cirnogodot/addons/func_godot/src/map/func_godot_map.gd

1160 lines
46 KiB
GDScript3
Raw Normal View History

2025-06-09 18:57:53 +02:00
@tool
@icon("res://addons/func_godot/icons/icon_slipgate3d.svg")
## A scene generator node that parses a Quake map file using a [FuncGodotFGDFile]. Uses a [FuncGodotMapSettings] resource to define map build settings.
## To use this node, select an instance of the node in the Godot editor and select "Quick Build", "Full Build", or "Unwrap UV2" from the toolbar. Alternatively, call [method manual_build] from code.
class_name FuncGodotMap extends Node3D
## How long to wait between child/owner batches
const YIELD_DURATION := 0.0
## Emitted when the build process successfully completes
signal build_complete()
## Emitted when the build process finishes a step. [code]progress[/code] is from 0.0-1.0
signal build_progress(step, progress)
## Emitted when the build process fails
signal build_failed()
## Emitted when UV2 unwrapping is completed
signal unwrap_uv2_complete()
@export_category("Map")
## Local path to Quake map file to build a scene from.
@export_file("*.map") var local_map_file: String = ""
## Global path to Quake map file to build a scene from. Overrides [member local_map_file].
@export_global_file("*.map") var global_map_file: String = ""
## Map path used by code. Do it this way to support both global and local paths.
var _map_file_internal: String = ""
## Map settings resource that defines map build scale, textures location, and more.
@export var map_settings: FuncGodotMapSettings = load(ProjectSettings.get_setting("func_godot/default_map_settings", "res://addons/func_godot/func_godot_default_map_settings.tres"))
@export_category("Build")
## If true, print profiling data before and after each build step.
@export var print_profiling_data: bool = false
## If true, stop the whole editor until build is complete.
@export var block_until_complete: bool = false
## How many nodes to set the owner of, or add children of, at once. Higher values may lead to quicker build times, but a less responsive editor.
@export var set_owner_batch_size: int = 1000
# Build context variables
var func_godot: FuncGodot = null
var profile_timestamps: Dictionary = {}
var add_child_array: Array = []
var set_owner_array: Array = []
var should_add_children: bool = true
var should_set_owners: bool = true
var texture_list: Array = []
var texture_loader = null
var texture_dict: Dictionary = {}
var texture_size_dict: Dictionary = {}
var material_dict: Dictionary = {}
var entity_definitions: Dictionary = {}
var entity_dicts: Array = []
var entity_mesh_dict: Dictionary = {}
var entity_nodes: Array = []
var entity_mesh_instances: Dictionary = {}
var entity_occluder_instances: Dictionary = {}
var entity_collision_shapes: Array = []
# Utility
## Verify that FuncGodot is functioning and that [member map_file] exists. If so, build the map. If not, signal [signal build_failed]
func verify_and_build() -> void:
if verify_parameters():
build_map()
else:
emit_signal("build_failed")
## Build the map.
func manual_build() -> void:
should_add_children = false
should_set_owners = false
verify_and_build()
## Return true if parameters are valid; FuncGodot should be functioning and [member map_file] should exist.
func verify_parameters() -> bool:
# Prioritize global map file path for building at runtime
_map_file_internal = global_map_file if global_map_file != "" else local_map_file
if _map_file_internal == "":
push_error("Error: Map file not set")
return false
if not FileAccess.file_exists(_map_file_internal):
if FileAccess.file_exists(_map_file_internal + ".import"):
_map_file_internal = _map_file_internal + ".import"
else:
push_error("Error: No such file %s" % _map_file_internal)
return false
if not map_settings:
push_error("Error: Map settings not set")
return false
if not func_godot:
func_godot = load("res://addons/func_godot/src/core/func_godot.gd").new()
if not func_godot:
push_error("Error: Failed to load func_godot.")
return false
return true
## Reset member variables that affect the current build
func reset_build_context() -> void:
add_child_array = []
set_owner_array = []
texture_list = []
texture_loader = null
texture_dict = {}
texture_size_dict = {}
material_dict = {}
entity_definitions = {}
entity_dicts = []
entity_mesh_dict = {}
entity_nodes = []
entity_mesh_instances = {}
entity_occluder_instances = {}
entity_collision_shapes = []
build_step_index = 0
build_step_count = 0
if func_godot:
func_godot = load("res://addons/func_godot/src/core/func_godot.gd").new()
func_godot.map_settings = map_settings
## Record the start time of a build step for profiling
func start_profile(item_name: String) -> void:
if print_profiling_data:
print(item_name)
profile_timestamps[item_name] = Time.get_unix_time_from_system()
## Finish profiling for a build step; print associated timing data
func stop_profile(item_name: String) -> void:
if print_profiling_data:
if item_name in profile_timestamps:
var delta: float = Time.get_unix_time_from_system() - profile_timestamps[item_name]
print("Completed in %s sec." % snapped(delta, 0.0001))
profile_timestamps.erase(item_name)
## Run a build step. [code]step_name[/code] is the method corresponding to the step.
func run_build_step(step_name: String) -> Variant:
start_profile(step_name)
var result : Variant = call(step_name)
stop_profile(step_name)
return result
## Add [code]node[/code] as a child of parent, or as a child of [code]below[/code] if non-null. Also queue for ownership assignment.
func add_child_editor(parent: Node, node: Node, below: Node = null) -> void:
if not node or not parent:
return
var prev_parent = node.get_parent()
if prev_parent:
prev_parent.remove_child(node)
if below:
below.add_sibling(node)
else:
parent.add_child(node)
set_owner_array.append(node)
## Set the owner of [code]node[/code] to the current scene.
func set_owner_editor(node: Node) -> void:
var tree : SceneTree = get_tree()
if not tree:
return
var edited_scene_root : Node = tree.get_edited_scene_root()
if not edited_scene_root:
return
node.set_owner(edited_scene_root)
var build_step_index : int = 0
var build_step_count : int = 0
var build_steps : Array = []
var post_attach_steps : Array = []
## Register a build step.
## [code]build_step[/code] is a string that corresponds to a method on this class, [code]arguments[/code] a list of arguments to pass to this method, and [code]target[/code] is a property on this class to save the return value of the build step in. If [code]post_attach[/code] is true, the step will be run after the scene hierarchy is completed.
func register_build_step(build_step: String, target: String = "", post_attach: bool = false) -> void:
(post_attach_steps if post_attach else build_steps).append([build_step, target])
build_step_count += 1
## Run all build steps. Emits [signal build_progress] after each step.
## If [code]post_attach[/code] is true, run post-attach steps instead and signal [signal build_complete] when finished.
func run_build_steps(post_attach : bool = false) -> void:
var target_array : Array = post_attach_steps if post_attach else build_steps
while target_array.size() > 0:
var build_step : Array = target_array.pop_front()
emit_signal("build_progress", build_step[0], float(build_step_index + 1) / float(build_step_count))
var scene_tree : SceneTree = get_tree()
if scene_tree and not block_until_complete:
await get_tree().create_timer(YIELD_DURATION).timeout
var result : Variant = run_build_step(build_step[0])
var target : String = build_step[1]
if target != "":
set(target, result)
build_step_index += 1
if scene_tree and not block_until_complete:
await get_tree().create_timer(YIELD_DURATION).timeout
if post_attach:
_build_complete()
else:
start_profile('add_children')
add_children()
## Register all steps for the build. See [method register_build_step] and [method run_build_steps]
func register_build_steps() -> void:
register_build_step('remove_children')
register_build_step('load_map')
register_build_step('fetch_texture_list', 'texture_list')
register_build_step('init_texture_loader', 'texture_loader')
register_build_step('load_textures', 'texture_dict')
register_build_step('build_texture_size_dict', 'texture_size_dict')
register_build_step('build_materials', 'material_dict')
register_build_step('fetch_entity_definitions', 'entity_definitions')
register_build_step('set_core_entity_definitions')
register_build_step('generate_geometry')
register_build_step('fetch_entity_dicts', 'entity_dicts')
register_build_step('build_entity_nodes', 'entity_nodes')
register_build_step('resolve_trenchbroom_group_hierarchy')
register_build_step('build_entity_mesh_dict', 'entity_mesh_dict')
register_build_step('build_entity_mesh_instances', 'entity_mesh_instances')
register_build_step('build_entity_occluder_instances', 'entity_occluder_instances')
register_build_step('build_entity_collision_shape_nodes', 'entity_collision_shapes')
## Register all post-attach steps for the build. See [method register_build_step] and [method run_build_steps]
func register_post_attach_steps() -> void:
register_build_step('build_entity_collision_shapes', "", true)
register_build_step('apply_entity_meshes', "", true)
register_build_step('apply_entity_occluders', "", true)
register_build_step('apply_properties_and_finish', "", true)
# Actions
## Build the map
func build_map() -> void:
reset_build_context()
if map_settings == null:
printerr("Skipping build process: No map settings resource!")
emit_signal("build_complete")
return
print('Building %s' % _map_file_internal)
#if print_profiling_data:
#print('\n')
start_profile('build_map')
register_build_steps()
register_post_attach_steps()
run_build_steps()
## Recursively unwrap UV2s for [code]node[/code] and its children, in preparation for baked lighting.
func unwrap_uv2(node: Node = null) -> void:
var target_node: Node = null
if node:
target_node = node
else:
target_node = self
print("Unwrapping mesh UV2s")
if target_node is MeshInstance3D:
if target_node.gi_mode == GeometryInstance3D.GI_MODE_STATIC:
var mesh: Mesh = target_node.get_mesh()
if mesh is ArrayMesh:
mesh.lightmap_unwrap(Transform3D.IDENTITY, map_settings.uv_unwrap_texel_size * map_settings.scale_factor)
for child in target_node.get_children():
unwrap_uv2(child)
if not node:
print("Unwrap complete")
emit_signal("unwrap_uv2_complete")
# Build Steps
## Recursively remove and delete all children of this node
func remove_children() -> void:
for child in get_children():
remove_child(child)
child.queue_free()
## Parse and load [member map_file]
func load_map() -> void:
func_godot.load_map(_map_file_internal, map_settings.use_trenchbroom_groups_hierarchy)
## Get textures found in [member map_file]
func fetch_texture_list() -> Array:
return func_godot.get_texture_list() as Array
## Initialize texture loader, allowing textures in [member base_texture_dir] and [member texture_wads] to be turned into materials
func init_texture_loader() -> FuncGodotTextureLoader:
return FuncGodotTextureLoader.new(map_settings)
## Build a dictionary from Map File texture names to their corresponding Texture2D resources in Godot
func load_textures() -> Dictionary:
return texture_loader.load_textures(texture_list) as Dictionary
## Build a dictionary from Map File texture names to Godot materials
func build_materials() -> Dictionary:
return texture_loader.create_materials(texture_list)
## Collect entity definitions from [member entity_fgd], as a dictionary from Map File classnames to entity definitions
func fetch_entity_definitions() -> Dictionary:
return map_settings.entity_fgd.get_entity_definitions()
## Hand the FuncGodot core the entity definitions
func set_core_entity_definitions() -> void:
var core_ent_defs: Dictionary = {}
for classname in entity_definitions:
core_ent_defs[classname] = {}
var entity_definition: FuncGodotFGDEntityClass = entity_definitions[classname]
if entity_definition is FuncGodotFGDSolidClass:
core_ent_defs[classname]['spawn_type'] = entity_definition.spawn_type
core_ent_defs[classname]['origin_type'] = entity_definition.origin_type
const MFlags = FuncGodotMapData.FuncGodotEntityMetadataInclusionFlags
var flags := MFlags.NONE
if entity_definition.add_textures_metadata: flags |= MFlags.TEXTURES
if entity_definition.add_vertex_metadata: flags |= MFlags.VERTEX
if entity_definition.add_face_normal_metadata: flags |= MFlags.FACE_NORMAL
if entity_definition.add_face_position_metadata: flags |= MFlags.FACE_POSITION
if entity_definition.add_collision_shape_face_range_metadata: flags |= MFlags.COLLISION_SHAPE_TO_FACE_RANGE_MAP
core_ent_defs[classname]['metadata_inclusion_flags'] = flags
func_godot.set_entity_definitions(core_ent_defs)
## Generate geometry from map file
func generate_geometry() -> void:
func_godot.generate_geometry(texture_size_dict);
## Get a list of dictionaries representing each entity from the FuncGodot core
func fetch_entity_dicts() -> Array:
return func_godot.get_entity_dicts()
## Build a dictionary from Map File textures to the sizes of their corresponding Godot textures
func build_texture_size_dict() -> Dictionary:
var texture_size_dict: Dictionary = {}
for tex_key in texture_dict:
var texture: Texture2D = texture_dict[tex_key] as Texture2D
if texture:
texture_size_dict[tex_key] = texture.get_size()
else:
texture_size_dict[tex_key] = Vector2.ONE
return texture_size_dict
2025-06-20 10:52:08 +02:00
static func get_script_by_class_name(name_of_class : String) -> Script:
if ResourceLoader.exists(name_of_class, "Script"):
return load(name_of_class) as Script
for global_class in ProjectSettings.get_global_class_list():
var found_name_of_class : String = global_class["class"]
var found_path : String = global_class["path"]
if found_name_of_class == name_of_class:
return load(found_path) as Script
return null
2025-06-09 18:57:53 +02:00
## Build nodes from the entities in [member entity_dicts]
func build_entity_nodes() -> Array:
var entity_nodes : Array = []
entity_nodes.resize(entity_dicts.size())
# TrenchBroom: Prevent generation of omitted layers
var omitted_entities : Array[int] = []
var omitted_groups: Array[String] = []
if map_settings.use_trenchbroom_groups_hierarchy:
# Omit layers
for entity_idx in range(0, entity_dicts.size()):
var entity_dict: Dictionary = entity_dicts[entity_idx] as Dictionary
var properties: Dictionary = entity_dict['properties'] as Dictionary
if '_tb_type' in properties and properties['_tb_type'] == '_tb_layer':
if '_tb_layer_omit_from_export' in properties and properties['_tb_layer_omit_from_export'] == "1":
omitted_entities.append(entity_idx)
omitted_groups.append("layer_" + str(properties.get('_tb_id', "-1")))
# Omit groups and top-level entities
for entity_idx in range(0, entity_dicts.size()):
if omitted_entities.find(entity_idx) != -1:
continue
var entity_dict: Dictionary = entity_dicts[entity_idx] as Dictionary
var properties: Dictionary = entity_dict['properties'] as Dictionary
if '_tb_layer' in properties:
if omitted_groups.find("layer_" + str(properties['_tb_layer'])) != -1:
omitted_entities.append(entity_idx)
if '_tb_id' in properties and properties['_tb_type'] == '_tb_group':
omitted_groups.append("group_" + str(properties.get('_tb_id', "-1")))
for entity_idx in range(0, entity_dicts.size()):
var entity_dict: Dictionary = entity_dicts[entity_idx] as Dictionary
var properties: Dictionary = entity_dict['properties'] as Dictionary
if map_settings.use_trenchbroom_groups_hierarchy:
if omitted_entities.find(entity_idx) != -1:
entity_nodes[entity_idx] = null
continue
if '_tb_group' in properties and omitted_groups.find("group_" + str(properties['_tb_group'])) != -1:
entity_nodes[entity_idx] = null
continue
var node: Node = null
var node_name: String = "entity_%s" % entity_idx
var should_add_child: bool = should_add_children
if 'classname' in properties:
var classname: String = properties['classname']
node_name += "_" + classname
if classname in entity_definitions:
var entity_definition: FuncGodotFGDEntityClass = entity_definitions[classname] as FuncGodotFGDEntityClass
var name_prop: String
if entity_definition.name_property in properties:
name_prop = str(properties[entity_definition.name_property])
elif map_settings.entity_name_property in properties:
name_prop = str(properties[map_settings.entity_name_property])
if not name_prop.is_empty():
node_name = "entity_" + name_prop
if entity_definition is FuncGodotFGDSolidClass:
if entity_definition.spawn_type == FuncGodotFGDSolidClass.SpawnType.MERGE_WORLDSPAWN:
entity_nodes[entity_idx] = null
continue
if entity_definition.node_class != "":
2025-06-20 10:52:08 +02:00
if ClassDB.class_exists(entity_definition.node_class):
node = ClassDB.instantiate(entity_definition.node_class)
else:
var script : Script = get_script_by_class_name(entity_definition.node_class)
if script is GDScript:
node = (script as GDScript).new()
2025-06-09 18:57:53 +02:00
elif entity_definition is FuncGodotFGDPointClass:
if entity_definition.scene_file:
var flag: PackedScene.GenEditState = PackedScene.GEN_EDIT_STATE_DISABLED
if Engine.is_editor_hint():
flag = PackedScene.GEN_EDIT_STATE_INSTANCE
node = entity_definition.scene_file.instantiate(flag)
elif entity_definition.node_class != "":
2025-06-20 10:52:08 +02:00
if ClassDB.class_exists(entity_definition.node_class):
node = ClassDB.instantiate(entity_definition.node_class)
else:
var script : Script = get_script_by_class_name(entity_definition.node_class)
if script is GDScript:
node = (script as GDScript).new()
2025-06-09 18:57:53 +02:00
if 'rotation_degrees' in node and entity_definition.apply_rotation_on_map_build:
var angles := Vector3.ZERO
if 'angles' in properties or 'mangle' in properties:
var key := 'angles' if 'angles' in properties else 'mangle'
var angles_raw = properties[key]
if not angles_raw is Vector3:
angles_raw = angles_raw.split_floats(' ')
if angles_raw.size() > 2:
angles = Vector3(-angles_raw[0], angles_raw[1], -angles_raw[2])
if key == 'mangle':
if entity_definition.classname.begins_with('light'):
angles = Vector3(angles_raw[1], angles_raw[0], -angles_raw[2])
elif entity_definition.classname == 'info_intermission':
angles = Vector3(angles_raw[0], angles_raw[1], -angles_raw[2])
else:
push_error("Invalid vector format for \'" + key + "\' in entity \'" + classname + "\'")
elif 'angle' in properties:
var angle = properties['angle']
if not angle is float:
angle = float(angle)
angles.y += angle
angles.y += 180
node.rotation_degrees = angles
if 'scale' in node and entity_definition.apply_scale_on_map_build:
if 'scale' in properties:
var scale_prop: Variant = properties['scale']
if typeof(scale_prop) == TYPE_STRING:
var scale_arr: PackedStringArray = (scale_prop as String).split(" ")
match scale_arr.size():
1: scale_prop = scale_arr[0].to_float()
3: scale_prop = Vector3(scale_arr[1].to_float(), scale_arr[2].to_float(), scale_arr[0].to_float())
2: scale_prop = Vector2(scale_arr[0].to_float(), scale_arr[0].to_float())
if typeof(scale_prop) == TYPE_FLOAT or typeof(scale_prop) == TYPE_INT:
node.scale *= scale_prop as float
elif node.scale is Vector3:
if typeof(scale_prop) == TYPE_VECTOR3 or typeof(scale_prop) == TYPE_VECTOR3I:
node.scale *= scale_prop as Vector3
elif node.scale is Vector2:
if typeof(scale_prop) == TYPE_VECTOR2 or typeof(scale_prop) == TYPE_VECTOR2I:
node.scale *= scale_prop as Vector2
else:
node = Node3D.new()
if entity_definition.script_class:
node.set_script(entity_definition.script_class)
if not node:
node = Node3D.new()
node.name = node_name
if 'origin' in properties and entity_dict.brush_count < 1:
var origin_vec: Vector3 = Vector3.ZERO
var origin_comps: PackedFloat64Array = properties['origin'].split_floats(' ')
if origin_comps.size() > 2:
origin_vec = Vector3(origin_comps[1], origin_comps[2], origin_comps[0])
else:
push_error("Invalid vector format for \'origin\' in " + node.name)
if 'position' in node:
if node.position is Vector3:
node.position = origin_vec * map_settings.scale_factor
elif node.position is Vector2:
node.position = Vector2(origin_vec.z, -origin_vec.y)
else:
if entity_idx != 0 and 'position' in node:
if node.position is Vector3:
node.position = entity_dict['center'] * map_settings.scale_factor
entity_nodes[entity_idx] = node
if should_add_child:
queue_add_child(self, node)
return entity_nodes
## Build [CollisionShape3D] nodes for brush entities
func build_entity_collision_shape_nodes() -> Array:
var entity_collision_shapes_arr: Array = []
for entity_idx in range(0, entity_nodes.size()):
var entity_collision_shapes: Array = []
var entity_dict: Dictionary = entity_dicts[entity_idx]
var properties: Dictionary = entity_dict['properties']
var node: Node = entity_nodes[entity_idx] as Node
var concave: bool = false
if 'classname' in properties:
var classname: String = properties['classname']
if classname in entity_definitions:
var entity_definition: FuncGodotFGDSolidClass = entity_definitions[classname] as FuncGodotFGDSolidClass
if entity_definition:
if entity_definition.collision_shape_type == FuncGodotFGDSolidClass.CollisionShapeType.NONE:
entity_collision_shapes_arr.append(null)
continue
elif entity_definition.collision_shape_type == FuncGodotFGDSolidClass.CollisionShapeType.CONCAVE:
concave = true
if entity_definition.spawn_type == FuncGodotFGDSolidClass.SpawnType.MERGE_WORLDSPAWN:
# TODO: Find the worldspawn object instead of assuming index 0
node = entity_nodes[0] as Node
if node and node is CollisionObject3D:
(node as CollisionObject3D).collision_layer = entity_definition.collision_layer
(node as CollisionObject3D).collision_mask = entity_definition.collision_mask
(node as CollisionObject3D).collision_priority = entity_definition.collision_priority
# don't create collision shapes that wont be attached to a CollisionObject3D as they are a waste
if not node or (not node is CollisionObject3D):
entity_collision_shapes_arr.append(null)
continue
if concave:
var collision_shape: CollisionShape3D = CollisionShape3D.new()
collision_shape.name = "entity_%s_collision_shape" % entity_idx
entity_collision_shapes.append(collision_shape)
queue_add_child(node, collision_shape)
else:
for brush_idx in entity_dict['brush_indices']:
var collision_shape: CollisionShape3D = CollisionShape3D.new()
collision_shape.name = "entity_%s_brush_%s_collision_shape" % [entity_idx, brush_idx]
entity_collision_shapes.append(collision_shape)
queue_add_child(node, collision_shape)
entity_collision_shapes_arr.append(entity_collision_shapes)
return entity_collision_shapes_arr
## Build the concrete [Shape3D] resources for each brush
func build_entity_collision_shapes() -> void:
for entity_idx in range(0, entity_dicts.size()):
var entity_dict: Dictionary = entity_dicts[entity_idx] as Dictionary
var properties: Dictionary = entity_dict['properties']
var entity_position: Vector3 = Vector3.ZERO
if entity_nodes[entity_idx] != null and entity_nodes[entity_idx].get("position"):
if entity_nodes[entity_idx].position is Vector3:
entity_position = entity_nodes[entity_idx].position
if entity_collision_shapes.size() < entity_idx:
continue
if entity_collision_shapes[entity_idx] == null:
continue
var entity_collision_shape: Array = entity_collision_shapes[entity_idx]
var concave: bool = false
var shape_margin: float = 0.04
var entity_definition: FuncGodotFGDSolidClass
if 'classname' in properties:
var classname: String = properties['classname']
if classname in entity_definitions:
entity_definition = entity_definitions[classname] as FuncGodotFGDSolidClass
if entity_definition:
match(entity_definition.collision_shape_type):
FuncGodotFGDSolidClass.CollisionShapeType.NONE:
continue
FuncGodotFGDSolidClass.CollisionShapeType.CONVEX:
concave = false
FuncGodotFGDSolidClass.CollisionShapeType.CONCAVE:
concave = true
shape_margin = entity_definition.collision_shape_margin
if entity_collision_shapes[entity_idx] == null:
continue
if concave:
func_godot.gather_entity_concave_collision_surfaces(entity_idx)
else:
func_godot.gather_entity_convex_collision_surfaces(entity_idx)
var entity_surfaces: Array = func_godot.fetch_surfaces(func_godot.surface_gatherer)
var metadata: Dictionary = func_godot.surface_gatherer.out_metadata
var collision_shape_to_face_range_map: Dictionary = {}
var face_shape_indices: Array[Vector2i] = metadata["shape_index_ranges"]
var entity_verts: PackedVector3Array = PackedVector3Array()
for surface_idx in range(0, entity_surfaces.size()):
if entity_surfaces[surface_idx] == null:
continue
var surface_verts: Array = entity_surfaces[surface_idx]
if concave:
var vertices: PackedVector3Array = surface_verts[Mesh.ARRAY_VERTEX] as PackedVector3Array
var indices: PackedInt32Array = surface_verts[Mesh.ARRAY_INDEX] as PackedInt32Array
for vert_idx in indices:
entity_verts.append(vertices[vert_idx])
else:
var shape_points = PackedVector3Array()
for vertex in surface_verts[Mesh.ARRAY_VERTEX]:
if not vertex in shape_points:
shape_points.append(vertex)
var shape: ConvexPolygonShape3D = ConvexPolygonShape3D.new()
shape.set_points(shape_points)
shape.margin = shape_margin
var collision_shape: CollisionShape3D = entity_collision_shape[surface_idx]
collision_shape.set_shape(shape)
# For face shape range metadata, we need to add info about child node names
if entity_definition and entity_definition.add_collision_shape_face_range_metadata:
collision_shape_to_face_range_map[collision_shape.name] = face_shape_indices[surface_idx]
if concave:
if entity_verts.size() == 0:
continue
var shape: ConcavePolygonShape3D = ConcavePolygonShape3D.new()
shape.set_faces(entity_verts)
shape.margin = shape_margin
var collision_shape: CollisionShape3D = entity_collision_shapes[entity_idx][0]
collision_shape.set_shape(shape)
if entity_definition and entity_definition.add_collision_shape_face_range_metadata:
collision_shape_to_face_range_map[collision_shape.name] = Vector2i(0, entity_verts.size() / 3)
if entity_definition:
if not entity_definition.add_face_normal_metadata:
metadata.erase("normals")
if not entity_definition.add_face_position_metadata:
metadata.erase("positions")
if not entity_definition.add_textures_metadata:
metadata.erase("textures")
metadata.erase("texture_names")
if not entity_definition.add_vertex_metadata:
metadata.erase("vertices")
metadata.erase("shape_index_ranges") # cleanup intermediate / buffer
if entity_definition.add_collision_shape_face_range_metadata:
metadata["collision_shape_to_face_range_map"] = collision_shape_to_face_range_map
if not metadata.is_empty():
entity_nodes[entity_idx].set_meta("func_godot_mesh_data", metadata)
## Build Dictionary from entity indices to [ArrayMesh] instances
func build_entity_mesh_dict() -> Dictionary:
var meshes: Dictionary = {}
var texture_surf_map: Dictionary
var texture_to_metadata_map: Dictionary
for texture in texture_dict:
texture_surf_map[texture] = Array()
texture_to_metadata_map[texture] = {}
var gather_task = func(i):
var texture: String = texture_dict.keys()[i]
var fetch_result = func_godot.gather_texture_surfaces(texture)
texture_surf_map[texture] = fetch_result["surfaces"]
texture_to_metadata_map[texture] = fetch_result["metadata"]
var task_id: int = WorkerThreadPool.add_group_task(gather_task, texture_dict.keys().size(), 4, true)
WorkerThreadPool.wait_for_group_task_completion(task_id)
for texture in texture_dict:
var texture_surfaces: Array = texture_surf_map[texture] as Array
for entity_idx in range(0, texture_surfaces.size()):
var entity_dict: Dictionary = entity_dicts[entity_idx] as Dictionary
var properties: Dictionary = entity_dict['properties']
var entity_surface = texture_surfaces[entity_idx]
var entity_definition: FuncGodotFGDSolidClass
if 'classname' in properties:
var classname: String = properties['classname']
if classname in entity_definitions:
entity_definition = entity_definitions[classname] as FuncGodotFGDSolidClass
if entity_definition:
if entity_definition.spawn_type == FuncGodotFGDSolidClass.SpawnType.MERGE_WORLDSPAWN:
entity_surface = null
if not entity_definition.build_visuals and not entity_definition.build_occlusion:
entity_surface = null
if entity_surface == null:
continue
if not entity_idx in meshes:
meshes[entity_idx] = ArrayMesh.new()
var mesh: ArrayMesh = meshes[entity_idx]
mesh.add_surface_from_arrays(ArrayMesh.PRIMITIVE_TRIANGLES, entity_surface)
mesh.surface_set_name(mesh.get_surface_count() - 1, texture)
mesh.surface_set_material(mesh.get_surface_count() - 1, material_dict[texture])
# Build metadata only if the node is set to not build collision. Otherwise we are already building it in build_entity_collision_shapes.
if entity_definition and entity_definition.collision_shape_type == FuncGodotFGDSolidClass.CollisionShapeType.NONE:
if not mesh.has_meta("func_godot_mesh_data"):
mesh.set_meta("func_godot_mesh_data", Dictionary())
var this_textures_metadata: Dictionary = texture_to_metadata_map[texture]
var entity_metadata: Dictionary = mesh.get_meta("func_godot_mesh_data")
var entity_index_ranges: Array[Vector2i] = this_textures_metadata["entity_index_ranges"]
var range: Vector2i = entity_index_ranges[entity_idx]
if entity_definition.add_vertex_metadata:
var vertices: PackedVector3Array = entity_metadata.get("vertices", PackedVector3Array())
vertices.append_array((this_textures_metadata["vertices"] as PackedVector3Array).slice(range.x * 3, range.y * 3))
entity_metadata["vertices"] = vertices
if entity_definition.add_face_normal_metadata:
var normals: PackedVector3Array = entity_metadata.get("normals", PackedVector3Array())
normals.append_array((this_textures_metadata["normals"] as PackedVector3Array).slice(range.x, range.y))
entity_metadata["normals"] = normals
if entity_definition.add_face_position_metadata:
var positions: PackedVector3Array = entity_metadata.get("positions", PackedVector3Array())
positions.append_array((this_textures_metadata["positions"] as PackedVector3Array).slice(range.x, range.y))
entity_metadata["positions"] = positions
if entity_definition.add_textures_metadata:
# different (if null: add empty) logic for texture_names due to not being able make a static typed
# Array[StringName] inline in the get() function
if not entity_metadata.has("texture_names"):
var new: Array[StringName] = []
entity_metadata["texture_names"] = new
var texture_names: Array[StringName] = entity_metadata["texture_names"]
var textures: PackedInt32Array = entity_metadata.get("textures", PackedInt32Array())
var texture_block: PackedInt32Array = []
texture_block.resize(range.y - range.x)
texture_block.fill(texture_names.size())
texture_names.append(StringName(texture))
textures.append_array(texture_block)
entity_metadata["textures"] = textures
return meshes
## Build [MeshInstance3D]s from brush entities and add them to the add child queue
func build_entity_mesh_instances() -> Dictionary:
var entity_mesh_instances: Dictionary = {}
for entity_idx in entity_mesh_dict:
var use_in_baked_light: bool = false
var shadow_casting_setting: GeometryInstance3D.ShadowCastingSetting = GeometryInstance3D.SHADOW_CASTING_SETTING_DOUBLE_SIDED
var render_layers: int = 1
var entity_dict: Dictionary = entity_dicts[entity_idx] as Dictionary
var properties: Dictionary = entity_dict['properties']
var classname: String = properties['classname']
if classname in entity_definitions:
var entity_definition: FuncGodotFGDSolidClass = entity_definitions[classname] as FuncGodotFGDSolidClass
if entity_definition:
if not entity_definition.build_visuals:
continue
if entity_definition.use_in_baked_light:
use_in_baked_light = true
elif '_shadow' in properties:
if properties['_shadow'] == "1":
use_in_baked_light = true
shadow_casting_setting = entity_definition.shadow_casting_setting
render_layers = entity_definition.render_layers
if not entity_mesh_dict[entity_idx]:
continue
var mesh_instance: MeshInstance3D = MeshInstance3D.new()
mesh_instance.name = 'entity_%s_mesh_instance' % entity_idx
mesh_instance.gi_mode = MeshInstance3D.GI_MODE_STATIC if use_in_baked_light else GeometryInstance3D.GI_MODE_DISABLED
mesh_instance.cast_shadow = shadow_casting_setting
mesh_instance.layers = render_layers
queue_add_child(entity_nodes[entity_idx], mesh_instance)
entity_mesh_instances[entity_idx] = mesh_instance
return entity_mesh_instances
func build_entity_occluder_instances() -> Dictionary:
var entity_occluder_instances: Dictionary = {}
for entity_idx in entity_mesh_dict:
var entity_dict: Dictionary = entity_dicts[entity_idx] as Dictionary
var properties: Dictionary = entity_dict['properties']
var classname: String = properties['classname']
if classname in entity_definitions:
var entity_definition: FuncGodotFGDSolidClass = entity_definitions[classname] as FuncGodotFGDSolidClass
if entity_definition:
if entity_definition.build_occlusion:
if not entity_mesh_dict[entity_idx]:
continue
var occluder_instance: OccluderInstance3D = OccluderInstance3D.new()
occluder_instance.name = 'entity_%s_occluder_instance' % entity_idx
queue_add_child(entity_nodes[entity_idx], occluder_instance)
entity_occluder_instances[entity_idx] = occluder_instance
return entity_occluder_instances
## Assign [ArrayMesh]es to their [MeshInstance3D] counterparts
func apply_entity_meshes() -> void:
for entity_idx in entity_mesh_instances:
var mesh: Mesh = entity_mesh_dict[entity_idx] as Mesh
var mesh_instance: MeshInstance3D = entity_mesh_instances[entity_idx] as MeshInstance3D
if not mesh or not mesh_instance:
if mesh.has_meta("func_godot_mesh_data"):
mesh.remove_meta("func_godot_mesh_data")
continue
mesh_instance.set_mesh(mesh)
queue_add_child(entity_nodes[entity_idx], mesh_instance)
if mesh.has_meta("func_godot_mesh_data"):
entity_nodes[entity_idx].set_meta("func_godot_mesh_data", mesh.get_meta("func_godot_mesh_data"))
mesh.remove_meta("func_godot_mesh_data")
func apply_entity_occluders() -> void:
for entity_idx in entity_mesh_dict:
var mesh: Mesh = entity_mesh_dict[entity_idx] as Mesh
var occluder_instance: OccluderInstance3D
if entity_idx in entity_occluder_instances:
occluder_instance = entity_occluder_instances[entity_idx]
if not mesh or not occluder_instance:
continue
var verts: PackedVector3Array
var indices: PackedInt32Array
var index: int = 0
for surf_idx in range(mesh.get_surface_count()):
var vert_count: int = verts.size()
var surf_array: Array = mesh.surface_get_arrays(surf_idx)
verts.append_array(surf_array[Mesh.ARRAY_VERTEX])
indices.resize(indices.size() + surf_array[Mesh.ARRAY_INDEX].size())
for new_index in surf_array[Mesh.ARRAY_INDEX]:
indices[index] = (new_index + vert_count)
index += 1
var occluder: ArrayOccluder3D = ArrayOccluder3D.new()
occluder.set_arrays(verts, indices)
occluder_instance.occluder = occluder
## Resolve entity group hierarchy, turning Trenchbroom groups into nodes and queueing their contents to be added to said nodes as children
func resolve_trenchbroom_group_hierarchy() -> void:
if not map_settings.use_trenchbroom_groups_hierarchy:
return
var parent_entities: Dictionary = {}
var child_entities: Dictionary = {}
# Gather all entities which are children in some group or parents in some group
for node_idx in range(0, entity_nodes.size()):
var node: Node = entity_nodes[node_idx]
var properties: Dictionary = entity_dicts[node_idx]['properties']
if not properties:
continue
if not ('_tb_id' in properties or '_tb_group' in properties or '_tb_layer' in properties):
continue
# identify children
if '_tb_group' in properties or '_tb_layer' in properties:
child_entities[node_idx] = node
# identify parents
if '_tb_id' in properties:
node.set_meta("_tb_type", properties['_tb_type'])
if properties['_tb_type'] == "_tb_group":
node.name = "group_" + str(properties['_tb_id'])
elif properties['_tb_type'] == "_tb_layer":
node.name = "layer_" + str(properties['_tb_layer_sort_index'])
if properties['_tb_name'] != "Unnamed":
node.name = node.name + "_" + properties['_tb_name']
parent_entities[node_idx] = node
var child_to_parent_map: Dictionary = {}
#For each child,...
for node_idx in child_entities:
var node: Node = child_entities[node_idx]
var properties: Dictionary = entity_dicts[node_idx]['properties']
var tb_group: Variant = null
if '_tb_group' in properties:
tb_group = properties['_tb_group']
elif '_tb_layer' in properties:
tb_group = properties['_tb_layer']
if tb_group == null:
continue
var parent: Node = null
var parent_properties: Dictionary = {}
var parent_entity = null
var parent_idx = null
# ...identify its direct parent out of the parent_entities array
for possible_parent in parent_entities:
parent_entity = parent_entities[possible_parent]
parent_properties = entity_dicts[possible_parent]['properties']
if parent_properties['_tb_id'] == tb_group:
parent = parent_entity
parent_idx = possible_parent
break
# If there's a match, pass it on to the child-parent relationship map
if parent:
child_to_parent_map[node_idx] = parent_idx
for child_idx in child_to_parent_map:
var child = entity_nodes[child_idx]
var parent_idx = child_to_parent_map[child_idx]
var parent = entity_nodes[parent_idx]
queue_add_child(parent, child, null, true)
## Add a child and its new parent to the add child queue. If [code]below[/code] is a node, add it as a child to that instead. If [code]relative[/code] is true, set the location of node relative to parent.
func queue_add_child(parent, node, below = null, relative = false) -> void:
add_child_array.append({"parent": parent, "node": node, "below": below, "relative": relative})
## Assign children to parents based on the contents of the add child queue (see [method queue_add_child])
func add_children() -> void:
while true:
for i in range(0, set_owner_batch_size):
if add_child_array.size() > 0:
var data: Dictionary = add_child_array.pop_front()
if data:
add_child_editor(data['parent'], data['node'], data['below'])
if data['relative']:
if (data['node'] is Node3D and data['parent'] is Node3D) or (data['node'] is Node2D and data['parent'] is Node2D):
data['node'].position -= data['parent'].position
continue
add_children_complete()
return
var scene_tree: SceneTree = get_tree()
if scene_tree and not block_until_complete:
await get_tree().create_timer(YIELD_DURATION).timeout
## Set owners and start post-attach build steps
func add_children_complete() -> void:
stop_profile('add_children')
if should_set_owners:
start_profile('set_owners')
set_owners()
else:
run_build_steps(true)
## Set owner of nodes generated by FuncGodot to scene root based on [member set_owner_array]
func set_owners() -> void:
while true:
for i in range(0, set_owner_batch_size):
var node: Node = set_owner_array.pop_front()
if node:
set_owner_editor(node)
else:
set_owners_complete()
return
var scene_tree: SceneTree = get_tree()
if scene_tree and not block_until_complete:
await get_tree().create_timer(YIELD_DURATION).timeout
## Finish profiling for set_owners and start post-attach build steps
func set_owners_complete() -> void:
stop_profile('set_owners')
run_build_steps(true)
## Apply Map File properties to [Node3D] instances, transferring Map File dictionaries to [Node3D.func_godot_properties]
## and then calling the appropriate callbacks.
func apply_properties_and_finish() -> void:
for entity_idx in range(0, entity_nodes.size()):
var entity_node: Node = entity_nodes[entity_idx] as Node
if not entity_node:
continue
var entity_dict: Dictionary = entity_dicts[entity_idx] as Dictionary
var properties: Dictionary = entity_dict['properties'] as Dictionary
if 'classname' in properties:
var classname: String = properties['classname']
if classname in entity_definitions:
var entity_definition: FuncGodotFGDEntityClass = entity_definitions[classname] as FuncGodotFGDEntityClass
for property in properties:
var prop_string = properties[property]
if property in entity_definition.class_properties:
var prop_default: Variant = entity_definition.class_properties[property]
match typeof(prop_default):
TYPE_INT:
properties[property] = prop_string.to_int()
TYPE_FLOAT:
properties[property] = prop_string.to_float()
TYPE_BOOL:
properties[property] = bool(prop_string.to_int())
TYPE_VECTOR3:
var prop_comps: PackedFloat64Array = prop_string.split_floats(" ")
if prop_comps.size() > 2:
properties[property] = Vector3(prop_comps[0], prop_comps[1], prop_comps[2])
else:
push_error("Invalid Vector3 format for \'" + property + "\' in entity \'" + classname + "\': " + prop_string)
properties[property] = prop_default
TYPE_VECTOR3I:
var prop_vec: Vector3i = prop_default
var prop_comps: PackedStringArray = prop_string.split(" ")
if prop_comps.size() > 2:
for i in 3:
prop_vec[i] = prop_comps[i].to_int()
else:
push_error("Invalid Vector3i format for \'" + property + "\' in entity \'" + classname + "\': " + prop_string)
properties[property] = prop_vec
TYPE_COLOR:
var prop_color: Color = prop_default
var prop_comps: PackedStringArray = prop_string.split(" ")
if prop_comps.size() > 2:
prop_color.r8 = prop_comps[0].to_int()
prop_color.g8 = prop_comps[1].to_int()
prop_color.b8 = prop_comps[2].to_int()
prop_color.a = 1.0
else:
push_error("Invalid Color format for \'" + property + "\' in entity \'" + classname + "\': " + prop_string)
properties[property] = prop_color
TYPE_DICTIONARY:
var prop_desc = entity_definition.class_property_descriptions[property]
if prop_desc is Array and prop_desc.size() > 1 and prop_desc[1] is int:
properties[property] = prop_string.to_int()
TYPE_ARRAY:
properties[property] = prop_string.to_int()
TYPE_VECTOR2:
var prop_comps: PackedFloat64Array = prop_string.split_floats(" ")
if prop_comps.size() > 1:
properties[property] = Vector2(prop_comps[0], prop_comps[1])
else:
push_error("Invalid Vector2 format for \'" + property + "\' in entity \'" + classname + "\': " + prop_string)
properties[property] = prop_default
TYPE_VECTOR2I:
var prop_vec: Vector2i = prop_default
var prop_comps: PackedStringArray = prop_string.split(" ")
if prop_comps.size() > 1:
for i in 2:
prop_vec[i] = prop_comps[i].to_int()
else:
push_error("Invalid Vector2i format for \'" + property + "\' in entity \'" + classname + "\': " + prop_string)
properties[property] = prop_vec
TYPE_VECTOR4:
var prop_comps: PackedFloat64Array = prop_string.split_floats(" ")
if prop_comps.size() > 3:
properties[property] = Vector4(prop_comps[0], prop_comps[1], prop_comps[2], prop_comps[3])
else:
push_error("Invalid Vector4 format for \'" + property + "\' in entity \'" + classname + "\': " + prop_string)
properties[property] = prop_default
TYPE_VECTOR4I:
var prop_vec: Vector4i = prop_default
var prop_comps: PackedStringArray = prop_string.split(" ")
if prop_comps.size() > 3:
for i in 4:
prop_vec[i] = prop_comps[i].to_int()
else:
push_error("Invalid Vector4i format for \'" + property + "\' in entity \'" + classname + "\': " + prop_string)
properties[property] = prop_vec
TYPE_NODE_PATH:
properties[property] = prop_string
TYPE_OBJECT:
properties[property] = prop_string
# Assign properties not defined with defaults from the entity definition
for property in entity_definitions[classname].class_properties:
if not property in properties:
var prop_default: Variant = entity_definition.class_properties[property]
# Flags
if prop_default is Array:
var prop_flags_sum := 0
for prop_flag in prop_default:
if prop_flag is Array and prop_flag.size() > 2:
if prop_flag[2] and prop_flag[1] is int:
prop_flags_sum += prop_flag[1]
properties[property] = prop_flags_sum
# Choices
elif prop_default is Dictionary:
var prop_desc = entity_definition.class_property_descriptions[property]
if prop_desc is Array and prop_desc.size() > 1 and (prop_desc[1] is int or prop_desc[1] is String):
properties[property] = prop_desc[1]
else:
properties[property] = 0
elif prop_default is Resource:
properties[property] = prop_default.resource_path
elif prop_default is NodePath or prop_default is Object or prop_default == null:
properties[property] = ""
# Everything else
else:
properties[property] = prop_default
if entity_definition.auto_apply_to_matching_node_properties:
for property in properties:
if property in entity_node:
if typeof(entity_node.get(property)) == typeof(properties[property]):
entity_node.set(property, properties[property])
else:
push_error("Entity %s property \'%s\' type mismatch with matching generated node property." % [entity_node.name, property])
if 'func_godot_properties' in entity_node:
entity_node.func_godot_properties = properties
if entity_node.has_method("_func_godot_apply_properties"):
entity_node.call("_func_godot_apply_properties", properties)
if entity_node.has_method("_func_godot_build_complete"):
entity_node.call_deferred("_func_godot_build_complete")
# Cleanup after build is finished (internal)
func _build_complete():
reset_build_context()
stop_profile('build_map')
print('Build complete\n')
emit_signal("build_complete")