Updated to Godot 4.5 and func_godot 2025.9. Added grid_white and grid_taupe textures. Unfucked func_godot and navmesh settings. Began blocking out motel layout in map.

This commit is contained in:
2025-10-14 02:15:54 -04:00
parent 986d2f5666
commit b402a162e1
154 changed files with 10450 additions and 10097 deletions

View File

@@ -1,28 +1,37 @@
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=3 format=3 uid="uid://cxy7jnh6d7msn"] [gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=4 format=3 uid="uid://cxy7jnh6d7msn"]
[ext_resource type="Script" uid="uid://cmoudtn1s5egw" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="1_0fsmp"] [ext_resource type="Script" uid="uid://5cow84q03m6a" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="1_0fsmp"]
[ext_resource type="Resource" uid="uid://nayxb8n7see2" path="res://addons/func_godot/fgd/phong_base.tres" id="1_c3bns"] [ext_resource type="Resource" uid="uid://nayxb8n7see2" path="res://addons/func_godot/fgd/phong_base.tres" id="1_c3bns"]
[ext_resource type="Resource" uid="uid://doo4ly322b4jc" path="res://addons/func_godot/fgd/vertex_merge_distance_base.tres" id="2_c03gr"]
[resource] [resource]
script = ExtResource("1_0fsmp") script = ExtResource("1_0fsmp")
spawn_type = 2 spawn_type = 2
origin_type = 0 origin_type = 3
build_visuals = true build_visuals = true
global_illumination_mode = 1
use_in_baked_light = true use_in_baked_light = true
shadow_casting_setting = 0 shadow_casting_setting = 1
build_occlusion = false build_occlusion = false
render_layers = 1 render_layers = 1
collision_shape_type = 2 collision_shape_type = 2
collision_layer = 1 collision_layer = 1
collision_mask = 1 collision_mask = 0
collision_priority = 1.0 collision_priority = 1.0
collision_shape_margin = 0.04 collision_shape_margin = 0.04
classname = "func_transparent" add_textures_metadata = false
description = "Static collidable geometry. Builds a StaticBody3D with a MeshInstance3D and a single convex CollisionShape3D. Does not occlude other VisualInstance3D nodes." add_vertex_metadata = false
add_face_position_metadata = false
add_face_normal_metadata = false
add_collision_shape_to_face_indices_metadata = false
add_collision_shape_face_range_metadata = false
classname = "func_detail"
description = "Static collidable geometry. Builds a StaticBody3D with a MeshInstance3D and a single concave CollisionShape3D. Does not occlude other VisualInstance3D nodes."
func_godot_internal = false func_godot_internal = false
base_classes = Array[Resource]([ExtResource("1_c3bns")]) base_classes = Array[Resource]([ExtResource("1_c3bns"), ExtResource("2_c03gr")])
class_properties = {} class_properties = {}
class_property_descriptions = {} class_property_descriptions = {}
auto_apply_to_matching_node_properties = false
meta_properties = { meta_properties = {
"color": Color(0.8, 0.8, 0.8, 1) "color": Color(0.8, 0.8, 0.8, 1)
} }

View File

@@ -1,13 +1,15 @@
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=3 format=3 uid="uid://ch3e0dix85uhb"] [gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=4 format=3 uid="uid://ch3e0dix85uhb"]
[ext_resource type="Resource" uid="uid://nayxb8n7see2" path="res://addons/func_godot/fgd/phong_base.tres" id="1_ar63x"] [ext_resource type="Resource" uid="uid://nayxb8n7see2" path="res://addons/func_godot/fgd/phong_base.tres" id="1_ar63x"]
[ext_resource type="Script" uid="uid://cmoudtn1s5egw" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="2_lhb87"] [ext_resource type="Resource" uid="uid://doo4ly322b4jc" path="res://addons/func_godot/fgd/vertex_merge_distance_base.tres" id="2_j7vgq"]
[ext_resource type="Script" uid="uid://5cow84q03m6a" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="2_lhb87"]
[resource] [resource]
script = ExtResource("2_lhb87") script = ExtResource("2_lhb87")
spawn_type = 2 spawn_type = 2
origin_type = 0 origin_type = 3
build_visuals = true build_visuals = true
global_illumination_mode = 1
use_in_baked_light = true use_in_baked_light = true
shadow_casting_setting = 1 shadow_casting_setting = 1
build_occlusion = false build_occlusion = false
@@ -17,12 +19,19 @@ collision_layer = 1
collision_mask = 1 collision_mask = 1
collision_priority = 1.0 collision_priority = 1.0
collision_shape_margin = 0.04 collision_shape_margin = 0.04
add_textures_metadata = false
add_vertex_metadata = false
add_face_position_metadata = false
add_face_normal_metadata = false
add_collision_shape_to_face_indices_metadata = false
add_collision_shape_face_range_metadata = false
classname = "func_detail_illusionary" classname = "func_detail_illusionary"
description = "Static geometry with no collision. Builds a Node3D with a MeshInstance3D. Does not occlude other VisualInstance3D nodes." description = "Static geometry with no collision. Builds a Node3D with a MeshInstance3D. Does not occlude other VisualInstance3D nodes."
func_godot_internal = false func_godot_internal = false
base_classes = Array[Resource]([ExtResource("1_ar63x")]) base_classes = Array[Resource]([ExtResource("1_ar63x"), ExtResource("2_j7vgq")])
class_properties = {} class_properties = {}
class_property_descriptions = {} class_property_descriptions = {}
auto_apply_to_matching_node_properties = false
meta_properties = { meta_properties = {
"color": Color(0.8, 0.8, 0.8, 1) "color": Color(0.8, 0.8, 0.8, 1)
} }

View File

@@ -0,0 +1,39 @@
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=4 format=3 uid="uid://b70vf4t5dc70t"]
[ext_resource type="Resource" uid="uid://nayxb8n7see2" path="res://addons/func_godot/fgd/phong_base.tres" id="1_5mwee"]
[ext_resource type="Script" uid="uid://5cow84q03m6a" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="2_8o081"]
[ext_resource type="Resource" uid="uid://doo4ly322b4jc" path="res://addons/func_godot/fgd/vertex_merge_distance_base.tres" id="2_bp8pb"]
[resource]
script = ExtResource("2_8o081")
spawn_type = 2
origin_type = 3
build_visuals = true
global_illumination_mode = 1
use_in_baked_light = true
shadow_casting_setting = 1
build_occlusion = true
render_layers = 1
collision_shape_type = 2
collision_layer = 1
collision_mask = 0
collision_priority = 1.0
collision_shape_margin = 0.04
add_textures_metadata = false
add_vertex_metadata = false
add_face_position_metadata = false
add_face_normal_metadata = false
add_collision_shape_to_face_indices_metadata = false
add_collision_shape_face_range_metadata = false
classname = "func_geo"
description = "Static collidable geometry. Builds a StaticBody3D with a MeshInstance3D, a single concave CollisionShape3D, and an OccluderInstance3D."
func_godot_internal = false
base_classes = Array[Resource]([ExtResource("1_5mwee"), ExtResource("2_bp8pb")])
class_properties = {}
class_property_descriptions = {}
auto_apply_to_matching_node_properties = false
meta_properties = {
"color": Color(0.8, 0.8, 0.8, 1)
}
node_class = "StaticBody3D"
name_property = ""

View File

@@ -1,16 +1,14 @@
[gd_resource type="Resource" script_class="FuncGodotFGDFile" load_steps=7 format=3 uid="uid://crgpdahjaj"] [gd_resource type="Resource" script_class="FuncGodotFGDFile" load_steps=9 format=3 uid="uid://crgpdahjaj"]
[ext_resource type="Script" uid="uid://ckisq2tcxg062" path="res://addons/func_godot/src/fgd/func_godot_fgd_file.gd" id="1_axt3h"] [ext_resource type="Script" uid="uid://drlmgulwbjwqu" path="res://addons/func_godot/src/fgd/func_godot_fgd_file.gd" id="1_axt3h"]
[ext_resource type="Resource" uid="uid://nayxb8n7see2" path="res://addons/func_godot/fgd/phong_base.tres" id="1_ehab8"] [ext_resource type="Resource" uid="uid://nayxb8n7see2" path="res://addons/func_godot/fgd/phong_base.tres" id="1_ehab8"]
[ext_resource type="Resource" uid="uid://doo4ly322b4jc" path="res://addons/func_godot/fgd/vertex_merge_distance_base.tres" id="2_7jebp"]
[ext_resource type="Resource" uid="uid://bdji3873bg32h" path="res://addons/func_godot/fgd/worldspawn.tres" id="2_ri2rx"] [ext_resource type="Resource" uid="uid://bdji3873bg32h" path="res://addons/func_godot/fgd/worldspawn.tres" id="2_ri2rx"]
[ext_resource type="Resource" uid="uid://b70vf4t5dc70t" path="res://addons/func_godot/fgd/func_geo.tres" id="3_7jigp"]
[ext_resource type="Resource" uid="uid://cxy7jnh6d7msn" path="res://addons/func_godot/fgd/func_detail.tres" id="3_fqfww"] [ext_resource type="Resource" uid="uid://cxy7jnh6d7msn" path="res://addons/func_godot/fgd/func_detail.tres" id="3_fqfww"]
[ext_resource type="Resource" uid="uid://dg5x44cc7flew" path="res://addons/func_godot/fgd/func_illusionary.tres" id="4_c4ucw"] [ext_resource type="Resource" uid="uid://dg5x44cc7flew" path="res://addons/func_godot/fgd/func_illusionary.tres" id="4_c4ucw"]
[ext_resource type="Resource" uid="uid://ch3e0dix85uhb" path="res://addons/func_godot/fgd/func_detail_illusionary.tres" id="5_b2q3p"] [ext_resource type="Resource" uid="uid://ch3e0dix85uhb" path="res://addons/func_godot/fgd/func_detail_illusionary.tres" id="5_b2q3p"]
[resource] [resource]
script = ExtResource("1_axt3h") script = ExtResource("1_axt3h")
export_file = false entity_definitions = Array[Resource]([ExtResource("1_ehab8"), ExtResource("2_7jebp"), ExtResource("2_ri2rx"), ExtResource("3_7jigp"), ExtResource("3_fqfww"), ExtResource("5_b2q3p"), ExtResource("4_c4ucw")])
model_key_word_supported = true
fgd_name = "FuncGodot"
base_fgd_files = Array[Resource]([])
entity_definitions = Array[Resource]([ExtResource("1_ehab8"), ExtResource("2_ri2rx"), ExtResource("3_fqfww"), ExtResource("5_b2q3p"), ExtResource("4_c4ucw"), ExtResource("3_fqfww")])

View File

@@ -1,13 +1,15 @@
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=3 format=3 uid="uid://dg5x44cc7flew"] [gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=4 format=3 uid="uid://dg5x44cc7flew"]
[ext_resource type="Resource" uid="uid://nayxb8n7see2" path="res://addons/func_godot/fgd/phong_base.tres" id="1_kv0mq"] [ext_resource type="Resource" uid="uid://nayxb8n7see2" path="res://addons/func_godot/fgd/phong_base.tres" id="1_kv0mq"]
[ext_resource type="Script" uid="uid://cmoudtn1s5egw" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="2_uffhi"] [ext_resource type="Resource" uid="uid://doo4ly322b4jc" path="res://addons/func_godot/fgd/vertex_merge_distance_base.tres" id="2_hovr4"]
[ext_resource type="Script" uid="uid://5cow84q03m6a" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="2_uffhi"]
[resource] [resource]
script = ExtResource("2_uffhi") script = ExtResource("2_uffhi")
spawn_type = 2 spawn_type = 2
origin_type = 0 origin_type = 3
build_visuals = true build_visuals = true
global_illumination_mode = 1
use_in_baked_light = true use_in_baked_light = true
shadow_casting_setting = 1 shadow_casting_setting = 1
build_occlusion = true build_occlusion = true
@@ -17,12 +19,19 @@ collision_layer = 1
collision_mask = 1 collision_mask = 1
collision_priority = 1.0 collision_priority = 1.0
collision_shape_margin = 0.04 collision_shape_margin = 0.04
add_textures_metadata = false
add_vertex_metadata = false
add_face_position_metadata = false
add_face_normal_metadata = false
add_collision_shape_to_face_indices_metadata = false
add_collision_shape_face_range_metadata = false
classname = "func_illusionary" classname = "func_illusionary"
description = "Static geometry with no collision. Builds a Node3D with a MeshInstance3D and an Occluder3D to aid in render culling of other VisualInstance3D nodes." description = "Static geometry with no collision. Builds a Node3D with a MeshInstance3D and an Occluder3D to aid in render culling of other VisualInstance3D nodes."
func_godot_internal = false func_godot_internal = false
base_classes = Array[Resource]([ExtResource("1_kv0mq")]) base_classes = Array[Resource]([ExtResource("1_kv0mq"), ExtResource("2_hovr4")])
class_properties = {} class_properties = {}
class_property_descriptions = {} class_property_descriptions = {}
auto_apply_to_matching_node_properties = false
meta_properties = { meta_properties = {
"color": Color(0.8, 0.8, 0.8, 1) "color": Color(0.8, 0.8, 0.8, 1)
} }

View File

