Initial commit

This commit is contained in:
Jens Pitkänen 2024-08-17 14:59:22 +03:00
commit 60baf7a626
102 changed files with 6188 additions and 0 deletions

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
# Godot 4+ specific ignores
.godot/
/android/
# Trenchbroom ignores
/**/autosave

View File

@ -0,0 +1,30 @@
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=3 format=3 uid="uid://cxy7jnh6d7msn"]
[ext_resource type="Script" 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"]
[resource]
script = ExtResource("1_0fsmp")
spawn_type = 2
origin_type = 0
build_visuals = true
use_in_baked_light = true
shadow_casting_setting = 1
build_occlusion = false
render_layers = 1
collision_shape_type = 2
collision_layer = 1
collision_mask = 0
collision_priority = 1.0
collision_shape_margin = 0.04
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
base_classes = Array[Resource]([ExtResource("1_c3bns")])
class_properties = {}
class_property_descriptions = {}
meta_properties = {
"color": Color(0.8, 0.8, 0.8, 1)
}
node_class = "StaticBody3D"
name_property = ""

View File

@ -0,0 +1,30 @@
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=3 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="Script" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="2_lhb87"]
[resource]
script = ExtResource("2_lhb87")
spawn_type = 2
origin_type = 0
build_visuals = true
use_in_baked_light = true
shadow_casting_setting = 1
build_occlusion = false
render_layers = 1
collision_shape_type = 0
collision_layer = 1
collision_mask = 1
collision_priority = 1.0
collision_shape_margin = 0.04
classname = "func_detail_illusionary"
description = "Static geometry with no collision. Builds a Node3D with a MeshInstance3D. Does not occlude other VisualInstance3D nodes."
func_godot_internal = false
base_classes = Array[Resource]([ExtResource("1_ar63x")])
class_properties = {}
class_property_descriptions = {}
meta_properties = {
"color": Color(0.8, 0.8, 0.8, 1)
}
node_class = "Node3D"
name_property = ""

View File

@ -0,0 +1,30 @@
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=3 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" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="2_8o081"]
[resource]
script = ExtResource("2_8o081")
spawn_type = 2
origin_type = 0
build_visuals = true
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
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")])
class_properties = {}
class_property_descriptions = {}
meta_properties = {
"color": Color(0.8, 0.8, 0.8, 1)
}
node_class = "StaticBody3D"
name_property = ""

View File

@ -0,0 +1,17 @@
[gd_resource type="Resource" script_class="FuncGodotFGDFile" load_steps=8 format=3 uid="uid://crgpdahjaj"]
[ext_resource type="Script" 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://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://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"]
[resource]
script = ExtResource("1_axt3h")
export_file = false
target_map_editor = 1
fgd_name = "FuncGodot"
base_fgd_files = Array[Resource]([])
entity_definitions = Array[Resource]([ExtResource("1_ehab8"), ExtResource("2_ri2rx"), ExtResource("3_7jigp"), ExtResource("3_fqfww"), ExtResource("5_b2q3p"), ExtResource("4_c4ucw")])

View File

@ -0,0 +1,30 @@
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=3 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="Script" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="2_uffhi"]
[resource]
script = ExtResource("2_uffhi")
spawn_type = 2
origin_type = 0
build_visuals = true
use_in_baked_light = true
shadow_casting_setting = 1
build_occlusion = true
render_layers = 1
collision_shape_type = 0
collision_layer = 1
collision_mask = 1
collision_priority = 1.0
collision_shape_margin = 0.04
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."
func_godot_internal = false
base_classes = Array[Resource]([ExtResource("1_kv0mq")])
class_properties = {}
class_property_descriptions = {}
meta_properties = {
"color": Color(0.8, 0.8, 0.8, 1)
}
node_class = "Node3D"
name_property = ""

View File

@ -0,0 +1,27 @@
[gd_resource type="Resource" script_class="FuncGodotFGDBaseClass" load_steps=2 format=3 uid="uid://nayxb8n7see2"]
[ext_resource type="Script" path="res://addons/func_godot/src/fgd/func_godot_fgd_base_class.gd" id="1_04y3n"]
[resource]
script = ExtResource("1_04y3n")
classname = "Phong"
description = "Phong shading options for SolidClass geometry."
func_godot_internal = false
base_classes = Array[Resource]([])
class_properties = {
"_phong": {
"Disabled": 0,
"Smooth shading": 1
},
"_phong_angle": 89.0
}
class_property_descriptions = {
"_phong": ["Phong shading", 0],
"_phong_angle": "Phong smoothing angle"
}
meta_properties = {
"color": Color(0.8, 0.8, 0.8, 1),
"size": AABB(-8, -8, -8, 8, 8, 8)
}
node_class = ""
name_property = ""

View File

@ -0,0 +1,29 @@
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=2 format=3 uid="uid://bdji3873bg32h"]
[ext_resource type="Script" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="1_62t8m"]
[resource]
script = ExtResource("1_62t8m")
spawn_type = 0
origin_type = 0
build_visuals = true
use_in_baked_light = true
shadow_casting_setting = 1
build_occlusion = true
render_layers = 1
collision_shape_type = 1
collision_layer = 1
collision_mask = 0
collision_priority = 1.0
collision_shape_margin = 0.04
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."
func_godot_internal = false
base_classes = Array[Resource]([])
class_properties = {}
class_property_descriptions = {}
meta_properties = {
"color": Color(0.8, 0.8, 0.8, 1)
}
node_class = "StaticBody3D"
name_property = ""

View File

