Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions codemp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ if(BuildMPEngine OR BuildMPDed)
"${MPDir}/qcommon/cvar.cpp"
"${MPDir}/qcommon/disablewarnings.h"
"${MPDir}/qcommon/files.cpp"
"${MPDir}/qcommon/g2_handle_mapper.h"
"${MPDir}/qcommon/g2_handle_mapper.cpp"
"${MPDir}/qcommon/game_version.h"
"${MPDir}/qcommon/GenericParser2.cpp"
"${MPDir}/qcommon/GenericParser2.h"
Expand Down
198 changes: 76 additions & 122 deletions codemp/client/cl_cgameapi.cpp

Large diffs are not rendered by default.

171 changes: 61 additions & 110 deletions codemp/client/cl_uiapi.cpp

Large diffs are not rendered by default.

65 changes: 41 additions & 24 deletions codemp/game/g_public.h
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,11 @@ typedef struct parms_s {
typedef struct Vehicle_s Vehicle_t;
#endif

// In the QVM, the Vehicle_s memory layout differs, because it contains pointers.
// We don't currently actually access any of its fields, so the type is a stub for now,
// to be properly defined if and when somebody needs it.
typedef struct Vehicle_qvm_s Vehicle_qvm_t;

// the server looks at a sharedEntity, which is the start of the game's gentity_t structure
//mod authors should not touch this struct
typedef struct sharedEntity_s {
Expand Down Expand Up @@ -256,11 +261,11 @@ typedef struct sharedEntity_s {

typedef struct sharedEntity_qvm_s {
entityState_t s; // communicated by server to clients
uint32_t playerState; //needs to be in the gentity for bg entity access
//if you want to actually see the contents I guess
//you will have to be sure to VMA it first.
uint32_t m_pVehicle; //vehicle data
uint32_t ghoul2; //g2 instance
qvmPointerTo(playerState_t) playerState; //needs to be in the gentity for bg entity access
//if you want to actually see the contents I guess
//you will have to be sure to VMA it first.
qvmPointerTo(Vehicle_qvm_t) m_pVehicle; //vehicle data (careful: QVM memory layout differs from native Vehicle_t!)
g2handle_t ghoul2; //g2 instance
int localAnimIndex; //index locally (game/cgame) to anim data for this skel
vec3_t modelScale; //needed for g2 collision

Expand All @@ -270,15 +275,15 @@ typedef struct sharedEntity_qvm_s {

//Script/ICARUS-related fields
int taskID[NUM_TIDS];
uint32_t parms;
uint32_t behaviorSet[NUM_BSETS];
uint32_t script_targetname;
qvmPointerTo(parms_t) parms;
qvmPointerTo(char) behaviorSet[NUM_BSETS];
qvmPointerTo(char) script_targetname;
int delayScriptTime;
uint32_t fullName;
qvmPointerTo(char) fullName;

//rww - targetname and classname are now shared as well. ICARUS needs access to them.
uint32_t targetname;
uint32_t classname; // set in QuakeEd
qvmPointerTo(char) targetname;
qvmPointerTo(char) classname; // set in QuakeEd

//rww - and yet more things to share. This is because the nav code is in the exe because it's all C++.
int waypoint; //Set once per frame, if you've moved, and if someone asks
Expand All @@ -292,17 +297,29 @@ typedef struct sharedEntity_qvm_s {
int next_roff_time; //rww - npc's need to know when they're getting roff'd
} sharedEntity_qvm_t;

// A pointer to a pointer in module memory.
// Dereference and translate using SV_EntityMapperReadPointer.
typedef union pointerMapper_u {
void** native; // pointer to a real pointer, which can be typecast and used directly
qvmPointer_t* qvm; // pointer to a 32-bit-qvm address, which must be resolved using VM_ArgPtr(*qvm)
} pointerMapper_t;
// Documentation helper to annotate the underlying type.
// Doesn't actually store anything.
#define pointerMapperFor(T) pointerMapper_t

typedef struct sharedEntityMapper_s {
entityState_t *s; // communicated by server to clients
playerState_t **playerState; //needs to be in the gentity for bg entity access
pointerMapperFor(playerState_t) playerState; //needs to be in the gentity for bg entity access
//if you want to actually see the contents I guess
//you will have to be sure to VMA it first.
#if (!defined(MACOS_X) && !defined(__GCC__) && !defined(__GNUC__))
Vehicle_t **m_pVehicle; //vehicle data
#else
struct Vehicle_s **m_pVehicle; //vehicle data
#endif
void **ghoul2; //g2 instance
pointerMapperFor(Vehicle_t | Vehicle_qvm_t) m_pVehicle; // vehicle data (careful: QVM memory layout differs from native Vehicle_t!)
// ghoul2 is a bit of a special case because it doesn't point to QVM memory;
// use SV_EntityMapperReadGhoul2 to correctly dereference this,
// then use the sv_g2Mapping to resolve the resulting handle.
union {
g2handleptr_t *ghoul2Native; // in native code, it's a pointer to a pointer
g2handle_t *ghoul2QVM; // but in QVM, it's a pointer to a 32 bit handle - hence the union
};
int *localAnimIndex; //index locally (game/cgame) to anim data for this skel
vec3_t *modelScale; //needed for g2 collision

Expand All @@ -312,15 +329,15 @@ typedef struct sharedEntityMapper_s {

//Script/ICARUS-related fields
int (*taskID)[NUM_TIDS];
parms_t **parms;
char **behaviorSet[NUM_BSETS];
char **script_targetname;
pointerMapperFor(parms_t) parms;
pointerMapperFor(char) behaviorSet[NUM_BSETS];
pointerMapperFor(char) script_targetname;
int *delayScriptTime;
char **fullName;
pointerMapperFor(char) fullName;

//rww - targetname and classname are now shared as well. ICARUS needs access to them.
char **targetname;
char **classname; // set in QuakeEd
pointerMapperFor(char) targetname;
pointerMapperFor(char) classname; // set in QuakeEd

//rww - and yet more things to share. This is because the nav code is in the exe because it's all C++.
int *waypoint; //Set once per frame, if you've moved, and if someone asks
Expand Down
21 changes: 11 additions & 10 deletions codemp/icarus/GameInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ int ICARUS_RunScript( sharedEntityMapper_t *ent, const char *name )

if ( ( ICARUS_entFilter == -1 ) || ( ICARUS_entFilter == ent->s->number ) )
{
Q3_DebugPrint( WL_VERBOSE, "%d Script %s executed by %s %s\n", svs.time, (char *) name, SV_EntityMapperReadString(ent->classname), SV_EntityMapperReadString(ent->targetname) );
Q3_DebugPrint( WL_VERBOSE, "%d Script %s executed by %s %s\n", svs.time, (char *) name, reinterpret_cast<char*>(SV_EntityMapperReadPointer(ent->classname)), reinterpret_cast<char*>(SV_EntityMapperReadPointer(ent->targetname)) );
}

return true;
Expand Down Expand Up @@ -253,7 +253,7 @@ void ICARUS_FreeEnt( sharedEntityMapper_t *ent )
return;

//Remove them from the ICARUSE_EntList list so that when their g_entity index is reused, ICARUS doesn't try to affect the new (incorrect) ent.
script_targetname = SV_EntityMapperReadString( ent->script_targetname );
script_targetname = reinterpret_cast<char*>(SV_EntityMapperReadPointer( ent->script_targetname ));
if VALIDSTRING( script_targetname )
{
char temp[1024];
Expand Down Expand Up @@ -288,7 +288,7 @@ Determines whether or not an entity needs ICARUS information

bool ICARUS_ValidEnt( sharedEntityMapper_t *ent )
{
const char *script_targetname = SV_EntityMapperReadString( ent->script_targetname );
const char* script_targetname = reinterpret_cast<char*>(SV_EntityMapperReadPointer(ent->script_targetname));
int i;

//Targeted by a script
Expand All @@ -298,7 +298,7 @@ bool ICARUS_ValidEnt( sharedEntityMapper_t *ent )
//Potentially able to call a script
for ( i = 0; i < NUM_BSETS; i++ )
{
if VALIDSTRING( SV_EntityMapperReadString(ent->behaviorSet[i]) )
if VALIDSTRING( reinterpret_cast<char*>(SV_EntityMapperReadPointer(ent->behaviorSet[i])) )
{
//Com_Printf( "WARNING: Entity %d (%s) has behaviorSet but no script_targetname -- using targetname\n", ent->s.number, ent->targetname );

Expand All @@ -307,16 +307,17 @@ bool ICARUS_ValidEnt( sharedEntityMapper_t *ent )
//and while this allows us to read it on our "fake" entity here, we can't modify pointers like this. We can however do
//something completely hackish such as the following.
assert(ent->s->number >= 0 && ent->s->number < MAX_GENTITIES);
sharedEntity_t *trueEntity = SV_GentityNum(ent->s->number);

//This works because we're modifying the actual shared game vm data and turning one pointer into another.
//While these pointers both look like garbage to us in here, they are not.
if ( VM_IsCurrentQVM() )
{
sharedEntity_qvm_t *trueEntityQVM = (sharedEntity_qvm_t*)trueEntity;
trueEntityQVM->script_targetname = trueEntityQVM->targetname;
*ent->script_targetname.qvm = *ent->targetname.qvm;
}
else
{
*ent->script_targetname.native = *ent->targetname.native;
}
else trueEntity->script_targetname = trueEntity->targetname;
return true;
}
}
Expand All @@ -334,7 +335,7 @@ Associate the entity's id and name so that it can be referenced later

void ICARUS_AssociateEnt( sharedEntityMapper_t *ent )
{
const char *script_targetname = SV_EntityMapperReadString( ent->script_targetname );
const char *script_targetname = reinterpret_cast<char*>(SV_EntityMapperReadPointer( ent->script_targetname ));
char temp[1024];

if ( VALIDSTRING( script_targetname ) == false )
Expand Down Expand Up @@ -647,7 +648,7 @@ void ICARUS_PrecacheEnt( sharedEntityMapper_t *ent )

for ( i = 0; i < NUM_BSETS; i++ )
{
if ( !(behaviorStr = SV_EntityMapperReadString(ent->behaviorSet[i])) )
if ( !(behaviorStr = reinterpret_cast<char*>(SV_EntityMapperReadPointer(ent->behaviorSet[i]))) )
continue;

if ( GetIDForString( BSTable, behaviorStr ) == -1 )
Expand Down
2 changes: 1 addition & 1 deletion codemp/icarus/Q3_Interface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,7 @@ void Q3_DebugPrint( int level, const char *format, ... )
if ( ( entNum < 0 ) || ( entNum >= MAX_GENTITIES ) )
entNum = 0;

Com_Printf ( S_COLOR_BLUE"DEBUG: %s(%d): %s\n", SV_EntityMapperReadString(SV_GentityMapperNum(entNum)->script_targetname), entNum, buffer );
Com_Printf ( S_COLOR_BLUE"DEBUG: %s(%d): %s\n", SV_EntityMapperReadPointer(SV_GentityMapperNum(entNum)->script_targetname), entNum, buffer );
break;
}
default:
Expand Down
70 changes: 70 additions & 0 deletions codemp/qcommon/g2_handle_mapper.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#include "g2_handle_mapper.h"

#include <limits>

Ghoul2HandleMapper::Ghoul2HandleMapper(vm_t*& vm)
: mNextKey(1) // Start at 1, because 0 has special meaning
, mVM(vm)
{
}

CGhoul2Info_v* Ghoul2HandleMapper::Lookup(g2handleptr_t handle) const
{
// use real pointers in native modules
if (mVM->dllHandle) return reinterpret_cast<CGhoul2Info_v*>(handle);

g2handle_t g2handle = static_cast<g2handle_t>(handle);
auto it = mMap.find(g2handle);

if (it == mMap.end()) return nullptr;
return it->second;
}

void Ghoul2HandleMapper::Update(g2handleptr_t& handleInout, CGhoul2Info_v* const newValue)
{
// native modules use real pointers instead of this mapper
if (!mVM->dllHandle) return;

if (handleInout) {
if (newValue) {
// update
mMap[handleInout] = newValue;
}
else {
// deletion
if (mMap.erase(handleInout)) {
// remember released key for recycling
mFreeList.push_back(handleInout);
}
handleInout = 0;
}
}
else {
if (newValue) {
// insertion
const g2handle_t key = NextKey();
auto& res = mMap.insert({ key, newValue });
if (!res.second) {
Com_Error(ERR_FATAL, "NextKey %d already present in Ghoul2HandleMapper", key);
}
handleInout = static_cast<g2handleptr_t>(key);
}
else {
// replace nothing with nothing - do nothing
}
}
}

Ghoul2HandleMapper::Map::key_type Ghoul2HandleMapper::NextKey()
{
if (mFreeList.empty()) {
if (mNextKey == std::numeric_limits<Map::key_type>::max()) {
// We'll probably run out of memory before this happens?
Com_Error(ERR_FATAL, "used up all available g2handle_t values, please restart the game");
}
return mNextKey++;
}
auto res = mFreeList.back();
mFreeList.pop_back();
return res;
}
59 changes: 59 additions & 0 deletions codemp/qcommon/g2_handle_mapper.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
===========================================================================
Copyright(C) 2025, OpenJK contributors

This file is part of the OpenJK source code.

OpenJK is free software; you can redistribute it and /or modify it
under the terms of the GNU General Public License version 2 as
published by the Free Software Foundation.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, see < http://www.gnu.org/licenses/>.
========================================================================== =
*/

#pragma once

#include <unordered_map>
#include <deque>

#include "qcommon/qcommon.h"
#include "ghoul2/ghoul2_shared.h"

// When using a QVM, we cannot pass CGhoul2Info_v* pointers directly into the modules (as the memory is not accessible),
// so this class translates them into opaque g2handle_t values instead.
// Native (non-QVM) modules in theory should also treat these pointers as opaque handles, but they might not,
// so we expose the original pointers unchanged in that case.
class Ghoul2HandleMapper {
typedef std::unordered_map<g2handle_t, CGhoul2Info_v*> Map;
typedef std::deque<Map::key_type> FreeList;

public:
Ghoul2HandleMapper(vm_t*& vm);

// Resolves a handle.
// NULL and invalid handles yield NULL.
CGhoul2Info_v* Lookup(g2handleptr_t handle) const;
// Update creates, updates or deletes an entry, based on which arguments are null.
// Passing a null handle and a value creates a mapping entry and returns it in the handle.
// Passing a valid handle and a null value deletes the entry and nulls the handle.
// Passing a valid handle and a new value updates the entry value and keeps the handle unchanged.
void Update(g2handleptr_t& handleInout, CGhoul2Info_v* newValue);

private:
// Uses the latest entry in the Free List, or otherwise the first unused index.
// ERR_FATALs when out of keys.
Map::key_type NextKey();

Map mMap;
Map::key_type mNextKey;
// We keep track of released handles so they can be re-used.
FreeList mFreeList;
vm_t*& mVM;
};
2 changes: 1 addition & 1 deletion codemp/qcommon/msg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1176,7 +1176,7 @@ void MSG_ReadDeltaEntity( msg_t *msg, entityState_t *from, entityState_t *to,
print = 1;
if (sv.state)
{
Com_Printf( "%3i: #%-3i (%s) ", msg->readcount, number, SV_EntityMapperReadString(SV_GentityMapperNum(number)->classname) );
Com_Printf( "%3i: #%-3i (%s) ", msg->readcount, number, SV_EntityMapperReadPointer(SV_GentityMapperNum(number)->classname) );
}
else
{
Expand Down
7 changes: 7 additions & 0 deletions codemp/qcommon/q_shared.h
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,14 @@ typedef union fileBuffer_u {
} fileBuffer_t;

typedef int32_t qhandle_t, thandle_t, fxHandle_t, sfxHandle_t, fileHandle_t, clipHandle_t, g2handle_t;
// In QVMs, this is an opaque numeric g2handle_t, while in native modules, it's a CGhoul2Info_v*
// (which modules are not _supposed_ to inspect, but conceivably might...)
typedef intptr_t g2handleptr_t;
// A pointer into QVM memory. Needs to be translated using VM_ArgPtr to resolve it.
typedef uint32_t qvmPointer_t;
// A helper to annotate qvmPointer_t with the pointer type.
// This is purely for documentation, the type information isn't actually stored anywhere.
#define qvmPointerTo(T) qvmPointer_t

#define NULL_HANDLE ((qhandle_t)0)
#define NULL_SOUND ((sfxHandle_t)0)
Expand Down
Loading