Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement qextensions #119

Closed
wants to merge 13 commits into from
Closed

Implement qextensions #119

wants to merge 13 commits into from

Conversation

g0mb4
Copy link

@g0mb4 g0mb4 commented Jan 16, 2025

Quake Extensions

1. About

Quake Extensions (qextensions) are Dynamic Link Library (DLL) files on Windows and Shared Object (SO) files on Linux. They allows external code to be executed within QC programs.

In theory it works on macOS using the Dynamic Librariy (dylib), but I do not have a Mac to test with.

2. Usage

The extensions cvar must be set to a non-zero value to enable qextensions. If it is set through the command line +extensions 1 must come before +map.

In order to use qextensions the new built-in functions must be defined.
This usually happens in defs.qc:

...
float(string path) OpenExtension = #93;
string(float id, string cmd, string arg) CallExtensionString = #94;
float(float id, string cmd, float arg) CallExtensionNumber = #95;
vector(float id, string cmd, vector arg) CallExtensionVector = #96;
float(float id, string cmd, entity arg) CallExtensionEntity = #97;
...

To use a specific extension e.g. hello, the following steps needs to be made:

1, Open/load the extension:

This needs to be done only once. The void() worldspawn is a perfect place to do that:

    ....
    float hello_ext;
    ...
    void() worldspawn = 
    {
        ...
        hello_ext = OpenExtension("hello");
        ...
    }
    ...

Note that the value returned from OpenExtension is the extension descriptor. It needs to be saved globally, since it will be used for calling the extension.

2, Call the extension functions:

    ...
    num = CallExtensionNumber(hello_ext, "hello", num);
    ...

This call is blocking, meaning that the game will wait for the extension to return before continuing. This may cause FPS drop if extension is not optimised. If extension takes too long, consider making asynchronous extension, where the result of the work of the extension is collected in a separate call.

There are 4 function that can be called:

    string(float id, string cmd, string arg) CallExtensionString;
    float(float id, string cmd, float arg) CallExtensionNumber;
    vector(float id, string cmd, vector arg) CallExtensionVector;
    float(float id, string cmd, entity arg) CallExtensionEntity;

The functions can not modify their arguments. More then one qextension can be used simultaneously.

3. Creating

qextensions can be created using any programming language that can output native .DLL/.SO files. This example code is written in C.

The qextension must export exactly 5 functions:

    int qextension_version (void)
    const char* qextension_string (const char *cmd, const char *arg)
    float qextension_number (const char *cmd, float arg)
    float* qextension_vector (const char *cmd, vec3_t arg)
    float qextension_entity (const char *cmd, edict_t *arg)

In Windows the function must be preceded with __declspec (dllexport) if MSVC is used.

The qextension_version must return 1. It is the version of the qextension system, not the specific extension.

Since the qextension is a native file, different platforms require different library files. OpenExtension("hello") will try to find the underlying library file depending on the platform quakespasm is running on:

  • hello_x86.dll on 32 bit Windows systems

  • hello_x86_64.dll on 64 bit Windows systems

  • hello_x86.so on 32 bit Linux systems

  • hello_x86_64.so on 64 bit Linux systems

If support of multiple platforms is required, each library file must be installed. This requires cross-compilation or different build systems.

4. Installation

qextensions use the filesystem of Quake. They can be placed into the game directory or embedded into a .PAK file, e.g.:

  • id1/hello_x86.dll -> OpenExtension("hello");

  • id1/pak0.pak/hello_x86_64.dll -> OpenExtension("hello");

  • id1/ext/hello_x86.dll -> OpenExtension("ext/hello");

  • mymod/hello_x86_64.so -> OpenExtension("hello"); (only if -game mymod was used)

5. qext_hello

This is an example qextension. It supports 64 bit Linux and Windows systems.

  • src/ contains the source code of the qextension
  • qsrc/ contains the source code of the mod uses the qextension

5.1. Building

qcc is required to be in the $PATH/%PATH% in order to compile progs.dat.

5.2. Linux

gcc is required to compile the qextension. Simply type:

make

A Win64 version also can be compiled, it requires the gcc-mingw-w64 toolchain.
Remove the following line from Makefile, if it is not present in your system:

cd src; make -f Makefile.w64

5.3. Windows

Open an x64 Native Tools Command Prompt for VS X ant type:

build.bat

5.4. Running

To run the hello qextension copy the generated hello folder to the folder of quakespasm (where id1 lives) then type:

quakespasm --args -game hello +extensions 1 +developer 1 +map start

If you see the outside of the start map the extension is working. Open the console and you should see:

world of Quake
11
'    2.0    3.0    4.0'
1

@sezero
Copy link
Owner

sezero commented Jan 16, 2025

CC @ericwa, @andrei-drexler, @Shpoike

@alexey-lysiuk
Copy link
Contributor

Taking security considerations into account, it would be nice to have a way to disable this feature.

@g0mb4
Copy link
Author

g0mb4 commented Jan 16, 2025

Sure, good idea.

g0mb4 added 2 commits January 16, 2025 17:11
This is 0 by default and prevents opening qextensions.
@vsonnier
Copy link
Contributor

vsonnier commented Jan 16, 2025

Maybe put the present dlopen-like code hidden away in pl_linux/win.c ?

@g0mb4
Copy link
Author

g0mb4 commented Jan 16, 2025

