Compare commits

...

75 Commits
main ... dev

Author SHA1 Message Date
blujai831 4510dd2293
Reintroduce AutoPathFollow as cleaner base class of MovingPlatform 2025-04-09 18:09:25 -07:00
blujai831 478a871ee9
Rename AutoPathFollow to MovingPlatform. (At some point during early development of the class, it ceased to be purely focused on automatically moving an object along a path, and became much more specifically focused on implementing a moving platform and only a moving platform.) 2025-04-09 17:54:26 -07:00
blujai831 ef54e30784
Fix around half the map textures being 2048x2048 (they should be 256x256) 2025-04-08 21:51:03 -07:00
blujai831 00c35dd79f
Implement moving platforms 2025-04-06 19:00:11 -07:00
blujai831 c203e3e4b8
Switch to Jolt physics 2025-04-06 16:28:07 -07:00
blujai831 7314d5081a
In gameplay camera, distinguish turning lerp speed from righting lerp speed. Reduce righting lerp speed. 2025-04-04 17:56:35 -07:00
blujai831 27196f752a
Adjust lerp weights in gameplay camera: notably, when turning to look at the target, do not lerp at all. 2025-04-04 17:26:11 -07:00
blujai831 397321a890
Configure project for web export 2025-04-04 17:19:17 -07:00
blujai831 48f34265ec
Move speed pad in front of ramp in test level 2025-04-04 17:18:53 -07:00
blujai831 19dedf862c
Model and implement constructed and artificial speedpads 2025-04-04 16:50:59 -07:00
blujai831 7428ad35b3
Create artificial springboard prop. Fix direction vector for springboard launch not being normalized when springboard is scaled up. 2025-04-04 13:09:32 -07:00
blujai831 2de09b54a1
Rename springboard to springboard_constructed 2025-04-04 11:15:29 -07:00
blujai831 c08ad25dd3
Create material crystal4 2025-04-01 20:38:59 -07:00
blujai831 85b2d76138
Increase analog look sensitivity. Fix analog look sensitivity going unused. 2025-04-01 20:22:21 -07:00
blujai831 c75c8e094c
Increase default top ground speed to create a more perceptible difference between slope climbability at low versus high speed. 2025-04-01 15:15:25 -07:00
blujai831 0ad77a03ec
Add runway to test map for testing slope entry at max velocity. Raise ground plane horizontality to a configurable exponent before using it to modulate forward acceleration. Experimentally, 1.0 (equivalent to not doing this) is too strict, and the strictest value which is lenient enough seems to be 0.45. 2025-04-01 12:46:01 -07:00
blujai831 846f327a22
Prevent unrealistic climbing by modulating ground acceleration by ground plane horizontality 2025-04-01 12:25:25 -07:00
blujai831 91ead7fb5c
Make hang state resolve faster 2025-04-01 12:15:32 -07:00
blujai831 5d45119a26
Add more test geometry to the test map 2025-04-01 12:10:45 -07:00
blujai831 50bdf9a044
Fix wall-slide state falling through floors 2025-03-31 09:32:10 -07:00
blujai831 0c2083d7dd
Create material crystal3 2025-03-31 09:31:45 -07:00
blujai831 1d4dc4b0d4
Make some more textures 2025-03-30 22:22:06 -07:00
blujai831 3b0b3bbaf4
Make skidding happen less often. 2025-03-30 17:16:32 -07:00
blujai831 6287e74ba5
Do not make the camera jump around trying to find the character when only the character's feet are hidden. 2025-03-30 17:08:55 -07:00
blujai831 b5a2309e6b
Prevent gameplay camera from getting stuck on walls. 2025-03-30 16:51:31 -07:00
blujai831 8fc34c825b
Always count true ground as ground regardless of orientation (no wall-jumping off the floor of the level). Do not consider ceilings walls either. 2025-03-30 16:39:03 -07:00
blujai831 aa5b6db5af
Reduce minimum slope considered steep enough to slide down 2025-03-30 16:28:50 -07:00
blujai831 ef18512dc5
Switch back to compatibility renderer 2025-03-30 16:22:25 -07:00
blujai831 ce466e7fb5
Fix wrong orientation on TestStick's launch-while-holding state 2025-03-30 16:04:38 -07:00
blujai831 6fced07e67
Fix some camera issues by only using gimbal margin to restrict voluntary camera movement, not automatic camera movement 2025-03-30 15:39:53 -07:00
blujai831 5ef367cac2
Improve camera responsiveness by following one frame ahead laterally and 20 frames ahead vertically -- the latter to improve vantage when jumping or taking springboards. 2025-03-30 15:35:16 -07:00
blujai831 ce77112c20
Make springboard shrink back down much faster. 2025-03-30 15:14:02 -07:00
blujai831 96e4a109a5
Refine springboard model, springboard code, and launch state code. 2025-03-30 15:08:04 -07:00
blujai831 fae190fc53
Implement untested springboard launch state for character. Fix coyote time too short on wall slide state. 2025-03-30 13:33:34 -07:00
blujai831 49260483e9
Commit changes made automatically by Godot 2025-03-30 13:31:33 -07:00
blujai831 839da45d6d
Enable triplanar UV1 on map materials so as to preview them. 2025-03-30 11:04:31 -07:00
blujai831 ce7a3495b8
Add another wood texture 2025-03-29 23:11:33 -07:00
blujai831 6bc4031993
Create foliage and wood textures 2025-03-29 22:54:51 -07:00
blujai831 21c26c3681
Make grass and ice materials 2025-03-29 21:57:09 -07:00
blujai831 a91aa22f68
Commit mysterious changes made automatically by Godot 2025-03-29 21:56:40 -07:00
blujai831 952a42d3a0
Switch to forward+ rendering 2025-03-29 18:32:54 -07:00
blujai831 73a28b6f96
Create new materials in Material Maker. 2025-03-29 18:32:34 -07:00
blujai831 0c4bd606de
Delete map textures in preparation to use Material Maker instead of hand-rolling textures. 2025-03-29 12:57:59 -07:00
blujai831 74e4e47a68
Fix incorrect syntax when using NodePath for property paths 2025-03-29 11:45:10 -07:00
blujai831 f2559cac32
Respect Godot's arbitrary decision to move things around 2025-03-29 11:43:44 -07:00
blujai831 0513c3a3a6
Disable mouse controls when the game window is embedded in the editor 2025-03-29 11:30:52 -07:00
blujai831 e8fc5b600c
Implement throwing. 2025-03-29 11:13:11 -07:00
blujai831 8b6cae07c8
Reorganize project hierarchy to keep all test assets in test directory 2025-03-29 09:55:22 -07:00
blujai831 a2ebebd82b
Use interact button instead of action buttons to pick up object. 2025-03-29 02:49:41 -07:00
blujai831 3463e09994
Test and refine carrying item code. 2025-03-29 02:44:10 -07:00
blujai831 09b25cf945
Iterate upon controls. 2025-03-29 02:12:43 -07:00
blujai831 147806124b
Fix unintuitive behavior of wall jump. Also fix jump sound always being interrupted by immediate landing sound. 2025-03-28 22:40:54 -07:00
blujai831 e08206c7c8
Initial prototype of character motion. 2025-03-28 19:02:51 -07:00
blujai831 95ef1df7f8
Update license agreement to forbid microtransactions 2025-03-28 00:21:14 -07:00
blujai831 bc974eea64
Update license agreement to claim copyright, because the part where I disclaimed it was an artifact of when I was considering doing the formal part of the document slightly differently 2025-03-28 00:08:10 -07:00
blujai831 57c9e1aad3
Modify license agreement to cover a few more bases 2025-03-28 00:03:46 -07:00
blujai831 bb812c4f68
Fix mistake in license agreement 2025-03-27 23:36:18 -07:00
blujai831 47d338c8f1
Convert license agreement to markdown and reformat for readability 2025-03-27 23:34:26 -07:00
blujai831 5e589e5284
Update license agreement 2025-03-27 23:27:16 -07:00
blujai831 d902b4a4a8
Model test map 2025-03-22 21:37:19 -07:00
blujai831 dd617d0760
Normalize ambience and sound effects to desired loudness 2025-03-21 11:16:39 -07:00
blujai831 ec1bde64ac
Import audio 2025-03-20 18:57:55 -07:00
blujai831 97d9576dd0
Rename textures snow and ice to snow1 and ice1 2025-03-19 22:46:38 -07:00
blujai831 f452af93b9
Draw some textures 2025-03-19 22:23:51 -07:00
blujai831 64a3499303
Import enemy models 2025-03-19 10:26:02 -07:00
blujai831 89c5846e08
Import prop models 2025-03-19 10:08:49 -07:00
blujai831 5aed6c8d1b
Make minor adjustments to icon.svg 2025-03-18 21:12:25 -07:00
blujai831 c77bd04911
Add circular background to icon 2025-03-18 18:19:11 -07:00
blujai831 9c6d13db27
Add icon 2025-03-18 17:55:14 -07:00
blujai831 ec749249dd
Configure func_godot_local_config 2025-03-18 14:34:06 -07:00
blujai831 4e7e31cc3c
Import character models 2025-03-17 18:38:17 -07:00
blujai831 e56b87b0e7
Install func_godot 2025-03-17 18:37:31 -07:00
blujai831 0916c2a3b5
Create Godot project 2025-03-17 14:38:59 -07:00
blujai831 c3e939d9c5
Add notice about missing history 2025-03-17 14:27:24 -07:00
blujai831 6c54526d25
Remove placeholder readme. 2025-03-17 14:23:05 -07:00
789 changed files with 33434 additions and 548 deletions