@@ -1,6 +1,6 @@
[gd_resource type="Resource" script_class="FuncGodotFGDBaseClass" load_steps=2 format=3 uid="uid://nayxb8n7see2"] [gd_resource type="Resource" script_class="FuncGodotFGDBaseClass" load_steps=2 format=3 uid="uid://nayxb8n7see2"]
[ext_resource type="Script" uid="uid://dyv4g30betqgm" path="res://addons/func_godot/src/fgd/func_godot_fgd_base_class.gd" id="1_04y3n"] [ext_resource type="Script" uid="uid://ck575aqs1sbrb" path="res://addons/func_godot/src/fgd/func_godot_fgd_base_class.gd" id="1_04y3n"]
[resource] [resource]
script = ExtResource("1_04y3n") script = ExtResource("1_04y3n")
@@ -19,6 +19,7 @@ class_property_descriptions = {
"_phong": ["Phong shading", 0], "_phong": ["Phong shading", 0],
"_phong_angle": "Phong smoothing angle" "_phong_angle": "Phong smoothing angle"
} }
auto_apply_to_matching_node_properties = false
meta_properties = { meta_properties = {
"color": Color(0.8, 0.8, 0.8, 1), "color": Color(0.8, 0.8, 0.8, 1),
"size": AABB(-8, -8, -8, 8, 8, 8) "size": AABB(-8, -8, -8, 8, 8, 8)

View File

@@ -0,0 +1,24 @@
[gd_resource type="Resource" script_class="FuncGodotFGDBaseClass" load_steps=2 format=3 uid="uid://doo4ly322b4jc"]
[ext_resource type="Script" uid="uid://ck575aqs1sbrb" path="res://addons/func_godot/src/fgd/func_godot_fgd_base_class.gd" id="1_h3atm"]
[resource]
script = ExtResource("1_h3atm")
classname = "VertexMergeDistance"
description = "Adjustable value to snap vertices to on map build. This can reduce instances of seams between polygons."
func_godot_internal = false
base_classes = Array[Resource]([])
class_properties = {
"_vertex_merge_distance": 0.008
}
class_property_descriptions = {
"_vertex_merge_distance": "Adjustable value to snap vertices to on map build. This can reduce instances of seams between polygons."
}
auto_apply_to_matching_node_properties = false
meta_properties = {
"color": Color(0.8, 0.8, 0.8, 1),
"size": AABB(-8, -8, -8, 8, 8, 8)
}
node_class = ""
name_property = ""
metadata/_custom_type_script = "uid://ck575aqs1sbrb"

View File

@@ -1,29 +1,17 @@
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=2 format=3 uid="uid://bdji3873bg32h"] [gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=3 format=3 uid="uid://bdji3873bg32h"]
[ext_resource type="Script" uid="uid://cmoudtn1s5egw" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="1_62t8m"] [ext_resource type="Script" uid="uid://5cow84q03m6a" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="1_62t8m"]
[ext_resource type="Resource" uid="uid://doo4ly322b4jc" path="res://addons/func_godot/fgd/vertex_merge_distance_base.tres" id="1_h1046"]
[resource] [resource]
script = ExtResource("1_62t8m") script = ExtResource("1_62t8m")
spawn_type = 0 spawn_type = 0
origin_type = 0 origin_type = 1
build_visuals = true
use_in_baked_light = true
shadow_casting_setting = 1
build_occlusion = true
render_layers = 1
collision_shape_type = 1
collision_layer = 3 collision_layer = 3
collision_mask = 1
collision_priority = 1.0
collision_shape_margin = 0.04
classname = "worldspawn" classname = "worldspawn"
description = "Default static world geometry. Builds a StaticBody3D with a single MeshInstance3D and a single convex CollisionShape3D shape. Also builds Occluder3D to aid in render culling of other VisualInstance3D nodes." description = "Default static world geometry. Builds a StaticBody3D with a single MeshInstance3D and a single convex CollisionShape3D shape. Also builds Occluder3D to aid in render culling of other VisualInstance3D nodes."
func_godot_internal = false base_classes = Array[Resource]([ExtResource("1_h1046")])
base_classes = Array[Resource]([])
class_properties = {}
class_property_descriptions = {}
meta_properties = { meta_properties = {
"color": Color(0.8, 0.8, 0.8, 1) "color": Color(0.8, 0.8, 0.8, 1)
} }
node_class = "StaticBody3D" node_class = "StaticBody3D"
name_property = ""

View File

@@ -1,19 +1,25 @@
[gd_resource type="Resource" script_class="FuncGodotMapSettings" load_steps=4 format=3 uid="uid://bkhxcqsquw1yg"] [gd_resource type="Resource" script_class="FuncGodotMapSettings" load_steps=5 format=3 uid="uid://bkhxcqsquw1yg"]
[ext_resource type="Material" uid="uid://cvex6toty8yn7" path="res://addons/func_godot/textures/default_material.tres" id="1_8l5wm"] [ext_resource type="Material" uid="uid://cvex6toty8yn7" path="res://addons/func_godot/textures/default_material.tres" id="1_8l5wm"]
[ext_resource type="Script" uid="uid://dhlc0pfo61428" path="res://addons/func_godot/src/map/func_godot_map_settings.gd" id="1_dlf23"] [ext_resource type="Script" uid="uid://38q6k0ctahjn" path="res://addons/func_godot/src/map/func_godot_map_settings.gd" id="1_dlf23"]
[ext_resource type="Resource" uid="uid://crgpdahjaj" path="res://addons/func_godot/fgd/func_godot_fgd.tres" id="1_hd7se"] [ext_resource type="Resource" uid="uid://crgpdahjaj" path="res://addons/func_godot/fgd/func_godot_fgd.tres" id="2_hf4oi"]
[ext_resource type="Script" uid="uid://cij36hpqc46c" path="res://addons/func_godot/src/import/quake_wad_file.gd" id="4_576s4"]
[resource] [resource]
script = ExtResource("1_dlf23") script = ExtResource("1_dlf23")
inverse_scale_factor = 32.0 inverse_scale_factor = 32.0
entity_fgd = ExtResource("1_hd7se") entity_fgd = ExtResource("2_hf4oi")
entity_name_property = "" entity_name_property = ""
entity_smoothing_property = "_phong"
entity_smoothing_angle_property = "_phong_angle"
use_groups_hierarchy = false
base_texture_dir = "res://textures" base_texture_dir = "res://textures"
texture_file_extensions = Array[String](["png", "jpg", "jpeg", "bmp", "tga", "webp"]) texture_file_extensions = Array[String](["png", "jpg", "jpeg", "bmp", "tga", "webp"])
clip_texture = "clip" clip_texture = "special/clip"
skip_texture = "skip" skip_texture = "special/skip"
texture_wads = Array[Resource]([]) origin_texture = "special/origin"
texture_wads = Array[ExtResource("4_576s4")]([])
base_material_dir = ""
material_file_extension = "tres" material_file_extension = "tres"
default_material = ExtResource("1_8l5wm") default_material = ExtResource("1_8l5wm")
default_material_albedo_uniform = "" default_material_albedo_uniform = ""
@@ -24,7 +30,6 @@ roughness_map_pattern = "%s_roughness.%s"
emission_map_pattern = "%s_emission.%s" emission_map_pattern = "%s_emission.%s"
ao_map_pattern = "%s_ao.%s" ao_map_pattern = "%s_ao.%s"
height_map_pattern = "%s_height.%s" height_map_pattern = "%s_height.%s"
unshaded = false orm_map_pattern = "%s_orm.%s"
save_generated_materials = false save_generated_materials = true
uv_unwrap_texel_size = 1.0 uv_unwrap_texel_size = 2.0
use_trenchbroom_groups_hierarchy = false

View File

@@ -1,7 +1,6 @@
[gd_resource type="Resource" script_class="FuncGodotLocalConfig" load_steps=2 format=3 uid="uid://bqjt7nyekxgog"] [gd_resource type="Resource" script_class="FuncGodotLocalConfig" load_steps=2 format=3 uid="uid://bqjt7nyekxgog"]
[ext_resource type="Script" uid="uid://bjtqjywscfgdy" path="res://addons/func_godot/src/util/func_godot_local_config.gd" id="1_g8kqj"] [ext_resource type="Script" uid="uid://xsjnhahhyein" path="res://addons/func_godot/src/util/func_godot_local_config.gd" id="1_g8kqj"]
[resource] [resource]
script = ExtResource("1_g8kqj") script = ExtResource("1_g8kqj")
export_func_godot_settings = false

View File

@@ -1,21 +1,24 @@
[gd_resource type="Resource" script_class="NetRadiantCustomGamePackConfig" load_steps=5 format=3 uid="uid://cv1k2e85fo2ax"] [gd_resource type="Resource" script_class="NetRadiantCustomGamePackConfig" load_steps=6 format=3 uid="uid://cv1k2e85fo2ax"]
[ext_resource type="Resource" uid="uid://crgpdahjaj" path="res://addons/func_godot/fgd/func_godot_fgd.tres" id="1_gct4v"] [ext_resource type="Resource" uid="uid://crgpdahjaj" path="res://addons/func_godot/fgd/func_godot_fgd.tres" id="1_gct4v"]
[ext_resource type="Script" uid="uid://ckvnkpgsci7hg" path="res://addons/func_godot/src/netradiant_custom/netradiant_custom_gamepack_config.gd" id="2_en8ro"] [ext_resource type="Script" uid="uid://dfhj3me2g5j0l" path="res://addons/func_godot/src/netradiant_custom/netradiant_custom_gamepack_config.gd" id="2_en8ro"]
[ext_resource type="Resource" uid="uid://f5erfnvbg6b7" path="res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_clip.tres" id="2_w7psh"] [ext_resource type="Resource" uid="uid://f5erfnvbg6b7" path="res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_clip.tres" id="2_w7psh"]
[ext_resource type="Resource" uid="uid://cfhg30jclb4lw" path="res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_skip.tres" id="3_6gpk8"] [ext_resource type="Resource" uid="uid://cfhg30jclb4lw" path="res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_skip.tres" id="3_6gpk8"]
[ext_resource type="Resource" uid="uid://bpnj14oaufdpt" path="res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_origin.tres" id="4_8rl60"]
[resource] [resource]
script = ExtResource("2_en8ro") script = ExtResource("2_en8ro")
export_file = false
gamepack_name = "func_godot" gamepack_name = "func_godot"
game_name = "FuncGodot" game_name = "FuncGodot"
base_game_path = "" base_game_path = ""
fgd_file = ExtResource("1_gct4v") fgd_file = ExtResource("1_gct4v")
netradiant_custom_shaders = Array[Resource]([ExtResource("2_w7psh"), ExtResource("3_6gpk8")]) netradiant_custom_shaders = Array[Resource]([ExtResource("2_w7psh"), ExtResource("3_6gpk8"), ExtResource("4_8rl60")])
texture_types = PackedStringArray("png", "jpg", "jpeg", "bmp", "tga") texture_types = PackedStringArray("png", "jpg", "jpeg", "bmp", "tga")
model_types = PackedStringArray("glb", "gltf", "obj") model_types = PackedStringArray("glb", "gltf", "obj")
sound_types = PackedStringArray("wav", "ogg") sound_types = PackedStringArray("wav", "ogg")
default_scale = "1.0" default_scale = "1.0"
clip_texture = "textures/special/clip" clip_texture = "textures/special/clip"
skip_texture = "textures/special/skip" skip_texture = "textures/special/skip"
map_type = 1
default_build_menu_variables = {}
default_build_menu_commands = {}

View File

@@ -1,6 +1,6 @@
[gd_resource type="Resource" script_class="NetRadiantCustomShader" load_steps=2 format=3 uid="uid://f5erfnvbg6b7"] [gd_resource type="Resource" script_class="NetRadiantCustomShader" load_steps=2 format=3 uid="uid://f5erfnvbg6b7"]
[ext_resource type="Script" uid="uid://dtmnasjcec1cl" path="res://addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd" id="1_cuylw"] [ext_resource type="Script" uid="uid://dn86acprv4e86" path="res://addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd" id="1_cuylw"]
[resource] [resource]
script = ExtResource("1_cuylw") script = ExtResource("1_cuylw")

View File

@@ -0,0 +1,8 @@
[gd_resource type="Resource" script_class="NetRadiantCustomShader" load_steps=2 format=3 uid="uid://bpnj14oaufdpt"]
[ext_resource type="Script" uid="uid://dn86acprv4e86" path="res://addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd" id="1_ah2cp"]
[resource]
script = ExtResource("1_ah2cp")
texture_path = "textures/special/origin"
shader_attributes = Array[String](["qer_trans 0.4"])

View File

@@ -1,6 +1,6 @@
[gd_resource type="Resource" script_class="NetRadiantCustomShader" load_steps=2 format=3 uid="uid://cfhg30jclb4lw"] [gd_resource type="Resource" script_class="NetRadiantCustomShader" load_steps=2 format=3 uid="uid://cfhg30jclb4lw"]
[ext_resource type="Script" uid="uid://dtmnasjcec1cl" path="res://addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd" id="1_4ja6h"] [ext_resource type="Script" uid="uid://dn86acprv4e86" path="res://addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd" id="1_4ja6h"]
[resource] [resource]
script = ExtResource("1_4ja6h") script = ExtResource("1_4ja6h")

View File

@@ -1,17 +1,15 @@
[gd_resource type="Resource" script_class="TrenchBroomGameConfig" load_steps=8 format=3 uid="uid://b44ah5b2000wa"] [gd_resource type="Resource" script_class="TrenchBroomGameConfig" load_steps=7 format=3 uid="uid://b44ah5b2000wa"]
[ext_resource type="Resource" uid="uid://crgpdahjaj" path="res://addons/func_godot/fgd/func_godot_fgd.tres" id="1_8u1vq"] [ext_resource type="Resource" uid="uid://crgpdahjaj" path="res://addons/func_godot/fgd/func_godot_fgd.tres" id="1_8u1vq"]
[ext_resource type="Resource" uid="uid://b4xhdj0e16lop" path="res://addons/func_godot/game_config/trenchbroom/tb_face_tag_clip.tres" id="1_rsp20"] [ext_resource type="Resource" uid="uid://b4xhdj0e16lop" path="res://addons/func_godot/game_config/trenchbroom/tb_face_tag_clip.tres" id="1_rsp20"]
[ext_resource type="Resource" uid="uid://ca7377sfgj074" path="res://addons/func_godot/game_config/trenchbroom/tb_face_tag_skip.tres" id="2_166i2"] [ext_resource type="Resource" uid="uid://ca7377sfgj074" path="res://addons/func_godot/game_config/trenchbroom/tb_face_tag_skip.tres" id="2_166i2"]
[ext_resource type="Script" uid="uid://c51cy33iaei4s" path="res://addons/func_godot/src/trenchbroom/trenchbroom_game_config.gd" id="2_ns6ah"] [ext_resource type="Script" uid="uid://cx44c4vnq8bt5" path="res://addons/func_godot/src/trenchbroom/trenchbroom_game_config.gd" id="2_ns6ah"]
[ext_resource type="Resource" uid="uid://37iduqf7tpxq" path="res://addons/func_godot/game_config/trenchbroom/tb_brush_tag_func.tres" id="3_mbuv7"] [ext_resource type="Resource" uid="uid://bkjxc54mmdhbo" path="res://addons/func_godot/game_config/trenchbroom/tb_face_tag_origin.tres" id="3_stisi"]
[ext_resource type="Resource" uid="uid://co2sb1ng7cw4i" path="res://addons/func_godot/game_config/trenchbroom/tb_brush_tag_trigger.tres" id="4_y0tjm"]
[ext_resource type="Texture2D" uid="uid://decwujsyhj0qy" path="res://addons/func_godot/icon32.png" id="6_tex5j"] [ext_resource type="Texture2D" uid="uid://decwujsyhj0qy" path="res://addons/func_godot/icon32.png" id="6_tex5j"]
[resource] [resource]
script = ExtResource("2_ns6ah") script = ExtResource("2_ns6ah")
export_file = false game_name = "FuncGodot"
game_name = "maskmaker"
icon = ExtResource("6_tex5j") icon = ExtResource("6_tex5j")
map_formats = Array[Dictionary]([{ map_formats = Array[Dictionary]([{
"format": "Valve", "format": "Valve",
@@ -25,9 +23,12 @@ map_formats = Array[Dictionary]([{
}, { }, {
"format": "Quake3" "format": "Quake3"
}]) }])
textures_root_folder = "textures"
texture_exclusion_patterns = Array[String](["*_albedo", "*_ao", "*_emission", "*_height", "*_metallic", "*_normal", "*_orm", "*_roughness", "*_sss"]) texture_exclusion_patterns = Array[String](["*_albedo", "*_ao", "*_emission", "*_height", "*_metallic", "*_normal", "*_orm", "*_roughness", "*_sss"])
palette_path = "textures/palette.lmp"
fgd_file = ExtResource("1_8u1vq") fgd_file = ExtResource("1_8u1vq")
entity_scale = "32" entity_scale = "32"
brush_tags = Array[Resource]([])
brushface_tags = Array[Resource]([ExtResource("1_rsp20"), ExtResource("2_166i2"), ExtResource("3_stisi")])
default_uv_scale = Vector2(1, 1) default_uv_scale = Vector2(1, 1)
brush_tags = Array[Resource]([ExtResource("3_mbuv7"), ExtResource("4_y0tjm")]) game_config_version = 0
brushface_tags = Array[Resource]([ExtResource("1_rsp20"), ExtResource("2_166i2")])

View File

@@ -1,6 +1,6 @@
[gd_resource type="Resource" script_class="TrenchBroomTag" load_steps=2 format=3 uid="uid://37iduqf7tpxq"] [gd_resource type="Resource" script_class="TrenchBroomTag" load_steps=2 format=3 uid="uid://37iduqf7tpxq"]
[ext_resource type="Script" uid="uid://5eacgso7tkh6" path="res://addons/func_godot/src/trenchbroom/trenchbroom_tag.gd" id="1_rn13a"] [ext_resource type="Script" uid="uid://b66qdknwqpfup" path="res://addons/func_godot/src/trenchbroom/trenchbroom_tag.gd" id="1_rn13a"]
[resource] [resource]
script = ExtResource("1_rn13a") script = ExtResource("1_rn13a")

View File

@@ -1,6 +1,6 @@
[gd_resource type="Resource" script_class="TrenchBroomTag" load_steps=2 format=3 uid="uid://co2sb1ng7cw4i"] [gd_resource type="Resource" script_class="TrenchBroomTag" load_steps=2 format=3 uid="uid://co2sb1ng7cw4i"]
[ext_resource type="Script" uid="uid://5eacgso7tkh6" path="res://addons/func_godot/src/trenchbroom/trenchbroom_tag.gd" id="1_msqpk"] [ext_resource type="Script" uid="uid://b66qdknwqpfup" path="res://addons/func_godot/src/trenchbroom/trenchbroom_tag.gd" id="1_msqpk"]
[resource] [resource]
script = ExtResource("1_msqpk") script = ExtResource("1_msqpk")

View File

@@ -1,6 +1,6 @@
[gd_resource type="Resource" script_class="TrenchBroomTag" load_steps=2 format=3 uid="uid://b4xhdj0e16lop"] [gd_resource type="Resource" script_class="TrenchBroomTag" load_steps=2 format=3 uid="uid://b4xhdj0e16lop"]
[ext_resource type="Script" uid="uid://5eacgso7tkh6" path="res://addons/func_godot/src/trenchbroom/trenchbroom_tag.gd" id="1_7td58"] [ext_resource type="Script" uid="uid://b66qdknwqpfup" path="res://addons/func_godot/src/trenchbroom/trenchbroom_tag.gd" id="1_7td58"]
[resource] [resource]
script = ExtResource("1_7td58") script = ExtResource("1_7td58")

View File

@@ -0,0 +1,11 @@
[gd_resource type="Resource" script_class="TrenchBroomTag" load_steps=2 format=3 uid="uid://bkjxc54mmdhbo"]
[ext_resource type="Script" uid="uid://b66qdknwqpfup" path="res://addons/func_godot/src/trenchbroom/trenchbroom_tag.gd" id="1_enkfc"]
[resource]
script = ExtResource("1_enkfc")
tag_name = "Origin"
tag_attributes = Array[String](["transparent"])
tag_match_type = 0
tag_pattern = "origin"
texture_name = ""

View File

@@ -1,6 +1,6 @@
[gd_resource type="Resource" script_class="TrenchBroomTag" load_steps=2 format=3 uid="uid://ca7377sfgj074"] [gd_resource type="Resource" script_class="TrenchBroomTag" load_steps=2 format=3 uid="uid://ca7377sfgj074"]
[ext_resource type="Script" uid="uid://5eacgso7tkh6" path="res://addons/func_godot/src/trenchbroom/trenchbroom_tag.gd" id="1_2teqe"] [ext_resource type="Script" uid="uid://b66qdknwqpfup" path="res://addons/func_godot/src/trenchbroom/trenchbroom_tag.gd" id="1_2teqe"]
[resource] [resource]
script = ExtResource("1_2teqe") script = ExtResource("1_2teqe")

View File

@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/icon.svg-99f2c56e0c1ce867c819715c68d9c120.cte
compress/mode=0 compress/mode=0
compress/high_quality=false compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1 compress/hdr_compression=1
compress/normal_map=0 compress/normal_map=0
compress/channel_pack=0 compress/channel_pack=0
@@ -25,6 +27,10 @@ mipmaps/generate=false
mipmaps/limit=-1 mipmaps/limit=-1
roughness/mode=0 roughness/mode=0
roughness/src_normal="" 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/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/normal_map_invert_y=false process/normal_map_invert_y=false

View File

@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/icon_godambler.svg-a6dbba375ab2a45be046a1875b
compress/mode=0 compress/mode=0
compress/high_quality=false compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1 compress/hdr_compression=1
compress/normal_map=0 compress/normal_map=0
compress/channel_pack=0 compress/channel_pack=0
@@ -25,6 +27,10 @@ mipmaps/generate=false
mipmaps/limit=-1 mipmaps/limit=-1
roughness/mode=0 roughness/mode=0
roughness/src_normal="" 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/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/normal_map_invert_y=false process/normal_map_invert_y=false

View File

@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/icon_godambler3d.svg-f7df9bfe58320474198644aa
compress/mode=0 compress/mode=0
compress/high_quality=false compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1 compress/hdr_compression=1
compress/normal_map=0 compress/normal_map=0
compress/channel_pack=0 compress/channel_pack=0
@@ -25,6 +27,10 @@ mipmaps/generate=false
mipmaps/limit=-1 mipmaps/limit=-1
roughness/mode=0 roughness/mode=0
roughness/src_normal="" 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/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/normal_map_invert_y=false process/normal_map_invert_y=false

View File

@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/icon_godot_ranger.svg-8572582518f54de6403b767
compress/mode=0 compress/mode=0
compress/high_quality=false compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1 compress/hdr_compression=1
compress/normal_map=0 compress/normal_map=0
compress/channel_pack=0 compress/channel_pack=0
@@ -25,6 +27,10 @@ mipmaps/generate=false
mipmaps/limit=-1 mipmaps/limit=-1
roughness/mode=0 roughness/mode=0
roughness/src_normal="" 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/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/normal_map_invert_y=false process/normal_map_invert_y=false

View File

@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/icon_godot_ranger3d.svg-a9a2c9bcf2e8b1e07a0a9
compress/mode=0 compress/mode=0
compress/high_quality=false compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1 compress/hdr_compression=1
compress/normal_map=0 compress/normal_map=0
compress/channel_pack=0 compress/channel_pack=0
@@ -25,6 +27,10 @@ mipmaps/generate=false
mipmaps/limit=-1 mipmaps/limit=-1
roughness/mode=0 roughness/mode=0
roughness/src_normal="" 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/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/normal_map_invert_y=false process/normal_map_invert_y=false

View File

@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/icon_quake_file.svg-1718b9a2b5e0b124f6d72bb4c
compress/mode=0 compress/mode=0
compress/high_quality=false compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1 compress/hdr_compression=1
compress/normal_map=0 compress/normal_map=0
compress/channel_pack=0 compress/channel_pack=0
@@ -25,6 +27,10 @@ mipmaps/generate=false
mipmaps/limit=-1 mipmaps/limit=-1
roughness/mode=0 roughness/mode=0
roughness/src_normal="" 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/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/normal_map_invert_y=false process/normal_map_invert_y=false

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<!-- Created using Krita: https://krita.org -->
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:krita="http://krita.org/namespaces/svg/krita"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
width="23.04pt"
height="23.04pt"
viewBox="0 0 23.04 23.04">
<defs/>
<path id="shape0" transform="translate(3.32859367052032, 1.70718625021606)" fill="#ffffff" stroke-opacity="0" stroke="#000000" stroke-width="0" stroke-linecap="square" stroke-linejoin="bevel" d="M0 2.17125L5.76 4.3425L11.52 2.17125L5.76 0Z" sodipodi:nodetypes="ccccc"/><path id="shape1" transform="translate(3.49734362007209, 4.23843624987037)" fill="#ffffff" fill-rule="evenodd" stroke-opacity="0" stroke="#000000" stroke-width="0" stroke-linecap="square" stroke-linejoin="bevel" d="M0 0L0 12.96L5.4 15.12L5.49 2.16Z" sodipodi:nodetypes="ccccc"/><path id="shape01" transform="translate(9.22078101093241, 4.42406124416546)" fill="#ffffff" stroke-opacity="0" stroke="#000000" stroke-width="0" stroke-linecap="square" stroke-linejoin="bevel" d="M0 1.8L5.0175 3.78563L10.4906 1.98563L5.085 0Z" sodipodi:nodetypes="ccccc"/><path id="shape11" transform="translate(9.38953096048418, 6.58406124416546)" fill="#ffffff" fill-rule="evenodd" stroke-opacity="0" stroke="#000000" stroke-width="0" stroke-linecap="square" stroke-linejoin="bevel" d="M0 0L0.03375 1.97438L4.69125 3.94875L4.6575 1.97438Z" sodipodi:nodetypes="ccccc"/><path id="shape011" transform="matrix(-1 0 0 1 19.705781131254 6.76968624399261)" fill="#ffffff" stroke-opacity="0" stroke="#000000" stroke-width="0" stroke-linecap="square" stroke-linejoin="bevel" d="M0.03375 0L0 1.97438L5.39438 3.76313L5.42813 1.78875Z" sodipodi:nodetypes="ccccc"/><path id="shape02" transform="translate(8.855151086652, 15.2240587499568)" fill="#ffffff" stroke-opacity="0" stroke="#000000" stroke-width="0" stroke-linecap="square" stroke-linejoin="bevel" d="M0 1.8L5.0175 3.78563L10.4906 1.98563L5.085 0Z" sodipodi:nodetypes="ccccc"/><path id="shape12" transform="translate(9.02390103620378, 17.3840587499568)" fill="#ffffff" fill-rule="evenodd" stroke-opacity="0" stroke="#000000" stroke-width="0" stroke-linecap="square" stroke-linejoin="bevel" d="M0 0L0.03375 1.97438L4.69125 3.94875L4.6575 1.97438Z" sodipodi:nodetypes="ccccc"/><path id="shape012" transform="matrix(-1 0 0 1 19.3401512069735 17.5696837497839)" fill="#ffffff" stroke-opacity="0" stroke="#000000" stroke-width="0" stroke-linecap="square" stroke-linejoin="bevel" d="M0.03375 0L0 1.97438L5.39438 3.76313L5.42813 1.78875Z" sodipodi:nodetypes="ccccc"/><path id="shape2" transform="translate(9.05484388076874, 8.91843624987036)" fill="#ffffff" fill-rule="evenodd" stroke-opacity="0" stroke="#000000" stroke-width="0" stroke-linecap="square" stroke-linejoin="bevel" d="M0 7.92L0 0L5.90625 2.16L5.805 5.76Z" sodipodi:nodetypes="ccccc"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bw74kacajcaxb"
path="res://.godot/imported/icon_slipgate.svg-f42668b28b92f93c031f56d95dfcf5a6.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/func_godot/icons/icon_slipgate.svg"
dest_files=["res://.godot/imported/icon_slipgate.svg-f42668b28b92f93c031f56d95dfcf5a6.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
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/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
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/icon_slipgate3d.svg-f125bef6ff5aa79b5fe3f232a
compress/mode=0 compress/mode=0
compress/high_quality=false compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1 compress/hdr_compression=1
compress/normal_map=0 compress/normal_map=0
compress/channel_pack=0 compress/channel_pack=0
@@ -25,6 +27,10 @@ mipmaps/generate=false
mipmaps/limit=-1 mipmaps/limit=-1
roughness/mode=0 roughness/mode=0
roughness/src_normal="" 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/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/normal_map_invert_y=false process/normal_map_invert_y=false

View File

@@ -1,7 +1,7 @@
[plugin] [plugin]
name="FuncGodot" name="FuncGodot"
description="Quake .map file support for Godot." description="Quake .map and Half-Life .vmf file support for Godot."
author="Shifty, Hannah Crawford, Emberlynn Bland, Tim Maccabe" author="Josh Palmer, Hannah Crawford, Emberlynn Bland, Tim Maccabe, Vera Lux"
version="2024.1" version="2025.9"
script="src/func_godot_plugin.gd" script="src/func_godot_plugin.gd"

View File

@@ -0,0 +1,190 @@
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
class_name FuncGodotData
## Container that holds various data structs to be used in the [FuncGodotMap] build process.
##
## FuncGodot utilizes multiple custom data structs to hold information parsed from the map file
## and read and modified by the other core build classes.
## All data structs extend from [RefCounted], therefore all data is passed by reference.
## [br][br]
## [FuncGodotData.FaceData][br]
## [FuncGodotData.BrushData][br]
## [FuncGodotData.PatchData][br]
## [FuncGodotData.GroupData][br]
## [FuncGodotData.EntityData][br]
## Data struct representing both a single map plane and a mesh face. Generated during parsing by plane definitions in the map file,
## it is further modified and utilized during the geo generation stage to create the final entity meshes.
class FaceData extends RefCounted:
## Vertex array for the face. Only populated in combination with other faces, as a result of planar intersections.
var vertices: PackedVector3Array = []
## Index array for the face. Used in ArrayMesh creation.
var indices: PackedInt32Array = []
## Vertex normal array for the face.
## By default, set to the planar normal, which results in flat shading. May be modified to adjust shading.
var normals: PackedVector3Array = []
## Tangent data for the face.
var tangents: PackedFloat32Array = []
## Local path to the texture without the extension, relative to the FuncGodotMap node's settings' base texture directory.
var texture: String
## UV transform data generated during the parsing stage. Used for both Standard and Valve 220 UV formats,
## though rotation is not applied to the transform when using Valve 220.
var uv: Transform2D
## Raw vector data provided by the Valve 220 format during parsing. It is used to calculate rotations.
## The presence of this data determines how face UVs and tangents are calculated.
var uv_axes: PackedVector3Array = []
## Raw plane data parsed from the map file using the id Tech coordinate system.
var plane: Plane
## Returns the average position of all vertices in the face. Only valid when the face has at least one vertex.
func get_centroid() -> Vector3:
return FuncGodotUtil.op_vec3_avg(vertices)
## Returns an arbitrary coplanar direction to use for winding the face.
## Only valid when the face has at least two vertices.
func get_basis() -> Vector3:
if vertices.size() < 2:
push_error("Cannot get winding basis without at least 2 vertices!")
return Vector3.ZERO
return (vertices[1] - vertices[0]).normalized()
## Prepares the face for OpenGL triangle winding order.
## Sorts the vertex array in-place by angle from the centroid.
func wind() -> void:
var centroid: Vector3 = get_centroid()
var u_axis: Vector3 = get_basis()
var v_axis: Vector3 = u_axis.cross(plane.normal).normalized()
var cmp_winding_angle: Callable = (
func(a: Vector3, b: Vector3) -> bool:
var dir_a: Vector3 = a - centroid
var dir_b: Vector3 = b - centroid
var angle_a: float = atan2(dir_a.dot(v_axis), dir_a.dot(u_axis))
var angle_b: float = atan2(dir_b.dot(v_axis), dir_b.dot(u_axis))
return angle_a < angle_b
)
var _vertices: Array[Vector3]
_vertices.assign(vertices)
_vertices.sort_custom(cmp_winding_angle)
vertices = _vertices
## Repopulate the [member indices] array to create a triangle fan.
## The face must be properly wound for the resulting indices to be valid.
func index_vertices() -> void:
var tri_count: int = vertices.size() - 2
indices.resize(tri_count * 3)
var index: int = 0
for i in tri_count:
indices[index] = 0
indices[index + 1] = i + 1
indices[index + 2] = i + 2
index += 3
## Data struct representing a single map format brush. It is largely meant as a container for [FuncGodotData.FaceData] data.
class BrushData extends RefCounted:
## Raw plane data parsed from the map file using the id Tech coordinate system.
var planes: Array[Plane]
## Collection of [FuncGodotData.FaceData].
var faces: Array[FaceData]
## [code]true[/code] if this brush is completely covered in the [i]Origin[/i] texture defined in [FuncGodotMapSettings].
## Determined during [FuncGodotParser] and utilized during [FuncGodotGeometryGenerator].
var origin: bool = false
## Data struct representing a patch def entity.
class PatchData extends RefCounted:
## Local path to the texture without the extension, relative to the FuncGodotMap node's settings' base texture directory.
var texture: String
var size: PackedInt32Array
var points: PackedVector3Array
var uvs: PackedVector2Array
## Data struct representing a TrenchBroom Group, TrenchBroom Layer, or Valve VisGroup.
## Generated during the parsing stage and utilized during both parsing and entity assembly stages.
class GroupData extends RefCounted:
enum GroupType { GROUP, LAYER, }
## Defines whether the group is a Group or a Layer. Currently only determines the name of the group.
var type: GroupType = GroupType.GROUP
## Group ID retrieved from the map file. Utilized during the parsing and entity assembly stages to determine
## which entities belong to which groups as well as which groups are children of other groups.
var id: int
## Generated during the parsing stage using the format of type_id_name, eg: group_2_Arkham.
var name: String
## ID of the parent group data, used to determine which group data is this group's parent.
var parent_id: int = -1
## Pointer to another group data that this group is a child of.
var parent: GroupData = null
## Pointer to generated Node3D representing this group in the SceneTree.
var node: Node3D = null
## If true, erases all entities assigned to this group and then the group itself at the end of the parsing stage, preventing those entities from being generated into nodes.
## Can be set in TrenchBroom on layers using the "omit layer" option.
var omit: bool = false
## Data struct representing a map format entity.
class EntityData extends RefCounted:
## All of the entity's key value pairs from the map file, retrieved during parsing.
## The func_godot_properties dictionary generated at the end of entity assembly is derived from this.
var properties: Dictionary = {}
## The entity's brush data collected during the parsing stage. If the entity's FGD resource cannot be found,
## the presence of a single brush determines this entity to be a Solid Entity.
var brushes: Array[BrushData] = []
## The entity's patch def data collected during the parsing stage. If the entity's FGD resource cannot be found,
## the presence of a single patch def determines this entity to be a Solid Entity.
var patches: Array[PatchData] = []
## Pointer to the group data this entity belongs to.
var group: GroupData = null
## The entity's FGD resource, determined by matching the classname properties of each.
## This can only be a [FuncGodotFGDSolidClass], [FuncGodotFGDPointClass], or [FuncGodotFGDModelPointClass].
var definition: FuncGodotFGDEntityClass = null
## Mesh resource generated during the geometry generation stage and applied during the entity assembly stage.
var mesh: ArrayMesh = null
## MeshInstance3D node generated during the entity assembly stage.
var mesh_instance: MeshInstance3D = null
## Optional mesh metadata compiled during the geometry generation stage, used to determine face information from collision.
var mesh_metadata: Dictionary = {}
## A collection of collision shape resources generated during the geometry generation stage and applied during the entity assembly stage.
var shapes: Array[Shape3D] = []
## A collection of [CollisionShape3D] nodes generated during the entity assembly stage. Each node corresponds to a shape in the [member shapes] array.
var collision_shapes: Array[CollisionShape3D] = []
## [OccluderInstance3D] node generated during the entity assembly stage using the [member mesh] resource.
var occluder_instance: OccluderInstance3D = null
## True global position of the entity's generated node that the mesh's vertices are offset by during the geometry generation stage.
var origin: Vector3 = Vector3.ZERO
## Checks the entity's FGD resource definition, returning whether the Solid Class has a [MeshInstance3D] built for it.
func is_visual() -> bool:
return (definition
and definition is FuncGodotFGDSolidClass
and definition.build_visuals)
## Checks the entity's FGD resource definition, returning whether the Solid Class CollisionShapeType is set to Convex.
func is_collision_convex() -> bool:
return (definition
and definition is FuncGodotFGDSolidClass
and definition.collision_shape_type == FuncGodotFGDSolidClass.CollisionShapeType.CONVEX
)
## Checks the entity's FGD resource definition, returning whether the Solid Class CollisionShapeType is set to Concave.
func is_collision_concave() -> bool:
return (definition
and definition is FuncGodotFGDSolidClass
and definition.collision_shape_type == FuncGodotFGDSolidClass.CollisionShapeType.CONCAVE
)
## Determines if the entity's mesh should be processed for normal smoothing.
## The smoothing property can be retrieved from [member FuncGodotMapSettings.entity_smoothing_property].
func is_smooth_shaded(smoothing_property: String = "_phong") -> bool:
return properties.get(smoothing_property, "0").to_int()
## Retrieves the entity's smoothing angle to determine if the face should be smoothed.
## The smoothing angle property can be retrieved from [member FuncGodotMapSettings.entity_smoothing_angle_property].
func get_smoothing_angle(smoothing_angle_property: String = "_phong_angle") -> float:
return properties.get(smoothing_angle_property, "89.0").to_float()
class VertexGroupData:
## Faces this vertex appears in.
var faces: Array[FaceData]
## Index within the associated face for this vertex.
var face_indices: PackedInt32Array
class ParseData:
var entities: Array[EntityData] = []
var groups: Array[GroupData] = []

View File

@@ -0,0 +1 @@
uid://cqye8dehq4c7q

View File

@@ -0,0 +1,451 @@
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
class_name FuncGodotEntityAssembler extends RefCounted
## Entity assembly class that is instantiated by a [FuncGodotMap] node.
const _SIGNATURE: String = "[ENT]"
# Namespacing
const _GroupData := FuncGodotData.GroupData
const _EntityData := FuncGodotData.EntityData
# Class members
## [FuncGodotMapSettings] provided by the [FuncGodotMap] during the build process.
var map_settings: FuncGodotMapSettings = null
## [enum FuncGodotMap.BuildFlags] that may affect the build process provided by the [FuncGodotMap].
var build_flags: int = 0
# Signals
## Emitted when a step in the entity assembly process is completed.
## It is connected to [method FuncGodotUtil.print_profile_info] method if [member FuncGodotMap.build_flags] SHOW_PROFILE_INFO flag is set.
signal declare_step(step: String)
func _init(settings: FuncGodotMapSettings) -> void:
map_settings = settings
## Attempts to retrieve a [Script] via class name, to allow for [GDScript] class instantiation.
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
## Generates a [Node3D] for a group's [SceneTree] representation and links the new [Node3D] to that group.
func generate_group_node(group_data: _GroupData) -> Node3D:
var group_node := Node3D.new()
group_node.name = group_data.name
group_data.node = group_node
return group_node
## Generates and assembles a new [Node] based upon processed [FuncGodotData.EntityData]. Depending upon provided data,
## additional [MeshInstance3D], [CollisionShape3D], and [OccluderInstance3D] nodes may also be generated.
func generate_solid_entity_node(node: Node, node_name: String, data: _EntityData, definition: FuncGodotFGDSolidClass) -> Node:
if definition.spawn_type == FuncGodotFGDSolidClass.SpawnType.MERGE_WORLDSPAWN:
return null
if definition.node_class != "":
if ClassDB.class_exists(definition.node_class):
node = ClassDB.instantiate(definition.node_class)
else:
var script: Script = get_script_by_class_name(definition.node_class)
if script is GDScript:
node = (script as GDScript).new()
else:
node = Node3D.new()
node.name = node_name
node_name = node_name.trim_suffix(definition.classname).trim_suffix("_")
var properties: Dictionary = data.properties
# Mesh Instance generation
if data.mesh:
var mesh_instance := MeshInstance3D.new()
mesh_instance.name = node_name + "_mesh_instance"
mesh_instance.mesh = data.mesh
mesh_instance.gi_mode = GeometryInstance3D.GI_MODE_DISABLED
if definition.global_illumination_mode:
mesh_instance.gi_mode = definition.global_illumination_mode
mesh_instance.cast_shadow = definition.shadow_casting_setting
mesh_instance.layers = definition.render_layers
node.add_child(mesh_instance)
data.mesh_instance = mesh_instance
# Occluder generation
if definition.build_occlusion and data.mesh:
var verts: PackedVector3Array = []
var indices: PackedInt32Array = []
var index: int = 0
for surf_idx in range(data.mesh.get_surface_count()):
var vert_count: int = verts.size()
var surf_array: Array = data.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.new()
occluder.set_arrays(verts, indices)
var occluder_instance := OccluderInstance3D.new()
occluder_instance.name = node_name + "_occluder_instance"
occluder_instance.occluder = occluder
node.add_child(occluder_instance)
data.occluder_instance = occluder_instance
if not (build_flags & FuncGodotMap.BuildFlags.DISABLE_SMOOTHING) and data.is_smooth_shaded(map_settings.entity_smoothing_property):
mesh_instance.mesh = FuncGodotUtil.smooth_mesh_by_angle(data.mesh, data.get_smoothing_angle(map_settings.entity_smoothing_angle_property))
# Collision generation
if data.shapes.size() and node is CollisionObject3D:
node.collision_layer = definition.collision_layer
node.collision_mask = definition.collision_mask
node.collision_priority = definition.collision_priority
var shape_to_face_array : Array[PackedInt32Array] = []
if data.mesh_metadata.has('shape_to_face_array'):
shape_to_face_array = data.mesh_metadata['shape_to_face_array']
data.mesh_metadata.erase('shape_to_face_array')
# Generate CollisionShape3D nodes and apply shapes
var face_index_metadata : Dictionary[String, PackedInt32Array] = {}
for i in data.shapes.size():
var shape := data.shapes[i]
var collision_shape := CollisionShape3D.new()
if definition.collision_shape_type == FuncGodotFGDSolidClass.CollisionShapeType.CONCAVE:
collision_shape.name = node_name + "_collision_shape"
else:
collision_shape.name = node_name + "_brush_%s_collision_shape" % i
collision_shape.shape = shape
collision_shape.shape.margin = definition.collision_shape_margin
collision_shape.owner = node.owner
node.add_child(collision_shape)
data.collision_shapes.append(collision_shape)
if shape_to_face_array.size() > i:
face_index_metadata[collision_shape.name] = shape_to_face_array[i]
if definition.add_collision_shape_to_face_indices_metadata:
data.mesh_metadata['collision_shape_to_face_indices_map'] = face_index_metadata
if "position" in node:
if node.position is Vector3:
node.position = FuncGodotUtil.id_to_opengl(data.origin) * map_settings.scale_factor
if not data.mesh_metadata.is_empty():
node.set_meta("func_godot_mesh_data", data.mesh_metadata)
return node
## Generates and assembles a new [Node] or [PackedScene] based upon processed [FuncGodotData.EntityData].
func generate_point_entity_node(node: Node, node_name: String, properties: Dictionary, definition: FuncGodotFGDPointClass) -> Node:
var classname: String = properties["classname"]
if definition.scene_file:
var flag: PackedScene.GenEditState = PackedScene.GEN_EDIT_STATE_DISABLED
if Engine.is_editor_hint():
flag = PackedScene.GEN_EDIT_STATE_INSTANCE
node = definition.scene_file.instantiate(flag)
elif definition.node_class != "":
if ClassDB.class_exists(definition.node_class):
node = ClassDB.instantiate(definition.node_class)
else:
var script: Script = get_script_by_class_name(definition.node_class)
if script is GDScript:
node = (script as GDScript).new()
else:
node = Node3D.new()
node.name = node_name
if "rotation_degrees" in node and 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 definition.classname.begins_with("light"):
angles = Vector3(angles_raw[1], angles_raw[0], -angles_raw[2])
elif 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 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
if "origin" in properties:
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)
return node
## Converts the [String] values of the entity data's [code]properties[/code] [Dictionary] to various [Variant] formats
## based upon the [FuncGodotFGDEntity]'s class properties, then attempts to send those properties to a [code]func_godot_properties[/code] [Dictionary]
## and an [code]_func_godot_apply_properties(properties: Dictionary)[/code] method on the node. A deferred call to [code]_func_godot_build_complete()[/code] is also made.
func apply_entity_properties(node: Node, data: _EntityData) -> void:
var properties: Dictionary = data.properties
if data.definition:
var def := data.definition
for property in properties:
var prop_string = properties[property]
if property in def.class_properties:
var prop_default: Variant = def.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 \'" + def.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 \'" + def.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 \'" + def.classname + "\': " + prop_string)
properties[property] = prop_color
TYPE_DICTIONARY:
var prop_desc = def.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 \'" + def.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 \'" + def.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 \'" + def.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 \'" + def.classname + "\': " + prop_string)
properties[property] = prop_vec
TYPE_STRING_NAME:
properties[property] = StringName(prop_string)
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 def.class_properties:
if not property in properties:
var prop_default: Variant = def.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 = def.class_property_descriptions.get(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]
elif prop_default.size():
properties[property] = prop_default[prop_default.keys().front()]
else:
properties[property] = 0
# Materials, Shaders, and Sounds
elif prop_default is Resource:
properties[property] = prop_default.resource_path
# Target Destination and Target Source
elif prop_default is NodePath or prop_default is Object or prop_default == null:
properties[property] = ""
# Everything else
else:
properties[property] = prop_default
if def.auto_apply_to_matching_node_properties:
for property in properties:
if property in node:
if typeof(node.get(property)) == typeof(properties[property]):
node.set(property, properties[property])
else:
push_error("Entity %s property \'%s\' type mismatch with matching generated node property." % [node.name, property])
if "func_godot_properties" in node:
node.func_godot_properties = properties
if node.has_method("_func_godot_apply_properties"):
node.call("_func_godot_apply_properties", properties)
if node.has_method("_func_godot_build_complete"):
node.call_deferred("_func_godot_build_complete")
## Generate a [Node] from [FuncGodotData.EntityData]. The returned node value can be [code]null[/code],
## in the case of [FuncGodotFGDSolidClass] entities with no [FuncGodotData.BrushData] entries.
func generate_entity_node(entity_data: _EntityData, entity_index: int) -> Node:
var node: Node = null
var node_name: String = "entity_%s" % entity_index
var properties: Dictionary = entity_data.properties
var entity_def: FuncGodotFGDEntityClass = entity_data.definition
if "classname" in entity_data.properties:
var classname: String = properties["classname"]
node_name += "_" + properties["classname"]
var default_point_def := FuncGodotFGDPointClass.new()
var default_solid_def := FuncGodotFGDSolidClass.new()
default_solid_def.collision_shape_type = FuncGodotFGDSolidClass.CollisionShapeType.NONE
if entity_def:
var name_prop: String
if entity_def.name_property in properties:
name_prop = str(properties[entity_def.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_def is FuncGodotFGDSolidClass:
node = generate_solid_entity_node(node, node_name, entity_data, entity_def)
elif entity_def is FuncGodotFGDPointClass:
node = generate_point_entity_node(node, node_name, properties, entity_def)
else:
push_error("Invalid entity definition for \"" + node_name + "\". Entity definition must be Solid Class or Point Class.")
node = generate_point_entity_node(node, node_name, properties, default_point_def)
if node and entity_def.script_class:
node.set_script(entity_def.script_class)
else:
push_error("No entity definition found for \"" + node_name + "\"")
if entity_data.brushes.size():
node = generate_solid_entity_node(node, node_name, entity_data, default_solid_def)
else:
node = generate_point_entity_node(node, node_name, properties, default_point_def)
return node
## Main entity assembly process called by [FuncGodotMap]. Generates and sorts group nodes in the [SceneTree] first,
## then generates and assembles [Node]s based upon the provided [FuncGodotData.EntityData] and adds them to the [SceneTree].
func build(map_node: FuncGodotMap, entities: Array[_EntityData], groups: Array[_GroupData]) -> void:
var scene_root := map_node.get_tree().edited_scene_root if map_node.get_tree() else map_node
build_flags = map_node.build_flags
if map_settings.use_groups_hierarchy:
declare_step.emit("Generating %s groups" % groups.size())
# Generate group nodes
for group in groups:
group.node = generate_group_node(group)
# Sort hierarchy and add them to the map
for group in groups:
if group.parent_id < 0:
map_node.add_child(group.node)
group.node.owner = scene_root
else:
for parent in groups:
if group.parent_id == parent.id:
parent.node.add_child(group.node)
group.node.owner = scene_root
declare_step.emit("Groups generation and sorting complete")
declare_step.emit("Assembling %s entities" % entities.size())
var entity_node: Node = null
for entity_index in entities.size():
var entity_data : _EntityData = entities[entity_index]
entity_node = generate_entity_node(entity_data, entity_index)
if entity_node:
if not map_settings.use_groups_hierarchy or not entity_data.group:
map_node.add_child(entity_node)
if entity_index == 0:
map_node.move_child(entity_node, 0)
elif map_settings.use_groups_hierarchy:
for group in groups:
if entity_data.group.id == group.id:
group.node.add_child(entity_node)
entity_node.owner = scene_root
if entity_data.mesh_instance:
entity_data.mesh_instance.owner = scene_root
for shape in entity_data.collision_shapes:
if shape:
shape.owner = scene_root
if entity_data.occluder_instance:
entity_data.occluder_instance.owner = scene_root
apply_entity_properties(entity_node, entity_data)
declare_step.emit("Entity assembly and property application complete")

View File

@@ -0,0 +1 @@
uid://dh73tfvwp7kr6

View File

@@ -1,139 +0,0 @@
class_name FuncGodot extends RefCounted
var map_data:= FuncGodotMapData.new()
var map_parser:= FuncGodotMapParser.new(map_data)
var geo_generator = preload("res://addons/func_godot/src/core/func_godot_geo_generator.gd").new(map_data)
var surface_gatherer:= FuncGodotSurfaceGatherer.new(map_data)
func load_map(filename: String, keep_tb_groups: bool) -> void:
map_parser.load(filename, keep_tb_groups)
func get_texture_list() -> PackedStringArray:
var g_textures: PackedStringArray
var tex_count: int = map_data.textures.size()
g_textures.resize(tex_count)
for i in range(tex_count):
g_textures.set(i, map_data.textures[i].name)
return g_textures
func set_entity_definitions(entity_defs: Dictionary) -> void:
for i in range(entity_defs.size()):
var classname: String = entity_defs.keys()[i]
var spawn_type: int = entity_defs.values()[i].get("spawn_type", FuncGodotMapData.FuncGodotEntitySpawnType.ENTITY)
var origin_type: int = entity_defs.values()[i].get("origin_type", FuncGodotMapData.FuncGodotEntityOriginType.IGNORE)
map_data.set_entity_types_by_classname(classname, spawn_type, origin_type)
func generate_geometry(texture_dict: Dictionary) -> void:
var keys: Array = texture_dict.keys()
for key in keys:
var val: Vector2 = texture_dict[key]
map_data.set_texture_size(key, val.x, val.y)
geo_generator.run()
func get_entity_dicts() -> Array:
var ent_dicts: Array
for entity in map_data.entities:
var dict: Dictionary
dict["brush_count"] = entity.brushes.size()
# TODO: This is a horrible remnant of the worldspawn layer system, remove it.
var brush_indices: PackedInt64Array
brush_indices.resize(entity.brushes.size())
for b in range(entity.brushes.size()):
brush_indices[b] = b
dict["brush_indices"] = brush_indices
dict["center"] = Vector3(entity.center.y, entity.center.z, entity.center.x)
dict["properties"] = entity.properties
ent_dicts.append(dict)
return ent_dicts
func gather_texture_surfaces_mt(texture_name: String, clip_filter_texture: String, skip_filter_texture: String, inverse_scale_factor: float) -> Array:
var sg: FuncGodotSurfaceGatherer = FuncGodotSurfaceGatherer.new(map_data)
sg.reset_params()
sg.split_type = FuncGodotSurfaceGatherer.SurfaceSplitType.ENTITY
sg.set_texture_filter(texture_name)
sg.set_clip_filter_texture(clip_filter_texture)
sg.set_skip_filter_texture(skip_filter_texture)
sg.run()
return _fetch_surfaces_internal(sg, inverse_scale_factor)
func gather_worldspawn_layer_surfaces(texture_name: String, clip_filter_texture: String, skip_filter_texture: String) -> void:
_gather_texture_surfaces_internal(texture_name, clip_filter_texture, skip_filter_texture)
func gather_entity_convex_collision_surfaces(entity_idx: int) -> void:
_gather_convex_collision_surfaces(entity_idx)
func gather_entity_concave_collision_surfaces(entity_idx: int, skip_filter_texture: String) -> void:
_gather_concave_collision_surfaces(entity_idx, skip_filter_texture)
func gather_worldspawn_layer_collision_surfaces(entity_idx: int) -> void:
_gather_convex_collision_surfaces(entity_idx)
func fetch_surfaces(inverse_scale_factor: float) -> Array:
return _fetch_surfaces_internal(surface_gatherer, inverse_scale_factor)
func _fetch_surfaces_internal(surf_gatherer: FuncGodotSurfaceGatherer, inverse_scale_factor: float) -> Array:
var surfs: Array[FuncGodotMapData.FuncGodotFaceGeometry] = surf_gatherer.out_surfaces
var surf_array: Array
for surf in surfs:
if surf == null or surf.vertices.size() == 0:
surf_array.append(null)
continue
var vertices: PackedVector3Array
var normals: PackedVector3Array
var tangents: PackedFloat64Array
var uvs: PackedVector2Array
for v in surf.vertices:
vertices.append(Vector3(v.vertex.y, v.vertex.z, v.vertex.x) / inverse_scale_factor)
normals.append(Vector3(v.normal.y, v.normal.z, v.normal.x))
tangents.append(v.tangent.y)
tangents.append(v.tangent.z)
tangents.append(v.tangent.x)
tangents.append(v.tangent.w)
uvs.append(Vector2(v.uv.x, v.uv.y))
var indices: PackedInt32Array
if surf.indicies.size() > 0:
indices.append_array(surf.indicies)
var brush_array: Array
brush_array.resize(Mesh.ARRAY_MAX)
brush_array[Mesh.ARRAY_VERTEX] = vertices
brush_array[Mesh.ARRAY_NORMAL] = normals
brush_array[Mesh.ARRAY_TANGENT] = tangents
brush_array[Mesh.ARRAY_TEX_UV] = uvs
brush_array[Mesh.ARRAY_INDEX] = indices
surf_array.append(brush_array)
return surf_array
# internal
func _gather_texture_surfaces_internal(texture_name: String, clip_filter_texture: String, skip_filter_texture: String) -> void:
surface_gatherer.reset_params()
surface_gatherer.split_type = FuncGodotSurfaceGatherer.SurfaceSplitType.ENTITY
surface_gatherer.set_texture_filter(texture_name)
surface_gatherer.set_clip_filter_texture(clip_filter_texture)
surface_gatherer.set_skip_filter_texture(skip_filter_texture)
surface_gatherer.run()
func _gather_convex_collision_surfaces(entity_idx: int) -> void:
surface_gatherer.reset_params()
surface_gatherer.split_type = FuncGodotSurfaceGatherer.SurfaceSplitType.BRUSH
surface_gatherer.entity_filter_idx = entity_idx
surface_gatherer.run()
func _gather_concave_collision_surfaces(entity_idx: int, skip_filter_texture: String) -> void:
surface_gatherer.reset_params()
surface_gatherer.split_type = FuncGodotSurfaceGatherer.SurfaceSplitType.NONE
surface_gatherer.entity_filter_idx = entity_idx
surface_gatherer.set_skip_filter_texture(skip_filter_texture)
surface_gatherer.run()

View File

@@ -1 +0,0 @@
uid://b13x4qa2xgrkb

View File

@@ -1,352 +0,0 @@
extends RefCounted
# Min distance between two verts in a brush before they're merged. Higher values fix angled brushes near extents.
const CMP_EPSILON:= 0.008
const UP_VECTOR:= Vector3(0.0, 0.0, 1.0)
const RIGHT_VECTOR:= Vector3(0.0, 1.0, 0.0)
const FORWARD_VECTOR:= Vector3(1.0, 0.0, 0.0)
var map_data: FuncGodotMapData
var wind_entity_idx: int = 0
var wind_brush_idx: int = 0
var wind_face_idx: int = 0
var wind_face_center: Vector3
var wind_face_basis: Vector3
var wind_face_normal: Vector3
func _init(in_map_data: FuncGodotMapData) -> void:
map_data = in_map_data
func sort_vertices_by_winding(a, b) -> bool:
var face:= map_data.entities[wind_entity_idx].brushes[wind_brush_idx].faces[wind_face_idx]
var face_geo:= map_data.entity_geo[wind_entity_idx].brushes[wind_brush_idx].faces[wind_face_idx]
var u:= wind_face_basis.normalized()
var v:= u.cross(wind_face_normal).normalized()
var loc_a = a.vertex - wind_face_center
var a_pu: float = loc_a.dot(u)
var a_pv: float = loc_a.dot(v)
var loc_b = b.vertex - wind_face_center
var b_pu: float = loc_b.dot(u)
var b_pv: float = loc_b.dot(v)
var a_angle:= atan2(a_pv, a_pu)
var b_angle:= atan2(b_pv, b_pu)
return a_angle < b_angle
func run() -> void:
# resize arrays
map_data.entity_geo.resize(map_data.entities.size())
for i in range(map_data.entity_geo.size()):
map_data.entity_geo[i] = FuncGodotMapData.FuncGodotEntityGeometry.new()
for e in range(map_data.entities.size()):
var entity:= map_data.entities[e]
var entity_geo:= map_data.entity_geo[e]
entity_geo.brushes.resize(entity.brushes.size())
for i in range(entity_geo.brushes.size()):
entity_geo.brushes[i] = FuncGodotMapData.FuncGodotBrushGeometry.new()
for b in range(entity.brushes.size()):
var brush:= entity.brushes[b]
var brush_geo:= entity_geo.brushes[b]
brush_geo.faces.resize(brush.faces.size())
for i in range(brush_geo.faces.size()):
brush_geo.faces[i] = FuncGodotMapData.FuncGodotFaceGeometry.new()
var generate_vertices_task = func(e):
var entity:= map_data.entities[e]
var entity_geo:= map_data.entity_geo[e]
entity.center = Vector3.ZERO
for b in range(entity.brushes.size()):
var brush:= entity.brushes[b]
brush.center = Vector3.ZERO
var vert_count: int = 0
generate_brush_vertices(e, b)
var brush_geo:= map_data.entity_geo[e].brushes[b]
for face in brush_geo.faces:
for vert in face.vertices:
brush.center += vert.vertex
vert_count += 1
if vert_count > 0:
brush.center /= float(vert_count)
entity.center += brush.center
if entity.brushes.size() > 0:
entity.center /= float(entity.brushes.size())
if entity.origin_type != FuncGodotMapData.FuncGodotEntityOriginType.IGNORE and 'origin' in entity.properties:
var origin_comps: PackedFloat64Array = entity.properties['origin'].split_floats(' ')
if origin_comps.size() > 2:
if entity.origin_type == FuncGodotMapData.FuncGodotEntityOriginType.ABSOLUTE:
entity.center = Vector3(origin_comps[0], origin_comps[1], origin_comps[2])
elif entity.origin_type == FuncGodotMapData.FuncGodotEntityOriginType.RELATIVE:
entity.center += Vector3(origin_comps[0], origin_comps[1], origin_comps[2])
var generate_vertices_task_id:= WorkerThreadPool.add_group_task(generate_vertices_task, map_data.entities.size(), 4, true)
WorkerThreadPool.wait_for_group_task_completion(generate_vertices_task_id)
# wind face vertices
for e in range(map_data.entities.size()):
var entity:= map_data.entities[e]
var entity_geo:= map_data.entity_geo[e]
for b in range(entity.brushes.size()):
var brush:= entity.brushes[b]
var brush_geo:= entity_geo.brushes[b]
for f in range(brush.faces.size()):
var face:= brush.faces[f]
var face_geo:= brush_geo.faces[f]
if face_geo.vertices.size() < 3:
continue
wind_entity_idx = e
wind_brush_idx = b
wind_face_idx = f
wind_face_basis = face_geo.vertices[1].vertex - face_geo.vertices[0].vertex
wind_face_center = Vector3.ZERO
wind_face_normal = face.plane_normal
for v in face_geo.vertices:
wind_face_center += v.vertex
wind_face_center /= face_geo.vertices.size()
face_geo.vertices.sort_custom(sort_vertices_by_winding)
wind_entity_idx = 0
# index face vertices
var index_faces_task:= func(e):
var entity_geo:= map_data.entity_geo[e]
for b in range(entity_geo.brushes.size()):
var brush_geo:= entity_geo.brushes[b]
for f in range(brush_geo.faces.size()):
var face_geo:= brush_geo.faces[f]
if face_geo.vertices.size() < 3:
continue
var i_count: int = 0
face_geo.indicies.resize((face_geo.vertices.size() - 2) * 3)
for i in range(face_geo.vertices.size() - 2):
face_geo.indicies[i_count] = 0
face_geo.indicies[i_count + 1] = i + 1
face_geo.indicies[i_count + 2] = i + 2
i_count += 3
var index_faces_task_id:= WorkerThreadPool.add_group_task(index_faces_task, map_data.entities.size(), 4, true)
WorkerThreadPool.wait_for_group_task_completion(index_faces_task_id)
func generate_brush_vertices(entity_idx: int, brush_idx: int) -> void:
var entity:= map_data.entities[entity_idx]
var brush:= entity.brushes[brush_idx]
var face_count: int = brush.faces.size()
var entity_geo:= map_data.entity_geo[entity_idx]
var brush_geo:= entity_geo.brushes[brush_idx]
var phong: bool = entity.properties.get("_phong", "0") == "1"
var phong_angle_str: String = entity.properties.get("_phong_angle", "89")
var phong_angle: float = float(phong_angle_str) if phong_angle_str.is_valid_float() else 89.0
for f0 in range(face_count):
var face:= brush.faces[f0]
var face_geo:= brush_geo.faces[f0]
var texture:= map_data.textures[face.texture_idx]
for f1 in range(face_count):
for f2 in range(face_count):
var vertex = intersect_face(brush.faces[f0], brush.faces[f1], brush.faces[f2])
if not vertex is Vector3:
continue
if not vertex_in_hull(brush.faces, vertex):
continue
var merged: bool = false
for f3 in range(f0):
var other_face_geo := brush_geo.faces[f3]
for i in range(len(other_face_geo.vertices)):
if other_face_geo.vertices[i].vertex.distance_to(vertex) < CMP_EPSILON:
vertex = other_face_geo.vertices[i].vertex
merged = true;
break
if merged:
break
var normal: Vector3
if phong:
var threshold:= cos((phong_angle + 0.01) * 0.0174533)
normal = face.plane_normal
if face.plane_normal.dot(brush.faces[f1].plane_normal) > threshold:
normal += brush.faces[f1].plane_normal
if face.plane_normal.dot(brush.faces[f2].plane_normal) > threshold:
normal += brush.faces[f2].plane_normal
normal = normal.normalized()
else:
normal = face.plane_normal
var uv: Vector2
var tangent: Vector4
if face.is_valve_uv:
uv = get_valve_uv(vertex, face, texture.width, texture.height)
tangent = get_valve_tangent(face)
else:
uv = get_standard_uv(vertex, face, texture.width, texture.height)
tangent = get_standard_tangent(face)
# Check for a duplicate vertex in the current face.
var duplicate_idx: int = -1
for i in range(face_geo.vertices.size()):
if face_geo.vertices[i].vertex == vertex:
duplicate_idx = i
break
if duplicate_idx < 0:
var new_face_vert:= FuncGodotMapData.FuncGodotFaceVertex.new()
new_face_vert.vertex = vertex
new_face_vert.normal = normal
new_face_vert.tangent = tangent
new_face_vert.uv = uv
face_geo.vertices.append(new_face_vert)
elif phong:
face_geo.vertices[duplicate_idx].normal += normal
# maybe optimisable?
for face_geo in brush_geo.faces:
for i in range(face_geo.vertices.size()):
face_geo.vertices[i].normal = face_geo.vertices[i].normal.normalized()
# returns null if no intersection, else intersection vertex.
func intersect_face(f0: FuncGodotMapData.FuncGodotFace, f1: FuncGodotMapData.FuncGodotFace, f2: FuncGodotMapData.FuncGodotFace):
var n0:= f0.plane_normal
var n1:= f1.plane_normal
var n2:= f2.plane_normal
var denom: float = n0.cross(n1).dot(n2)
if denom < CMP_EPSILON:
return null
return (n1.cross(n2) * f0.plane_dist + n2.cross(n0) * f1.plane_dist + n0.cross(n1) * f2.plane_dist) / denom
func vertex_in_hull(faces: Array[FuncGodotMapData.FuncGodotFace], vertex: Vector3) -> bool:
for face in faces:
var proj: float = face.plane_normal.dot(vertex)
if proj > face.plane_dist and absf(face.plane_dist - proj) > CMP_EPSILON:
return false
return true
func get_standard_uv(vertex: Vector3, face: FuncGodotMapData.FuncGodotFace, texture_width: int, texture_height: int) -> Vector2:
var uv_out: Vector2
var du:= absf(face.plane_normal.dot(UP_VECTOR))
var dr:= absf(face.plane_normal.dot(RIGHT_VECTOR))
var df:= absf(face.plane_normal.dot(FORWARD_VECTOR))
if du >= dr and du >= df:
uv_out = Vector2(vertex.x, -vertex.y)
elif dr >= du and dr >= df:
uv_out = Vector2(vertex.x, -vertex.z)
elif df >= du and df >= dr:
uv_out = Vector2(vertex.y, -vertex.z)
var angle: float = deg_to_rad(face.uv_extra.rot)
uv_out = Vector2(
uv_out.x * cos(angle) - uv_out.y * sin(angle),
uv_out.x * sin(angle) + uv_out.y * cos(angle))
uv_out.x /= texture_width
uv_out.y /= texture_height
uv_out.x /= face.uv_extra.scale_x
uv_out.y /= face.uv_extra.scale_y
uv_out.x += face.uv_standard.x / texture_width
uv_out.y += face.uv_standard.y / texture_height
return uv_out
func get_valve_uv(vertex: Vector3, face: FuncGodotMapData.FuncGodotFace, texture_width: int, texture_height: int) -> Vector2:
var uv_out: Vector2
var u_axis:= face.uv_valve.u.axis
var v_axis:= face.uv_valve.v.axis
var u_shift:= face.uv_valve.u.offset
var v_shift:= face.uv_valve.v.offset
uv_out.x = u_axis.dot(vertex);
uv_out.y = v_axis.dot(vertex);
uv_out.x /= texture_width;
uv_out.y /= texture_height;
uv_out.x /= face.uv_extra.scale_x;
uv_out.y /= face.uv_extra.scale_y;
uv_out.x += u_shift / texture_width;
uv_out.y += v_shift / texture_height;
return uv_out
func get_standard_tangent(face: FuncGodotMapData.FuncGodotFace) -> Vector4:
var du:= face.plane_normal.dot(UP_VECTOR)
var dr:= face.plane_normal.dot(RIGHT_VECTOR)
var df:= face.plane_normal.dot(FORWARD_VECTOR)
var dua:= absf(du)
var dra:= absf(dr)
var dfa:= absf(df)
var u_axis: Vector3
var v_sign: float = 0.0
if dua >= dra and dua >= dfa:
u_axis = FORWARD_VECTOR
v_sign = signf(du)
elif dra >= dua and dra >= dfa:
u_axis = FORWARD_VECTOR
v_sign = -signf(dr)
elif dfa >= dua and dfa >= dra:
u_axis = RIGHT_VECTOR
v_sign = signf(df)
v_sign *= signf(face.uv_extra.scale_y);
u_axis = u_axis.rotated(face.plane_normal, deg_to_rad(-face.uv_extra.rot) * v_sign)
return Vector4(u_axis.x, u_axis.y, u_axis.z, v_sign)
func get_valve_tangent(face: FuncGodotMapData.FuncGodotFace) -> Vector4:
var u_axis:= face.uv_valve.u.axis.normalized()
var v_axis:= face.uv_valve.v.axis.normalized()
var v_sign = -signf(face.plane_normal.cross(u_axis).dot(v_axis))
return Vector4(u_axis.x, u_axis.y, u_axis.z, v_sign)
func get_entities() -> Array[FuncGodotMapData.FuncGodotEntityGeometry]:
return map_data.entity_geo
func get_brush_vertex_count(entity_idx: int, brush_idx: int) -> int:
var vertex_count: int = 0
var brush_geo:= map_data.entity_geo[entity_idx].brushes[brush_idx]
for face in brush_geo.faces:
vertex_count += face.vertices.size()
return vertex_count
func get_brush_index_count(entity_idx: int, brush_idx: int) -> int:
var index_count: int = 0
var brush_geo:= map_data.entity_geo[entity_idx].brushes[brush_idx]
for face in brush_geo.faces:
index_count += face.indicies.size()
return index_count

View File

@@ -1 +0,0 @@
uid://bc8yx7le80s04

View File

@@ -1,135 +0,0 @@
class_name FuncGodotMapData extends RefCounted
var entities: Array[FuncGodotMapData.FuncGodotEntity]
var entity_geo: Array[FuncGodotMapData.FuncGodotEntityGeometry]
var textures: Array[FuncGodotMapData.FuncGodotTextureData]
func register_texture(name: String) -> int:
for i in range(textures.size()):
if textures[i].name == name:
return i
textures.append(FuncGodotTextureData.new(name))
return textures.size() - 1
func set_texture_size(name: String, width: int, height: int) -> void:
for i in range(textures.size()):
if textures[i].name == name:
textures[i].width = width
textures[i].height = height
return
func find_texture(texture_name: String) -> int:
for i in range(textures.size()):
if textures[i].name == texture_name:
return i
return -1
func set_entity_types_by_classname(classname: String, spawn_type: int, origin_type: int) -> void:
for entity in entities:
if entity.properties.has("classname") and entity.properties["classname"] == classname:
entity.spawn_type = spawn_type as FuncGodotMapData.FuncGodotEntitySpawnType
if entity.spawn_type == FuncGodotMapData.FuncGodotEntitySpawnType.ENTITY:
entity.origin_type = origin_type as FuncGodotMapData.FuncGodotEntityOriginType
else:
entity.origin_type = FuncGodotMapData.FuncGodotEntityOriginType.IGNORE
func clear() -> void:
entities.clear()
entity_geo.clear()
textures.clear()
# --------------------------------------------------------------------------------------------------
# Nested Types
# --------------------------------------------------------------------------------------------------
enum FuncGodotEntitySpawnType {
WORLDSPAWN = 0,
MERGE_WORLDSPAWN = 1,
ENTITY = 2
}
enum FuncGodotEntityOriginType {
IGNORE = 0,
ABSOLUTE = 1,
RELATIVE = 2
}
class FuncGodotFacePoints:
var v0: Vector3
var v1: Vector3
var v2: Vector3
class FuncGodotValveTextureAxis:
var axis: Vector3
var offset: float
class FuncGodotValveUV:
var u: FuncGodotValveTextureAxis
var v: FuncGodotValveTextureAxis
func _init() -> void:
u = FuncGodotValveTextureAxis.new()
v = FuncGodotValveTextureAxis.new()
class FuncGodotFaceUVExtra:
var rot: float
var scale_x: float
var scale_y: float
class FuncGodotFace:
var plane_points: FuncGodotFacePoints
var plane_normal: Vector3
var plane_dist: float
var texture_idx: int
var is_valve_uv: bool
var uv_standard: Vector2
var uv_valve: FuncGodotValveUV
var uv_extra: FuncGodotFaceUVExtra
func _init() -> void:
plane_points = FuncGodotFacePoints.new()
uv_valve = FuncGodotValveUV.new()
uv_extra = FuncGodotFaceUVExtra.new()
class FuncGodotBrush:
var faces: Array[FuncGodotFace]
var center: Vector3
class FuncGodotEntity:
var properties: Dictionary
var brushes: Array[FuncGodotBrush]
var center: Vector3
var spawn_type: FuncGodotEntitySpawnType
var origin_type: FuncGodotEntityOriginType
class FuncGodotFaceVertex:
var vertex: Vector3
var normal: Vector3
var uv: Vector2
var tangent: Vector4
func duplicate() -> FuncGodotFaceVertex:
var new_vert := FuncGodotFaceVertex.new()
new_vert.vertex = vertex
new_vert.normal = normal
new_vert.uv = uv
new_vert.tangent = tangent
return new_vert
class FuncGodotFaceGeometry:
var vertices: Array[FuncGodotFaceVertex]
var indicies: Array[int]
class FuncGodotBrushGeometry:
var faces: Array[FuncGodotFaceGeometry]
class FuncGodotEntityGeometry:
var brushes: Array[FuncGodotBrushGeometry]
class FuncGodotTextureData:
var name: String
var width: int
var height: int
func _init(in_name: String):
name = in_name

View File

@@ -1 +0,0 @@
uid://ct2p43hd44co8

View File

@@ -1,307 +0,0 @@
class_name FuncGodotMapParser extends RefCounted
var scope:= FuncGodotMapParser.ParseScope.FILE
var comment: bool = false
var entity_idx: int = -1
var brush_idx: int = -1
var face_idx: int = -1
var component_idx: int = 0
var prop_key: String = ""
var current_property: String = ""
var valve_uvs: bool = false
var current_face: FuncGodotMapData.FuncGodotFace
var current_brush: FuncGodotMapData.FuncGodotBrush
var current_entity: FuncGodotMapData.FuncGodotEntity
var map_data: FuncGodotMapData
var _keep_tb_groups: bool = false
func _init(in_map_data: FuncGodotMapData) -> void:
map_data = in_map_data
func load(map_file: String, keep_tb_groups: bool) -> bool:
current_face = FuncGodotMapData.FuncGodotFace.new()
current_brush = FuncGodotMapData.FuncGodotBrush.new()
current_entity = FuncGodotMapData.FuncGodotEntity.new()
scope = FuncGodotMapParser.ParseScope.FILE
comment = false
entity_idx = -1
brush_idx = -1
face_idx = -1
component_idx = 0
valve_uvs = false
_keep_tb_groups = keep_tb_groups
var map: FileAccess = FileAccess.open(map_file, FileAccess.READ)
if map == null:
printerr("Error: Failed to open map file (" + map_file + ")")
return false
while not map.eof_reached():
var line: String = map.get_line()
if comment:
comment = false
var tokens := split_string(line, [" ", "\t"], true)
for s in tokens:
token(s)
return true
func split_string(s: String, delimeters: Array[String], allow_empty: bool = true) -> Array[String]:
var parts: Array[String] = []
var start := 0
var i := 0
while i < s.length():
if s[i] in delimeters:
if allow_empty or start < i:
parts.push_back(s.substr(start, i - start))
start = i + 1
i += 1
if allow_empty or start < i:
parts.push_back(s.substr(start, i - start))
return parts
func set_scope(new_scope: FuncGodotMapParser.ParseScope) -> void:
"""
match new_scope:
ParseScope.FILE:
print("Switching to file scope.")
ParseScope.ENTITY:
print("Switching to entity " + str(entity_idx) + "scope")
ParseScope.PROPERTY_VALUE:
print("Switching to property value scope")
ParseScope.BRUSH:
print("Switching to brush " + str(brush_idx) + " scope")
ParseScope.PLANE_0:
print("Switching to face " + str(face_idx) + " plane 0 scope")
ParseScope.PLANE_1:
print("Switching to face " + str(face_idx) + " plane 1 scope")
ParseScope.PLANE_2:
print("Switching to face " + str(face_idx) + " plane 2 scope")
ParseScope.TEXTURE:
print("Switching to texture scope")
ParseScope.U:
print("Switching to U scope")
ParseScope.V:
print("Switching to V scope")
ParseScope.VALVE_U:
print("Switching to Valve U scope")
ParseScope.VALVE_V:
print("Switching to Valve V scope")
ParseScope.ROT:
print("Switching to rotation scope")
ParseScope.U_SCALE:
print("Switching to U scale scope")
ParseScope.V_SCALE:
print("Switching to V scale scope")
"""
scope = new_scope
func token(buf_str: String) -> void:
if comment:
return
elif buf_str == "//":
comment = true
return
match scope:
FuncGodotMapParser.ParseScope.FILE:
if buf_str == "{":
entity_idx += 1
brush_idx = -1
set_scope(FuncGodotMapParser.ParseScope.ENTITY)
FuncGodotMapParser.ParseScope.ENTITY:
if buf_str.begins_with('"'):
prop_key = buf_str.substr(1)
if prop_key.ends_with('"'):
prop_key = prop_key.left(-1)
set_scope(FuncGodotMapParser.ParseScope.PROPERTY_VALUE)
elif buf_str == "{":
brush_idx += 1
face_idx = -1
set_scope(FuncGodotMapParser.ParseScope.BRUSH)
elif buf_str == "}":
commit_entity()
set_scope(FuncGodotMapParser.ParseScope.FILE)
FuncGodotMapParser.ParseScope.PROPERTY_VALUE:
var is_first = buf_str[0] == '"'
var is_last = buf_str.right(1) == '"'
if is_first:
if current_property != "":
current_property = ""
if not is_last:
current_property += buf_str + " "
else:
current_property += buf_str
if is_last:
current_entity.properties[prop_key] = current_property.substr(1, len(current_property) - 2)
set_scope(FuncGodotMapParser.ParseScope.ENTITY)
FuncGodotMapParser.ParseScope.BRUSH:
if buf_str == "(":
face_idx += 1
component_idx = 0
set_scope(FuncGodotMapParser.ParseScope.PLANE_0)
elif buf_str == "}":
commit_brush()
set_scope(FuncGodotMapParser.ParseScope.ENTITY)
FuncGodotMapParser.ParseScope.PLANE_0:
if buf_str == ")":
component_idx = 0
set_scope(FuncGodotMapParser.ParseScope.PLANE_1)
else:
match component_idx:
0:
current_face.plane_points.v0.x = float(buf_str)
1:
current_face.plane_points.v0.y = float(buf_str)
2:
current_face.plane_points.v0.z = float(buf_str)
component_idx += 1
FuncGodotMapParser.ParseScope.PLANE_1:
if buf_str != "(":
if buf_str == ")":
component_idx = 0
set_scope(FuncGodotMapParser.ParseScope.PLANE_2)
else:
match component_idx:
0:
current_face.plane_points.v1.x = float(buf_str)
1:
current_face.plane_points.v1.y = float(buf_str)
2:
current_face.plane_points.v1.z = float(buf_str)
component_idx += 1
FuncGodotMapParser.ParseScope.PLANE_2:
if buf_str != "(":
if buf_str == ")":
component_idx = 0
set_scope(FuncGodotMapParser.ParseScope.TEXTURE)
else:
match component_idx:
0:
current_face.plane_points.v2.x = float(buf_str)
1:
current_face.plane_points.v2.y = float(buf_str)
2:
current_face.plane_points.v2.z = float(buf_str)
component_idx += 1
FuncGodotMapParser.ParseScope.TEXTURE:
current_face.texture_idx = map_data.register_texture(buf_str)
set_scope(FuncGodotMapParser.ParseScope.U)
FuncGodotMapParser.ParseScope.U:
if buf_str == "[":
valve_uvs = true
component_idx = 0
set_scope(FuncGodotMapParser.ParseScope.VALVE_U)
else:
valve_uvs = false
current_face.uv_standard.x = float(buf_str)
set_scope(FuncGodotMapParser.ParseScope.V)
FuncGodotMapParser.ParseScope.V:
current_face.uv_standard.y = float(buf_str)
set_scope(FuncGodotMapParser.ParseScope.ROT)
FuncGodotMapParser.ParseScope.VALVE_U:
if buf_str == "]":
component_idx = 0
set_scope(FuncGodotMapParser.ParseScope.VALVE_V)
else:
match component_idx:
0:
current_face.uv_valve.u.axis.x = float(buf_str)
1:
current_face.uv_valve.u.axis.y = float(buf_str)
2:
current_face.uv_valve.u.axis.z = float(buf_str)
3:
current_face.uv_valve.u.offset = float(buf_str)
component_idx += 1
FuncGodotMapParser.ParseScope.VALVE_V:
if buf_str != "[":
if buf_str == "]":
set_scope(FuncGodotMapParser.ParseScope.ROT)
else:
match component_idx:
0:
current_face.uv_valve.v.axis.x = float(buf_str)
1:
current_face.uv_valve.v.axis.y = float(buf_str)
2:
current_face.uv_valve.v.axis.z = float(buf_str)
3:
current_face.uv_valve.v.offset = float(buf_str)
component_idx += 1
FuncGodotMapParser.ParseScope.ROT:
current_face.uv_extra.rot = float(buf_str)
set_scope(FuncGodotMapParser.ParseScope.U_SCALE)
FuncGodotMapParser.ParseScope.U_SCALE:
current_face.uv_extra.scale_x = float(buf_str)
set_scope(FuncGodotMapParser.ParseScope.V_SCALE)
FuncGodotMapParser.ParseScope.V_SCALE:
current_face.uv_extra.scale_y = float(buf_str)
commit_face()
set_scope(FuncGodotMapParser.ParseScope.BRUSH)
func commit_entity() -> void:
if current_entity.properties.has('_tb_type') and map_data.entities.size() > 0:
map_data.entities[0].brushes.append_array(current_entity.brushes)
current_entity.brushes.clear()
if !_keep_tb_groups:
current_entity = FuncGodotMapData.FuncGodotEntity.new()
return
var new_entity:= FuncGodotMapData.FuncGodotEntity.new()
new_entity.spawn_type = FuncGodotMapData.FuncGodotEntitySpawnType.ENTITY
new_entity.properties = current_entity.properties
new_entity.brushes = current_entity.brushes
map_data.entities.append(new_entity)
current_entity = FuncGodotMapData.FuncGodotEntity.new()
func commit_brush() -> void:
current_entity.brushes.append(current_brush)
current_brush = FuncGodotMapData.FuncGodotBrush.new()
func commit_face() -> void:
var v0v1: Vector3 = current_face.plane_points.v1 - current_face.plane_points.v0
var v1v2: Vector3 = current_face.plane_points.v2 - current_face.plane_points.v1
current_face.plane_normal = v1v2.cross(v0v1).normalized()
current_face.plane_dist = current_face.plane_normal.dot(current_face.plane_points.v0)
current_face.is_valve_uv = valve_uvs
current_brush.faces.append(current_face)
current_face = FuncGodotMapData.FuncGodotFace.new()
# Nested
enum ParseScope{
FILE,
COMMENT,
ENTITY,
PROPERTY_VALUE,
BRUSH,
PLANE_0,
PLANE_1,
PLANE_2,
TEXTURE,
U,
V,
VALVE_U,
VALVE_V,
ROT,
U_SCALE,
V_SCALE
}

View File

@@ -1 +0,0 @@
uid://baybsken3c4pi

View File

@@ -1,139 +0,0 @@
class_name FuncGodotSurfaceGatherer extends RefCounted
var map_data: FuncGodotMapData
var split_type: SurfaceSplitType = SurfaceSplitType.NONE
var entity_filter_idx: int = -1
var texture_filter_idx: int = -1
var clip_filter_texture_idx: int
var skip_filter_texture_idx: int
var out_surfaces: Array[FuncGodotMapData.FuncGodotFaceGeometry]
func _init(in_map_data: FuncGodotMapData) -> void:
map_data = in_map_data
func set_texture_filter(texture_name: String) -> void:
texture_filter_idx = map_data.find_texture(texture_name)
func set_clip_filter_texture(texture_name: String) -> void:
clip_filter_texture_idx = map_data.find_texture(texture_name)
func set_skip_filter_texture(texture_name: String) -> void:
skip_filter_texture_idx = map_data.find_texture(texture_name)
func filter_entity(entity_idx: int) -> bool:
if entity_filter_idx != -1 and entity_idx != entity_filter_idx:
return true
return false
func filter_brush(entity_idx: int, brush_idx: int) -> bool:
var entity:= map_data.entities[entity_idx]
var brush:= entity.brushes[brush_idx]
# omit brushes that are fully-textured with clip
if clip_filter_texture_idx != -1:
var fully_textured: bool = true
for face in brush.faces:
if face.texture_idx != clip_filter_texture_idx:
fully_textured = false
break
if fully_textured:
return true
return false
func filter_face(entity_idx: int, brush_idx: int, face_idx: int) -> bool:
var face:= map_data.entities[entity_idx].brushes[brush_idx].faces[face_idx]
var face_geo:= map_data.entity_geo[entity_idx].brushes[brush_idx].faces[face_idx]
if face_geo.vertices.size() < 3:
return true
if clip_filter_texture_idx != -1 and face.texture_idx == clip_filter_texture_idx:
return true
# omit faces textured with skip
if skip_filter_texture_idx != -1 and face.texture_idx == skip_filter_texture_idx:
return true
# omit filtered texture indices
if texture_filter_idx != -1 and face.texture_idx != texture_filter_idx:
return true
return false
func run() -> void:
out_surfaces.clear()
var index_offset: int = 0
var surf: FuncGodotMapData.FuncGodotFaceGeometry
if split_type == SurfaceSplitType.NONE:
surf = add_surface()
index_offset = len(out_surfaces) - 1
for e in range(map_data.entities.size()):
var entity:= map_data.entities[e]
var entity_geo:= map_data.entity_geo[e]
if filter_entity(e):
continue
if split_type == SurfaceSplitType.ENTITY:
if entity.spawn_type == FuncGodotMapData.FuncGodotEntitySpawnType.MERGE_WORLDSPAWN:
add_surface()
surf = out_surfaces[0]
index_offset = surf.vertices.size()
else:
surf = add_surface()
index_offset = surf.vertices.size()
for b in range(entity.brushes.size()):
if filter_brush(e, b):
continue
var brush:= entity.brushes[b]
var brush_geo:= entity_geo.brushes[b]
if split_type == SurfaceSplitType.BRUSH:
index_offset = 0
surf = add_surface()
for f in range(brush.faces.size()):
var face_geo:= brush_geo.faces[f]
if filter_face(e, b, f):
continue
for v in range(face_geo.vertices.size()):
var vert:= face_geo.vertices[v].duplicate()
if entity.spawn_type == FuncGodotMapData.FuncGodotEntitySpawnType.ENTITY:
vert.vertex -= entity.center
surf.vertices.append(vert)
for i in range((face_geo.vertices.size() - 2) * 3):
surf.indicies.append(face_geo.indicies[i] + index_offset)
index_offset += face_geo.vertices.size()
func add_surface() -> FuncGodotMapData.FuncGodotFaceGeometry:
var surf:= FuncGodotMapData.FuncGodotFaceGeometry.new()
out_surfaces.append(surf)
return surf
func reset_params() -> void:
split_type = SurfaceSplitType.NONE
entity_filter_idx = -1
texture_filter_idx = -1
clip_filter_texture_idx = -1
skip_filter_texture_idx = -1
# nested
enum SurfaceSplitType{
NONE,
ENTITY,
BRUSH
}

View File

@@ -1 +0,0 @@
uid://d3n8qk8adx3yp

View File

@@ -0,0 +1,567 @@
@icon("res://addons/func_godot/icons/icon_slipgate.svg")
class_name FuncGodotGeometryGenerator extends RefCounted
## Geometry generation class that is instantiated by a [FuncGodotMap] node.
const _SIGNATURE: String = "[GEO]"
# Namespacing
const _VERTEX_EPSILON := FuncGodotUtil._VERTEX_EPSILON
const _VERTEX_EPSILON2 := _VERTEX_EPSILON * _VERTEX_EPSILON
const _HYPERPLANE_SIZE := 65355.0
const _OriginType := FuncGodotFGDSolidClass.OriginType
const _GroupData := FuncGodotData.GroupData
const _EntityData := FuncGodotData.EntityData
const _BrushData := FuncGodotData.BrushData
const _PatchData := FuncGodotData.PatchData
const _FaceData := FuncGodotData.FaceData
const _VertexGroupData := FuncGodotData.VertexGroupData
# Class members
var map_settings: FuncGodotMapSettings = null
var entity_data: Array[_EntityData]
var texture_materials: Dictionary[String, Material]
var texture_sizes: Dictionary[String, Vector2]
# Signals
## Emitted when beginning a new step of the generation process.
signal declare_step(step: String)
func _init(settings: FuncGodotMapSettings = null) -> void:
map_settings = settings
#region TOOLS
func is_skip(face: _FaceData) -> bool:
return FuncGodotUtil.is_skip(face.texture, map_settings)
func is_clip(face: _FaceData) -> bool:
return FuncGodotUtil.is_clip(face.texture, map_settings)
func is_origin(face: _FaceData) -> bool:
return FuncGodotUtil.is_origin(face.texture, map_settings)
#endregion
#region PATCHES
func sample_bezier_curve(controls: Array[Vector3], t: float) -> Vector3:
var points: Array[Vector3] = controls.duplicate()
for i in controls.size():
for j in controls.size() - 1 - i:
points[j] = points[j].lerp(points[j + 1], t)
return points[0]
func sample_bezier_surface(controls: Array[Vector3], width: int, height: int, u: float, v: float) -> Vector3:
var curve: Array[Vector3] = []
for x in range(width):
var col: Array[Vector3] = []
for y in range(height):
var idx := y * width + x
col.append(controls[idx])
curve.append(sample_bezier_curve(col, v))
return sample_bezier_curve(curve, u)
# Generate patch triangle indices
func get_triangle_indices(width: int, height: int) -> Array[int]:
var indices: Array[int] = []
if width < 2 or height < 2:
return indices
for row in range(height - 1):
for col in range(width - 1):
## First triangle of the square; top left, top right, bottom left
indices.append(col + row * width)
indices.append((col + 1) + row * width)
indices.append(col + (row + 1) * width)
## Second triangle of the square; top right, bottom right, bottom left
indices.append((col + 1) + row * width)
indices.append((col + 1) + (row + 1) * width)
indices.append(col + (row + 1) * width)
return indices
func create_patch_mesh(data: Array[_PatchData], mesh: Mesh):
return
#endregion
#region BRUSHES
func generate_base_winding(plane: Plane) -> PackedVector3Array:
var up := Vector3.UP
if abs(plane.normal.dot(up)) > 0.9:
up = Vector3.RIGHT
var right: Vector3 = plane.normal.cross(up).normalized()
var forward: Vector3 = right.cross(plane.normal).normalized()
var centroid: Vector3 = plane.get_center()
# construct oversized square on the plane to clip against
var winding := PackedVector3Array()
winding.append(centroid + (right * _HYPERPLANE_SIZE) + (forward * _HYPERPLANE_SIZE))
winding.append(centroid + (right * -_HYPERPLANE_SIZE) + (forward * _HYPERPLANE_SIZE))
winding.append(centroid + (right * -_HYPERPLANE_SIZE) + (forward * -_HYPERPLANE_SIZE))
winding.append(centroid + (right * _HYPERPLANE_SIZE) + (forward * -_HYPERPLANE_SIZE))
return winding
func generate_face_vertices(brush: _BrushData, face_index: int, vertex_merge_distance: float = 0.0) -> PackedVector3Array:
var plane: Plane = brush.faces[face_index].plane
# Generate initial square polygon to clip other planes against
var winding: PackedVector3Array = generate_base_winding(plane)
for other_face_index in brush.faces.size():
if other_face_index == face_index:
continue
# NOTE: This may need to be recentered to the origin, then moved back to the correct face position
# This problem may arise from floating point inaccuracy, given a large enough initial brush
winding = Geometry3D.clip_polygon(winding, brush.faces[other_face_index].plane)
if winding.is_empty():
break
# Reduce seams between vertices
for i in winding.size():
winding.set(i, winding.get(i).snappedf(vertex_merge_distance))
return winding
func generate_brush_vertices(entity_index: int, brush_index: int) -> void:
var entity: _EntityData = entity_data[entity_index]
var brush: _BrushData = entity.brushes[brush_index]
var vertex_merge_distance: float = entity.properties.get(map_settings.vertex_merge_distance_property, 0.0) as float
for face_index in brush.faces.size():
var face: _FaceData = brush.faces[face_index]
face.vertices = generate_face_vertices(brush, face_index, vertex_merge_distance)
face.normals.resize(face.vertices.size())
face.normals.fill(face.plane.normal)
var tangent: PackedFloat32Array = FuncGodotUtil.get_face_tangent(face)
# convert into OpenGL coordinates
for i in face.vertices.size():
face.tangents.append(tangent[1]) # Y
face.tangents.append(tangent[2]) # Z
face.tangents.append(tangent[0]) # X
face.tangents.append(tangent[3]) # W
return
func generate_entity_vertices(entity_index: int) -> void:
var entity: _EntityData = entity_data[entity_index]
for brush_index in entity.brushes.size():
generate_brush_vertices(entity_index, brush_index)
func determine_entity_origins(entity_index: int) -> void:
var entity: _EntityData = entity_data[entity_index]
var origin_type := _OriginType.BRUSH
if entity.definition is not FuncGodotFGDSolidClass:
if entity.brushes.is_empty():
return
else:
origin_type = entity.definition.origin_type
if entity_index == 0:
entity.origin = Vector3.ZERO
return
var entity_mins: Vector3 = Vector3.INF
var entity_maxs: Vector3 = Vector3.INF
var origin_mins: Vector3 = Vector3.INF
var origin_maxs: Vector3 = -Vector3.INF
for brush in entity.brushes:
for face in brush.faces:
for vertex in face.vertices:
if entity_mins != Vector3.INF:
entity_mins = entity_mins.min(vertex)
else:
entity_mins = vertex
if entity_maxs != Vector3.INF:
entity_maxs = entity_maxs.max(vertex)
else:
entity_maxs = vertex
if brush.origin:
if origin_mins != Vector3.INF:
origin_mins = origin_mins.min(vertex)
else:
origin_mins = vertex
if origin_maxs != Vector3.INF:
origin_maxs = origin_maxs.max(vertex)
else:
origin_maxs = vertex
# Default origin type is BOUNDS_CENTER
if entity_maxs != Vector3.INF and entity_mins != Vector3.INF:
entity.origin = entity_maxs - ((entity_maxs - entity_mins) * 0.5)
if origin_type != _OriginType.BOUNDS_CENTER and entity.brushes.size() > 0:
match origin_type:
_OriginType.ABSOLUTE, _OriginType.RELATIVE:
if "origin" in entity.properties:
var origin_comps: PackedFloat64Array = entity.properties["origin"].split_floats(" ")
if origin_comps.size() > 2:
if entity.origin_type == _OriginType.ABSOLUTE:
entity.origin = Vector3(origin_comps[0], origin_comps[1], origin_comps[2])
else: # _OriginType.RELATIVE
entity.origin += Vector3(origin_comps[0], origin_comps[1], origin_comps[2])
_OriginType.BRUSH:
if origin_mins != Vector3.INF:
entity.origin = origin_maxs - ((origin_maxs - origin_mins) * 0.5)
_OriginType.BOUNDS_MINS:
entity.origin = entity_mins
_OriginType.BOUNDS_MAXS:
entity.origin = entity_maxs
_OriginType.AVERAGED:
entity.origin = Vector3.ZERO
var vertices: PackedVector3Array
for brush in entity.brushes:
for face in brush.faces:
vertices.append_array(face.vertices)
entity.origin = FuncGodotUtil.op_vec3_avg(vertices)
func wind_entity_faces(entity_index: int) -> void:
var entity: _EntityData = entity_data[entity_index]
for brush in entity.brushes:
for face in brush.faces:
# Faces should already be wound from the new generation process, but this should be tested further first.
face.wind()
face.index_vertices()
func smooth_entity_vertices(entity_index: int) -> void:
var entity: _EntityData = entity_data[entity_index]
if not entity.is_smooth_shaded(map_settings.entity_smoothing_property):
return
var smoothing_angle: float = deg_to_rad(entity.get_smoothing_angle(map_settings.entity_smoothing_angle_property))
var vertex_map: Dictionary[Vector3, _VertexGroupData] = {}
# Group vertices by position and build map. NOTE: Vector3 keys can suffer from floating point precision.
# However, the vertex position should have already been snapped to _VERTEX_EPSILON.
for brush in entity.brushes:
for face in brush.faces:
for i in face.vertices.size():
var pos := face.vertices[i].snappedf(_VERTEX_EPSILON)
if not vertex_map.has(pos):
vertex_map[pos] = _VertexGroupData.new()
var data := vertex_map[pos]
data.faces.append(face)
data.face_indices.append(i)
var smoothed_normals: PackedVector3Array
for vertex_group in vertex_map.values():
if vertex_group.faces.size() <= 1:
continue
# Collect final normals in a temporary arrays
# These cannot be applied until all original normals have been checked.
smoothed_normals = []
for i in vertex_group.faces.size():
var this_face: _FaceData = vertex_group.faces[i]
var this_index: int = vertex_group.face_indices[i]
var this_normal: Vector3 = this_face.normals[this_index]
var average_normal: Vector3 = this_normal
for j in vertex_group.faces.size():
# Skip this face
if i == j:
continue
var other_face: _FaceData = vertex_group.faces[j]
var other_index: int = vertex_group.face_indices[j]
var other_normal: Vector3 = other_face.normals[other_index]
if this_normal.angle_to(other_normal) <= smoothing_angle:
average_normal += other_normal
# Store the averaged normal
smoothed_normals.append(average_normal.normalized())
# Apply smoothed normals back to face data
for i in vertex_group.faces.size():
var face: _FaceData = vertex_group.faces[i]
var index: int = vertex_group.face_indices[i]
face.normals[index] = smoothed_normals[i]
return
#endregion
func generate_entity_surfaces(entity_index: int) -> void:
var entity: _EntityData = entity_data[entity_index]
# Don't build for non-solid classes or solids without any brushes.
if not entity or entity.brushes.is_empty():
return
var def: FuncGodotFGDSolidClass
if entity.definition is not FuncGodotFGDSolidClass:
def = FuncGodotFGDSolidClass.new()
else:
def = entity.definition
var op_entity_ogl_xf: Callable = func(v: Vector3) -> Vector3:
return (FuncGodotUtil.id_to_opengl(v - entity.origin) * map_settings.scale_factor)
# Surface groupings <texture_name, Array[Face]>
var surfaces: Dictionary[String, Array] = {}
# Metadata
var current_metadata_index: int = 0
var texture_names_metadata: Array[StringName] = []
var textures_metadata: PackedInt32Array = []
var vertices_metadata: PackedVector3Array = []
var normals_metadata: PackedVector3Array = []
var positions_metadata: PackedVector3Array = []
var shape_to_face_metadata: Array[PackedInt32Array] = []
var face_index_metadata_map: Dictionary[_FaceData, PackedInt32Array] = {}
# Arrange faces by surface texture
for brush in entity.brushes:
for face in brush.faces:
if is_skip(face) or is_origin(face):
continue
if not surfaces.has(face.texture):
surfaces[face.texture] = []
surfaces[face.texture].append(face)
# Cache order for consistency when rebuilding
var textures: Array[String] = surfaces.keys()
# Output mesh data
var mesh := ArrayMesh.new()
var mesh_arrays: Array[Array] = []
var build_concave: bool = entity.is_collision_concave()
var concave_vertices: PackedVector3Array
# Iteration variables
var arrays: Array
var faces: Array
# MULTISURFACE SCOPE BEGIN
for texture_name in textures:
# SURFACE SCOPE BEGIN
faces = surfaces[texture_name]
# Get texture index for metadata
var tex_index: int = texture_names_metadata.size()
if def.add_textures_metadata:
texture_names_metadata.append(texture_name)
# Prepare new array
arrays = Array()
arrays.resize(ArrayMesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = PackedVector3Array()
arrays[Mesh.ARRAY_NORMAL] = PackedVector3Array()
arrays[Mesh.ARRAY_TANGENT] = PackedFloat32Array()
arrays[Mesh.ARRAY_TEX_UV] = PackedVector2Array()
arrays[Mesh.ARRAY_INDEX] = PackedInt32Array()
# Begin fresh index offset for this subarray
var index_offset: int = 0
for face in faces:
# FACE SCOPE BEGIN
# Reject invalid faces
if face.vertices.size() < 3 or is_skip(face) or is_origin(face):
continue
# Create trimesh points regardless of texture
if build_concave:
var tris: PackedVector3Array
tris.resize(face.indices.size())
# Add triangles from face indices directly
# TODO: This can possibly be merged with the below loop in a clever way
for i in face.indices.size():
tris[i] = op_entity_ogl_xf.call(face.vertices[face.indices[i]])
concave_vertices.append_array(tris)
# Do not generate visuals for clip textures
if is_clip(face):
continue
# Handle metadata for this face
# Add metadata per triangle rather than per face to keep consistent metadata
var num_tris = face.indices.size() / 3
if def.add_textures_metadata:
var tex_array: Array[int] = []
tex_array.resize(num_tris)
tex_array.fill(tex_index)
textures_metadata.append_array(tex_array)
if def.add_face_normal_metadata:
var normal_array: Array[Vector3] = []
normal_array.resize(num_tris)
normal_array.fill(FuncGodotUtil.id_to_opengl(face.plane.normal))
normals_metadata.append_array(normal_array)
if def.add_face_position_metadata:
for i in num_tris:
var triangle_indices: Array[int] = []
var triangle_vertices: Array[Vector3] = []
triangle_indices.assign(face.indices.slice(i * 3, i * 3 + 3))
triangle_vertices.assign(triangle_indices.map(func(idx : int) -> Vector3: return face.vertices[idx]))
var position := FuncGodotUtil.op_vec3_avg(triangle_vertices)
positions_metadata.append(op_entity_ogl_xf.call(position))
if def.add_vertex_metadata:
for i in face.indices:
vertices_metadata.append(op_entity_ogl_xf.call(face.vertices[i]))
if def.add_collision_shape_to_face_indices_metadata:
face_index_metadata_map[face] = PackedInt32Array(range(current_metadata_index, current_metadata_index + num_tris))
current_metadata_index += num_tris
# Append face data to surface array
for i in face.vertices.size():
# TODO: Mesh metadata may be generated here.
var v: Vector3 = face.vertices[i]
arrays[ArrayMesh.ARRAY_VERTEX].append(op_entity_ogl_xf.call(v))
arrays[ArrayMesh.ARRAY_NORMAL].append(FuncGodotUtil.id_to_opengl(face.normals[i]))
var tx_sz: Vector2 = texture_sizes.get(face.texture, Vector2.ONE * map_settings.inverse_scale_factor)
arrays[ArrayMesh.ARRAY_TEX_UV].append(FuncGodotUtil.get_face_vertex_uv(v, face, tx_sz))
for j in 4:
arrays[ArrayMesh.ARRAY_TANGENT].append(face.tangents[(i * 4) + j])
# Create offset indices for the visual mesh
var op_shift_index: Callable = (func(a: int) -> int: return a + index_offset)
arrays[ArrayMesh.ARRAY_INDEX].append_array(Array(face.indices).map(op_shift_index))
index_offset += face.vertices.size()
# FACE SCOPE END
if FuncGodotUtil.filter_face(texture_name, map_settings):
continue
mesh_arrays.append(arrays)
# SURFACE SCOPE END
# MULTISURFACE SCOPE END
textures.erase(map_settings.clip_texture)
if def.build_visuals:
# Build mesh
for array_index in mesh_arrays.size():
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, mesh_arrays[array_index])
mesh.surface_set_name(array_index, textures[array_index])
mesh.surface_set_material(array_index, texture_materials[textures[array_index]])
# Apply mesh metadata
if def.add_textures_metadata:
entity.mesh_metadata["texture_names"] = texture_names_metadata
entity.mesh_metadata["textures"] = textures_metadata
if def.add_vertex_metadata:
entity.mesh_metadata["vertices"] = vertices_metadata
if def.add_face_normal_metadata:
entity.mesh_metadata["normals"] = normals_metadata
if def.add_face_position_metadata:
entity.mesh_metadata["positions"] = positions_metadata
entity.mesh = mesh
# Clear up unusued memory
arrays = []
surfaces = {}
if entity.is_collision_convex():
var sh: ConvexPolygonShape3D
for b in entity.brushes:
if b.planes.is_empty() or b.origin:
continue
var points := Array(Geometry3D.compute_convex_mesh_points(b.planes)).map(op_entity_ogl_xf)
if points.is_empty():
continue
sh = ConvexPolygonShape3D.new()
sh.points = points
entity.shapes.append(sh)
if def.add_collision_shape_to_face_indices_metadata:
# convex collision has one shape per brush, so collect the
# indices for this brush's faces
var face_indices_array : PackedInt32Array = []
for face in b.faces:
if face_index_metadata_map.has(face):
face_indices_array.append_array(face_index_metadata_map[face])
shape_to_face_metadata.append(face_indices_array)
elif build_concave and concave_vertices.size():
var sh := ConcavePolygonShape3D.new()
sh.set_faces(concave_vertices)
entity.shapes.append(sh)
if def.add_collision_shape_to_face_indices_metadata:
# for concave collision the shape will always represent every face
# in the entity, so just add every face here
var face_indices_array : PackedInt32Array = []
for fm in face_index_metadata_map.values():
face_indices_array.append_array(fm)
shape_to_face_metadata.append(face_indices_array)
if def.add_collision_shape_to_face_indices_metadata:
# this metadata will be mapped to the actual shape node names during entity assembly
entity.mesh_metadata["shape_to_face_array"] = shape_to_face_metadata
func unwrap_uv2s(entity_index: int, texel_size: float) -> void:
var entity: _EntityData = entity_data[entity_index]
if entity.mesh:
if (entity.definition as FuncGodotFGDSolidClass).global_illumination_mode:
entity.mesh.lightmap_unwrap(Transform3D.IDENTITY, texel_size)
# Main build process
func build(build_flags: int, entities: Array[_EntityData]) -> Error:
var entity_count: int = entities.size()
declare_step.emit("Preparing %s %s" % [entity_count, "entity" if entity_count == 1 else "entities"])
entity_data = entities
declare_step.emit("Gathering materials")
var texture_map: Array[Dictionary] = FuncGodotUtil.build_texture_map(entity_data, map_settings)
texture_materials = texture_map[0]
texture_sizes = texture_map[1]
var task_id: int
declare_step.emit("Generating brush vertices")
task_id = WorkerThreadPool.add_group_task(generate_entity_vertices, entity_count, -1, false, "Generate Brush Vertices")
WorkerThreadPool.wait_for_group_task_completion(task_id)
declare_step.emit("Determining solid entity origins")
task_id = WorkerThreadPool.add_group_task(determine_entity_origins, entity_count, -1, false, "Determine Entity Origins")
WorkerThreadPool.wait_for_group_task_completion(task_id)
declare_step.emit("Winding faces")
task_id = WorkerThreadPool.add_group_task(wind_entity_faces, entity_count, -1, false, "Wind Brush Faces")
WorkerThreadPool.wait_for_group_task_completion(task_id)
# TODO: Reimplement after solving issues
#if not (build_flags & FuncGodotMap.BuildFlags.DISABLE_SMOOTHING):
# declare_step.emit("Smoothing entity faces")
# task_id = WorkerThreadPool.add_group_task(smooth_entity_vertices, entity_count, -1, false, "Smooth Entities")
# WorkerThreadPool.wait_for_group_task_completion(task_id)
declare_step.emit("Generating surfaces")
task_id = WorkerThreadPool.add_group_task(generate_entity_surfaces, entity_count, -1, false, "Generate Surfaces")
WorkerThreadPool.wait_for_group_task_completion(task_id)
if build_flags & FuncGodotMap.BuildFlags.UNWRAP_UV2:
declare_step.emit("Unwrapping UV2s")
var texel_size: float = map_settings.uv_unwrap_texel_size * map_settings.scale_factor
for entity_index in entity_count:
unwrap_uv2s(entity_index, texel_size)
declare_step.emit("Geometry generation complete")
return OK

View File

@@ -0,0 +1 @@
uid://b1yg28xbyno7v

View File

@@ -0,0 +1,439 @@
@icon("res://addons/func_godot/icons/icon_godambler.svg")
class_name FuncGodotParser extends RefCounted
## MAP and VMF parser class that is instantiated by a [FuncGodotMap] node during the build process.
##
## @tutorial(Quake Wiki Map Format Article): https://quakewiki.org/wiki/Quake_Map_Format
## @tutorial(Valve Developer Wiki VMF Article): https://developer.valvesoftware.com/wiki/VMF_(Valve_Map_Format)
const _SIGNATURE: String = "[PRS]"
const _GroupData := FuncGodotData.GroupData
const _EntityData := FuncGodotData.EntityData
const _BrushData := FuncGodotData.BrushData
const _PatchData := FuncGodotData.PatchData
const _FaceData := FuncGodotData.FaceData
const _ParseData := FuncGodotData.ParseData
## Emitted when a step in the parsing process is completed.
## It is connected to [method FuncGodotUtil.print_profile_info] method if [member FuncGodotMap.build_flags] SHOW_PROFILE_INFO flag is set.
signal declare_step(step: String)
## Parses the map file, generating entity and group data and sub-data, then returns the generated data as an array of arrays.
## The first array is Array[FuncGodotData.EntityData], while the second array is Array[FuncGodotData.GroupData].
func parse_map_data(map_file: String, map_settings: FuncGodotMapSettings) -> _ParseData:
var map_data: PackedStringArray = []
var parse_data := _ParseData.new()
declare_step.emit("Loading map file %s" % map_file)
# Retrieve real path if needed
if map_file.begins_with("uid://"):
var uid := ResourceUID.text_to_id(map_file)
if not ResourceUID.has_id(uid):
printerr("Error: failed to retrieve path for UID (%s)" % map_file)
return parse_data
map_file = ResourceUID.get_id_path(uid)
# Open the map file
var file: FileAccess = FileAccess.open(map_file, FileAccess.READ)
if not file:
file = FileAccess.open(map_file + ".import", FileAccess.READ)
if file:
map_file += ".import"
else:
printerr("Error: Failed to open map file (" + map_file + ")")
return parse_data
# Packed map file resources need to be accessed differently in exported projects.
if map_file.ends_with(".import"):
while not file.eof_reached():
var line: String = file.get_line()
if line.begins_with("path"):
file.close()
line = line.replace("path=", "")
line = line.replace('"', '')
var data: String = (load(line) as QuakeMapFile).map_data
if data.is_empty():
printerr("Error: Failed to open map file (" + line + ")")
return parse_data
map_data = data.split("\n")
break
else:
while not file.eof_reached():
map_data.append(file.get_line())
# Determine map type and parse data
if map_file.to_lower().contains(".map"):
declare_step.emit("Parsing as Quake MAP")
parse_data = _parse_quake_map(map_data, map_settings, parse_data)
elif map_file.to_lower().contains(".vmf"):
declare_step.emit("Parsing as Source VMF")
parse_data = _parse_vmf(map_data, map_settings, parse_data)
# Determine group hierarchy
declare_step.emit("Determining groups hierarchy")
var groups_data: Array[_GroupData] = parse_data.groups
for g in groups_data:
if g.parent_id != -1:
for p in groups_data:
if p.id == g.parent_id:
g.parent = p
break
var entities_data: Array[_EntityData] = parse_data.entities
var entity_defs: Dictionary[String, FuncGodotFGDEntityClass] = map_settings.entity_fgd.get_entity_definitions()
declare_step.emit("Checking entity omission and definition status")
for i in range(entities_data.size() - 1, -1, -1):
var entity: _EntityData = entities_data[i]
# Delete entities from omitted groups
if entity.group != null and entity.group.omit == true:
entities_data.remove_at(i)
continue
# Provide entity definition to entity data. This gets used in both
# geo generation and entity assembly.
if "classname" in entity.properties:
var classname: String = entity.properties["classname"]
if classname in entity_defs:
entity.definition = entity_defs[classname]
# Delete omitted groups
declare_step.emit("Removing omitted layers and groups")
for i in range(groups_data.size() - 1, -1, -1):
if groups_data[i].omit == true:
groups_data.remove_at(i)
declare_step.emit("Map parsing complete")
return parse_data
## Parser subroutine called by [method parse_map_data], specializing in the Quake MAP format.
func _parse_quake_map(map_data: PackedStringArray, map_settings: FuncGodotMapSettings, parse_data: _ParseData) -> _ParseData:
var entities_data: Array[_EntityData] = parse_data.entities
var groups_data: Array[_GroupData] = parse_data.groups
var ent: _EntityData = null
var brush: _BrushData = null
var patch: _PatchData = null
var scope: int = 0 # Scope level, to keep track of where we are in PatchDef parsing
for line in map_data:
line = line.replace("\t", "")
#region START DATA
# Start entity, brush, or patchdef
if line.begins_with("{"):
if not ent:
ent = _EntityData.new()
else:
if not patch:
brush = _BrushData.new()
else:
scope += 1
continue
#endregion
#region COMMIT DATA
# Commit entity or brush
if line.begins_with("}"):
if brush:
ent.brushes.append(brush)
brush = null
elif patch:
if scope:
scope -= 1
else:
ent.patches.append(patch)
patch = null
else:
# TrenchBroom layers and groups
if ent.properties["classname"] == "func_group" and ent.properties.has("_tb_type"):
# Merge TB Group / Layer structural brushes with worldspawn
if entities_data.size():
entities_data[0].brushes.append_array(ent.brushes)
# Create group data
var group: _GroupData = _GroupData.new()
var props: Dictionary = ent.properties
group.id = props["_tb_id"] as int
if props["_tb_type"] == "_tb_layer":
group.type = _GroupData.GroupType.GROUP
group.name = "layer_"
else:
group.name = "group_"
group.name = group.name + str(group.id)
if props["_tb_name"] != "Unnamed":
group.name = group.name + "_" + (props["_tb_name"] as String).replace(" ", "_")
if props.has("_tb_layer"):
group.parent_id = props["_tb_layer"] as int
if props.has("_tb_group"):
group.parent_id = props["_tb_group"] as int
if props.has("_tb_layer_omit_from_export"):
group.omit = true
# Commit group
groups_data.append(group)
# Commit entity
else:
entities_data.append(ent)
ent = null
continue
#endregion
#region PROPERTY DATA
# Retrieve key value pairs
if line.begins_with("\""):
var tokens: PackedStringArray = line.split("\" \"")
if tokens.size() < 2:
tokens = line.split("\"\"")
var key: String = tokens[0].trim_prefix("\"")
var value: String = tokens[1].trim_suffix("\"")
ent.properties[key] = value
#endregion
#region BRUSH DATA
if brush and line.begins_with("("):
line = line.replace("(","")
var tokens: PackedStringArray = line.split(" ) ")
# Retrieve plane data
var points: PackedVector3Array
points.resize(3)
for i in 3:
tokens[i] = tokens[i].trim_prefix("(")
var pts: PackedFloat64Array = tokens[i].split_floats(" ", false)
var point := Vector3(pts[0], pts[1], pts[2])
points[i] = point
var plane := Plane(points[0], points[1], points[2])
brush.planes.append(plane)
var face: _FaceData = _FaceData.new()
face.plane = plane
# Retrieve texture data
var tex: String = String()
if tokens[3].begins_with("\""): # textures with spaces get surrounded by double quotes
var last_quote := tokens[3].rfind("\"")
tex = tokens[3].substr(1, last_quote - 1)
tokens = tokens[3].substr(last_quote + 2).split(" ] ")
else:
tex = tokens[3].get_slice(" ", 0)
tokens = tokens[3].trim_prefix(tex + " ").split(" ] ")
face.texture = tex
# Check for origin brushes. Brushes must be completely textured with origin to be valid.
if brush.faces.is_empty():
if tex == map_settings.origin_texture:
brush.origin = true
elif brush.origin == true:
if tex != map_settings.origin_texture:
brush.origin = false
# Retrieve UV data
var uv: Transform2D = Transform2D.IDENTITY
# Valve 220: texname [ ux uy ux offsetX ] [vx vy vz offsetY] rotation scaleX scaleY
if tokens.size() > 1:
var coords: PackedFloat64Array
for i in 2:
coords = tokens[i].trim_prefix("[ ").split_floats(" ", false)
face.uv_axes.append(Vector3(coords[0], coords[1], coords[2])) # Save axis vectors separately
face.uv.origin[i] = coords[3] # UV offset stored as transform origin
coords = tokens[2].split_floats(" ", false)
# UV scale factor stored in basis
face.uv.x = Vector2(coords[1], 0.0)
face.uv.y = Vector2(0.0, coords[2])
# Quake Standard: texname offsetX offsetY rotation scaleX scaleY
else:
var coords: PackedFloat64Array = tokens[0].split_floats(" ", false)
face.uv.origin = Vector2(coords[0], coords[1])
var r: float = deg_to_rad(coords[2])
face.uv.x = Vector2(cos(r), -sin(r)) * coords[3]
face.uv.y = Vector2(sin(r), cos(r)) * coords[4]
brush.faces.append(face)
continue
#endregion
#region PATCH DATA
if patch:
if line.begins_with("("):
line = line.replace("( ","")
# Retrieve patch control points
if patch.size:
var tokens: PackedStringArray = line.replace("(", "").split(" )", false)
for i in tokens.size():
var subtokens: PackedFloat64Array = tokens[i].split_floats(" ", false)
patch.points.append(Vector3(subtokens[0], subtokens[1], subtokens[2]))
patch.uvs.append(Vector2(subtokens[3], subtokens[4]))
# Retrieve patch size
else:
var tokens: PackedStringArray = line.replace(")","").split(" ", false)
patch.size.resize(tokens.size())
for i in tokens.size():
patch.size[i] = tokens[i].to_int()
# Retrieve patch texture
elif not line.begins_with(")"):
patch.texture = line.replace("\"","")
if line.begins_with("patchDef"):
brush = null
patch = _PatchData.new()
continue
#endregion
#region ASSIGN GROUPS
for e in entities_data:
var group_id: int = -1
if e.properties.has("_tb_layer"):
group_id = e.properties["_tb_layer"] as int
elif e.properties.has("_tb_group"):
group_id = e.properties["_tb_group"] as int
if group_id != -1:
for g in groups_data:
if g.id == group_id:
e.group = g
break
#endregion
return parse_data
## Parser subroutine called by [method parse_map_data], specializing in the Valve Map Format used by Hammer based editors.
func _parse_vmf(map_data: PackedStringArray, map_settings: FuncGodotMapSettings, parse_data: _ParseData) -> _ParseData:
var entities_data: Array[_EntityData] = parse_data.entities
var groups_data: Array[_GroupData] = parse_data.groups
var ent: _EntityData = null
var brush: _BrushData = null
var group: _GroupData = null
var group_parent_hierarchy: Array[_GroupData] = []
var scope: int = 0
for line in map_data:
line = line.replace("\t", "")
#region START DATA
if line.begins_with("entity") or line.begins_with("world"):
ent = _EntityData.new()
continue
if line.begins_with("solid"):
brush = _BrushData.new()
continue
if brush and line.begins_with("{"):
scope += 1
continue
if line == "visgroup":
if group != null:
groups_data.append(group)
group_parent_hierarchy.append(group)
group = _GroupData.new()
if group_parent_hierarchy.size():
group.parent = group_parent_hierarchy.back()
group.parent_id = group.parent.id
continue
#endregion
#region COMMIT DATA
if line.begins_with("}"):
if scope > 0:
scope -= 1
if not scope:
if brush:
if brush.faces.size():
ent.brushes.append(brush)
brush = null
elif ent:
entities_data.append(ent)
ent = null
elif group:
groups_data.append(group)
group = null
elif group_parent_hierarchy.size():
group_parent_hierarchy.pop_back()
continue
#endregion
# Retrieve key value pairs
if (ent or group) and line.begins_with("\""):
var tokens: PackedStringArray = line.split("\" \"")
var key: String = tokens[0].trim_prefix("\"")
var value: String = tokens[1].trim_suffix("\"")
#region BRUSH DATA
if brush:
if scope > 1:
match key:
"plane":
tokens = value.replace("(", "").split(")", false)
var points: PackedVector3Array
points.resize(3)
for i in 3:
tokens[i] = tokens[i].trim_prefix("(")
var pts: PackedFloat64Array = tokens[i].split_floats(" ", false)
var point: Vector3 = Vector3(pts[0], pts[1], pts[2])
points[i] = point
brush.planes.append(Plane(points[0], points[1], points[2]))
brush.faces.append(_FaceData.new())
brush.faces[-1].plane = brush.planes[-1]
continue
"material":
if brush.faces.size():
brush.faces[-1].texture = value
# Origin brush needs to be completely set to origin, otherwise it's invalid
if brush.faces.size() < 2:
if value == map_settings.origin_texture:
brush.origin = true
elif brush.origin == true:
if value != map_settings.origin_texture:
brush.origin = false
continue
"uaxis", "vaxis":
if brush.faces.size():
value = value.replace("[", "")
var vals: PackedFloat64Array = value.replace("]", "").split_floats(" ", false)
var face: _FaceData = brush.faces[-1]
face.uv_axes.append(Vector3(vals[0], vals[1], vals[2]))
if key.begins_with("u"):
face.uv.origin.x = vals[3] # Offset
face.uv.x *= vals[4] # Scale
else:
face.uv.origin.y = vals[3] # Offset
face.uv.y *= vals[4] # Scale
continue
"rotation":
# Rotation isn't used in Valve 220 mapping and VMFs are 220 exclusive
continue
"visgroupid":
# Don't put worldspawn into a group
if entities_data.size():
# Only nodes can be organized into groups in the SceneTree, so only use the first brush's group
if not ent.properties.has(key):
ent.properties[key] = value
#endregion
elif ent:
ent.properties[key] = value
continue
elif group:
if key == "name":
group.name = "group_%s_" + value
elif key == "visgroupid":
group.id = value.to_int()
group.name = group.name % value
group.name = group.name.replace(" ", "_")
continue
#region ASSIGN GROUPS
for e in entities_data:
if e.properties.has("visgroupid"):
var group_id: int = e.properties["visgroupid"] as int
for g in groups_data:
if g.id == group_id:
e.group = g
break
#endregion
return parse_data

View File

@@ -0,0 +1 @@
uid://dflet6p5hbqts

View File

@@ -1,7 +1,14 @@
@tool @tool
## Special inheritance class for [FuncGodotFGDSolidClass] and [FuncGodotFGDPointClass] entity definitions. Useful for adding shared or common properties and descriptions. @icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
class_name FuncGodotFGDBaseClass class_name FuncGodotFGDBaseClass extends FuncGodotFGDEntityClass
extends FuncGodotFGDEntityClass ## Special inheritance class for [FuncGodotFGDSolidClass] and [FuncGodotFGDPointClass] entity definitions.
##
## Inheritance class for [FuncGodotFGDSolidClass] and [FuncGodotFGDPointClass] entities,
## used to shared or common properties and descriptions across different definitions.
##
## @tutorial(Quake Wiki Entity Article): https://quakewiki.org/wiki/Entity
## @tutorial(Level Design Book: Entity Types and Settings): https://book.leveldesignbook.com/appendix/resources/formats/fgd#entity-types-and-settings-basic
## @tutorial(Valve Developer Wiki FGD Article): https://developer.valvesoftware.com/wiki/FGD#Class_Types_and_Properties
func _init() -> void: func _init() -> void:
prefix = "@BaseClass" prefix = "@BaseClass"

View File

@@ -1 +1 @@
uid://dyv4g30betqgm uid://ck575aqs1sbrb

View File

@@ -1,13 +1,20 @@
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg") @icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
## Base entity definition class. Not to be used directly, use [FuncGodotFGDBaseClass], [FuncGodotFGDSolidClass], or [FuncGodotFGDPointClass] instead. class_name FuncGodotFGDEntityClass extends Resource
class_name FuncGodotFGDEntityClass ## Entity definition template. WARNING! Not to be used directly! Use [FuncGodotFGDBaseClass], [FuncGodotFGDSolidClass], or [FuncGodotFGDPointClass] instead.
extends Resource ##
## Entity definition template. It holds all of the common entity class properties shared between [FuncGodotFGDBaseClass], [FuncGodotFGDSolidClass], or [FuncGodotFGDPointClass].
## Not to be used directly, use one of the aforementioned FGD class types instead.
##
## @tutorial(Quake Wiki Entity Article): https://quakewiki.org/wiki/Entity
## @tutorial(Level Design Book: Entity Types and Settings): https://book.leveldesignbook.com/appendix/resources/formats/fgd#entity-types-and-settings-basic
## @tutorial(Valve Developer Wiki FGD Article): https://developer.valvesoftware.com/wiki/FGD#Class_Types_and_Properties
## @tutorial(Valve Developer Wiki Entity Descriptions): https://developer.valvesoftware.com/wiki/FGD#Entity_Description
var prefix: String = "" var prefix: String = ""
@export_group("Entity Definition") @export_group("Entity Definition")
## Entity classname. This is a required field in all entity types as it is parsed by both the map editor and by FuncGodot on map build. ## Entity classname. [b][i]This is a required field in all entity types[/i][/b] as it is parsed by both the map editor and by FuncGodot on map build.
@export var classname : String = "" @export var classname : String = ""
## Entity description that appears in the map editor. Not required. ## Entity description that appears in the map editor. Not required.
@@ -16,16 +23,21 @@ var prefix: String = ""
## Entity does not get written to the exported FGD. Entity is only used for [FuncGodotMap] build process. ## Entity does not get written to the exported FGD. Entity is only used for [FuncGodotMap] build process.
@export var func_godot_internal : bool = false @export var func_godot_internal : bool = false
## FuncGodotFGDBaseClass resources to inherit [member class_properties] and [member class_descriptions] from. ## [FuncGodotFGDBaseClass] resources to inherit [member class_properties] and [member class_descriptions] from.
@export var base_classes: Array[Resource] = [] @export var base_classes: Array[Resource] = []
## Key value pair properties that will appear in the map editor. After building the FuncGodotMap in Godot, these properties will be added to a Dictionary that gets applied to the generated Node, as long as that Node is a tool script with an exported `func_godot_properties` Dictionary. ## Key value pair properties that will appear in the map editor. After building the [FuncGodotMap] in Godot, these properties will be added to a [Dictionary]
## that gets applied to the generated node, as long as that node is a tool script with an exported `func_godot_properties` Dictionary.
@export var class_properties : Dictionary = {} @export var class_properties : Dictionary = {}
## Descriptions for previously defined key value pair properties. ## Map editor descriptions for previously defined key value pair properties. Optional but recommended.
@export var class_property_descriptions : Dictionary = {} @export var class_property_descriptions : Dictionary = {}
## Appearance properties for the map editor. See the [**Valve FGD**](https://developer.valvesoftware.com/wiki/FGD#Entity_Description) and [**TrenchBroom**](https://trenchbroom.github.io/manual/latest/#display-models-for-entities) documentation for more information. ## Automatically applies entity class properties to matching properties in the generated node.
## When using this feature, class properties need to be the correct type or you may run into errors on map build.
@export var auto_apply_to_matching_node_properties : bool = false
## Appearance properties for the map editor. See the Valve Developer Wiki and TrenchBroom documentation for more information.
@export var meta_properties : Dictionary = { @export var meta_properties : Dictionary = {
"size": AABB(Vector3(-8, -8, -8), Vector3(8, 8, 8)), "size": AABB(Vector3(-8, -8, -8), Vector3(8, 8, 8)),
"color": Color(0.8, 0.8, 0.8) "color": Color(0.8, 0.8, 0.8)
@@ -33,45 +45,47 @@ var prefix: String = ""
@export_group("Node Generation") @export_group("Node Generation")
## Node to generate on map build. This can be a built-in Godot class or a GDExtension class. For Point Class entities that use Scene File instantiation leave this blank. ## Node to generate on map build. This can be a built-in Godot class, a GDScript class, or a GDExtension class.
## For Point Class entities that use Scene File instantiation leave this blank.
@export var node_class := "" @export var node_class := ""
## Class property to use in naming the generated node. Overrides `name_property` in [FuncGodotMapSettings]. ## Optional class property to use in naming the generated node. Overrides [member FuncGodotMapSettings.name_property].
## Naming occurs before adding to the [SceneTree] and applying properties. ## Naming occurs before adding to the [SceneTree] and applying properties.
## Nodes will be named `"entity_" + name_property`. An entity's name should be unique, otherwise you may run into unexpected behavior. ## Nodes will be named `"entity_" + name_property`. An entity's name should be unique, otherwise you may run into unexpected behavior.
@export var name_property := "" @export var name_property := ""
func build_def_text(model_key_supported: bool = true) -> String: ## Parses the definition and outputs it into the FGD format.
func build_def_text(target_editor: FuncGodotFGDFile.FuncGodotTargetMapEditors = FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM) -> String:
# Class prefix # Class prefix
var res : String = prefix var res : String = prefix
# Meta properties # Meta properties
var base_str = "" var base_str = ""
var meta_props = meta_properties.duplicate() var meta_props = meta_properties.duplicate()
for base_class in base_classes: for base_class in base_classes:
if not 'classname' in base_class: if not 'classname' in base_class:
continue continue
base_str += base_class.classname base_str += base_class.classname
if base_class != base_classes.back(): if base_class != base_classes.back():
base_str += ", " base_str += ", "
if base_str != "": if base_str != "":
meta_props['base'] = base_str meta_props['base'] = base_str
for prop in meta_props: for prop in meta_props:
if prefix == '@SolidClass': if prefix == '@SolidClass':
if prop == "size" or prop == "model": if prop == "size" or prop == "model":
continue continue
if prop == 'model' and not model_key_supported: if prop == 'model' and target_editor != FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM:
continue continue
var value = meta_props[prop] var value = meta_props[prop]
res += " " + prop + "(" res += " " + prop + "("
if value is AABB: if value is AABB:
res += "%s %s %s, %s %s %s" % [ res += "%s %s %s, %s %s %s" % [
value.position.x, value.position.x,
@@ -89,13 +103,15 @@ func build_def_text(model_key_supported: bool = true) -> String:
] ]
elif value is String: elif value is String:
res += value res += value
elif value is Dictionary and target_editor == FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM:
res += JSON.stringify(value)
res += ")" res += ")"
res += " = " + classname res += " = " + classname
if prefix != "@BaseClass": # having a description in BaseClasses crashes some editors if prefix != "@BaseClass": # having a description in BaseClasses crashes some editors
var normalized_description = description.replace("\n", " ").strip_edges() if prefix != "@BaseClass" else "" var normalized_description = description.replace("\"", "\'")
if normalized_description != "": if normalized_description != "":
res += " : \"%s\" " % [normalized_description] res += " : \"%s\" " % [normalized_description]
else: # Having no description crashes some editors else: # Having no description crashes some editors
@@ -117,7 +133,8 @@ func build_def_text(model_key_supported: bool = true) -> String:
if value is Dictionary and class_property_descriptions[prop] is Array: if value is Dictionary and class_property_descriptions[prop] is Array:
var prop_arr: Array = class_property_descriptions[prop] var prop_arr: Array = class_property_descriptions[prop]
if prop_arr.size() > 1 and (prop_arr[1] is int or prop_arr[1] is String): if prop_arr.size() > 1 and (prop_arr[1] is int or prop_arr[1] is String):
prop_description = "\"" + prop_arr[0] + "\" : " + str(prop_arr[1]) var value_str : String = str(prop_arr[1]) if prop_arr[1] is int else "\"" + prop_arr[1] + "\""
prop_description = "\"" + prop_arr[0] + "\" : " + value_str
else: else:
prop_description = "\"\" : 0" prop_description = "\"\" : 0"
printerr(str(prop) + " has incorrect description format. Should be [String description, int / String default value].") printerr(str(prop) + " has incorrect description format. Should be [String description, int / String default value].")
@@ -159,6 +176,9 @@ func build_def_text(model_key_supported: bool = true) -> String:
prop_val = FuncGodotUtil.newline() + "\t[" + FuncGodotUtil.newline() prop_val = FuncGodotUtil.newline() + "\t[" + FuncGodotUtil.newline()
for choice in value: for choice in value:
var choice_val = value[choice] var choice_val = value[choice]
if typeof(choice_val) == TYPE_STRING:
if not (choice_val as String).begins_with("\""):
choice_val = "\"" + choice_val + "\""
prop_val += "\t\t" + str(choice_val) + " : \"" + choice + "\"" + FuncGodotUtil.newline() prop_val += "\t\t" + str(choice_val) + " : \"" + choice + "\"" + FuncGodotUtil.newline()
prop_val += "\t]" prop_val += "\t]"
TYPE_ARRAY: TYPE_ARRAY:
@@ -169,29 +189,45 @@ func build_def_text(model_key_supported: bool = true) -> String:
prop_val += "\t]" prop_val += "\t]"
TYPE_NODE_PATH: TYPE_NODE_PATH:
prop_type = "target_destination" prop_type = "target_destination"
prop_val = "\"\""
TYPE_OBJECT: TYPE_OBJECT:
prop_type = "target_source" if value is Resource:
prop_val = "\"" + value.resource_path + "\""
if value is Material:
if target_editor != FuncGodotFGDFile.FuncGodotTargetMapEditors.JACK:
prop_type = "material"
else:
prop_type = "shader"
elif value is Texture2D:
prop_type = "decal"
elif value is AudioStream:
prop_type = "sound"
else:
prop_type = "target_source"
prop_val = "\"\""
if prop_val: if prop_val:
res += "\t" res += "\t"
res += prop res += prop
res += "(" res += "("
res += prop_type res += prop_type
res += ")" res += ")"
if not value is Array: if not value is Array:
if not value is Dictionary or prop_description != "": if not value is Dictionary or prop_description != "":
res += " : " res += " : "
res += prop_description res += prop_description
if value is bool or value is Dictionary or value is Array: if value is bool:
res += " : 1 = " if value else " : 0 = "
elif value is Dictionary or value is Array:
res += " = " res += " = "
else: else:
res += " : " res += " : "
res += prop_val res += prop_val
res += FuncGodotUtil.newline() res += FuncGodotUtil.newline()
res += "]" + FuncGodotUtil.newline() res += "]" + FuncGodotUtil.newline()
return res return res

View File

@@ -1 +1 @@
uid://b6vs8fw3on4e2 uid://cgkrrgcimlr8y

View File

@@ -1,19 +1,28 @@
@tool @tool
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg") @icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
## [Resource] file used to express a set of [FuncGodotFGDEntity] definitions. Can be exported as an FGD file for use with a Quake map editor. Used in conjunction with a [FuncGodotMapSetting] resource to generate nodes in a [FuncGodotMap] node. class_name FuncGodotFGDFile extends Resource
class_name FuncGodotFGDFile ## [Resource] file used to express a set of [FuncGodotFGDEntity] definitions.
extends Resource ##
## Can be exported as an FGD file for use with a Quake or Hammer-based map editor. Used in conjunction with [FuncGodotMapSetting] to generate nodes in a [FuncGodotMap] node.
##
## @tutorial(Level Design Book FGD Chapter): https://book.leveldesignbook.com/appendix/resources/formats/fgd
## @tutorial(Valve Developer Wiki FGD Article): https://developer.valvesoftware.com/wiki/FGD
## Supported map editors enum, used in conjunction with [member target_map_editor].
enum FuncGodotTargetMapEditors {
OTHER,
TRENCHBROOM,
JACK,
NET_RADIANT_CUSTOM,
}
## Builds and exports the FGD file. ## Builds and exports the FGD file.
@export var export_file: bool: @export_tool_button("Export FGD") var export_file := export_button
get:
return export_file # TODO Converter40 Non existent get function
set(new_export_file):
if new_export_file != export_file:
do_export_file(model_key_word_supported)
func do_export_file(model_key_supported: bool = true, fgd_output_folder: String = "") -> void: func export_button() -> void:
do_export_file(target_map_editor)
func do_export_file(target_editor: FuncGodotTargetMapEditors = FuncGodotTargetMapEditors.TRENCHBROOM, fgd_output_folder: String = "") -> void:
if not Engine.is_editor_hint(): if not Engine.is_editor_hint():
return return
@@ -25,20 +34,36 @@ func do_export_file(model_key_supported: bool = true, fgd_output_folder: String
if fgd_name == "": if fgd_name == "":
print("Skipping export: Empty FGD name") print("Skipping export: Empty FGD name")
if not DirAccess.dir_exists_absolute(fgd_output_folder):
if DirAccess.make_dir_recursive_absolute(fgd_output_folder) != OK:
print("Skipping export: Failed to create directory")
return
var fgd_file = fgd_output_folder + "/" + fgd_name + ".fgd" var fgd_file = fgd_output_folder.path_join(fgd_name + ".fgd")
print("Exporting FGD to ", fgd_file)
var file_obj := FileAccess.open(fgd_file, FileAccess.WRITE) var file_obj := FileAccess.open(fgd_file, FileAccess.WRITE)
file_obj.store_string(build_class_text(model_key_supported)) if not file_obj:
print("Failed to open file for writing: ", fgd_file)
return
print("Exporting FGD to ", fgd_file)
file_obj.store_string(build_class_text(target_editor))
file_obj.close() file_obj.close()
@export_group("Map Editor") @export_group("Map Editor")
## Some map editors do not support the "model" key word and require the "studio" key word instead. ## Some map editors do not support the features found in others
## If you get errors in your map editor, try changing this setting. ## (ex: TrenchBroom supports the "model" key word while others require "studio",
## J.A.C.K. uses the "shader" key word while others use "material", etc...).
## If you get errors in your map editor, try changing this setting and re-exporting.
## This setting is overridden when the FGD is built via the Game Config resource. ## This setting is overridden when the FGD is built via the Game Config resource.
@export var model_key_word_supported: bool = true @export var target_map_editor: FuncGodotTargetMapEditors = FuncGodotTargetMapEditors.TRENCHBROOM
# Some map editors do not support the "model" key word and require the "studio" key word instead.
# If you get errors in your map editor, try changing this setting.
# This setting is overridden when the FGD is built via the Game Config resource.
#@export var model_key_word_supported: bool = true
@export_group("FGD") @export_group("FGD")
@@ -51,12 +76,14 @@ func do_export_file(model_key_supported: bool = true, fgd_output_folder: String
## Array of resources that inherit from [FuncGodotFGDEntityClass]. This array defines the entities that will be added to the exported FGD file and the nodes that will be generated in a [FuncGodotMap]. ## Array of resources that inherit from [FuncGodotFGDEntityClass]. This array defines the entities that will be added to the exported FGD file and the nodes that will be generated in a [FuncGodotMap].
@export var entity_definitions: Array[Resource] = [] @export var entity_definitions: Array[Resource] = []
func build_class_text(model_key_supported: bool = true) -> String: func build_class_text(target_editor: FuncGodotTargetMapEditors = FuncGodotTargetMapEditors.TRENCHBROOM) -> String:
var res : String = "" var res : String = ""
for base_fgd in base_fgd_files: for base_fgd in base_fgd_files:
if base_fgd is FuncGodotFGDFile: if base_fgd is FuncGodotFGDFile:
res += base_fgd.build_class_text(model_key_supported) res += base_fgd.build_class_text(target_editor)
else:
printerr("Base Fgd Files contains incorrect resource type! Should only be type FuncGodotFGDFile.")
var entities = get_fgd_classes() var entities = get_fgd_classes()
for ent in entities: for ent in entities:
@@ -65,7 +92,7 @@ func build_class_text(model_key_supported: bool = true) -> String:
if ent.func_godot_internal: if ent.func_godot_internal:
continue continue
var ent_text = ent.build_def_text(model_key_supported) var ent_text = ent.build_def_text(target_editor)
res += ent_text res += ent_text
if ent != entities[-1]: if ent != entities[-1]:
res += "\n" res += "\n"
@@ -84,8 +111,8 @@ func get_fgd_classes() -> Array:
res.append(cur_ent_def) res.append(cur_ent_def)
return res return res
func get_entity_definitions() -> Dictionary: func get_entity_definitions() -> Dictionary[String, FuncGodotFGDEntityClass]:
var res : Dictionary = {} var res: Dictionary[String, FuncGodotFGDEntityClass] = {}
for base_fgd in base_fgd_files: for base_fgd in base_fgd_files:
var fgd_res = base_fgd.get_entity_definitions() var fgd_res = base_fgd.get_entity_definitions()

View File

@@ -1 +1 @@
uid://ckisq2tcxg062 uid://drlmgulwbjwqu

View File

@@ -1,25 +1,36 @@
@tool @tool
## A special type of [FuncGodotFGDPointClass] entity that can automatically generate a special simplified GLB model file for the map editor display. @icon("res://addons/func_godot/icons/icon_godambler3d.svg")
class_name FuncGodotFGDModelPointClass extends FuncGodotFGDPointClass
## A special type of [FuncGodotFGDPointClass] entity that automatically generates a special simplified GLB model file for the map editor display.
## Only supported in map editors that support GLTF or GLB. ## Only supported in map editors that support GLTF or GLB.
class_name FuncGodotFGDModelPointClass ##
extends FuncGodotFGDPointClass ## @tutorial(Quake Wiki Entity Article): https://quakewiki.org/wiki/Entity
## @tutorial(Level Design Book: Entity Types and Settings): https://book.leveldesignbook.com/appendix/resources/formats/fgd#entity-types-and-settings-basic
## @tutorial(Valve Developer Wiki FGD Article): https://developer.valvesoftware.com/wiki/FGD#Class_Types_and_Properties
## @tutorial(dumptruck_ds' Quake Mapping Entities Tutorial): https://www.youtube.com/watch?v=gtL9f6_N2WM
## @tutorial(Level Design Book: Display Models for Entities): https://book.leveldesignbook.com/appendix/resources/formats/fgd#display-models-for-entities
## @tutorial(Valve Developer Wiki FGD Article: Entity Description Section): https://developer.valvesoftware.com/wiki/FGD#Entity_Description
## @tutorial(TrenchBroom Manual: Display Models for Entities): https://trenchbroom.github.io/manual/latest/#display-models-for-entities
enum TargetMapEditor { enum TargetMapEditor {
GENERIC, GENERIC, ## Entity definition uses the [b]@studio[/b] key word. [member scale_expression] is ignored. Supported by all map editors.
TRENCHBROOM TRENCHBROOM ## Entity definition uses the [b]@model[/b] key word. [member scale_expression] is applied if set.
} }
## Determines how model interprets [member scale_expression].
@export var target_map_editor: TargetMapEditor = TargetMapEditor.GENERIC @export var target_map_editor: TargetMapEditor = TargetMapEditor.GENERIC
## Display model export folder relative to the model folder set by [FuncGodotLocalConfig]. ## Display model export folder relative to [member ProjectSettings.func_godot/model_point_class_save_path].
@export var models_sub_folder : String = "" @export var models_sub_folder : String = ""
## Scale expression applied to model. See the [TrenchBroom Documentation](https://trenchbroom.github.io/manual/latest/#display-models-for-entities) for more information. ## Scale expression applied to model. Only used by TrenchBroom. If left empty, uses [member ProjectSettings.func_godot/default_inverse_scale_factor]. [br][br]Read the TrenchBroom Manual for more information on the "scale expression" feature.
@export var scale_expression : String = "" @export var scale_expression : String = ""
## Model Point Class can override the 'size' meta property by auto-generating a value from the meshes' [AABB]. Proper generation requires 'scale_expression' set to a float or [Vector3]. **WARNING:** Generated size property unlikely to align cleanly to grid! ## Model Point Class can override the 'size' meta property by auto-generating a value from the meshes' [AABB]. Proper generation requires [member scale_expression] set to a float or vector. [br][br][color=orange]WARNING:[/color] Generated size property unlikely to align cleanly to grid!
@export var generate_size_property : bool = false @export var generate_size_property : bool = false
## Degrees to rotate model prior to export. Different editors may handle GLTF transformations differently. If your model isn't oriented correctly, try modifying this property.
@export var rotation_offset: Vector3 = Vector3(0.0, 0.0, 0.0)
## Creates a .gdignore file in the model export folder to prevent Godot importing the display models. Only needs to be generated once. ## Creates a .gdignore file in the model export folder to prevent Godot importing the display models. Only needs to be generated once.
@export var generate_gd_ignore_file : bool = false : @export_tool_button("Generate GD Ignore File", "FileAccess") var generate_gd_ignore_file : Callable = _generate_gd_ignore_file
set(ignore):
func _generate_gd_ignore_file() -> void:
if Engine.is_editor_hint():
var path: String = _get_game_path().path_join(_get_model_folder()) var path: String = _get_game_path().path_join(_get_model_folder())
var error: Error = DirAccess.make_dir_recursive_absolute(path) var error: Error = DirAccess.make_dir_recursive_absolute(path)
if error != Error.OK: if error != Error.OK:
@@ -32,7 +43,8 @@ enum TargetMapEditor {
file.store_string('') file.store_string('')
file.close() file.close()
func build_def_text(model_key_supported: bool = true) -> String: ## Builds and saves the display model into the specified destination, then parses the definition and outputs it into the FGD format.
func build_def_text(target_editor: FuncGodotFGDFile.FuncGodotTargetMapEditors = FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM) -> String:
_generate_model() _generate_model()
return super() return super()
@@ -41,13 +53,15 @@ func _generate_model() -> void:
return return
var gltf_state := GLTFState.new() var gltf_state := GLTFState.new()
var path = _get_export_dir() var path: String = _get_export_dir()
var node = _get_node() var node: Node3D = _get_node()
if node == null: return if not node:
return
if not _create_gltf_file(gltf_state, path, node): if not _create_gltf_file(gltf_state, path, node):
printerr("could not create gltf file") printerr("could not create gltf file")
return return
node.queue_free() node.queue_free()
if target_map_editor == TargetMapEditor.TRENCHBROOM: if target_map_editor == TargetMapEditor.TRENCHBROOM:
const model_key: String = "model" const model_key: String = "model"
if scale_expression.is_empty(): if scale_expression.is_empty():
@@ -61,7 +75,7 @@ func _generate_model() -> void:
meta_properties["studio"] = '"%s"' % _get_local_path() meta_properties["studio"] = '"%s"' % _get_local_path()
if generate_size_property: if generate_size_property:
meta_properties["size"] = _generate_size_from_aabb(gltf_state.meshes) meta_properties["size"] = _generate_size_from_aabb(gltf_state.meshes, gltf_state.get_nodes())
func _get_node() -> Node3D: func _get_node() -> Node3D:
var node := scene_file.instantiate() var node := scene_file.instantiate()
@@ -80,7 +94,7 @@ func _get_local_path() -> String:
return _get_model_folder().path_join('%s.glb' % classname) return _get_model_folder().path_join('%s.glb' % classname)
func _get_model_folder() -> String: func _get_model_folder() -> String:
var model_dir: String = FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.GAME_PATH_MODELS_FOLDER) as String var model_dir: String = ProjectSettings.get_setting("func_godot/model_point_class_save_path", "") as String
if not models_sub_folder.is_empty(): if not models_sub_folder.is_empty():
model_dir = model_dir.path_join(models_sub_folder) model_dir = model_dir.path_join(models_sub_folder)
return model_dir return model_dir
@@ -93,13 +107,15 @@ func _create_gltf_file(gltf_state: GLTFState, path: String, node: Node3D) -> boo
var gltf_document := GLTFDocument.new() var gltf_document := GLTFDocument.new()
gltf_state.create_animations = false gltf_state.create_animations = false
node.rotate_y(deg_to_rad(-90)) node.rotate_x(deg_to_rad(rotation_offset.x))
node.rotate_y(deg_to_rad(rotation_offset.y))
node.rotate_z(deg_to_rad(rotation_offset.z))
# With TrenchBroom we can specify a scale expression, but for other editors we need to scale our models manually. # With TrenchBroom we can specify a scale expression, but for other editors we need to scale our models manually.
if target_map_editor != TargetMapEditor.TRENCHBROOM: if target_map_editor != TargetMapEditor.TRENCHBROOM:
var scale_factor: Vector3 = Vector3.ONE var scale_factor: Vector3 = Vector3.ONE
if scale_expression.is_empty(): if scale_expression.is_empty():
scale_factor *= FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.DEFAULT_INVERSE_SCALE) as float scale_factor *= ProjectSettings.get_setting("func_godot/default_inverse_scale_factor", 32.0) as float
else: else:
if scale_expression.begins_with('\''): if scale_expression.begins_with('\''):
var scale_arr := scale_expression.split_floats(' ', false) var scale_arr := scale_expression.split_floats(' ', false)
@@ -126,31 +142,43 @@ func _save_to_file_system(gltf_document: GLTFDocument, gltf_state: GLTFState, pa
return return
error = gltf_document.write_to_filesystem(gltf_state, path) error = gltf_document.write_to_filesystem(gltf_state, path)
if error != OK: if error != Error.OK:
printerr("Failed writing to file system", error) printerr("Failed writing to file system", error)
return return
print('Exported model to ', path) print('Exported model to ', path)
func _generate_size_from_aabb(meshes: Array[GLTFMesh]) -> AABB: func _generate_size_from_aabb(meshes: Array[GLTFMesh], nodes: Array[GLTFNode]) -> AABB:
var aabb := AABB() var aabb := AABB()
for mesh in meshes: for mesh in meshes:
aabb = aabb.merge(mesh.mesh.get_mesh().get_aabb()) aabb = aabb.merge(mesh.mesh.get_mesh().get_aabb())
var pos_ofs := Vector3.ZERO
if not nodes.is_empty():
var ct: int = 0
for node in nodes:
if node.parent == 0:
pos_ofs += node.position
ct += 1
pos_ofs /= maxi(ct, 1)
aabb.position += pos_ofs
# Reorient the AABB so it matches TrenchBroom's coordinate system # Reorient the AABB so it matches TrenchBroom's coordinate system
var size_prop := AABB() var size_prop := AABB()
size_prop.position = Vector3(aabb.position.z, aabb.position.x, aabb.position.y) size_prop.position = Vector3(aabb.position.z, aabb.position.x, aabb.position.y)
size_prop.size = Vector3(aabb.size.z, aabb.size.x, aabb.size.y) size_prop.size = Vector3(aabb.size.z, aabb.size.x, aabb.size.y)
# Scale the size bounds to our scale factor # Scale the size bounds to our scale factor
# Scale factor will need to be set if we decide to auto-generate our bounds # Scale factor will need to be set if we decide to auto-generate our bounds
var scale_factor: Vector3 = Vector3.ONE var scale_factor: Vector3 = Vector3.ONE
if target_map_editor == TargetMapEditor.TRENCHBROOM: if target_map_editor == TargetMapEditor.TRENCHBROOM:
if scale_expression.begins_with('\''): if scale_expression.is_empty():
var scale_arr := scale_expression.split_floats(' ', false) scale_factor *= ProjectSettings.get_setting("func_godot/default_inverse_scale_factor", 32.0) as float
if scale_arr.size() == 3: else:
scale_factor *= Vector3(scale_arr[0], scale_arr[1], scale_arr[2]) if scale_expression.begins_with('\''):
elif scale_expression.to_float() > 0: var scale_arr := scale_expression.split_floats(' ', false)
scale_factor *= scale_expression.to_float() if scale_arr.size() == 3:
scale_factor *= Vector3(scale_arr[0], scale_arr[1], scale_arr[2])
elif scale_expression.to_float() > 0:
scale_factor *= scale_expression.to_float()
size_prop.position *= scale_factor size_prop.position *= scale_factor
size_prop.size *= scale_factor size_prop.size *= scale_factor

View File

@@ -1 +1 @@
uid://ieokw1i153l2 uid://ldfqjtq0br35

View File

@@ -1,20 +1,35 @@
@tool @tool
## FGD PointClass entity definition, used to define point entities. @icon("res://addons/func_godot/icons/icon_godambler3d.svg")
## PointClass entities can use either the `node_class` or the `scene_file` property to tell [FuncGodotMap] what to generate on map build. class_name FuncGodotFGDPointClass extends FuncGodotFGDEntityClass
class_name FuncGodotFGDPointClass ## FGD PointClass entity definition.
extends FuncGodotFGDEntityClass ##
## A resource used to define an FGD PointClass entity. PointClass entities can use either the [member FuncGodotFGDEntityClass.node_class]
## or the [member scene_file] property to tell [FuncGodotMap] what to generate on map build.
##
## @tutorial(Quake Wiki Entity Article): https://quakewiki.org/wiki/Entity
## @tutorial(Level Design Book: Entity Types and Settings): https://book.leveldesignbook.com/appendix/resources/formats/fgd#entity-types-and-settings-basic
## @tutorial(Valve Developer Wiki FGD Article): https://developer.valvesoftware.com/wiki/FGD#Class_Types_and_Properties
## @tutorial(dumptruck_ds' Quake Mapping Entities Tutorial): https://www.youtube.com/watch?v=gtL9f6_N2WM
## @tutorial(Level Design Book: Display Models for Entities): https://book.leveldesignbook.com/appendix/resources/formats/fgd#display-models-for-entities
## @tutorial(Valve Developer Wiki FGD Article: Entity Description Section): https://developer.valvesoftware.com/wiki/FGD#Entity_Description
## @tutorial(TrenchBroom Manual: Display Models for Entities): https://trenchbroom.github.io/manual/latest/#display-models-for-entities
func _init() -> void: func _init() -> void:
prefix = "@PointClass" prefix = "@PointClass"
@export_group ("Scene") @export_group ("Scene")
## An optional scene file to instantiate on map build. Overrides `node_class` and `script_class`. ## An optional [PackedScene] file to instantiate on map build. Overrides [member FuncGodotFGDEntityClass.node_class] and [member script_class].
@export var scene_file: PackedScene @export var scene_file: PackedScene
## An optional script file to attach to the node generated on map build. Ignored if `scene_file` is specified.
@export_group ("Scripting") @export_group ("Scripting")
## An optional [Script] resource to attach to the node generated on map build. Ignored if [member scene_file] is specified.
@export var script_class: Script @export var script_class: Script
@export_group("Build") @export_group("Build")
## Toggles whether entity will use `angles`, `mangle`, or `angle` to determine rotations on [FuncGodotMap] build, prioritizing the key value pairs in that order. Set to `false` if you would like to define how the generated node is rotated yourself. ## Toggles whether entity will use `angles`, `mangle`, or `angle` to determine rotations on [FuncGodotMap] build, prioritizing the key value pairs in that order.
## Set to [code]false[/code] if you would like to define how the generated node is rotated yourself.
@export var apply_rotation_on_map_build : bool = true @export var apply_rotation_on_map_build : bool = true
## Toggles whether entity will use `scale` to determine the generated node or scene's scale. This is performed on the top level node.
## The property can be a [float], [Vector3], or [Vector2]. Set to [code]false[/code] if you would like to define how the generated node is scaled yourself.
@export var apply_scale_on_map_build: bool = true

View File

@@ -1 +1 @@
uid://dq8gw52aflgyo uid://cxsqwtsqd8w33

View File

@@ -1,19 +1,31 @@
@tool @tool
## FGD SolidClass entity definition, used to define brush entities. @icon("res://addons/func_godot/icons/icon_slipgate3d.svg")
## A [MeshInstance3D] will be generated by FuncGodotMap according to this definition's Visual Build settings. If FuncGodotFGDSolidClass [member node_class] inherits [CollisionObject3D] then one or more [CollisionShape3D] nodes will be generated according to Collision Build settings. class_name FuncGodotFGDSolidClass extends FuncGodotFGDEntityClass
class_name FuncGodotFGDSolidClass ## FGD SolidClass entity definition that generates a mesh from [FuncGodotData.BrushData].
extends FuncGodotFGDEntityClass ##
## A [MeshInstance3D] will be generated by [FuncGodotMap] according to this definition's Visual Build settings.
## If [member FuncGodotFGDEntityClass.node_class] inherits [CollisionObject3D]
## then one or more [CollisionShape3D] nodes will be generated according to Collision Build settings.
##
## @tutorial(Quake Wiki Entity Article): https://quakewiki.org/wiki/Entity
## @tutorial(Level Design Book: Entity Types and Settings): https://book.leveldesignbook.com/appendix/resources/formats/fgd#entity-types-and-settings-basic
## @tutorial(Valve Developer Wiki FGD Article): https://developer.valvesoftware.com/wiki/FGD#Class_Types_and_Properties
## @tutorial(dumptruck_ds' Quake Mapping Entities Tutorial): https://www.youtube.com/watch?v=gtL9f6_N2WM
enum SpawnType { enum SpawnType {
WORLDSPAWN = 0, ## Is worldspawn WORLDSPAWN = 0, ## Builds the geometry of this entity relative to the FuncGodotMap position.
MERGE_WORLDSPAWN = 1, ## Should be combined with worldspawn MERGE_WORLDSPAWN = 1, ## This entity's geometry is merged with the [b]worldspawn[/b] entity and this entity is removed. Behavior mimics [b]func_group[/b] in modern Quake compilers.
ENTITY = 2, ## Is its own separate entity ENTITY = 2, ## This entity is built as its own object. It finds the origin of the entity based on [member origin_type].
} }
enum OriginType { enum OriginType {
IGNORE = 0, ## Ignore origin property and only use averaged brush vertices for positioning. Standard Quake 1 / Half-Life behavior. AVERAGED = 0, ## Use averaged brush vertices for center position. This is the old Qodot behavior.
ABSOLUTE = 1, ## Use origin property for position center, ignoring brush vertice positions. ABSOLUTE = 1, ## Use [code]origin[/code] class property in global coordinates as the center position.
RELATIVE = 2 ## Use origin relative to averaged brush vertice positions. Use this setting if brush entity vertices have coordinates local to the origin. RELATIVE = 2, ## Calculate center position using [code]origin[/code] class property as an offset to the entity's bounding box center.
BRUSH = 3, ## Calculate center position based on the bounding box center of all brushes using the 'origin' texture specified in the [FuncGodotMapSettings]. If no Origin Brush is found, fall back to BOUNDS_CENTER. This is the default option and recommended for most entities.
BOUNDS_CENTER = 4, ## Use the center of the entity's bounding box for center position.
BOUNDS_MINS = 5, ## Use the lowest bounding box coordinates for center position. This is standard Quake and Half-Life brush entity behavior.
BOUNDS_MAXS = 6, ## Use the highest bounding box coordinates for center position.
} }
enum CollisionShapeType { enum CollisionShapeType {
@@ -24,13 +36,15 @@ enum CollisionShapeType {
## Controls whether this Solid Class is the worldspawn, is combined with the worldspawn, or is spawned as its own free-standing entity. ## Controls whether this Solid Class is the worldspawn, is combined with the worldspawn, or is spawned as its own free-standing entity.
@export var spawn_type: SpawnType = SpawnType.ENTITY @export var spawn_type: SpawnType = SpawnType.ENTITY
## Controls how this Solid Class utilizes the `origin` key value pair to find its position. ## Controls how this Solid Class determines its center position. Only valid if [member spawn_type] is set to ENTITY.
@export var origin_type: OriginType = OriginType.IGNORE @export var origin_type: OriginType = OriginType.BRUSH
@export_group("Visual Build") @export_group("Visual Build")
## Controls whether a [MeshInstance3D] is built for this Solid Class. ## Controls whether a [MeshInstance3D] is built for this Solid Class.
@export var build_visuals : bool = true @export var build_visuals : bool = true
## Sets generated [MeshInstance3D] to be available for UV2 unwrapping after [FuncGodotMap] build. Utilized in baked lightmapping. ## Global illumination mode for the generated [MeshInstance3D]. Setting to [b]GI_MODE_STATIC[/b] will unwrap the mesh's UV2 during build.
@export var global_illumination_mode : GeometryInstance3D.GIMode = GeometryInstance3D.GI_MODE_STATIC
## @deprecated: Use [member global_illumination_mode] instead. [br]Sets generated [MeshInstance3D] to be available for UV2 unwrapping after [FuncGodotMap] build. Utilized in baked lightmapping.
@export var use_in_baked_light : bool = true @export var use_in_baked_light : bool = true
## Shadow casting setting allows for further lightmapping customization. ## Shadow casting setting allows for further lightmapping customization.
@export var shadow_casting_setting : GeometryInstance3D.ShadowCastingSetting = GeometryInstance3D.SHADOW_CASTING_SETTING_ON @export var shadow_casting_setting : GeometryInstance3D.ShadowCastingSetting = GeometryInstance3D.SHADOW_CASTING_SETTING_ON
@@ -51,8 +65,41 @@ enum CollisionShapeType {
## The collision margin for the Solid Class' collision shapes. Not used in Godot Physics. See [Shape3D] for details. ## The collision margin for the Solid Class' collision shapes. Not used in Godot Physics. See [Shape3D] for details.
@export var collision_shape_margin: float = 0.04 @export var collision_shape_margin: float = 0.04
## The following properties tell FuncGodot to add a [i]"func_godot_mesh_data"[/i] Dictionary to the metadata of the generated node upon build.
## This data is parallelized, so that each element of the array is ordered to reference the same face in the mesh.
@export_group("Mesh Metadata")
## Add a texture lookup table to the generated node's metadata on build.[br][br]
## The data is split between an [Array] of [StringName] called [i]"texture_names"[/i] containing all currently used texture materials
## and a [PackedInt32Array] called [i]"textures"[/i] where each element is an index corresponding to the [i]"texture_names"[/i] entries.
@export var add_textures_metadata: bool = false
## Add a [PackedVector3Array] called [i]"vertices"[/i] to the generated node's metadata on build.[br][br]
## This is a list of every vertex in the generated node's [MeshInstance3D]. Every 3 vertices represent a single face.
@export var add_vertex_metadata: bool = false
## Add a [PackedVector3Array] called [i]"positions"[/i] to the generated node's metadata on build.[br][br]
## This is a list of positions for each face, local to the generated node, calculated by averaging the vertices to find the face's center.
@export var add_face_position_metadata: bool = false
## Add a [PackedVector3Array] called [i]"normals"[/i] to the generated node's metadata on build.[br][br]
## Contains a list of each face's normal.
@export var add_face_normal_metadata: bool = false
## Add a [Dictionary] called [i]"collision_shape_to_face_indices_map"[/i] in the generated node's metadata on build.[br][br]
## Contains keys of strings, which are the names of child [CollisionShape3D] nodes, and values of
## [PackedInt32Array], containing indices of that child's faces.[br][br]
## For example, an element of [br][br][code]{ "entity_1_brush_0_collision_shape" : [0, 1, 3] }[/code][br][br]
## shows that this solid class has been generated with one child collision shape named
## [i]entity_1_brush_0_collision_shape[/i] which handles 3 faces of the mesh with collision, at indices 0, 1, and 3.
@export var add_collision_shape_to_face_indices_metadata : bool = false
## [s]Add a [Dictionary] called [i]"collision_shape_to_face_range_map"[/i] in the generated node's metadata on build.[br][br]
## Contains keys of strings, which are the names of child [CollisionShape3D] nodes, and values of
## [Vector2i], where [i]X[/i] represents the starting index of that child's faces and [i]Y[/i] represents the
## ending index.[br][br]
## For example, an element of [br][br][code]{ "entity_1_brush_0_collision_shape" : Vector2i(0, 15) }[/code][br][br]
## shows that this solid class has been generated with one child collision shape named
## [i]entity_1_brush_0_collision_shape[/i] which handles the first 15 faces of the parts of the mesh with collision.[/s]
## @deprecated: No longer supported or planned as of 2025.7, but retained in case a contributor provides an appropriate solution in the future.
@export var add_collision_shape_face_range_metadata: bool = false
@export_group("Scripting") @export_group("Scripting")
## An optional script file to attach to the node generated on map build. ## An optional [Script] file to attach to the node generated on map build.
@export var script_class: Script @export var script_class: Script
func _init(): func _init():

View File

@@ -1 +1 @@
uid://cmoudtn1s5egw uid://5cow84q03m6a

View File

@@ -1,13 +1,12 @@
@tool @tool
class_name FuncGodotPlugin @icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
extends EditorPlugin class_name FuncGodotPlugin extends EditorPlugin
var map_import_plugin : QuakeMapImportPlugin = null var map_import_plugin : QuakeMapImportPlugin = null
var palette_import_plugin : QuakePaletteImportPlugin = null var palette_import_plugin : QuakePaletteImportPlugin = null
var wad_import_plugin: QuakeWadImportPlugin = null var wad_import_plugin: QuakeWadImportPlugin = null
var func_godot_map_control: Control = null #var func_godot_map_progress_bar: Control = null
var func_godot_map_progress_bar: Control = null
var edited_object_ref: WeakRef = weakref(null) var edited_object_ref: WeakRef = weakref(null)
func _get_plugin_name() -> String: func _get_plugin_name() -> String:
@@ -15,16 +14,13 @@ func _get_plugin_name() -> String:
func _handles(object: Object) -> bool: func _handles(object: Object) -> bool:
return object is FuncGodotMap return object is FuncGodotMap
func _edit(object: Object) -> void: func _edit(object: Object) -> void:
edited_object_ref = weakref(object) edited_object_ref = weakref(object)
func _make_visible(visible: bool) -> void: #func _make_visible(visible: bool) -> void:
if func_godot_map_control: #if func_godot_map_progress_bar:
func_godot_map_control.set_visible(visible) #func_godot_map_progress_bar.set_visible(visible)
if func_godot_map_progress_bar:
func_godot_map_progress_bar.set_visible(visible)
func _enter_tree() -> void: func _enter_tree() -> void:
# Import plugins # Import plugins
@@ -36,16 +32,46 @@ func _enter_tree() -> void:
add_import_plugin(palette_import_plugin) add_import_plugin(palette_import_plugin)
add_import_plugin(wad_import_plugin) add_import_plugin(wad_import_plugin)
# FuncGodotMap button #func_godot_map_progress_bar = create_func_godot_map_progress_bar()
func_godot_map_control = create_func_godot_map_control() #func_godot_map_progress_bar.set_visible(false)
func_godot_map_control.set_visible(false) #add_control_to_container(EditorPlugin.CONTAINER_INSPECTOR_BOTTOM, func_godot_map_progress_bar)
add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, func_godot_map_control)
func_godot_map_progress_bar = create_func_godot_map_progress_bar()
func_godot_map_progress_bar.set_visible(false)
add_control_to_container(EditorPlugin.CONTAINER_INSPECTOR_BOTTOM, func_godot_map_progress_bar)
add_custom_type("FuncGodotMap", "Node3D", preload("res://addons/func_godot/src/map/func_godot_map.gd"), null) add_custom_type("FuncGodotMap", "Node3D", preload("res://addons/func_godot/src/map/func_godot_map.gd"), null)
# Default Map Settings
if not ProjectSettings.has_setting("func_godot/default_map_settings"):
ProjectSettings.set_setting("func_godot/default_map_settings", "res://addons/func_godot/func_godot_default_map_settings.tres")
var property_info = {
"name": "func_godot/default_map_settings",
"type": TYPE_STRING,
"hint": PROPERTY_HINT_FILE,
"hint_string": "*.tres"
}
ProjectSettings.add_property_info(property_info)
ProjectSettings.set_as_basic("func_godot/default_map_settings", true)
ProjectSettings.set_initial_value("func_godot/default_map_settings", "res://addons/func_godot/func_godot_default_map_settings.tres")
# Default Inverse Scale Factor
if not ProjectSettings.has_setting("func_godot/default_inverse_scale_factor"):
ProjectSettings.set_setting("func_godot/default_inverse_scale_factor", 32.0)
var property_info = {
"name": "func_godot/default_inverse_scale_factor",
"type": TYPE_FLOAT
}
ProjectSettings.add_property_info(property_info)
ProjectSettings.set_as_basic("func_godot/default_inverse_scale_factor", true)
ProjectSettings.set_initial_value("func_godot/default_inverse_scale_factor", 32.0)
# Model Point Class Default Path
if not ProjectSettings.has_setting("func_godot/model_point_class_save_path"):
ProjectSettings.set_setting("func_godot/model_point_class_save_path", "")
var property_info = {
"name": "func_godot/model_point_class_save_path",
"type": TYPE_STRING
}
ProjectSettings.add_property_info(property_info)
ProjectSettings.set_as_basic("func_godot/model_point_class_save_path", true)
ProjectSettings.set_initial_value("func_godot/model_point_class_save_path", "")
func _exit_tree() -> void: func _exit_tree() -> void:
remove_custom_type("FuncGodotMap") remove_custom_type("FuncGodotMap")
@@ -57,119 +83,49 @@ func _exit_tree() -> void:
map_import_plugin = null map_import_plugin = null
palette_import_plugin = null palette_import_plugin = null
wad_import_plugin = null wad_import_plugin = null
if func_godot_map_control:
remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, func_godot_map_control)
func_godot_map_control.queue_free()
func_godot_map_control = null
if func_godot_map_progress_bar:
remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_BOTTOM, func_godot_map_progress_bar)
func_godot_map_progress_bar.queue_free()
func_godot_map_progress_bar = null
## Create the toolbar controls for [FuncGodotMap] instances in the editor
func create_func_godot_map_control() -> Control:
var separator = VSeparator.new()
var icon = TextureRect.new() #if func_godot_map_progress_bar:
icon.texture = preload("res://addons/func_godot/icons/icon_slipgate3d.svg") #remove_control_from_container(EditorPlugin.CONTAINER_INSPECTOR_BOTTOM, func_godot_map_progress_bar)
icon.size_flags_vertical = Control.SIZE_SHRINK_CENTER #func_godot_map_progress_bar.queue_free()
#func_godot_map_progress_bar = null
var build_button = Button.new()
build_button.text = "Build"
build_button.connect("pressed",Callable(self,"func_godot_map_build"))
var unwrap_uv2_button = Button.new()
unwrap_uv2_button.text = "Unwrap UV2"
unwrap_uv2_button.connect("pressed",Callable(self,"func_godot_map_unwrap_uv2"))
var control = HBoxContainer.new()
control.add_child(separator)
control.add_child(icon)
control.add_child(build_button)
control.add_child(unwrap_uv2_button)
return control
## Create a progress bar for building a [FuncGodotMap] # Create a progress bar for building a [FuncGodotMap]
func create_func_godot_map_progress_bar() -> Control: #func create_func_godot_map_progress_bar() -> Control:
var progress_label = Label.new() #var progress_label = Label.new()
progress_label.name = "ProgressLabel" #progress_label.name = "ProgressLabel"
progress_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER #progress_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
progress_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER #progress_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
#
#var progress_bar := ProgressBar.new()
#progress_bar.name = "ProgressBar"
#progress_bar.show_percentage = false
#progress_bar.min_value = 0.0
#progress_bar.max_value = 1.0
#progress_bar.custom_minimum_size.y = 30
#progress_bar.set_anchors_and_offsets_preset(Control.PRESET_LEFT_WIDE)
#progress_bar.add_child(progress_label)
#progress_label.set_anchors_and_offsets_preset(Control.PRESET_LEFT_WIDE)
#progress_label.offset_top = -9
#progress_label.offset_left = 3
#
#return progress_bar
var progress_bar := ProgressBar.new() # Update the build progress bar (see: [method create_func_godot_map_progress_bar]) to display the current step and progress (0-1)
progress_bar.name = "ProgressBar" #func func_godot_map_build_progress(step: String, progress: float) -> void:
progress_bar.show_percentage = false #var progress_label = func_godot_map_progress_bar.get_node("ProgressLabel")
progress_bar.min_value = 0.0 #func_godot_map_progress_bar.value = progress
progress_bar.max_value = 1.0 #progress_label.text = step.capitalize()
progress_bar.custom_minimum_size.y = 30
progress_bar.set_anchors_and_offsets_preset(Control.PRESET_LEFT_WIDE)
progress_bar.add_child(progress_label)
progress_label.set_anchors_and_offsets_preset(Control.PRESET_LEFT_WIDE)
progress_label.offset_top = -9
progress_label.offset_left = 3
return progress_bar
## Create the "Build" button for [FuncGodotMap]s in the editor
func func_godot_map_build() -> void:
var edited_object : FuncGodotMap = edited_object_ref.get_ref()
if not edited_object:
return
edited_object.should_add_children = true
edited_object.should_set_owners = true
set_func_godot_map_control_disabled(true)
edited_object.build_progress.connect(func_godot_map_build_progress)
edited_object.build_complete.connect(func_godot_map_build_complete.bind(edited_object))
edited_object.build_failed.connect(func_godot_map_build_complete.bind(edited_object))
edited_object.verify_and_build()
## Create the "Unwrap UV2" button for [FuncGodotMap]s in the editor
func func_godot_map_unwrap_uv2() -> void:
var edited_object = edited_object_ref.get_ref()
if not edited_object:
return
if not edited_object is FuncGodotMap:
return
set_func_godot_map_control_disabled(true)
edited_object.connect("unwrap_uv2_complete", func_godot_map_build_complete.bind(edited_object))
edited_object.unwrap_uv2()
## Enable or disable the control for [FuncGodotMap]s in the editor
func set_func_godot_map_control_disabled(disabled: bool) -> void:
if not func_godot_map_control:
return
for child in func_godot_map_control.get_children():
if child is Button:
child.set_disabled(disabled)
## Update the build progress bar (see: [method create_func_godot_map_progress_bar]) to display the current step and progress (0-1)
func func_godot_map_build_progress(step: String, progress: float) -> void:
var progress_label = func_godot_map_progress_bar.get_node("ProgressLabel")
func_godot_map_progress_bar.value = progress
progress_label.text = step.capitalize()
## Callback for when the build process for a [FuncGodotMap] is finished. ## Callback for when the build process for a [FuncGodotMap] is finished.
func func_godot_map_build_complete(func_godot_map: FuncGodotMap) -> void: func func_godot_map_build_complete(func_godot_map: FuncGodotMap) -> void:
var progress_label = func_godot_map_progress_bar.get_node("ProgressLabel") #var progress_label = func_godot_map_progress_bar.get_node("ProgressLabel")
progress_label.text = "Build Complete" #progress_label.text = "Build Complete"
set_func_godot_map_control_disabled(false) #if func_godot_map.is_connected("build_progress",Callable(self,"func_godot_map_build_progress")):
#func_godot_map.disconnect("build_progress",Callable(self,"func_godot_map_build_progress"))
if func_godot_map.is_connected("build_progress",Callable(self,"func_godot_map_build_progress")):
func_godot_map.disconnect("build_progress",Callable(self,"func_godot_map_build_progress"))
if func_godot_map.is_connected("build_complete",Callable(self,"func_godot_map_build_complete")): if func_godot_map.is_connected("build_complete",Callable(self,"func_godot_map_build_complete")):
func_godot_map.disconnect("build_complete",Callable(self,"func_godot_map_build_complete")) func_godot_map.disconnect("build_complete",Callable(self,"func_godot_map_build_complete"))
if func_godot_map.is_connected("build_failed",Callable(self,"func_godot_map_build_complete")): if func_godot_map.is_connected("build_failed",Callable(self,"func_godot_map_build_complete")):
func_godot_map.disconnect("build_failed",Callable(self,"func_godot_map_build_complete")) func_godot_map.disconnect("build_failed",Callable(self,"func_godot_map_build_complete"))

View File

@@ -1 +1 @@
uid://di8rxxgk8bt6b uid://bqy3tr83l7di

View File

@@ -1,5 +1,14 @@
@icon("res://addons/func_godot/icons/icon_quake_file.svg") @icon("res://addons/func_godot/icons/icon_quake_file.svg")
class_name QuakeMapFile class_name QuakeMapFile extends Resource
extends Resource ## Map file that can be built by [FuncGodotMap].
##
## Map file that can be built by a [FuncGodotMap]. Supports the Quake and Valve map formats.
##
## @tutorial(Quake Wiki Map Format Article): https://quakewiki.org/wiki/Quake_Map_Format
## @tutorial(Valve Developer Wiki VMF Article): https://developer.valvesoftware.com/wiki/VMF_(Valve_Map_Format)
## Number of times this map file has been imported.
@export var revision: int = 0 @export var revision: int = 0
## Raw map data.
@export_multiline var map_data: String = ""

View File

@@ -1 +1 @@
uid://cumjyudn5ug5t uid://cxvwf50mehesf

View File

@@ -1,8 +1,5 @@
@tool @tool
class_name QuakeMapImportPlugin class_name QuakeMapImportPlugin extends EditorImportPlugin
extends EditorImportPlugin
# Quake super.map import plugin
func _get_importer_name() -> String: func _get_importer_name() -> String:
return 'func_godot.map' return 'func_godot.map'
@@ -14,7 +11,7 @@ func _get_resource_type() -> String:
return 'Resource' return 'Resource'
func _get_recognized_extensions() -> PackedStringArray: func _get_recognized_extensions() -> PackedStringArray:
return PackedStringArray(['map']) return PackedStringArray(['map','vmf'])
func _get_priority(): func _get_priority():
return 1.0 return 1.0
@@ -36,11 +33,11 @@ func _import(source_file, save_path, options, r_platform_variants, r_gen_files)
var map_resource : QuakeMapFile = null var map_resource : QuakeMapFile = null
var existing_resource := load(save_path_str) as QuakeMapFile if ResourceLoader.exists(save_path_str):
if(existing_resource != null): map_resource = load(save_path_str) as QuakeMapFile
map_resource = existing_resource
map_resource.revision += 1 map_resource.revision += 1
else: else:
map_resource = QuakeMapFile.new() map_resource = QuakeMapFile.new()
map_resource.map_data = FileAccess.open(source_file, FileAccess.READ).get_as_text(true)
return ResourceSaver.save(map_resource, save_path_str) return ResourceSaver.save(map_resource, save_path_str)

View File

@@ -1 +1 @@
uid://chmlk6gbseg5k uid://dnsj08ot32vpc

View File

@@ -1,7 +1,13 @@
@icon("res://addons/func_godot/icons/icon_quake_file.svg") @icon("res://addons/func_godot/icons/icon_quake_file.svg")
class_name QuakePaletteFile class_name QuakePaletteFile extends Resource
extends Resource ## Quake LMP palette format file used with [QuakeWadFile].
##
## Quake LMP palette format file used in conjunction with a Quake WAD2 format [QuakeWadFile].
## Not required for the Valve WAD3 format.
##
## @tutorial(Quake Wiki Palette Article): https://quakewiki.org/wiki/Quake_palette#palette.lmp
## Collection of [Color]s retrieved from the LMP palette file.
@export var colors: PackedColorArray @export var colors: PackedColorArray
func _init(colors): func _init(colors):

View File

@@ -1 +1 @@
uid://bsoyy4aub0hwo uid://dqhjx7jjbif5d

View File

@@ -1,8 +1,5 @@
@tool @tool
class_name QuakePaletteImportPlugin class_name QuakePaletteImportPlugin extends EditorImportPlugin
extends EditorImportPlugin
# Quake super.map import plugin
func _get_importer_name() -> String: func _get_importer_name() -> String:
return 'func_godot.palette' return 'func_godot.palette'
@@ -58,4 +55,4 @@ func _import(source_file, save_path, options, r_platform_variants, r_gen_files)
var palette_resource := QuakePaletteFile.new(colors) var palette_resource := QuakePaletteFile.new(colors)
return ResourceSaver.save(palette_resource, save_path_str) return ResourceSaver.save(palette_resource, save_path_str)

View File

@@ -1 +1 @@
uid://djehnbm5nyrwu uid://c6k7hftart3u3

View File

@@ -1,8 +1,14 @@
@icon("res://addons/func_godot/icons/icon_quake_file.svg") @icon("res://addons/func_godot/icons/icon_quake_file.svg")
class_name QuakeWadFile class_name QuakeWadFile extends Resource
extends Resource ## Texture container in the WAD2 or WAD3 format.
##
## Texture container in the Quake WAD2 or Valve WAD3 format.
##
## @tutorial(Quake Wiki WAD Article): https://quakewiki.org/wiki/Texture_Wad
## @tutorial(Valve Developer Wiki WAD3 Article): https://developer.valvesoftware.com/wiki/WAD
@export var textures: Dictionary ## Collection of [ImageTexture] imported from the WAD file.
@export var textures: Dictionary[String, ImageTexture]
func _init(textures: Dictionary): func _init(textures: Dictionary = Dictionary()):
self.textures = textures self.textures = textures

View File

@@ -1 +1 @@
uid://clpdmkhontvmb uid://cij36hpqc46c

View File

@@ -1,13 +1,24 @@
@tool @tool
class_name QuakeWadImportPlugin extends EditorImportPlugin class_name QuakeWadImportPlugin extends EditorImportPlugin
enum WadEntryType { enum WadFormat {
Quake,
HalfLife
}
enum QuakeWadEntryType {
Palette = 0x40, Palette = 0x40,
SBarPic = 0x42, SBarPic = 0x42,
MipsTexture = 0x44, MipsTexture = 0x44,
ConsolePic = 0x45 ConsolePic = 0x45
} }
enum HalfLifeWadEntryType {
QPic = 0x42,
MipsTexture = 0x43,
FixedFont = 0x45
}
const TEXTURE_NAME_LENGTH := 16 const TEXTURE_NAME_LENGTH := 16
const MAX_MIP_LEVELS := 4 const MAX_MIP_LEVELS := 4
@@ -15,7 +26,7 @@ func _get_importer_name() -> String:
return 'func_godot.wad' return 'func_godot.wad'
func _get_visible_name() -> String: func _get_visible_name() -> String:
return 'Quake Texture2D WAD' return 'Quake WAD'
func _get_resource_type() -> String: func _get_resource_type() -> String:
return 'Resource' return 'Resource'
@@ -61,21 +72,25 @@ func _import(source_file, save_path, options, r_platform_variants, r_gen_files)
var err = FileAccess.get_open_error() var err = FileAccess.get_open_error()
print(['Error opening super.wad file: ', err]) print(['Error opening super.wad file: ', err])
return err return err
var palette_path : String = options['palette_file']
var palette_file : QuakePaletteFile = load(palette_path) as QuakePaletteFile
if not palette_file:
print('Error: Invalid palette file')
return ERR_CANT_ACQUIRE_RESOURCE
# Read WAD header # Read WAD header
var magic : PackedByteArray = file.get_buffer(4) var magic : PackedByteArray = file.get_buffer(4)
var magic_string : String = magic.get_string_from_ascii() var magic_string : String = magic.get_string_from_ascii()
var wad_format: int = WadFormat.Quake
if(magic_string != 'WAD2'):
if magic_string == 'WAD3':
wad_format = WadFormat.HalfLife
elif magic_string != 'WAD2':
print('Error: Invalid WAD magic') print('Error: Invalid WAD magic')
return ERR_INVALID_DATA return ERR_INVALID_DATA
var palette_path : String = options['palette_file']
var palette_file : QuakePaletteFile = load(palette_path) as QuakePaletteFile
if wad_format == WadFormat.Quake and not palette_file:
print('Error: Invalid Quake palette file')
file.close()
return ERR_CANT_ACQUIRE_RESOURCE
var num_entries : int = file.get_32() var num_entries : int = file.get_32()
var dir_offset : int = file.get_32() var dir_offset : int = file.get_32()
@@ -94,8 +109,9 @@ func _import(source_file, save_path, options, r_platform_variants, r_gen_files)
var unknown : int = file.get_16() var unknown : int = file.get_16()
var name : PackedByteArray = file.get_buffer(TEXTURE_NAME_LENGTH) var name : PackedByteArray = file.get_buffer(TEXTURE_NAME_LENGTH)
var name_string : String = name.get_string_from_ascii() var name_string : String = name.get_string_from_ascii()
if type == int(WadEntryType.MipsTexture): if (wad_format == WadFormat.Quake and type == int(QuakeWadEntryType.MipsTexture)) or (
wad_format == WadFormat.HalfLife and type == int(HalfLifeWadEntryType.MipsTexture)):
entries.append([ entries.append([
offset, offset,
in_wad_size, in_wad_size,
@@ -104,51 +120,90 @@ func _import(source_file, save_path, options, r_platform_variants, r_gen_files)
compression, compression,
name_string name_string
]) ])
# Read mip textures # Read mip textures
var texture_data_array: Array = [] var texture_data_array: Array = []
for entry in entries: for entry in entries:
var offset : int = entry[0] var offset : int = entry[0]
file.seek(0)
file.seek(offset) file.seek(offset)
var name : PackedByteArray = file.get_buffer(TEXTURE_NAME_LENGTH) var name : PackedByteArray = file.get_buffer(TEXTURE_NAME_LENGTH)
var name_string : String = name.get_string_from_ascii() var name_string : String = name.get_string_from_ascii()
var width : int = file.get_32() var width : int = file.get_32()
var height : int = file.get_32() var height : int = file.get_32()
var mip_offsets : Array = [] var mip_offsets : Array = []
for idx in range(0, MAX_MIP_LEVELS): for idx in range(0, MAX_MIP_LEVELS):
mip_offsets.append(file.get_32()) mip_offsets.append(file.get_32())
var num_pixels : int = width * height var num_pixels : int = width * height
texture_data_array.append([name_string, width, height, file.get_buffer(num_pixels)]) var pixels : PackedByteArray = file.get_buffer(num_pixels)
if wad_format == WadFormat.Quake:
texture_data_array.append([name_string, width, height, pixels])
continue
# Half-Life WADs have a 256 color palette embedded in each texture
elif wad_format == WadFormat.HalfLife:
# Find the end of the mipmap data
file.seek(offset + mip_offsets[-1] + (width / 8) * (height / 8))
file.get_16()
var palette_colors := PackedColorArray()
for idx in 256:
var red : int = file.get_8()
var green : int = file.get_8()
var blue : int = file.get_8()
var color := Color(red / 255.0, green / 255.0, blue / 255.0)
palette_colors.append(color)
texture_data_array.append([name_string, width, height, pixels, palette_colors])
# Create texture resources # Create texture resources
var textures : Dictionary = {} var textures : Dictionary[String, ImageTexture] = {}
for texture_data in texture_data_array: for texture_data in texture_data_array:
var name : String = texture_data[0] var name : String = texture_data[0]
var width : int = texture_data[1] var width : int = texture_data[1]
var height : int = texture_data[2] var height : int = texture_data[2]
var pixels : PackedByteArray = texture_data[3] var pixels : PackedByteArray = texture_data[3]
var texture_image : Image
var pixels_rgb := PackedByteArray() var pixels_rgb := PackedByteArray()
for palette_color in pixels:
var rgb_color := palette_file.colors[palette_color] as Color if wad_format == WadFormat.HalfLife:
pixels_rgb.append(rgb_color.r8) var colors : PackedColorArray = texture_data[4]
pixels_rgb.append(rgb_color.g8) for palette_color in pixels:
pixels_rgb.append(rgb_color.b8) var rgb_color : Color = colors[palette_color]
pixels_rgb.append(rgb_color.r8)
var texture_image := Image.create_from_data(width, height, false, Image.FORMAT_RGB8, pixels_rgb) pixels_rgb.append(rgb_color.g8)
pixels_rgb.append(rgb_color.b8)
# Color(0, 0, 255) is used for transparency in Half-Life
if rgb_color.b == 1 and rgb_color.r == 0 and rgb_color.b == 0:
pixels_rgb.append(0)
else:
pixels_rgb.append(255)
texture_image = Image.create_from_data(width, height, false, Image.FORMAT_RGBA8, pixels_rgb)
else: # WadFormat.Quake
for palette_color in pixels:
var rgb_color : Color = palette_file.colors[palette_color]
pixels_rgb.append(rgb_color.r8)
pixels_rgb.append(rgb_color.g8)
pixels_rgb.append(rgb_color.b8)
# Palette index 255 is used for transparency
if palette_color != 255:
pixels_rgb.append(255)
else:
pixels_rgb.append(0)
texture_image = Image.create_from_data(width, height, false, Image.FORMAT_RGBA8, pixels_rgb)
if options["generate_mipmaps"] == true: if options["generate_mipmaps"] == true:
texture_image.generate_mipmaps() texture_image.generate_mipmaps()
var texture := ImageTexture.create_from_image(texture_image) #,Texture2D.FLAG_MIPMAPS | Texture2D.FLAG_REPEAT | Texture2D.FLAG_ANISOTROPIC_FILTER var texture := ImageTexture.create_from_image(texture_image) #,Texture2D.FLAG_MIPMAPS | Texture2D.FLAG_REPEAT | Texture2D.FLAG_ANISOTROPIC_FILTER
textures[name.to_lower()] = texture
textures[name] = texture
# Save WAD resource # Save WAD resource
var wad_resource := QuakeWadFile.new(textures) var wad_resource := QuakeWadFile.new(textures)
return ResourceSaver.save(wad_resource, save_path_str) return ResourceSaver.save(wad_resource, save_path_str)

View File

@@ -1 +1 @@
uid://cv4ixyaqrjisb uid://ridgf32rxg6s

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1 @@
uid://d0x6kb7gh8xb4 uid://cwu5cf7a0awcd

View File

@@ -1,19 +1,49 @@
@tool
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg") @icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
class_name FuncGodotMapSettings extends Resource
## Reusable map settings configuration for [FuncGodotMap] nodes. ## Reusable map settings configuration for [FuncGodotMap] nodes.
class_name FuncGodotMapSettings
extends Resource
## Ratio between map editor units and Godot units. FuncGodot will divide brush coordinates by this number when building. This does not affect entity properties unless scripted to do so. #region BUILD
@export var inverse_scale_factor: float = 32.0 @export_category("Build Settings")
## Set automatically when [member inverse_scale_factor] is changed. Used primarily during the build process.
var scale_factor: float = 0.03125
## Ratio between map editor units and Godot units. FuncGodot will divide brush coordinates by this number and save the results to [member scale_factor].
## This does not affect entity properties unless scripted to do so.
@export var inverse_scale_factor: float = 32.0 :
set(value):
if value == 0.0:
printerr("Error: Cannot set Inverse Scale Factor to Zero")
return
inverse_scale_factor = value
scale_factor = 1.0 / value
## [FuncGodotFGDFile] that translates map file classnames into Godot nodes and packed scenes. ## [FuncGodotFGDFile] that translates map file classnames into Godot nodes and packed scenes.
@export var entity_fgd: FuncGodotFGDFile = preload("res://addons/func_godot/fgd/func_godot_fgd.tres") @export var entity_fgd: FuncGodotFGDFile = preload("res://addons/func_godot/fgd/func_godot_fgd.tres")
## Default class property to use in naming generated nodes. This setting is overridden by `name_property` in [FuncGodotFGDEntityClass]. ## Default class property to use in naming generated nodes. This setting is overridden by [member FuncGodotFGDEntityClass.name_property].
## Naming occurs before adding to the [SceneTree] and applying properties. ## Naming occurs before adding to the [SceneTree] and applying properties.
## Nodes will be named `"entity_" + name_property`. An entity's name should be unique, otherwise you may run into unexpected behavior. ## Nodes will be named `"entity_" + name_property`. An entity's name should be unique, otherwise you may run into unexpected behavior.
@export var entity_name_property: String = "" @export var entity_name_property: String = ""
## Class property that determines whether the [FuncGodotFGDSolidClass] entity performs mesh smoothing operations.
@export var entity_smoothing_property: String = "_phong"
## Class property that contains the angular threshold that determines when a [FuncGodotFGDSolidClass] entity's mesh vertices are smoothed.
@export var entity_smoothing_angle_property: String = "_phong_angle"
## If true, will organize [SceneTree] using TrenchBroom Layers and Groups or Hammer Visgroups. Groups will be generated as [Node3D] nodes.
## All non-entity structural brushes will be moved out of their groups and merged into the `Worldspawn` entity.
## Any Layers toggled to be omitted from export in TrenchBroom and their child entities and groups will not be built.
@export var use_groups_hierarchy: bool = false
## Class property that contains the snapping epsilon for generated vertices of [FuncGodotFGDSolidClass] entities.
## Utilizing this property can help reduce instances of seams between polygons.
@export var vertex_merge_distance_property: String = "_vertex_merge_distance"
#endregion
#region TEXTURES
@export_category("Textures") @export_category("Textures")
## Base directory for textures. When building materials, FuncGodot will search this directory for texture files with matching names to the textures assigned to map brush faces. ## Base directory for textures. When building materials, FuncGodot will search this directory for texture files with matching names to the textures assigned to map brush faces.
@@ -22,17 +52,38 @@ extends Resource
## File extensions to search for texture data. ## File extensions to search for texture data.
@export var texture_file_extensions: Array[String] = ["png", "jpg", "jpeg", "bmp", "tga", "webp"] @export var texture_file_extensions: Array[String] = ["png", "jpg", "jpeg", "bmp", "tga", "webp"]
## Optional path for the clip texture, relative to [member base_texture_dir]. Brush faces textured with the clip texture will have those Faces removed from the generated [MeshInstance3D] but not the generated [CollisionShape3D]. ## Optional path for the clip texture, relative to [member base_texture_dir].
@export var clip_texture: String = "special/clip" ## Brush faces textured with the clip texture will have those faces removed from the generated [Mesh] but not the generated [Shape3D].
@export var clip_texture: String = "special/clip":
set(tex):
clip_texture = tex.to_lower()
## Optional path for the skip texture, relative to [member base_texture_dir]. Brush faces textured with the skip texture will have those Faces removed from the generated [MeshInstance3D]. If the [FuncGodotFGDSolidClass] `collision_shape_type` is set to concave then it will also remove collision from those faces in the generated [CollisionShape3D]. ## Optional path for the skip texture, relative to [member base_texture_dir].
@export var skip_texture: String = "special/skip" ## Brush faces textured with the skip texture will have those faces removed from the generated [Mesh].
## If [member FuncGodotFGDSolidClass.collision_shape_type] is set to concave then it will also remove collision from those faces in the generated [Shape3D].
@export var skip_texture: String = "special/skip":
set(tex):
skip_texture = tex.to_lower()
## Optional [QuakeWADFile] resources to apply textures from. See the [Quake Wiki](https://quakewiki.org/wiki/Texture_Wad) for more information on Quake Texture WADs. ## Optional path for the origin texture, relative to [member base_texture_dir].
@export var texture_wads: Array[Resource] = [] ## Brush faces textured with the origin texture will have those faces removed from the generated [Mesh] and [Shape3D].
## The bounds of these faces will be used to calculate the origin point of the entity.
@export var origin_texture: String = "special/origin":
set(tex):
origin_texture = tex.to_lower()
## Optional [QuakeWadFile] resources to apply textures from. See the [Quake Wiki](https://quakewiki.org/wiki/Texture_Wad) for more information on Quake Texture WADs.
@export var texture_wads: Array[QuakeWadFile] = []
#endregion
#region MATERIALS
@export_category("Materials") @export_category("Materials")
## Base directory for loading and saving materials. When building materials, FuncGodot will search this directory for material resources
## with matching names to the textures assigned to map brush faces. If not found, will fall back to [member base_texture_dir].
@export_dir var base_material_dir: String = ""
## File extension to search for [Material] definitions ## File extension to search for [Material] definitions
@export var material_file_extension: String = "tres" @export var material_file_extension: String = "tres"
@@ -56,21 +107,19 @@ extends Resource
@export var ao_map_pattern: String = "%s_ao.%s" @export var ao_map_pattern: String = "%s_ao.%s"
## Automatic PBR material generation height map pattern ## Automatic PBR material generation height map pattern
@export var height_map_pattern: String = "%s_height.%s" @export var height_map_pattern: String = "%s_height.%s"
## Automatic PBR material generation ORM map pattern
@export var orm_map_pattern: String = "%s_orm.%s"
## If true, all materials will be unshaded, ignoring light. Also known as "fullbright". ## Save automatically generated materials to disk, allowing reuse across [FuncGodotMap] nodes.
@export var unshaded: bool = false ## [i]NOTE: Materials do not use the [member default_material] settings after saving.[/i]
## Save automatically generated materials to disk, allowing reuse across [FuncGodotMap] nodes. [i]NOTE: Materials do not use the Default Material settings after saving.[/i]
@export var save_generated_materials: bool = true @export var save_generated_materials: bool = true
#endregion
@export_category("UV Unwrap") @export_category("UV Unwrap")
## Texel size for UV2 unwrapping. ## Texel size for UV2 unwrapping.
## A texel size of 1 will lead to a 1:1 correspondence between texture texels and lightmap texels. Larger values will produce less detailed lightmaps. To conserve memory and filesize, use the largest value that still looks good. ## Actual texel size is uv_unwrap_texel_size / [member inverse_scale_factor]. A ratio of 1/16 is usually a good place to start with
@export var uv_unwrap_texel_size: float = 1.0 ## (if inverse_scale_factor is 32, start with a uv_unwrap_texel_size of 2).
## Larger values will produce less detailed lightmaps. To conserve memory and filesize, use the largest value that still looks good.
@export_category("TrenchBroom") @export var uv_unwrap_texel_size: float = 2.0
## If true, will organize Scene Tree using Trenchbroom Layers and Groups. Layers and Groups will be generated as [Node3D] nodes.
## All structural brushes will be moved out of the Layers and Groups and merged into the Worldspawn entity.
@export var use_trenchbroom_groups_hierarchy: bool = false

View File

@@ -1 +1 @@
uid://dhlc0pfo61428 uid://38q6k0ctahjn

View File

@@ -1,20 +1,21 @@
@tool @tool
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg") @icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
class_name NetRadiantCustomGamePackConfig extends Resource
## Builds a gamepack for NetRadiant Custom. ## Builds a gamepack for NetRadiant Custom.
class_name NetRadiantCustomGamePackConfig ##
extends Resource ## Resource that builds a gamepack configuration for NetRadiant Custom.
## Button to export / update this gamepack's configuration in the NetRadiant Custom Gamepacks Folder. enum NetRadiantCustomMapType {
@export var export_file: bool: QUAKE_1, ## Removes PatchDef entries from the map file.
get: QUAKE_3 ## Allows the saving of PatchDef entries in the map file.
return export_file }
set(new_export_file):
if new_export_file != export_file: @export_tool_button("Export Gamepack") var _export_file: Callable = export_file
if Engine.is_editor_hint():
do_export_file()
## Gamepack folder and file name. Must be lower case and must not contain special characters. ## Gamepack folder and file name. Must be lower case and must not contain special characters.
@export var gamepack_name : String = "func_godot" @export var gamepack_name : String = "func_godot":
set(new_name):
gamepack_name = new_name.to_lower()
## Name of the game in NetRadiant Custom's gamepack list. ## Name of the game in NetRadiant Custom's gamepack list.
@export var game_name : String = "FuncGodot" @export var game_name : String = "FuncGodot"
@@ -22,13 +23,15 @@ extends Resource
## Directory path containing your maps, textures, shaders, etc... relative to your project directory. ## Directory path containing your maps, textures, shaders, etc... relative to your project directory.
@export var base_game_path : String = "" @export var base_game_path : String = ""
## FGD resource to include with this gamepack. If using multiple FGD resources, this should be the master FGD that contains them in the `base_fgd_files` resource array. ## [FuncGodotFGDFile] to include with this gamepack. If using multiple FGD file resources,
## this should be the master FGD that contains them in [member FuncGodotFGDFile.base_fgd_files].
@export var fgd_file : FuncGodotFGDFile = preload("res://addons/func_godot/fgd/func_godot_fgd.tres") @export var fgd_file : FuncGodotFGDFile = preload("res://addons/func_godot/fgd/func_godot_fgd.tres")
## [NetRadiantCustomShader] resources for shader file generation. ## Collection of [NetRadiantCustomShader] resources for shader file generation.
@export var netradiant_custom_shaders : Array[Resource] = [ @export var netradiant_custom_shaders : Array[Resource] = [
preload("res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_clip.tres"), preload("res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_clip.tres"),
preload("res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_skip.tres") preload("res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_skip.tres"),
preload("res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_origin.tres")
] ]
## Supported texture file types. ## Supported texture file types.
@@ -43,14 +46,31 @@ extends Resource
## Default scale of textures in NetRadiant Custom. ## Default scale of textures in NetRadiant Custom.
@export var default_scale : String = "1.0" @export var default_scale : String = "1.0"
## Clip texture path that gets applied to weapclip and nodraw shaders. ## Clip texture path that gets applied to [i]weapclip[/i] and [i]nodraw[/i] shaders.
@export var clip_texture: String = "textures/special/clip" @export var clip_texture: String = "textures/special/clip"
## Skip texture path that gets applied to caulk and nodrawnonsolid shaders. ## Skip texture path that gets applied to [i]caulk[/i] and [i]nodrawnonsolid[/i] shaders.
@export var skip_texture: String = "textures/special/skip" @export var skip_texture: String = "textures/special/skip"
## Generates completed text for a .shader file. ## Quake map type NetRadiant will filter the map for, determining whether PatchDef entries are saved.
func build_shader_text() -> String: ## [color=red][b]WARNING![/b][/color] Toggling this option may be destructive!
@export var map_type: NetRadiantCustomMapType = NetRadiantCustomMapType.QUAKE_1
## Variables to include in the exported gamepack's [code]default_build_menu.xml[/code].[br][br]
## Each [String] key defines a variable name, and its corresponding [String] value as the literal command-line string
## to execute in place of this variable identifier[br][br]
## Entries may be referred to by key in [member default_build_menu_commands] values.
@export var default_build_menu_variables: Dictionary
## Commands to include in the exported gamepack's [code]default_build_menu.xml[/code].[br][br]
## Keys, specified as a [String], define the build option name as you want it to appear in NetRadiant Custom.[br][br]
## Values represent commands taken within each option.[br][br]They may be either a [String] or an [Array] of [String] elements
## that will be used as the full command-line text issued by each command [i]within[/i] its associated build option key.[br][br]
## They may reference entries in [member default_build_menu_variables] by using brackets: [code][variable key name][/code]
@export var default_build_menu_commands: Dictionary
# Generates completed text for a .shader file.
func _build_shader_text() -> String:
var shader_text: String = "" var shader_text: String = ""
for shader_res in netradiant_custom_shaders: for shader_res in netradiant_custom_shaders:
shader_text += (shader_res as NetRadiantCustomShader).texture_path + "\n{\n" shader_text += (shader_res as NetRadiantCustomShader).texture_path + "\n{\n"
@@ -59,8 +79,8 @@ func build_shader_text() -> String:
shader_text += "}\n" shader_text += "}\n"
return shader_text return shader_text
## Generates completed text for a .gamepack file. # Generates completed text for a .gamepack file.
func build_gamepack_text() -> String: func _build_gamepack_text() -> String:
var texturetypes_str: String = "" var texturetypes_str: String = ""
for texture_type in texture_types: for texture_type in texture_types:
texturetypes_str += texture_type texturetypes_str += texture_type
@@ -78,6 +98,13 @@ func build_gamepack_text() -> String:
soundtypes_str += sound_type soundtypes_str += sound_type
if sound_type != sound_types[-1]: if sound_type != sound_types[-1]:
soundtypes_str += " " soundtypes_str += " "
var maptype_str: String
if map_type == NetRadiantCustomMapType.QUAKE_3:
maptype_str = "mapq3"
else:
maptype_str = "mapq1"
var gamepack_text: String = """<?xml version="1.0"?> var gamepack_text: String = """<?xml version="1.0"?>
<game <game
@@ -96,9 +123,9 @@ func build_gamepack_text() -> String:
texturetypes="%s" texturetypes="%s"
modeltypes="%s" modeltypes="%s"
soundtypes="%s" soundtypes="%s"
maptypes="mapq1" maptypes="%s"
shaders="quake3" shaders="quake3"
entityclass="halflife" entityclass="quake3"
entityclasstype="fgd" entityclasstype="fgd"
entities="quake" entities="quake"
brushtypes="quake" brushtypes="quake"
@@ -126,6 +153,7 @@ func build_gamepack_text() -> String:
texturetypes_str, texturetypes_str,
modeltypes_str, modeltypes_str,
soundtypes_str, soundtypes_str,
maptype_str,
default_scale, default_scale,
clip_texture, clip_texture,
skip_texture, skip_texture,
@@ -133,9 +161,10 @@ func build_gamepack_text() -> String:
skip_texture skip_texture
] ]
## Exports or updates a folder in the /games directory, with an icon, .cfg, and all accompanying FGDs. ## Exports this game's configuration with an icon, .cfg, and all accompanying FGD files in the [FuncGodotLocalConfig] [b]NetRadiant Custom Gamepacks Folder[/b].
func do_export_file() -> void: func export_file() -> void:
if (FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.MAP_EDITOR_GAME_PATH) as String).is_empty(): var game_path: String = FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.MAP_EDITOR_GAME_PATH) as String
if game_path.is_empty():
printerr("Skipping export: Map Editor Game Path not set in Project Configuration") printerr("Skipping export: Map Editor Game Path not set in Project Configuration")
return return
@@ -158,7 +187,8 @@ func do_export_file() -> void:
var gamepack_dir_paths: Array = [ var gamepack_dir_paths: Array = [
gamepacks_folder + "/" + gamepack_name + ".game", gamepacks_folder + "/" + gamepack_name + ".game",
gamepacks_folder + "/" + gamepack_name + ".game/" + base_game_path, gamepacks_folder + "/" + gamepack_name + ".game/" + base_game_path,
gamepacks_folder + "/" + gamepack_name + ".game/scripts" gamepacks_folder + "/" + gamepack_name + ".game/scripts",
game_path + "/scripts"
] ]
var err: Error var err: Error
@@ -178,24 +208,50 @@ func do_export_file() -> void:
print("Exporting NetRadiant Custom Gamepack to ", target_file_path) print("Exporting NetRadiant Custom Gamepack to ", target_file_path)
file = FileAccess.open(target_file_path, FileAccess.WRITE) file = FileAccess.open(target_file_path, FileAccess.WRITE)
if file != null: if file != null:
file.store_string(build_gamepack_text()) file.store_string(_build_gamepack_text())
file.close() file.close()
else: else:
printerr("Error: Could not modify " + target_file_path) printerr("Error: Could not modify " + target_file_path)
# .shader # .shader
# NOTE: To work properly, this should go in the game path. For now, I'm leaving the export to NRC as well, so it can easily
# be repackaged for distribution. However, I believe in the end, it shouldn't exist there.
# We'll need to make a decision for this. - Vera
var shader_text: String = _build_shader_text()
# build to <gamepack path>/scripts/
target_file_path = gamepacks_folder + "/" + gamepack_name + ".game/scripts/" + gamepack_name + ".shader" target_file_path = gamepacks_folder + "/" + gamepack_name + ".game/scripts/" + gamepack_name + ".shader"
print("Exporting NetRadiant Custom Shader to ", target_file_path) print("Exporting NetRadiant Custom shader definitions to ", target_file_path)
file = FileAccess.open(target_file_path, FileAccess.WRITE) file = FileAccess.open(target_file_path, FileAccess.WRITE)
if file != null: if file != null:
file.store_string(build_shader_text()) file.store_string(shader_text)
file.close()
else:
printerr("Error: Could not modify " + target_file_path)
# build to <game path>/scripts/
target_file_path = game_path.path_join("scripts/%s.shader" % gamepack_name)
print("Exporting NetRadiant Custom shader definitions to ", target_file_path)
file = FileAccess.open(target_file_path, FileAccess.WRITE)
if file != null:
file.store_string(shader_text)
file.close()
else:
printerr("Error: could not modify " + target_file_path)
# shaderlist.txt - see above NOTE regarding duplication
target_file_path = gamepacks_folder + "/" + gamepack_name + ".game/scripts/shaderlist.txt"
print("Exporting NetRadiant Custom shader list to ", target_file_path)
file = FileAccess.open(target_file_path, FileAccess.WRITE)
if file != null:
file.store_string(gamepack_name)
file.close() file.close()
else: else:
printerr("Error: Could not modify " + target_file_path) printerr("Error: Could not modify " + target_file_path)
# shaderlist.txt # game path/scripts/shaderlist.txt
target_file_path = gamepacks_folder + "/" + gamepack_name + ".game/scripts/shaderlist.txt" target_file_path = game_path.path_join("scripts/shaderlist.txt")
print("Exporting NetRadiant Custom Default Buld Menu to ", target_file_path) print("Exporting NetRadiant Custom shader list to ", target_file_path)
file = FileAccess.open(target_file_path, FileAccess.WRITE) file = FileAccess.open(target_file_path, FileAccess.WRITE)
if file != null: if file != null:
file.store_string(gamepack_name) file.store_string(gamepack_name)
@@ -205,15 +261,56 @@ func do_export_file() -> void:
# default_build_menu.xml # default_build_menu.xml
target_file_path = gamepacks_folder + "/" + gamepack_name + ".game/default_build_menu.xml" target_file_path = gamepacks_folder + "/" + gamepack_name + ".game/default_build_menu.xml"
print("Exporting NetRadiant Custom Default Buld Menu to ", target_file_path) print("Exporting NetRadiant Custom default build menu to ", target_file_path)
file = FileAccess.open(target_file_path, FileAccess.WRITE) file = FileAccess.open(target_file_path, FileAccess.WRITE)
if file != null: if file != null:
file.store_string("<?xml version=\"1.0\"?><project version=\"2.0\"></project>") file.store_string("<?xml version=\"1.0\"?>\n<project version=\"2.0\">\n")
file.close()
else: for key in default_build_menu_variables.keys():
printerr("Error: Could not modify " + target_file_path) if key is String:
if default_build_menu_variables[key] is String:
file.store_string('\t<var name="%s">%s</var>\n' % [key, default_build_menu_variables[key]])
else:
push_error(
"Variable key '%s' value '%s' is invalid type: %s; should be: String" % [
key, default_build_menu_variables[key],
type_string(typeof(default_build_menu_variables[key]))
])
else:
push_error(
"Variable '%s' is an invalid key type: %s; should be: String" % [
key, type_string(typeof(key))
])
for key in default_build_menu_commands.keys():
if key is String:
file.store_string('\t<build name="%s">\n' % key)
if default_build_menu_commands[key] is String:
file.store_string('\t\t<command>%s</command>\n\t</build>\n' % default_build_menu_commands[key])
elif default_build_menu_commands[key] is Array:
for command in default_build_menu_commands[key]:
if command is String:
file.store_string('\t\t<command>%s</command>\n' % command)
else:
push_error("Build option '%s' has invalid command: %s with type: %s; should be: String" % [
key, command, type_string(typeof(command))
])
file.store_string('\t</build>\n')
else:
push_error("Build option '%s' is an invalid type: %s; should be: String" % [
key, type_string(typeof(key))
])
file.store_string("</project>")
# FGD # FGD
var export_fgd : FuncGodotFGDFile = fgd_file.duplicate() var export_fgd : FuncGodotFGDFile = fgd_file.duplicate()
export_fgd.do_export_file(true, gamepacks_folder + "/" + gamepack_name + ".game/" + base_game_path) export_fgd.do_export_file(FuncGodotFGDFile.FuncGodotTargetMapEditors.NET_RADIANT_CUSTOM, gamepacks_folder + "/" + gamepack_name + ".game/" + base_game_path)
print("NetRadiant Custom Gamepack export complete\n") print("NetRadiant Custom Gamepack export complete\n")

View File

@@ -1 +1 @@
uid://ckvnkpgsci7hg uid://dfhj3me2g5j0l

View File

@@ -1,9 +1,11 @@
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg") @icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
## Resource that gets built into a shader file that applies a special effect to a specified texture in NetRadiant Custom.
class_name NetRadiantCustomShader class_name NetRadiantCustomShader
extends Resource extends Resource
## Shader resource for NetRadiant Custom configurations.
##
## Resource that gets built into a shader file that applies a special effect to a specified texture in NetRadiant Custom.
## Path to texture without extension, eg: `textures/special/clip`. ## Path to texture without extension, eg: [i]"textures/special/clip"[/i].
@export var texture_path: String @export var texture_path: String
## Array of shader properties to apply to faces using [member texture_path]. ## Array of shader properties to apply to faces using [member texture_path].

View File

@@ -1 +1 @@
uid://dtmnasjcec1cl uid://dn86acprv4e86

View File

@@ -1,17 +1,20 @@
@tool @tool
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg") @icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
## Defines a game in TrenchBroom to express a set of entity definitions and editor behaviors. class_name TrenchBroomGameConfig extends Resource
class_name TrenchBroomGameConfig ## Game configuration definition for TrenchBroom.
extends Resource ##
## Defines a game for TrenchBroom to express a set of entity definitions and editor behaviors.
##
## @tutorial(TrenchBroom Manual Game Configuration Information): https://trenchbroom.github.io/manual/latest/#game_configuration
## Button to export / update this game's configuration and FGD file in the TrenchBroom Games Path. enum GameConfigVersion {
@export var export_file: bool: Latest,
get: Version4,
return export_file Version8,
set(new_export_file): Version9
if new_export_file != export_file: }
if Engine.is_editor_hint():
do_export_file() @export_tool_button("Export GameConfig") var _export_file: Callable = export_file
## Name of the game in TrenchBroom's game list. ## Name of the game in TrenchBroom's game list.
@export var game_name : String = "FuncGodot" @export var game_name : String = "FuncGodot"
@@ -19,7 +22,8 @@ extends Resource
## Icon for TrenchBroom's game list. ## Icon for TrenchBroom's game list.
@export var icon : Texture2D = preload("res://addons/func_godot/icon32.png") @export var icon : Texture2D = preload("res://addons/func_godot/icon32.png")
## Available map formats when creating a new map in TrenchBroom. The order of elements in the array is the order TrenchBroom will list the available formats. The `initialmap` key value is optional. ## Available map formats when creating a new map in TrenchBroom. The order of elements in the array is the order TrenchBroom will list the available formats.
## The [i]"initialmap"[/i] key value is optional.
@export var map_formats: Array[Dictionary] = [ @export var map_formats: Array[Dictionary] = [
{ "format": "Valve", "initialmap": "initial_valve.map" }, { "format": "Valve", "initialmap": "initial_valve.map" },
{ "format": "Standard", "initialmap": "initial_standard.map" }, { "format": "Standard", "initialmap": "initial_standard.map" },
@@ -27,43 +31,63 @@ extends Resource
{ "format": "Quake3" } { "format": "Quake3" }
] ]
@export_category("Textures")
## Path to top level textures folder relative to the game path. Also referred to as materials in the latest versions of TrenchBroom.
@export var textures_root_folder: String = "textures"
## Textures matching these patterns will be hidden from TrenchBroom. ## Textures matching these patterns will be hidden from TrenchBroom.
@export var texture_exclusion_patterns: Array[String] = ["*_albedo", "*_ao", "*_emission", "*_height", "*_metallic", "*_normal", "*_orm", "*_roughness", "*_sss"] @export var texture_exclusion_patterns: Array[String] = ["*_albedo", "*_ao", "*_emission", "*_height", "*_metallic", "*_normal", "*_orm", "*_roughness", "*_sss"]
## FGD resource to include with this game. If using multiple FGD resources, this should be the master FGD that contains them in the `base_fgd_files` resource array. ## Palette path relative to your Game Path. Only needed for Quake WAD2 files. Half-Life WAD3 files contain the palettes within the texture information.
@export var palette_path: String = "textures/palette.lmp"
@export_category("Entities")
## [FuncGodotFGDFile] resource to include with this game. If using multiple FGD File resources,
## this should be the master FGD File that contains them in [member FuncGodotFGDFile.base_fgd_files].
@export var fgd_file : FuncGodotFGDFile = preload("res://addons/func_godot/fgd/func_godot_fgd.tres") @export var fgd_file : FuncGodotFGDFile = preload("res://addons/func_godot/fgd/func_godot_fgd.tres")
## Scale expression that modifies the default display scale of entities in TrenchBroom. See the [**TrenchBroom Documentation**](https://trenchbroom.github.io/manual/latest/#game_configuration_files_entities) for more information. ## Scale expression that modifies the default display scale of entities in TrenchBroom.
## See [url="https://trenchbroom.github.io/manual/latest/#game_configuration_files_entities"]TrenchBroom Manual Entity Configuration Information[/url] for more information.
@export var entity_scale: String = "32" @export var entity_scale: String = "32"
## Scale of textures on new brushes. ## Arrays containing the [TrenchbroomTag] resource type.
@export var default_uv_scale : Vector2 = Vector2(1, 1) @export_category("Tags")
## Arrays containing the TrenchBroomTag resource type. ## [TrenchbroomTag] resources that apply to brush entities.
@export_category("Editor Hint Tags")
## TrenchBroomTag resources that apply to brush entities.
@export var brush_tags : Array[Resource] = [] @export var brush_tags : Array[Resource] = []
## TrenchBroomTag resources that apply to brush faces. ## [TrenchbroomTag] resources that apply to brush faces.
@export var brushface_tags : Array[Resource] = [ @export var brushface_tags : Array[Resource] = [
preload("res://addons/func_godot/game_config/trenchbroom/tb_face_tag_clip.tres"), preload("res://addons/func_godot/game_config/trenchbroom/tb_face_tag_clip.tres"),
preload("res://addons/func_godot/game_config/trenchbroom/tb_face_tag_skip.tres") preload("res://addons/func_godot/game_config/trenchbroom/tb_face_tag_skip.tres"),
preload("res://addons/func_godot/game_config/trenchbroom/tb_face_tag_origin.tres")
] ]
## Matches tag key enum to the String name used in .cfg @export_category("Face Attributes")
static func get_match_key(tag_match_type: int) -> String:
## Default scale of textures on new brushes and when UV scale is reset.
@export var default_uv_scale : Vector2 = Vector2(1, 1)
@export_category("Compatibility")
## Game configuration format compatible with the version of TrenchBroom being used.
@export var game_config_version: GameConfigVersion = GameConfigVersion.Latest
# Matches tag key enum to the [String] name used in .cfg
static func _get_match_key(tag_match_type: int) -> String:
match tag_match_type: match tag_match_type:
TrenchBroomTag.TagMatchType.TEXTURE: TrenchBroomTag.TagMatchType.TEXTURE:
return "texture" return "material"
TrenchBroomTag.TagMatchType.CLASSNAME: TrenchBroomTag.TagMatchType.CLASSNAME:
return "classname" return "classname"
_: _:
push_error("Tag match type %s is not valid" % [tag_match_type]) push_error("Tag match type %s is not valid" % [tag_match_type])
return "ERROR" return "ERROR"
## Generates completed text for a .cfg file. # Generates completed text for a .cfg file.
func build_class_text() -> String: func _build_class_text() -> String:
var map_formats_str : String = "" var map_formats_str : String = ""
for map_format in map_formats: for map_format in map_formats:
map_formats_str += "{ \"format\": \"" + map_format.format + "\"" map_formats_str += "{ \"format\": \"" + map_format.format + "\""
@@ -82,61 +106,47 @@ func build_class_text() -> String:
var fgd_filename_str : String = "\"" + fgd_file.fgd_name + ".fgd\"" var fgd_filename_str : String = "\"" + fgd_file.fgd_name + ".fgd\""
var brush_tags_str = parse_tags(brush_tags) var brush_tags_str = _parse_tags(brush_tags)
var brushface_tags_str = parse_tags(brushface_tags) var brushface_tags_str = _parse_tags(brushface_tags)
var uv_scale_str = parse_default_uv_scale(default_uv_scale) var uv_scale_str = _parse_default_uv_scale(default_uv_scale)
var config_text : String = """{ var config_text : String = ""
"version": 8, match game_config_version:
"name": "%s", GameConfigVersion.Latest, GameConfigVersion.Version8, GameConfigVersion.Version9:
"icon": "icon.png", config_text = _get_game_config_v9v8_text() % [
"fileformats": [ game_name,
%s map_formats_str,
], textures_root_folder,
"filesystem": { texture_exclusion_patterns_str,
"searchpath": ".", palette_path,
"packageformat": { "extension": ".zip", "format": "zip" } fgd_filename_str,
}, entity_scale,
"textures": { brush_tags_str,
"root": "textures", brushface_tags_str,
"extensions": [".bmp", ".exr", ".hdr", ".jpeg", ".jpg", ".png", ".tga", ".webp"], uv_scale_str
"excludes": [ %s ] ]
},
"entities": {
"definitions": [ %s ],
"defaultcolor": "0.6 0.6 0.6 1.0",
"scale": %s
},
"tags": {
"brush": [
%s
],
"brushface": [
%s
]
},
"faceattribs": {
"defaults": {
%s
},
"contentflags": [],
"surfaceflags": []
}
}
"""
return config_text % [
game_name,
map_formats_str,
texture_exclusion_patterns_str,
fgd_filename_str,
entity_scale,
brush_tags_str,
brushface_tags_str,
uv_scale_str
]
## Converts brush, FuncGodotFace, and attribute tags into a .cfg-usable String. GameConfigVersion.Version4:
func parse_tags(tags: Array) -> String: config_text = _get_game_config_v4_text() % [
game_name,
map_formats_str,
textures_root_folder,
texture_exclusion_patterns_str,
palette_path,
fgd_filename_str,
entity_scale,
brush_tags_str,
brushface_tags_str,
uv_scale_str
]
_:
push_error("Unsupported Game Config Version!")
return config_text
# Converts brush, face, and attribute tags into a .cfg-usable String.
func _parse_tags(tags: Array) -> String:
var tags_str := "" var tags_str := ""
for brush_tag in tags: for brush_tag in tags:
if brush_tag.tag_match_type >= TrenchBroomTag.TagMatchType.size(): if brush_tag.tag_match_type >= TrenchBroomTag.TagMatchType.size():
@@ -149,19 +159,21 @@ func parse_tags(tags: Array) -> String:
if brush_tag_attrib != brush_tag.tag_attributes[-1]: if brush_tag_attrib != brush_tag.tag_attributes[-1]:
attribs_str += ", " attribs_str += ", "
tags_str += "\t\t\t\t\"attribs\": [ %s ],\n" % attribs_str tags_str += "\t\t\t\t\"attribs\": [ %s ],\n" % attribs_str
tags_str += "\t\t\t\t\"match\": \"%s\",\n" % get_match_key(brush_tag.tag_match_type) tags_str += "\t\t\t\t\"match\": \"%s\",\n" % _get_match_key(brush_tag.tag_match_type)
tags_str += "\t\t\t\t\"pattern\": \"%s\"" % brush_tag.tag_pattern tags_str += "\t\t\t\t\"pattern\": \"%s\"" % brush_tag.tag_pattern
if brush_tag.texture_name != "": if brush_tag.texture_name != "":
tags_str += ",\n" tags_str += ",\n"
tags_str += "\t\t\t\t\"texture\": \"%s\"" % brush_tag.texture_name tags_str += "\t\t\t\t\"material\": \"%s\"" % brush_tag.texture_name
tags_str += "\n" tags_str += "\n"
tags_str += "\t\t\t}" tags_str += "\t\t\t}"
if brush_tag != tags[-1]: if brush_tag != tags[-1]:
tags_str += "," tags_str += ","
if game_config_version > GameConfigVersion.Latest and game_config_version < GameConfigVersion.Version9:
tags_str = tags_str.replace("material", "texture")
return tags_str return tags_str
## Converts array of flags to .cfg String. # Converts array of flags to .cfg String.
func parse_flags(flags: Array) -> String: func _parse_flags(flags: Array) -> String:
var flags_str := "" var flags_str := ""
for attrib_flag in flags: for attrib_flag in flags:
flags_str += "{\n" flags_str += "{\n"
@@ -172,23 +184,23 @@ func parse_flags(flags: Array) -> String:
flags_str += "," flags_str += ","
return flags_str return flags_str
## Converts default uv scale vector to .cfg String. # Converts default uv scale vector to .cfg String.
func parse_default_uv_scale(texture_scale : Vector2) -> String: func _parse_default_uv_scale(texture_scale : Vector2) -> String:
var entry_str = "\"scale\": [{x}, {y}]" var entry_str = "\"scale\": [{x}, {y}]"
return entry_str.format({ return entry_str.format({
"x": texture_scale.x, "x": texture_scale.x,
"y": texture_scale.y "y": texture_scale.y
}) })
## Exports or updates a folder in the /games directory, with an icon, .cfg, and all accompanying FGDs. ## Exports this game's configuration with an icon, .cfg, and all accompanying FGD files in the [FuncGodotLocalConfig] [b]Trenchbroom Game Config Folder[/b].
func do_export_file() -> void: func export_file() -> void:
var config_folder: String = FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.TRENCHBROOM_GAME_CONFIG_FOLDER) as String var config_folder: String = FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.TRENCHBROOM_GAME_CONFIG_FOLDER) as String
if config_folder.is_empty(): if config_folder.is_empty():
printerr("Skipping export: No TrenchBroom Game folder") printerr("Skipping export: No TrenchBroom Game folder")
return return
# Make sure FGD file is set # Make sure FGD file is set
if !fgd_file: if not fgd_file:
printerr("Skipping export: No FGD file") printerr("Skipping export: No FGD file")
return return
@@ -212,10 +224,106 @@ func do_export_file() -> void:
var target_file_path: String = config_folder + "/GameConfig.cfg" var target_file_path: String = config_folder + "/GameConfig.cfg"
print("Exporting TrenchBroom Game Config to ", target_file_path) print("Exporting TrenchBroom Game Config to ", target_file_path)
var file = FileAccess.open(target_file_path, FileAccess.WRITE) var file = FileAccess.open(target_file_path, FileAccess.WRITE)
file.store_string(build_class_text()) file.store_string(_build_class_text())
file.close() file.close()
# FGD # FGD
var export_fgd : FuncGodotFGDFile = fgd_file.duplicate() var export_fgd : FuncGodotFGDFile = fgd_file.duplicate()
export_fgd.do_export_file(true, config_folder) export_fgd.do_export_file(FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM, config_folder)
print("TrenchBroom Game Config export complete\n") print("TrenchBroom Game Config export complete\n")
#region GameConfigDeclarations
func _get_game_config_v4_text() -> String:
return """\
{
"version": 4,
"name": "%s",
"icon": "icon.png",
"fileformats": [
%s
],
"filesystem": {
"searchpath": ".",
"packageformat": { "extension": ".zip", "format": "zip" }
},
"textures": {
"package": { "type": "directory", "root": "%s" },
"format": { "extensions": ["jpg", "jpeg", "tga", "png", "D", "C"], "format": "image" },
"excludes": [ %s ],
"palette": "%s",
"attribute": ["_tb_textures", "wad"]
},
"entities": {
"definitions": [ %s ],
"defaultcolor": "0.6 0.6 0.6 1.0",
"modelformats": [ "bsp, mdl, md2" ],
"scale": %s
},
"tags": {
"brush": [
%s
],
"brushface": [
%s
]
},
"faceattribs": {
"defaults": {
%s
},
"contentflags": [],
"surfaceflags": []
}
}
"""
func _get_game_config_v9v8_text() -> String:
var config_text: String = """\
{
"version": 9,
"name": "%s",
"icon": "icon.png",
"fileformats": [
%s
],
"filesystem": {
"searchpath": ".",
"packageformat": { "extension": ".zip", "format": "zip" }
},
"materials": {
"root": "%s",
"extensions": [".bmp", ".exr", ".hdr", ".jpeg", ".jpg", ".png", ".tga", ".webp", ".D", ".C"],
"excludes": [ %s ],
"palette": "%s",
"attribute": "wad"
},
"entities": {
"definitions": [ %s ],
"defaultcolor": "0.6 0.6 0.6 1.0",
"scale": %s
},
"tags": {
"brush": [
%s
],
"brushface": [
%s
]
},
"faceattribs": {
"defaults": {
%s
},
"contentflags": [],
"surfaceflags": []
}
}
"""
if game_config_version == GameConfigVersion.Version8:
config_text = config_text.replace(": 9,", ": 8,")
config_text = config_text.replace("material", "texture")
return config_text
#endregion

View File

@@ -1 +1 @@
uid://c51cy33iaei4s uid://cx44c4vnq8bt5

View File

@@ -1,8 +1,12 @@
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg") @icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
## Pattern matching tags to enable a number of features in TrenchBroom, including display appearance and menu filtering options. This resource gets added to the [TrenchBroomGameConfig] resource. Does not affect appearance or functionality in Godot. class_name TrenchBroomTag extends Resource
## See the TrenchBroom Documentation on [**Tags under the Game Configuration section**](https://trenchbroom.github.io/manual/latest/#game_configuration_files) and [**Special Bruch FuncGodotFace Types**](https://trenchbroom.github.io/manual/latest/#special_brush_face_types) for more information. ## Pattern matching tag added to [TrenchbroomGameConfig] for appearance and menu filtering purposes.
class_name TrenchBroomTag ##
extends Resource ## Pattern matching tags to enable a number of features in TrenchBroom, including display appearance and menu filtering options.
## This resource gets added to the [TrenchBroomGameConfig] resource. Does not affect appearance or functionality in Godot.
##
## @tutorial(TrenchBroom Manual Game Configuration): https://trenchbroom.github.io/manual/latest/#game_configuration_files
## @tutorial(TrenchBroom Manual Special Brush Face Types): https://trenchbroom.github.io/manual/latest/#special_brush_face_types
enum TagMatchType { enum TagMatchType {
TEXTURE, ## Tag applies to any brush face with a texture matching the texture name. TEXTURE, ## Tag applies to any brush face with a texture matching the texture name.
@@ -19,7 +23,7 @@ enum TagMatchType {
@export var tag_match_type: TagMatchType @export var tag_match_type: TagMatchType
## A string that filters which flag, param, or classname to use. [code]*[/code] can be used as a wildcard to include multiple options. ## A string that filters which flag, param, or classname to use. [code]*[/code] can be used as a wildcard to include multiple options.
## [b]Example:[/b] [code]trigger_*[/code] with [constant TagMatchType] [i]Classname[/i] will apply this tag to all brush entities with the [code]trigger_[/code] prefix. ## [b]Example:[/b] [code]trigger*[/code] with [constant TagMatchType] [i]Classname[/i] will apply this tag to all brush entities with the [code]trigger[/code] prefix.
@export var tag_pattern: String @export var tag_pattern: String
## A string that filters which textures recieve these attributes. Only used with a [constant TagMatchType] of [i]Texture[/i]. ## A string that filters which textures recieve these attributes. Only used with a [constant TagMatchType] of [i]Texture[/i].

View File

@@ -1 +1 @@
uid://5eacgso7tkh6 uid://b66qdknwqpfup

View File

@@ -1,23 +1,32 @@
@tool @tool
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg") @icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
class_name FuncGodotLocalConfig extends Resource
## Local machine project wide settings. [color=red]WARNING![/color] Do not create your own! Use the resource in [i]addons/func_godot[/i].
##
## Local machine project wide settings. Can define global defaults for some FuncGodot properties. ## Local machine project wide settings. Can define global defaults for some FuncGodot properties.
## DO NOT CREATE A NEW RESOURCE! This resource works by saving a configuration file to your game's *user://* folder and pulling the properties from that config file rather than this resource. ## [color=red][b]DO NOT CREATE A NEW RESOURCE![/b][/color] This resource works by saving a configuration file to your game's [b][i]user://[/i][/b] folder
## Use the premade `addons/func_godot/func_godot_local_config.tres` instead. ## and pulling the properties from that config file rather than this resource. Use the premade [b][i]addons/func_godot/func_godot_local_config.tres[/i][/b] instead.
class_name FuncGodotLocalConfig ## [br][br]
extends Resource ## [b]Fgd Output Folder :[/b] Global directory path that [FuncGodotFGDFile] saves to when exported. Overridden when exported from a game configuration resource like [TrenchBroomGameConfig].[br][br]
## [b]Trenchbroom Game Config Folder :[/b] Global directory path where your TrenchBroom game configuration should be saved to. Consult the [url="https://trenchbroom.github.io/manual/latest/#game_configuration_files"]TrenchBroom Manual's Game Configuration documentation[/url] for more information.[br][br]
## [b]Netradiant Custom Gamepacks Folder :[/b] Global directory path where your NetRadiant Custom gamepacks are saved. On Windows this is the [i]gamepacks[/i] folder in your NetRadiant Custom installation.[br][br]
## [b]Map Editor Game Path :[/b] Global directory path to your mapping folder where all of your mapping assets exist. This is usually either your project folder or a subfolder within it.[br][br]
## [b]Game Path Models Folder :[/b] Relative directory path from your Map Editor Game Path to a subfolder containing any display models you might use for your map editor. Currently only used by [FuncGodotFGDModelPointClass].[br][br]
## [b]Default Inverse Scale Factor :[/b] Scale factor that affects how [FuncGodotFGDModelPointClass] entities scale their map editor display models. Not used with TrenchBroom, use [member TrenchBroomGameConfig.entity_scale] expression instead.[br][br]
enum PROPERTY { enum PROPERTY {
FGD_OUTPUT_FOLDER, FGD_OUTPUT_FOLDER,
TRENCHBROOM_GAME_CONFIG_FOLDER, TRENCHBROOM_GAME_CONFIG_FOLDER,
NETRADIANT_CUSTOM_GAMEPACKS_FOLDER, NETRADIANT_CUSTOM_GAMEPACKS_FOLDER,
MAP_EDITOR_GAME_PATH, MAP_EDITOR_GAME_PATH,
GAME_PATH_MODELS_FOLDER, #GAME_PATH_MODELS_FOLDER,
DEFAULT_INVERSE_SCALE #DEFAULT_INVERSE_SCALE
} }
@export var export_func_godot_settings: bool: set = _save_settings @export_tool_button("Export func_godot settings", "Save") var _save_settings = export_func_godot_settings
@export_tool_button("Reload func_godot settings", "Reload") var _load_settings = reload_func_godot_settings
const CONFIG_PROPERTIES: Array[Dictionary] = [ const _CONFIG_PROPERTIES: Array[Dictionary] = [
{ {
"name": "fgd_output_folder", "name": "fgd_output_folder",
"usage": PROPERTY_USAGE_EDITOR, "usage": PROPERTY_USAGE_EDITOR,
@@ -46,44 +55,44 @@ const CONFIG_PROPERTIES: Array[Dictionary] = [
"hint": PROPERTY_HINT_GLOBAL_DIR, "hint": PROPERTY_HINT_GLOBAL_DIR,
"func_godot_type": PROPERTY.MAP_EDITOR_GAME_PATH "func_godot_type": PROPERTY.MAP_EDITOR_GAME_PATH
}, },
{ #{
"name": "game_path_models_folder", #"name": "game_path_models_folder",
"usage": PROPERTY_USAGE_EDITOR, #"usage": PROPERTY_USAGE_EDITOR,
"type": TYPE_STRING, #"type": TYPE_STRING,
"func_godot_type": PROPERTY.GAME_PATH_MODELS_FOLDER #"func_godot_type": PROPERTY.GAME_PATH_MODELS_FOLDER
}, #},
{ #{
"name": "default_inverse_scale_factor", #"name": "default_inverse_scale_factor",
"usage": PROPERTY_USAGE_EDITOR, #"usage": PROPERTY_USAGE_EDITOR,
"type": TYPE_FLOAT, #"type": TYPE_FLOAT,
"func_godot_type": PROPERTY.DEFAULT_INVERSE_SCALE #"func_godot_type": PROPERTY.DEFAULT_INVERSE_SCALE
} #}
] ]
var settings_dict: Dictionary var _settings_dict: Dictionary
var loaded := false var _loaded := false
## Retrieve a setting from the local configuration.
static func get_setting(name: PROPERTY) -> Variant: static func get_setting(name: PROPERTY) -> Variant:
var settings = load("res://addons/func_godot/func_godot_local_config.tres") var settings: FuncGodotLocalConfig = load("res://addons/func_godot/func_godot_local_config.tres")
if not settings.loaded: settings.reload_func_godot_settings()
settings._load_settings() return settings._settings_dict.get(PROPERTY.keys()[name], '') as Variant
return settings.settings_dict.get(PROPERTY.keys()[name], '') as Variant
func _get_property_list() -> Array: func _get_property_list() -> Array:
return CONFIG_PROPERTIES.duplicate() return _CONFIG_PROPERTIES.duplicate()
func _get(property: StringName) -> Variant: func _get(property: StringName) -> Variant:
var config = _get_config_property(property) var config = _get_config_property(property)
if config == null and not config is Dictionary: if config == null and not config is Dictionary:
return null return null
_try_loading() _try_loading()
return settings_dict.get(PROPERTY.keys()[config['func_godot_type']], _get_default_value(config['type'])) return _settings_dict.get(PROPERTY.keys()[config['func_godot_type']], _get_default_value(config['type']))
func _set(property: StringName, value: Variant) -> bool: func _set(property: StringName, value: Variant) -> bool:
var config = _get_config_property(property) var config = _get_config_property(property)
if config == null and not config is Dictionary: if config == null and not config is Dictionary:
return false return false
settings_dict[PROPERTY.keys()[config['func_godot_type']]] = value _settings_dict[PROPERTY.keys()[config['func_godot_type']]] = value
return true return true
func _get_default_value(type) -> Variant: func _get_default_value(type) -> Variant:
@@ -100,34 +109,39 @@ func _get_default_value(type) -> Variant:
return null return null
func _get_config_property(name: StringName) -> Variant: func _get_config_property(name: StringName) -> Variant:
for config in CONFIG_PROPERTIES: for config in _CONFIG_PROPERTIES:
if config['name'] == name: if config['name'] == name:
return config return config
return null return null
func _load_settings() -> void: ## Reload this system's configuration settings into the Local Config resource.
loaded = true func reload_func_godot_settings() -> void:
_loaded = true
var path = _get_path() var path = _get_path()
if not FileAccess.file_exists(path): return if not FileAccess.file_exists(path):
return
var settings = FileAccess.get_file_as_string(path) var settings = FileAccess.get_file_as_string(path)
settings_dict = {} _settings_dict = {}
if not settings or settings.is_empty(): return if not settings or settings.is_empty():
return
settings = JSON.parse_string(settings) settings = JSON.parse_string(settings)
for key in settings.keys(): for key in settings.keys():
settings_dict[key] = settings[key] _settings_dict[key] = settings[key]
notify_property_list_changed() notify_property_list_changed()
func _try_loading() -> void: func _try_loading() -> void:
if not loaded: _load_settings() if not _loaded:
reload_func_godot_settings()
func _save_settings(_s = null) -> void: ## Export the current resource settings to a configuration file in this game's [i]user://[/i] folder.
if settings_dict.size() == 0: func export_func_godot_settings() -> void:
if _settings_dict.size() == 0:
return return
var path = _get_path() var path = _get_path()
var file = FileAccess.open(path, FileAccess.WRITE) var file = FileAccess.open(path, FileAccess.WRITE)
var json = JSON.stringify(settings_dict) var json = JSON.stringify(_settings_dict)
file.store_line(json) file.store_line(json)
loaded = false _loaded = false
print("Saved settings to ", path) print("Saved settings to ", path)
func _get_path() -> String: func _get_path() -> String:

View File

@@ -1 +1 @@
uid://bjtqjywscfgdy uid://xsjnhahhyein

View File

@@ -1,184 +0,0 @@
class_name FuncGodotTextureLoader
enum PBRSuffix {
ALBEDO,
NORMAL,
METALLIC,
ROUGHNESS,
EMISSION,
AO,
HEIGHT
}
# Suffix string / Godot enum / StandardMaterial3D property
const PBR_SUFFIX_NAMES: Dictionary = {
PBRSuffix.ALBEDO: 'albedo',
PBRSuffix.NORMAL: 'normal',
PBRSuffix.METALLIC: 'metallic',
PBRSuffix.ROUGHNESS: 'roughness',
PBRSuffix.EMISSION: 'emission',
PBRSuffix.AO: 'ao',
PBRSuffix.HEIGHT: 'height',
}
const PBR_SUFFIX_PATTERNS: Dictionary = {
PBRSuffix.ALBEDO: '%s_albedo.%s',
PBRSuffix.NORMAL: '%s_normal.%s',
PBRSuffix.METALLIC: '%s_metallic.%s',
PBRSuffix.ROUGHNESS: '%s_roughness.%s',
PBRSuffix.EMISSION: '%s_emission.%s',
PBRSuffix.AO: '%s_ao.%s',
PBRSuffix.HEIGHT: '%s_height.%s'
}
var PBR_SUFFIX_TEXTURES: Dictionary = {
PBRSuffix.ALBEDO: StandardMaterial3D.TEXTURE_ALBEDO,
PBRSuffix.NORMAL: StandardMaterial3D.TEXTURE_NORMAL,
PBRSuffix.METALLIC: StandardMaterial3D.TEXTURE_METALLIC,
PBRSuffix.ROUGHNESS: StandardMaterial3D.TEXTURE_ROUGHNESS,
PBRSuffix.EMISSION: StandardMaterial3D.TEXTURE_EMISSION,
PBRSuffix.AO: StandardMaterial3D.TEXTURE_AMBIENT_OCCLUSION,
PBRSuffix.HEIGHT: StandardMaterial3D.TEXTURE_HEIGHTMAP
}
const PBR_SUFFIX_PROPERTIES: Dictionary = {
PBRSuffix.NORMAL: 'normal_enabled',
PBRSuffix.EMISSION: 'emission_enabled',
PBRSuffix.AO: 'ao_enabled',
PBRSuffix.HEIGHT: 'heightmap_enabled',
}
var map_settings: FuncGodotMapSettings = FuncGodotMapSettings.new()
var texture_wad_resources: Array = []
# Overrides
func _init(new_map_settings: FuncGodotMapSettings) -> void:
map_settings = new_map_settings
load_texture_wad_resources()
# Business Logic
func load_texture_wad_resources() -> void:
texture_wad_resources.clear()
for texture_wad in map_settings.texture_wads:
if texture_wad and not texture_wad in texture_wad_resources:
texture_wad_resources.append(texture_wad)
func load_textures(texture_list: Array) -> Dictionary:
var texture_dict: Dictionary = {}
for texture_name in texture_list:
texture_dict[texture_name] = load_texture(texture_name)
return texture_dict
func load_texture(texture_name: String) -> Texture2D:
# Load albedo texture if it exists
for texture_extension in map_settings.texture_file_extensions:
var texture_path: String = "%s/%s.%s" % [map_settings.base_texture_dir, texture_name, texture_extension]
if ResourceLoader.exists(texture_path, "Texture2D"):
return load(texture_path) as Texture2D
var texture_name_lower: String = texture_name.to_lower()
for texture_wad in texture_wad_resources:
if texture_name_lower in texture_wad.textures:
return texture_wad.textures[texture_name_lower]
return load("res://addons/func_godot/textures/default_texture.png") as Texture2D
func create_materials(texture_list: Array) -> Dictionary:
var texture_materials: Dictionary = {}
#prints("TEXLI", texture_list)
for texture in texture_list:
texture_materials[texture] = create_material(texture)
return texture_materials
func create_material(texture_name: String) -> Material:
# Autoload material if it exists
var material_dict: Dictionary = {}
var material_path: String = "%s/%s.%s" % [map_settings.base_texture_dir, texture_name, map_settings.material_file_extension]
if not material_path in material_dict and FileAccess.file_exists(material_path):
var loaded_material: Material = load(material_path)
if loaded_material:
material_dict[material_path] = loaded_material
# If material already exists, use it
if material_path in material_dict:
return material_dict[material_path]
var material: Material = null
if map_settings.default_material:
material = map_settings.default_material.duplicate()
else:
material = StandardMaterial3D.new()
var texture: Texture2D = load_texture(texture_name)
if not texture:
return material
if material is BaseMaterial3D:
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED if map_settings.unshaded else BaseMaterial3D.SHADING_MODE_PER_PIXEL
if material is StandardMaterial3D:
material.set_texture(StandardMaterial3D.TEXTURE_ALBEDO, texture)
elif material is ShaderMaterial && map_settings.default_material_albedo_uniform != "":
material.set_shader_parameter(map_settings.default_material_albedo_uniform, texture)
var pbr_textures : Dictionary = get_pbr_textures(texture_name)
for pbr_suffix in PBRSuffix.values():
var suffix: int = pbr_suffix
var tex: Texture2D = pbr_textures[suffix]
if tex:
if material is ShaderMaterial:
material = StandardMaterial3D.new()
material.set_texture(StandardMaterial3D.TEXTURE_ALBEDO, texture)
var enable_prop: String = PBR_SUFFIX_PROPERTIES[suffix] if suffix in PBR_SUFFIX_PROPERTIES else ""
if(enable_prop != ""):
material.set(enable_prop, true)
material.set_texture(PBR_SUFFIX_TEXTURES[suffix], tex)
material_dict[material_path] = material
if (map_settings.save_generated_materials and material
and texture_name != map_settings.clip_texture
and texture_name != map_settings.skip_texture
and texture.resource_path != "res://addons/func_godot/textures/default_texture.png"):
ResourceSaver.save(material, material_path)
return material
# PBR texture fetching
func get_pbr_suffix_pattern(suffix: int) -> String:
if not suffix in PBR_SUFFIX_NAMES:
return ''
var pattern_setting: String = "%s_map_pattern" % [PBR_SUFFIX_NAMES[suffix]]
if pattern_setting in map_settings:
return map_settings.get(pattern_setting)
return PBR_SUFFIX_PATTERNS[suffix]
func get_pbr_texture(texture: String, suffix: PBRSuffix) -> Texture2D:
var texture_comps: PackedStringArray = texture.split('/')
if texture_comps.size() == 0:
return null
for texture_extension in map_settings.texture_file_extensions:
var path: String = "%s/%s/%s" % [
map_settings.base_texture_dir,
'/'.join(texture_comps),
get_pbr_suffix_pattern(suffix) % [
texture_comps[-1],
texture_extension
]
]
if(FileAccess.file_exists(path)):
return load(path) as Texture2D
return null
func get_pbr_textures(texture_name: String) -> Dictionary:
var pbr_textures: Dictionary = {}
for pbr_suffix in PBRSuffix.values():
pbr_textures[pbr_suffix] = get_pbr_texture(texture_name, pbr_suffix)
return pbr_textures

View File

@@ -1 +0,0 @@
uid://pfjqfqn3imye

View File

@@ -1,40 +1,408 @@
## General-purpose utility functions namespaced to FuncGodot for compatibility
class_name FuncGodotUtil class_name FuncGodotUtil
## Static class with a number of reuseable utility methods that can be called at Editor or Run Time.
## Print debug messages. True to print, false to ignore const _VERTEX_EPSILON: float = 0.008
const DEBUG : bool = true
## Const-predicated print function to avoid excess log spam. Print msg if [constant DEBUG] is `true`. const _VEC3_UP_ID := Vector3(0.0, 0.0, 1.0)
static func debug_print(msg) -> void: const _VEC3_RIGHT_ID := Vector3(0.0, 1.0, 0.0)
if(DEBUG): const _VEC3_FORWARD_ID := Vector3(1.0, 0.0, 0.0)
print(msg)
## Return a string that corresponds to the current OS's newline control character(s) ## Connected by the [FuncGodotMap] node to the build process' sub-components if the
## [member FuncGodotMap.build_flags]'s SHOW_PROFILE_INFO flag is set.
static func print_profile_info(message: String, signature: String) -> void:
prints(signature, message)
## Return a [String] that corresponds to the current [OS]'s newline control characters.
static func newline() -> String: static func newline() -> String:
if OS.get_name() == "Windows": if OS.get_name() == "Windows":
return "\r\n" return "\r\n"
else: else:
return "\n" return "\n"
## Create a dictionary suitable for creating a category with name when overriding [method Object._get_property_list] #region MATH
static func category_dict(name: String) -> Dictionary:
return property_dict(name, TYPE_STRING, -1, "", PROPERTY_USAGE_CATEGORY)
## Creates a property with name and type from [enum @GlobalScope.Variant.Type]. static func op_vec3_sum(lhs: Vector3, rhs: Vector3) -> Vector3:
## Optionally, provide hint from [enum @GlobalScope.PropertyHint] and corresponding hint_string, and usage from [enum @GlobalScope.PropertyUsageFlags]. return lhs + rhs
static func property_dict(name: String, type: int, hint: int = -1, hint_string: String = "", usage: int = -1) -> Dictionary:
var dict := {
'name': name,
'type': type
}
if hint != -1: static func op_vec3_avg(array: Array[Vector3]) -> Vector3:
dict['hint'] = hint if array.is_empty():
push_error("Cannot average empty Vector3 array!")
return Vector3()
return array.reduce(op_vec3_sum, Vector3()) / array.size()
if hint_string != "": ## Conversion from id tech coordinate system to Godot, from a top-down perspective.
dict['hint_string'] = hint_string static func id_to_opengl(vec: Vector3) -> Vector3:
return Vector3(vec.y, vec.z, vec.x)
if usage != -1: ## Check if a point is inside a convex hull defined by a series of planes by an epsilon constant.
dict['usage'] = usage static func is_point_in_convex_hull(planes: Array[Plane], vertex: Vector3) -> bool:
for plane in planes:
var distance: float = plane.normal.dot(vertex) - plane.d
if distance > _VERTEX_EPSILON:
return false
return true
return dict #endregion
#region PATCH DEF
## Returns the control points that defines a cubic curve for a equivalent input quadratic curve.
static func elevate_quadratic(p0: Vector3, p1: Vector3, p2: Vector3) -> Array[Vector3]:
return [p0, p0 + (2.0/3.0) * (p1 - p0), p2 + (2.0/3.0) * (p1 - p2), p2 ]
## Create a Curve3D and bake points.
static func create_curve(start: Vector3, control: Vector3, end: Vector3, bake_interval: float = 0.05) -> Curve3D:
var ret := Curve3D.new()
ret.bake_interval = bake_interval
update_ref_curve(ret, start, control, end, bake_interval)
return ret
## Update a Curve3D given quadratic inputs.
static func update_ref_curve(curve: Curve3D, p0: Vector3, p1: Vector3, p2: Vector3, bake_interval: float = 0.05) -> void:
curve.clear_points()
curve.bake_interval = bake_interval
curve.add_point(p0, (p1 - p0) * (2.0 / 3.0))
curve.add_point(p1, (p1 - p0) * (1.0 / 3.0), (p2 - p1) * (1.0 / 3.0))
curve.add_point(p2, (p2 - p1 * (2.0 / 3.0)))
#endregion
#region TEXTURES
## Fallback texture if the one defined in the [QuakeMapFile] cannot be found.
const default_texture_path: String = "res://addons/func_godot/textures/default_texture.png"
const _pbr_textures: PackedInt32Array = [
StandardMaterial3D.TEXTURE_ALBEDO,
StandardMaterial3D.TEXTURE_NORMAL,
StandardMaterial3D.TEXTURE_METALLIC,
StandardMaterial3D.TEXTURE_ROUGHNESS,
StandardMaterial3D.TEXTURE_EMISSION,
StandardMaterial3D.TEXTURE_AMBIENT_OCCLUSION,
StandardMaterial3D.TEXTURE_HEIGHTMAP,
ORMMaterial3D.TEXTURE_ORM
]
## Searches for a Texture2D within the base texture directory or the WAD files added to map settings.
## If not found, a default texture is returned.
static func load_texture(texture_name: String, wad_resources: Array[QuakeWadFile], map_settings: FuncGodotMapSettings) -> Texture2D:
for texture_file_extension in map_settings.texture_file_extensions:
var texture_path: String = map_settings.base_texture_dir.path_join(texture_name + "." + texture_file_extension)
if ResourceLoader.exists(texture_path):
return load(texture_path)
var texture_name_lower: String = texture_name.to_lower()
for wad in wad_resources:
if texture_name_lower in wad.textures:
return wad.textures[texture_name_lower]
return load(default_texture_path)
## Filters faces textured with Skip during the geometry generation step of the build process.
static func is_skip(texture: String, map_settings: FuncGodotMapSettings) -> bool:
if map_settings:
return texture.to_lower() == map_settings.skip_texture
return false
## Filters faces textured with Clip during the geometry generation step of the build process.
static func is_clip(texture: String, map_settings: FuncGodotMapSettings) -> bool:
if map_settings:
return texture.to_lower() == map_settings.clip_texture
return false
## Filters faces textured with Origin during the parsing and geometry generation steps of the build process.
static func is_origin(texture: String, map_settings: FuncGodotMapSettings) -> bool:
if map_settings:
return texture.to_lower() == map_settings.origin_texture
return false
## Filters faces textured with any of the tool textures during the geometry generation step of the build process.
static func filter_face(texture: String, map_settings: FuncGodotMapSettings) -> bool:
if map_settings:
texture = texture.to_lower()
if (texture == map_settings.skip_texture
or texture == map_settings.clip_texture
or texture == map_settings.origin_texture
):
return true
return false
## Adds PBR textures to an existing [BaseMaterial3D].
static func build_base_material(map_settings: FuncGodotMapSettings, material: BaseMaterial3D, texture: String) -> void:
var path: String = map_settings.base_texture_dir.path_join(texture)
# Check if there is a subfolder with our PBR textures
if DirAccess.open(path.path_join(path)):
path = path.path_join(path)
var pbr_suffixes: PackedStringArray = [
map_settings.albedo_map_pattern,
map_settings.normal_map_pattern,
map_settings.metallic_map_pattern,
map_settings.roughness_map_pattern,
map_settings.emission_map_pattern,
map_settings.ao_map_pattern,
map_settings.height_map_pattern,
map_settings.orm_map_pattern,
]
for texture_file_extension in map_settings.texture_file_extensions:
for i in pbr_suffixes.size():
if not pbr_suffixes[i].is_empty():
var pbr: String = pbr_suffixes[i] % [path, texture_file_extension]
if ResourceLoader.exists(pbr):
material.set_texture(_pbr_textures[i], load(pbr))
## Builds both materials and sizes dictionaries for use in the geometry generation step of the build process.
## Both dictionaries use texture names as keys. The materials dictionary uses [Material] as values,
## while the sizes dictionary saves the albedo texture sizes to aid in UV mapping.
static func build_texture_map(entity_data: Array[FuncGodotData.EntityData], map_settings: FuncGodotMapSettings) -> Array[Dictionary]:
var texture_materials: Dictionary[String, Material] = {}
var texture_sizes: Dictionary[String, Vector2] = {}
# Prepare WAD files
var wad_resources: Array[QuakeWadFile] = []
for wad in map_settings.texture_wads:
if wad and not wad in wad_resources:
wad_resources.append(wad)
for entity in entity_data:
if not entity.is_visual():
continue
for brush in entity.brushes:
for face in brush.faces:
var texture_name: String = face.texture
if filter_face(texture_name, map_settings):
continue
if texture_materials.has(texture_name):
continue
var material_path: String = map_settings.base_material_dir if not map_settings.base_material_dir.is_empty() else map_settings.base_texture_dir
material_path = material_path.path_join(texture_name) + "." + map_settings.material_file_extension
material_path = material_path.replace("*", "")
if ResourceLoader.exists(material_path):
var material: Material = load(material_path)
texture_materials[texture_name] = material
if material is BaseMaterial3D:
var albedo = material.albedo_texture
if albedo is Texture2D:
texture_sizes[texture_name] = material.albedo_texture.get_size()
elif material is ShaderMaterial:
var albedo = material.get_shader_parameter(map_settings.default_material_albedo_uniform)
if albedo is Texture2D:
texture_sizes[texture_name] = albedo.get_size()
if not texture_sizes.has(texture_name):
var texture: Texture2D = load_texture(texture_name, wad_resources, map_settings)
if texture:
texture_sizes[texture_name] = texture.get_size()
if not texture_sizes.has(texture_name):
texture_sizes[texture_name] = Vector2.ONE * map_settings.inverse_scale_factor
# Material generation
elif map_settings.default_material:
var material = map_settings.default_material.duplicate(true)
var texture: Texture2D = load_texture(texture_name, wad_resources, map_settings)
texture_sizes[texture_name] = texture.get_size()
if material is BaseMaterial3D:
material.albedo_texture = texture
build_base_material(map_settings, material, texture_name)
elif material is ShaderMaterial:
material.set_shader_parameter(map_settings.default_material_albedo_uniform, texture)
if (map_settings.save_generated_materials and material
and texture_name != map_settings.clip_texture
and texture_name != map_settings.skip_texture
and texture_name != map_settings.origin_texture
and texture.resource_path != default_texture_path):
ResourceSaver.save(material, material_path)
texture_materials[texture_name] = material
else: # No default material exists
printerr("Error: No default material found in map settings")
return [texture_materials, texture_sizes]
#endregion
#region UV MAPPING
## Returns UV coordinate calculated from the Valve 220 UV format.
static func get_valve_uv(vertex: Vector3, u_axis: Vector3, v_axis: Vector3, uv_basis := Transform2D.IDENTITY, texture_size := Vector2.ONE) -> Vector2:
var uv := Vector2(u_axis.dot(vertex), v_axis.dot(vertex))
uv += (uv_basis.origin * uv_basis.get_scale())
uv.x /= uv_basis.x.x
uv.y /= uv_basis.y.y
uv.x /= texture_size.x
uv.y /= texture_size.y
return uv
## Returns UV coordinate calculated from the original id Standard UV format.
static func get_quake_uv(vertex: Vector3, normal: Vector3, uv_basis := Transform2D.IDENTITY, texture_size := Vector2.ONE) -> Vector2:
var nx := absf(normal.dot(Vector3.RIGHT))
var ny := absf(normal.dot(Vector3.UP))
var nz := absf(normal.dot(Vector3.FORWARD))
var uv: Vector2
if ny >= nx and ny >= nz:
uv = Vector2(vertex.x, -vertex.z)
elif nx >= ny and nx >= nz:
uv = Vector2(vertex.y, -vertex.z)
else:
uv = Vector2(vertex.x, vertex.y)
var uv_out := uv.rotated(uv_basis.get_rotation())
uv_out /= uv_basis.get_scale()
uv_out += uv_basis.origin
uv_out /= texture_size
return uv_out
## Determines which UV format is being used and returns the UV coordinate.
static func get_face_vertex_uv(vertex: Vector3, face: FuncGodotData.FaceData, texture_size: Vector2) -> Vector2:
if face.uv_axes.size() >= 2:
return get_valve_uv(vertex, face.uv_axes[0], face.uv_axes[1], face.uv, texture_size)
else:
return get_quake_uv(vertex, face.plane.normal, face.uv, texture_size)
## Returns the tangent calculated from the Valve 220 UV format.
static func get_valve_tangent(u: Vector3, v: Vector3, normal: Vector3) -> PackedFloat32Array:
var u_axis: Vector3 = u.normalized()
var v_axis: Vector3 = v.normalized()
var v_sign: float = -signf(normal.cross(u_axis).dot(v_axis))
return [u_axis.x, u_axis.y, u_axis.z, v_sign]
# NOTE: we may still need to orthonormalize tangents. Just in case, here's a rough outline.
#var tangent: Vector3 = u.normalized()
#tangent = (tangent - normal * normal.dot(tangent)).normalized()
#
## in the case of parallel U or V axes to planar normal, reconstruct the tangent
#if tangent.length_squared() < 0.01:
# if absf(normal.y) < 0.9:
# tangent = Vector3.UP.cross(normal)
# else:
# tangent = Vector3.RIGHT.cross(normal)
#
#tangent = tangent.normalized()
#return [tangent.x, tangent.y, tangent.z, -signf(normal.cross(tangent).dot(v.normalized))]
## Returns the tangent calculated from the original id Standard UV format.
static func get_quake_tangent(normal: Vector3, uv_y_scale: float, uv_rotation: float) -> PackedFloat32Array:
var dx := normal.dot(_VEC3_RIGHT_ID)
var dy := normal.dot(_VEC3_UP_ID)
var dz := normal.dot(_VEC3_FORWARD_ID)
var dxa := absf(dx)
var dya := absf(dy)
var dza := absf(dz)
var u_axis: Vector3
var v_sign: float = 0.0
if dya >= dxa and dya >= dza:
u_axis = _VEC3_FORWARD_ID
v_sign = signf(dy)
elif dxa >= dya and dxa >= dza:
u_axis = _VEC3_FORWARD_ID
v_sign = -signf(dx)
elif dza >= dya and dza >= dxa:
u_axis = _VEC3_RIGHT_ID
v_sign = signf(dz)
v_sign *= signf(uv_y_scale)
u_axis = u_axis.rotated(normal, deg_to_rad(-uv_rotation) * v_sign)
return [u_axis.x, u_axis.y, u_axis.z, v_sign]
static func get_face_tangent(face: FuncGodotData.FaceData) -> PackedFloat32Array:
if face.uv_axes.size() >= 2:
return get_valve_tangent(face.uv_axes[0], face.uv_axes[1], face.plane.normal)
else:
return get_quake_tangent(face.plane.normal, face.uv.get_scale().y, face.uv.get_rotation())
#endregion
#region MESH
static func smooth_mesh_by_angle(mesh: ArrayMesh, angle_deg: float = 89.0) -> Mesh:
if not mesh:
push_error("Need a source mesh to smooth")
return
var angle: float = deg_to_rad(clampf(angle_deg, 0.0, 360.0))
var mesh_vertices: Array[Vector3] = []
var mesh_normals: Array[Vector3] = []
var surface_data: Array[Dictionary] = []
var mdt: MeshDataTool
var st := SurfaceTool.new()
# Collect surface information
for surface_index in mesh.get_surface_count():
mdt = MeshDataTool.new()
if mdt.create_from_surface(mesh, surface_index) != OK:
continue
var info: Dictionary = {
"mdt": mdt,
"ofs": mesh_vertices.size(),
"mat": mesh.surface_get_material(surface_index)
}
surface_data.append(info)
for i in mdt.get_vertex_count():
mesh_vertices.append(mdt.get_vertex(i))
mesh_normals.append(mdt.get_vertex_normal(i))
var groups: Dictionary = {}
# Group vertices by position
for i in mesh_vertices.size():
var pos := mesh_vertices[i]
# this is likely already snapped from the map building process
var key := pos.snappedf(_VERTEX_EPSILON)
if not groups.has(key):
groups[key] = [i]
else:
groups[key].append(i)
# Collect normals. Likely optimizable.
for group in groups.values():
for i in group:
var this := mesh_normals[i]
var normal_out := Vector3()
for j in group:
var other := mesh_normals[j]
if this.angle_to(other) <= angle:
normal_out += other
mesh_normals[i] = normal_out.normalized()
var smoothed_mesh := ArrayMesh.new()
# Construct smoothed output mesh
for dict in surface_data:
mdt = dict["mdt"]
var offset: int = dict["ofs"]
for i in mdt.get_vertex_count():
mdt.set_vertex_normal(i, mesh_normals[offset + i])
st = SurfaceTool.new()
st.begin(Mesh.PRIMITIVE_TRIANGLES)
st.set_material(dict["mat"])
for i in mdt.get_face_count():
for j in 3:
var index := mdt.get_face_vertex(i, j)
st.set_normal(mdt.get_vertex_normal(index))
st.set_uv(mdt.get_vertex_uv(index))
st.set_tangent(mdt.get_vertex_tangent(index))
st.add_vertex(mdt.get_vertex(index))
smoothed_mesh = st.commit(smoothed_mesh)
return smoothed_mesh
#endregion

View File

@@ -1 +1 @@
uid://dy80wnu2py4dk uid://bursmx2g1betd

View File

@@ -5,4 +5,4 @@
[resource] [resource]
albedo_texture = ExtResource("1_ncj77") albedo_texture = ExtResource("1_ncj77")
metallic_specular = 0.0 metallic_specular = 0.0
texture_filter = 4 texture_filter = 2

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 B

View File

@@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dutip72dl002r"
path="res://.godot/imported/origin.png-85b62dd151467f05fa8f98ed6d2927d0.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/func_godot/textures/special/origin.png"
dest_files=["res://.godot/imported/origin.png-85b62dd151467f05fa8f98ed6d2927d0.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
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/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

View File

@@ -12,21 +12,21 @@ font_size = 96
[sub_resource type="LabelSettings" id="LabelSettings_jqn4v"] [sub_resource type="LabelSettings" id="LabelSettings_jqn4v"]
font = ExtResource("3_mcea6") font = ExtResource("3_mcea6")
font_size = 68 font_size = 48
font_color = Color(1, 1, 1, 0.501961) font_color = Color(1, 1, 1, 0.501961)
outline_size = 16 outline_size = 16
outline_color = Color(0, 0, 0, 0.501961) outline_color = Color(0, 0, 0, 0.501961)
[sub_resource type="LabelSettings" id="LabelSettings_fpuge"] [sub_resource type="LabelSettings" id="LabelSettings_fpuge"]
font = ExtResource("3_mcea6") font = ExtResource("3_mcea6")
font_size = 80 font_size = 60
font_color = Color(1, 1, 1, 0.627451) font_color = Color(1, 1, 1, 0.627451)
outline_size = 16 outline_size = 16
outline_color = Color(0, 0, 0, 0.627451) outline_color = Color(0, 0, 0, 0.627451)
[sub_resource type="LabelSettings" id="LabelSettings_npr4g"] [sub_resource type="LabelSettings" id="LabelSettings_npr4g"]
font = ExtResource("3_mcea6") font = ExtResource("3_mcea6")
font_size = 96 font_size = 72
outline_size = 16 outline_size = 16
outline_color = Color(0, 0, 0, 1) outline_color = Color(0, 0, 0, 1)
@@ -68,7 +68,7 @@ theme_override_constants/separation = -8
[node name="prompt_overflow" type="Label" parent="hud_bottom/promptbox"] [node name="prompt_overflow" type="Label" parent="hud_bottom/promptbox"]
visible = false visible = false
layout_mode = 2 layout_mode = 2
text = "..." text = ". . ."
label_settings = SubResource("LabelSettings_jqn4v") label_settings = SubResource("LabelSettings_jqn4v")
horizontal_alignment = 1 horizontal_alignment = 1
vertical_alignment = 1 vertical_alignment = 1
@@ -97,6 +97,23 @@ label_settings = SubResource("LabelSettings_npr4g")
horizontal_alignment = 1 horizontal_alignment = 1
vertical_alignment = 1 vertical_alignment = 1
[node name="subtitle" type="MarginContainer" parent="."]
visible = false
anchors_preset = 12
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_top = -300.0
grow_horizontal = 2
grow_vertical = 0
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="ColorRect" type="ColorRect" parent="subtitle"]
visible = false
layout_mode = 2
color = Color(0.15, 0.15, 0.15, 0.784314)
[node name="icon_eye" type="TextureRect" parent="."] [node name="icon_eye" type="TextureRect" parent="."]
visible = false visible = false
offset_right = 192.0 offset_right = 192.0

View File

@@ -20,7 +20,7 @@ var on_route : bool = false
@export var behavior_type : String = "guard" @export var behavior_type : String = "guard"
var state : String = "default" var state : String = "default"
var current_waypoint : int = -1 var current_waypoint : int = 0
var paused : bool = false var paused : bool = false
var pauseanim : String var pauseanim : String
var pauseturn : bool var pauseturn : bool
@@ -226,7 +226,7 @@ func movement_process(delta):
if !paused: if !paused:
walk_toward(current_path_point) walk_toward(current_path_point)
if global_transform.origin.distance_to(current_path_point) <= 0.5: if global_transform.origin.distance_to(current_path_point) <= 0.25:
current_path_index += 1 current_path_index += 1
if current_path_index >= current_path.size(): if current_path_index >= current_path.size():
if on_route: if on_route:
@@ -265,9 +265,10 @@ func angle_toward(target : Vector3):
return (Vector2(target.z, target.x) - Vector2($CharacterArmature.global_transform.origin.z, $CharacterArmature.global_transform.origin.x)).angle() return (Vector2(target.z, target.x) - Vector2($CharacterArmature.global_transform.origin.z, $CharacterArmature.global_transform.origin.x)).angle()
func walk_toward(point: Vector3): func walk_toward(point: Vector3):
var direction = global_transform.origin.direction_to(point) var direction = global_transform.origin.direction_to(point)
velocity.x = direction.x * walk_speed velocity.x = direction.x * walk_speed
velocity.z = direction.z * walk_speed velocity.z = direction.z * walk_speed
print(direction)
lerp_toward(atan2(velocity.x, velocity.z)) lerp_toward(atan2(velocity.x, velocity.z))
@@ -289,7 +290,8 @@ func setpath(target_position):
target_position, target_position,
true true
) )
path_end_point = current_path[len(current_path)-1] if len(current_path) > 0:
path_end_point = current_path[len(current_path)-1]
func clearpath(): func clearpath():
current_path = [] current_path = []

1
maps/autosave/.gdignore Normal file
View File

@@ -0,0 +1 @@

View File

@@ -1,14 +0,0 @@
[remap]
importer="func_godot.map"
type="Resource"
uid="uid://bmjn0u44o5mvf"
path="res://.godot/imported/cafemotel.1.map-47307151d410699b7cf7c2fe6d296bb7.tres"
[deps]
source_file="res://maps/autosave/cafemotel.1.map"
dest_files=["res://.godot/imported/cafemotel.1.map-47307151d410699b7cf7c2fe6d296bb7.tres"]
[params]

View File

@@ -1,14 +0,0 @@
[remap]
importer="func_godot.map"
type="Resource"
uid="uid://dnemivow04qpk"
path="res://.godot/imported/cafemotel.10.map-d2df9a014684f3492d3e6763eb7f1da0.tres"
[deps]
source_file="res://maps/autosave/cafemotel.10.map"
dest_files=["res://.godot/imported/cafemotel.10.map-d2df9a014684f3492d3e6763eb7f1da0.tres"]
[params]

Some files were not shown because too many files have changed in this diff Show More