Introducing a copy of the id Tech 4 EntityDef system into Nuclide.
This commit is contained in:
parent
054e703177
commit
d45d2f1dc4
7 changed files with 330 additions and 0 deletions
45
Documentation/EntityDef.md
Normal file
45
Documentation/EntityDef.md
Normal file
|
@ -0,0 +1,45 @@
|
|||
# EntityDef
|
||||
|
||||
## Overview
|
||||
|
||||
In **id Tech 4**, we have been introduced to external entity definitions. They are pretty straightforward by merely being a set of default values for an existing entity class.
|
||||
|
||||
This can be used in a variety of ways.
|
||||
|
||||
- Create new entity classes with new behaviour without any code
|
||||
- Create a base class that other entityDefs can inherit from
|
||||
- Create prefabs that get updated across all maps
|
||||
|
||||
This system can also be used in combination with [MapTweaks](Documentation/MapTweaks.md), where an entityDef is used instead of a base class depending on the parameters you decide to test.
|
||||
|
||||
## Syntax
|
||||
|
||||
Let's take a look at an example **EntityDef**:
|
||||
|
||||
```
|
||||
entityDef item_health_small {
|
||||
"spawnclass" "item_health"
|
||||
"spawnflags" "1"
|
||||
}
|
||||
```
|
||||
|
||||
This is from the port of *Deathmatch Classic*, and will ensure that **item_health_small** from *Quake III Arena* gets spawned as an **item_health** with its *spawnflags* set to the one that will make it a small health pickup.
|
||||
|
||||
You can have as many key/value pairs as you like. However, you can only specify one *spawnclass* and you cannot do circular inheritance.
|
||||
|
||||
## Special Keys
|
||||
|
||||
These are reserved keys and are meant to be used by the **build_game.sh** script to generate an entities.def file for your game.
|
||||
|
||||
| Key | Description |
|
||||
|--------------------|------------------------------------------------------------------------|
|
||||
| editor_usage | Description text used by the editor. |
|
||||
| editor_model | Model to use in the editor. |
|
||||
| editor_var **KEY** | Description text for the specified key. |
|
||||
| editor_color | Normalized color vector defining the bounding box color in the editor. |
|
||||
| editor_mins | Vector defining the mins of the entity bounding box. |
|
||||
| editor_maxs | Vector defining the maxs of the entity bounding box. |
|
||||
|
||||
# References
|
||||
|
||||
- [id.sdk page on Entity Defs](http://icculus.org/~marco/notmine/id-dev/www.iddevnet.com/doom3/entitydefs.html)
|
36
Documentation/MapTweaks.md
Normal file
36
Documentation/MapTweaks.md
Normal file
|
@ -0,0 +1,36 @@
|
|||
# MapTweaks
|
||||
|
||||
## Overview
|
||||
|
||||
This is a very customizable system that applies changes to levels/maps depending on a variable amount of parameters. It was invented specifically for Nuclide and designed to work together with [EntityDefs](Documentation/EntityDef.md).
|
||||
|
||||
## Syntax
|
||||
|
||||
All MapTweaks are defined within `scripts/maptweaks.txt`.
|
||||
|
||||
Let's take a look at an example **MapTweak**:
|
||||
|
||||
```
|
||||
hldm_tweak
|
||||
{
|
||||
when-cvar deathmatch equals 2
|
||||
when-serverinfo *bspversion equals 30
|
||||
|
||||
replace weapon_gauss info_null
|
||||
replace weapon_egon info_null
|
||||
}
|
||||
```
|
||||
|
||||
The `hldm_tweaks` is just a user-defined name. It doesn't affect functionality.
|
||||
|
||||
The `when-cvar` and `when-serverinfo` lines are **checks**. each one is checked individually and only if all are positive will the `replace` lines take effect.
|
||||
|
||||
You can have as many lines in there as you like.
|
||||
|
||||
Other than `equals`, you can also use one of the following keywords when comparing values:
|
||||
|
||||
- **less-than**
|
||||
- **greater-than**
|
||||
- **is-not**
|
||||
|
||||
At this time, `when-cvar` and `when-serverinfo` only do comparisons on numbers. So you cannot check for strings at this time.
|
2
Doxyfile
2
Doxyfile
|
@ -888,8 +888,10 @@ INPUT = src/ \
|
|||
Documentation/Materials/MatShaders.md \
|
||||
Documentation/Materials/MatGoldSrc.md \
|
||||
Documentation/Shaders/ \
|
||||
Documentation/EntityDef.md \
|
||||
Documentation/Surf_data.md \
|
||||
Documentation/Prop_data.md \
|
||||
Documentation/MapTweaks.md \
|
||||
Documentation/Sound/ \
|
||||
Documentation/Models/ \
|
||||
Documentation/History.md \
|
||||
|
|
239
src/server/entityDef.qc
Normal file
239
src/server/entityDef.qc
Normal file
|
@ -0,0 +1,239 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Vera Visions LLC.
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
|
||||
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
/* entityDef implementation
|
||||
|
||||
these definitions are a further abstraction from how we view
|
||||
entity definitions. this system tries to be mostly compatible
|
||||
with the def system in id Tech 4 (Doom 3, Quake 4, Prey, etc.)
|
||||
|
||||
however, we are not aiming for full compatibility right now as
|
||||
that will require further abstraction.
|
||||
|
||||
that said, the origin of this idea dates way back to when
|
||||
Team Fortress Software created MapC for Team Fortress 2 when
|
||||
it was originally on Quake II's engine.
|
||||
|
||||
http://www.teamfortress.com/tfii/mc2mapc.html (go to the wayback machine for this)
|
||||
|
||||
the gist is, that an entity def can set a base spawnclass (e.g. func_door)
|
||||
and populate it with key/value pairs. the amount of code the programmers
|
||||
has to implement is massively reduced and we can create prefabs much easier
|
||||
as a result.
|
||||
|
||||
overview:
|
||||
|
||||
entityDef func_illusionary {
|
||||
"spawnclass" "func_wall"
|
||||
"solid" "0"
|
||||
"movetype"
|
||||
"0"
|
||||
}
|
||||
*/
|
||||
|
||||
/* games can feel free to set this to whatever you need. */
|
||||
#ifndef ENTITYDEF_MAX
|
||||
#define ENTITYDEF_MAX 128
|
||||
#endif
|
||||
|
||||
typedef struct
|
||||
{
|
||||
string entClass;
|
||||
string spawnClass;
|
||||
string spawnData;
|
||||
string inheritKeys;
|
||||
} entityDef_t;
|
||||
|
||||
entityDef_t g_entDefTable[ENTITYDEF_MAX];
|
||||
var int g_entDefCount;
|
||||
|
||||
void
|
||||
EntityDef_ReadFile(string filePath)
|
||||
{
|
||||
filestream defFile;
|
||||
string tempString = "";
|
||||
entityDef_t currentDef;
|
||||
int braceDepth = 0i;
|
||||
string lastWord = __NULL__;
|
||||
|
||||
currentDef.entClass = "";
|
||||
currentDef.spawnClass = "";
|
||||
currentDef.spawnData = "";
|
||||
currentDef.inheritKeys = "";
|
||||
|
||||
/* bounds check */
|
||||
if (g_entDefCount >= ENTITYDEF_MAX) {
|
||||
error(sprintf("EntityDef_ReadFile: reached limit of %d defs\n", ENTITYDEF_MAX));
|
||||
}
|
||||
|
||||
/* open file */
|
||||
defFile = fopen(filePath, FILE_READ);
|
||||
if (defFile < 0) {
|
||||
error(sprintf("EntityDef_ReadFile: unable to read %S\n", filePath));
|
||||
}
|
||||
|
||||
/* line by line */
|
||||
while ((tempString = fgets(defFile))) {
|
||||
int lineSegments = tokenize_console(tempString);
|
||||
|
||||
/* word for word */
|
||||
for (int i = 0i; i < lineSegments; i++) {
|
||||
string word = argv(i);
|
||||
|
||||
switch (word) {
|
||||
case "{":
|
||||
braceDepth++;
|
||||
break;
|
||||
case "}":
|
||||
braceDepth--;
|
||||
|
||||
/* we've reached the end of a definition */
|
||||
if (braceDepth == 0) {
|
||||
/* we have something somewhat valid I guess */
|
||||
if (currentDef.entClass != "" && currentDef.spawnClass != "") {
|
||||
g_entDefTable[g_entDefCount].entClass = currentDef.entClass;
|
||||
g_entDefTable[g_entDefCount].spawnClass = currentDef.spawnClass;
|
||||
g_entDefTable[g_entDefCount].spawnData = currentDef.spawnData;
|
||||
g_entDefTable[g_entDefCount].inheritKeys = currentDef.inheritKeys;
|
||||
|
||||
/* increment the def count */
|
||||
if (g_entDefCount < ENTITYDEF_MAX)
|
||||
g_entDefCount++;
|
||||
}
|
||||
currentDef.entClass = "";
|
||||
currentDef.spawnClass = "";
|
||||
currentDef.spawnData = "";
|
||||
currentDef.inheritKeys = "";
|
||||
}
|
||||
break;
|
||||
default:
|
||||
/* anything outside braces defines the classname for the next def */
|
||||
if (braceDepth == 0 && lastWord == "entityDef") {
|
||||
currentDef.entClass = word;
|
||||
} else if (braceDepth == 1) {
|
||||
/* spawnclass is reserved and the next keyword specs it */
|
||||
if (word == "spawnclass") {
|
||||
currentDef.spawnClass = argv(i+1);
|
||||
i++;
|
||||
} else if (word == "inherit") {
|
||||
currentDef.inheritKeys = argv(i+1);
|
||||
i++;
|
||||
} else if (substring(word, 0, 7) == "editor_") {
|
||||
/* do nothing */
|
||||
i++;
|
||||
} else { /* rest gets dumped into spawndata */
|
||||
currentDef.spawnData = strcat(currentDef.spawnData, "\"", word, "\"", " ");
|
||||
}
|
||||
}
|
||||
}
|
||||
lastWord = word;
|
||||
}
|
||||
}
|
||||
|
||||
/* clean up */
|
||||
fclose(defFile);
|
||||
}
|
||||
|
||||
void
|
||||
EntityDef_Init(void)
|
||||
{
|
||||
searchhandle pm;
|
||||
pm = search_begin("def/*.def", TRUE, TRUE);
|
||||
for (int i = 0; i < search_getsize(pm); i++) {
|
||||
EntityDef_ReadFile(search_getfilename(pm, i));
|
||||
}
|
||||
search_end(pm);
|
||||
|
||||
#if 0
|
||||
for (int i = 0i; i < g_entDefCount; i++) {
|
||||
int numKeys = tokenize_console(g_entDefTable[i].spawnData);
|
||||
print(sprintf("edef %i: %S\n", i, g_entDefTable[i].entClass));
|
||||
print(sprintf("\tspawnclass: %S\n", g_entDefTable[i].spawnClass));
|
||||
print(sprintf("\tinheritKeys: %S\n", g_entDefTable[i].inheritKeys));
|
||||
print(sprintf("\tspawnData:\n", g_entDefTable[i].spawnData));
|
||||
|
||||
for (int c = 0; c < numKeys; c+=2) {
|
||||
print(sprintf("\t\t%S %S\n", argv(c), argv(c+1)));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static void
|
||||
EntityDef_PrepareEntity(entity target, int id)
|
||||
{
|
||||
string spawnClass;
|
||||
int spawnWords = 0i;
|
||||
NSEntity targetEnt = (NSEntity)target;
|
||||
entity oldSelf = self;
|
||||
|
||||
/* first we spawn it as the base spawnclass */
|
||||
if (!isfunction(g_entDefTable[id].spawnClass)) {
|
||||
spawnClass = strcat("spawnfunc_", g_entDefTable[id].spawnClass);
|
||||
} else {
|
||||
spawnClass = g_entDefTable[id].spawnClass;
|
||||
}
|
||||
|
||||
/* init */
|
||||
self = target;
|
||||
callfunction(spawnClass);
|
||||
self = oldSelf;
|
||||
|
||||
/* first load all keys we inherit from the 'inherited' class */
|
||||
for (int x = 0; x < g_entDefCount; x++) {
|
||||
/* found the thing we're supposed to inherit */
|
||||
if (g_entDefTable[x].entClass == g_entDefTable[id].inheritKeys) {
|
||||
spawnWords = tokenize_console(g_entDefTable[x].spawnData);
|
||||
for (int i = 0; i < spawnWords; i+= 2) {
|
||||
targetEnt.SpawnKey(argv(i), argv(i+1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* now we load the field overrides from the entDef */
|
||||
spawnWords = tokenize_console(g_entDefTable[id].spawnData);
|
||||
for (int i = 0; i < spawnWords; i+= 2) {
|
||||
targetEnt.SpawnKey(argv(i), argv(i+1));
|
||||
}
|
||||
|
||||
/* now we load our own spawndata, which starts and ends with braces */
|
||||
spawnWords = tokenize_console(__fullspawndata);
|
||||
for (int i = 1; i < (spawnWords - 1); i+= 2) {
|
||||
|
||||
/* ignore this, always */
|
||||
if (argv(i) != "classname")
|
||||
targetEnt.SpawnKey(argv(i), argv(i+1));
|
||||
}
|
||||
targetEnt.Spawned();
|
||||
targetEnt.Respawn();
|
||||
|
||||
/* now we rename the classname for better visibility */
|
||||
self.classname = g_entDefTable[id].entClass;
|
||||
__fullspawndata = "";
|
||||
}
|
||||
|
||||
bool
|
||||
EntityDef_SpawnClassname(string className)
|
||||
{
|
||||
for (int i = 0i; i < g_entDefCount; i++) {
|
||||
if (className == g_entDefTable[i].entClass) {
|
||||
EntityDef_PrepareEntity(self, i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
|
@ -491,6 +491,8 @@ worldspawn(void)
|
|||
lightstyle(12, "mmnnmmnnnmmnn");
|
||||
lightstyle(63, "a");
|
||||
Skill_Init();
|
||||
|
||||
EntityDef_Init();
|
||||
MapTweaks_Init();
|
||||
|
||||
precache_model("models/error.vvm");
|
||||
|
@ -770,6 +772,8 @@ to remove in case we won't initialize it.
|
|||
void
|
||||
CheckSpawn(void() spawnfunc)
|
||||
{
|
||||
if (EntityDef_SpawnClassname(self.classname))
|
||||
return;
|
||||
if (MapTweak_EntitySpawn(self))
|
||||
return;
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ vote.qc
|
|||
weapons.qc
|
||||
modelevent.qc
|
||||
mapcycle.qc
|
||||
entityDef.qc
|
||||
maptweaks.qc
|
||||
entry.qc
|
||||
#endlist
|
||||
|
|
|
@ -167,6 +167,9 @@ MapTweak_FinishSpawn(entity targetEntity, string newClassname)
|
|||
entity oldSelf = self;
|
||||
self = targetEntity;
|
||||
|
||||
if (EntityDef_SpawnClassname(newClassname))
|
||||
return;
|
||||
|
||||
if (!isfunction(newClassname)) {
|
||||
self.classname = strcat("spawnfunc_", newClassname);
|
||||
} else {
|
||||
|
|
Loading…
Reference in a new issue