4
.editorconfig Normal file
View File

@ -0,0 +1,4 @@
root = true
[*]
charset = utf-8

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

3
.gitignore vendored
View File

@ -1,2 +1,5 @@
*.local
*.local.*
# Godot 4+ specific ignores
.godot/
/android/

119
LICENSE.md Normal file
View File

@ -0,0 +1,119 @@
# blujai831's Extralegal-Copyleft Doomsday License Agreement (ECDLA)
This work copyright (c)2025-present alias blujai831 <webmaster@blujai831.dev>.
## FORMALLY:
I, Licensor, hereby extend to the Licensee of a copy of this Work
all non-exclusive non-patent rights to the Work
which you, applicable jurisdiction, would vest in me the authority to bestow.
I cannot be held liable under any circumstances for any consequences
of owning, using, conveying, etc the Work.
Any conditions or restrictions stipulated below this point are null and void.
## INFORMALLY, AND ADDRESSED TO THE COURT:
IANAL, clearly, but if you, applicable jurisdiction,
are "some of the good ones," I trust you'll interpret the above clause
in its clearly intended spirit, and not pick it apart
and find likely-obvious loopholes or ambiguities
just to screw over one or both parties to whatever dumbass lawsuit.
If you're going to maliciously misinterpret this license agreement anyway,
then with the world being what it is these days,
there's clearly nothing anyone can do to stop you
from within the system anymore anyway.
## INFORMALLY, AND ADDRESSED TO THE LICENSEE:
This is an extralegal copyleft license agreement.
The intention is to notify you of those of your rights with regard to the work
which I as its rightful owner will personally respect
in my own private and individual conduct toward you.
It is not to be mistaken as meaningfully legally binding,
and that's because the government has gone to shit.
My home country is now officially a fascist empire.
Applicable jurisdiction can no longer be trusted to do their fucking job
and not just gag themselves all day on Tronald J. Dump's stumpy orange chode.
### PERMISSIONS:
You have my informal permission to keep and use a copy of this work
in any of the following ways:
* private use;
* studying the source code in order to paraphrase it in your own work,
not subject to any restrictions, not even those specified below;
* copying, modifying, redistributing, or providing access to
the entire work or substantial verbatim portions thereof,
for profit or otherwise, subject to restrictions specified below;
* anything along those lines.
### RESTRICTIONS:
I informally forbid you from using any copy of this work
in any of the following ways:
* any way whatsoever, if you're a scum-sucking piece of garbage
(meaning a capitalist pig, one-percenter, fascist, fascist apologist,
royalist, racist / sexist / homophobic / transphobic / queerphobic /
Islamophobic / antisemitic / anti-Palestinian / ableist / sizeist /
poorphobic / classist / anti-Irish / pro-war / nationalist /
modern-day federal government official / etc other unspecified variety
of horrible person);
* any way intended to generate profit to any such extent
as to *turn you into* a capitalist pig
(though I sympathize with the temptation);
* on behalf of, or to knowingly assist with any aim,
any scum-sucking piece of garbage as previously defined;
* any way intended or highly likely to result in physical injury or death
to anyone who isn't a scum-sucking piece of garbage as previously defined;
* any way intended to remove anyone from any labor market
or deny anyone access to survival essentials,
supposing such person is not a scum-sucking piece of garbage
as previously defined;
* any way intended to hurt the reputation of someone who doesn't deserve it
(though I will be inclined to be lenient if I can be convinced
*you genuinely believe* they deserve it, even if I don't agree);
* subscription leasing;
* sublicensing / relicensing;
* misrepresentation of the work's origin;
* microtransactions;
* training, prompting, or combining with generated output of,
an artificial neural network, large language model,
or anything else that uses gradient descent for any purpose;
* minting or exchanging NFTs or cryptocurrency;
* otherwise destroying the fucking Earth in any way
more severe than the work already does;
* surveillance;
* censorship;
* spyware / malware;
* incorporating telemetry or backdoors;
* aiding an authoritarian regime;
* you know, that sort of thing.
### CONSEQUENCES:
If someone violates the informal spirit of this license agreement,
I will personally come after them in an extralegal capacity (in Minecraft).
ACAB. Fuck the police and motherfuck the law.

View File

@ -1,538 +0,0 @@
Copyright (c)2024 alias blujai831 <webmaster@blujai831.dev>
Any definitions which precede the row of 78 hyphens apply only
within any paragraphs preceding the row of 78 hyphens,
not within any paragraphs subsequent to the row of 78 hyphens,
and no definition subsequent to the row of 78 hyphens applies
within any paragraphs preceding the row of 78 hyphens.
The prior paragraph is NOT to be understood to imply that if any definition
preceding the row of 78 hyphens is identical in meaning to any definition
subsequent to the row of 78 hyphens then either is rendered invalid.
In such a case, the definition preceding the row of 78 hyphens continues
to apply within any paragraphs preceding the row of 78 hyphens,
and the definition subsequent to the row of 78 hyphens continues to apply
within any paragraphs subsequent to the row of 78 hyphens, even though
the two definitions are identical in meaning.
This Document is defined as all text between and including the instance
of "Copyright (c)2024 alias blujai831 <webmaster@blujai831.dev>"
that occurs above the one in this paragraph and the instance
of "this License is not intended to restrict the license of any rights
under applicable law" that occurs below the row of 78 hyphens
(and thus also below any of the ones in this paragraph). (The omission
of the period / full stop (.) in the quotation "this License is not intended
to restrict the license of any rights under applicable law"
is strictly an issue of correct grammar, and should not be taken to mean
the period / full stop at the end of This Document as thereby defined
is not also part of This Document, which in fact it is.)
I, the Licensor (defined as alias blujai831 <webmaster@blujai831.dev>),
hereby offer the Work (defined as the combined data content,
excluding This Document itself, of all files organized as descendants,
immediate or otherwise, of the immediate parent directory of any file
whose data is an ASCII encoding of This Document verbatim) subject
to This License (defined as the portion of This Document which is below,
and not above, the row of 78 hyphens).
This License is a verbatim reproduction of Thufie's CNPLv7,
meaning her Cooperative Nonviolent Public License version 7,
but I, the Licensor, hereby declare that the Work is available
under the CNPLv7+, meaning version 7 or any later revision.
I, the Licensor, hereby disclaim the text of This License itself
as property of Thufie, not myself. To avoid confusion with This License itself,
I will not reproduce herein the Other License under which the text
of This License itself is offered, but that Other License can be found
at the following internet address:
https://git.pixie.town/thufie/npl-builder/src/branch/main/LICENSE
All paragraphs in This Document up to and including this paragraph,
but excluding any subsequent paragraphs, are not part of This License
and should not be mistaken as such, and should be understood to serve only
to inform the intended context in which to interpret This License.
I, the Licensor, hereby assert that because all paragraphs in This Document
up to and including this paragraph, but excluding any subsequent paragraphs,
are not part of This License, I therefore have not modified This License,
only contextualized it. As such, I assert that the requirements
which the Other License imposes upon any modified version of This License
do not apply to This Document.
------------------------------------------------------------------------------
THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS
COOPERATIVE NON-VIOLENT PUBLIC LICENSE ("LICENSE"). THE WORK IS
PROTECTED BY COPYRIGHT AND ALL OTHER APPLICABLE LAWS. ANY USE OF THE
WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS
PROHIBITED. BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED IN THIS
LICENSE, YOU AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE
EXTENT THIS LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR
GRANTS YOU THE RIGHTS CONTAINED HERE IN AS CONSIDERATION FOR ACCEPTING
THE TERMS AND CONDITIONS OF THIS LICENSE AND FOR AGREEING TO BE BOUND BY
THE TERMS AND CONDITIONS OF THIS LICENSE.
Definitions
An Act of War is any action of one country against any group either with
an intention to provoke a conflict or an action that occurs during a
declared war or during armed conflict between military forces of any
origin. This includes but is not limited to enforcing sanctions or
sieges, supplying armed forces, or profiting from the manufacture of
tools or weaponry used in military conflict.
An Adaptation is a work based upon the Work, or upon the Work and other
pre-existing works, such as a translation, adaptation, derivative work,
arrangement of music or other alterations of a literary or artistic
work, or phonogram or performance and includes cinematographic
adaptations or any other form in which the Work may be recast,
transformed, or adapted including in any form recognizably derived from
the original, except that a work that constitutes a Collection will not
be considered an Adaptation for the purpose of this License. For the
avoidance of doubt, where the Work is a musical work, performance or
phonogram, the synchronization of the Work in timed-relation with a
moving image ("synching") will be considered an Adaptation for the
purpose of this License. In addition, where the Work is designed to
output a neural network the output of the neural network will be
considered an Adaptation for the purpose of this license.
Bodily Harm is any physical hurt or injury to a person that interferes
with the health or comfort of the person and that is more than merely
transient or trifling in nature.
Distribute is to make available to the public the original and copies of
the Work or Adaptation, as appropriate, through sale, gift or any other
transfer of possession or ownership.
Incarceration is Confinement in a jail, prison, or any other place where
individuals of any kind are held against either their will or (if their
will cannot be determined) the will of their legal guardian or
guardians. In the case of a conflict between the will of the individual
and the will of their legal guardian or guardians, the will of the
individual will take precedence.
Licensor is The individual, individuals, entity, or entities that
offer(s) the Work under the terms of this License
Original Author is in the case of a literary or artistic work, the
individual, individuals, entity or entities who created the Work or if
no individual or entity can be identified, the publisher; and in
addition
- in the case of a performance the actors, singers, musicians,
dancers, and other persons who act, sing, deliver, declaim, play in,
interpret or otherwise perform literary or artistic works or
expressions of folklore;
- in the case of a phonogram the producer being the person or legal
entity who first fixes the sounds of a performance or other sounds;
and,
- in the case of broadcasts, the organization that transmits the
broadcast.
Work is the literary and/or artistic work offered under the terms of
this License including without limitation any production in the
literary, scientific and artistic domain, whatever may be the mode or
form of its expression including digital form, such as a book, pamphlet
and other writing; a lecture, address, sermon or other work of the same
nature; a dramatic or dramatico-musical work; a choreographic work or
entertainment in dumb show; a musical composition with or without words;
a cinematographic work to which are assimilated works expressed by a
process analogous to cinematography; a work of drawing, painting,
architecture, sculpture, engraving or lithography; a photographic work
to which are assimilated works expressed by a process analogous to
photography; a work of applied art; an illustration, map, plan, sketch
or three-dimensional work relative to geography, topography,
architecture or science; a performance; a broadcast; a phonogram; a
compilation of data to the extent it is protected as a copyrightable
work; or a work performed by a variety or circus performer to the extent
it is not otherwise considered a literary or artistic work.
You means an individual or entity exercising rights under this License
who has not previously violated the terms of this License with respect
to the Work, or who has received express permission from the Licensor to
exercise rights under this License despite a previous violation.
Publicly Perform means to perform public recitations of the Work and to
communicate to the public those public recitations, by any means or
process, including by wire or wireless means or public digital
performances; to make available to the public Works in such a way that
members of the public may access these Works from a place and at a place
individually chosen by them; to perform the Work to the public by any
means or process and the communication to the public of the performances
of the Work, including by public digital performance; to broadcast and
rebroadcast the Work by any means including signs, sounds or images.
Reproduce is to make copies of the Work by any means including without
limitation by sound or visual recordings and the right of fixation and
reproducing fixations of the Work, including storage of a protected
performance or phonogram in digital form or other electronic medium.
Software is any digital Work which, through use of a third-party piece
of Software or through the direct usage of itself on a computer system,
the memory of the computer is modified dynamically or semi-dynamically.
"Software", secondly, processes or interprets information.
Source Code is the human-readable form of Software through which the
Original Author and/or Distributor originally created, derived, and/or
modified it.
Surveilling is the use of the Work to either overtly or covertly observe
and record persons and or their activities.
A Network Service is the use of a piece of Software to interpret or
modify information that is subsequently and directly served to users
over the Internet.
To Discriminate is use of a work to differentiate between humans in a
such a way which prioritizes some above others on the basis of percieved
membership within certain groups.
Hate Speech is Communication or any form of expression which is solely
for the purpose of expressing hatred for some group or advocating a form
of Discrimination between humans.
Coercion is leveraging of the threat of force or use of force to
intimidate a person in order to gain compliance, or to offer large
incentives which aim to entice a person to act against their will.
Fair Dealing Rights
Nothing in this License is intended to reduce, limit, or restrict any
uses free from copyright or rights arising from limitations or
exceptions that are provided for in connection with the copyright
protection under copyright law or other applicable laws.
License Grant
Subject to the terms and conditions of this License, Licensor hereby
grants You a worldwide, royalty-free, non-exclusive, perpetual (for the
duration of the applicable copyright) license to exercise the rights in
the Work as stated below:
To Reproduce the Work, to incorporate the Work into one or more
Collections, and to Reproduce the Work as incorporated in the
Collections
To create and Reproduce Adaptations provided that any such Adaptation,
including any translation in any medium, takes reasonable steps to
clearly label, demarcate or otherwise identify that changes were made to
the original Work. For example, a translation could be marked "The
original work was translated from English to Spanish," or a modification
could indicate "The original work has been modified."
To Distribute and Publicly Perform the Work including as incorporated in
Collections.
To Distribute and Publicly Perform Adaptations. The above rights may be
exercised in all media and formats whether now known or hereafter
devised. The above rights include the right to make such modifications
as are technically necessary to exercise the rights in other media and
formats. This License constitutes the entire agreement between the
parties with respect to the Work licensed here. There are no
understandings, agreements or representations with respect to the Work
not specified here. Licensor shall not be bound by any additional
provisions that may appear in any communication from You. This License
may not be modified without the mutual written agreement of the Licensor
and You. All rights not expressly granted by Licensor are hereby
reserved, including but not limited to the rights set forth in
Non-waivable Compulsory License Schemes, Waivable Compulsory License
Schemes, and Voluntary License Schemes in the restrictions.
Restrictions
The license granted in the license grant above is expressly made subject
to and limited by the following restrictions:
You may Distribute or Publicly Perform the Work only under the terms of
this License. You must include a copy of, or the Uniform Resource
Identifier (URI) for, this License with every copy of the Work You
Distribute or Publicly Perform. You may not offer or impose any terms on
the Work that restrict the terms of this License or the ability of the
recipient of the Work to exercise the rights granted to that recipient
under the terms of the License. You may not sublicense the Work. You
must keep intact all notices that refer to this License and to the
disclaimer of warranties with every copy of the Work You Distribute or
Publicly Perform. When You Distribute or Publicly Perform the Work, You
may not impose any effective technological measures on the Work that
restrict the ability of a recipient of the Work from You to exercise the
rights granted to that recipient under the terms of the License. This
Section applies to the Work as incorporated in a Collection, but this
does not require the Collection apart from the Work itself to be made
subject to the terms of this License. If You create a Collection, upon
notice from any Licensor You must, to the extent practicable, remove
from the Collection any credit as requested. If You create an
Adaptation, upon notice from any Licensor You must, to the extent
practicable, remove from the Adaptation any credit as requested.
Commercial Restrictions
You may not exercise any of the rights granted to You in the above
section in any manner that is primarily intended for or directed toward
commercial advantage or private monetary compensation unless you meet
the following requirements.
i. You are a worker-owned business or worker-owned collective.
ii. after tax, all financial gain, surplus, profits and benefits
produced by the business or collective are distributed among the
worker-owners unless a set amount is to be allocated towards
community projects as decided by a previously-established consensus
agreement between the worker-owners where all worker-owners agreed.
iii. You are not using such rights on behalf of a business other than
those specified in (i) or (ii) above, nor are using such rights as
a proxy on behalf of a business with the intent to circumvent the
aforementioned restrictions on such a business.
The exchange of the Work for other copyrighted works by means of digital
file-sharing or otherwise shall not be considered to be intended for or
directed toward commercial advantage or private monetary compensation,
provided there is no payment of any monetary compensation in connection
with the exchange of copyrighted works.
If the Work meets the definition of Software, You may exercise the
rights granted in the license grant only if You provide a copy of the
corresponding Source Code from which the Work was derived in digital
form, or You provide a URI for the corresponding Source Code of the
Work, to any recipients upon request.
If the Work is used as or for a Network Service, You may exercise the
rights granted in the license grant only if You provide a copy of the
corresponding Source Code from which the Work was derived in digital
form, or You provide a URI for the corresponding Source Code to the
Work, to any recipients of the data served or modified by the Web
Service.
Any use by a business that is privately owned and managed, and that
seeks to generate profit from the labor of employees paid by salary or
other wages, is not permitted under this license.
You may exercise the rights granted in the license grant for any
purposes only if:
i. You do not use the Work for the purpose of inflicting Bodily Harm on
human beings (subject to criminal prosecution or otherwise) outside
of providing medical aid or undergoing a voluntary procedure under
no form of Coercion.
ii. You do not use the Work for the purpose of Surveilling or tracking
individuals for financial gain.
iii. You do not use the Work in an Act of War.
iv. You do not use the Work for the purpose of supporting or profiting
from an Act of War.
v. You do not use the Work for the purpose of Incarceration.
vi. You do not use the Work for the purpose of extracting, processing,
or refining, oil, gas, or coal. Or to in any other way to
deliberately pollute the environment as a byproduct of manufacturing
or irresponsible disposal of hazardous materials.
vii. You do not use the Work for the purpose of expediting,
coordinating, or facilitating paid work undertaken by individuals
under the age of 12 years.
viii. You do not use the Work to either Discriminate or spread Hate
Speech on the basis of sex, sexual orientation, gender identity,
race, age, disability, color, national origin, religion, caste, or
lower economic status.
If You Distribute, or Publicly Perform the Work or any Adaptations or
Collections, You must, unless a request has been made by any Licensor to
remove credit from a Collection or Adaptation, keep intact all copyright
notices for the Work and provide, reasonable to the medium or means You
are utilizing:
i. the name of the Original Author (or pseudonym, if applicable) if
supplied, and/or if the Original Author and/or Licensor designate
another party or parties (e.g., a sponsor institute, publishing
entity, journal) for attribution ("Attribution Parties") in
Licensor's copyright notice, terms of service or by other reasonable
means, the name of such party or parties;
ii. the title of the Work if supplied;
iii. to the extent reasonably practicable, the URI, if any, that
Licensor to be associated with the Work, unless such URI does not
refer to the copyright notice or licensing information for the
Work; and,
iv. in the case of an Adaptation, a credit identifying the use of the
Work in the Adaptation (e.g., "French translation of the Work by
Original Author," or "Screenplay based on original Work by Original
Author").
If any Licensor has sent notice to request removing credit, You must, to
the extent practicable, remove any credit as requested. The credit
required by this Section may be implemented in any reasonable manner;
provided, however, that in the case of an Adaptation or Collection, at a
minimum such credit will appear, if a credit for all contributing
authors of the Adaptation or Collection appears, then as part of these
credits and in a manner at least as prominent as the credits for the
other contributing authors. For the avoidance of doubt, You may only use
the credit required by this Section for the purpose of attribution in
the manner set out above and, by exercising Your rights under this
License, You may not implicitly or explicitly assert or imply any
connection with, sponsorship or endorsement by the Original Author,
Licensor and/or Attribution Parties, as appropriate, of You or Your use
of the Work, without the separate, express prior written permission of
the Original Author, Licensor and/or Attribution Parties.
Non-waivable Compulsory License Schemes. In those jurisdictions in which
the right to collect royalties through any statutory or compulsory
licensing scheme cannot be waived, the Licensor reserves the exclusive
right to collect such royalties for any exercise by You of the rights
granted under this License
Waivable Compulsory License Schemes. In those jurisdictions in which the
right to collect royalties through any statutory or compulsory licensing
scheme can be waived, the Licensor reserves the exclusive right to
collect such royalties for any exercise by You of the rights granted
under this License if Your exercise of such rights is for a purpose or
use which is otherwise than noncommercial as permitted under Commercial
Restrictions and otherwise waives the right to collect royalties through
any statutory or compulsory licensing scheme.
Voluntary License Schemes. The Licensor reserves the right to collect
royalties, whether individually or, in the event that the Licensor is a
member of a collecting society that administers voluntary licensing
schemes, via that society, from any exercise by You of the rights
granted under this License that is for a purpose or use which is
otherwise than noncommercial as permitted under the license grant.
Except as otherwise agreed in writing by the Licensor or as may be
otherwise permitted by applicable law, if You Reproduce, Distribute or
Publicly Perform the Work either by itself or as part of any Adaptations
or Collections, You must not distort, mutilate, modify or take other
derogatory action in relation to the Work which would be prejudicial to
the Original Author's honor or reputation. Licensor agrees that in those
jurisdictions (e.g. Japan), in which any exercise of the right granted
in the license grant of this License (the right to make Adaptations)
would be deemed to be a distortion, mutilation, modification or other
derogatory action prejudicial to the Original Author's honor and
reputation, the Licensor will waive or not assert, as appropriate, this
Section, to the fullest extent permitted by the applicable national law,
to enable You to reasonably exercise Your right under the license grant
of this License (right to make Adaptations) but not otherwise.
Do not make any legal claim against anyone accusing the Work, with or
without changes, alone or with other works, of infringing any patent
claim.
Representations Warranties and Disclaimer
UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR
OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY
KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE,
INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY,
FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF
LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS,
WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE
EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
Limitation on Liability
EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL
LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL,
INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF
THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED
OF THE POSSIBILITY OF SUCH DAMAGES.
Termination
This License and the rights granted hereunder will terminate
automatically upon any breach by You of the terms of this License.
Individuals or entities who have received Adaptations or Collections
from You under this License, however, will not have their licenses
terminated provided such individuals or entities remain in full
compliance with those licenses. The Sections on definitions, fair
dealing rights, representations, warranties, and disclaimer, limitation
on liability, termination, and revised license versions will survive any
termination of this License.
Subject to the above terms and conditions, the license granted here is
perpetual (for the duration of the applicable copyright in the Work).
Notwithstanding the above, Licensor reserves the right to release the
Work under different license terms or to stop distributing the Work at
any time; provided, however that any such election will not serve to
withdraw this License (or any other license that has been, or is
required to be, granted under the terms of this License), and this
License will continue in full force and effect unless terminated as
stated above.
Revised License Versions
This License may receive future revisions in the original spirit of the
license intended to strengthen This License. Each version of This
License has an incrementing version number.
Unless otherwise specified like in the below subsection The Licensor has
only granted this current version of This License for The Work. In this
case future revisions do not apply.
The Licensor may specify that the latest available revision of This
License be used for The Work by either explicitly writing so or by
suffixing the License URI with a "+" symbol.
The Licensor may specify that The Work is also available under the terms
of This License's current revision as well as specific future revisions.
The Licensor may do this by writing it explicitly or suffixing the
License URI with any additional version numbers each separated by a
comma.
Miscellaneous
Each time You Distribute or Publicly Perform the Work or a Collection,
the Licensor offers to the recipient a license to the Work on the same
terms and conditions as the license granted to You under this License.
Each time You Distribute or Publicly Perform an Adaptation, Licensor
offers to the recipient a license to the original Work on the same terms
and conditions as the license granted to You under this License.
If the Work is classified as Software, each time You Distribute or
Publicly Perform an Adaptation, Licensor offers to the recipient a copy
and/or URI of the corresponding Source Code on the same terms and
conditions as the license granted to You under this License.
If the Work is used as a Network Service, each time You Distribute or
Publicly Perform an Adaptation, or serve data derived from the Software,
the Licensor offers to any recipients of the data a copy and/or URI of
the corresponding Source Code on the same terms and conditions as the
license granted to You under this License.
If any provision of this License is invalid or unenforceable under
applicable law, it shall not affect the validity or enforceability of
the remainder of the terms of this License, and without further action
by the parties to this agreement, such provision shall be reformed to
the minimum extent necessary to make such provision valid and
enforceable.
No term or provision of this License shall be deemed waived and no
breach consented to unless such waiver or consent shall be in writing
and signed by the party to be charged with such waiver or consent.
This License constitutes the entire agreement between the parties with
respect to the Work licensed here. There are no understandings,
agreements or representations with respect to the Work not specified
here. Licensor shall not be bound by any additional provisions that may
appear in any communication from You. This License may not be modified
without the mutual written agreement of the Licensor and You.
The rights granted under, and the subject matter referenced, in this
License were drafted utilizing the terminology of the Berne Convention
for the Protection of Literary and Artistic Works (as amended on
September 28, 1979), the Rome Convention of 1961, the WIPO Copyright
Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 and
the Universal Copyright Convention (as revised on July 24, 1971). These
rights and subject matter take effect in the relevant jurisdiction in
which the License terms are sought to be enforced according to the
corresponding provisions of the implementation of those treaty
provisions in the applicable national law. If the standard suite of
rights granted under applicable copyright law includes additional rights
not granted under this License, such additional rights are deemed to be
included in the License; this License is not intended to restrict the
license of any rights under applicable law.

View File

@ -0,0 +1,5 @@
# This repository is missing a lot of history!
This project has gone through many, many iterations.
Though this *repository* began its life in March 2025,
this *project* has been in development on and off for at least 5 years.

View File

@ -1,10 +0,0 @@
# Hi! You're early!
This is the root commit. There's nothing here yet.
The main branch is reserved for feature-complete and known-working refs.
If you're reading this on the head of the main branch, it's probably because
the project is in such early development that no feature-complete
and known-working refs exist yet. In this case, to take a look
at development progress, and/or test whatever incomplete content
is available thus far, you can checkout the dev branch instead.

21
addons/func_godot/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 func-godot
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,51 @@
<p align="center"><img src="https://github.com/func-godot/.github/assets/44485952/53bdc589-33e8-4a39-8707-01a5f850d155" alt="FuncGodotRanger" width="15%" />
<img src="https://github.com/func-godot/.github/assets/44485952/b7c19218-2089-4319-a2bd-6ce4b354c1ce" alt="FuncGodot" width="80%"/></p>
***FuncGodot*** is a plugin for [Godot 4](https://godotengine.org/) that allows users to generate Godot scenes using the [Quake MAP file format](https://quakewiki.org/wiki/Quake_Map_Format). Map files can be made in a variety of editors, the most commonly recommended one being [TrenchBroom](https://trenchbroom.github.io/). It is a reworking and rewrite of the [Qodot](https://github.com/QodotPlugin/Qodot) plugin for Godot 3 and 4.
[Full documentation is available online](https://func-godot.github.io/func_godot_docs/) as well as off. Release zip files come pre-packaged with the manual, but if you need to you can [download the most up-to-date standalone copy here](https://github.com/func-godot/func_godot_docs/releases/).
For more help or to contribute to the community, join us on the [Official FuncGodot Discord](https://discord.gg/eBQ7EfNZSZ)!
<p align="center"><img src="https://github.com/func-godot/.github/assets/44485952/0a4d2436-884e-4cee-94a8-220df3813627" alt="TrenchBroom" width="45%" />
<img src="https://github.com/func-godot/.github/assets/44485952/25e96e49-3482-40cf-ade9-99e83c3eca7d" alt="Godot FuncGodotMap Built" width="45%"/></p>
## Features
- Godot Scene Generation
- File support for Quake `map`, `wad`, and `lmp` palette formats
- File support for Half-Life `wad` format
- Meshes from `map` brush geometry
- Materials and UVs from `map` texture definitions
- Convex and concave collision shapes
- Entity Definition Support
- Fully customizable entities that can be defined for TrenchBroom and generated in Godot
- Leverage the `map` format's classname and key value pair systems
- Define the visual and collision properties of brush entities on a per-classname basis
- Retrieve easy to access mesh metadata for per face material information
- Define point entities that can be generated from node class name and script or from packed scenes
- Generate GLB display models with correct orientation and scale for point entities in map editors with GLTF support
- FGD (Forge Game Data) export
- TrenchBroom Integration
- GameConfig export
- Brush and Face Tags
- `model` keyword and scale expression
- NetRadiant Custom Integration
- Gamepack Export
- Shader definitions
- Customizable build options
## Confirmed Compatible Map Editors
- TrenchBroom
- J.A.C.K.
- NetRadiant Custom[br]
Help us add to this list by testing out your preferred map editor and helping us come up with compatibility solutions!
## Credits
FuncGodot was created by [Hannah "EMBYR" Crawford](https://embyr.sh/), [Emberlynn Bland](https://github.com/deertears/), and [Tim "RhapsodyInGeek" Maccabe](https://github.com/RhapsodyInGeek), reworked from the [Godot 4 port of Qodot](https://github.com/QodotPlugin/Qodot/tree/main) by Embyr, with contributions from members of the FuncGodot, Qodot, Godot, and Quake Mapping Communities.
Both plugins are based on the original [Qodot for Godot 3.5](https://github.com/QodotPlugin/qodot-plugin/) created by [Josh "Shifty" Palmer](https://twitter.com/ShiftyAxel).
<p align="center"><img src="https://github.com/func-godot/.github/assets/44485952/9ff9cd96-024b-4202-b4a2-611741b81609" alt="Godambler" /></p>

View File

@ -0,0 +1,36 @@
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=3 format=3 uid="uid://cxy7jnh6d7msn"]
[ext_resource type="Script" uid="uid://x05pvu4ag5m8" 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 = 4
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
add_textures_metadata = false
add_vertex_metadata = false
add_face_position_metadata = false
add_face_normal_metadata = false
add_collision_shape_face_range_metadata = false
classname = "func_detail"
description = "Static collidable geometry. Builds a StaticBody3D with a MeshInstance3D and a single concave CollisionShape3D. Does not occlude other VisualInstance3D nodes."
func_godot_internal = false
base_classes = Array[Resource]([ExtResource("1_c3bns")])
class_properties = {}
class_property_descriptions = {}
auto_apply_to_matching_node_properties = false
meta_properties = {
"color": Color(0.8, 0.8, 0.8, 1)
}
node_class = "StaticBody3D"
name_property = ""

View File

@ -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 = 4
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 = 4
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 = 4
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 = 4
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,31 @@
[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"
origin_texture = "special/origin"
texture_wads = Array[Resource]([])
material_file_extension = "tres"
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"
orm_map_pattern = "%s_orm.%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" uid="uid://w8xm4enwa7j3" 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,24 @@
[gd_resource type="Resource" script_class="NetRadiantCustomGamePackConfig" load_steps=6 format=3 uid="uid://cv1k2e85fo2ax"]
[ext_resource type="Resource" uid="uid://crgpdahjaj" path="res://addons/func_godot/fgd/func_godot_fgd.tres" id="1_gct4v"]
[ext_resource type="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"]
[ext_resource type="Resource" uid="uid://bpnj14oaufdpt" path="res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_origin.tres" id="4_8rl60"]
[resource]
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"), ExtResource("4_8rl60")])
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"
default_build_menu_variables = {}
default_build_menu_commands = {}

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://bpnj14oaufdpt"]
[ext_resource type="Script" path="res://addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd" id="1_ah2cp"]
[resource]
script = ExtResource("1_ah2cp")
texture_path = "textures/special/origin"
shader_attributes = Array[String](["qer_trans 0.4"])

View File

@ -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,35 @@
[gd_resource type="Resource" script_class="TrenchBroomGameConfig" load_steps=7 format=3 uid="uid://b44ah5b2000wa"]
[ext_resource type="Resource" uid="uid://crgpdahjaj" path="res://addons/func_godot/fgd/func_godot_fgd.tres" id="1_8u1vq"]
[ext_resource type="Resource" uid="uid://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://bkjxc54mmdhbo" path="res://addons/func_godot/game_config/trenchbroom/tb_face_tag_origin.tres" id="3_stisi"]
[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"
}])
textures_root_folder = "textures"
texture_exclusion_patterns = Array[String](["*_albedo", "*_ao", "*_emission", "*_height", "*_metallic", "*_normal", "*_orm", "*_roughness", "*_sss"])
palette_path = "textures/palette.lmp"
fgd_file = ExtResource("1_8u1vq")
entity_scale = "32"
brush_tags = Array[Resource]([])
brushface_tags = Array[Resource]([ExtResource("1_rsp20"), ExtResource("2_166i2"), ExtResource("3_stisi")])
default_uv_scale = Vector2(1, 1)
game_config_version = 0

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://bkjxc54mmdhbo"]
[ext_resource type="Script" path="res://addons/func_godot/src/trenchbroom/trenchbroom_tag.gd" id="1_enkfc"]
[resource]
script = ExtResource("1_enkfc")
tag_name = "Origin"
tag_attributes = Array[String](["transparent"])
tag_match_type = 0
tag_pattern = "origin"
texture_name = ""

View File

@ -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="2025.1"
script="src/func_godot_plugin.gd"

View File

@ -0,0 +1,136 @@
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 map_settings: FuncGodotMapSettings = null:
set(new):
if not new or new == map_settings: return
surface_gatherer.map_settings = new
map_settings = new
var surface_gatherer:= FuncGodotSurfaceGatherer.new(map_data, map_settings)
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.BOUNDS_CENTER)
var metadata_inclusion_flags: int = entity_defs.values()[i].get("metadata_inclusion_flags", FuncGodotMapData.FuncGodotEntityMetadataInclusionFlags.NONE)
map_data.set_entity_types_by_classname(classname, spawn_type, origin_type, metadata_inclusion_flags)
func get_texture_info(texture_name: String) -> FuncGodotMapData.FuncGodotTextureType:
if texture_name == map_settings.origin_texture:
return FuncGodotMapData.FuncGodotTextureType.ORIGIN
return FuncGodotMapData.FuncGodotTextureType.NORMAL
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_info(key, val.x, val.y, get_texture_info(key))
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) -> Dictionary:
var sg: FuncGodotSurfaceGatherer = FuncGodotSurfaceGatherer.new(map_data, map_settings)
sg.reset_params()
sg.split_type = FuncGodotSurfaceGatherer.SurfaceSplitType.ENTITY
const MFlags = FuncGodotMapData.FuncGodotEntityMetadataInclusionFlags
sg.metadata_skip_flags = MFlags.TEXTURES | MFlags.COLLISION_SHAPE_TO_FACE_RANGE_MAP
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.set_origin_filter_texture(map_settings.origin_texture)
sg.run()
return {
surfaces = fetch_surfaces(sg),
metadata = sg.out_metadata,
}
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.set_origin_filter_texture(map_settings.origin_texture)
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
const MFlags = FuncGodotMapData.FuncGodotEntityMetadataInclusionFlags
surface_gatherer.metadata_skip_flags |= MFlags.COLLISION_SHAPE_TO_FACE_RANGE_MAP
surface_gatherer.set_skip_filter_texture(map_settings.skip_texture)
surface_gatherer.set_origin_filter_texture(map_settings.origin_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.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 @@
uid://b24mq2kl3uos3

View File

@ -0,0 +1,381 @@
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 > 0.0:
return (n1.cross(n2) * f0.plane_dist + n2.cross(n0) * f1.plane_dist + n0.cross(n1) * f2.plane_dist) / denom
return null
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]
var entity_mins: Vector3 = Vector3.INF
var entity_maxs: Vector3 = Vector3.INF
var origin_mins: Vector3 = Vector3.INF
var origin_maxs: Vector3 = -Vector3.INF
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
# Check if this is a special brush (eg: origin)
var brush_texture_type: FuncGodotMapData.FuncGodotTextureType = FuncGodotMapData.FuncGodotTextureType.NORMAL
if brush.faces.size() > 0:
brush_texture_type = map_data.textures[brush.faces[0].texture_idx].type
# Check that all the faces match the same type
for face_idx in range(1,brush.faces.size()):
if map_data.textures[brush.faces[face_idx].texture_idx].type != brush_texture_type:
brush_texture_type = FuncGodotMapData.FuncGodotTextureType.NORMAL # Reset face type if it doesn't match
break
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:
if entity_mins != Vector3.INF:
entity_mins = entity_mins.min(vert.vertex)
else:
entity_mins = vert.vertex
if entity_maxs != Vector3.INF:
entity_maxs = entity_maxs.max(vert.vertex)
else:
entity_maxs = vert.vertex
if brush_texture_type == FuncGodotMapData.FuncGodotTextureType.ORIGIN:
if origin_mins != Vector3.INF:
origin_mins = origin_mins.min(vert.vertex)
else:
origin_mins = vert.vertex
if origin_maxs != Vector3.INF:
origin_maxs = origin_maxs.max(vert.vertex)
else:
origin_maxs = vert.vertex
brush.center += vert.vertex
vert_count += 1
if vert_count > 0:
brush.center /= float(vert_count)
# Default origin type is BOUNDS_CENTER
if entity_maxs != Vector3.INF and entity_mins != Vector3.INF:
entity.center = entity_maxs - ((entity_maxs - entity_mins) * 0.5)
if entity.origin_type != FuncGodotMapData.FuncGodotEntityOriginType.BOUNDS_CENTER and entity.brushes.size() > 0:
match entity.origin_type:
FuncGodotMapData.FuncGodotEntityOriginType.ABSOLUTE, FuncGodotMapData.FuncGodotEntityOriginType.RELATIVE:
if '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])
else: # OriginType.RELATIVE
entity.center += Vector3(origin_comps[0], origin_comps[1], origin_comps[2])
FuncGodotMapData.FuncGodotEntityOriginType.BRUSH:
if origin_mins != Vector3.INF:
entity.center = origin_maxs - ((origin_maxs - origin_mins) * 0.5)
FuncGodotMapData.FuncGodotEntityOriginType.BOUNDS_MINS:
entity.center = entity_mins
FuncGodotMapData.FuncGodotEntityOriginType.BOUNDS_MAXS:
entity.center = entity_maxs
FuncGodotMapData.FuncGodotEntityOriginType.AVERAGED:
entity.center = Vector3.ZERO
for b in range(entity.brushes.size()):
entity.center += entity.brushes[b].center
entity.center /= float(entity.brushes.size())
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 @@
uid://45ofko3isqax

View File

@ -0,0 +1,158 @@
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_info(name: String, width: int, height: int, type: FuncGodotTextureType) -> void:
for i in range(textures.size()):
if textures[i].name == name:
textures[i].width = width
textures[i].height = height
textures[i].type = type
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, meta_flags: int) -> void:
for entity in entities:
if entity.properties.has("classname") and entity.properties["classname"] == classname:
entity.metadata_inclusion_flags = meta_flags as FuncGodotMapData.FuncGodotEntityMetadataInclusionFlags
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.AVERAGED
func clear() -> void:
entities.clear()
entity_geo.clear()
textures.clear()
# --------------------------------------------------------------------------------------------------
# Nested Types
# --------------------------------------------------------------------------------------------------
enum FuncGodotEntitySpawnType {
WORLDSPAWN = 0,
MERGE_WORLDSPAWN = 1,
ENTITY = 2
}
enum FuncGodotEntityOriginType {
AVERAGED = 0,
ABSOLUTE = 1,
RELATIVE = 2,
BRUSH = 3,
BOUNDS_CENTER = 4,
BOUNDS_MINS = 5,
BOUNDS_MAXS = 6,
}
enum FuncGodotEntityMetadataInclusionFlags {
NONE = 0,
ENTITY_INDEX_RANGES = 1,
TEXTURES = 2,
VERTEX = 4,
FACE_POSITION = 8,
FACE_NORMAL = 16,
COLLISION_SHAPE_TO_FACE_RANGE_MAP = 32,
}
enum FuncGodotTextureType {
NORMAL = 0,
ORIGIN = 1
}
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
var metadata_inclusion_flags: FuncGodotEntityMetadataInclusionFlags
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
var type: FuncGodotTextureType
func _init(in_name: String):
name = in_name

View File

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

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 @@
uid://t2opsxxx7dvk

View File

@ -0,0 +1,217 @@
class_name FuncGodotSurfaceGatherer extends RefCounted
var map_data: FuncGodotMapData
var map_settings: FuncGodotMapSettings
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 origin_filter_texture_idx: int
var metadata_skip_flags: int
var out_surfaces: Array[FuncGodotMapData.FuncGodotFaceGeometry]
var out_metadata: Dictionary
func _init(in_map_data: FuncGodotMapData, in_map_settings: FuncGodotMapSettings) -> void:
map_data = in_map_data
map_settings = in_map_settings
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 set_origin_filter_texture(texture_name: String) -> void:
origin_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
# Omit faces textured with Clip
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 faces textured with Origin
if origin_filter_texture_idx != -1 and face.texture_idx == origin_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 texture_names: Array[StringName] = []
var textures: PackedInt32Array = []
var vertices: PackedVector3Array = []
var positions: PackedVector3Array = []
var normals: PackedVector3Array = []
var shape_index_ranges: Array[Vector2i] = []
var entity_index_ranges: Array[Vector2i] = []
var index_offset: int = 0
var entity_face_range: Vector2i = Vector2i.ZERO
const MFlags = FuncGodotMapData.FuncGodotEntityMetadataInclusionFlags
var build_entity_index_ranges: bool = not metadata_skip_flags & MFlags.ENTITY_INDEX_RANGES
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]
var shape_face_range := Vector2i.ZERO
var total_entity_tris := 0
var include_normals_metadata: bool = not metadata_skip_flags & MFlags.FACE_NORMAL and entity.metadata_inclusion_flags & MFlags.FACE_NORMAL
var include_vertices_metadata: bool = not metadata_skip_flags & MFlags.VERTEX and entity.metadata_inclusion_flags & MFlags.VERTEX
var include_textures_metadata: bool = not metadata_skip_flags & MFlags.TEXTURES and entity.metadata_inclusion_flags & MFlags.TEXTURES
var include_positions_metadata: bool = not metadata_skip_flags & MFlags.FACE_POSITION and entity.metadata_inclusion_flags & MFlags.FACE_POSITION
var include_shape_range_metadata: bool = not metadata_skip_flags & MFlags.COLLISION_SHAPE_TO_FACE_RANGE_MAP and entity.metadata_inclusion_flags & MFlags.COLLISION_SHAPE_TO_FACE_RANGE_MAP
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]
var total_brush_tris:= 0
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]
var face: FuncGodotMapData.FuncGodotFace = brush.faces[f]
var num_tris = face_geo.vertices.size() - 2
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)
if include_normals_metadata:
var normal := Vector3(face.plane_normal.y, face.plane_normal.z, face.plane_normal.x)
for i in num_tris:
normals.append(normal)
if include_shape_range_metadata or build_entity_index_ranges:
total_brush_tris += num_tris
if include_textures_metadata:
var texname := StringName(map_data.textures[face.texture_idx].name)
var index: int
if texture_names.is_empty():
texture_names.append(texname)
index = 0
elif texture_names.back() == texname:
# Common case, faces with textures are next to each other
index = texture_names.size() - 1
else:
var texture_name_index: int = texture_names.find(texname)
if texture_name_index == -1:
index = texture_names.size()
texture_names.append(texname)
else:
index = texture_name_index
# Metadata addresses triangles, so we have to duplicate the info for each tri
for i in num_tris:
textures.append(index)
var avg_vertex_pos := Vector3.ZERO
var avg_vertex_pos_ct: int = 0
for i in range(num_tris * 3):
surf.indicies.append(face_geo.indicies[i] + index_offset)
var vertex: Vector3 = surf.vertices[surf.indicies.back()].vertex
vertex = Vector3(vertex.y, vertex.z, vertex.x) * map_settings.scale_factor
if include_vertices_metadata:
vertices.append(vertex)
if include_positions_metadata:
avg_vertex_pos_ct += 1
avg_vertex_pos += vertex
if avg_vertex_pos_ct == 3:
avg_vertex_pos /= 3
positions.append(avg_vertex_pos)
avg_vertex_pos = Vector3.ZERO
avg_vertex_pos_ct = 0
index_offset += face_geo.vertices.size()
if include_shape_range_metadata:
shape_face_range.x = shape_face_range.y
shape_face_range.y = shape_face_range.x + total_brush_tris
shape_index_ranges.append(shape_face_range)
if build_entity_index_ranges:
total_entity_tris += total_brush_tris
if build_entity_index_ranges:
entity_face_range.x = entity_face_range.y
entity_face_range.y = entity_face_range.x + total_entity_tris
entity_index_ranges.append(entity_face_range)
out_metadata = {
textures = textures,
texture_names = texture_names,
normals = normals,
vertices = vertices,
positions = positions,
shape_index_ranges = shape_index_ranges,
}
if build_entity_index_ranges:
out_metadata["entity_index_ranges"] = entity_index_ranges
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
metadata_skip_flags = FuncGodotMapData.FuncGodotEntityMetadataInclusionFlags.ENTITY_INDEX_RANGES
# nested
enum SurfaceSplitType{
NONE,
ENTITY,
BRUSH
}