@ -0,0 +1,30 @@
[gd_resource type="Resource" script_class="FuncGodotMapSettings" load_steps=4 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="Script" 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"]
[resource]
script = ExtResource("1_dlf23")
inverse_scale_factor = 32.0
entity_fgd = ExtResource("1_hd7se")
entity_name_property = ""
base_texture_dir = "res://textures"
texture_file_extensions = Array[String](["png", "jpg", "jpeg", "bmp", "tga", "webp"])
clip_texture = "special/clip"
skip_texture = "special/skip"
texture_wads = Array[Resource]([])
material_file_extension = "tres"
unshaded = false
default_material = ExtResource("1_8l5wm")
default_material_albedo_uniform = ""
albedo_map_pattern = "%s_albedo.%s"
normal_map_pattern = "%s_normal.%s"
metallic_map_pattern = "%s_metallic.%s"
roughness_map_pattern = "%s_roughness.%s"
emission_map_pattern = "%s_emission.%s"
ao_map_pattern = "%s_ao.%s"
height_map_pattern = "%s_height.%s"
save_generated_materials = true
uv_unwrap_texel_size = 2.0
use_trenchbroom_groups_hierarchy = false

View File

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

View File

@ -0,0 +1,21 @@
[gd_resource type="Resource" script_class="NetRadiantCustomGamePackConfig" load_steps=5 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="Script" 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://cfhg30jclb4lw" path="res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_skip.tres" id="3_6gpk8"]
[resource]
script = ExtResource("2_en8ro")
export_file = false
gamepack_name = "func_godot"
game_name = "FuncGodot"
base_game_path = ""
fgd_file = ExtResource("1_gct4v")
netradiant_custom_shaders = Array[Resource]([ExtResource("2_w7psh"), ExtResource("3_6gpk8")])
texture_types = PackedStringArray("png", "jpg", "jpeg", "bmp", "tga")
model_types = PackedStringArray("glb", "gltf", "obj")
sound_types = PackedStringArray("wav", "ogg")
default_scale = "1.0"
clip_texture = "textures/special/clip"
skip_texture = "textures/special/skip"

View File

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

View File

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

View File

@ -0,0 +1,33 @@
[gd_resource type="Resource" script_class="TrenchBroomGameConfig" load_steps=8 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://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="Script" 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://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"]
[resource]
script = ExtResource("2_ns6ah")
export_file = false
game_name = "FuncGodot"
icon = ExtResource("6_tex5j")
map_formats = Array[Dictionary]([{
"format": "Valve",
"initialmap": "initial_valve.map"
}, {
"format": "Standard",
"initialmap": "initial_standard.map"
}, {
"format": "Quake2",
"initialmap": "initial_quake2.map"
}, {
"format": "Quake3"
}])
texture_exclusion_patterns = Array[String](["*_albedo", "*_ao", "*_emission", "*_height", "*_metallic", "*_normal", "*_orm", "*_roughness", "*_sss"])
fgd_file = ExtResource("1_8u1vq")
entity_scale = "32"
default_uv_scale = Vector2(1, 1)
brush_tags = Array[Resource]([ExtResource("3_mbuv7"), ExtResource("4_y0tjm")])
brushface_tags = Array[Resource]([ExtResource("1_rsp20"), ExtResource("2_166i2")])

View File

@ -0,0 +1,11 @@
[gd_resource type="Resource" script_class="TrenchBroomTag" load_steps=2 format=3 uid="uid://37iduqf7tpxq"]
[ext_resource type="Script" path="res://addons/func_godot/src/trenchbroom/trenchbroom_tag.gd" id="1_rn13a"]
[resource]
script = ExtResource("1_rn13a")
tag_name = "Func"
tag_attributes = Array[String]([])
tag_match_type = 1
tag_pattern = "func*"
texture_name = ""

View File

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

View File

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

View File

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

BIN
addons/func_godot/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cp2as6ujvknyu"
path="res://.godot/imported/icon.png-6db43b6a52df1ce3744a82f15cbdbbea.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/func_godot/icon.png"
dest_files=["res://.godot/imported/icon.png-6db43b6a52df1ce3744a82f15cbdbbea.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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bx5buvf1ydm7q"
path="res://.godot/imported/icon.svg-99f2c56e0c1ce867c819715c68d9c120.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/func_godot/icon.svg"
dest_files=["res://.godot/imported/icon.svg-99f2c56e0c1ce867c819715c68d9c120.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://decwujsyhj0qy"
path="res://.godot/imported/icon32.png-7025e2d95a64a3066b7947e1900b4daf.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/func_godot/icon32.png"
dest_files=["res://.godot/imported/icon32.png-7025e2d95a64a3066b7947e1900b4daf.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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bm2kwpq18quv0"
path="res://.godot/imported/icon_godambler.svg-a6dbba375ab2a45be046a1875b8d41e6.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/func_godot/icons/icon_godambler.svg"
dest_files=["res://.godot/imported/icon_godambler.svg-a6dbba375ab2a45be046a1875b8d41e6.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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dieefivfbkovw"
path="res://.godot/imported/icon_godambler3d.svg-f7df9bfe58320474198644aa06a8f3f6.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/func_godot/icons/icon_godambler3d.svg"
dest_files=["res://.godot/imported/icon_godambler3d.svg-f7df9bfe58320474198644aa06a8f3f6.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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cfxlhjsefleff"
path="res://.godot/imported/icon_godot_ranger.svg-8572582518f54de6403b767a923b5a92.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/func_godot/icons/icon_godot_ranger.svg"
dest_files=["res://.godot/imported/icon_godot_ranger.svg-8572582518f54de6403b767a923b5a92.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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://brm515f5ivx8m"
path="res://.godot/imported/icon_godot_ranger3d.svg-a9a2c9bcf2e8b1e07a0a941a16264e98.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/func_godot/icons/icon_godot_ranger3d.svg"
dest_files=["res://.godot/imported/icon_godot_ranger3d.svg-a9a2c9bcf2e8b1e07a0a941a16264e98.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

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="16"
height="16"
viewBox="0 0 16 16"
id="svg2"
version="1.1"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
inkscape:export-filename="/home/djrm/Projects/godot/tools/editor/icons/icon_node.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90"
sodipodi:docname="icon_qodot_node.svg">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="31.999999"
inkscape:cx="9.2742768"
inkscape:cy="5.9794586"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-bbox-midpoints="true"
inkscape:snap-object-midpoints="true"
inkscape:snap-center="true"
inkscape:window-width="1849"
inkscape:window-height="942"
inkscape:window-x="2365"
inkscape:window-y="478"
inkscape:window-maximized="0">
<inkscape:grid
type="xygrid"
id="grid3336"
empspacing="4" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1036.3622)">
<g
transform="matrix(0.00898883,0,0,0.00898883,2.84873,1036.9616)"
id="g8"
style="fill:#e0e0e0;fill-opacity:0.99607843">
<path
sodipodi:type="inkscape:offset"
inkscape:radius="32.247818"
inkscape:original="M 805.49219 48.865234 L 805.49219 52 L 805.49219 70.470703 C 908.63676 125.4219 990.75756 219.02053 1030.5371 328.76562 C 1072.5343 442.84389 1069.0608 572.88678 1019.4355 684.0293 C 962.84256 813.15825 847.75434 914.65851 712.625 954.68555 C 687.95498 962.05941 662.7661 967.4194 637.26562 970.68359 C 637.37347 905.40191 636.93661 840.04476 637.35156 774.81055 C 644.27173 753.89399 667.02241 744.70589 686.79883 740.29102 C 696.25394 738.22707 705.90322 736.98982 715.58203 736.76953 C 714.41755 731.86565 718.02354 722.1913 713.58203 720.56445 L 439 720.56445 C 440.15426 725.49354 436.59893 734.98734 440.9375 736.83008 C 464.02438 738.2287 488.57642 742.59448 506.58789 758.30469 C 513.72262 764.95503 519.23908 774.34645 517.41992 784.43945 L 517.41992 970.64062 C 392.2186 952.28136 280.08237 877.51602 203.18555 778.49805 C 146.20758 705.03793 105.92922 616.34944 98.074219 523.1582 C 90.131888 410.66597 124.62307 296.10332 192.38281 205.89258 C 233.9121 150.23193 287.72076 103.61704 348.8125 70.462891 C 348.8205 63.264976 348.82683 56.067058 348.83203 48.869141 C 226.43763 103.66249 125.42782 204.32365 70.367188 326.61133 C 10.634796 457.1454 4.9961598 611.01761 54.242188 745.77539 C 100.18371 873.54309 194.5137 982.99051 313.99414 1047.4492 C 376.81007 1081.5901 446.33117 1103.3409 517.41992 1110.9785 C 517.56059 1169.4421 517.13902 1227.9564 517.62891 1286.3887 C 536.50698 1390.7196 555.38596 1495.0499 574.26367 1599.3809 C 595.24637 1494.6025 616.32947 1389.8338 637.24609 1285.0488 C 637.24709 1227.0293 637.249 1169.0098 637.25 1110.9902 C 753.86537 1099.5192 866.94739 1050.9022 953.70898 971.83203 C 1036.6427 897.08153 1094.4372 795.63679 1118.3398 686.71289 C 1142.8039 576.4452 1136.0436 458.67677 1094.3887 353.33398 C 1044.4835 224.74362 944.8252 116.42175 820.77539 56.089844 C 815.72077 53.598792 810.60447 51.234804 805.49219 48.865234 z "
style="fill:#e0e0e0;fill-opacity:0.99607843;stroke:none;stroke-width:5;stroke-miterlimit:10;stroke-opacity:1"
id="path6"
d="m 347.90625,16.636719 a 32.251043,32.251043 0 0 0 -12.25,2.798828 C 205.88469,77.531491 99.412299,183.61822 41.001953,313.29102 -22.348317,451.80048 -28.265288,613.88282 23.927734,756.76758 72.647329,892.19719 172.02966,1007.4904 298.65234,1075.8125 c 58.00997,31.5205 121.31762,53.0204 186.51172,63.377 -0.002,48.987 -0.19484,98.1365 0.21875,147.4687 a 32.251043,32.251043 0 0 0 0.51367,5.4727 c 18.87811,104.3311 37.75711,208.6614 56.63477,312.9921 a 32.251043,32.251043 0 0 0 63.35156,0.5899 c 20.97874,-104.7586 42.06315,-209.5337 62.98633,-314.3516 a 32.251043,32.251043 0 0 0 0.625,-6.3125 c 8.4e-4,-48.5498 0.003,-97.1 0.004,-145.6504 112.94142,-16.8214 220.92348,-66.2925 305.84961,-143.65817 88.14404,-79.4662 149.14894,-186.66163 174.48434,-302.08984 25.7675,-116.18424 18.7794,-240.2345 -25.416,-352.06836 C 1071.4778,205.24135 966.47079,91.107321 834.92773,27.115234 829.42805,24.407001 824.10868,21.95088 819.05273,19.607422 A 32.251043,32.251043 0 0 0 773.24414,48.865234 V 52 70.470703 a 32.251043,32.251043 0 0 0 17.08594,28.460938 c 96.0021,51.145979 172.95277,138.924249 209.88872,240.824219 a 32.251043,32.251043 0 0 0 0.057,0.15039 c 39.0531,106.08119 35.7505,227.87147 -10.28517,330.97461 a 32.251043,32.251043 0 0 0 -0.0898,0.20312 C 937.26564,791.18147 829.14264,886.53883 703.4668,923.76562 a 32.251043,32.251043 0 0 0 -0.0762,0.0234 c -11.19816,3.34713 -22.50938,6.2295 -33.90429,8.67578 -0.0393,-50.21706 -0.16903,-100.33931 0.10156,-150.23828 0.86271,-0.9191 2.16446,-1.94384 4.36719,-3.27734 4.48441,-2.7148 12.01568,-5.42153 19.80273,-7.16602 7.58488,-1.65065 15.16509,-2.60516 22.5586,-2.77343 a 32.251043,32.251043 0 0 0 30.64062,-39.69141 c 2.24778,9.46588 0.68077,5.89079 1.03906,0.11719 0.17915,-2.88681 0.72749,-6.86388 -1.11718,-14.20313 -1.84468,-7.33925 -10.07914,-20.50769 -22.20508,-24.94922 a 32.251043,32.251043 0 0 0 -11.0918,-1.96679 H 439 a 32.251043,32.251043 0 0 0 -31.40234,39.58203 c -2.13044,-9.10803 -0.65038,-5.75562 -1.00782,-0.0645 -0.17902,2.85053 -0.68774,6.79348 1.02344,13.91016 1.71118,7.11668 9.00286,19.79227 20.7168,24.76758 a 32.251043,32.251043 0 0 0 10.6582,2.50781 c 20.32647,1.23139 36.93313,5.44255 46.25977,13.48242 a 32.251043,32.251043 0 0 0 -0.0762,1.9375 V 931.33789 C 384.2764,906.36086 293.26933,841.91584 228.66602,758.73438 l -0.0117,-0.0156 C 174.92035,689.43588 137.4999,606.28431 130.23242,520.66211 122.92233,416.31754 155.17118,309.12943 218.16797,225.25977 a 32.251043,32.251043 0 0 0 0.0606,-0.082 c 38.7059,-51.87653 89.03359,-95.47481 145.96484,-126.371089 a 32.251043,32.251043 0 0 0 16.86719,-28.308594 c 0.008,-7.202903 0.0143,-14.404177 0.0195,-21.605469 A 32.251043,32.251043 0 0 0 347.90625,16.636719 Z M 484.59961,781.89453 c 0.6695,0.62405 0.56402,0.47412 0.65039,0.54102 a 32.251043,32.251043 0 0 0 0,0.0449 z" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c0464gp8lby0w"
path="res://.godot/imported/icon_quake_file.svg-1718b9a2b5e0b124f6d72bb4c72d2ee6.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/func_godot/icons/icon_quake_file.svg"
dest_files=["res://.godot/imported/icon_quake_file.svg-1718b9a2b5e0b124f6d72bb4c72d2ee6.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

@ -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="#fc7f7f" 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="#fc7f7f" 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="#fc7f7f" 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="#fc7f7f" 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="#fc7f7f" 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="#fc7f7f" 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="#fc7f7f" 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="#fc7f7f" 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="#fc7f7f" 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://cfvririkaa4tv"
path="res://.godot/imported/icon_slipgate3d.svg-f125bef6ff5aa79b5fe3f232a083425e.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/func_godot/icons/icon_slipgate3d.svg"
dest_files=["res://.godot/imported/icon_slipgate3d.svg-f125bef6ff5aa79b5fe3f232a083425e.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

Binary file not shown.

View File

@ -0,0 +1,14 @@
[remap]
importer="func_godot.palette"
type="Resource"
uid="uid://drgnc41yfybr"
path="res://.godot/imported/palette.lmp-138c33f2ac0cab3ad6373e7c0425cf00.tres"
[deps]
source_file="res://addons/func_godot/palette.lmp"
dest_files=["res://.godot/imported/palette.lmp-138c33f2ac0cab3ad6373e7c0425cf00.tres"]
[params]

View File

@ -0,0 +1,7 @@
[plugin]
name="FuncGodot"
description="Quake .map file support for Godot."
author="Shifty, Hannah Crawford, Emberlynn Bland, Tim Maccabe"
version="2024.1"
script="src/func_godot_plugin.gd"

View File

@ -0,0 +1,116 @@
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)
var map_settings: FuncGodotMapSettings = null
func load_map(filename: String, keep_tb_groups: bool) -> void:
map_parser.load_map(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(texture_name: String) -> 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(map_settings.clip_texture)
sg.set_skip_filter_texture(map_settings.skip_texture)
sg.run()
return fetch_surfaces(sg)
func gather_entity_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_entity_concave_collision_surfaces(entity_idx: int) -> 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(map_settings.skip_texture)
surface_gatherer.run()
func fetch_surfaces(sg: FuncGodotSurfaceGatherer) -> Array:
var surfs: Array[FuncGodotMapData.FuncGodotFaceGeometry] = sg.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) / map_settings.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

View File

@ -0,0 +1,327 @@
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: FuncGodotMapData.FuncGodotFaceVertex, b: FuncGodotMapData.FuncGodotFaceVertex) -> bool:
var face: FuncGodotMapData.FuncGodotFace = map_data.entities[wind_entity_idx].brushes[wind_brush_idx].faces[wind_face_idx]
var face_geo: FuncGodotMapData.FuncGodotFaceGeometry = map_data.entity_geo[wind_entity_idx].brushes[wind_brush_idx].faces[wind_face_idx]
var u: Vector3 = wind_face_basis.normalized()
var v: Vector3 = u.cross(wind_face_normal).normalized()
var loc_a: Vector3 = a.vertex - wind_face_center
var a_pu: float = loc_a.dot(u)
var a_pv: float = loc_a.dot(v)
var loc_b: Vector3 = b.vertex - wind_face_center
var b_pu: float = loc_b.dot(u)
var b_pv: float = loc_b.dot(v)
var a_angle: float = atan2(a_pv, a_pu)
var b_angle: float = atan2(b_pv, b_pu)
return a_angle < b_angle
# returns null if no intersection, else intersection vertex.
func intersect_face(f0: FuncGodotMapData.FuncGodotFace, f1: FuncGodotMapData.FuncGodotFace, f2: FuncGodotMapData.FuncGodotFace) -> Variant:
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 generate_brush_vertices(entity_idx: int, brush_idx: int) -> void:
var entity: FuncGodotMapData.FuncGodotEntity = map_data.entities[entity_idx]
var brush: FuncGodotMapData.FuncGodotBrush = entity.brushes[brush_idx]
var face_count: int = brush.faces.size()
var entity_geo: FuncGodotMapData.FuncGodotEntityGeometry = map_data.entity_geo[entity_idx]
var brush_geo: FuncGodotMapData.FuncGodotBrushGeometry = 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: FuncGodotMapData.FuncGodotFace = brush.faces[f0]
var face_geo: FuncGodotMapData.FuncGodotFaceGeometry = brush_geo.faces[f0]
var texture: FuncGodotMapData.FuncGodotTextureData = 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 : FuncGodotMapData.FuncGodotFaceGeometry = 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 = face.plane_normal
if phong:
var threshold:= cos((phong_angle + 0.01) * 0.0174533)
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()
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()
func run() -> void:
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: FuncGodotMapData.FuncGodotEntity = map_data.entities[e]
var entity_geo: FuncGodotMapData.FuncGodotEntityGeometry = 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: FuncGodotMapData.FuncGodotBrush = entity.brushes[b]
var brush_geo: FuncGodotMapData.FuncGodotBrushGeometry = 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: FuncGodotMapData.FuncGodotEntity = map_data.entities[e]
var entity_geo: FuncGodotMapData.FuncGodotEntityGeometry = map_data.entity_geo[e]
entity.center = Vector3.ZERO
for b in range(entity.brushes.size()):
var brush: FuncGodotMapData.FuncGodotBrush = entity.brushes[b]
brush.center = Vector3.ZERO
var vert_count: int = 0
generate_brush_vertices(e, b)
var brush_geo: FuncGodotMapData.FuncGodotBrushGeometry = 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: FuncGodotMapData.FuncGodotEntity = map_data.entities[e]
var entity_geo: FuncGodotMapData.FuncGodotEntityGeometry = map_data.entity_geo[e]
for b in range(entity.brushes.size()):
var brush: FuncGodotMapData.FuncGodotBrush = entity.brushes[b]
var brush_geo: FuncGodotMapData.FuncGodotBrushGeometry = entity_geo.brushes[b]
for f in range(brush.faces.size()):
var face: FuncGodotMapData.FuncGodotFace = brush.faces[f]
var face_geo: FuncGodotMapData.FuncGodotFaceGeometry = 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: FuncGodotMapData.FuncGodotEntityGeometry = map_data.entity_geo[e]
for b in range(entity_geo.brushes.size()):
var brush_geo: FuncGodotMapData.FuncGodotBrushGeometry = entity_geo.brushes[b]
for f in range(brush_geo.faces.size()):
var face_geo: FuncGodotMapData.FuncGodotFaceGeometry = 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)

View File

@ -0,0 +1,135 @@
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

@ -0,0 +1,326 @@
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(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 lines: PackedStringArray = []
var map: FileAccess = FileAccess.open(map_file, FileAccess.READ)
if map == null:
printerr("Error: Failed to open map file (" + map_file + ")")
return false
if map_file.ends_with(".import"):
while not map.eof_reached():
var line: String = map.get_line()
if line.begins_with("path"):
map.close()
line = line.replace("path=", "");
line = line.replace('"', '')
var map_data: String = (load(line) as QuakeMapFile).map_data
if map_data.is_empty():
printerr("Error: Failed to open map file (" + line + ")")
return false
lines = map_data.split("\n")
break
else:
while not map.eof_reached():
var line: String = map.get_line()
lines.append(line)
for line in lines:
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

@ -0,0 +1,119 @@
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_face(entity_idx: int, brush_idx: int, face_idx: int) -> bool:
var face: FuncGodotMapData.FuncGodotFace = map_data.entities[entity_idx].brushes[brush_idx].faces[face_idx]
var face_geo: FuncGodotMapData.FuncGodotFaceGeometry = 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()):
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: FuncGodotMapData.FuncGodotFaceGeometry = brush_geo.faces[f]
if filter_face(e, b, f):
continue
for v in range(face_geo.vertices.size()):
var vert: FuncGodotMapData.FuncGodotFaceVertex = 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

@ -0,0 +1,7 @@
@tool
## Special inheritance class for [FuncGodotFGDSolidClass] and [FuncGodotFGDPointClass] entity definitions. Useful for adding shared or common properties and descriptions.
class_name FuncGodotFGDBaseClass
extends FuncGodotFGDEntityClass
func _init() -> void:
prefix = "@BaseClass"

View File

@ -0,0 +1,209 @@
@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
var prefix: String = ""
@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.
@export var classname : String = ""
## Entity description that appears in the map editor. Not required.
@export_multiline var description : String = ""
## Entity does not get written to the exported FGD. Entity is only used for [FuncGodotMap] build process.
@export var func_godot_internal : bool = false
## FuncGodotFGDBaseClass resources to inherit [member class_properties] and [member class_descriptions] from.
@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.
@export var class_properties : Dictionary = {}
## Descriptions for previously defined key value pair properties.
@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.
@export var meta_properties : Dictionary = {
"size": AABB(Vector3(-8, -8, -8), Vector3(8, 8, 8)),
"color": Color(0.8, 0.8, 0.8)
}
@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.
@export var node_class := ""
## Class property to use in naming the generated node. Overrides `name_property` in [FuncGodotMapSettings].
## 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.
@export var name_property := ""
func build_def_text(target_editor: FuncGodotFGDFile.FuncGodotTargetMapEditors = FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM) -> String:
# Class prefix
var res : String = prefix
# Meta properties
var base_str = ""
var meta_props = meta_properties.duplicate()
for base_class in base_classes:
if not 'classname' in base_class:
continue
base_str += base_class.classname
if base_class != base_classes.back():
base_str += ", "
if base_str != "":
meta_props['base'] = base_str
for prop in meta_props:
if prefix == '@SolidClass':
if prop == "size" or prop == "model":
continue
if prop == 'model' and target_editor != FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM:
continue
var value = meta_props[prop]
res += " " + prop + "("
if value is AABB:
res += "%s %s %s, %s %s %s" % [
value.position.x,
value.position.y,
value.position.z,
value.size.x,
value.size.y,
value.size.z
]
elif value is Color:
res += "%s %s %s" % [
value.r8,
value.g8,
value.b8
]
elif value is String:
res += value
res += ")"
res += " = " + classname
if prefix != "@BaseClass": # having a description in BaseClasses crashes some editors
var normalized_description = description.replace("\n", " ").strip_edges() if prefix != "@BaseClass" else ""
if normalized_description != "":
res += " : \"%s\" " % [normalized_description]
else: # Having no description crashes some editors
res += " : \"" + classname + "\" "
if class_properties.size() > 0:
res += FuncGodotUtil.newline() + "[" + FuncGodotUtil.newline()
else:
res += "["
# Class properties
for prop in class_properties:
var value = class_properties[prop]
var prop_val = null
var prop_type := ""
var prop_description: String
if prop in class_property_descriptions:
# Optional default value for Choices can be set up as [String, int]
if value is Dictionary and class_property_descriptions[prop] is Array:
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):
prop_description = "\"" + prop_arr[0] + "\" : " + str(prop_arr[1])
else:
prop_description = "\"\" : 0"
printerr(str(prop) + " has incorrect description format. Should be [String description, int / String default value].")
else:
prop_description = "\"" + class_property_descriptions[prop] + "\""
else:
prop_description = "\"\""
match typeof(value):
TYPE_INT:
prop_type = "integer"
prop_val = str(value)
TYPE_FLOAT:
prop_type = "float"
prop_val = "\"" + str(value) + "\""
TYPE_STRING:
prop_type = "string"
prop_val = "\"" + value + "\""
TYPE_BOOL:
prop_type = "choices"
prop_val = FuncGodotUtil.newline() + "\t[" + FuncGodotUtil.newline()
prop_val += "\t\t" + str(0) + " : \"No\"" + FuncGodotUtil.newline()
prop_val += "\t\t" + str(1) + " : \"Yes\"" + FuncGodotUtil.newline()
prop_val += "\t]"
TYPE_VECTOR2, TYPE_VECTOR2I:
prop_type = "string"
prop_val = "\"%s %s\"" % [value.x, value.y]
TYPE_VECTOR3, TYPE_VECTOR3I:
prop_type = "string"
prop_val = "\"%s %s %s\"" % [value.x, value.y, value.z]
TYPE_VECTOR4, TYPE_VECTOR4I:
prop_type = "string"
prop_val = "\"%s %s %s %s\"" % [value[0], value[1], value[2], value[3]]
TYPE_COLOR:
prop_type = "color255"
prop_val = "\"%s %s %s\"" % [value.r8, value.g8, value.b8]
TYPE_DICTIONARY:
prop_type = "choices"
prop_val = FuncGodotUtil.newline() + "\t[" + FuncGodotUtil.newline()
for choice in value:
var choice_val = value[choice]
prop_val += "\t\t" + str(choice_val) + " : \"" + choice + "\"" + FuncGodotUtil.newline()
prop_val += "\t]"
TYPE_ARRAY:
prop_type = "flags"
prop_val = FuncGodotUtil.newline() + "\t[" + FuncGodotUtil.newline()
for arr_val in value:
prop_val += "\t\t" + str(arr_val[1]) + " : \"" + str(arr_val[0]) + "\" : " + ("1" if arr_val[2] else "0") + FuncGodotUtil.newline()
prop_val += "\t]"
TYPE_NODE_PATH:
prop_type = "target_destination"
TYPE_OBJECT:
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"
if prop_val:
res += "\t"
res += prop
res += "("
res += prop_type
res += ")"
if not value is Array:
if not value is Dictionary or prop_description != "":
res += " : "
res += prop_description
if value is bool or value is Dictionary or value is Array:
res += " = "
else:
res += " : "
res += prop_val
res += FuncGodotUtil.newline()
res += "]" + FuncGodotUtil.newline()
return res

View File

@ -0,0 +1,164 @@
@tool
@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
## 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.
@export var export_file: bool:
get:
return export_file # TODO Converter40 Non existent get function
set(new_export_file):
if new_export_file != export_file:
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():
return
if fgd_output_folder.is_empty():
fgd_output_folder = FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.FGD_OUTPUT_FOLDER) as String
if fgd_output_folder.is_empty():
print("Skipping export: No game config folder")
return
if fgd_name == "":
print("Skipping export: Empty FGD name")
var fgd_file = fgd_output_folder + "/" + fgd_name + ".fgd"
print("Exporting FGD to ", fgd_file)
var file_obj := FileAccess.open(fgd_file, FileAccess.WRITE)
file_obj.store_string(build_class_text(target_editor))
file_obj.close()
@export_group("Map Editor")
## Some map editors do not support the features found in others
## (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.
@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")
## FGD output filename without the extension.
@export var fgd_name: String = "FuncGodot"
## Array of [FuncGodotFGDFile] resources to include in FGD file output. All of the entities included with these FuncGodotFGDFile resources will be prepended to the outputted FGD file.
@export var base_fgd_files: Array[Resource] = []
## 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] = []
func build_class_text(target_editor: FuncGodotTargetMapEditors = FuncGodotTargetMapEditors.TRENCHBROOM) -> String:
var res : String = ""
for base_fgd in base_fgd_files:
if base_fgd is FuncGodotFGDFile:
res += base_fgd.build_class_text(target_editor)
var entities = get_fgd_classes()
for ent in entities:
if not ent is FuncGodotFGDEntityClass:
continue
if ent.func_godot_internal:
continue
var ent_text = ent.build_def_text(target_editor)
res += ent_text
if ent != entities[-1]:
res += "\n"
return res
## This getter does a little bit of validation. Providing only an array of non-null uniquely-named entity definitions
func get_fgd_classes() -> Array:
var res : Array = []
for cur_ent_def_ind in range(entity_definitions.size()):
var cur_ent_def = entity_definitions[cur_ent_def_ind]
if cur_ent_def == null:
continue
elif not (cur_ent_def is FuncGodotFGDEntityClass):
printerr("Bad value in entity definition set at position %s! Not an entity defintion." % cur_ent_def_ind)
continue
res.append(cur_ent_def)
return res
func get_entity_definitions() -> Dictionary:
var res : Dictionary = {}
for base_fgd in base_fgd_files:
var fgd_res = base_fgd.get_entity_definitions()
for key in fgd_res:
res[key] = fgd_res[key]
for ent in get_fgd_classes():
# Skip entities without classnames
if ent.classname.replace(" ","") == "":
printerr("Skipping " + ent.get_path() + ": Empty classname")
continue
if ent is FuncGodotFGDPointClass or ent is FuncGodotFGDSolidClass:
var entity_def = ent.duplicate()
var meta_properties := {}
var class_properties := {}
var class_property_descriptions := {}
for base_class in _generate_base_class_list(entity_def):
for meta_property in base_class.meta_properties:
meta_properties[meta_property] = base_class.meta_properties[meta_property]
for class_property in base_class.class_properties:
class_properties[class_property] = base_class.class_properties[class_property]
for class_property_desc in base_class.class_property_descriptions:
class_property_descriptions[class_property_desc] = base_class.class_property_descriptions[class_property_desc]
for meta_property in entity_def.meta_properties:
meta_properties[meta_property] = entity_def.meta_properties[meta_property]
for class_property in entity_def.class_properties:
class_properties[class_property] = entity_def.class_properties[class_property]
for class_property_desc in entity_def.class_property_descriptions:
class_property_descriptions[class_property_desc] = entity_def.class_property_descriptions[class_property_desc]
entity_def.meta_properties = meta_properties
entity_def.class_properties = class_properties
entity_def.class_property_descriptions = class_property_descriptions
res[ent.classname] = entity_def
return res
func _generate_base_class_list(entity_def : Resource, visited_base_classes = []) -> Array:
var base_classes : Array = []
visited_base_classes.append(entity_def.classname)
# End recursive search if no more base_classes
if len(entity_def.base_classes) == 0:
return base_classes
# Traverse up to the next level of hierarchy, if not already visited
for base_class in entity_def.base_classes:
if not base_class.classname in visited_base_classes:
base_classes.append(base_class)
base_classes += _generate_base_class_list(base_class, visited_base_classes)
else:
printerr(str("Entity '", entity_def.classname,"' contains cycle/duplicate to Entity '", base_class.classname, "'"))
return base_classes

View File

@ -0,0 +1,162 @@
@tool
## A special type of [FuncGodotFGDPointClass] entity that can automatically generate a special simplified GLB model file for the map editor display.
## Only supported in map editors that support GLTF or GLB.
class_name FuncGodotFGDModelPointClass
extends FuncGodotFGDPointClass
enum TargetMapEditor {
GENERIC,
TRENCHBROOM
}
## Determines how model interprets [member scale_expression].
@export var target_map_editor: TargetMapEditor = TargetMapEditor.GENERIC
## Display model export folder relative to the model folder set by [FuncGodotLocalConfig].
@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.
@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!
@export var generate_size_property : bool = false
## 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 :
set(ignore):
var path: String = _get_game_path().path_join(_get_model_folder())
var error: Error = DirAccess.make_dir_recursive_absolute(path)
if error != Error.OK:
printerr("Failed creating dir for GDIgnore file", error)
return
path = path.path_join('.gdignore')
if FileAccess.file_exists(path):
return
var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
file.store_string('')
file.close()
func build_def_text(target_editor: FuncGodotFGDFile.FuncGodotTargetMapEditors = FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM) -> String:
_generate_model()
return super()
func _generate_model() -> void:
if not scene_file:
return
var gltf_state := GLTFState.new()
var path = _get_export_dir()
var node = _get_node()
if node == null: return
if not _create_gltf_file(gltf_state, path, node):
printerr("could not create gltf file")
return
node.queue_free()
if target_map_editor == TargetMapEditor.TRENCHBROOM:
const model_key: String = "model"
if scale_expression.is_empty():
meta_properties[model_key] = '"%s"' % _get_local_path()
else:
meta_properties[model_key] = '{"path": "%s", "scale": %s }' % [
_get_local_path(),
scale_expression
]
else:
meta_properties["studio"] = '"%s"' % _get_local_path()
if generate_size_property:
meta_properties["size"] = _generate_size_from_aabb(gltf_state.meshes)
func _get_node() -> Node3D:
var node := scene_file.instantiate()
if node is Node3D:
return node as Node3D
node.queue_free()
printerr("Scene is not of type 'Node3D'")
return null
func _get_export_dir() -> String:
var work_dir: String = _get_game_path()
var model_dir: String = _get_model_folder()
return work_dir.path_join(model_dir).path_join('%s.glb' % classname)
func _get_local_path() -> String:
return _get_model_folder().path_join('%s.glb' % classname)
func _get_model_folder() -> String:
var model_dir: String = FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.GAME_PATH_MODELS_FOLDER) as String
if not models_sub_folder.is_empty():
model_dir = model_dir.path_join(models_sub_folder)
return model_dir
func _get_game_path() -> String:
return FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.MAP_EDITOR_GAME_PATH) as String
func _create_gltf_file(gltf_state: GLTFState, path: String, node: Node3D) -> bool:
var global_export_path = path
var gltf_document := GLTFDocument.new()
gltf_state.create_animations = false
node.rotate_y(deg_to_rad(-90))
# 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:
var scale_factor: Vector3 = Vector3.ONE
if scale_expression.is_empty():
scale_factor *= FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.DEFAULT_INVERSE_SCALE) as float
else:
if scale_expression.begins_with('\''):
var scale_arr := scale_expression.split_floats(' ', false)
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()
if scale_factor.length() == 0:
scale_factor = Vector3.ONE # Don't let the node scale into oblivion!
node.scale *= scale_factor
var error: Error = gltf_document.append_from_scene(node, gltf_state)
if error != Error.OK:
printerr("Failed appending to gltf document", error)
return false
call_deferred("_save_to_file_system", gltf_document, gltf_state, global_export_path)
return true
func _save_to_file_system(gltf_document: GLTFDocument, gltf_state: GLTFState, path: String) -> void:
var error: Error = DirAccess.make_dir_recursive_absolute(path.get_base_dir())
if error != Error.OK:
printerr("Failed creating dir", error)
return
error = gltf_document.write_to_filesystem(gltf_state, path)
if error != OK:
printerr("Failed writing to file system", error)
return
print('Exported model to ', path)
func _generate_size_from_aabb(meshes: Array[GLTFMesh]) -> AABB:
var aabb := AABB()
for mesh in meshes:
aabb = aabb.merge(mesh.mesh.get_mesh().get_aabb())
# Reorient the AABB so it matches TrenchBroom's coordinate system
var size_prop := AABB()
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)
# Scale the size bounds to our scale factor
# Scale factor will need to be set if we decide to auto-generate our bounds
var scale_factor: Vector3 = Vector3.ONE
if target_map_editor == TargetMapEditor.TRENCHBROOM:
if scale_expression.begins_with('\''):
var scale_arr := scale_expression.split_floats(' ', false)
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.size *= scale_factor
size_prop.size += size_prop.position
# Round the size so it can stay on grid level 1 at least
for i in 3:
size_prop.position[i] = round(size_prop.position[i])
size_prop.size[i] = round(size_prop.size[i])
return size_prop

View File

@ -0,0 +1,20 @@
@tool
## FGD PointClass entity definition, used to define point entities.
## 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
func _init() -> void:
prefix = "@PointClass"
@export_group ("Scene")
## An optional scene file to instantiate on map build. Overrides `node_class` and `script_class`.
@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 var script_class: Script
@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.
@export var apply_rotation_on_map_build : bool = true

View File

@ -0,0 +1,59 @@
@tool
## FGD SolidClass entity definition, used to define brush entities.
## 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
enum SpawnType {
WORLDSPAWN = 0, ## Is worldspawn
MERGE_WORLDSPAWN = 1, ## Should be combined with worldspawn
ENTITY = 2, ## Is its own separate entity
}
enum OriginType {
IGNORE = 0, ## Ignore origin property and only use averaged brush vertices for positioning. Standard Quake 1 / Half-Life behavior.
ABSOLUTE = 1, ## Use origin property for position center, ignoring brush vertice positions.
RELATIVE = 2 ## Use origin relative to averaged brush vertice positions. Use this setting if brush entity vertices have coordinates local to the origin.
}
enum CollisionShapeType {
NONE, ## No collision shape is built. Useful for decorative geometry like vines, hanging wires, grass, etc...
CONVEX, ## Will build a Convex CollisionShape3D for each brush used to make this Solid Class. Required for non-[StaticBody3D] nodes like [Area3D].
CONCAVE ## Should have a concave collision shape
}
## 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
## Controls how this Solid Class utilizes the `origin` key value pair to find its position.
@export var origin_type: OriginType = OriginType.IGNORE
@export_group("Visual Build")
## Controls whether a [MeshInstance3D] is built for this Solid Class.
@export var build_visuals : bool = true
## Sets generated [MeshInstance3D] to be available for UV2 unwrapping after [FuncGodotMap] build. Utilized in baked lightmapping.
@export var use_in_baked_light : bool = true
## Shadow casting setting allows for further lightmapping customization.
@export var shadow_casting_setting : GeometryInstance3D.ShadowCastingSetting = GeometryInstance3D.SHADOW_CASTING_SETTING_ON
## Automatically build [OccluderInstance3D] for this entity.
@export var build_occlusion : bool = false
## This Solid Class' [MeshInstance3D] will only be visible for [Camera3D]s whose cull mask includes any of these render layers.
@export_flags_3d_render var render_layers: int = 1
@export_group("Collision Build")
## Controls how collisions are built for this Solid Class.
@export var collision_shape_type: CollisionShapeType = CollisionShapeType.CONVEX
## The physics layers this Solid Class can be detected in.
@export_flags_3d_physics var collision_layer: int = 1
## The physics layers this Solid Class scans.
@export_flags_3d_physics var collision_mask: int = 1
## The priority used to solve colliding when penetration occurs. The higher the priority is, the lower the penetration into the Solid Class will be. This can for example be used to prevent the player from breaking through the boundaries of a level.
@export var collision_priority: float = 1.0
## 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_group("Scripting")
## An optional script file to attach to the node generated on map build.
@export var script_class: Script
func _init():
prefix = "@SolidClass"

View File

@ -0,0 +1,176 @@
@tool
class_name FuncGodotPlugin
extends EditorPlugin
var map_import_plugin : QuakeMapImportPlugin = null
var palette_import_plugin : QuakePaletteImportPlugin = null
var wad_import_plugin: QuakeWadImportPlugin = null
var func_godot_map_control: Control = null
var func_godot_map_progress_bar: Control = null
var edited_object_ref: WeakRef = weakref(null)
func _get_plugin_name() -> String:
return "FuncGodot"
func _handles(object: Object) -> bool:
return object is FuncGodotMap
func _edit(object: Object) -> void:
edited_object_ref = weakref(object)
func _make_visible(visible: bool) -> void:
if func_godot_map_control:
func_godot_map_control.set_visible(visible)
if func_godot_map_progress_bar:
func_godot_map_progress_bar.set_visible(visible)
func _enter_tree() -> void:
# Import plugins
map_import_plugin = QuakeMapImportPlugin.new()
palette_import_plugin = QuakePaletteImportPlugin.new()
wad_import_plugin = QuakeWadImportPlugin.new()
add_import_plugin(map_import_plugin)
add_import_plugin(palette_import_plugin)
add_import_plugin(wad_import_plugin)
# FuncGodotMap button
func_godot_map_control = create_func_godot_map_control()
func_godot_map_control.set_visible(false)
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)
func _exit_tree() -> void:
remove_custom_type("FuncGodotMap")
remove_import_plugin(map_import_plugin)
remove_import_plugin(palette_import_plugin)
if wad_import_plugin:
remove_import_plugin(wad_import_plugin)
map_import_plugin = null
palette_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()
icon.texture = preload("res://addons/func_godot/icons/icon_slipgate3d.svg")
icon.size_flags_vertical = Control.SIZE_SHRINK_CENTER
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]
func create_func_godot_map_progress_bar() -> Control:
var progress_label = Label.new()
progress_label.name = "ProgressLabel"
progress_label.horizontal_alignment = HORIZONTAL_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
## 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)
if not edited_object.is_connected("unwrap_uv2_complete", func_godot_map_build_complete):
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.
func func_godot_map_build_complete(func_godot_map: FuncGodotMap) -> void:
var progress_label = func_godot_map_progress_bar.get_node("ProgressLabel")
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_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")):
func_godot_map.disconnect("build_failed",Callable(self,"func_godot_map_build_complete"))

