Upcoming Tutorial: The Seamless Multi-Map World

Discuss programming topics for the various GPL'd game engine sources.
Baker
Posts: 3666
Joined: Tue Mar 14, 2006 5:15 am

Upcoming Tutorial: The Seamless Multi-Map World

Post by Baker »

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 night is young. How else can I annoy the world before sunsrise? 8) Inquisitive minds want to know ! And if they don't -- well like that ever has stopped me before ..
Downsider
Posts: 621
Joined: Tue Sep 16, 2008 1:35 am

Post by Downsider »

So more or less what Half-Life does? I say Half-Life because Half-Life certainly illustrates it better.

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. 8)
Sajt
Posts: 1215
Joined: Sat Oct 16, 2004 3:39 am

Post by Sajt »

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.
Downsider
Posts: 621
Joined: Tue Sep 16, 2008 1:35 am

Post by Downsider »

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?
r00k
Posts: 1111
Joined: Sat Nov 13, 2004 10:39 pm

Post by r00k »

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.
gnounc
Posts: 428
Joined: Mon Apr 06, 2009 6:26 am

Post by gnounc »

This has been talked about for a long time.
I think its pretty slick, I'd like to see it in-game
mh
Posts: 2292
Joined: Sat Jan 12, 2008 1:38 am

Post by mh »

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.
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.

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
Baker
Posts: 3666
Joined: Tue Mar 14, 2006 5:15 am

Post by Baker »

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. 8)
Actually no, communication failure on my part. Non-player entities can't make the trip across levels.
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").
Aw, hell. Didn't think of FRIK_FILE.

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.
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?
Meanwhile ... Downsider comes up with an insightful point that I don't think anything else would think of.

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? 8) Inquisitive minds want to know ! And if they don't -- well like that ever has stopped me before ..
Sajt
Posts: 1215
Joined: Sat Oct 16, 2004 3:39 am

Post by Sajt »

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.)
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?
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!

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?
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.
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.

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.
Baker
Posts: 3666
Joined: Tue Mar 14, 2006 5:15 am

Post by Baker »

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?
Very easy.

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? 8) Inquisitive minds want to know ! And if they don't -- well like that ever has stopped me before ..
frag.machine
Posts: 2126
Joined: Sat Nov 25, 2006 1:49 pm

Post by frag.machine »

mh wrote:
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.
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.

I wonder though how far it would be possible to go with grouping each of the ID1 episodes into one map.
I already did this for episode 1. :)

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)
Spike
Posts: 2914
Joined: Fri Nov 05, 2004 3:12 am
Location: UK
Contact:

Post by Spike »

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.
Spirit
Posts: 1065
Joined: Sat Nov 20, 2004 9:00 pm
Contact:

Post by Spirit »

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.
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).
Improve Quaddicted, send me a pull request: https://github.com/SpiritQuaddicted/Quaddicted-reviews
frag.machine
Posts: 2126
Joined: Sat Nov 25, 2006 1:49 pm

Post by frag.machine »

Spirit wrote:
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.
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).
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.

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)
Baker
Posts: 3666
Joined: Tue Mar 14, 2006 5:15 am

Post by Baker »

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 ...

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
}
and replace with this code.

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
}
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 ...
#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
PART II: QuakeC Tutorial

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;


2. Now find the following:

Code: Select all

	spot = find (world, classname, "info_player_start");
	if (!spot)
		error ("PutClientInServer: no info_player_start on level");
	
	return spot;
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).

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");
PART III: Mapping Differences

You can now do ...
{
"classname" "trigger_changelevel"
"spawnflags" "1" // Important, a normal trigger_changelevel removes itself
"map" "map1.spawn2" // Specify map plus spawn point name
...
}
Which when used would spawn you at:
{
"classname" "info_player_start"
"targetname" "spawn2" // This is the name of this spawn point
"origin" "512 -416 192"
}
The general idea is that you now can have multiple info_player_start spots as multiple entry points into a map.

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? 8) Inquisitive minds want to know ! And if they don't -- well like that ever has stopped me before ..
Post Reply