|
| 1 | + |
| 2 | +The FTEQW engine offers many improvements in changelevel functionality over the |
| 3 | +original Quake engine from 1996. In the original engine, your only way to send |
| 4 | +player data across a level transition was through the `parm` globals in the |
| 5 | +QuakeC server code, which meant you had exactly 16 floating-point numbers to |
| 6 | +use to store any data the players needed to remember across a level change. |
| 7 | +Everything else about the player's state was dropped. This worked well enough |
| 8 | +for Quake, but is severely limiting if you need to send anything that couldn't |
| 9 | +fit into a `float`. |
| 10 | + |
| 11 | +Not only does FTEQW offer 64 `parm` globals instead of 16 (for DarkPlaces |
| 12 | +compatibility), it also has a `parm_string` global which can store a |
| 13 | +theoretically unlimited amount of string or buffer data. In this document I'll |
| 14 | +show you how I decided to use it for my projects. |
| 15 | + |
| 16 | +When the player spawns in a level for the first time, the function |
| 17 | +`SetNewParms()` is called in the SSQC progs. This is where you set the default |
| 18 | +`parm` values for every player that spawns in a level. For example, let's say |
| 19 | +that `parm1` is going to store the player's health value across a level change. |
| 20 | +You would set the default health value like so: |
| 21 | + |
| 22 | +```c |
| 23 | +void() SetNewParms = |
| 24 | +{ |
| 25 | + parm1 = 100; |
| 26 | +}; |
| 27 | +``` |
| 28 | + |
| 29 | +When the engine is sending the players through a level transition, it calls the |
| 30 | +function `SetChangeParms()` for each player entity in sequence. For storing the |
| 31 | +player's health, you would do it like so: |
| 32 | + |
| 33 | +```c |
| 34 | +void() SetChangeParms = |
| 35 | +{ |
| 36 | + parm1 = self.health; |
| 37 | +}; |
| 38 | +``` |
| 39 | + |
| 40 | +But, as stated before, this can get rather limiting if the player has data that |
| 41 | +doesn't fit into a floating point number, such as a dynamic inventory of items |
| 42 | +and weapons or an stats tree or something. This is where `parm_string` comes |
| 43 | +in. If you're using a modern version of FTEQW and the FTEQCC compiler, you also |
| 44 | +have access to several JSON parsing builtins. We will be using these to make |
| 45 | +the usage of `parm_string` easier. |
| 46 | + |
| 47 | +If we wanted to store more complex data in `parm_string`, the default value for |
| 48 | +it should be a valid empty JSON node, like so: |
| 49 | + |
| 50 | +```c |
| 51 | +void() SetNewParms = |
| 52 | +{ |
| 53 | + parm1 = 100; |
| 54 | + parm_string = "{}"; |
| 55 | +}; |
| 56 | +``` |
| 57 | + |
| 58 | +And when `SetChangeParms()` is called, you will write your extended data as |
| 59 | +valid JSON entries in `parm_string`, like so: |
| 60 | + |
| 61 | +```c |
| 62 | +void() SetChangeParms = |
| 63 | +{ |
| 64 | + parm1 = self.health; |
| 65 | + parm_string = strcat( |
| 66 | + "{\"some_extended_field_string\":\"", |
| 67 | + self.some_extended_field_string, |
| 68 | + "\"}" |
| 69 | + ); |
| 70 | +}; |
| 71 | +``` |
| 72 | + |
| 73 | +Note we are using the `strcat()` builtin to construct our JSON string rather |
| 74 | +than `sprintf()`, because `sprintf()` seems to have some difficulties with very |
| 75 | +long strings. |
| 76 | + |
| 77 | +The above code will store a per-player string value called |
| 78 | +`some_extended_field_string` into `parm_string` for storage across the level |
| 79 | +transition. Ideally you'd wanna make some helper functions to easily write and |
| 80 | +format your JSON, but this is just a basic example that constructs it directly. |
| 81 | + |
| 82 | +Now the interesting part comes when it's time to read the data back. You'd want |
| 83 | +to do this in the function `PutClientInServer()` after setting up your |
| 84 | +fundamental player class, like so: |
| 85 | + |
| 86 | +```c |
| 87 | +void() PutClientInServer = |
| 88 | +{ |
| 89 | + // setup fundamental player state |
| 90 | + self.takedamage = DAMAGE_YES; |
| 91 | + self.solid = SOLID_SLIDEBOX; |
| 92 | + self.movetype = MOVETYPE_WALK; |
| 93 | + self.fixangle = TRUE; |
| 94 | + setsize(self, [-16, -16, -36], [16, 16, 36]); |
| 95 | + self.flags |= FL_CLIENT; |
| 96 | + self.view_ofs = [0, 0, 28]; |
| 97 | + self.classname = "player"; |
| 98 | + |
| 99 | + // load health value from parm1 |
| 100 | + self.health = parm1; |
| 101 | + |
| 102 | + // parse parm_string as json |
| 103 | + jsonnode root = json_parse(parm_string); |
| 104 | + if (!root) |
| 105 | + error("couldn't decode parm_string as JSON!"); |
| 106 | + |
| 107 | + // check if we have the field |
| 108 | + // if so, copy it out as a string |
| 109 | + if (root["some_extended_field_string"]) |
| 110 | + self.some_extended_field_string = root["some_extended_field_string"].s; |
| 111 | + |
| 112 | + // clean up |
| 113 | + json_free(root); |
| 114 | +}; |
| 115 | +``` |
| 116 | + |
| 117 | +You don't have to use `parm_string` as JSON, though. You could just print the |
| 118 | +values as space-separated tokens and use the `tokenize()` or |
| 119 | +`tokenize_console()` builtins to read them back. Either way, it's a good method |
| 120 | +for storing lots of custom data across level transitions. Enjoy! |
0 commit comments