View File

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

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 @@
uid://xvjltt7tv5oo

View File

@ -0,0 +1,217 @@
@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 = {}
## Automatically applies entity class properties to matching properties in the generated node. When using this feature, class properties need to be the correct type or you may run into errors on map build.
@export var auto_apply_to_matching_node_properties : bool = false
## Appearance properties for the map editor. See the [**Valve 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("\"", "\'")
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]
if typeof(choice_val) == TYPE_STRING:
if not (choice_val as String).begins_with("\""):
choice_val = "\"" + choice_val + "\""
prop_val += "\t\t" + str(choice_val) + " : \"" + choice + "\"" + FuncGodotUtil.newline()
prop_val += "\t]"
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"
prop_val = "\"\""
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"
prop_val = "\"\""
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 @@
uid://c4ayooijesh7b

View File

@ -0,0 +1,166 @@
@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)
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
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 @@
uid://cco5k31h73edb

View File

@ -0,0 +1,166 @@
@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 :
get:
return generate_gd_ignore_file
set(ignore):
if (ignore != generate_gd_ignore_file):
if Engine.is_editor_hint():
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 @@
uid://dnfn41t46vuih

View File

@ -0,0 +1,23 @@
@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
## Toggles whether entity will use `scale` to determine the generated node or scene's scale. This is performed on the top level node. The property can be a [float], [Vector3], or [Vector2].
@export var apply_scale_on_map_build: bool = true

