mirror of
https://github.com/yann64/haikuports.git
synced 2026-03-19 01:46:00 +01:00
Add QuakeC compiler FTEQCC (#9538)
This commit is contained in:
227
games-util/fteqcc/additional-files/bitflags.txt
Normal file
227
games-util/fteqcc/additional-files/bitflags.txt
Normal file
@@ -0,0 +1,227 @@
|
||||
Bitflags and FTEQCC (for beginners)
|
||||
===================================
|
||||
By Marco Hladik
|
||||
Last updated: 8th of August 2018
|
||||
|
||||
|
||||
FTEQCC has quite a number of cool ways to help you work with bitflags.
|
||||
But first of all, you might not know how they work.
|
||||
Note that they exist in most if not all programming languages and
|
||||
are not exclusive to QuakeC in any way.
|
||||
|
||||
There have been books written about the notation decades ago,
|
||||
but very few people try to apply C programming skills to QuakeC
|
||||
codebases, as seen on literally every QuakeC project ever.
|
||||
|
||||
Obviously, the stock Quake codebase uses bitflags a lot.
|
||||
It's the key to making the inventory system work, for example.
|
||||
|
||||
What are bitflags?!
|
||||
===================
|
||||
|
||||
They are used for making Quake's weapon system work, as I've said.
|
||||
You've got a datatype which consists of bytes.
|
||||
Those bytes consists of bits, where can be either 0 or 1.
|
||||
A bitflag is simply a check for a specific bit being set or unset
|
||||
on a variable. This makes it possible to cram a lot more information
|
||||
into fewer datatypes.
|
||||
|
||||
All of the inventory system fits into one field: .items
|
||||
|
||||
self.items = IT_SHOTGUN | IT_SUPER_NAILGUN;
|
||||
|
||||
Is in reality just this:
|
||||
|
||||
self.items = 1 | 8;
|
||||
|
||||
So what's going on here? Well, let's look at it in simplefied 1-byte side-view:
|
||||
|
||||
1 Byte = 8 Bits
|
||||
0 0 0 0 0 0 0 0
|
||||
|
||||
And those 8 bits can represent many decimal values. The above is 0.
|
||||
|
||||
0 0 0 0 0 0 0 1
|
||||
|
||||
^ This example is 1.
|
||||
|
||||
0 0 0 0 0 0 1 0
|
||||
|
||||
^ This is 2.
|
||||
|
||||
0 0 0 0 0 0 1 1
|
||||
|
||||
^ This is 3.
|
||||
The explanation for how these are added together can be more easily seen
|
||||
with the following overlay:
|
||||
|
||||
128 64 32 16 8 4 2 1
|
||||
0 0 0 1 0 0 1 1 == 1 + 2 + 16 = 19
|
||||
|
||||
Whenever a bit is positive, in the example of the Quake item system, that
|
||||
represents that one item is present.
|
||||
|
||||
This means that the items have to be defined as multiples of 2.
|
||||
That's why the shotgun is 1, and the nailgun is 4 instead of 3.
|
||||
Because 3 would present that the shotgun and the super shotgun are present.
|
||||
|
||||
Flag Definitions
|
||||
================
|
||||
|
||||
Quake defined the values of bitflags in a `straightforward` manner:
|
||||
|
||||
// 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 = 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;
|
||||
|
||||
If you wanted to add a new weapon it was getting kind of annoying
|
||||
getting a calulator out and ending up with weird, long
|
||||
numbers like 2147483648, which represents 0x80000000 in hex, btw.
|
||||
|
||||
Anyway, FTEQCC adds a great new cousin to our beloved enum {}
|
||||
declaration! It's called enumflags {}:
|
||||
|
||||
enumflags {
|
||||
IT_SHOTGUN,
|
||||
IT_SUPER_SHOTGUN,
|
||||
IT_NAILGUN,
|
||||
IT_SUPER_NAILGUN,
|
||||
IT_GRENADE_LAUNCHER,
|
||||
IT_ROCKET_LAUNCHER,
|
||||
IT_LIGHTNING,
|
||||
IT_SHELLS,
|
||||
IT_NAILS,
|
||||
IT_ROCKETS,
|
||||
IT_CELLS,
|
||||
IT_AXE,
|
||||
IT_ARMOR1,
|
||||
IT_ARMOR2,
|
||||
IT_ARMOR3,
|
||||
IT_SUPERHEALTH,
|
||||
IT_KEY1,
|
||||
IT_KEY2,
|
||||
IT_INVISIBILITY,
|
||||
IT_INVULNERABILITY,
|
||||
IT_SUIT,
|
||||
IT_QUAD
|
||||
};
|
||||
|
||||
The first entry has the value of 1, the second the value of 2 and after that
|
||||
the value of 4. All it does it represent a bit shift.
|
||||
Thanks to that, you no longer have to manually specify multiples of 2.
|
||||
What a time to be alive. However, you can also choose to do it via macros
|
||||
and manually specify which bits you want to use via hex.
|
||||
|
||||
#define IT_SHOTGUN 0x1i
|
||||
#define IT_SUPER_SHOTGUN 0x2i
|
||||
#define IT_NAILGUN 0x4i
|
||||
#define IT_SUPER_NAILGUN 0x8i
|
||||
...
|
||||
|
||||
You get the idea.
|
||||
You want to specify the `i` at the end to indicate that you're working
|
||||
with an integer value and not a floating point one.
|
||||
|
||||
Comparisons
|
||||
===========
|
||||
|
||||
How do you check how a `weapon` is present in our .items field? Simple:
|
||||
|
||||
if ( self.items & IT_NAILGUN ) {
|
||||
// We got the the nailgun in our inventory!
|
||||
}
|
||||
|
||||
^ Checks if the bit for `4` is positive. The result will return `4` (not 1).
|
||||
This is helpful later when unsetting the bit using subtraction.
|
||||
|
||||
if ( !( self.items & IT_SUPER_NAILGUN ) ) {
|
||||
// We do not have the super nailgun at this time.
|
||||
}
|
||||
|
||||
^ Checks if the bit for `8`, aka the super nailgun, is not positive.
|
||||
You get the idea!
|
||||
|
||||
Setting an flag reliably
|
||||
========================
|
||||
|
||||
This and the next chapter are about bitwise operations.
|
||||
For simplicities sake, I'll tell you the quickest and safest way of
|
||||
working with them. If you want to learn more, look up Bitwise Operations
|
||||
on your favorite wiki and or book. Check a library some time.
|
||||
|
||||
The easiest and most reliable way, without killing the other bits is:
|
||||
|
||||
self.items |= IT_SHOTGUN;
|
||||
|
||||
^ Will tell it to keep whatever was in self.items before, but make sure
|
||||
that the bit for IT_SHOTGUN is positive.
|
||||
This effectively adds a shotgun into your inventory. If you were to
|
||||
simply do:
|
||||
|
||||
self.items += IT_SHOTGUN;
|
||||
|
||||
The resulting bits would mess up if the shotgun was already present.
|
||||
For example, imagine you had the shotgun and super shortgun already:
|
||||
|
||||
128 64 32 16 8 4 2 1
|
||||
0 0 0 0 0 0 1 1 == 1 + 2 = 3
|
||||
|
||||
An operation of `self.items += IT_SHOTGUN` would effectively do 3 + 1.
|
||||
That results in the decimal result of self.items being 4:
|
||||
|
||||
128 64 32 16 8 4 2 1
|
||||
0 0 0 0 0 1 0 0 == 4
|
||||
|
||||
So suddenly, our shotgun and super shotgun has been taken out the inventory!
|
||||
This effectively replaced those two with the nailgun, as IT_NAILGUN = 4.
|
||||
|
||||
Unsetting a flag reliably
|
||||
=========================
|
||||
|
||||
One easy to read way is the notation of:
|
||||
|
||||
self.items -= ( self.items & IT_SUPER_NAILGUN );
|
||||
|
||||
Basically what this does will keep whatever was in self.items
|
||||
and subtract whatever the result inside the ( ) is.
|
||||
self.items & IT_SUPER_NAILGUN will return `8` if present.
|
||||
So that means it'll do:
|
||||
|
||||
self.items -= 8;
|
||||
|
||||
If the super nailgun is present, and do:
|
||||
|
||||
self.items -= 0;
|
||||
|
||||
...if not. Which of course results in self.items not changing.
|
||||
A more effective alternative would be to do:
|
||||
|
||||
self.items &= ~IT_SUPER_NAILGUN;
|
||||
|
||||
But that's hard for most beginners to remember and to understand.
|
||||
|
||||
I'm just trying to keep things simple here. Most QuakeC starters
|
||||
are not engine programmers who know a lot about bit manipulation
|
||||
and bitwise operations.
|
||||
|
||||
232
games-util/fteqcc/additional-files/classes.txt
Normal file
232
games-util/fteqcc/additional-files/classes.txt
Normal file
@@ -0,0 +1,232 @@
|
||||
Classes and QuakeC
|
||||
==================
|
||||
By Marco Hladik
|
||||
Last updated: 8th of August 2018
|
||||
|
||||
Introduction
|
||||
============
|
||||
|
||||
Object Oriented Programming makes a lot of sense for game-logic.
|
||||
Especially in an environment like QuakeC, where the language already
|
||||
attempts an introduction of datatypes (in this case `entity`) that
|
||||
are meant to represent "objects".
|
||||
However, the entity type has its downsides once you attempt to
|
||||
scale the codebase.
|
||||
|
||||
The entity type in QuakeC uses something called `fields` to give
|
||||
them their attributes.
|
||||
|
||||
self.health = 100;
|
||||
|
||||
Sets the field `.health` of entity `self` to 100. .health is just one
|
||||
of many fields used in the stock QuakeC codebase.
|
||||
There are a set of fields that are a requirement to have, like .origin
|
||||
and .angles, as they're networked by default so that the engine knows
|
||||
how to render said entity. Some other fields like .weapon and .items
|
||||
as well as .armor are specific to Quake 1 only.
|
||||
|
||||
One thing that always annoyed me about regular entities in QuakeC is that
|
||||
every field was globally declared for _every_ other entity in the game.
|
||||
So you end up with thousands of fields (in the case of Xonotic) that
|
||||
are being allocated for the entities.
|
||||
Thousands of references, most of the time not going anywhere.
|
||||
This leads to some programmers committing something titled `field reuse`.
|
||||
Suddenly you'll see people use one field (like .health) to store info
|
||||
about something completely unrelated to what the name implies.
|
||||
|
||||
Each field costs each entity at least 4 bytes of memory.
|
||||
Vectors are worse, because they're 3 floats bundled together.
|
||||
So that's at least 12 bytes on every entity spawned - even when unused.
|
||||
|
||||
Anyway, classes and FTEQCC solve this problem somewhat.
|
||||
Classes are, technically, by extension, entities.
|
||||
They inherit all the global, standard fields any other entity gets.
|
||||
So your object of class "CFunBalloon" will have all of the globally
|
||||
defined fields .origin, .angles as well as .modelindex available.
|
||||
Classes can also be referenced as entities and manipulated:
|
||||
|
||||
CFunBallon tommy = spawn( CFunBallon );
|
||||
entity tommyreference = (entity)tommy;
|
||||
setorigin( tommyreference, '0 0 0' ); // Perfectly valid
|
||||
|
||||
This also works in reverse. If you know that the entity type being returned
|
||||
is of a specific class, you can cast it to said class and manipulate
|
||||
its exclusive fields. The compiler is fine with it.
|
||||
|
||||
Basic Declaration
|
||||
=================
|
||||
|
||||
Here it is:
|
||||
|
||||
class CMyClass { };
|
||||
|
||||
This officially declares a class, titled CMyClass. It has no attributes,
|
||||
other than the ones it inherits from all entities (aka all fields).
|
||||
It also has no methods associated with it.
|
||||
You don't have to call it C**** anything.
|
||||
You can also call it Bob or Miguel for all the compiler cares.
|
||||
That's just how most programmers, coming from other languages would declare it.
|
||||
|
||||
Attributes/Members/Fields
|
||||
=========================
|
||||
|
||||
Every language calls them differently. I don't think there's an official
|
||||
preference on how to call it in QuakeC, so I'll just call them attributes.
|
||||
|
||||
Now since we've got our CMyClass, we want to add an attribute to it.
|
||||
One that no other class or entity can use.
|
||||
|
||||
class CMyClass
|
||||
{
|
||||
int myattribute;
|
||||
};
|
||||
|
||||
Some languages have public/private declarations for attributes and methods.
|
||||
QuakeC doesn't care. Let's move on.
|
||||
|
||||
Spawning a class
|
||||
================
|
||||
|
||||
A class cannot just be defined and then manipulated.
|
||||
Just like an entity reference, it has to be spawned first.
|
||||
spawn() will take care of that.
|
||||
|
||||
CMyClass tom = spawn( CMyClass );
|
||||
|
||||
spawn() for standard entity types does not take a parameter, however
|
||||
it's a requirement here. The spawn() will also tell it to
|
||||
launch the constructor. Which we'll go into next.
|
||||
|
||||
Map entities can also directly spawn and be initialized as a class instead
|
||||
of a function.
|
||||
|
||||
Constructor
|
||||
===========
|
||||
|
||||
The constructor needs to be defined first inside the class description:
|
||||
|
||||
class CMyClass
|
||||
{
|
||||
int myattribute;
|
||||
|
||||
void() CMyClass; // The constructor declaration
|
||||
};
|
||||
|
||||
Upon spawning the class with spawn(), it'll look for the member method
|
||||
titled after said class.
|
||||
|
||||
// The constructor definition
|
||||
void CMyClass :: CMyClass ( void )
|
||||
{
|
||||
if ( !myattribute )
|
||||
myattribute = 100;
|
||||
}
|
||||
|
||||
Inside this example constructor, it'll define myattribute to be 100 when
|
||||
created via spawn() and myattribute has no value.
|
||||
|
||||
It's also possible to modify the attributes inside of an object
|
||||
with the spawn() intrinsic before the constructor is called:
|
||||
|
||||
CMyClass tom = spawn( CMyClass, myattribute: 255, model: "Tom" );
|
||||
|
||||
Will set the myattribute to 255. Which will prevent the constructor
|
||||
from re-defining it to 100 due to the `if (!myattribute)` in the constructor.
|
||||
|
||||
It'll also set the `model` field it inherits from entities to "Tom".
|
||||
Just to give you an example of what defining multiple attributes looks like
|
||||
when using spawn().
|
||||
|
||||
Methods
|
||||
=======
|
||||
|
||||
Let's create a method titled 'Destruct'. This will teach more than one thing
|
||||
in particular.
|
||||
|
||||
Declaration (shove that into your class description somewhere):
|
||||
virtual void() Destruct;
|
||||
|
||||
Definition:
|
||||
void CMyClass :: Destruct ( void )
|
||||
{
|
||||
remove( this );
|
||||
}
|
||||
|
||||
`this` effectively acts like `self`, when referring to which object is calling
|
||||
its member function.
|
||||
|
||||
This also changes the way you can call member functions:
|
||||
|
||||
entity oldself = self;
|
||||
self = other_entity;
|
||||
self.FooBar();
|
||||
self = oldself;
|
||||
|
||||
Can now be replaced comfortably with:
|
||||
|
||||
myobject.FooBar();
|
||||
|
||||
Because the compiler knows that if FooBar() is a method of a class that you'll
|
||||
be wanting to execute it on `myobject` and not whatever `self` is set to.
|
||||
In Stock QuakeC you always have to beware what `self` is set to when calling
|
||||
entity functions. Not here.
|
||||
|
||||
Conversion
|
||||
==========
|
||||
|
||||
It is indeed possible to convert an already spawned, existing entity.
|
||||
This means going from entity to an object of a class.
|
||||
For example, in Server-Side QuakeC there's the function PutClientInServer.
|
||||
If you were to put the following line in there:
|
||||
|
||||
spawnfunc_CMyClass();
|
||||
|
||||
It would convert the already spawned player entity into an object of the class
|
||||
CMyClass.
|
||||
|
||||
Inheritance
|
||||
===========
|
||||
|
||||
One of the biggest pros of classes in my opinion.
|
||||
We all know that CMyClass has already inherited all the standard `entity` type
|
||||
fields. However, FTEQCC supports single inheritance.
|
||||
|
||||
class CCoolestClass : CMyClass
|
||||
{
|
||||
int newattribute;
|
||||
void() CCoolestClass;
|
||||
};
|
||||
|
||||
void CCoolestClass :: CCoolestClass ( void )
|
||||
{
|
||||
myattribute = 666;
|
||||
newattribute = 123;
|
||||
Destruct();
|
||||
}
|
||||
|
||||
So now CCoolestClass inherited all of CMyClass and the basic `entity` type.
|
||||
This allows an unprecedented amount of complexity conveyed in very few
|
||||
pieces of code.
|
||||
|
||||
Remember that if you override a method of a class you inherit, that you have to
|
||||
declare that as such inside the class definition.
|
||||
|
||||
Problems/Workarounds
|
||||
====================
|
||||
|
||||
As you noticed in the Chapter for Methods, we can set fields without
|
||||
specifically notating `this.` before setting `myattribute`.
|
||||
This results in functions like e.g. find() to complain if you were to call
|
||||
it like this inside a member method:
|
||||
|
||||
blargh = find( world, netname, "Tom" ); // error
|
||||
|
||||
It will tell you about 'netname' not being defined. The compiler thinks that
|
||||
netname is a member of your class whose value you want to pass as a parameter
|
||||
and not a type of field/attribute reference.
|
||||
|
||||
Adding a :: before netname will tell the compiler that you're
|
||||
looking for a field/attribute.
|
||||
|
||||
blargh = find( world, ::netname, "Tom" ); // valid
|
||||
|
||||
585
games-util/fteqcc/additional-files/csqc_for_idiots.txt
Normal file
585
games-util/fteqcc/additional-files/csqc_for_idiots.txt
Normal file
@@ -0,0 +1,585 @@
|
||||
This little document is a little primer on CSQC for people that have no understanding of it.
|
||||
The reader is expected to have an understanding of SSQC basics (entities, globals, functions, prototypes, fields, etc).
|
||||
As I am the primary author of FTEQW+FTEQCC, I am biased towards it and its feature set. I will attempt to include sidenotes where my instructions might not match other engines (namely: DarkPlaces), but the code examples given are intended to run in either engine.
|
||||
|
||||
|
||||
|
||||
|
||||
Motivation for CSQC:
|
||||
Quake's archetecture is that of client and server. Even in a single player game, your engine is running both a client and server simultaneously (for example, demos are simply a capture of all the server->client packets).
|
||||
This archetecture brings with it the ability for new clients to connect mid-game, without having to have run the game right from the start of the map.
|
||||
And this is the distinction between SSQC and CSQC.
|
||||
Traditional QC code is compiled into a single progs.dat file which runs purely on the server. It controls where, when, and how things move from one place to another, and the server will automatically 'replicate' a subset of the current state of the game to each connected client each network frame, and that is that subset which the client draws (for terminology, Quake3 refers to these gamestate copies as 'snapshots', which is a good way to think of it).
|
||||
SSQC, running on the server, has absolutely no control over the client beyond what it can send it. Primarily this includes snapshots, stuffcmds, stats, and 'temporary entities'. But worse than this, SSQC is commonly running on a remote machine, with latency, packetloss, and other networking limitations.
|
||||
|
||||
CSQC provides a way around these issues.
|
||||
A) CSQC has full control over the screen. It can draw the game where it wants. It can draw a hud how it wants.
|
||||
B) CSQC is running locally. It has no latency issues at all. Mouse movement is present directly within the frame that follows, rather than needing to bounce off the server.
|
||||
C) CSQC can perform its own player prediction, allowing mods to freely provide their own player physics without breaking client expectations.
|
||||
D) CSQC can interact with input devices like keyboard and mice, providing rich user interfaces.
|
||||
|
||||
CSQC's down sides.
|
||||
A) CSQC is separate and distinct from SSQC. The two modules share no data (but may still share significant parts of code). You will need to learn how to get data from ssqc to csqc and vice versa.
|
||||
B) As a large after-gpl feature, different engines tend to have their own interpretation of the CSQC spec. As a result, cross-engine compatibility may be hard, but is not impossible.
|
||||
C) CSQC's versatility can be a curse. The engine tries to avoid doing too many things automatically, most noticably in regards to the screen - a CSQC mod with functions but no code results in nothing being drawn to the screen. Mods need a small bootstrap chunk of code to retain feature compatibility.
|
||||
D) CSQC attempts to clean up the API slightly, which can result in certain differences between SSQC and CSQC.
|
||||
E) Compared to SSQC, CSQC is often written in the context of a single client. It has a greater focus on callbacks, globals, and builtins to retrieve data from snapshots and other hiding places within the engine.
|
||||
F) CSQC needs a certain amount of cheat protection. Thus there are limitations in place to ensure that the csprogs matches one available on the server (the original versions had other additional limitations!).
|
||||
G) CSQC is running on the client, and is typically autodownloaded. The client must thus also place certain additional restrictions on console commands and cvars, just as it would via stuffcmds.
|
||||
|
||||
In essence, csqc sits somewhere between the middle of the client's renderer, networking, and input modules. Basically, the use input is filtered via csqc before going to the client/networking. Snapshots are still parsed by the engine's networking, but the renderer will not draw them without the csqc saying to do so.
|
||||
|
||||
|
||||
|
||||
|
||||
Setting up a basic CSQC mod and compiling:
|
||||
CSQC is compiled just like SSQC. Both start from a .src file, but typically a different one.
|
||||
While you can use a progs.src inside a different directory, I will write this with the expectation that your CSQC's .src file can be found as eg: c:\quake\mymod\src\csprogs.src
|
||||
Where SSQC outputs to a 'progs.dat' (or 'qwprogs.dat'), CSQC outputs to a 'csprogs.dat' file. This is forms the first line of your csprogs.src file. Typically with a ..\ prefix so that the qcc writes it to the right directory...
|
||||
The second line of your csprogs.src should be to specify some sort of 'defs.qc' equivelent for CSQC. Sadly, CSQC has different system+field+builtin defs from SSQC and thus needs a different file.
|
||||
With FTEQW, you can get the engine to generate such a file for you by issuing the command 'pr_dumpplatform -O csdefs -Tcs' at FTEQW's console (be warned that it includes FTEQCC extensions which will not work in other qccs). You'll want to copy this file to your src directory (instead of it being in your home dir where the qcc can't see it - the given command will name the system path that you'll need to copy it from). If you're exclusively targetting DP, you may wish to get DP-specific defs from other sources, but be warned that certain builtins may have different names, and you will need to work around that. For the things documented here, using an FTEQW-generated csdefs.qc should work okay with current DP versions.
|
||||
And the third line of your csprogs.src file should name one of your own code files (which you will need to write).
|
||||
|
||||
So, inside c:\quake\mymod\src\csprogs.src we have these lines:
|
||||
../csprogs.dat //the name of the file to
|
||||
csdefs.qc //system defs to match the engine, including various extensions
|
||||
view.qc //name it whatever you want. This is where you will be writing code.
|
||||
|
||||
To make things easy (assuming you're using FTEQCC), you can add the following line to your defs.qc
|
||||
#pragma sourcefile csprogs.src
|
||||
This line will cause fteqcc to automatically start compiling your csprogs.dat after it compiles your progs.dat. Its one of those convienience features.
|
||||
Note that this will not affect your actual progs.dat file (beyond compiling it).
|
||||
Alternatively you can rename your progs.src to ssprogs.src, and make a new progs.src that contains two pragmas that name your two src files, then you can freely choose just ssqc, just csqc, or both.
|
||||
Or you can instead use separate batch files and the -srcfile argument to distinguish between which src file to use.
|
||||
Create an empty view.qc file and try compiling. You should get a csprogs.dat from it (side note: if you used DP-specific defs instead, you'll get errors about functions not being defined, you'll need to make stubs for these at some point, typically they'll need to return false).
|
||||
|
||||
|
||||
|
||||
|
||||
Our first Code (the 3d game view):
|
||||
open up your empty view.qc file and add this function:
|
||||
void(float width, float height, float menushown) CSQC_UpdateView =
|
||||
{
|
||||
drawfill('0 0 0', [width, height, 0], '1 1 0', 1, 0);
|
||||
drawstring('0 0 0', "Hello, World", '8 8 0', '0 0 1', 1, 0);
|
||||
};
|
||||
If you save+compile+run, you will now find that your fledgeling mod has had its first effect... The game view has disappeared, to be replaced by a yellow screen! oh noes! By adding that function, the engine now knows you want to take explicit control of the screen, and it is no longer doing that automatically for you. So we need to at least get back to what we had. This will need some explaination. If you're just aiming for a hud and nothing else, you can grab the example code at the end of this section and otherwise skip this section.
|
||||
|
||||
Contrary to 2d drawing, 3d drawing in csqc is based around building up an entire scene then telling the renderer to do its stuff in one go. This model is somewhat derived from the way Quake3 works, and allows the engine to provide its own shadow, rtlight, etc effects within the gameview as a whole. 3d rendering is thus based upon the concept of an active scene or view (with its set of view properties), as well as a list of the entities present in that scene.
|
||||
Due to certain expectations with input, the scene is NOT normally cleared automatically by the engine. Even if it was, certain default properties change over frames anyway (like the player position). Thus the First thing we need to do is to clear the scene with:
|
||||
clearscene();
|
||||
Well, okay, that function name was a little obvious. Remember I said that the scene had various 'view properties'? Well, clearscene just set them to their default values for you. However, the default is for the engine to not draw a hud at all. So if only as an example of how to reconfigure scene properties, you'll need the following line too:
|
||||
setviewprop(VF_DRAWENGINESBAR, 1);
|
||||
Lets throw in a crosshair too:
|
||||
setviewprop(VF_DRAWCROSSHAIR, 1);
|
||||
For reference:
|
||||
//setviewprop(VF_MIN, '0 0 0');
|
||||
//setviewprop(VF_SIZE, [width, height, 0]);
|
||||
Will allow you to move the 3d view around on the screen. The min+size values should be in virtual positions, but DP currently uses physical pixels (sidenote: alternative coordinate systems SUCK!)
|
||||
Right, so now our scene includes a crosshair, a status bar (aka: hud). The view position and angles are in their default positions (ie: angle pulled from client state, origin pulled from prediction or lerped from snapshots).
|
||||
Its still not being drawn though. We've only said where it should be. We've not actually said to draw it.
|
||||
So lets do that for the luls:
|
||||
renderscene();
|
||||
Urr, yeah, another obvious name there from Mr Imaginative.
|
||||
If you compile and run, what do we get? Try it!
|
||||
You didn't try it did you. You just read the next line without bothering... Well, if you HAD run it, you would have seen that we now get a fullscreen view of the game, with an sbar and the world. We can run around as normal... But there's something missing, and that, my friend, is entities.
|
||||
renderscene will not automatically hunt for entities for you. The easiest way to achieve this is with the following line:
|
||||
addentities((intermission?0:MASK_VIEWMODEL)|MASK_ENGINE);
|
||||
Of course, you'll want to ensure that the call is between clearscene and renderscene, or they'll just be wiped at the start of the next frame.
|
||||
This builtin reaches into the networking snapshot code and pulls out all the entities that match the masks. MASK_ENGINE refers to the snapshot entities. MASK_VIEWMODEL refers to the entity that is normally generated from your ssqc player's .weaponmodel and .weaponfame fields (which you generally don't want to appear during intermission).
|
||||
If you have spawned some entities in csqc, you can call addentity(yourent) to add a copy of it the scene also. This is useful for one-only entities like custom view models being predicted in csqc.
|
||||
But for csqc entities, the easiest way to add them to the scene is to set your entity's .drawmask to something evil like MASK_ENGINE, and then addentities will automatically add it to the scene for you. More on this later.
|
||||
|
||||
One thing to note about the following code is that getviewprop also exists, if you wish to read a default like VF_ORIGIN. For instance, you can make the view bob randomly by reading its current position, adding sin(time*period)*magnitude to the z coord.
|
||||
|
||||
So, to compact that into something small for the lazy people who skipped most of this section. I'll add a few other no-op values commented out as the value given is the default(ish) already.
|
||||
void(float width, float height, float menushown) CSQC_UpdateView =
|
||||
{
|
||||
clearscene(); //wipe entity lists. reset view properties to their defaults.
|
||||
setviewprop(VF_DRAWENGINESBAR, 1); //draw a status bar (or hud or whatever the engine normally does) around the screen.
|
||||
setviewprop(VF_DRAWCROSSHAIR, 1); //draw a crosshair in the middle of the screen.
|
||||
//setviewprop(VF_ORIGIN, '0 0 0'); //view position of the scene (after view_ofs effects).
|
||||
//setviewprop(VF_ANGLES, '0 0 0'); //override the view angles. input will work as normal. other players will see your player as normal. your screen will just be pointing a different direction.
|
||||
//setviewprop(VF_DRAWWORLD, 1); //whether the world entity should be drawn. set to 0 if you want a completely empty scene.
|
||||
//setviewprop(VF_MIN, '0 0 0'); //top-left coord (x,y) of the scene viewport in virtual pixels (or annoying physical pixels in dp).
|
||||
//setviewprop(VF_SIZE, [width, height, 0]); //virtual size (width,height) of the scene viewport in virtual pixels (or annoying physical pixels in dp).
|
||||
//setviewprop(VF_AFOV, cvar("fov")); //note: fov_x and fov_y control individual axis. afov is general
|
||||
//setviewprop(VF_PERSPECTIVE, 1); //1 means like quake and other 3d games. 0 means isometric.
|
||||
addentities((intermission?0:MASK_VIEWMODEL)|MASK_ENGINE); //add various entities to the scene's lists.
|
||||
renderscene(); //draw the scene to the screen using the various properties.
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
Our first hud (2d stuff and stats):
|
||||
Unlike 3d stuff which uses some huge complex scene concept, 2d stuff is immediate only. All 2d drawing calls are immediately sent to the renderer in the exact order they are given, and can thus overlap without issues. Later calls will always overwrite whatever is already on the screen at the position specified.
|
||||
First of all, add a call at the bottom of your CSQC_UpdateView function to some Hud_Draw function:
|
||||
Hud_Draw([width, height]);
|
||||
And then include the following code just before your CSQC_UpdateView function (or in a different file ideally, just be sure to prototype etc like you would in ssqc):
|
||||
float stitems, stitems2, stweapon;
|
||||
void Hud_Draw(vector scrsz)
|
||||
{
|
||||
vector pos = [(scrsz_x-320)/2, pos_y = scrsz_y - 24, 0]; //calculate the top-left of the sbar, assuming it is 320 units wide and placed in the bottom-middle of the screen
|
||||
//get some commonly refered to values
|
||||
stitems = getstatbits(STAT_ITEMS, 0, 23); //this is the player's self.items value (STAT_ITEMS is generated specially by the server)
|
||||
stitems2 = getstatbits(STAT_ITEMS, 23, 9); //this is the player's self.items2 value if it exists, otherwise it is the serverflags global
|
||||
stweapon = getstatf(STAT_ACTIVEWEAPON); //this is the player's self.weapon value.
|
||||
|
||||
drawpic(pos, "sbar", '320 24 0', '1 1 1', 0.5, 0); //draw the sbar background image slightly transparently
|
||||
|
||||
Hud_DrawLargeValue(pos+'24 0 0', getstatf(STAT_ARMOR), 25);
|
||||
Hud_DrawLargeValue(pos+'136 0 0', getstatf(STAT_HEALTH), 25);
|
||||
Hud_DrawLargeValue(pos+'248 0 0', getstatf(STAT_AMMO), 10);
|
||||
};
|
||||
|
||||
And here's the helper function I used which wasn't defined above.
|
||||
void(vector pos, float value, float threshhold) Hud_DrawLargeValue =
|
||||
{
|
||||
float c;
|
||||
float len;
|
||||
string s;
|
||||
if (value < 0)
|
||||
value = 0; //hrm
|
||||
if (value>999)
|
||||
value = 999;
|
||||
s = ftos(floor(value));
|
||||
len = strlen(s);
|
||||
pos_x += 24 * (3-len);
|
||||
if (value <= threshhold)
|
||||
{ //use alternate (red) numbers
|
||||
|
||||
while(len>0)
|
||||
{
|
||||
len-=1;
|
||||
c = str2chr(s, len);
|
||||
drawpic(pos+len * '24 0 0', sprintf("anum_%g", c-'0'), '24 24 0', '1 1 1', 1, 0);
|
||||
}
|
||||
}
|
||||
else
|
||||
{ //use normal numbers
|
||||
|
||||
while(len>0)
|
||||
{
|
||||
len-=1;
|
||||
c = str2chr(s, len);
|
||||
drawpic(pos+len * '24 0 0', sprintf("num_%g", c-'0'), '24 24 0', '1 1 1', 1, 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Okay, try running that.
|
||||
Now you get your basic sbar appearing, with health and armor values, that goes red if the values are low.
|
||||
Note that I'm not going to give any more hud code. Go write your own! Here's one I wrote earlier: http://sourceforge.net/p/fteqw/code/HEAD/tree/trunk/quakec/csqctest/src/cs/hud.qc?format=raw
|
||||
|
||||
There's a few generic builtins there which you may have never seen before. sprintf: generates a formatted temp-string (%g, %f are replaced with a float argument, %s uses a string argument, %v uses a vector argument, most other things can use %i for debugging), str2chr(x,y): reads the character at position Y from string X. These are both available in ssqc as parts of various extensions and are really quite useful.
|
||||
Here are the builtins for 2d stuff:
|
||||
vector(string picname) drawgetimagesize = #318; //retrieves the size of a picture.
|
||||
float(vector position, float character, vector size, vector rgb, float alpha, optional float drawflag) drawcharacter = #320;//draws a single character.
|
||||
float(vector position, string text, vector size, vector rgb, float alpha, optional float drawflag) drawrawstring = #321;//draws a string without stopping for colour codes
|
||||
float(vector position, string text, vector size, vector rgb, float alpha, float drawflag) drawstring = #326; //supports colour codes
|
||||
float(vector position, string pic, vector size, vector rgb, float alpha, optional float drawflag) drawpic = #322; //draw a picture on the screen. gfx/foo.lmp, or no extra path+extension for wad images.
|
||||
float(vector position, vector size, vector rgb, float alpha, optional float drawflag) drawfill = #323; //draws a simple box
|
||||
float(string text, float usecolours, optional vector fontsize) stringwidth = #327; //gets the virtual width of the string, so you can size things properly.
|
||||
void(vector pos, vector sz, string pic, vector srcpos, vector srcsz, vector rgb, float alpha, optional float drawflag) drawsubpic = #328;//draws a subregion of a picture.
|
||||
All of those builtins can be used to draw various 2d stuff. Using different fonts is an extra extension that I'm too lazy to explain here.
|
||||
But there's also getstatf and getstatbits that need some explaining. To do so, I'll need to explain the network protocol a little.
|
||||
|
||||
Quake's networking protocol is more than just entity snapshots. It also includes a concept of stats. In NQ, different stats are transfered to the client with various limitations, while in QW they're generally more generic. Generic aproaches are good, so all stats are exposed via the same sort of interface similar to the one that the engine uses.
|
||||
Basically, all the various player-specific fields form the various bits of your hud are various 'stats' (technical term rather than general term).
|
||||
The first 32 stats are reserved by the engine for one reason or another, either for future expansion or because its just part of the protocol that has always existed (they are typically some representation of various player fields and are safe to read - later ones may depend upon server extensions). Stats between 32 and 127 inclusive are 'free' stats reserved for mod-specific purposes by QC.
|
||||
getstatf reads the numeric stat into a float.
|
||||
getstati reads the numeric stat into an integer (as most qc code doesn't support ints, you would normally think it useless).
|
||||
getstats reads a string stat, returning a tempstring.
|
||||
getstatbits is a bit more awkward, and is provided only for legacy compatibility with STAT_ITEMS. This needs more explaining... STAT_ITEMS, as you will see in the Hud_Draw function I gave you, is read twice. Why read it twice, you might ask... Because STAT_ITEMS is an integer bitfield over the network. Worse - the upper bits of this integer (which cannot normally be used in floats due to precision) is packed with info on runes currently held! So, in order to read both parts of this stat, you need a special builtin which can decode the various integer bits into a float which qc can actually use. The positions and bitcounts should generally be useable for any mod, although you might need to handle runes being a little shifted if your mod does not make use of items2.
|
||||
So that's how you read a stat, but you'll still need to know how to create your own stats for your awesome hud, and that requires some ssqc (which is where the info is).
|
||||
|
||||
The main way to add a stat is with this builtin:
|
||||
void(float num, float type, .__variant fld) clientstat = #232;
|
||||
Which is typically invoked from worldspawn using something like:
|
||||
clientstat(STAT_MYSUPERSTAT, EV_FLOAT, mysuperstat);
|
||||
where mysuperstat was defined in defs.qc or whereever as:
|
||||
.float mysuperstat;
|
||||
And, as noted above, is read with getstatf(STAT_MYSUPERSTAT)
|
||||
You'll want to share the STAT_FOO defines between ssqc+csqc.
|
||||
|
||||
.int foo; clientstat(STAT_FOO, EV_INTEGER, foo); int myfoo = getstati(STAT_FOO); works too, if your qcc+engine support integers.
|
||||
.string foobar; clientstat(STAT_FOOBAR, EV_STRING, foobar); string myfoobar = getstats(STAT_FOOBAR); works as well. Hurrah.
|
||||
.entity enemy; clientstat(STAT_ENEMYNUM, EV_INTEGER, foo); float myenemynum = getstatf(STAT_ENEMYNUM); works too, if your qcc+engine support integers.
|
||||
However, vectors are not normally supported as stats. You will need to separate the various componants (ie: myvector_x) and send+read as 3 separate floats.
|
||||
Entities are sent only as numbers _in the ssqc_. The numbers don't always match up, thus findfloat(world, entnum, getstatf(STAT_ENTNUM)) generally needs to be used if its to refer to some csqc entity. Note that it may still evaluate to world, as entities may not be in the pvs or may not even be visible to csqc. Stats and snapshots may all have different latency and arrive at different times due to packetloss or whatever.
|
||||
As noted above, those custom STAT_FOO values should normally be between 32 and 127 for futureproofing. You can generally go higher too, but its not guarenteed.
|
||||
|
||||
Sidenote: DP compat with stats is more awkward. Unlike FTEQW, DP still has exclusively integer stats. numeric stats are specifically integer stats thus float stats are thus rounded towards 0. strings on the other hand are limited to 15 chars, and actually consume 4 consecutive stats. If you actually need floating point precision with DP, you can tell the engine that the stat is actually an integer, using ev_integer and getstati. Doing so will avoid truncation and has resulted in some enterprising dp users renaming getstati to getstatf just to confuse everyone, so be careful of that if you're using dp-specific defs...
|
||||
|
||||
|
||||
|
||||
|
||||
User Input (guis):
|
||||
User input (keyboard and mice and stuff) can be intercepted using the CSQC_InputEvent function.
|
||||
This function is a generic entry point for multiple different sorts of events. The type of event is passed in evtype. The later arguments have different meanings depending on the event type.
|
||||
Event handling is basically implemented via interception. That is, if you want to do something with the event you can do so, and you can then 'cancel' the event in the engine's eyes by returning TRUE.
|
||||
So, if you did something and now want the engine to ignore it, return TRUE. If you want the engine to handle the event for you, return FALSE.
|
||||
Note that not all events can be meaningfully canceled, and other events should not be canceled.
|
||||
For example, if the user presses a key, the key has been pressed, the unicode meaning of that key combination is still determined (normally by the operating system), and you will still receive a release event when the user releases the key, but by cancelling the event, you can get the engine to avoid doing any key binding logic. Of course, key UP events should generally never be canceled as a general rule. The reasoning for this is that if you are cancelling up events, you must be very careful in tracking down events and the owners of those down events, because if you cancel a down event and NOT an up event, you can end up with the engine's key binding system thinking a key is still pressed and thus with the player running forwards endlessly.
|
||||
So:
|
||||
Key down events:
|
||||
scanx: this is the quake KEY_ code value associated with the physical hardware button. May be set to 0. You'll always get the same code for each key regardless of whether shift etc are pressed also.
|
||||
chary: this is the unicode code point of the key. May be set to 0. You'll get different values from the same key depending on whether shift etc is pressed.
|
||||
devid: for mouse buttons, this is a mouse-device/touch-event id (see mouse absolute events for details). for keyboard buttons, this is a keyboard device id.
|
||||
cancel to avoid bindings (like disabling weapon switches or +forward etc).
|
||||
Either scanx or chary may be set to 0, but not both at the same time.
|
||||
Some windowing systems support scanx and chary both being set in a single call (like scanx==KEY_1 and chary=='!'), or may invoke the function twice (ie: scanx==KEY_1 and chary==0, and scanx==0 and chary=='!').
|
||||
This is significant when it comes to dead keys and unicode input in certain locales.
|
||||
Thus if you are typing, you should use ONLY chary for actual typing, with scanx for control keys like the cursor keys+delete, and otherwise support bindings via the scanx value and ignore chary.
|
||||
key up events:
|
||||
scanx: as in key down events.
|
||||
chary: as in key down events.
|
||||
devid: as in key down events.
|
||||
It is not guarenteed that you will receive unicode up events as the windowing system may not support it, but you will receive scancode up events.
|
||||
cancelling avoids cancelling -forward type bindings which should normally always be safe for duplicates, thus cancelling up events should generally be avoided.
|
||||
mouse motion events:
|
||||
scanx: how many pixels across the screen the mouse has moved (scaled in terms of virtual pixels, so may be fractional).
|
||||
chary: how many pixels down the screen the mouse has moved (scaled in terms of virtual pixels, so may be fractional).
|
||||
devid: a mouse device or touch event id. see mouse absolute events for details.
|
||||
If running a UI, you would want to add the two values to the previous mouse cursor position before returning TRUE.
|
||||
cancel to avoid the engine changing view angles.
|
||||
mouse absolute events:
|
||||
scanx: the absolute X position on the screen in terms of virtual pixels.
|
||||
chary: the absolute X position on the screen in terms of virtual pixels.
|
||||
devid: a mouse device or touch event id.
|
||||
The device id for mice and touch events can be somewhat complex.
|
||||
For mice, you are likely to see all mice appear as id 0, simply because the operating system merges all mouse devices to simulate a single one which is all the engine can see.
|
||||
On windows, you tend to need an engine that supports raw input in order to get distinguishable mouse devices.
|
||||
When used in a mouse-driven user interface, a mouse devid is a simple mouse device id that is constant for every event that the mouse generates.
|
||||
For touch events, things are more complicated. Touch ids have a limited life span in order to facilitate multitouch screens.
|
||||
A multitouch sequence thus follows these lines:
|
||||
mouse absolute
|
||||
key down (scanx=key_mouse1)
|
||||
mouse absolute etc
|
||||
key up (scanx=key_mouse1)
|
||||
After which, the touch id can be recycled as a different touch event.
|
||||
(Side note: to test multitouch in fteqw+vista+, use these console commands: in_simulatemultitouch 1; in_rawinput 1; in_restart; note that you will need multiple mice plugged in, one for each 'finger')
|
||||
A mouse device may use either mouse motion or mouse absolute events depending on whether the mouse is grabbed for use exclusively with quake or not.
|
||||
It is not uncommon for engines to automatically switch to absolute positions when the console is down.
|
||||
cancel to avoid changing view angles if the engine has some sort of touchscreen mode enabled, otherwise does nothing.
|
||||
accelerometer events:
|
||||
scanx: x acceleration
|
||||
chary: y acceleration (-9.8 (gravity) if the screen is held vertically I believe)
|
||||
devid: z acceleration (-9.8 (gravity) if the screen is flat facing upwards)
|
||||
focus events:
|
||||
scanx: now has mouse focus. true=focused. false=unfocused. -1=unchanged
|
||||
chary: now has keyboard focus. true=focused. false=unfocused. -1=unchanged
|
||||
devid: the mouse or keyboard for which focus changed.
|
||||
for the sake of simplicity, you can just ignore any events with a devid other than 0.
|
||||
note that it is the engine's choice whether the qc receives input or not. if the console or the menus are active, those may exclusively receive input instead.
|
||||
|
||||
So for a windowing system with a mouse cursor and a simple text field, we can write this simple code:
|
||||
string userinputtext;
|
||||
vector mousecursor;
|
||||
float(float evtype, float scanx, float chary, float devid) CSQC_InputEvent =
|
||||
{
|
||||
switch(evtype)
|
||||
{
|
||||
case IE_KEYDOWN:
|
||||
string newt = userinputtext;
|
||||
if (scanx == KEY_BACKSPACE)
|
||||
newt = substring(newt, 0, -2); //-1 = end of string. -2=one char before the end of the string
|
||||
else if (scanx == KEY_ENTER)
|
||||
{
|
||||
//send it to the server in the form of a say command. just to show some useful(ish) interaction with the server.
|
||||
localcmd(strcat("cmd say ", userinputtext, "\n"));
|
||||
newt = "";
|
||||
}
|
||||
else if (chary)
|
||||
newt = strcat(newt, chr2str(chary)); //add the char to the end.
|
||||
else if (scanx == KEY_MOUSE2)
|
||||
newt = ""; //clear it on right click.
|
||||
//other scancodes not recognised, but don't add redundant 0s on the end.
|
||||
//temp strings are not valid between calls.
|
||||
if (newt != userinputtext)
|
||||
{
|
||||
strunzone(userinputtext);
|
||||
userinputtext = strzone(newt);
|
||||
}
|
||||
break;
|
||||
case IE_KEYUP:
|
||||
return FALSE; //don't break things if the menu was enabled mid-game while other keys are potentially still held, or some other way.
|
||||
case IE_MOUSEDELTA:
|
||||
//note: we can cope with multiple separate mouse devices here.
|
||||
mousecursor_x += scanx;
|
||||
mousecursor_y += chary;
|
||||
return TRUE; //don't change view angles
|
||||
case IE_MOUSEABS:
|
||||
if (devid != 0) //no stuttering please.
|
||||
return FALSE;
|
||||
mousecursor_x = scanx;
|
||||
mousecursor_y = chary;
|
||||
return TRUE; //don't change view angles
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
};
|
||||
//NOTE: this conflicts with previous examples! skip the drawfill and add the rest after your renderscene call or something.
|
||||
void(float width, float height, float menushown) CSQC_UpdateView =
|
||||
{
|
||||
drawfill('0 0 0', [width, height, 0], '1 1 0', 1, 0); //clear the screen
|
||||
drawstring('0 0 0', userinputtext, '8 8 0', '0 0 1', 1, 0); //draw their input text
|
||||
float sc = (sin(time*8)+1.5)*8
|
||||
drawcharacter(mousecursor - '0.5 0.5 0'*sc, '+', sc*'1 1 0', '0 0 1', 1, 0); //draw a lame cursor with pulsing size (to distinguish it from any possible engine cursor)
|
||||
};
|
||||
|
||||
Of course, as you go further into windowing systems, you'll need to make some sort of heirachy or screen regions or some other logic so that things can be clicked on. Explaining how to write UI wigits is somewhat outside of the scope of this tutorial, so look elsewhere for such things.
|
||||
|
||||
If you wish to use a 2d mouse cursor to interact with 3d positions, you can do something like:
|
||||
vector() CursorToWorldCoord =
|
||||
{
|
||||
vector wnear = unproject([mousecursor_x, mousecursor_y, 0]); //determine the world coordinate for the mouse cursor upon the near clip plane
|
||||
vector wfar = unproject([mousecursor_x, mousecursor_y, 100000]);//determine the world coordinate for the mouse cursor upon the far clip plane, with an outrageously large value as a workaround for dp.
|
||||
traceline(wnear, wfar, TRUE, world);
|
||||
return trace_endpos;
|
||||
};
|
||||
Similarly:
|
||||
vector(vector worldpos) WorldToScreen =
|
||||
{
|
||||
vector twodee = project(worldpos);
|
||||
twodee_z = 0; //discard all depth info (note that depth is typically scaled between 0 and 1, so avoid)
|
||||
return twodee;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
Right, now its time to explain CSQC-only entities in proper detail:
|
||||
I'm going to introduce you to a new entrypoint just for the luls:
|
||||
void(float apiver, string enginename, float enginever) CSQC_Init =
|
||||
{ //note: the three arguments are engine-defined, and can be used to to detect if the engine version you're running in has bugs. Just call error("please update your engine") if it's got known fatal bugs, but try not to exclude engines you've not tested against (ie: forks where your code will otherwise work fine).
|
||||
local entity foo = spawn();
|
||||
precache_model("progs/player.mdl"); //or something else
|
||||
setmodel(foo, "progs/player.mdl"); //same as ssqc, see, its easy!
|
||||
setorigin(foo, '0 0 0'); //you'll need to fix the origin to ensure you don't put it in a wall...
|
||||
foo.drawmask = MASK_ENGINE; //cause the entity to be added to the scene automatically via the addentities(MASK_ENGINE) builtin.
|
||||
};
|
||||
|
||||
So yeah, when the csprogs is loaded, the CSQC_Init function is automatically called by the engine. Inside we spawn an entity, just like we would in ssqc. Give it a position, just like you would in ssqc, etc.
|
||||
In fact, the only difference so far is the extra drawmask line. This line is so that the core csqc can handle subviews gracefully instead of all entities being added to the scene unconditionally.
|
||||
So if we try running it, our CSQC_UpdateView (at least ones that actually draw a view) automatically pick up our new entity, but we've not set a think function nor are we trying to animate it.
|
||||
CSQC entities do not interpolate or autoanimate, nor does the engine interpolate them. What's worse, is that think functions with their predefined interval can result in video frames skipping with jerky animations! oh noes!
|
||||
So lets add an extra csqc-specific line to where you spawned your entity.
|
||||
foo.predraw = AnimateSomeLameModel;
|
||||
Nice simple function field assignment there...
|
||||
And we need to define that function. Here's one I wrote earlier:
|
||||
float() AnimateSomeLameModel =
|
||||
{
|
||||
#define FIRSTFRAME 0
|
||||
#define NUMFRAMES 6
|
||||
self.lerpfrac -= frametime * 10; //animate at 10fps
|
||||
while(self.lerpfrac < 0) //protects against sudden low framerates.
|
||||
{
|
||||
self.frame2 = self.frame; //if we're at 0, frame2 became redundant anyway
|
||||
self.frame += 1; //move on to the next frame
|
||||
if (self.frame >= FIRSTFRAME+NUMFRAMES) //this should be relative to the current animation that should be shown
|
||||
self.frame = FIRSTFRAME; //restart it.
|
||||
self.lerpfrac += 1; //go to the start of the frame
|
||||
}
|
||||
|
||||
makevectors(getviewprop(VF_ANGLES)); //set v_forward etc.
|
||||
vector vieworg = getviewprop(VF_ORIGIN);//read the current view origin
|
||||
setorigin(self, vieworg + v_forward*128);//reposition the entity to 128 units infront of the view so it doesn't get lost in the wall, so we know its visible.
|
||||
return FALSE;
|
||||
};
|
||||
Yes, its completely lame, but it demonstrates the basics of animation, which is what is important, right?
|
||||
Anyway, the three fields 'frame', 'frame2', and 'lerpfrac' work together to provide animation blending. A lerpfrac of 1 means that only frame2 is used, while a lerpfrac of 0 means that only frame is used. A lerpfrac of 0.5 means that both are used with the result being 50% between them. If you don't do the lerpfrac thing and instead just set frame inside some think function, the animation will NOT be smooth due to the lack of auto interpolation.
|
||||
With the code above, I've made lerpfrac act as a stepping timer that keeps counting down from 1 to 0. when it drops below 0, a new frame is chosen and the old one is archived off in frame2 and lerpfrac is reset to about 1. This means that the frame choice after a frame switch is still mostly the old frame, blending into the new frame over time.
|
||||
Be warned that frametime is in local time rather than tied to server time, and thus the animation is purely clientside with no relation to server timings and is thus unsyncronised. If you want a more robust solution, you would need to syncronise based upon the time difference between time and some starttime value when the new action became valid.
|
||||
Sidenote: To animate within a framegroup, you will need to update self.frame1time and self.frame2time each frame. This is the absolute time into the animation. If you increment the value by something like: frametime*distance_traveled/animated_distance_traveled, it should be possible to achieve foot syncronisation with different movement speed settings.
|
||||
|
||||
|
||||
|
||||
|
||||
CSQC Entity Networking:
|
||||
CSQC entity networking is works with callbacks, and includes tolerance for packetloss and latency. Basically, the SSQC sets various SendFlags on the entity to say which parts of the entity have changed, and the server calls the SendEntity callback in order to build an update mssage at some point in the future. The callback is told the SendFlags that need to be sent.
|
||||
The redundantcy and vauge wording is where packetloss tolerance comes in. Basically, the server is responsible for tracking the flags in each message, and any which never arrived on the client must be resent. This can take a successful round-trip before dropped packets are noticed, hence the 'in the future', and why the callback is told the fields which got dropped.
|
||||
The network message itself is framed only by the entity number. The callback is responsible for stating exactly what sort of entity it is, and what is actually inside the message. This is typically achieved with a leading entitytype id byte.
|
||||
|
||||
In SSQC:
|
||||
float(entity to, float sendflags) MySendEntity =
|
||||
{
|
||||
WriteByte(MSG_ENTITY, 5); //write some entity type header
|
||||
WriteByte(MSG_ENTITY, sendflags); //tell the csqc what is actually present.
|
||||
if (sendflags & 1)
|
||||
{
|
||||
WriteCoord(MSG_ENTITY, self.origin_x);
|
||||
WriteCoord(MSG_ENTITY, self.origin_y);
|
||||
WriteCoord(MSG_ENTITY, self.origin_z);
|
||||
}
|
||||
if (sendflags & 2)
|
||||
WriteByte(MSG_ENTITY, self.frame);
|
||||
|
||||
if (sendflags & 128)
|
||||
WriteByte(MSG_ENTITY, self.modelindex); //sending a modelindex is smaller than sending an entire string.
|
||||
|
||||
return TRUE; //handled. If you return FALSE here, the entity will be considered invisible to the player.
|
||||
};
|
||||
|
||||
In your spawn function:
|
||||
self.SendEntity = MySendEntity;
|
||||
And when the origin changes:
|
||||
self.SendFlags |= 1;
|
||||
And when the frame changes:
|
||||
self.SendFlags |= 2;
|
||||
Note how the SendEntity function refers to sendflag 128, but its not set anywhere. Well, 'new' entities always have sendflags set to 0xfffff or something just large enough to avoid overflowing floats. Thus entities which have just been spawned or otherwise have just entered a player's pvs will always have sendflag bit 128 set, and thus the modelindex will be sent for such new entities without needing to explicitly state it.
|
||||
|
||||
From the CSQC side, the engine notices the entity update and then calls CSQC_Ent_Update. If the entity is new, it calls CSQC_Ent_Update beforehand.
|
||||
The CSQC must then read whatever header the ssqc wrote, determine what sort of entity it is, and determine what the meaning of the various fields are.
|
||||
Once the SSQC removes the entity or it just becomes invisible, the server will send a remove event, which the client will handle by calling CSQC_Ent_Remove. The CSQC is expected to then call remove(self) before returning.
|
||||
void(float isnew) CSQC_Ent_Update =
|
||||
{
|
||||
float enttype = readbyte():
|
||||
float flags;
|
||||
switch (enttype)
|
||||
{
|
||||
case 5:
|
||||
flags = readbyte();
|
||||
if (flags & 1)
|
||||
{
|
||||
self.origin_x = readcoord();
|
||||
self.origin_y = readcoord();
|
||||
self.origin_z = readcoord();
|
||||
setorigin(self, self.origin); //for correctness.
|
||||
}
|
||||
if (flags & 2)
|
||||
self.frame = readbyte();
|
||||
if (flags & 128)
|
||||
{
|
||||
setmodelindex(self, readbyte());
|
||||
self.drawmask = MASK_ENGINE;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
error("Unknown entity type! oh noes! panic!");
|
||||
}
|
||||
};
|
||||
void() CSQC_Ent_Remove =
|
||||
{
|
||||
remove(self);
|
||||
};
|
||||
As you add new types of entities, you'll need to add more cases. Ideally you'd just move much of that code into a function call.
|
||||
Also, you might want to combine the enttype byte and the flags byte, at least for entities which don't have many flags.
|
||||
If you keep your entity networking fairly generic, you can combine various projectiles into common classes, and doing so shouldn't really need more than 16 different classes, thus giving four flags for potentially less overhead. But that's optimisation talk and thus perhaps out of place here. Anyway, you get the idea.
|
||||
The above example is very simplistic, of course, as it only sets origin, frame, and model.
|
||||
There's a few things to look out for. As described in the previous section, there is no automatic interpolation or animation lerping, so you'll need to combine that with the predraw stuff. To interpolate positions, you'll have to keep track of the update time, old update time, new origin and old origin. lerping is then:
|
||||
local float frac = (time - self.lastupdate) / (self.lastupdate - self.prevupdate);
|
||||
frac = bound(0, frac, 1);
|
||||
self.origin = self.prevorigin + (self.lastorigin-self.prevorigin) * frac;
|
||||
You can of course make assumptions along the lines of an entity updating 10 times a second on the dot, which may or may not result in more robust interpolation.
|
||||
When interpolating angles, be sure to take care with wrapping from 360 down to 0.
|
||||
|
||||
|
||||
|
||||
|
||||
Prediction:
|
||||
In theory, prediction is easy. Simply keep an input frame log, track which input frame was the last one applied to movement data, and applay any unacknowledged input frames to the last state received using the same logic that the server will use.
|
||||
In practise, prediction is not reliable. It is a guess. A guess which is not particuarly compatible with traditional NQ player entity physics frames with their lack of syncronisation or even acknowledgements.
|
||||
Generally this means that you must implement ALL player physics code yourself, using tracebox. Alternatively you may be able to get away with using the engine's runstandardplayerphysics function instead, modifying the various input_* values slightly before calling it.
|
||||
|
||||
From the SSQC side, things are fairly simple. The server calls:
|
||||
void() SV_RunClientCommand =
|
||||
{
|
||||
input_angles_y += 180; //a fun mindfuck.
|
||||
runstandardplayerphysics(self); //and apply the input frame to the current entity state.
|
||||
}:
|
||||
|
||||
From the CSQC side, when you receive an entity state the servercommandframe global will say the input frame number that was last acknowledged by the server. Thus any player entity from the server will have this input frame already applied. While the clientcommandframe global is the input frame that is currently being generated.
|
||||
Thus if 'self' has its fields set to the most recently received state, you can apply all pending input frames with:
|
||||
for (if = servercommandframe+1; if <= clientcommandframe; if+=1)
|
||||
{
|
||||
if (!getinputstate(if))
|
||||
continue; //that input frame is too old.
|
||||
input_angles_y += 180; //a fun mindfuck.
|
||||
runstandardplayerphysics(self); //and apply the input frame to the current entity state.
|
||||
};
|
||||
The input frame that matches clientcommandframe is updated each frame, until it is finally sent at which point the global is bumped. Input frames will not be remembered forever, but will otherwise be static until they are forgotten.
|
||||
You can then set the VF_ORIGIN view property to match the predicted position. You will also likely want to smooth out steps, and perhaps add error correction too.
|
||||
|
||||
|
||||
|
||||
|
||||
Advanced Skeletal Animation (FTE_CSQC_SKELETONOBJECTS):
|
||||
When animating players and such, you may desire more than just frames.
|
||||
If you're using a skeletal model format (like IQM), then more complete bone control becomes available to you.
|
||||
When your entity is first spawned after you set the model, add some code like:
|
||||
self.skeletonindex = skel_create(self.modelindex);
|
||||
Each frame, update your .frame, .frame2, .lerpfrac, .frame1time, .frame2time accordingly and then call:
|
||||
skel_build(self.skeletonindex, self, self.modelindex, retainfrac, firstbone, lastbone, addfrac);
|
||||
That call needs some explaining. Especially the last four arguments.
|
||||
retainfrac is what fraction of the pose info to retain. At the start of each frame you should generally pass 0. Later calls will generally pass 1.
|
||||
addfrac is what fraction of the animation data to add. Note that this also serves as a sort of scaler. All skel_build calls for each bone in a single skeletal object should add up to 1 to avoid extra scaling.
|
||||
Using these two values you can blend multiple animations together, for instance allowing running animations to fade into a standing animation, or to blend both sideways and forwards running animations together at rates depending on direction+speed in order to achieve foot-sync.
|
||||
firstbone and lastbone provide a way for you to affect only a specific region of the model. Beware that this requires that your skeleton has bones that are carefully ordered.
|
||||
If firstbone is 0, it'll be corrected to 1. Bone 0 is not otherwise valid, as 0 refers to the entity position itself.
|
||||
If lastbone is 0, it'll be replaced with the total bone count.
|
||||
If ordered correctly, you can split the model by legs+torso by a bone at the base of the spine. With that achieved, you can find the spine bone with skel_find_bone(self.skeletonindex, "spine1") or so (depends what your model calls it), and then build the two parts of your animation with:
|
||||
float() playerpredraw =
|
||||
{
|
||||
//interpolate origin, and calc displacement
|
||||
//note: you probably want to replace this with prediction if you're working on the local player's entity.
|
||||
float frac = (time - self.lastupdate) / (self.lastupdate - self.prevupdate);
|
||||
frac = bound(0, frac, 1);
|
||||
vector newpos = self.prevorigin + (self.lastorigin-self.prevorigin) * frac;
|
||||
vector moved = newpos - self.origin; //this is how much we've moved since the last render frame, note that it implies frametime.
|
||||
self.origin = newpos;
|
||||
//determine animation weights
|
||||
makevectors([0, self.angles_y, 0]); //set v_forward,v_right vectors so we can do dot products to see how much of which direction the entity moved in.
|
||||
self.forwardweight += fabs(v_forward*moved) * 32; //scaler should rise to 1 after 0.1 secs when moving at 320qu
|
||||
self.sideweight += fabs(v_right*moved) * 32; //scaler should rise to 1 after 0.1 secs when moving at 320qu
|
||||
//clamp
|
||||
float sc = self.forwardweight + self.sideweight;
|
||||
if (sc > 1)
|
||||
sc = 1/sc; //greater than 1 would add negative legs...
|
||||
else if (moved_x || moved_y)
|
||||
sc = 1; //don't drop any movement weights if we're moving
|
||||
else
|
||||
sc = max(0, sc - frametime * 10); //drop down to 0 over 0.1 secs if we stopped moving.
|
||||
self.forwardweight *= sc;
|
||||
self.sideweight *= sc;
|
||||
self.lerpfrac = 0; //just in case...
|
||||
//add forward animation (retain frac = 0 to reset it)
|
||||
self.frame = $forwardanimation;
|
||||
self.frame1time = (self.forwardtime += frametime * (v_forward*moved));
|
||||
skel_build(self.skeletonindex, self, self.modelindex, 0, 0, spinebone, self.forwardweight);
|
||||
//add side animation (retain frac = 1 to not overwrite forward)
|
||||
self.frame = $sideanimation;
|
||||
self.frame1time = (self.sidetime += frametime * (v_right*moved));
|
||||
skel_build(self.skeletonindex, self, self.modelindex, 1, 0, spinebone, self.sideweight);
|
||||
//add idle animation (retain frac = 1 to not overwrite directions)
|
||||
self.frame = $standanimation;
|
||||
self.frame1time = (self.sidetime += frametime);
|
||||
skel_build(self.skeletonindex, self, self.modelindex, 1, 0, spinebone, 1-(self.forwardweight+self.sideweight));
|
||||
//add torso animation (retain frac = 0 to reset torso parts. note that this doesn't affect legs due to the bone ranges)
|
||||
self.frame = $torsoanimation;
|
||||
self.frame1time = time - torsostarttime;
|
||||
skel_build(self.skeletonindex, self, self.modelindex, 0, spinebone, 0, 1);
|
||||
return FALSE;
|
||||
}
|
||||
and in your spawn code:
|
||||
setmodel(self, "progs/skelplayer.iqm");
|
||||
self.skeletonindex = skel_create(self.modelindex);
|
||||
self.predraw = playerpredraw;
|
||||
self.drawmask = MASK_ENGINE;
|
||||
and when its killed:
|
||||
skel_delete(self.skeletonindex);
|
||||
Then when your entity is added to the scene (via addentities), the predraw function is called to interpolate your entity's origin and update its skeltal object.
|
||||
You'll have to ensure your parse code updates lastupdate and prevupdate timestamps, along with lastorigin and prevorigin, that it updates the angles.
|
||||
$forwardanimation etc frame macros will need to be set to the correct framegroups within the model (note that this code requires framegroups, and would become more complex if you wished to animate between descrete frames for each animation, but that doing so is possible by using lerpfrac and frame2 instead of frame1time).
|
||||
The code does not interpolate angles, which is something you'll probably want to add, as well as add a concept of actions so that you can blend between idle+shoot animations as appropriate.
|
||||
|
||||
|
||||
|
||||
|
||||
Ragdoll (FTE_QC_RAGDOLL / FTE_QC_RAGDOLL_WIP):
|
||||
Ragdoll support is dependant upon ODE, which requires cvar("physics_ode_enable") to be set in order to work properly.
|
||||
Once you have a skeletal object, you can add in ragdoll by calling skel_ragupdate(self, "doll foo.doll", 0); at the end of your predraw function (which will cause the ragdoll to update and move based upon an animated model, and then call skel_ragupdate(self, "animate 0", 0); once your entity dies and goes limp (typically, this is done part-way through the death animation, so the ragdoll inherits a certain amount of velocity from said animation).
|
||||
The .doll file describes the bodies and joints within the model. If a body is set to animate, it will use the bone data your code specifies. If a body is not set to animate, it will be ragdolled even when the player is still alive. This can be used for things like ponytails, cloth, etc.
|
||||
237
games-util/fteqcc/additional-files/entry_qc.txt
Normal file
237
games-util/fteqcc/additional-files/entry_qc.txt
Normal file
@@ -0,0 +1,237 @@
|
||||
QuakeC Entry Functions
|
||||
==================================
|
||||
|
||||
QuakeC can output 3 different types of modules.
|
||||
A client-side module, a menu module and a server-side module.
|
||||
|
||||
The server-side module can talk to the client-side module and vice versa.
|
||||
However, the menu module is, as of yet, only a stripped down, omni-present
|
||||
variant of the client-side module.
|
||||
|
||||
CSQC = client-side module
|
||||
MenuQC = menu module
|
||||
SSQC = server-side module
|
||||
|
||||
All of them have different entry functions.
|
||||
They're like callbacks from the engine for whenever something happens.
|
||||
|
||||
SSQC SPECIFIC ENTRY FUNCTIONS
|
||||
=============================
|
||||
|
||||
Standard Quake (minimum, required!):
|
||||
------------------------------------
|
||||
This is unused and will never be called from the engine in standard Quake.
|
||||
void main( void )
|
||||
|
||||
Called once every frame, before all the entities ran.
|
||||
`self` is not valid:
|
||||
void StartFrame ( void )
|
||||
|
||||
Run before player physics.
|
||||
`self` is one of the clients on the server:
|
||||
void PlayerPreThink ( void )
|
||||
|
||||
Run after player physics.
|
||||
`self` is one of the clients on the server:
|
||||
void PlayerPostThink( void )
|
||||
|
||||
'kill' command issued.
|
||||
`self` in this function, is the client in question:
|
||||
void ClientKill( void )
|
||||
|
||||
Handle player connection message.
|
||||
`self` in this function, is the client in question:
|
||||
void ClientConnect( void )
|
||||
|
||||
Handle player spawning/setup.
|
||||
`self` in this function, is the client in question:
|
||||
void PutClientInServer( void )
|
||||
|
||||
Handle player parting message.
|
||||
`self` in this function, is the client in question:
|
||||
void ClientDisconnect( void )
|
||||
|
||||
Singleplayer stuff. Sets the parmX `globals` for each player.
|
||||
Take a look at what Quake does and you'll hopefully understand.
|
||||
It's quite limited and `self` in these functions is the client in question:
|
||||
void SetNewParms( void )
|
||||
void SetChangeParms( void )
|
||||
|
||||
Noteworthy QuakeWorld / FTE QuakeWorld additions:
|
||||
-------------------------------------------------
|
||||
Called when a spectator joins the game.
|
||||
`self` in this function, is the spectator in question:
|
||||
void SpectatorConnect( void )
|
||||
|
||||
Called when a spectator disconnects from the game.
|
||||
`self` in this function, is the spectator in question:
|
||||
void SpectatorDisconnect( void )
|
||||
|
||||
Called each frame for each spectator.
|
||||
`self` in this function, is the spectator in question:
|
||||
void SpectatorThink( void )
|
||||
|
||||
Provides QC with a way to intercept 'cmd foo' commands from the client.
|
||||
Very handy. Self will be set to the sending client, while the 'cmd'
|
||||
argument can be tokenize()d and each element retrieved via argv(argno).
|
||||
Unrecognised cmds MUST be passed on to the clientcommand builtin.
|
||||
`self` in this function, is the client in question:
|
||||
void SV_ParseClientCommand( string strCmd )
|
||||
|
||||
For each frame that the server is paused, this function will
|
||||
be called to give the gamecode a chance to unpause the server
|
||||
again. the flPaused argument says how long the server has
|
||||
been paused for (the time global is frozen and will not increment
|
||||
while paused).
|
||||
`self` is not valid:
|
||||
void SV_PausedTic( float flPaused )
|
||||
|
||||
Called to give the qc a change to block pause/unpause requests.
|
||||
Return false for the pause request to be ignored. newstatus is 1
|
||||
if the user is trying to pause the game. For the duration of the
|
||||
call, self will be set to the player who tried to pause, or to
|
||||
world if it was triggered by a server-side event.
|
||||
`self` is not valid:
|
||||
float SV_ShouldPause( float flNewStatus ) { return FALSE; }
|
||||
|
||||
Called each time a player movement packet was received from a client.
|
||||
`self` is set to the player entity which should be updated, while the input_*
|
||||
globals specify the various properties stored within the input packet.
|
||||
The contents of this function should be somewaht identical to the equivalent
|
||||
function in CSQC, or prediction misses will occur. If you're feeling lazy,
|
||||
you can simply call 'runstandardplayerphysics' after modifying the inputs.
|
||||
void SV_RunClientCommand ( void )
|
||||
|
||||
Called after non-player entities have been run at the end of
|
||||
the physics frame. Player physics is performed out of order
|
||||
and can/will still occur between EndFrame and BeginFrame.
|
||||
`self` is not valid:
|
||||
void EndFrame ( void )
|
||||
|
||||
Called whenever a new entity is set to be spawned by the SSQC,
|
||||
Override this function if you want to prevent the engine
|
||||
from filtering entities by skill/deathmatch - which it will
|
||||
absolutely do otherwise!
|
||||
void CheckSpawn( void() spawnfunc )
|
||||
|
||||
Gives you the opportunity to handle loading of save-files.
|
||||
'fh' is the filehandle which is already opened. You only need
|
||||
to parse it with fgets(fh) while it returns a valid result.
|
||||
I suggest you remove all existing entities beforehand manually
|
||||
before spawning new ones.
|
||||
void SV_PerformLoad( float fh, float entcount, float playerslots )
|
||||
|
||||
Gives you the opportunity to save the game. Just like PerformLoad,
|
||||
the filehandle is already open for you.
|
||||
void SV_PerformSave( float fh, float entcount, float playerslots )
|
||||
|
||||
This is also valid for CSQC:
|
||||
This function is called when the progs is confirmed to be officially loaded.
|
||||
Beware however that entities are not ready to spawn just yet.
|
||||
You can however insert data into globals or other data structures,
|
||||
which can be respected by all entities later.
|
||||
void init ( void )
|
||||
|
||||
This is also valid for CSQC:
|
||||
This is the equivalent of init(), but at a point where entity slots are
|
||||
available to be allocated to the server side progs. If you need to
|
||||
call spawn() on any entity, do it no earlier than here.
|
||||
void initents ( void )
|
||||
|
||||
CSQC SPECIFIC ENTRY FUNCTIONS
|
||||
=============================
|
||||
None of the CSQC entry functions are mandatory, you use them depending on your
|
||||
use case. However I'll try to prop up the important ones for the majority of
|
||||
programmers and those lesser used (The Wastes uses ALL of them).
|
||||
|
||||
Essentials:
|
||||
-----------
|
||||
Called once at the initialization of the CSQC module, which happens upon joining
|
||||
a server.
|
||||
void CSQC_Init ( float flAPI, string strBuild, float flVersion )
|
||||
|
||||
Happens when a current game session running the CSQC module is shutting down or
|
||||
the client disconnects.
|
||||
void CSQC_Shutdown ( void )
|
||||
|
||||
Run every frame. This calls the engine to render the 3D scene and to draw
|
||||
and overlay if desired. That overlay could be a HUD, it could be another scene
|
||||
as well. There are no limits as to what and how many of such things you can and
|
||||
will display.
|
||||
void CSQC_UpdateView ( float flWidth, float flHeight, float fNotMenu )
|
||||
|
||||
Whenever an input device is pressed, the engine calls this function to let the
|
||||
CSQC know. You will then have to filter out what input events you support and
|
||||
how they interact with your game-logic.
|
||||
For example, flEV could be IE_KEYDOWN and flKey could be K_ESCAPE.
|
||||
This means the Escape key on your keyboard was being held down.
|
||||
The this function returns TRUE, then the engine won't handle the input
|
||||
event internally any further until the next.
|
||||
float CSQC_InputEvent ( float flEV, float flKey, float flChar, float fDevID )
|
||||
|
||||
Pretty good ones:
|
||||
-----------------
|
||||
Called whenever the video mode has changed, or vid_reload has been called.
|
||||
If you define custom shaders, you want those initialized in here, otherwise
|
||||
they'll get wiped after clients' change their video mode:
|
||||
void CSQC_RendererRestarted ( string strRenderer )
|
||||
|
||||
Called whenever the client enters a console command. This is the first place
|
||||
where it will get checked. If this function returns FALSE, then the engine will
|
||||
attempt to interpret the command (unless a MenuQC module is present):
|
||||
float CSQC_ConsoleCommand ( string strCommand )
|
||||
|
||||
This is where all WriteByte() SVC_GAMEEVENT network packets arrive.
|
||||
That way the Server-Side QuakeC can send reliable/unreliable packets to all
|
||||
or individual players manually.
|
||||
void CSQC_Parse_Event ( void )
|
||||
|
||||
This is where you have the chance to alter the input_* globals before the client
|
||||
sends them off to the server, where they'd arrive inside SSQC's entry function
|
||||
SV_RunClientCommand if present:
|
||||
void CSQC_Input_Frame ( void )
|
||||
|
||||
Called when the world has finished loading on the client.
|
||||
void CSQC_WorldLoaded ( void )
|
||||
|
||||
This is where you handle all the entity packets that the .SendEntity field sends
|
||||
out to the individual players:
|
||||
void CSQC_Ent_Update ( float flNew )
|
||||
|
||||
MENUQC SPECIFIC ENTRY FUNCTIONS
|
||||
===============================
|
||||
|
||||
void() m_init;
|
||||
void() m_shutdown;
|
||||
|
||||
Provides the menuqc with a chance to draw. Will be called even if the menu does
|
||||
not have focus, so be sure to avoid that.
|
||||
WARNING: vecScreenSize is not provided in DP.
|
||||
void m_draw (vector vecScreenSize)
|
||||
|
||||
Additional drawing function to draw loading screens. If flOpaque is set, then
|
||||
this function must ensure that the entire screen is overdrawn (even if just by
|
||||
a black drawfill):
|
||||
void m_drawloading (vector vecScreenSize, float flOpaque)
|
||||
|
||||
Called whenever a key is pressed, the mouse is moved, etc. evtype will be one
|
||||
of the IE_* constants. The other arguments vary depending on the evtype.
|
||||
Key presses are not guarenteed to have both scan and unichar values set at the
|
||||
same time. This mirrors the CSQC version of the input function. FTE ONLY:
|
||||
float Menu_InputEvent ( float evtype, float scanx, float chary, float devid )
|
||||
|
||||
Legacy/Darkplaces KeyUp/KeyDown functions, mirroring Quake 1/2's internal
|
||||
menu code:
|
||||
void m_keydown (float scan, float chr)
|
||||
void m_keyup (float scan, float chr)
|
||||
|
||||
This is a bit of an annoyance, this is meant to be called whenever `togglemenu`
|
||||
is called. Which only works when the focus is on the game.
|
||||
So it doesn't always toggle in all situations.
|
||||
void m_toggle (float flWantmode)
|
||||
|
||||
Basically what happens when CSQC_ConsoleCommand returns FALSE or MenuQC is in
|
||||
focus first. If this method returns FALSE then the engine will attempt to
|
||||
interpret the command, unless CSQC_ConsoleCommand returns TRUE and the game
|
||||
is not in focus. Basically just store your damn menu specific commands here:
|
||||
float m_consolecommand ( string strCommand )
|
||||
@@ -0,0 +1,55 @@
|
||||
How/Why to avoid findchain()
|
||||
============================
|
||||
By Marco Hladik
|
||||
Last updated: 8th of August 2018
|
||||
|
||||
If you're a QuakeC progs programmer you will have come across bits of code,
|
||||
similar to the one here:
|
||||
|
||||
// Cycle through all the player entities and set their health to 100
|
||||
entity b = findchain( classname, "player" );
|
||||
while ( b ) {
|
||||
b.health = 100;
|
||||
b = b.chain;
|
||||
}
|
||||
|
||||
.chain is a field used by every entity in the stock Quake progs code.
|
||||
It's a linked list created by whenever you call findchain() - it might seem
|
||||
harmless to you, but it's generally a bad practice using it.
|
||||
|
||||
For example, you cannot call findchain() inside an ongoing chain loop.
|
||||
It also conflicts with findradius(), which shared the same field .chain as well
|
||||
to point to the next entity in the linked list.
|
||||
Those entities are most likely going to be related to players.
|
||||
So they'll override the existing entity reference from the loop that hasn't
|
||||
finished. It'll result in uncontrollable garbage.
|
||||
|
||||
So you can never combine findchain() with findradius(). Ever.
|
||||
|
||||
However, there are ways to avoid using findchain() AND findradius() altogether,
|
||||
by just using find(). This is possible because find()'s first parameter
|
||||
tells the builtin which entity to start from in the entity list.
|
||||
This is quite clever, as it allows us to simply route the output of the last
|
||||
result into a new find() call - thus jumping from one entity to the next.
|
||||
|
||||
// Does the same thing as the above code, while avoiding .chain
|
||||
for ( entity b = world; ( b = find( b, classname, "player" ) ); ) {
|
||||
b.health = 100;
|
||||
}
|
||||
|
||||
Now, you can also replace findradius() by using the for loop above, while
|
||||
manually checking whether or not the entities are within a specified radius.
|
||||
|
||||
// Set the health of everyone to 100, 500 units around `somevector`
|
||||
for ( entity b = world; ( b = find( b, classname, "player" ) ); ) {
|
||||
if ( vlen( b.origin - somevector ) < 500 ) {
|
||||
b.health = 100;
|
||||
}
|
||||
}
|
||||
|
||||
findchainfloat(), which was a Darkplaces addition, has the same problems.
|
||||
If your loops don't work right, it might be a chain conflict.
|
||||
These happen more often you think!
|
||||
Someone might call a function inside a loop that contains a findradius() call
|
||||
or a findchain()/findchainfloat() one. It'll screw things up.
|
||||
Don't risk it! Say no to these :)
|
||||
1912
games-util/fteqcc/additional-files/fteqcc_manual.txt
Normal file
1912
games-util/fteqcc/additional-files/fteqcc_manual.txt
Normal file
File diff suppressed because it is too large
Load Diff
100
games-util/fteqcc/additional-files/sendevent.txt
Normal file
100
games-util/fteqcc/additional-files/sendevent.txt
Normal file
@@ -0,0 +1,100 @@
|
||||
SENDEVENT - EVERYTHING ONE NEEDS TO KNOW!
|
||||
=========================================
|
||||
By Marco Hladik
|
||||
Last updated: 10th of August 2018
|
||||
|
||||
sendevent() is the function you call from the CSQC to communicate, reliably, to
|
||||
the SSQC on the server.
|
||||
|
||||
USAGE
|
||||
=====
|
||||
|
||||
The first parameter of sendevent() specifies the function to call in SSQC:
|
||||
|
||||
sendevent( "Myfunction", "" );
|
||||
|
||||
However, it will not look for "void() Myfunction" on the SSQC, there is a prefix
|
||||
reserved for all sendevent calls. That one is `CSEv".
|
||||
So in reality, the above command will execute this on the SSQC:
|
||||
|
||||
void CSEv_Myfunction ( void ) { }
|
||||
|
||||
Now, what about that second parameter?
|
||||
|
||||
.. , "" );
|
||||
|
||||
Well, sendevent has the ability to send data. 6 arguments can be passed, max.
|
||||
That second parameter string specifies the type of data you want to send.
|
||||
|
||||
For example, you want to ask the server to set the health of a player to a
|
||||
specific value:
|
||||
|
||||
CSQC:
|
||||
sendevent( "SetPlayerHealth", "f", 124 );
|
||||
|
||||
SSQC:
|
||||
void CSEv_SetPlayerHealth_f ( float h ) {
|
||||
...
|
||||
}
|
||||
|
||||
As you can see, the `f` indicates the type float.
|
||||
`s` would indicate a string.
|
||||
`v` would indicate a vector.
|
||||
`i` would indicate an integer.
|
||||
'e' would indicate an entity (more about that in the last chapter).
|
||||
|
||||
The second parameter specifying the arguments will append to the name of the
|
||||
function you're trying to call.
|
||||
It sadly does not yet support structs.
|
||||
|
||||
CSQC:
|
||||
sendevent( "SetColorAndName", "fffs", 255, 0, 128, "Tommy" );
|
||||
|
||||
SSQC:
|
||||
void CSEv_SetColorAndName_fffs ( float r, float g, float b, string name ) {
|
||||
...
|
||||
}
|
||||
|
||||
..is what a longer sendevent with multiple arguments would look like.
|
||||
6 arguments are the maximum. This is because QuakeC supports 8 arguments max per
|
||||
function call. If you, however, only want to send floats and require more than
|
||||
6 arguments, you can store them inside vectors:
|
||||
|
||||
CSQC:
|
||||
vector v1 = [ myfloat1, myfloat2, myfloat3 ];
|
||||
vector v2 = [ myfloat4, myfloat5, myfloat6 ];
|
||||
vector v3 = [ myfloat7, myfloat8, myfloat9 ];
|
||||
vector v4 = [ myfloat10, myfloat11, myfloat12 ];
|
||||
vector v5 = [ myfloat13, myfloat14, myfloat15 ];
|
||||
vector v6 = [ myfloat16, myfloat17, myfloat18 ];
|
||||
sendevent( "Bah", "vvvvvv", v1, v2, v3, v4, v5, v6 );
|
||||
|
||||
SSQC:
|
||||
void CSEv_Bah_vvvvvv ( vector v1, vector v2, vector v3, vector v4, vector v5,
|
||||
vector v6 ) {
|
||||
myfloat1 = v1[0];
|
||||
myfloat2 = v1[1];
|
||||
myfloat3 = v1[2];
|
||||
myfloat4 = v2[0];
|
||||
...
|
||||
}
|
||||
|
||||
NOTE ABOUT SENDEVENT AND ENTITIES
|
||||
=================================
|
||||
|
||||
If you pass an entity via sendevent, it'll in reality send only the entnum.
|
||||
The entnum is the essentially the entity-id.
|
||||
If the entity does not exist via the SSQC (like, it has been removed since or
|
||||
is a client-side only entity) then the entity parameter on the SSQC function
|
||||
call will return `world` aka 0/__NULL__.
|
||||
|
||||
For protective reasons, entities that are removed have their entnums reserved
|
||||
for a specific amount of time before being able to be used again.
|
||||
Player entities, for example, will allow their entnums to be recycled after 2
|
||||
seconds. Any other entity its entnum will be reusable after only half a second.
|
||||
This should avoid most entnum conflicts.
|
||||
|
||||
Basically, during this time, the CSEV_* will not return `world`.
|
||||
|
||||
HOWEVER: This is entirely unreliable behaviour.
|
||||
Check if the .classname is VALID on the SSQC before doing anything fancy on it.
|
||||
73
games-util/fteqcc/fteqcc-20230920~git.recipe
Normal file
73
games-util/fteqcc/fteqcc-20230920~git.recipe
Normal file
@@ -0,0 +1,73 @@
|
||||
SUMMARY="Next-generation QuakeC compiler"
|
||||
DESCRIPTION="FTEQCC is a sophisicated QuakeC compiler designed to enhance the experience of \
|
||||
writing QuakeC.
|
||||
|
||||
Features:
|
||||
- Vanilla QC and C/C++-like syntax are supported.
|
||||
- Objected-oriented features such as classes, inheritance.
|
||||
- Debugging features and integration with FTEQW.
|
||||
- Arrays and array-emulation for engine targets that don't support it.
|
||||
- Support for typedefs, enum, enumflags (bitfields), structs, unions.
|
||||
- Support for pointers.
|
||||
- Real integer type support.
|
||||
|
||||
... and so much more"
|
||||
HOMEPAGE="https://www.fteqcc.org/"
|
||||
COPYRIGHT="2002-2023 Forethought Entertainment"
|
||||
LICENSE="GNU GPL v2"
|
||||
REVISION="1"
|
||||
srcGitRev="eb6b127d9ce10949f00bcad8c5953b7092c92d12"
|
||||
SOURCE_URI="https://github.com/fte-team/fteqw/archive/$srcGitRev.tar.gz"
|
||||
CHECKSUM_SHA256="8d19bf1ca4816d0fb35fda7401ef28b7ca02391ca14ca10843f22d64845d1614"
|
||||
SOURCE_DIR="fteqw-$srcGitRev"
|
||||
ADDITIONAL_FILES="bitflags.txt
|
||||
classes.txt
|
||||
csqc_for_idiots.txt
|
||||
entry_qc.txt
|
||||
findchain_considered_harmful.txt
|
||||
fteqcc_manual.txt
|
||||
sendevent.txt"
|
||||
|
||||
ARCHITECTURES="all !x86_gcc2"
|
||||
SECONDARY_ARCHITECTURES="x86"
|
||||
|
||||
PROVIDES="
|
||||
fteqcc$secondaryArchSuffix = $portVersion
|
||||
cmd:fteqcc$secondaryArchSuffix = $portVersion
|
||||
"
|
||||
REQUIRES="
|
||||
haiku$secondaryArchSuffix
|
||||
lib:libz$secondaryArchSuffix
|
||||
"
|
||||
|
||||
BUILD_REQUIRES="
|
||||
haiku${secondaryArchSuffix}_devel
|
||||
devel:libz$secondaryArchSuffix
|
||||
"
|
||||
BUILD_PREREQUIRES="
|
||||
cmd:gcc$secondaryArchSuffix
|
||||
cmd:ld$secondaryArchSuffix
|
||||
cmd:pkg_config$secondaryArchSuffix
|
||||
cmd:make
|
||||
"
|
||||
|
||||
BUILD()
|
||||
{
|
||||
cd $sourceDir/engine/
|
||||
make $jobArgs qcc-rel PKGCONFIG=pkg-config BASELDFLAGS="-lpthread" -f Makefile
|
||||
}
|
||||
|
||||
INSTALL()
|
||||
{
|
||||
mkdir -p $docDir
|
||||
cp $portDir/additional-files/bitflags.txt $docDir/bitflags.txt
|
||||
cp $portDir/additional-files/classes.txt $docDir/classes.txt
|
||||
cp $portDir/additional-files/csqc_for_idiots.txt $docDir/csqc_for_idiots.txt
|
||||
cp $portDir/additional-files/entry_qc.txt $docDir/entry_qc.txt
|
||||
cp $portDir/additional-files/findchain_considered_harmful.txt $docDir/findchain_considered_harmful.txt
|
||||
cp $portDir/additional-files/fteqcc_manual.txt $docDir/fteqcc_manual.txt
|
||||
cp $portDir/additional-files/sendevent.txt $docDir/sendevent.txt
|
||||
|
||||
mkdir -p $binDir
|
||||
cp -r $sourceDir/engine/release/fteqcc $binDir/
|
||||
}
|
||||
Reference in New Issue
Block a user