From d45d2f1dc4cdf2a0c13b1390c58b1e49c892e3a8 Mon Sep 17 00:00:00 2001 From: Marco Cawthorne Date: Sun, 30 Apr 2023 18:01:26 -0700 Subject: [PATCH] Introducing a copy of the id Tech 4 EntityDef system into Nuclide. --- Documentation/EntityDef.md | 45 +++++++ Documentation/MapTweaks.md | 36 ++++++ Doxyfile | 2 + src/server/entityDef.qc | 239 +++++++++++++++++++++++++++++++++++++ src/server/entry.qc | 4 + src/server/include.src | 1 + src/server/maptweaks.qc | 3 + 7 files changed, 330 insertions(+) create mode 100644 Documentation/EntityDef.md create mode 100644 Documentation/MapTweaks.md create mode 100644 src/server/entityDef.qc diff --git a/Documentation/EntityDef.md b/Documentation/EntityDef.md new file mode 100644 index 00000000..bf7cb311 --- /dev/null +++ b/Documentation/EntityDef.md @@ -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) \ No newline at end of file diff --git a/Documentation/MapTweaks.md b/Documentation/MapTweaks.md new file mode 100644 index 00000000..6ecc2f4c --- /dev/null +++ b/Documentation/MapTweaks.md @@ -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. \ No newline at end of file diff --git a/Doxyfile b/Doxyfile index 5383afef..90297f20 100644 --- a/Doxyfile +++ b/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 \ diff --git a/src/server/entityDef.qc b/src/server/entityDef.qc new file mode 100644 index 00000000..62059a8b --- /dev/null +++ b/src/server/entityDef.qc @@ -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; +} diff --git a/src/server/entry.qc b/src/server/entry.qc index 6f29add7..ada9ee1e 100644 --- a/src/server/entry.qc +++ b/src/server/entry.qc @@ -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; diff --git a/src/server/include.src b/src/server/include.src index 9096581f..b32c8f5c 100644 --- a/src/server/include.src +++ b/src/server/include.src @@ -12,6 +12,7 @@ vote.qc weapons.qc modelevent.qc mapcycle.qc +entityDef.qc maptweaks.qc entry.qc #endlist diff --git a/src/server/maptweaks.qc b/src/server/maptweaks.qc index 5b7e25d6..498fed72 100644 --- a/src/server/maptweaks.qc +++ b/src/server/maptweaks.qc @@ -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 {