View File

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

View File

@ -0,0 +1,88 @@
@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 {
AVERAGED = 0, ## Use averaged brush vertices for center position. This is the old Qodot behavior.
ABSOLUTE = 1, ## Use `origin` class property in global coordinates as the center position.
RELATIVE = 2, ## Calculate center position using `origin` class property as an offset to the entity's bounding box center.
BRUSH = 3, ## Calculate center position based on the bounding box center of all brushes using the 'origin' texture specified in the [FuncGodotMapSettings].
BOUNDS_CENTER = 4, ## Use the center of the entity's bounding box for center position. This is the default option and recommended for most entities.
BOUNDS_MINS = 5, ## Use the lowest bounding box coordinates for center position. This is standard Quake and Half-Life brush entity behavior.
BOUNDS_MAXS = 6, ## Use the highest bounding box coordinates for center position.
}
enum CollisionShapeType {
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 determines its center position. Only valid if [member spawn_type] is set to ENTITY.
@export var origin_type: OriginType = OriginType.BOUNDS_CENTER
@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
## The following properties tell FuncGodot to add a [i]"func_godot_mesh_data"[/i] Dictionary to the metadata of the generated node upon build.
## This data is parallelized, so that each element of the array is ordered to reference the same face in the mesh.
@export_group("Mesh Metadata")
## Add a texture lookup table to the generated node's metadata on build.[br][br]
## The data is split between an [Array] of [StringName] called [i]"texture_names"[/i] containing all currently used texture materials
## and a [PackedInt32Array] called [i]"textures"[/i] where each element is an index corresponding to the [i]"texture_names"[/i] entries.
@export var add_textures_metadata: bool = false
## Add a [PackedVector3Array] called [i]"vertices"[/i] to the generated node's metadata on build.[br][br]
## This is a list of every vertex in the generated node's [MeshInstance3D]. Every 3 vertices represent a single face.
@export var add_vertex_metadata: bool = false
## Add a [PackedVector3Array] called [i]"positions"[/i] to the generated node's metadata on build.[br][br]
## This is a list of positions for each face, local to the generated node, calculated by averaging the vertices to find the face's center.
@export var add_face_position_metadata = false
## Add a [PackedVector3Array] called [i]"normals"[/i] to the generated node's metadata on build.[br][br]
## Contains a list of each face's normal.
@export var add_face_normal_metadata = false
## Add a [Dictionary] called [i]"collision_shape_to_face_range_map"[/i] in the generated node's metadata on build.[br][br]
## Contains keys of strings, which are the names of child [CollisionShape3D] nodes, and values of
## [Vector2i], where [i]X[/i] represents the starting index of that child's faces and [i]Y[/i] represents the
## ending index.[br][br]
## For example, an element of [br][br][code]{ "entity_1_brush_0_collision_shape" : Vector2i(0, 15) }[/code][br][br]
## shows that this solid class has been generated with one child collision shape named
## [i]entity_1_brush_0_collision_shape[/i] which handles the first 15 faces of the parts of the mesh with collision.
@export var add_collision_shape_face_range_metadata = false
@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 @@
uid://x05pvu4ag5m8

View File

@ -0,0 +1,186 @@
@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)
ProjectSettings.set("func_godot/default_map_settings", "res://addons/func_godot/func_godot_default_map_settings.tres")
var property_info = {
"name": "func_godot/default_map_settings",
"type": TYPE_STRING,
"hint": PROPERTY_HINT_FILE,
"hint_string": "*.tres"
}
ProjectSettings.add_property_info(property_info)
ProjectSettings.set_initial_value("func_godot/default_map_settings", "res://addons/func_godot/func_godot_default_map_settings.tres")
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_INSPECTOR_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 @@
uid://j0jkcc407iro

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 @@
uid://whl8no362161

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 @@
uid://dnyr3it622ra3

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 @@
uid://m5bg1u782i4m

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 @@
uid://ec840o473e71

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 = Dictionary()):
self.textures = textures