View File

@ -0,0 +1,6 @@
@icon("res://addons/func_godot/icons/icon_quake_file.svg")
class_name QuakeMapFile
extends Resource
@export var revision: int = 0
@export_multiline var map_data: String = ""

View File

@ -0,0 +1,47 @@
@tool
class_name QuakeMapImportPlugin
extends EditorImportPlugin
# Quake super.map import plugin
func _get_importer_name() -> String:
return 'func_godot.map'
func _get_visible_name() -> String:
return 'Quake Map'
func _get_resource_type() -> String:
return 'Resource'
func _get_recognized_extensions() -> PackedStringArray:
return PackedStringArray(['map'])
func _get_priority():
return 1.0
func _get_save_extension() -> String:
return 'tres'
func _get_import_options(path, preset):
return []
func _get_preset_count() -> int:
return 0
func _get_import_order():
return 0
func _import(source_file, save_path, options, r_platform_variants, r_gen_files) -> Error:
var save_path_str = '%s.%s' % [save_path, _get_save_extension()]
var map_resource : QuakeMapFile = null
var existing_resource := load(save_path_str) as QuakeMapFile
if(existing_resource != null):
map_resource = existing_resource
map_resource.revision += 1
else:
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)

View File

@ -0,0 +1,8 @@
@icon("res://addons/func_godot/icons/icon_quake_file.svg")
class_name QuakePaletteFile
extends Resource
@export var colors: PackedColorArray
func _init(colors):
self.colors = colors

View File

@ -0,0 +1,61 @@
@tool
class_name QuakePaletteImportPlugin
extends EditorImportPlugin
# Quake super.map import plugin
func _get_importer_name() -> String:
return 'func_godot.palette'
func _get_visible_name() -> String:
return 'Quake Palette'
func _get_resource_type() -> String:
return 'Resource'
func _get_recognized_extensions() -> PackedStringArray:
return PackedStringArray(['lmp'])
func _get_save_extension() -> String:
return 'tres'
func _get_import_options(path, preset):
return []
func _get_preset_count() -> int:
return 0
func _get_priority():
return 1.0
func _get_import_order():
return 0
func _import(source_file, save_path, options, r_platform_variants, r_gen_files) -> Error:
var save_path_str : String = '%s.%s' % [save_path, _get_save_extension()]
var file = FileAccess.open(source_file, FileAccess.READ)
if file == null:
var err = FileAccess.get_open_error()
print(['Error opening super.lmp file: ', err])
return err
var colors := PackedColorArray()
while true:
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)
colors.append(color)
if file.eof_reached():
break
if colors.size() == 256:
break
var palette_resource := QuakePaletteFile.new(colors)
return ResourceSaver.save(palette_resource, save_path_str)

View File

@ -0,0 +1,8 @@
@icon("res://addons/func_godot/icons/icon_quake_file.svg")
class_name QuakeWadFile
extends Resource
@export var textures: Dictionary
func _init(textures: Dictionary):
self.textures = textures

View File

@ -0,0 +1,209 @@
@tool
class_name QuakeWadImportPlugin extends EditorImportPlugin
enum WadFormat {
Quake,
HalfLife
}
enum QuakeWadEntryType {
Palette = 0x40,
SBarPic = 0x42,
MipsTexture = 0x44,
ConsolePic = 0x45
}
enum HalfLifeWadEntryType {
QPic = 0x42,
MipsTexture = 0x43,
FixedFont = 0x45
}
const TEXTURE_NAME_LENGTH := 16
const MAX_MIP_LEVELS := 4
func _get_importer_name() -> String:
return 'func_godot.wad'
func _get_visible_name() -> String:
return 'Texture2D WAD'
func _get_resource_type() -> String:
return 'Resource'
func _get_recognized_extensions() -> PackedStringArray:
return PackedStringArray(['wad'])
func _get_save_extension() -> String:
return 'res'
func _get_option_visibility(path: String, option_name: StringName, options: Dictionary) -> bool:
return true
func _get_import_options(path, preset) -> Array[Dictionary]:
return [
{
'name': 'palette_file',
'default_value': 'res://addons/func_godot/palette.lmp',
'property_hint': PROPERTY_HINT_FILE,
'hint_string': '*.lmp'
},
{
'name': 'generate_mipmaps',
'default_value': true,
'property_hint': PROPERTY_HINT_NONE
}
]
func _get_preset_count() -> int:
return 0
func _get_import_order() -> int:
return 0
func _get_priority() -> float:
return 1.0
func _import(source_file, save_path, options, r_platform_variants, r_gen_files) -> Error:
var save_path_str : String = '%s.%s' % [save_path, _get_save_extension()]
var file = FileAccess.open(source_file, FileAccess.READ)
if file == null:
var err = FileAccess.get_open_error()
print(['Error opening super.wad file: ', err])
return err
# Read WAD header
var magic : PackedByteArray = file.get_buffer(4)
var magic_string : String = magic.get_string_from_ascii()
var wad_format: int = WadFormat.Quake
if magic_string == 'WAD3':
wad_format = WadFormat.HalfLife
elif magic_string != 'WAD2':
print('Error: Invalid WAD magic')
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 dir_offset : int = file.get_32()
# Read entry list
file.seek(0)
file.seek(dir_offset)
var entries : Array = []
for entry_idx in range(0, num_entries):
var offset : int = file.get_32()
var in_wad_size : int = file.get_32()
var size : int = file.get_32()
var type : int = file.get_8()
var compression : int = file.get_8()
var unknown : int = file.get_16()
var name : PackedByteArray = file.get_buffer(TEXTURE_NAME_LENGTH)
var name_string : String = name.get_string_from_ascii()
if (wad_format == WadFormat.Quake and type == int(QuakeWadEntryType.MipsTexture)) or (
wad_format == WadFormat.HalfLife and type == int(HalfLifeWadEntryType.MipsTexture)):
entries.append([
offset,
in_wad_size,
size,
type,
compression,
name_string
])
# Read mip textures
var texture_data_array: Array = []
for entry in entries:
var offset : int = entry[0]
file.seek(offset)
var name : PackedByteArray = file.get_buffer(TEXTURE_NAME_LENGTH)
var name_string : String = name.get_string_from_ascii()
var width : int = file.get_32()
var height : int = file.get_32()
var mip_offsets : Array = []
for idx in range(0, MAX_MIP_LEVELS):
mip_offsets.append(file.get_32())
var num_pixels : int = width * height
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
var textures : Dictionary = {}
for texture_data in texture_data_array:
var name : String = texture_data[0]
var width : int = texture_data[1]
var height : int = texture_data[2]
var pixels : PackedByteArray = texture_data[3]
var texture_image : Image
var pixels_rgb := PackedByteArray()
if wad_format == WadFormat.HalfLife:
var colors : PackedColorArray = texture_data[4]
for palette_color in pixels:
var rgb_color : Color = colors[palette_color]
pixels_rgb.append(rgb_color.r8)
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:
texture_image.generate_mipmaps()
var texture := ImageTexture.create_from_image(texture_image) #,Texture2D.FLAG_MIPMAPS | Texture2D.FLAG_REPEAT | Texture2D.FLAG_ANISOTROPIC_FILTER
textures[name] = texture
# Save WAD resource
var wad_resource := QuakeWadFile.new(textures)
return ResourceSaver.save(wad_resource, save_path_str)

View File

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

View File

@ -0,0 +1,79 @@
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
## 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.
@export var inverse_scale_factor: float = 32.0
## [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")
## Default class property to use in naming generated nodes. This setting is overridden by `name_property` in [FuncGodotFGDEntityClass].
## 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.
@export var entity_name_property: String = ""
@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.
@export_dir var base_texture_dir: String = "res://textures"
## File extensions to search for texture data.
@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].
@export var clip_texture: String = "special/clip"
## 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].
@export var skip_texture: String = "special/skip"
## 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[Resource] = []
@export_category("Materials")
## File extension to search for [Material] definitions
@export var material_file_extension: String = "tres"
## [Material] used as template when generating missing materials.
@export var default_material: Material = preload("res://addons/func_godot/textures/default_material.tres")
## Sampler2D uniform that supplies the Albedo in a custom shader when [member default_material] is a [ShaderMaterial].
@export var default_material_albedo_uniform: String = ""
## Automatic PBR material generation albedo map pattern.
@export var albedo_map_pattern: String = "%s_albedo.%s"
## Automatic PBR material generation normal map pattern.
@export var normal_map_pattern: String = "%s_normal.%s"
## Automatic PBR material generation metallic map pattern
@export var metallic_map_pattern: String = "%s_metallic.%s"
## Automatic PBR material generation roughness map pattern
@export var roughness_map_pattern: String = "%s_roughness.%s"
## Automatic PBR material generation emission map pattern
@export var emission_map_pattern: String = "%s_emission.%s"
## Automatic PBR material generation ambient occlusion map pattern
@export var ao_map_pattern: String = "%s_ao.%s"
## Automatic PBR material generation height map pattern
@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".
@export var unshaded: bool = false
## 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_category("UV Unwrap")
## Texel size for UV2 unwrapping.
## Actual texel size is uv_unwrap_texel_size / inverse_scale_factor. A ratio of 1/16 is usually a good place to start with (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 var uv_unwrap_texel_size: float = 2.0
@export_category("TrenchBroom")
## 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

@ -0,0 +1,273 @@
@tool
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
## Builds a gamepack for NetRadiant Custom.
class_name NetRadiantCustomGamePackConfig
extends Resource
## Button to export / update this gamepack's configuration in the NetRadiant Custom Gamepacks Folder.
@export var export_file: bool:
get:
return export_file
set(new_export_file):
if new_export_file != 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.
@export var gamepack_name : String = "func_godot"
## Name of the game in NetRadiant Custom's gamepack list.
@export var game_name : String = "FuncGodot"
## Directory path containing your maps, textures, shaders, etc... relative to your project directory.
@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.
@export var fgd_file : FuncGodotFGDFile = preload("res://addons/func_godot/fgd/func_godot_fgd.tres")
## [NetRadiantCustomShader] resources for shader file generation.
@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_skip.tres")
]
## Supported texture file types.
@export var texture_types : PackedStringArray = ["png", "jpg", "jpeg", "bmp", "tga"]
## Supported model file types.
@export var model_types : PackedStringArray = ["glb", "gltf", "obj"]
## Supported audio file types.
@export var sound_types : PackedStringArray = ["wav", "ogg"]
## Default scale of textures in NetRadiant Custom.
@export var default_scale : String = "1.0"
## Clip texture path that gets applied to weapclip and nodraw shaders.
@export var clip_texture: String = "textures/special/clip"
## Skip texture path that gets applied to caulk and nodrawnonsolid shaders.
@export var skip_texture: String = "textures/special/skip"
## 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_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 Radiant.[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_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 = ""
for shader_res in netradiant_custom_shaders:
shader_text += (shader_res as NetRadiantCustomShader).texture_path + "\n{\n"
for shader_attrib in (shader_res as NetRadiantCustomShader).shader_attributes:
shader_text += "\t" + shader_attrib + "\n"
shader_text += "}\n"
return shader_text
## Generates completed text for a .gamepack file.
func build_gamepack_text() -> String:
var texturetypes_str: String = ""
for texture_type in texture_types:
texturetypes_str += texture_type
if texture_type != texture_types[-1]:
texturetypes_str += " "
var modeltypes_str: String = ""
for model_type in model_types:
modeltypes_str += model_type
if model_type != model_types[-1]:
modeltypes_str += " "
var soundtypes_str: String = ""
for sound_type in sound_types:
soundtypes_str += sound_type
if sound_type != sound_types[-1]:
soundtypes_str += " "
var gamepack_text: String = """<?xml version="1.0"?>
<game
type="q3"
index="1"
name="%s"
enginepath_win32="C:/%s/"
engine_win32="%s.exe"
enginepath_linux="/usr/local/games/%s/"
engine_linux="%s"
basegame="%s"
basegamename="%s"
unknowngamename="Custom %s modification"
shaderpath="scripts"
archivetypes="pk3"
texturetypes="%s"
modeltypes="%s"
soundtypes="%s"
maptypes="mapq1"
shaders="quake3"
entityclass="halflife"
entityclasstype="fgd"
entities="quake"
brushtypes="quake"
patchtypes="quake3"
q3map2_type="quake3"
default_scale="%s"
shader_weapclip="%s"
shader_caulk="%s"
shader_nodraw="%s"
shader_nodrawnonsolid="%s"
common_shaders_name="Common"
common_shaders_dir="common/"
/>
"""
return gamepack_text % [
game_name,
game_name,
gamepack_name,
game_name,
gamepack_name,
base_game_path,
game_name,
game_name,
texturetypes_str,
modeltypes_str,
soundtypes_str,
default_scale,
clip_texture,
skip_texture,
clip_texture,
skip_texture
]
## Exports or updates a folder in the /games directory, with an icon, .cfg, and all accompanying FGDs.
func do_export_file() -> void:
if (FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.MAP_EDITOR_GAME_PATH) as String).is_empty():
printerr("Skipping export: Map Editor Game Path not set in Project Configuration")
return
var gamepacks_folder: String = FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.NETRADIANT_CUSTOM_GAMEPACKS_FOLDER) as String
if gamepacks_folder.is_empty():
printerr("Skipping export: No NetRadiant Custom gamepacks folder")
return
# Make sure FGD file is set
if !fgd_file:
printerr("Skipping export: No FGD file")
return
# Make sure we're actually in the NetRadiant Custom gamepacks folder
if DirAccess.open(gamepacks_folder + "/games") == null:
printerr("Skipping export: No \'games\' folder. Is this the NetRadiant Custom gamepacks folder?")
return
# Create gamepack folders in case they do not exist
var gamepack_dir_paths: Array = [
gamepacks_folder + "/" + gamepack_name + ".game",
gamepacks_folder + "/" + gamepack_name + ".game/" + base_game_path,
gamepacks_folder + "/" + gamepack_name + ".game/scripts"
]
var err: Error
for path in gamepack_dir_paths:
if DirAccess.open(path) == null:
print("Couldn't open " + path + ", creating...")
err = DirAccess.make_dir_recursive_absolute(path)
if err != OK:
printerr("Skipping export: Failed to create directory")
return
var target_file_path: String
var file: FileAccess
# .gamepack
target_file_path = gamepacks_folder + "/games/" + gamepack_name + ".game"
print("Exporting NetRadiant Custom Gamepack to ", target_file_path)
file = FileAccess.open(target_file_path, FileAccess.WRITE)
if file != null:
file.store_string(build_gamepack_text())
file.close()
else:
printerr("Error: Could not modify " + target_file_path)
# .shader
target_file_path = gamepacks_folder + "/" + gamepack_name + ".game/scripts/" + gamepack_name + ".shader"
print("Exporting NetRadiant Custom Shader to ", target_file_path)
file = FileAccess.open(target_file_path, FileAccess.WRITE)
if file != null:
file.store_string(build_shader_text())
file.close()
else:
printerr("Error: Could not modify " + target_file_path)
# shaderlist.txt
target_file_path = gamepacks_folder + "/" + gamepack_name + ".game/scripts/shaderlist.txt"
print("Exporting NetRadiant Custom Default Buld Menu to ", target_file_path)
file = FileAccess.open(target_file_path, FileAccess.WRITE)
if file != null:
file.store_string(gamepack_name)
file.close()
else:
printerr("Error: Could not modify " + target_file_path)
# 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)
file = FileAccess.open(target_file_path, FileAccess.WRITE)
if file != null:
file.store_string("<?xml version=\"1.0\"?>\n<project version=\"2.0\">\n")
for key in default_build_menu_variables.keys():
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
var export_fgd : FuncGodotFGDFile = fgd_file.duplicate()
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")

View File

@ -0,0 +1,10 @@
@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
extends Resource
## Path to texture without extension, eg: `textures/special/clip`.
@export var texture_path: String
## Array of shader properties to apply to faces using [member texture_path].
@export var shader_attributes : Array[String] = ["qer_trans 0.4"]

View File

@ -0,0 +1,221 @@
@tool
@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
## Button to export / update this game's configuration and FGD file in the TrenchBroom Games Path.
@export var export_file: bool:
get:
return export_file
set(new_export_file):
if new_export_file != export_file:
if Engine.is_editor_hint():
do_export_file()
## Name of the game in TrenchBroom's game list.
@export var game_name : String = "FuncGodot"
## Icon for TrenchBroom's game list.
@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.
@export var map_formats: Array[Dictionary] = [
{ "format": "Valve", "initialmap": "initial_valve.map" },
{ "format": "Standard", "initialmap": "initial_standard.map" },
{ "format": "Quake2", "initialmap": "initial_quake2.map" },
{ "format": "Quake3" }
]
## 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"]
## 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.
@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.
@export var entity_scale: String = "32"
## Scale of textures on new brushes.
@export var default_uv_scale : Vector2 = Vector2(1, 1)
## Arrays containing the TrenchBroomTag resource type.
@export_category("Editor Hint Tags")
## TrenchBroomTag resources that apply to brush entities.
@export var brush_tags : Array[Resource] = []
## TrenchBroomTag resources that apply to brush faces.
@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_skip.tres")
]
## 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:
TrenchBroomTag.TagMatchType.TEXTURE:
return "texture"
TrenchBroomTag.TagMatchType.CLASSNAME:
return "classname"
_:
push_error("Tag match type %s is not valid" % [tag_match_type])
return "ERROR"
## Generates completed text for a .cfg file.
func build_class_text() -> String:
var map_formats_str : String = ""
for map_format in map_formats:
map_formats_str += "{ \"format\": \"" + map_format.format + "\""
if map_format.has("initialmap"):
map_formats_str += ", \"initialmap\": \"" + map_format.initialmap + "\""
if map_format != map_formats[-1]:
map_formats_str += " },\n\t\t"
else:
map_formats_str += " }"
var texture_exclusion_patterns_str := ""
for tex_pattern in texture_exclusion_patterns:
texture_exclusion_patterns_str += "\"" + tex_pattern + "\""
if tex_pattern != texture_exclusion_patterns[-1]:
texture_exclusion_patterns_str += ", "
var fgd_filename_str : String = "\"" + fgd_file.fgd_name + ".fgd\""
var brush_tags_str = parse_tags(brush_tags)
var brushface_tags_str = parse_tags(brushface_tags)
var uv_scale_str = parse_default_uv_scale(default_uv_scale)
var config_text : String = """{
"version": 8,
"name": "%s",
"icon": "icon.png",
"fileformats": [
%s
],
"filesystem": {
"searchpath": ".",
"packageformat": { "extension": ".zip", "format": "zip" }
},
"textures": {
"root": "textures",
"extensions": [".bmp", ".exr", ".hdr", ".jpeg", ".jpg", ".png", ".tga", ".webp"],
"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.
func parse_tags(tags: Array) -> String:
var tags_str := ""
for brush_tag in tags:
if brush_tag.tag_match_type >= TrenchBroomTag.TagMatchType.size():
continue
tags_str += "{\n"
tags_str += "\t\t\t\t\"name\": \"%s\",\n" % brush_tag.tag_name
var attribs_str := ""
for brush_tag_attrib in brush_tag.tag_attributes:
attribs_str += "\"%s\"" % brush_tag_attrib
if brush_tag_attrib != brush_tag.tag_attributes[-1]:
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\"pattern\": \"%s\"" % brush_tag.tag_pattern
if brush_tag.texture_name != "":
tags_str += ",\n"
tags_str += "\t\t\t\t\"texture\": \"%s\"" % brush_tag.texture_name
tags_str += "\n"
tags_str += "\t\t\t}"
if brush_tag != tags[-1]:
tags_str += ","
return tags_str
## Converts array of flags to .cfg String.
func parse_flags(flags: Array) -> String:
var flags_str := ""
for attrib_flag in flags:
flags_str += "{\n"
flags_str += "\t\t\t\t\"name\": \"%s\",\n" % attrib_flag.attrib_name
flags_str += "\t\t\t\t\"description\": \"%s\"\n" % attrib_flag.attrib_description
flags_str += "\t\t\t}"
if attrib_flag != flags[-1]:
flags_str += ","
return flags_str
## Converts default uv scale vector to .cfg String.
func parse_default_uv_scale(texture_scale : Vector2) -> String:
var entry_str = "\"scale\": [{x}, {y}]"
return entry_str.format({
"x": texture_scale.x,
"y": texture_scale.y
})
## Exports or updates a folder in the /games directory, with an icon, .cfg, and all accompanying FGDs.
func do_export_file() -> void:
var config_folder: String = FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.TRENCHBROOM_GAME_CONFIG_FOLDER) as String
if config_folder.is_empty():
printerr("Skipping export: No TrenchBroom Game folder")
return
# Make sure FGD file is set
if !fgd_file:
printerr("Skipping export: No FGD file")
return
var config_dir := DirAccess.open(config_folder)
# Create config folder in case it does not exist
if config_dir == null:
print("Couldn't open directory, creating...")
var err := DirAccess.make_dir_recursive_absolute(config_folder)
if err != OK:
printerr("Skipping export: Failed to create directory")
return
# Icon
var icon_path : String = config_folder + "/icon.png"
print("Exporting icon to ", icon_path)
var export_icon : Image = icon.get_image()
export_icon.resize(32, 32, Image.INTERPOLATE_LANCZOS)
export_icon.save_png(icon_path)
# .cfg
var target_file_path: String = config_folder + "/GameConfig.cfg"
print("Exporting TrenchBroom Game Config to ", target_file_path)
var file = FileAccess.open(target_file_path, FileAccess.WRITE)
file.store_string(build_class_text())
file.close()
# FGD
var export_fgd : FuncGodotFGDFile = fgd_file.duplicate()
export_fgd.do_export_file(FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM, config_folder)
print("TrenchBroom Game Config export complete\n")

View File

@ -0,0 +1,26 @@
@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.
## 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.
class_name TrenchBroomTag
extends Resource
enum TagMatchType {
TEXTURE, ## Tag applies to any brush face with a texture matching the texture name.
CLASSNAME ## Tag applies to any brush entity with a class name matching the tag pattern.
}
## Name to define this tag. Not used as the matching pattern.
@export var tag_name: String
## The attributes applied to matching faces or brush entities. Only "_transparent" is supported in TrenchBroom, which makes matching faces or brush entities transparent.
@export var tag_attributes : Array[String] = ["transparent"]
## Determines how the tag is matched. See [constant 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.
## [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
## A string that filters which textures recieve these attributes. Only used with a [constant TagMatchType] of [i]Texture[/i].
@export var texture_name: String

View File

@ -0,0 +1,142 @@
@tool
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
## 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.
## Use the premade `addons/func_godot/func_godot_local_config.tres` instead.
class_name FuncGodotLocalConfig
extends Resource
enum PROPERTY {
FGD_OUTPUT_FOLDER,
TRENCHBROOM_GAME_CONFIG_FOLDER,
NETRADIANT_CUSTOM_GAMEPACKS_FOLDER,
MAP_EDITOR_GAME_PATH,
GAME_PATH_MODELS_FOLDER,
DEFAULT_INVERSE_SCALE
}
@export var export_func_godot_settings: bool: set = _save_settings
@export var reload_func_godot_settings: bool = false :
set(value):
_load_settings()
const CONFIG_PROPERTIES: Array[Dictionary] = [
{
"name": "fgd_output_folder",
"usage": PROPERTY_USAGE_EDITOR,
"type": TYPE_STRING,
"hint": PROPERTY_HINT_GLOBAL_DIR,
"func_godot_type": PROPERTY.FGD_OUTPUT_FOLDER
},
{
"name": "trenchbroom_game_config_folder",
"usage": PROPERTY_USAGE_EDITOR,
"type": TYPE_STRING,
"hint": PROPERTY_HINT_GLOBAL_DIR,
"func_godot_type": PROPERTY.TRENCHBROOM_GAME_CONFIG_FOLDER
},
{
"name": "netradiant_custom_gamepacks_folder",
"usage": PROPERTY_USAGE_EDITOR,
"type": TYPE_STRING,
"hint": PROPERTY_HINT_GLOBAL_DIR,
"func_godot_type": PROPERTY.NETRADIANT_CUSTOM_GAMEPACKS_FOLDER
},
{
"name": "map_editor_game_path",
"usage": PROPERTY_USAGE_EDITOR,
"type": TYPE_STRING,
"hint": PROPERTY_HINT_GLOBAL_DIR,
"func_godot_type": PROPERTY.MAP_EDITOR_GAME_PATH
},
{
"name": "game_path_models_folder",
"usage": PROPERTY_USAGE_EDITOR,
"type": TYPE_STRING,
"func_godot_type": PROPERTY.GAME_PATH_MODELS_FOLDER
},
{
"name": "default_inverse_scale_factor",
"usage": PROPERTY_USAGE_EDITOR,
"type": TYPE_FLOAT,
"func_godot_type": PROPERTY.DEFAULT_INVERSE_SCALE
}
]
var settings_dict: Dictionary
var loaded := false
static func get_setting(name: PROPERTY) -> Variant:
var settings = load("res://addons/func_godot/func_godot_local_config.tres")
if not settings.loaded:
settings._load_settings()
return settings.settings_dict.get(PROPERTY.keys()[name], '') as Variant
func _get_property_list() -> Array:
return CONFIG_PROPERTIES.duplicate()
func _get(property: StringName) -> Variant:
var config = _get_config_property(property)
if config == null and not config is Dictionary:
return null
_try_loading()
return settings_dict.get(PROPERTY.keys()[config['func_godot_type']], _get_default_value(config['type']))
func _set(property: StringName, value: Variant) -> bool:
var config = _get_config_property(property)
if config == null and not config is Dictionary:
return false
settings_dict[PROPERTY.keys()[config['func_godot_type']]] = value
return true
func _get_default_value(type) -> Variant:
match type:
TYPE_STRING: return ''
TYPE_INT: return 0
TYPE_FLOAT: return 0.0
TYPE_BOOL: return false
TYPE_VECTOR2: return Vector2.ZERO
TYPE_VECTOR3: return Vector3.ZERO
TYPE_ARRAY: return []
TYPE_DICTIONARY: return {}
push_error("Invalid setting type. Returning null")
return null
func _get_config_property(name: StringName) -> Variant:
for config in CONFIG_PROPERTIES:
if config['name'] == name:
return config
return null
func _load_settings() -> void:
loaded = true
var path = _get_path()
if not FileAccess.file_exists(path):
return
var settings = FileAccess.get_file_as_string(path)
settings_dict = {}
if not settings or settings.is_empty():
return
settings = JSON.parse_string(settings)
for key in settings.keys():
settings_dict[key] = settings[key]
notify_property_list_changed()
func _try_loading() -> void:
if not loaded:
_load_settings()
func _save_settings(_s = null) -> void:
if settings_dict.size() == 0:
return
var path = _get_path()
var file = FileAccess.open(path, FileAccess.WRITE)
var json = JSON.stringify(settings_dict)
file.store_line(json)
loaded = false
print("Saved settings to ", path)
func _get_path() -> String:
var application_name: String = ProjectSettings.get('application/config/name')
application_name = application_name.replace(" ", "_")
return 'user://' + application_name + '_FuncGodotConfig.json'

View File

@ -0,0 +1,190 @@
class_name FuncGodotTextureLoader
enum PBRSuffix {
ALBEDO,
NORMAL,
METALLIC,
ROUGHNESS,
EMISSION,
AO,
HEIGHT,
ORM
}
# 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',
PBRSuffix.ORM: 'orm'
}
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',
PBRSuffix.ORM: '%s_orm.%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,
PBRSuffix.ORM: ORMMaterial3D.TEXTURE_ORM
}
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") or ResourceLoader.exists(texture_path + ".import", "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) or FileAccess.file_exists(material_path + ".remap")):
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)
elif material is ORMMaterial3D:
material.set_texture(ORMMaterial3D.TEXTURE_ALBEDO, 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

@ -0,0 +1,40 @@
## General-purpose utility functions namespaced to FuncGodot for compatibility
class_name FuncGodotUtil
## Print debug messages. True to print, false to ignore
const DEBUG : bool = true
## Const-predicated print function to avoid excess log spam. Print msg if [constant DEBUG] is `true`.
static func debug_print(msg) -> void:
if(DEBUG):
print(msg)
## Return a string that corresponds to the current OS's newline control character(s)
static func newline() -> String:
if OS.get_name() == "Windows":
return "\r\n"
else:
return "\n"
## Create a dictionary suitable for creating a category with name when overriding [method Object._get_property_list]
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].
## Optionally, provide hint from [enum @GlobalScope.PropertyHint] and corresponding hint_string, and usage from [enum @GlobalScope.PropertyUsageFlags].
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:
dict['hint'] = hint
if hint_string != "":
dict['hint_string'] = hint_string
if usage != -1:
dict['usage'] = usage
return dict

View File

@ -0,0 +1,8 @@
[gd_resource type="StandardMaterial3D" load_steps=2 format=3 uid="uid://cvex6toty8yn7"]
[ext_resource type="Texture2D" uid="uid://cyg2snr1w5xw5" path="res://addons/func_godot/textures/default_texture.png" id="1_ncj77"]
[resource]
albedo_texture = ExtResource("1_ncj77")
metallic_specular = 0.0
texture_filter = 2

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cyg2snr1w5xw5"
path="res://.godot/imported/default_texture.png-145fbd5fef7f63ace60797fecb133a19.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/func_godot/textures/default_texture.png"
dest_files=["res://.godot/imported/default_texture.png-145fbd5fef7f63ace60797fecb133a19.ctex"]
[params]
compress/mode=3
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
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=0

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

8
gmtk24.csproj Normal file
View File

@ -0,0 +1,8 @@
<Project Sdk="Godot.NET.Sdk/4.3.0">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework Condition=" '$(GodotTargetPlatform)' == 'android' ">net7.0</TargetFramework>
<TargetFramework Condition=" '$(GodotTargetPlatform)' == 'ios' ">net8.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
</Project>

19
gmtk24.sln Normal file
View File

@ -0,0 +1,19 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2012
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gmtk24", "gmtk24.csproj", "{6BF26BA4-8B1D-4630-B304-3CC3AC40F736}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
ExportDebug|Any CPU = ExportDebug|Any CPU
ExportRelease|Any CPU = ExportRelease|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{6BF26BA4-8B1D-4630-B304-3CC3AC40F736}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6BF26BA4-8B1D-4630-B304-3CC3AC40F736}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6BF26BA4-8B1D-4630-B304-3CC3AC40F736}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
{6BF26BA4-8B1D-4630-B304-3CC3AC40F736}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
{6BF26BA4-8B1D-4630-B304-3CC3AC40F736}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
{6BF26BA4-8B1D-4630-B304-3CC3AC40F736}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
EndGlobalSection
EndGlobal

BIN
maps/demo.exr Normal file

Binary file not shown.

26
maps/demo.exr.import Normal file
View File

@ -0,0 +1,26 @@
[remap]
importer="2d_array_texture"
type="CompressedTexture2DArray"
uid="uid://ujfyssexski7"
path="res://.godot/imported/demo.exr-3842fccf4f3f45bb955d2f579f2e5875.ctexarray"
metadata={
"vram_texture": false
}
[deps]
source_file="res://maps/demo.exr"
dest_files=["res://.godot/imported/demo.exr-3842fccf4f3f45bb955d2f579f2e5875.ctexarray"]
[params]
compress/mode=3
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/channel_pack=1
mipmaps/generate=false
mipmaps/limit=-1
slices/horizontal=1
slices/vertical=1

BIN
maps/demo.lmbake Normal file

Binary file not shown.

300
maps/demo.tscn Normal file

File diff suppressed because one or more lines are too long

28
maps/hazy_env.tscn Normal file
View File

@ -0,0 +1,28 @@
[gd_scene load_steps=5 format=3 uid="uid://br315evr8x2gt"]
[ext_resource type="CameraAttributesPhysical" uid="uid://cxyj2tvfksjl6" path="res://maps/hazy_env_camera_attrs.tres" id="1_84mop"]
[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_2n8bf"]
sky_top_color = Color(0.749822, 0.794765, 0.857504, 1)
sky_horizon_color = Color(1, 1, 1, 1)
ground_bottom_color = Color(1, 1, 1, 1)
ground_horizon_color = Color(1, 1, 1, 1)
[sub_resource type="Sky" id="Sky_jvem5"]
sky_material = SubResource("ProceduralSkyMaterial_2n8bf")
[sub_resource type="Environment" id="Environment_exkg8"]
background_mode = 2
sky = SubResource("Sky_jvem5")
glow_enabled = true
[node name="HazyEnv" type="Node3D"]
[node name="Sun" type="DirectionalLight3D" parent="."]
transform = Transform3D(0.707107, 0.5, -0.5, 0, 0.707107, 0.707107, 0.707107, -0.5, 0.5, 0, 2, 0)
light_angular_distance = 0.5
light_bake_mode = 1
[node name="Env" type="WorldEnvironment" parent="."]
environment = SubResource("Environment_exkg8")
camera_attributes = ExtResource("1_84mop")

View File

@ -0,0 +1,6 @@
[gd_resource type="CameraAttributesPhysical" format=3 uid="uid://cxyj2tvfksjl6"]
[resource]
auto_exposure_enabled = true
auto_exposure_min_exposure_value = 0.5
auto_exposure_max_exposure_value = 1.5

1
misc/icon.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>

After

Width:  |  Height:  |  Size: 994 B

37
misc/icon.svg.import Normal file
View File

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bpwl48we1nsuq"
path="res://.godot/imported/icon.svg-2be59eb5925b98a41040443b64335822.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://misc/icon.svg"
dest_files=["res://.godot/imported/icon.svg-2be59eb5925b98a41040443b64335822.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

35
project.godot Normal file
View File

@ -0,0 +1,35 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[application]
config/name="gmtk24"
run/main_scene="res://maps/demo.tscn"
config/features=PackedStringArray("4.3", "C#", "Forward Plus")
config/icon="res://misc/icon.svg"
[dotnet]
project/assembly_name="gmtk24"
[editor_plugins]
enabled=PackedStringArray("res://addons/func_godot/plugin.cfg")
[importer_defaults]
texture={
"mipmaps/generate": true
}
[rendering]
lights_and_shadows/use_physical_light_units=true
anti_aliasing/quality/msaa_3d=2

BIN
textures/asphalt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 B

View File

@ -0,0 +1,35 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d1qe6nsgnm668"
path.s3tc="res://.godot/imported/asphalt.png-ac61e7ecbfd4001b91872200a6ce765b.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
[deps]
source_file="res://textures/asphalt.png"
dest_files=["res://.godot/imported/asphalt.png-ac61e7ecbfd4001b91872200a6ce765b.s3tc.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
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=0

8
textures/asphalt.tres Normal file
View File

@ -0,0 +1,8 @@
[gd_resource type="StandardMaterial3D" load_steps=2 format=3 uid="uid://biro7l4e6n6vi"]
[ext_resource type="Texture2D" uid="uid://d1qe6nsgnm668" path="res://textures/asphalt.png" id="1_adacw"]
[resource]
albedo_texture = ExtResource("1_adacw")
metallic_specular = 0.0
texture_filter = 2

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 B

View File

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cyg2snr1w5xw5"
path="res://.godot/imported/default_texture.png-881a2e123e5cebffe367630531266e1b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://textures/default_texture.png"
dest_files=["res://.godot/imported/default_texture.png-881a2e123e5cebffe367630531266e1b.ctex"]
[params]
compress/mode=3
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
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=0

View File

@ -0,0 +1,8 @@
[gd_resource type="StandardMaterial3D" load_steps=2 format=3 uid="uid://bs5ogxy0wvvj3"]
[ext_resource type="Texture2D" uid="uid://cyg2snr1w5xw5" path="res://textures/default_texture.png" id="1_u5p53"]
[resource]
albedo_texture = ExtResource("1_u5p53")
metallic_specular = 0.0
texture_filter = 2

BIN
textures/red_metal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 B

View File

@ -0,0 +1,35 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://daehkv152sck7"
path.s3tc="res://.godot/imported/red_metal.png-3b795a61edf3045e2e954b9a4785e55f.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
[deps]
source_file="res://textures/red_metal.png"
dest_files=["res://.godot/imported/red_metal.png-3b795a61edf3045e2e954b9a4785e55f.s3tc.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
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=0

8
textures/red_metal.tres Normal file
View File

@ -0,0 +1,8 @@
[gd_resource type="StandardMaterial3D" load_steps=2 format=3 uid="uid://cqn1kke18jdyo"]
[ext_resource type="Texture2D" uid="uid://daehkv152sck7" path="res://textures/red_metal.png" id="1_hsryr"]
[resource]
albedo_texture = ExtResource("1_hsryr")
metallic_specular = 0.0
texture_filter = 2

BIN
textures/special/clip.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,35 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dhmu0toe1itnr"
path.s3tc="res://.godot/imported/clip.png-b88af128d9765ee842c60c6e4c027bc0.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
[deps]
source_file="res://textures/special/clip.png"
dest_files=["res://.godot/imported/clip.png-b88af128d9765ee842c60c6e4c027bc0.s3tc.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
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=0

BIN
textures/special/skip.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,35 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bk5oo263y3u7w"
path.s3tc="res://.godot/imported/skip.png-2026d0f841ccf5af70e5ac1bf277c3e0.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
[deps]
source_file="res://textures/special/skip.png"
dest_files=["res://.godot/imported/skip.png-2026d0f841ccf5af70e5ac1bf277c3e0.s3tc.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
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=0

12
trenchbroom/main_fgd.tres Normal file
View File

@ -0,0 +1,12 @@
[gd_resource type="Resource" script_class="FuncGodotFGDFile" load_steps=3 format=3 uid="uid://c7sg28h7vyo3o"]
[ext_resource type="Resource" uid="uid://crgpdahjaj" path="res://addons/func_godot/fgd/func_godot_fgd.tres" id="1_53ed1"]
[ext_resource type="Script" path="res://addons/func_godot/src/fgd/func_godot_fgd_file.gd" id="1_eav6a"]
[resource]
script = ExtResource("1_eav6a")
export_file = false
target_map_editor = 1
fgd_name = "Gmtk24"
base_fgd_files = Array[Resource]([ExtResource("1_53ed1")])
entity_definitions = Array[Resource]([])

341
trenchbroom/maps/demo.map Normal file
View File

@ -0,0 +1,341 @@
// Game: Gmtk24
// Format: Valve
// entity 0
{
"mapversion" "220"
"classname" "worldspawn"
"_tb_mod" "textures"
// brush 0
{
( -912 -176 32 ) ( -912 176 0 ) ( -912 176 32 ) default_texture [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -768 -176 32 ) ( -912 -176 0 ) ( -912 -176 32 ) default_texture [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -768 176 0 ) ( -912 -176 0 ) ( -768 -176 0 ) default_texture [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -768 176 32 ) ( -912 -176 32 ) ( -912 176 32 ) default_texture [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -768 176 32 ) ( -912 176 0 ) ( -768 176 0 ) default_texture [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -768 176 32 ) ( -768 -176 0 ) ( -768 -176 32 ) default_texture [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 1
{
( 912 -176 32 ) ( 912 176 0 ) ( 912 176 32 ) default_texture [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 1056 -176 32 ) ( 912 -176 0 ) ( 912 -176 32 ) default_texture [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 1056 176 0 ) ( 912 -176 0 ) ( 1056 -176 0 ) default_texture [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( 1056 176 32 ) ( 912 -176 32 ) ( 912 176 32 ) default_texture [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( 1056 176 32 ) ( 912 176 0 ) ( 1056 176 0 ) default_texture [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 1056 176 32 ) ( 1056 -176 0 ) ( 1056 -176 32 ) default_texture [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 2
{
( -824 -80 64 ) ( -824 80 48 ) ( -824 80 64 ) asphalt [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -368 -80 64 ) ( -432 -80 48 ) ( -432 -80 64 ) asphalt [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -368 80 48 ) ( -432 -80 48 ) ( -368 -80 48 ) asphalt [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -368 80 64 ) ( -432 -80 64 ) ( -432 80 64 ) asphalt [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -368 80 64 ) ( -432 80 48 ) ( -368 80 48 ) asphalt [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 944 80 64 ) ( 944 -80 48 ) ( 944 -80 64 ) asphalt [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 3
{
( -824 -80 64 ) ( -824 80 48 ) ( -824 80 64 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -368 80 64 ) ( -368 80 48 ) ( -432 80 48 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -368 80 32 ) ( -432 -80 32 ) ( -368 -80 32 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -368 80 80 ) ( -432 -80 80 ) ( -432 80 80 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -368 96 64 ) ( -432 96 48 ) ( -368 96 48 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 944 80 64 ) ( 944 -80 48 ) ( 944 -80 64 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 4
{
( -824 -256 64 ) ( -824 -96 48 ) ( -824 -96 64 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -368 -96 64 ) ( -368 -96 48 ) ( -432 -96 48 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -368 -96 32 ) ( -432 -256 32 ) ( -368 -256 32 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -368 -96 80 ) ( -432 -256 80 ) ( -432 -96 80 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -368 -80 64 ) ( -432 -80 48 ) ( -368 -80 48 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 944 -96 64 ) ( 944 -256 48 ) ( 944 -256 64 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 5
{
( -672 -112 656 ) ( -688 -128 624 ) ( -688 -96 624 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -656 -112 656 ) ( -640 -128 624 ) ( -688 -128 624 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -640 -128 624 ) ( -640 -96 624 ) ( -688 -96 624 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -672 -96 656 ) ( -656 -96 656 ) ( -656 -112 656 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -688 -96 624 ) ( -640 -96 624 ) ( -656 -96 656 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -656 -96 656 ) ( -640 -96 624 ) ( -640 -128 624 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 6
{
( -672 96 656 ) ( -688 96 624 ) ( -688 128 624 ) red_metal [ -1.2246467991473532e-16 -1 0 0 ] [ 0 0 -1 0 ] 180 1 1
( -640 96 624 ) ( -688 96 624 ) ( -672 96 656 ) red_metal [ 1 -1.2246467991473532e-16 0 0 ] [ 0 0 -1 0 ] 180 1 1
( -688 128 624 ) ( -688 96 624 ) ( -640 96 624 ) red_metal [ 1 -1.2246467991473532e-16 0 0 ] [ 1.2246467991473532e-16 1 0 0 ] 180 1 1
( -656 96 656 ) ( -672 96 656 ) ( -672 112 656 ) red_metal [ -1 1.2246467991473532e-16 0 0 ] [ 1.2246467991473532e-16 1 0 0 ] 180 1 1
( -672 112 656 ) ( -688 128 624 ) ( -640 128 624 ) red_metal [ -1 1.2246467991473532e-16 0 0 ] [ 0 0 -1 0 ] 180 1 1
( -656 112 656 ) ( -640 128 624 ) ( -640 96 624 ) red_metal [ 1.2246467991473532e-16 1 0 0 ] [ 0 0 -1 0 ] 180 1 1
}
// brush 7
{
( -712 -128 80 ) ( -712 -80 32 ) ( -712 -80 80 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -640 -152 80 ) ( -688 -152 32 ) ( -688 -152 80 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -640 -80 0 ) ( -688 -128 0 ) ( -640 -128 0 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -656 -128 256 ) ( -688 -112 256 ) ( -560 -112 256 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -640 -96 80 ) ( -688 -96 32 ) ( -640 -96 32 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -616 -80 80 ) ( -616 -128 32 ) ( -616 -128 80 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 8
{
( -712 96 80 ) ( -712 144 32 ) ( -712 144 80 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -640 96 80 ) ( -688 96 32 ) ( -688 96 80 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -640 144 0 ) ( -688 96 0 ) ( -640 96 0 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -656 -128 256 ) ( -688 -112 256 ) ( -560 -112 256 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -640 152 80 ) ( -688 152 32 ) ( -640 152 32 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -616 144 80 ) ( -616 96 32 ) ( -616 96 80 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 9
{
( -704 -128 80 ) ( -704 -80 32 ) ( -704 -80 80 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -640 -144 80 ) ( -688 -144 32 ) ( -688 -144 80 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -656 -128 256 ) ( -560 -112 256 ) ( -688 -112 256 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -656 -128 400 ) ( -688 -112 400 ) ( -560 -112 400 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -640 -96 80 ) ( -688 -96 32 ) ( -640 -96 32 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -624 -80 80 ) ( -624 -128 32 ) ( -624 -128 80 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 10
{
( -704 96 80 ) ( -704 144 32 ) ( -704 144 80 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -640 96 80 ) ( -688 96 32 ) ( -688 96 80 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -656 -128 256 ) ( -560 -112 256 ) ( -688 -112 256 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -656 -128 400 ) ( -688 -112 400 ) ( -560 -112 400 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -640 144 80 ) ( -688 144 32 ) ( -640 144 32 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -624 144 80 ) ( -624 96 32 ) ( -624 96 80 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 11
{
( -696 -128 80 ) ( -696 -80 32 ) ( -696 -80 80 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -640 -136 80 ) ( -688 -136 32 ) ( -688 -136 80 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -656 -128 400 ) ( -560 -112 400 ) ( -688 -112 400 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -656 -128 544 ) ( -688 -112 544 ) ( -560 -112 544 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -640 -96 80 ) ( -688 -96 32 ) ( -640 -96 32 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -632 -80 80 ) ( -632 -128 32 ) ( -632 -128 80 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 12
{
( -696 96 80 ) ( -696 144 32 ) ( -696 144 80 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -640 96 80 ) ( -688 96 32 ) ( -688 96 80 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -656 -128 400 ) ( -560 -112 400 ) ( -688 -112 400 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -656 -128 544 ) ( -688 -112 544 ) ( -560 -112 544 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -640 136 80 ) ( -688 136 32 ) ( -640 136 32 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -632 144 80 ) ( -632 96 32 ) ( -632 96 80 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 13
{
( -688 -128 80 ) ( -688 -80 32 ) ( -688 -80 80 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -640 -128 80 ) ( -688 -128 32 ) ( -688 -128 80 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -656 -128 544 ) ( -560 -112 544 ) ( -688 -112 544 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -640 -80 624 ) ( -688 -128 624 ) ( -688 -80 624 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -640 -96 80 ) ( -688 -96 32 ) ( -640 -96 32 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -640 -80 80 ) ( -640 -128 32 ) ( -640 -128 80 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 14
{
( -688 96 80 ) ( -688 144 32 ) ( -688 144 80 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -640 96 80 ) ( -688 96 32 ) ( -688 96 80 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -656 -128 544 ) ( -560 -112 544 ) ( -688 -112 544 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -640 144 624 ) ( -688 96 624 ) ( -688 144 624 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -640 128 80 ) ( -688 128 32 ) ( -640 128 32 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -640 144 80 ) ( -640 96 32 ) ( -640 96 80 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 15
{
( 712 14.460894756598918 20.519769259644818 ) ( 712 189.82337649086298 207.19595949289317 ) ( 712 155.88225099390866 241.1370849898475 ) red_metal [ 0 -0.7071067811865467 0.7071067811865482 -12.284271 ] [ 0 -0.7071067811865482 -0.7071067811865467 -7.2649384 ] 315 1 1
( 808 -25.13708498984772 60.117749006091515 ) ( 808 14.460894756598918 20.519769259644818 ) ( 712 14.460894756598918 20.519769259644818 ) red_metal [ -1 0 0 0 ] [ 0 -0.7071067811865467 0.7071067811865482 -12.284271 ] 0 1 1
( 712 155.88225099390866 241.1370849898475 ) ( 808 155.88225099390866 241.1370849898475 ) ( 808 -25.13708498984772 60.117749006091515 ) red_metal [ 1 0 0 0 ] [ 0 -0.7071067811865482 -0.7071067811865467 -7.2649384 ] 0 1 1
( 712 189.82337649086298 207.19595949289317 ) ( 808 189.82337649086298 207.19595949289317 ) ( 808 155.88225099390866 241.1370849898475 ) red_metal [ 1 0 0 0 ] [ 0 -0.7071067811865467 0.7071067811865482 -12.284271 ] 0 1 1
( 712 14.460894756598918 20.519769259644818 ) ( 808 14.460894756598918 20.519769259644818 ) ( 808 189.82337649086298 207.19595949289317 ) red_metal [ -1 0 0 0 ] [ 0 -0.7071067811865482 -0.7071067811865467 -7.2649384 ] 0 1 1
( 808 189.82337649086298 207.19595949289317 ) ( 808 14.460894756598918 20.519769259644818 ) ( 808 -25.13708498984772 60.117749006091515 ) red_metal [ 0 0.7071067811865467 -0.7071067811865482 12.284271 ] [ 0 -0.7071067811865482 -0.7071067811865467 -7.2649384 ] 45 1 1
}
// brush 16
{
( 752 -96 288 ) ( 752 96 192 ) ( 752 96 288 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 776 -96 288 ) ( 760 -96 192 ) ( 760 -96 288 ) red_metal [ 1 0 0 8 ] [ 0 0 -1 0 ] 0 1 1
( 776 96 192 ) ( 760 -96 192 ) ( 776 -96 192 ) red_metal [ -1 0 0 -8 ] [ 0 -1 0 0 ] 0 1 1
( 776 96 256 ) ( 760 -96 256 ) ( 760 96 256 ) red_metal [ 1 0 0 8 ] [ 0 -1 0 0 ] 0 1 1
( 776 96 288 ) ( 760 96 192 ) ( 776 96 192 ) red_metal [ -1 0 0 -8 ] [ 0 0 -1 0 ] 0 1 1
( 784 96 288 ) ( 784 -96 192 ) ( 784 -96 288 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 17
{
( 720 -128 80 ) ( 720 -80 32 ) ( 720 -80 80 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 792 -152 80 ) ( 744 -152 32 ) ( 744 -152 80 ) red_metal [ 1 0 0 8 ] [ 0 0 -1 0 ] 0 1 1
( 792 -80 0 ) ( 744 -128 0 ) ( 792 -128 0 ) red_metal [ -1 0 0 -8 ] [ 0 -1 0 0 ] 0 1 1
( 776 -128 256 ) ( 744 -112 256 ) ( 872 -112 256 ) red_metal [ 1 0 0 8 ] [ 0 -1 0 0 ] 0 1 1
( 792 -96 80 ) ( 744 -96 32 ) ( 792 -96 32 ) red_metal [ -1 0 0 -8 ] [ 0 0 -1 0 ] 0 1 1
( 816 -80 80 ) ( 816 -128 32 ) ( 816 -128 80 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 18
{
( 728 -128 80 ) ( 728 -80 32 ) ( 728 -80 80 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 792 -144 80 ) ( 744 -144 32 ) ( 744 -144 80 ) red_metal [ 1 0 0 8 ] [ 0 0 -1 0 ] 0 1 1
( 776 -128 256 ) ( 872 -112 256 ) ( 744 -112 256 ) red_metal [ -1 0 0 -8 ] [ 0 -1 0 0 ] 0 1 1
( 776 -128 400 ) ( 744 -112 400 ) ( 872 -112 400 ) red_metal [ 1 0 0 8 ] [ 0 -1 0 0 ] 0 1 1
( 792 -96 80 ) ( 744 -96 32 ) ( 792 -96 32 ) red_metal [ -1 0 0 -8 ] [ 0 0 -1 0 ] 0 1 1
( 808 -80 80 ) ( 808 -128 32 ) ( 808 -128 80 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 19
{
( 736 -128 80 ) ( 736 -80 32 ) ( 736 -80 80 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 792 -136 80 ) ( 744 -136 32 ) ( 744 -136 80 ) red_metal [ 1 0 0 8 ] [ 0 0 -1 0 ] 0 1 1
( 776 -128 400 ) ( 872 -112 400 ) ( 744 -112 400 ) red_metal [ -1 0 0 -8 ] [ 0 -1 0 0 ] 0 1 1
( 776 -128 544 ) ( 744 -112 544 ) ( 872 -112 544 ) red_metal [ 1 0 0 8 ] [ 0 -1 0 0 ] 0 1 1
( 792 -96 80 ) ( 744 -96 32 ) ( 792 -96 32 ) red_metal [ -1 0 0 -8 ] [ 0 0 -1 0 ] 0 1 1
( 800 -80 80 ) ( 800 -128 32 ) ( 800 -128 80 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 20
{
( 760 -96 576 ) ( 760 96 480 ) ( 760 96 576 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 776 -96 576 ) ( 760 -96 480 ) ( 760 -96 576 ) red_metal [ 1 0 0 8 ] [ 0 0 -1 0 ] 0 1 1
( 776 96 496 ) ( 760 -96 496 ) ( 776 -96 496 ) red_metal [ -1 0 0 -8 ] [ 0 -1 0 0 ] 0 1 1
( 776 96 544 ) ( 760 -96 544 ) ( 760 96 544 ) red_metal [ 1 0 0 8 ] [ 0 -1 0 0 ] 0 1 1
( 776 96 576 ) ( 760 96 480 ) ( 776 96 480 ) red_metal [ -1 0 0 -8 ] [ 0 0 -1 0 ] 0 1 1
( 776 96 576 ) ( 776 -96 480 ) ( 776 -96 576 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 21
{
( 736 136 400 ) ( 736 128 544 ) ( 736 96 544 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 736 96 544 ) ( 800 96 544 ) ( 800 96 400 ) red_metal [ 1 0 0 8 ] [ 0 0 -1 0 ] 0 1 1
( 800 96 400 ) ( 800 136 400 ) ( 736 136 400 ) red_metal [ -1 0 0 -8 ] [ 0 -1 0 0 ] 0 1 1
( 736 128 544 ) ( 800 128 544 ) ( 800 96 544 ) red_metal [ 1 0 0 8 ] [ 0 -1 0 0 ] 0 1 1
( 736 136 400 ) ( 800 136 400 ) ( 800 128 544 ) red_metal [ -1 0 0 -8 ] [ 0 0 -1 0 ] 0 1 1
( 800 128 544 ) ( 800 136 400 ) ( 800 96 400 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 22
{
( 744 96 80 ) ( 744 144 32 ) ( 744 144 80 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 792 96 80 ) ( 744 96 32 ) ( 744 96 80 ) red_metal [ 1 0 0 8 ] [ 0 0 -1 0 ] 0 1 1
( 776 -128 544 ) ( 872 -112 544 ) ( 744 -112 544 ) red_metal [ -1 0 0 -8 ] [ 0 -1 0 0 ] 0 1 1
( 792 144 624 ) ( 744 96 624 ) ( 744 144 624 ) red_metal [ 1 0 0 8 ] [ 0 -1 0 0 ] 0 1 1
( 792 128 80 ) ( 744 128 32 ) ( 792 128 32 ) red_metal [ -1 0 0 -8 ] [ 0 0 -1 0 ] 0 1 1
( 792 144 80 ) ( 792 96 32 ) ( 792 96 80 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 23
{
( 744 -128 80 ) ( 744 -80 32 ) ( 744 -80 80 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 792 -128 80 ) ( 744 -128 32 ) ( 744 -128 80 ) red_metal [ 1 0 0 8 ] [ 0 0 -1 0 ] 0 1 1
( 776 -128 544 ) ( 872 -112 544 ) ( 744 -112 544 ) red_metal [ -1 0 0 -8 ] [ 0 -1 0 0 ] 0 1 1
( 792 -80 624 ) ( 744 -128 624 ) ( 744 -80 624 ) red_metal [ 1 0 0 8 ] [ 0 -1 0 0 ] 0 1 1
( 792 -96 80 ) ( 744 -96 32 ) ( 792 -96 32 ) red_metal [ -1 0 0 -8 ] [ 0 0 -1 0 ] 0 1 1
( 792 -80 80 ) ( 792 -128 32 ) ( 792 -128 80 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 24
{
( 760 -112 656 ) ( 744 -128 624 ) ( 744 -96 624 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 776 -112 656 ) ( 792 -128 624 ) ( 744 -128 624 ) red_metal [ 1 0 0 8 ] [ 0 0 -1 0 ] 0 1 1
( 792 -128 624 ) ( 792 -96 624 ) ( 744 -96 624 ) red_metal [ -1 0 0 -8 ] [ 0 -1 0 0 ] 0 1 1
( 760 -96 656 ) ( 776 -96 656 ) ( 776 -112 656 ) red_metal [ 1 0 0 8 ] [ 0 -1 0 0 ] 0 1 1
( 744 -96 624 ) ( 792 -96 624 ) ( 776 -96 656 ) red_metal [ -1 0 0 -8 ] [ 0 0 -1 0 ] 0 1 1
( 776 -96 656 ) ( 792 -96 624 ) ( 792 -128 624 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 25
{
( 832 72 136 ) ( 816 72 104 ) ( 816 104 104 ) red_metal [ -1.2246467991473532e-16 -1 0 8 ] [ 0 0 -1 8 ] 180 1 1
( 864 72 104 ) ( 816 72 104 ) ( 832 72 136 ) red_metal [ 1 -1.2246467991473532e-16 0 0 ] [ 0 0 -1 8 ] 180 1 1
( 816 104 104 ) ( 816 72 104 ) ( 864 72 104 ) red_metal [ 1 -1.2246467991473532e-16 0 0 ] [ 1.2246467991473532e-16 1 0 -8 ] 180 1 1
( 848 72 136 ) ( 832 72 136 ) ( 832 88 136 ) red_metal [ -1 1.2246467991473532e-16 0 0 ] [ 1.2246467991473532e-16 1 0 -8 ] 180 1 1
( 832 88 136 ) ( 816 104 104 ) ( 864 104 104 ) red_metal [ -1 1.2246467991473532e-16 0 0 ] [ 0 0 -1 8 ] 180 1 1
( 848 88 136 ) ( 864 104 104 ) ( 864 72 104 ) red_metal [ 1.2246467991473532e-16 1 0 -8 ] [ 0 0 -1 8 ] 180 1 1
}
// brush 26
{
( 760 -96 656 ) ( 760 96 560 ) ( 760 96 656 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 776 -96 656 ) ( 760 -96 560 ) ( 760 -96 656 ) red_metal [ 1 0 0 8 ] [ 0 0 -1 0 ] 0 1 1
( 776 96 592 ) ( 760 -96 592 ) ( 776 -96 592 ) red_metal [ -1 0 0 -8 ] [ 0 -1 0 0 ] 0 1 1
( 776 96 624 ) ( 760 -96 624 ) ( 760 96 624 ) red_metal [ 1 0 0 8 ] [ 0 -1 0 0 ] 0 1 1
( 776 96 656 ) ( 760 96 560 ) ( 776 96 560 ) red_metal [ -1 0 0 -8 ] [ 0 0 -1 0 ] 0 1 1
( 776 96 656 ) ( 776 -96 560 ) ( 776 -96 656 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 27
{
( -680 -96 288 ) ( -680 96 192 ) ( -680 96 288 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -656 -96 288 ) ( -672 -96 192 ) ( -672 -96 288 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -656 96 192 ) ( -672 -96 192 ) ( -656 -96 192 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -656 96 256 ) ( -672 -96 256 ) ( -672 96 256 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -656 96 288 ) ( -672 96 192 ) ( -656 96 192 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -648 96 288 ) ( -648 -96 192 ) ( -648 -96 288 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 28
{
( -680 -96 432 ) ( -680 96 336 ) ( -680 96 432 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -656 -96 432 ) ( -672 -96 336 ) ( -672 -96 432 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -656 96 344 ) ( -672 -96 344 ) ( -656 -96 344 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -656 96 400 ) ( -672 -96 400 ) ( -672 96 400 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -656 96 432 ) ( -672 96 336 ) ( -656 96 336 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -648 96 432 ) ( -648 -96 336 ) ( -648 -96 432 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 29
{
( -672 -96 576 ) ( -672 96 480 ) ( -672 96 576 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -656 -96 576 ) ( -672 -96 480 ) ( -672 -96 576 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -656 96 496 ) ( -672 -96 496 ) ( -656 -96 496 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -656 96 544 ) ( -672 -96 544 ) ( -672 96 544 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -656 96 576 ) ( -672 96 480 ) ( -656 96 480 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -656 96 576 ) ( -656 -96 480 ) ( -656 -96 576 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 30
{
( -672 -96 656 ) ( -672 96 560 ) ( -672 96 656 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -656 -96 656 ) ( -672 -96 560 ) ( -672 -96 656 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -656 96 592 ) ( -672 -96 592 ) ( -656 -96 592 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -656 96 624 ) ( -672 -96 624 ) ( -672 96 624 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( -656 96 656 ) ( -672 96 560 ) ( -656 96 560 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( -656 96 656 ) ( -656 -96 560 ) ( -656 -96 656 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 31
{
( 832 120 400 ) ( 832 80 400 ) ( 832 128 256 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 848 80 256 ) ( 832 80 256 ) ( 848 80 400 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 848 128 256 ) ( 832 128 256 ) ( 848 80 256 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( 848 80 400 ) ( 832 80 400 ) ( 848 120 400 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
( 848 120 400 ) ( 832 120 400 ) ( 848 128 256 ) red_metal [ -1 0 0 8 ] [ 0 0.055470019622522716 -0.9984603532054125 0.8875122 ] 0 1 1
( 848 80 400 ) ( 848 120 400 ) ( 848 80 256 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 32
{
( 664 72 256 ) ( 664 120 256 ) ( 664 72 400 ) red_metal [ 0 -1 0 8 ] [ 0 0 -1 0 ] 0 1 1
( 664 72 400 ) ( 680 72 400 ) ( 664 72 256 ) red_metal [ 1 0 0 -8 ] [ 0 0 -1 0 ] 0 1 1
( 664 72 256 ) ( 680 72 256 ) ( 664 120 256 ) red_metal [ -1 0 0 8 ] [ 0 -1 0 8 ] 0 1 1
( 664 112 400 ) ( 680 112 400 ) ( 664 72 400 ) red_metal [ 1 0 0 -8 ] [ 0 -1 0 8 ] 0 1 1
( 664 120 256 ) ( 680 120 256 ) ( 664 112 400 ) red_metal [ -1 0 0 0 ] [ 0 0.055470019622522716 -0.9984603532054125 1.3312988 ] 0 1 1
( 680 72 256 ) ( 680 72 400 ) ( 680 120 256 ) red_metal [ 0 -1 0 8 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 33
{
( 720 152 408 ) ( 720 152 373.679012345679 ) ( 720 192 408 ) red_metal [ 0 -1 0 7.9999847 ] [ 0 0 -1 8 ] 0 1 1
( 768 152 408 ) ( 768 152 373.679012345679 ) ( 720 152 408 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 8 ] 0 1 1
( 768 152 408 ) ( 720 152 408 ) ( 768 192 408 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 8 ] 0 1 1
( 768 152 373.679012345679 ) ( 768 193.77777777777737 376 ) ( 720 152 373.679012345679 ) red_metal [ 1 0 0 8 ] [ 0 -0.9984603532054125 -0.055470019622523146 8.357285 ] 0 1 1
( 720 192 408 ) ( 720 193.77777777777737 376 ) ( 768 192 408 ) red_metal [ -1 0 0 -8 ] [ 0 0.055470019622522716 -0.9984603532054125 4.8813477 ] 0 1 1
( 768 192 408 ) ( 768 193.77777777777737 376 ) ( 768 152 408 ) red_metal [ 0 1 0 -7.9999847 ] [ 0 0 -1 8 ] 0 1 1
}
// brush 34
{
( 728 208 256 ) ( 728 206.22222222222223 288 ) ( 728 160 256 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 728 160 256 ) ( 728 160 285.43209876543204 ) ( 776 160 256 ) red_metal [ 1 0 0 8 ] [ 0 0 -1 0 ] 0 1 1
( 728 206.22222222222223 288 ) ( 776 206.22222222222223 288 ) ( 728 160 285.43209876543204 ) red_metal [ -1 0 0 0 ] [ 0 -0.9984603532054125 -0.055470019622523146 15.901459 ] 0 1 1
( 776 208 256 ) ( 728 208 256 ) ( 776 160 256 ) red_metal [ -1 0 0 -8 ] [ 0 -1 0 0 ] 0 1 1
( 776 208 256 ) ( 776 206.22222222222223 288 ) ( 728 208 256 ) red_metal [ -1 0 0 0 ] [ 0 0.055470019622522716 -0.9984603532054125 -3.5500793 ] 0 1 1
( 776 160 256 ) ( 776 160 285.43209876543204 ) ( 776 208 256 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 35
{
( 784 -112 344 ) ( 784 75.03709410492462 344 ) ( 784 -112 400 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
( 784 -112 400 ) ( 816 -112 400 ) ( 784 -112 344 ) red_metal [ 1 0 0 -8 ] [ 0 0 -1 0 ] 0 1 1
( 816 -112 344 ) ( 816 75.03709410492462 344 ) ( 784 -112 344 ) red_metal [ -1 0 0 8 ] [ 0 -1 0 0 ] 0 1 1
( 784 -112 400 ) ( 784 71.92598299381353 400 ) ( 816 -112 400 ) red_metal [ 1 0 0 -8 ] [ 0 -1 0 0 ] 0 1 1
( 816 75.03709410492462 344 ) ( 816 71.92598299381353 400 ) ( 784 75.03709410492462 344 ) red_metal [ 1 0 0 0 ] [ 0 0.055470019622522716 -0.9984603532054125 0.8875122 ] 0 1 1
( 816 -112 400 ) ( 816 71.92598299381353 400 ) ( 816 -112 344 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
}
// brush 36
{
( 832 71.92598299381353 440 ) ( 832 73.85168082391803 405.3374390581189 ) ( 832 80 440 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 8 ] 0 1 1
( 864 71.92598299381353 440 ) ( 864 73.85168082391803 405.3374390581189 ) ( 832 71.92598299381353 440 ) red_metal [ 1 0 0 0 ] [ 0 0.055470019622522716 -0.9984603532054125 8.825958 ] 0 1 1
( 864 80 440 ) ( 864 71.92598299381353 440 ) ( 832 80 440 ) red_metal [ 1 0 0 -8 ] [ 0 -1 0 0 ] 0 1 1
( 864 80 405.679012345679 ) ( 832 80 405.679012345679 ) ( 864 73.85168082391803 405.3374390581189 ) red_metal [ 1 0 0 0 ] [ 0 -0.9984603532054125 -0.055470019622523146 2.243431 ] 0 1 1
( 832 80 440 ) ( 832 80 405.679012345679 ) ( 864 80 440 ) red_metal [ -1 0 0 8 ] [ 0 0 -1 8 ] 0 1 1
( 864 80 440 ) ( 864 80 405.679012345679 ) ( 864 71.92598299381353 440 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 8 ] 0 1 1
}
}

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