2025-06-09 18:57:53 +02:00
@ tool
@ icon ( " res://addons/func_godot/icons/icon_godot_ranger.svg " )
2025-09-11 15:02:08 +02:00
class_name FuncGodotFGDFile extends Resource
## [Resource] file used to express a set of [FuncGodotFGDEntity] definitions.
##
## Can be exported as an FGD file for use with a Quake or Hammer-based map editor. Used in conjunction with [FuncGodotMapSetting] to generate nodes in a [FuncGodotMap] node.
##
## @tutorial(Level Design Book FGD Chapter): https://book.leveldesignbook.com/appendix/resources/formats/fgd
## @tutorial(Valve Developer Wiki FGD Article): https://developer.valvesoftware.com/wiki/FGD
2025-06-09 18:57:53 +02:00
## 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.
2025-09-11 15:02:08 +02:00
@ export_tool_button ( " Export FGD " ) var export_file : = export_button
func export_button ( ) - > void :
do_export_file ( target_map_editor )
2025-06-09 18:57:53 +02:00
func do_export_file ( target_editor : FuncGodotTargetMapEditors = FuncGodotTargetMapEditors . TRENCHBROOM , fgd_output_folder : String = " " ) - > void :
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 ( ) :
2025-12-28 22:53:18 +01:00
printerr ( " Skipping export: No game config folder " )
2025-06-09 18:57:53 +02:00
return
if fgd_name == " " :
2025-12-28 22:53:18 +01:00
printerr ( " Skipping export: Empty FGD name " )
2025-09-11 15:02:08 +02:00
if not DirAccess . dir_exists_absolute ( fgd_output_folder ) :
if DirAccess . make_dir_recursive_absolute ( fgd_output_folder ) != OK :
2025-12-28 22:53:18 +01:00
printerr ( " Skipping export: Failed to create directory " )
2025-09-11 15:02:08 +02:00
return
2025-06-09 18:57:53 +02:00
2025-09-11 15:02:08 +02:00
var fgd_file = fgd_output_folder . path_join ( fgd_name + " .fgd " )
2025-06-09 18:57:53 +02:00
var file_obj : = FileAccess . open ( fgd_file , FileAccess . WRITE )
2025-09-11 15:02:08 +02:00
if not file_obj :
2025-12-28 22:53:18 +01:00
printerr ( " Failed to open file for writing: " , fgd_file )
2025-09-11 15:02:08 +02:00
return
print ( " Exporting FGD to " , fgd_file )
2025-06-09 18:57:53 +02:00
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 ] = [ ]
2025-12-28 22:53:18 +01:00
## Toggles whether [FuncGodotFGDModelPointClass] resources will generate models from their [PackedScene] files.
@ export var generate_model_point_class_models : bool = true
2025-06-09 18:57:53 +02:00
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 )
else :
printerr ( " Base Fgd Files contains incorrect resource type! Should only be type FuncGodotFGDFile. " )
var entities = get_fgd_classes ( )
for ent in entities :
if not ent is FuncGodotFGDEntityClass :
continue
if ent . func_godot_internal :
continue
2025-12-28 22:53:18 +01:00
if ent is FuncGodotFGDModelPointClass :
ent . _model_generation_enabled = generate_model_point_class_models
2025-06-09 18:57:53 +02:00
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
2025-09-11 15:02:08 +02:00
func get_entity_definitions ( ) - > Dictionary [ String , FuncGodotFGDEntityClass ] :
var res : Dictionary [ String , FuncGodotFGDEntityClass ] = { }
2025-06-09 18:57:53 +02:00
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 ( )
2025-12-28 22:53:18 +01:00
var meta_properties : Dictionary [ String , Variant ] = { }
var class_properties : Dictionary [ String , Variant ] = { }
var class_property_descriptions : Dictionary [ String , Variant ] = { }
2025-06-09 18:57:53 +02:00
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