View File

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

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 'Quake 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 @@
uid://bdwl0c5ybl14s

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,85 @@
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
@tool
## 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.
var scale_factor: float = 0.03125
@export var inverse_scale_factor: float = 32.0 :
set(value):
inverse_scale_factor = value
scale_factor = 1.0 / value
## [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 path for the origin texture, relative to [member base_texture_dir]. Brush faces textured with the origin texture will have those faces removed from the generated [MeshInstance3D]. The bounds of these faces will be used to calculate the origin point of the entity.
@export var origin_texture: String = "special/origin"
## 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"
## 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.
## Any Layers toggled to be omitted from export in TrenchBroom will not be built.
@export var use_trenchbroom_groups_hierarchy: bool = false

View File

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

View File

@ -0,0 +1,274 @@
@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"),
preload("res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_origin.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_menu_commands] values.
@export var default_build_menu_variables: Dictionary
## Commands to include in the exported gamepack's [code]default_build_menu.xml[/code].[br][br]
## Keys, specified as a [String], define the build option name as you want it to appear in 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_menu_variables]
## by using brackets: [code][variable key name][/code]
@export var default_build_menu_commands: Dictionary
## Generates completed text for a .shader file.
func build_shader_text() -> String:
var shader_text: String = ""
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 @@
uid://66p8vvvsl43w

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 @@
uid://rw1omaqusjdp

View File

@ -0,0 +1,331 @@
@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
## Keeps track of each individual version
enum GameConfigVersion {
Latest,
Version4,
Version8,
Version9
}
## 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" }
]
@export_category("Textures")
## Path to top level textures folder relative to the game path. Also referred to as materials in the latest versions of TrenchBroom.
@export var textures_root_folder: String = "textures"
## Textures matching these patterns will be hidden from TrenchBroom.
@export var texture_exclusion_patterns: Array[String] = ["*_albedo", "*_ao", "*_emission", "*_height", "*_metallic", "*_normal", "*_orm", "*_roughness", "*_sss"]
## Palette path relative to your Game Path. Only needed for Quake WAD2 files. Half-Life WAD3 files contain the palettes within the texture information.
@export var palette_path: String = "textures/palette.lmp"
@export_category("Entities")
## 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"
## Arrays containing the TrenchBroomTag resource type.
@export_category("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"),
preload("res://addons/func_godot/game_config/trenchbroom/tb_face_tag_origin.tres")
]
@export_category("Face Attributes")
## Default scale of textures on new brushes and when UV scale is reset.
@export var default_uv_scale : Vector2 = Vector2(1, 1)
@export_category("Compatibility")
## Game configuration format compatible with the version of TrenchBroom being used.
@export var game_config_version: GameConfigVersion = GameConfigVersion.Latest
## Matches tag key enum to the String name used in .cfg
static func get_match_key(tag_match_type: int) -> String:
match tag_match_type:
TrenchBroomTag.TagMatchType.TEXTURE:
return "material"
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 = ""
match game_config_version:
GameConfigVersion.Latest, GameConfigVersion.Version8, GameConfigVersion.Version9:
config_text = get_game_config_v9v8_text() % [
game_name,
map_formats_str,
textures_root_folder,
texture_exclusion_patterns_str,
palette_path,
fgd_filename_str,
entity_scale,
brush_tags_str,
brushface_tags_str,
uv_scale_str
]
GameConfigVersion.Version4:
config_text = get_game_config_v4_text() % [
game_name,
map_formats_str,
textures_root_folder,
texture_exclusion_patterns_str,
palette_path,
fgd_filename_str,
entity_scale,
brush_tags_str,
brushface_tags_str,
uv_scale_str
]
_:
push_error("Unsupported Game Config Version!")
return config_text
## Converts brush, 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\"material\": \"%s\"" % brush_tag.texture_name
tags_str += "\n"
tags_str += "\t\t\t}"
if brush_tag != tags[-1]:
tags_str += ","
if game_config_version > GameConfigVersion.Latest and game_config_version < GameConfigVersion.Version9:
tags_str = tags_str.replace("material", "texture")
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")
#region GameConfigDeclarations
func get_game_config_v4_text() -> String:
return """\
{
"version": 4,
"name": "%s",
"icon": "icon.png",
"fileformats": [
%s
],
"filesystem": {
"searchpath": ".",
"packageformat": { "extension": ".zip", "format": "zip" }
},
"textures": {
"package": { "type": "directory", "root": "%s" },
"format": { "extensions": ["jpg", "jpeg", "tga", "png", "D", "C"], "format": "image" },
"excludes": [ %s ],
"palette": "%s",
"attribute": ["_tb_textures", "wad"]
},
"entities": {
"definitions": [ %s ],
"defaultcolor": "0.6 0.6 0.6 1.0",
"modelformats": [ "bsp, mdl, md2" ],
"scale": %s
},
"tags": {
"brush": [
%s
],
"brushface": [
%s
]
},
"faceattribs": {
"defaults": {
%s
},
"contentflags": [],
"surfaceflags": []
}
}
"""
func get_game_config_v9v8_text() -> String:
var config_text: String = """\
{
"version": 9,
"name": "%s",
"icon": "icon.png",
"fileformats": [
%s
],
"filesystem": {
"searchpath": ".",
"packageformat": { "extension": ".zip", "format": "zip" }
},
"materials": {
"root": "%s",
"extensions": [".bmp", ".exr", ".hdr", ".jpeg", ".jpg", ".png", ".tga", ".webp", ".D", ".C"],
"excludes": [ %s ],
"palette": "%s",
"attribute": "wad"
},
"entities": {
"definitions": [ %s ],
"defaultcolor": "0.6 0.6 0.6 1.0",
"scale": %s
},
"tags": {
"brush": [
%s
],
"brushface": [
%s
]
},
"faceattribs": {
"defaults": {
%s
},
"contentflags": [],
"surfaceflags": []
}
}
"""
if game_config_version == GameConfigVersion.Version8:
config_text = config_text.replace(": 9,", ": 8,")
config_text = config_text.replace("material", "texture")
return config_text
#endregion

View File

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

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 @@
uid://bxsq1l5s778ax

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 @@
uid://w8xm4enwa7j3

View File

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

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