Dear @vsonnier! I like the idea. I did not know about the SDL functions, but they allow more portable code as I see. If I drop the mechanism to map a memory region as a file I can merge the platform-specific code. I'll experiment a bit.

@vsonnier
Copy link
Contributor

vsonnier commented Jan 16, 2025

The problem with SDL_LoadObject is that it won't be able to load from a Quake .pak file, is it a problem ?

Small detail remark : Replace the super-generic extension CVAR and naming around the dynamic QC functionality with something more explicit, idk pr_dynamic_extension

For instance, we have pr_checkextension 0/1 in vkQuake/QSS to enable or disable support to FTE extensions, where pr_ stands for "program" to refer to QC logic.

@g0mb4
Copy link
Author

g0mb4 commented Jan 16, 2025

I'd like to keep the ability to load from .pak files, I think it is neat.

Thank you for the suggestions, I tried to follow the style of the code, but yep, the names are a bit generic.

FTE extensions? Am I reimplementig the wheel?

@vsonnier
Copy link
Contributor

vsonnier commented Jan 16, 2025

FTE extensions? Am I reimplementig the wheel?

Think of it at an additional extended library of more extensions, but still hard-coded in the regular C source :

https://github.com/Novum/vkQuake/blob/ea6d5eb86e5186b622f3ddf909ba57233a79430f/Quake/pr_ext.c#L4999

In mods like Arcane Dimensions, QC checks dynamically for the existence of a particular extension "category", listed here :
https://github.com/Novum/vkQuake/blob/ea6d5eb86e5186b622f3ddf909ba57233a79430f/Quake/pr_ext.c#L5278

Ex, in .qc Aracane Dimensions pesudo-code:

if (checkextension("FTE_PART_SCRIPT") {
  // call qc FTE api to have FTE particules effects and such.
} else {
   // use classic Quake particles...
}

So for ports supporting FTE particles and such, Arcane Dimensions effectivelly show sophisticated particles effects, and classic particles on other ports.

With your improvement, it goes beyond that and new APIs are part of the loaded native library + compagnon .qc code, not the executable so it adds flexibility, unless I'm mistaken ?

I suppose with additionnal effort one mod may implement 2 variants of code like Arcane Dimensions did to be compatible with non-supporting ports, but that seems a lot of work.

@sezero
Copy link
Owner

sezero commented Jan 16, 2025

I'd like to hear others' opinions, but yes, this does feel like re-inventing the wheel.

Close this? Any ideas to pick from this?

@g0mb4
Copy link
Author

g0mb4 commented Jan 16, 2025

Thank you for the detailed explanation!

Yes, the "logic" moves to the dynamic library, where the QC calls only provides input.

Back in the day I implemented some basic websocket support for Arma3 this way, this is where I got the idea.

@vsonnier
Copy link
Contributor

vsonnier commented Jan 16, 2025

In any case, this solution has the same drawbacks as Quake 2 / Quake 3 mods : we need to ship native platform-specific binaries.

The advantage of .qc bytecode is that it is universal and timeless. Once considered a drawback because "slow" leading Carmack to switch to logic implemented in native code with Q2/Q3, this is now a huge advantage I think.

Nowadays we have Q2 mods that can't be run with Q2 Remaster for this very reason, while all Quake 1 creations can be run on any port... (except if using specific extensions, leading to workarounds as above :)

@vsonnier
Copy link
Contributor

vsonnier commented Jan 16, 2025

I assume that such native extensions can't be Network-aware, (similar to Client-Side QC drawing HUDs and other things) otherwise we'll need to implement protocol extensions on top of that work, like FTE protocol did.

Of course, since it is native code we can do anything including side-stepping Quake clients communication by creating IP side channels, here protocol means the official Quake communication logic : https://quakewiki.org/wiki/Network_Protocols

Note that Quake protocol is also locally used in a single-player Quake client as a stream of commands that drive the engine itself.

@vsonnier
Copy link
Contributor

Advantages of this solution, IMHO :

  • Offloading calculation-heavy operations to native code,
  • Coding in a familliar language.
  • Easier to debug code using usual native IDE and tools ?
  • others ?

@Shpoike
Copy link

Shpoike commented Jan 17, 2025 via email

@vsonnier
Copy link
Contributor

vsonnier commented Jan 17, 2025

Right, I've looked a little more closely and wondered how you could load a function with an arbitrary signature, for universality...

Well there is no miracle and you have to know the signatures biforehand to import the symbols. So if I read it correctly, the only functions your current API implementd are the following you listed:

float(string path) OpenExtension = #93;
string(float id, string cmd, string arg) CallExtensionString = #94;
float(float id, string cmd, float arg) CallExtensionNumber = #95;
vector(float id, string cmd, vector arg) CallExtensionVector = #96;
float(float id, string cmd, entity arg) CallExtensionEntity = #97;

Well in this case this is very limited indeed, and if you later grow the amount of signatures I'm afraid @sezero is right :

This is reinventing the wheel and it should be much simpler to add existing QC extensions like FTE and such. That would be equivalent to merge QSS into QuakeSpasm, so why not use QSS directlty ?

Unless we manage to call arbitrary/variadic signature from QC and back, I don't see how it could work.

@g0mb4
Copy link
Author

g0mb4 commented Jan 17, 2025

It seems like the community has decided, thank you for your time, though.

@sezero
Copy link
Owner

sezero commented Jan 17, 2025

OK, closing. Thank you.

@sezero sezero closed this Jan 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants