Upcoming Tutorial: The Seamless Multi-Map World
Upcoming Tutorial: The Seamless Multi-Map World
Thanks to some QuakeC guidance to Lardarse, I have successfully made an engine and QuakeC modification that allows seamless travel between maps leveraging the #ifdef Quake2 engine code and some mild finessing.
The way this works at least in my implementation is that you can do a trigger_changelevel indicating "mymap.spawn1" or "mymap.spawn2" with the "no intermission" flag set and travel between maps in the state that you left them.
So you could build a little universe made of several maps and travel amongst them without skipping a beat.
From a strict Quake perspective (which isn't what I am seeking with this), one drawback is that "save games" are rather meaningless because the progress of a single map just doesn't suffice to describe your current environment. But my interests in this aren't strict traditional Quake and since this is highly locked to the engine modifications required which require an altered progsdef, well ... for me this is just fine but this isn't standard engine friendly at all.
The way this works at least in my implementation is that you can do a trigger_changelevel indicating "mymap.spawn1" or "mymap.spawn2" with the "no intermission" flag set and travel between maps in the state that you left them.
So you could build a little universe made of several maps and travel amongst them without skipping a beat.
From a strict Quake perspective (which isn't what I am seeking with this), one drawback is that "save games" are rather meaningless because the progress of a single map just doesn't suffice to describe your current environment. But my interests in this aren't strict traditional Quake and since this is highly locked to the engine modifications required which require an altered progsdef, well ... for me this is just fine but this isn't standard engine friendly at all.
The night is young. How else can I annoy the world before sunsrise? Inquisitive minds want to know ! And if they don't -- well like that ever has stopped me before ..
You should mention the term "hub system" so old-timers know right away that you are not talking about something else. By the way, this feature is also possible in pure QuakeC, which I did in 2004 or so using FRIK_FILE (it also supported savegames, since each map had its own save file in a folder reserved for the save "slot").
F. A. Špork, an enlightened nobleman and a great patron of art, had a stately Baroque spa complex built on the banks of the River Labe.
There are other limits aside from dimensions. In particular the 65536 surfaces limit is likely to be hit early, and there's nothing anyone can do about that as indexes to them are stored in the BSP as unsigned shorts.r00k wrote:I can understand why map size is limited clientside, but logically serverside the map dimensions are infinite. Why cant the server order and group together maps into 1 logical map?
would be interesting to see all the id1 maps stacked in the tightest configuration making the smallest dm map group.
I wonder though how far it would be possible to go with grouping each of the ID1 episodes into one map.
We had the power, we had the space, we had a sense of time and place
We knew the words, we knew the score, we knew what we were fighting for
We knew the words, we knew the score, we knew what we were fighting for
Actually no, communication failure on my part. Non-player entities can't make the trip across levels.Downsider wrote:Entitys that travel across map when they're within a certain range of the change level trigger, more or less? Sounds neat, and honestly I've always pondered how such an effect was achieved.
Aw, hell. Didn't think of FRIK_FILE.Sajt wrote:You should mention the term "hub system" so old-timers know right away that you are not talking about something else. By the way, this feature is also possible in pure QuakeC, which I did in 2004 or so using FRIK_FILE (it also supported savegames, since each map had its own save file in a folder reserved for the save "slot").
However, thinking about it a bit more I am not absolutely certain FRIK_FILE could do "it all" at least in a non-DP engine. Hmmmm.
I didn't say hub system because I was thinking more Half-Life or Q2 and well ... I never thought of those as hub systems.
Meanwhile ... Downsider comes up with an insightful point that I don't think anything else would think of.Downsider wrote:FRIK_FILE isn't exactly pure QC.
If an engine dev is willing to add FRIK_FILE, why not add this as a proper, easier to use system?
Scores:
Baker -2 (double fail)
Sajt +1
Downsider +5
Anyhow ... tutorial still will be coming ... I'm not sure the FRIK_FILE only method would work as well. I can't remember why at the moment though or if I am even right.
The night is young. How else can I annoy the world before sunsrise? Inquisitive minds want to know ! And if they don't -- well like that ever has stopped me before ..
Okay, I went back and looked at my code for saving. It does work, but it was sort of ugly code-wise, and couldn't hook onto Quake's save/load commands of course, using impulses instead...
Like Quake2, a save slot is a folder. It contains one global file containing the player's fields and which map he is currently on. This file can also be used across transitions for storing more info than fits in the QC parms. Then the save folder also contains an additional file for each map so far visited, each containing something resembling an entity dump. Like Quake2, there is an extra folder called "current" which holds the state for the current game you're playing. (This has the unfortunate side-effect of not allowing two copies of the singleplayer game to run at once, but who would want to do that? It could be hacked around, anyway...)
I also had changelevel transitions with "landmarks" a la Half-Life. That is, each changelevel trigger has a corresponding one on the other level, allowing back and forth travel. Your position relative to a linked "landmark" entity is saved and re-added to the corresponding landmark on the other side. This, along with a small and unobtrusive loading graphic, gives a "seamless" feel to the level transitions.
Yes, I had to manually write functions to save specified fields to files and read them back again. I'm sure that these days among the ridiculous DP extensions are some reflection-like functions that would let you iterate through entity fields just like the engine could, making the QC for this a lot cleaner and more maintainable.
As for coop, it would probably be complicated whether you implemented a hub system in QC or in the engine. I never even let myself think about that...
(By the way, if you wanted to see my bad juvenile code for this, look at this file and others in the repository. It's even more primitive than I remembered, but it was working in simple test maps.)
But ignore that. Your point is totally valid. For the QC method to be elegant enough not to provoke vomiting, further QC extensions would be required, which would probably be even more complicated than a full engine-side hub implementation.
Anyway, someone who would actually go through the effort of making a single-player hub-based campaign would probably not mind limiting players to a select few engines. And it would probably be relatively simple to implement in the engine, anyway... Right Baker?
edit: Fixed a split infinitive! Aargh!
Like Quake2, a save slot is a folder. It contains one global file containing the player's fields and which map he is currently on. This file can also be used across transitions for storing more info than fits in the QC parms. Then the save folder also contains an additional file for each map so far visited, each containing something resembling an entity dump. Like Quake2, there is an extra folder called "current" which holds the state for the current game you're playing. (This has the unfortunate side-effect of not allowing two copies of the singleplayer game to run at once, but who would want to do that? It could be hacked around, anyway...)
I also had changelevel transitions with "landmarks" a la Half-Life. That is, each changelevel trigger has a corresponding one on the other level, allowing back and forth travel. Your position relative to a linked "landmark" entity is saved and re-added to the corresponding landmark on the other side. This, along with a small and unobtrusive loading graphic, gives a "seamless" feel to the level transitions.
Yes, I had to manually write functions to save specified fields to files and read them back again. I'm sure that these days among the ridiculous DP extensions are some reflection-like functions that would let you iterate through entity fields just like the engine could, making the QC for this a lot cleaner and more maintainable.
As for coop, it would probably be complicated whether you implemented a hub system in QC or in the engine. I never even let myself think about that...
(By the way, if you wanted to see my bad juvenile code for this, look at this file and others in the repository. It's even more primitive than I remembered, but it was working in simple test maps.)
FRIK_FILE is usually the very first extension a new engine will support. It might take ten more years to get a reasonably wide base of support for a new complicated feature like engine-side hubs. I used to be able to live outside the passage of time (waiting untold eons for CSQC, which would appear just after I finally "retired"), but that doesn't seem to work for me anymore!Downsider wrote:FRIK_FILE isn't exactly pure QC.
If an engine dev is willing to add FRIK_FILE, why not add this as a proper, easier to use system?
But ignore that. Your point is totally valid. For the QC method to be elegant enough not to provoke vomiting, further QC extensions would be required, which would probably be even more complicated than a full engine-side hub implementation.
Anyway, someone who would actually go through the effort of making a single-player hub-based campaign would probably not mind limiting players to a select few engines. And it would probably be relatively simple to implement in the engine, anyway... Right Baker?
Actually, I hold those games, along with maybe ye olde Hexen, to be the very definition of a hub system. But I'm no authority on this subject.Baker wrote:I didn't say hub system because I was thinking more Half-Life or Q2 and well ... I never thought of those as hub systems.
edit: Fixed a split infinitive! Aargh!
F. A. Špork, an enlightened nobleman and a great patron of art, had a stately Baroque spa complex built on the banks of the River Labe.
Very easy.Sajt wrote:Anyway, someone who would actually go through the effort of making a single-player hub-based campaign would probably not mind limiting players to a select few engines. And it would probably be relatively simple to implement in the engine, anyway... Right Baker?
I spent maybe 30 minutes enabling the appropriate Q2 engine code already included in the Q1 source release, maybe another 30 minutes opening some Q2 maps in a text editor to look at the entities, 30 minutes on the map, 30 minutes realizing the engine-code "as is" didn't deal properly with deaths and 2 hours pondering some QC issues (for instance "why are trigger_changelevels removing themselves". grrr. Was a real problem).
Yeah, you hit on the "each savegame must be a folder" limitation. I haven't dealt with that yet, not that really is hard to deal with.
The night is young. How else can I annoy the world before sunsrise? Inquisitive minds want to know ! And if they don't -- well like that ever has stopped me before ..
-
- Posts: 2126
- Joined: Sat Nov 25, 2006 1:49 pm
I already did this for episode 1.mh wrote:There are other limits aside from dimensions. In particular the 65536 surfaces limit is likely to be hit early, and there's nothing anyone can do about that as indexes to them are stored in the BSP as unsigned shorts.r00k wrote:I can understand why map size is limited clientside, but logically serverside the map dimensions are infinite. Why cant the server order and group together maps into 1 logical map?
would be interesting to see all the id1 maps stacked in the tightest configuration making the smallest dm map group.
I wonder though how far it would be possible to go with grouping each of the ID1 episodes into one map.
EDIT: Regarding proper ways to store all info from a persistent world, here my $0.02: I don't think FRIK_FILE is adequate to such task. We are talking about something that behaves pretty much in the same way most mmorpgs (storing everything for every entity found by the player, all the events that occured, etc), and the solution adopted in all of these examples was to use a RDBMS for that. IMHO, SQLite is the logical choice for such project: it's a fairly complete SQL embedded server in a single .c file, and would be easy to add a couple bult-ins to allow execute SQL commands from QuakeC.
I know FrikaC made a cgi-bin version of the quakec interpreter once and wrote part of his website in QuakeC (LordHavoc)
you could use pakfiles instead of directories. But then what's the difference between a pak and a zip, and a compressed 'folder' and a regular directory, as far as file navigation is concerned.
Saved games need multiple maps, but you don't need to save every part of the current saved game data, nor the player.
The currently known maps (other than the actual current) can be stored as temp files anywhere on the hard drive (and probably in the disk cache). Just copy them to the saved game when saving. You don't need to keep the current map as valid.
FTE's saved games use directories. Current maps actually go in the gamedir, which is kinda bad. Q2 requires fopenable file names for each, and it helps to keep hexen2 mechanisms consistant.
Landmarks are useful for single player, but if you want coop support, you will likely need to teleport all other players to it, without spawnfragging. Like a regular coop map change. If you want to stick to the quake theme, you'd be using portals/slipgates to change maps anyway. :)
I suppose for coop, you could 'teleport' them, and transfer the triggering player through the landmark. Imho, this is the responsibility of the gamecode rather than the engine, the engine needs to provide only the name of the landmark. Offset from the landmark would be copied over with the player, so it works with mutliple players.
Copying non-player entities over is somewhat pointless, and complicated by precaches.
Copying player entities only really needs the parms for the most part, though megahealth/powerups will reset. Hexen2+FTE copy the entire player entity over, all fields. The QC gets a callback with an argument specifying the time difference between the two maps, and the timer fields are adjusted in QC (lots of +=, reflection can't do this as its just a float).
One limitation of both h2+q2 is that in order to change the state of a door/bridge/etc, you need to explicitly travel from one map to the one that has the door. This limitation is perhaps not too serious, but consider the reactions of NPCs to deaths in another part of the hub. An example from quake is the runes.
One thing I still want to do is to have a game server running multiple maps at once, with the players switching between the maps at will. This resolves uglyness with hubs+coop, as well as provides 'load' balancing with small maps in multiplayer (20 players on a dual map = 10 instances of the same map). But yeah, its not pretty.
FRIK_FILE cannot provide support for hubs with engine-based saved games, at least other than quicksave. You can write your own saved game functionality though, which shouldn't be hard if the level cache is saved cleanly. You would need to use serverflags (rune persistance stuff) to flag if it was a new unit or an continuation of a hub. Yes, you would have to hard code the list of fields to save. Add each field ref to an array (frikqcc or fteqcc ones), and save/load those ones depending on the field type. Shouldn't be too ugly.
Regarding limiting engines: If your extension is transparent enough, supporting engines will just work, and non-supporting engines will still work, they'll just respawn everything over map changes. With the use of serverflags in the mod, this need not be fatal, so long as the various doors are opened as required, ala prydon.
For reference, FTE exposes 1 extra argument to the changelevel builtin, and 1 extra global string, named spawnspot. Add the extra argument, it'll save the old map, and spawnspot will be cleared. Call it without the extra argument (or empty), and it'll flush all the old maps and do a regular map change. So QC changes are just: a) select player_start (and coops) such that targetname matches spawnspot, and b) make trigger_changelevel pass self.target as an optional second argument to changelevel. You won't have landmarks, but it'll be able to act like hexen2. Engines+mods that support it will use a hub system, engines that don't will respawn all ents. To use landmarks, you would need to transfer the player's offset from the changelevel/landmark trigger, and spawn them offset from the new one, using the spawnspot to specify which landmark to use.
Transfering all player fields is logically a separate extension, and will work without hubs.
Saved games need multiple maps, but you don't need to save every part of the current saved game data, nor the player.
The currently known maps (other than the actual current) can be stored as temp files anywhere on the hard drive (and probably in the disk cache). Just copy them to the saved game when saving. You don't need to keep the current map as valid.
FTE's saved games use directories. Current maps actually go in the gamedir, which is kinda bad. Q2 requires fopenable file names for each, and it helps to keep hexen2 mechanisms consistant.
Landmarks are useful for single player, but if you want coop support, you will likely need to teleport all other players to it, without spawnfragging. Like a regular coop map change. If you want to stick to the quake theme, you'd be using portals/slipgates to change maps anyway. :)
I suppose for coop, you could 'teleport' them, and transfer the triggering player through the landmark. Imho, this is the responsibility of the gamecode rather than the engine, the engine needs to provide only the name of the landmark. Offset from the landmark would be copied over with the player, so it works with mutliple players.
Copying non-player entities over is somewhat pointless, and complicated by precaches.
Copying player entities only really needs the parms for the most part, though megahealth/powerups will reset. Hexen2+FTE copy the entire player entity over, all fields. The QC gets a callback with an argument specifying the time difference between the two maps, and the timer fields are adjusted in QC (lots of +=, reflection can't do this as its just a float).
One limitation of both h2+q2 is that in order to change the state of a door/bridge/etc, you need to explicitly travel from one map to the one that has the door. This limitation is perhaps not too serious, but consider the reactions of NPCs to deaths in another part of the hub. An example from quake is the runes.
One thing I still want to do is to have a game server running multiple maps at once, with the players switching between the maps at will. This resolves uglyness with hubs+coop, as well as provides 'load' balancing with small maps in multiplayer (20 players on a dual map = 10 instances of the same map). But yeah, its not pretty.
FRIK_FILE cannot provide support for hubs with engine-based saved games, at least other than quicksave. You can write your own saved game functionality though, which shouldn't be hard if the level cache is saved cleanly. You would need to use serverflags (rune persistance stuff) to flag if it was a new unit or an continuation of a hub. Yes, you would have to hard code the list of fields to save. Add each field ref to an array (frikqcc or fteqcc ones), and save/load those ones depending on the field type. Shouldn't be too ugly.
Regarding limiting engines: If your extension is transparent enough, supporting engines will just work, and non-supporting engines will still work, they'll just respawn everything over map changes. With the use of serverflags in the mod, this need not be fatal, so long as the various doors are opened as required, ala prydon.
For reference, FTE exposes 1 extra argument to the changelevel builtin, and 1 extra global string, named spawnspot. Add the extra argument, it'll save the old map, and spawnspot will be cleared. Call it without the extra argument (or empty), and it'll flush all the old maps and do a regular map change. So QC changes are just: a) select player_start (and coops) such that targetname matches spawnspot, and b) make trigger_changelevel pass self.target as an optional second argument to changelevel. You won't have landmarks, but it'll be able to act like hexen2. Engines+mods that support it will use a hub system, engines that don't will respawn all ents. To use landmarks, you would need to transfer the player's offset from the changelevel/landmark trigger, and spawn them offset from the new one, using the spawnspot to specify which landmark to use.
Transfering all player fields is logically a separate extension, and will work without hubs.
Database access was recently suggested by quin when I asked for new bounty ideas. The problem is that a) I cannot estimate the amount of work and b) I do not really see fantastic use cases that I personally would like or care about (apart from very simple stuff).frag.machine wrote:EDIT: Regarding proper ways to store all info from a persistent world, here my $0.02: I don't think FRIK_FILE is adequate to such task. We are talking about something that behaves pretty much in the same way most mmorpgs (storing everything for every entity found by the player, all the events that occured, etc), and the solution adopted in all of these examples was to use a RDBMS for that. IMHO, SQLite is the logical choice for such project: it's a fairly complete SQL embedded server in a single .c file, and would be easy to add a couple bult-ins to allow execute SQL commands from QuakeC.
Improve Quaddicted, send me a pull request: https://github.com/SpiritQuaddicted/Quaddicted-reviews
-
- Posts: 2126
- Joined: Sat Nov 25, 2006 1:49 pm
a) is actually quite doable. SQLite can be embedded to almost any ANSI-C program without great effort. The bigger problem would be actually to define the built-ins in QuakeC, but even this can be worked around. For more info, this is SQLite's site.Spirit wrote:Database access was recently suggested by quin when I asked for new bounty ideas. The problem is that a) I cannot estimate the amount of work and b) I do not really see fantastic use cases that I personally would like or care about (apart from very simple stuff).frag.machine wrote:EDIT: Regarding proper ways to store all info from a persistent world, here my $0.02: I don't think FRIK_FILE is adequate to such task. We are talking about something that behaves pretty much in the same way most mmorpgs (storing everything for every entity found by the player, all the events that occured, etc), and the solution adopted in all of these examples was to use a RDBMS for that. IMHO, SQLite is the logical choice for such project: it's a fairly complete SQL embedded server in a single .c file, and would be easy to add a couple bult-ins to allow execute SQL commands from QuakeC.
Regarding b) I can foresee a good number of possibilities: besides the idea of seamless worlds, one could create quest-oriented mods, where all the related data - which monsters, their locations, npc dialogs and behavior, etc - could be defined in terms of SQL load scripts. Imagine Prydon Gate supporting new quests just by database downloads (another nifty feature to add to an engine, BTW).
I know FrikaC made a cgi-bin version of the quakec interpreter once and wrote part of his website in QuakeC (LordHavoc)
Databases ... bleh. Sounds like a cross-platform killer. Combined with FRIK_FILE and adding some extra persistent fields, you'd really have quite enough room for a mountain-sized heap of creativity with minimal effort.
One opinion. And maybe wrong and narrowminded.
PART I: Engine Tutorial
1. Take stock GLQuake. Much of this will involve enabling QUAKE2 build code. Source: http://forums.inside3d.com/viewtopic.php?t=1281 (free MS VC+ Express) or http://www.quakedev.com/files/quake1/q1source.zip (MS Visual Studio 6)
2. hostcmd.c - Change all instances of #ifdef QUAKE2 to "#if 666" to enable except line #615 which I don't see as important. This makes it so upon entering and exiting a level that the state of the level is written to file or read from a file.
3. pr_cmds.c - Make PF_changelevel support a "startspot" for being able to enter a map at different locations. Find this code ...
and replace with this code.
3. progdefs.h - Change #ifdef QUAKE2 to #if 666
4. server.h - Change all instances of #ifdef QUAKE2 to #if 666. We are taking the whole Q2'd deal.
5. sv_main.c - On lines 1044, 1090, 1171 change #ifdef QUAKE2 to #if 666 to enable.
6. Open hostcmd.c AGAIN and add the yellow because when you die it needs to restore the state at the beginning of the level as when you arrived instead of the normal restoring the map to as if you just got there ...
1. Replace defs.qc with this one to mirror the Q2 build global variables. It is also important for the CRC check.
The fields added are startspot, null, basevelocity, drawPercent, gravity, mass, light_level, items2, pitch_speed, dmg, dmgtime, air_finished, pain_finished, radsuit_finished, speed.
2. Now find the following:
And replace with ...
(This should be rewritten to loop through all the info_player_start entities and then match the startspot ... but I was lazy and only had 5 minutes when working with the qc).
PART III: Mapping Differences
You can now do ...
With every info_player_start you should specify the targetname as the name of the spawn point.
With every trigger_changelevel, you need to set the flag for no intermission or the trigger_changelevel will remove itself.
Quick with room for improvement and some need for polish, but very much functional and fun to play around with.
One opinion. And maybe wrong and narrowminded.
PART I: Engine Tutorial
1. Take stock GLQuake. Much of this will involve enabling QUAKE2 build code. Source: http://forums.inside3d.com/viewtopic.php?t=1281 (free MS VC+ Express) or http://www.quakedev.com/files/quake1/q1source.zip (MS Visual Studio 6)
2. hostcmd.c - Change all instances of #ifdef QUAKE2 to "#if 666" to enable except line #615 which I don't see as important. This makes it so upon entering and exiting a level that the state of the level is written to file or read from a file.
3. pr_cmds.c - Make PF_changelevel support a "startspot" for being able to enter a map at different locations. Find this code ...
Code: Select all
/*
==============
PF_changelevel
==============
*/
void PF_changelevel (void)
{
#ifdef QUAKE2
char *s1, *s2;
if (svs.changelevel_issued)
return;
svs.changelevel_issued = true;
s1 = G_STRING(OFS_PARM0);
s2 = G_STRING(OFS_PARM1);
if ((int)pr_global_struct->serverflags & (SFL_NEW_UNIT | SFL_NEW_EPISODE))
Cbuf_AddText (va("changelevel %s %s\n",s1, s2));
else
Cbuf_AddText (va("changelevel2 %s %s\n",s1, s2));
#else
char *s;
// make sure we don't issue two changelevels
if (svs.changelevel_issued)
return;
svs.changelevel_issued = true;
s = G_STRING(OFS_PARM0);
Cbuf_AddText (va("changelevel %s\n",s));
#endif
}
Code: Select all
/*
==============
PF_changelevel
==============
*/
void PF_changelevel (void)
{
#if 666
char *s, *s1, *s2;
if (svs.changelevel_issued)
return;
svs.changelevel_issued = true;
#if 0
s1 = G_STRING(OFS_PARM0);
s2 = G_STRING(OFS_PARM1);
#endif
#if 1
s = G_STRING(OFS_PARM0);
COM_StripExtension (s, s1);
s2 = COM_FileExtension (s);
#endif
if ((int)pr_global_struct->serverflags & (SFL_NEW_UNIT | SFL_NEW_EPISODE)) {
Cbuf_AddText (va("changelevel %s %s\n",s1, s2));
}
else {
Cbuf_AddText (va("changelevel2 %s %s\n",s1, s2));
}
#else
char *s;
// make sure we don't issue two changelevels
if (svs.changelevel_issued)
return;
svs.changelevel_issued = true;
s = G_STRING(OFS_PARM0);
Cbuf_AddText (va("changelevel %s\n",s));
#endif
}
4. server.h - Change all instances of #ifdef QUAKE2 to #if 666. We are taking the whole Q2'd deal.
5. sv_main.c - On lines 1044, 1090, 1171 change #ifdef QUAKE2 to #if 666 to enable.
6. Open hostcmd.c AGAIN and add the yellow because when you die it needs to restore the state at the beginning of the level as when you arrived instead of the normal restoring the map to as if you just got there ...
PART II: QuakeC Tutorial#if 666
strcpy(startspot, sv.startspot);
// try to restore the new level
if (LoadGamestate (mapname, startspot))
SV_SpawnServer (mapname, startspot);
else
SV_SpawnServer (mapname, NULL);
#else
SV_SpawnServer (mapname);
#endif
1. Replace defs.qc with this one to mirror the Q2 build global variables. It is also important for the CRC check.
The fields added are startspot, null, basevelocity, drawPercent, gravity, mass, light_level, items2, pitch_speed, dmg, dmgtime, air_finished, pain_finished, radsuit_finished, speed.
Code: Select all
/*
==============================================================================
SOURCE FOR GLOBALVARS_T C STRUCTURE
==============================================================================
*/
//
// system globals
//
entity self;
entity other;
entity world;
float time;
float frametime;
float force_retouch; // force all entities to touch triggers
// next frame. this is needed because
// non-moving things don't normally scan
// for triggers, and when a trigger is
// created (like a teleport trigger), it
// needs to catch everything.
// decremented each frame, so set to 2
// to guarantee everything is touched
string mapname;
string startspot;
float deathmatch;
float coop;
float teamplay;
float serverflags; // propagated from level to level, used to
// keep track of completed episodes
float total_secrets;
float total_monsters;
float found_secrets; // number of secrets found
float killed_monsters; // number of monsters killed
// spawnparms are used to encode information about clients across server
// level changes
float parm1, parm2, parm3, parm4, parm5, parm6, parm7, parm8, parm9, parm10, parm11, parm12, parm13, parm14, parm15, parm16;
//
// global variables set by built in functions
//
vector v_forward, v_up, v_right; // set by makevectors()
// set by traceline / tracebox
float trace_allsolid;
float trace_startsolid;
float trace_fraction;
vector trace_endpos;
vector trace_plane_normal;
float trace_plane_dist;
entity trace_ent;
float trace_inopen;
float trace_inwater;
entity msg_entity; // destination of single entity writes
string null;
//
// required prog functions
//
void() main; // only for testing
void() StartFrame;
void() PlayerPreThink;
void() PlayerPostThink;
void() ClientKill;
void() ClientConnect;
void() PutClientInServer; // call after setting the parm1... parms
void() ClientDisconnect;
void() SetNewParms; // called when a client first connects to
// a server. sets parms so they can be
// saved off for restarts
void() SetChangeParms; // call to set parms for self so they can
// be saved for a level transition
//================================================
void end_sys_globals; // flag for structure dumping
//================================================
/*
==============================================================================
SOURCE FOR ENTVARS_T C STRUCTURE
==============================================================================
*/
//
// system fields (*** = do not set in prog code, maintained by C code)
//
.float modelindex; // *** model index in the precached list
.vector absmin, absmax; // *** origin + mins / maxs
.float ltime; // local time for entity
.float movetype;
.float solid;
.vector origin; // ***
.vector oldorigin; // ***
.vector velocity;
.vector angles;
.vector avelocity;
.vector basevelocity;
.vector punchangle; // temp angle adjust from damage or recoil
.string classname; // spawn function
.string model;
.float frame;
.float skin;
.float effects;
.float drawPercent;
.float gravity;
.float mass;
.float light_level;
.vector mins, maxs; // bounding box extents reletive to origin
.vector size; // maxs - mins
.void() touch;
.void() use;
.void() think;
.void() blocked; // for doors or plats, called when can't push other
.float nextthink;
.entity groundentity;
// stats
.float health;
.float frags;
.float weapon; // one of the IT_SHOTGUN, etc flags
.string weaponmodel;
.float weaponframe;
.float currentammo;
.float ammo_shells, ammo_nails, ammo_rockets, ammo_cells;
.float items; // bit flags
.float items2;
.float takedamage;
.entity chain;
.float deadflag;
.vector view_ofs; // add to origin to get eye point
.float button0; // fire
.float button1; // use
.float button2; // jump
.float impulse; // weapon changes
.float fixangle;
.vector v_angle; // view / targeting angle for players
.float idealpitch; // calculated pitch angle for lookup up slopes
.float pitch_speed;
.string netname;
.entity enemy;
.float flags;
.float colormap;
.float team;
.float max_health; // players maximum health is stored here
.float teleport_time; // don't back up
.float armortype; // save this fraction of incoming damage
.float armorvalue;
.float waterlevel; // 0 = not in, 1 = feet, 2 = wast, 3 = eyes
.float watertype; // a contents value
.float ideal_yaw;
.float yaw_speed;
.entity aiment;
.entity goalentity; // a movetarget or an enemy
.float spawnflags;
.string target;
.string targetname;
// damage is accumulated through a frame. and sent as one single
// message, so the super shotgun doesn't generate huge messages
.float dmg_take;
.float dmg_save;
.entity dmg_inflictor;
.entity owner; // who launched a missile
.vector movedir; // mostly for doors, but also used for waterjump
.string message; // trigger messages
.float sounds; // either a cd track number or sound number
.string noise, noise1, noise2, noise3; // contains names of wavs to play
.float dmg;
.float dmgtime;
.float air_finished;
.float pain_finished;
.float radsuit_finished;
.float speed;
//================================================
void end_sys_fields; // flag for structure dumping
//================================================
/*
==============================================================================
VARS NOT REFERENCED BY C CODE
==============================================================================
*/
//
// constants
//
float FALSE = 0;
float TRUE = 1;
// edict.flags
float FL_FLY = 1;
float FL_SWIM = 2;
float FL_CLIENT = 8; // set for all client edicts
float FL_INWATER = 16; // for enter / leave water splash
float FL_MONSTER = 32;
float FL_GODMODE = 64; // player cheat
float FL_NOTARGET = 128; // player cheat
float FL_ITEM = 256; // extra wide size for bonus items
float FL_ONGROUND = 512; // standing on something
float FL_PARTIALGROUND = 1024; // not all corners are valid
float FL_WATERJUMP = 2048; // player jumping out of water
float FL_JUMPRELEASED = 4096; // for jump debouncing
// edict.movetype values
float MOVETYPE_NONE = 0; // never moves
//float MOVETYPE_ANGLENOCLIP = 1;
//float MOVETYPE_ANGLECLIP = 2;
float MOVETYPE_WALK = 3; // players only
float MOVETYPE_STEP = 4; // discrete, not real time unless fall
float MOVETYPE_FLY = 5;
float MOVETYPE_TOSS = 6; // gravity
float MOVETYPE_PUSH = 7; // no clip to world, push and crush
float MOVETYPE_NOCLIP = 8;
float MOVETYPE_FLYMISSILE = 9; // fly with extra size against monsters
float MOVETYPE_BOUNCE = 10;
float MOVETYPE_BOUNCEMISSILE = 11; // bounce with extra size
// edict.solid values
float SOLID_NOT = 0; // no interaction with other objects
float SOLID_TRIGGER = 1; // touch on edge, but not blocking
float SOLID_BBOX = 2; // touch on edge, block
float SOLID_SLIDEBOX = 3; // touch on edge, but not an onground
float SOLID_BSP = 4; // bsp clip, touch on edge, block
// range values
float RANGE_MELEE = 0;
float RANGE_NEAR = 1;
float RANGE_MID = 2;
float RANGE_FAR = 3;
// deadflag values
float DEAD_NO = 0;
float DEAD_DYING = 1;
float DEAD_DEAD = 2;
float DEAD_RESPAWNABLE = 3;
// takedamage values
float DAMAGE_NO = 0;
float DAMAGE_YES = 1;
float DAMAGE_AIM = 2;
// items
float IT_AXE = 4096;
float IT_SHOTGUN = 1;
float IT_SUPER_SHOTGUN = 2;
float IT_NAILGUN = 4;
float IT_SUPER_NAILGUN = 8;
float IT_GRENADE_LAUNCHER = 16;
float IT_ROCKET_LAUNCHER = 32;
float IT_LIGHTNING = 64;
float IT_EXTRA_WEAPON = 128;
float IT_SHELLS = 256;
float IT_NAILS = 512;
float IT_ROCKETS = 1024;
float IT_CELLS = 2048;
float IT_ARMOR1 = 8192;
float IT_ARMOR2 = 16384;
float IT_ARMOR3 = 32768;
float IT_SUPERHEALTH = 65536;
float IT_KEY1 = 131072;
float IT_KEY2 = 262144;
float IT_INVISIBILITY = 524288;
float IT_INVULNERABILITY = 1048576;
float IT_SUIT = 2097152;
float IT_QUAD = 4194304;
// point content values
float CONTENT_EMPTY = -1;
float CONTENT_SOLID = -2;
float CONTENT_WATER = -3;
float CONTENT_SLIME = -4;
float CONTENT_LAVA = -5;
float CONTENT_SKY = -6;
float STATE_TOP = 0;
float STATE_BOTTOM = 1;
float STATE_UP = 2;
float STATE_DOWN = 3;
vector VEC_ORIGIN = '0 0 0';
vector VEC_HULL_MIN = '-16 -16 -24';
vector VEC_HULL_MAX = '16 16 32';
vector VEC_HULL2_MIN = '-32 -32 -24';
vector VEC_HULL2_MAX = '32 32 64';
// protocol bytes
float SVC_TEMPENTITY = 23;
float SVC_KILLEDMONSTER = 27;
float SVC_FOUNDSECRET = 28;
float SVC_INTERMISSION = 30;
float SVC_FINALE = 31;
float SVC_CDTRACK = 32;
float SVC_SELLSCREEN = 33;
float TE_SPIKE = 0;
float TE_SUPERSPIKE = 1;
float TE_GUNSHOT = 2;
float TE_EXPLOSION = 3;
float TE_TAREXPLOSION = 4;
float TE_LIGHTNING1 = 5;
float TE_LIGHTNING2 = 6;
float TE_WIZSPIKE = 7;
float TE_KNIGHTSPIKE = 8;
float TE_LIGHTNING3 = 9;
float TE_LAVASPLASH = 10;
float TE_TELEPORT = 11;
// sound channels
// channel 0 never willingly overrides
// other channels (1-7) allways override a playing sound on that channel
float CHAN_AUTO = 0;
float CHAN_WEAPON = 1;
float CHAN_VOICE = 2;
float CHAN_ITEM = 3;
float CHAN_BODY = 4;
float ATTN_NONE = 0;
float ATTN_NORM = 1;
float ATTN_IDLE = 2;
float ATTN_STATIC = 3;
// update types
float UPDATE_GENERAL = 0;
float UPDATE_STATIC = 1;
float UPDATE_BINARY = 2;
float UPDATE_TEMP = 3;
// entity effects
float EF_BRIGHTFIELD = 1;
float EF_MUZZLEFLASH = 2;
float EF_BRIGHTLIGHT = 4;
float EF_DIMLIGHT = 8;
// messages
float MSG_BROADCAST = 0; // unreliable to all
float MSG_ONE = 1; // reliable to one (msg_entity)
float MSG_ALL = 2; // reliable to all
float MSG_INIT = 3; // write to the init string
//================================================
//
// globals
//
float movedist;
float gameover; // set when a rule exits
string string_null; // null string, nothing should be held here
float empty_float;
entity newmis; // launch_spike sets this after spawning it
entity activator; // the entity that activated a trigger or brush
entity damage_attacker; // set by T_Damage
float framecount;
float skill;
//================================================
//
// world fields (FIXME: make globals)
//
.string wad;
.string map;
.float worldtype; // 0=medieval 1=metal 2=base
//================================================
.string killtarget;
//
// quakeed fields
//
.float light_lev; // not used by game, but parsed by light util
.float style;
//
// monster ai
//
.void() th_stand;
.void() th_walk;
.void() th_run;
.void() th_missile;
.void() th_melee;
.void(entity attacker, float damage) th_pain;
.void() th_die;
.entity oldenemy; // mad at this player before taking damage
.float speed;
.float lefty;
.float search_time;
.float attack_state;
float AS_STRAIGHT = 1;
float AS_SLIDING = 2;
float AS_MELEE = 3;
float AS_MISSILE = 4;
//
// player only fields
//
.float walkframe;
.float attack_finished;
.float pain_finished;
.float invincible_finished;
.float invisible_finished;
.float super_damage_finished;
.float radsuit_finished;
.float invincible_time, invincible_sound;
.float invisible_time, invisible_sound;
.float super_time, super_sound;
.float rad_time;
.float fly_sound;
.float axhitme;
.float show_hostile; // set to time+0.2 whenever a client fires a
// weapon or takes damage. Used to alert
// monsters that otherwise would let the player go
.float jump_flag; // player jump flag
.float swim_flag; // player swimming sound flag
.float air_finished; // when time > air_finished, start drowning
.float bubble_count; // keeps track of the number of bubbles
.string deathtype; // keeps track of how the player died
//
// object stuff
//
.string mdl;
.vector mangle; // angle at start
.vector oldorigin; // only used by secret door
.float t_length, t_width;
//
// doors, etc
//
.vector dest, dest1, dest2;
.float wait; // time from firing to restarting
.float delay; // time from activation to firing
.entity trigger_field; // door's trigger entity
.string noise4;
//
// monsters
//
.float pausetime;
.entity movetarget;
//
// doors
//
.float aflag;
.float dmg; // damage done by door when hit
//
// misc
//
.float cnt; // misc flag
//
// subs
//
.void() think1;
.vector finaldest, finalangle;
//
// triggers
//
.float count; // for counting triggers
//
// plats / doors / buttons
//
.float lip;
.float state;
.vector pos1, pos2; // top and bottom positions
.float height;
//
// sounds
//
.float waitmin, waitmax;
.float distance;
.float volume;
//===========================================================================
//
// builtin functions
//
void(vector ang) makevectors = #1; // sets v_forward, etc globals
void(entity e, vector o) setorigin = #2;
void(entity e, string m) setmodel = #3; // set movetype and solid first
void(entity e, vector min, vector max) setsize = #4;
// #5 was removed
void() break = #6;
float() random = #7; // returns 0 - 1
void(entity e, float chan, string samp, float vol, float atten) sound = #8;
vector(vector v) normalize = #9;
void(string e) error = #10;
void(string e) objerror = #11;
float(vector v) vlen = #12;
float(vector v) vectoyaw = #13;
entity() spawn = #14;
void(entity e) remove = #15;
// sets trace_* globals
// nomonsters can be:
// An entity will also be ignored for testing if forent == test,
// forent->owner == test, or test->owner == forent
// a forent of world is ignored
void(vector v1, vector v2, float nomonsters, entity forent) traceline = #16;
entity() checkclient = #17; // returns a client to look for
entity(entity start, .string fld, string match) find = #18;
string(string s) precache_sound = #19;
string(string s) precache_model = #20;
void(entity client, string s)stuffcmd = #21;
entity(vector org, float rad) findradius = #22;
void(string s) bprint = #23;
void(entity client, string s) sprint = #24;
void(string s) dprint = #25;
string(float f) ftos = #26;
string(vector v) vtos = #27;
void() coredump = #28; // prints all edicts
void() traceon = #29; // turns statment trace on
void() traceoff = #30;
void(entity e) eprint = #31; // prints an entire edict
float(float yaw, float dist) walkmove = #32; // returns TRUE or FALSE
// #33 was removed
float(float yaw, float dist) droptofloor= #34; // TRUE if landed on floor
void(float style, string value) lightstyle = #35;
float(float v) rint = #36; // round to nearest int
float(float v) floor = #37; // largest integer <= v
float(float v) ceil = #38; // smallest integer >= v
// #39 was removed
float(entity e) checkbottom = #40; // true if self is on ground
float(vector v) pointcontents = #41; // returns a CONTENT_*
// #42 was removed
float(float f) fabs = #43;
vector(entity e, float speed) aim = #44; // returns the shooting vector
float(string s) cvar = #45; // return cvar.value
void(string s) localcmd = #46; // put string into local que
entity(entity e) nextent = #47; // for looping through all ents
void(vector o, vector d, float color, float count) particle = #48;// start a particle effect
void() ChangeYaw = #49; // turn towards self.ideal_yaw
// at self.yaw_speed
// #50 was removed
vector(vector v) vectoangles = #51;
//
// direct client message generation
//
void(float to, float f) WriteByte = #52;
void(float to, float f) WriteChar = #53;
void(float to, float f) WriteShort = #54;
void(float to, float f) WriteLong = #55;
void(float to, float f) WriteCoord = #56;
void(float to, float f) WriteAngle = #57;
void(float to, string s) WriteString = #58;
void(float to, entity s) WriteEntity = #59;
//
// broadcast client message generation
//
// void(float f) bWriteByte = #59;
// void(float f) bWriteChar = #60;
// void(float f) bWriteShort = #61;
// void(float f) bWriteLong = #62;
// void(float f) bWriteCoord = #63;
// void(float f) bWriteAngle = #64;
// void(string s) bWriteString = #65;
// void(entity e) bWriteEntity = #66;
void(float step) movetogoal = #67;
string(string s) precache_file = #68; // no effect except for -copy
void(entity e) makestatic = #69;
void(string s) changelevel = #70;
//#71 was removed
void(string var, string val) cvar_set = #72; // sets cvar.value
void(entity client, string s) centerprint = #73; // sprint, but in middle
void(vector pos, string samp, float vol, float atten) ambientsound = #74;
string(string s) precache_model2 = #75; // registered version only
string(string s) precache_sound2 = #76; // registered version only
string(string s) precache_file2 = #77; // registered version only
void(entity e) setspawnparms = #78; // set parm1... to the
// values at level start
// for coop respawn
//============================================================================
//
// subs.qc
//
void(vector tdest, float tspeed, void() func) SUB_CalcMove;
void(entity ent, vector tdest, float tspeed, void() func) SUB_CalcMoveEnt;
void(vector destangle, float tspeed, void() func) SUB_CalcAngleMove;
void() SUB_CalcMoveDone;
void() SUB_CalcAngleMoveDone;
void() SUB_Null;
void() SUB_UseTargets;
void() SUB_Remove;
//
// combat.qc
//
void(entity targ, entity inflictor, entity attacker, float damage) T_Damage;
float (entity e, float healamount, float ignore) T_Heal; // health function
float(entity targ, entity inflictor) CanDamage;
Code: Select all
spot = find (world, classname, "info_player_start");
if (!spot)
error ("PutClientInServer: no info_player_start on level");
return spot;
(This should be rewritten to loop through all the info_player_start entities and then match the startspot ... but I was lazy and only had 5 minutes when working with the qc).
Code: Select all
if (!startspot) {
// No startspot specified, use default
spot = find (world, targetname, "default");
} else {
spot = find (world, targetname, startspot);
// Looking for info_player_start named startspot
}
if (!spot)
error ("PutClientInServer: no applicable spawn point");
You can now do ...
Which when used would spawn you at:{
"classname" "trigger_changelevel"
"spawnflags" "1" // Important, a normal trigger_changelevel removes itself
"map" "map1.spawn2" // Specify map plus spawn point name
...
}
The general idea is that you now can have multiple info_player_start spots as multiple entry points into a map.{
"classname" "info_player_start"
"targetname" "spawn2" // This is the name of this spawn point
"origin" "512 -416 192"
}
With every info_player_start you should specify the targetname as the name of the spawn point.
With every trigger_changelevel, you need to set the flag for no intermission or the trigger_changelevel will remove itself.
Quick with room for improvement and some need for polish, but very much functional and fun to play around with.
The night is young. How else can I annoy the world before sunsrise? Inquisitive minds want to know ! And if they don't -- well like that ever has stopped me before ..