/* =========================================================================== Copyright (C) 2025 the OpenMoHAA team This file is part of OpenMoHAA source code. OpenMoHAA source code is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. OpenMoHAA source code 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 OpenMoHAA source code; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA =========================================================================== */ // DESCRIPTION: // Functions for doing model animation and attachments #include "cg_local.h" #include "tiki.h" static qboolean cg_forceModelAllowed = qfalse; /* =============== CG_GetPlayerModelTiki =============== */ const char *CG_GetPlayerModelTiki(const char *modelName) { return va("models/player/%s.tik", modelName); } /* =============== CG_GetPlayerLocalModelTiki =============== */ const char *CG_GetPlayerLocalModelTiki(const char *modelName) { return va("models/player/%s.tik", modelName); } /* =============== CG_PlayerTeamIcon =============== */ void CG_PlayerTeamIcon(refEntity_t *pModel, entityState_t *pPlayerState) { qboolean bInArtillery, bInTeam, bSpecialIcon; if (cg_protocol < PROTOCOL_MOHTA_MIN) { if (pPlayerState->eFlags & EF_ALLIES) { cg.clientinfo[pPlayerState->number].team = TEAM_ALLIES; } else if (pPlayerState->eFlags & EF_AXIS) { cg.clientinfo[pPlayerState->number].team = TEAM_AXIS; } else { cg.clientinfo[pPlayerState->number].team = TEAM_NONE; } } if (pPlayerState->number == cg.snap->ps.clientNum) { return; } bInTeam = qfalse; bSpecialIcon = qfalse; if (cgs.gametype > GT_FFA && (cg.snap->ps.stats[STAT_TEAM] == TEAM_ALLIES && (pPlayerState->eFlags & EF_ALLIES) || cg.snap->ps.stats[STAT_TEAM] == TEAM_AXIS && (pPlayerState->eFlags & EF_AXIS) || cg.snap->ps.stats[STAT_TEAM] != TEAM_AXIS && cg.snap->ps.stats[STAT_TEAM] != TEAM_ALLIES && (pPlayerState->eFlags & EF_ANY_TEAM) != 0)) { bInTeam = qtrue; } if (cgs.gametype <= GT_FFA) { return; } bInArtillery = qfalse; if (pPlayerState->eFlags & EF_PLAYER_ARTILLERY) { bInArtillery = qtrue; } if (bInTeam || (pPlayerState->eFlags & (EF_PLAYER_IN_MENU | EF_PLAYER_TALKING)) || bInArtillery) { int i; int iTag; float fAlpha; float fDist; vec3_t vTmp; refEntity_t iconEnt; memset(&iconEnt, 0, sizeof(iconEnt)); if ((pPlayerState->eFlags & EF_PLAYER_TALKING) != 0 && ((cg.time >> 8) & 1) != 0) { iconEnt.hModel = cgi.R_RegisterModel("textures/hud/talking_headicon.spr"); bSpecialIcon = qtrue; } else if ((pPlayerState->eFlags & EF_PLAYER_IN_MENU) != 0) { iconEnt.hModel = cgi.R_RegisterModel("textures/hud/inmenu_headicon.spr"); bSpecialIcon = qtrue; } else { if (!bInTeam) { return; } if (bInArtillery) { iconEnt.hModel = cgi.R_RegisterModel("textures/hud/inmenu_artilleryicon.spr"); bSpecialIcon = qtrue; } else if ((pPlayerState->eFlags & 0x80) != 0) { iconEnt.hModel = cgi.R_RegisterModel("textures/hud/allies_headicon.spr"); } else { iconEnt.hModel = cgi.R_RegisterModel("textures/hud/axis_headicon.spr"); } } memset(vTmp, 0, sizeof(vTmp)); AnglesToAxis(vTmp, iconEnt.axis); iconEnt.scale = 0.5f; iconEnt.renderfx = 0; iconEnt.reType = RT_SPRITE; iconEnt.shaderTime = 0.0f; iconEnt.frameInfo[0].index = 0; iconEnt.shaderRGBA[0] = -1; iconEnt.shaderRGBA[1] = -1; iconEnt.shaderRGBA[2] = -1; VectorCopy(pModel->origin, iconEnt.origin); iTag = cgi.Tag_NumForName(pModel->tiki, "eyes bone"); if (iTag == -1) { iconEnt.origin[2] = iconEnt.origin[2] + 96.0f; } else { orientation_t oEyes = cgi.TIKI_Orientation(pModel, iTag); for (i = 0; i < 3; ++i) { VectorMA(iconEnt.origin, oEyes.origin[i], pModel->axis[i], iconEnt.origin); } iconEnt.origin[2] = iconEnt.origin[2] + 20.0f; } VectorSubtract(iconEnt.origin, cg.refdef.vieworg, vTmp); fDist = VectorLength(vTmp); if (fDist < 256.0f) { iconEnt.scale = fDist / 853.0f + 0.2f; } else if (fDist > 512.0f) { // Make sure to scale so the icon can be seen far away iconEnt.scale = (fDist - 512.0f) / 2560.0f + 0.5f; } if (iconEnt.scale > 1.0f) { iconEnt.scale = 1.0f; } if (fDist > 256.0) { fAlpha = 1.0f; } else if (fDist >= 72.0f) { fAlpha = (fDist - 72.0f) / 184.0f; } else { fAlpha = 0.0f; } if (cg.snap->ps.stats[STAT_TEAM] == TEAM_ALLIES || cg.snap->ps.stats[STAT_TEAM] == TEAM_AXIS) { fAlpha = fAlpha * 0.65f; } else { fAlpha = fAlpha * 0.4f; } if (bSpecialIcon) { int value = (int)((fAlpha + 0.6f) * 255.0f); if (value > 255) { value = 255; } iconEnt.shaderRGBA[3] = value; } else { iconEnt.shaderRGBA[3] = (int)(fAlpha * 255.0f); } if (fAlpha > 0.0 || bSpecialIcon) { if (bSpecialIcon) { VectorMA(iconEnt.origin, -2.0f, cg.refdef.viewaxis[0], iconEnt.origin); iconEnt.scale += 0.05f; } cgi.R_AddRefSpriteToScene(&iconEnt); if (bSpecialIcon && bInTeam && fAlpha > 0.0f) { if (pPlayerState->eFlags & EF_ALLIES) { iconEnt.hModel = cgi.R_RegisterModel("textures/hud/allies_headicon.spr"); } else { iconEnt.hModel = cgi.R_RegisterModel("textures/hud/axis_headicon.spr"); } VectorMA(iconEnt.origin, 4.0f, cg.refdef.viewaxis[0], iconEnt.origin); iconEnt.scale = iconEnt.scale - 0.1; iconEnt.shaderRGBA[3] = (int)(fAlpha * 255.0f); cgi.R_AddRefSpriteToScene(&iconEnt); } } } } /* =============== CG_InterpolateAnimParms Interpolate between current and next entity =============== */ void CG_InterpolateAnimParms(entityState_t *state, entityState_t *sNext, refEntity_t *model) { static cvar_t *vmEntity = NULL; int i; float t; float animLength; float t1, t2; if (!vmEntity) { vmEntity = cgi.Cvar_Get("viewmodelanim", "1", 0); } if (sNext && sNext->usageIndex == state->usageIndex) { t1 = cg.time - cg.snap->serverTime; t2 = cg.nextSnap->serverTime - cg.snap->serverTime; t = t1 / t2; model->actionWeight = (sNext->actionWeight - state->actionWeight) * t + state->actionWeight; for (i = 0; i < MAX_FRAMEINFOS; i++) { if (sNext->frameInfo[i].weight) { model->frameInfo[i].index = sNext->frameInfo[i].index; if (sNext->frameInfo[i].index == state->frameInfo[i].index && state->frameInfo[i].weight) { model->frameInfo[i].weight = (sNext->frameInfo[i].weight - state->frameInfo[i].weight) * t + state->frameInfo[i].weight; if (sNext->frameInfo[i].time >= state->frameInfo[i].time) { model->frameInfo[i].time = (sNext->frameInfo[i].time - state->frameInfo[i].time) * t + state->frameInfo[i].time; } else { animLength = cgi.Anim_Time(model->tiki, sNext->frameInfo[i].index); if (!animLength) { t1 = 0.0; } else { t1 = (animLength + sNext->frameInfo[i].time - state->frameInfo[i].time) * t + state->frameInfo[i].time; } t2 = t1; while (t2 > animLength) { t2 -= animLength; if (t2 == t1) { t2 = 1.0; break; } t1 = t2; } model->frameInfo[i].time = t2; } } else { animLength = cgi.Anim_Time(model->tiki, sNext->frameInfo[i].index); if (!animLength) { t1 = 0.0; } else { t1 = sNext->frameInfo[i].time - (cg.nextSnap->serverTime - cg.time) / 1000.0; } model->frameInfo[i].time = Q_max(0, t1); model->frameInfo[i].weight = sNext->frameInfo[i].weight; } } else if (sNext->frameInfo[i].index == state->frameInfo[i].index) { animLength = cgi.Anim_Time(model->tiki, sNext->frameInfo[i].index); if (!animLength) { t1 = 0.0; } else { t1 = (cg.time - cg.snap->serverTime) / 1000.0 + state->frameInfo[i].time; } model->frameInfo[i].index = Q_clamp_int(state->frameInfo[i].index, 0, model->tiki->a->num_anims - 1); model->frameInfo[i].time = Q_min(animLength, t1); model->frameInfo[i].weight = (1.0 - t) * state->frameInfo[i].weight; } else { model->frameInfo[i].index = -1; model->frameInfo[i].weight = 0.0; } } } else { // no next state, don't blend anims model->actionWeight = state->actionWeight; for (i = 0; i < MAX_FRAMEINFOS; i++) { if (state->frameInfo[i].weight) { model->frameInfo[i].index = Q_clamp_int(state->frameInfo[i].index, 0, model->tiki->a->num_anims - 1); model->frameInfo[i].time = state->frameInfo[i].time; model->frameInfo[i].weight = state->frameInfo[i].weight; } else { model->frameInfo[i].index = -1; model->frameInfo[i].weight = 0.0; } } } if (vmEntity->integer == state->number) { static cvar_t *curanim; if (!curanim) { curanim = cgi.Cvar_Get("viewmodelanimslot", "1", 0); } cgi.Cvar_Set("viewmodelanimclienttime", va("%0.2f", model->frameInfo[curanim->integer].time)); } } /* =============== CG_CastFootShadow Cast complex foot shadow using lights =============== */ void CG_CastFootShadow(const vec_t *vLightPos, vec_t *vLightIntensity, int iTag, refEntity_t *model) { int i; float fAlpha; float fLength; float fWidth; float fAlphaOfs; float fOfs; float fPitchCos; vec3_t vPos; vec3_t vEnd; vec3_t vDelta; vec3_t vLightAngles; trace_t trace; orientation_t oFoot; VectorCopy(model->origin, vPos); oFoot = cgi.TIKI_Orientation(model, iTag); VectorMA(oFoot.origin, 2, oFoot.axis[1], vEnd); for (i = 0; i < 3; i++) { VectorMA(vPos, vEnd[i], model->axis[i], vPos); } if (cg_shadowdebug->integer) { vec3_t vDir; // // show debug lines // memset(vDir, 0, sizeof(vDir)); for (i = 0; i < 3; ++i) { VectorMA(vDir, oFoot.axis[0][i], model->axis[i], vDir); } VectorMA(vPos, 32.0, vDir, vEnd); cgi.R_DebugLine(vPos, vEnd, 1.0, 0.0, 0.0, 1.0); memset(vDir, 0, sizeof(vDir)); for (i = 0; i < 3; ++i) { VectorMA(vDir, oFoot.axis[1][i], model->axis[i], vDir); } VectorMA(vPos, 32.0, vDir, vEnd); cgi.R_DebugLine(vPos, vEnd, 0.0, 1.0, 0.0, 1.0); memset(vDir, 0, sizeof(vDir)); for (i = 0; i < 3; ++i) { VectorMA(vDir, oFoot.axis[2][i], model->axis[i], vDir); } VectorMA(vPos, 32.0, vDir, vEnd); cgi.R_DebugLine(vPos, vEnd, 0.0, 0.0, 1.0, 1.0); } // calculate the direction VectorSubtract(vLightPos, vPos, vDelta); VectorNormalizeFast(vDelta); vectoangles(vDelta, vLightAngles); // normalize to 180 degrees if (vLightAngles[0] > 180) { vLightAngles[0] -= 360; } if (vLightAngles[0] > -5.7319679) { // FIXME: what is -5.7319679? return; } fPitchCos = cos(DEG2RAD(vLightAngles[0])); if (fPitchCos > 0.955) { fAlpha = 1.0 - (fPitchCos - 0.955) * 25; } else { fAlpha = 1.0; } fLength = fPitchCos * fPitchCos * 32.0 + fPitchCos * 8.0 + 10.0; fOfs = 0.5 - (-4.1 / tan(DEG2RAD(vLightAngles[0])) + 4.0 - fLength) / fLength * 0.5; VectorMA(vPos, -96.0, vDelta, vEnd); CG_Trace(&trace, vPos, vec3_origin, vec3_origin, vEnd, 0, MASK_FOOTSHADOW, qfalse, qtrue, "CG_CastFootShadow"); if (cg_shadowdebug->integer) { cgi.R_DebugLine(vPos, vLightPos, 0.75, 0.75, 0.5, 1.0); cgi.R_DebugLine(vPos, vEnd, 1.0, 1.0, 1.0, 1.0); } if (trace.fraction == 1.0) { return; } trace.fraction -= 0.0427f; if (trace.fraction < 0) { trace.fraction = 0; } fWidth = 10.f - (1.f - trace.fraction) * 6.f; fAlphaOfs = (1.f - trace.fraction) * fAlpha; fAlpha = Q_max(vLightIntensity[0], Q_max(vLightIntensity[1], vLightIntensity[2])); if (fAlpha < 0.1) { vLightIntensity[0] *= 0.1 / fAlpha * fAlphaOfs; vLightIntensity[1] *= 0.1 / fAlpha * fAlphaOfs; vLightIntensity[2] *= 0.1 / fAlpha * fAlphaOfs; } else { vLightIntensity[0] *= fAlphaOfs; vLightIntensity[1] *= fAlphaOfs; vLightIntensity[2] *= fAlphaOfs; } fAlpha = Q_max(vLightIntensity[0], Q_max(vLightIntensity[1], vLightIntensity[2])); if (fAlpha > 0.6) { vLightIntensity[0] *= 0.6 / fAlpha; vLightIntensity[1] *= 0.6 / fAlpha; vLightIntensity[2] *= 0.6 / fAlpha; } if (vLightIntensity[0] <= 0.01 && vLightIntensity[1] <= 0.01 && vLightIntensity[2] <= 0.01) { return; } CG_ImpactMark( cgs.media.footShadowMarkShader, trace.endpos, trace.plane.normal, vLightAngles[1], fWidth, fLength, vLightIntensity[0], vLightIntensity[1], vLightIntensity[2], 1.0, qfalse, qtrue, qfalse, qfalse, 0.5, fOfs ); } /* =============== CG_CastSimpleFeetShadow Cast basic feet shadow =============== */ void CG_CastSimpleFeetShadow( const trace_t *pTrace, float fWidth, float fAlpha, int iRightTag, int iLeftTag, const dtiki_t *tiki, refEntity_t *model ) { int i; float fShadowYaw; float fLength; vec3_t vPos, vRightPos, vLeftPos; vec3_t vDelta; orientation_t oFoot; // // right foot // VectorCopy(pTrace->endpos, vRightPos); oFoot = cgi.TIKI_Orientation(model, iRightTag); VectorMA(oFoot.origin, 3, oFoot.axis[1], vPos); for (i = 0; i < 3; i++) { VectorMA(vRightPos, vPos[i], model->axis[i], vRightPos); } VectorMA(vRightPos, -2, oFoot.axis[1], vRightPos); // // left foot // VectorCopy(pTrace->endpos, vLeftPos); oFoot = cgi.TIKI_Orientation(model, iLeftTag); VectorMA(oFoot.origin, 3, oFoot.axis[1], vPos); for (i = 0; i < 3; i++) { VectorMA(vLeftPos, vPos[i], model->axis[i], vLeftPos); } VectorAdd(vRightPos, vLeftPos, vPos); VectorScale(vPos, 0.5, vPos); VectorSubtract(vRightPos, vLeftPos, vDelta); VectorMA(vLeftPos, 0.5, vDelta, vPos); // get the facing yaw fShadowYaw = vectoyaw(vDelta); fLength = VectorNormalize(vDelta) * 0.5 + 12; if (fLength < fWidth * 0.7) { fLength = fWidth * 0.7; } // add the mark CG_ImpactMark( cgs.media.shadowMarkShader, vPos, pTrace->plane.normal, fShadowYaw, fWidth * 0.7, fLength, fAlpha, fAlpha, fAlpha, 1.0, qfalse, qtrue, qfalse, qfalse, 0.5, 0.5 ); } /* =============== CG_EntityShadow Returns the Z component of the surface being shadowed should it return a full plane instead of a Z? =============== */ #define SHADOW_DISTANCE 96 qboolean CG_EntityShadow(centity_t *cent, refEntity_t *model) { int iTagL, iTagR; float alpha; float fWidth; vec3_t end; vec3_t vMins, vMaxs; vec3_t vSize; trace_t trace; iTagR = -1; if (cg_shadows->integer == 0) { return qfalse; } if (model->renderfx & RF_SKYENTITY) { // no shadows on sky entities return qfalse; } if (cg_shadows->integer == 2 && (model->renderfx & RF_SHADOW_PRECISE)) { iTagL = cgi.Tag_NumForName(model->tiki, "Bip01 L Foot"); if (iTagL != -1) { iTagR = cgi.Tag_NumForName(model->tiki, "Bip01 R Foot"); } if (iTagR != -1) { int iNumLights, iCurrLight; vec3_t avLightPos[16], avLightIntensity[16]; iNumLights = Q_clamp(cg_shadowscount->integer, 1, 8); iNumLights = cgi.R_GatherLightSources(model->origin, avLightPos, avLightIntensity, iNumLights); if (iNumLights) { for (iCurrLight = 0; iCurrLight < iNumLights; iCurrLight++) { CG_CastFootShadow(avLightPos[iCurrLight], avLightIntensity[iCurrLight], iTagL, model); CG_CastFootShadow(avLightPos[iCurrLight], avLightIntensity[iCurrLight], iTagR, model); } // shadow was casted properly return qtrue; } } } // send a trace down from the player to the ground VectorCopy(model->origin, end); end[2] -= SHADOW_DISTANCE; cgi.CM_BoxTrace(&trace, model->origin, end, vec3_origin, vec3_origin, 0, MASK_PLAYERSOLID, qfalse); // no shadow if too high if (trace.fraction == 1.0) { return qfalse; } // since 2.0: no shadow if solid if (trace.startsolid || trace.allsolid) { return qfalse; } if ((cg_shadows->integer == 3) && (model->renderfx & RF_SHADOW_PRECISE)) { return qtrue; } // // get the bounds of the current frame // fWidth = model->scale * cgi.R_ModelRadius(model->hModel); if (fWidth < 1) { return qfalse; } // fade the shadow out with height alpha = (1.0 - trace.fraction) * 0.65f; if (model->renderfx & RF_SHADOW_PRECISE) { iTagL = cgi.Tag_NumForName(model->tiki, "Bip01 L Foot"); if (iTagL != -1) { iTagR = cgi.Tag_NumForName(model->tiki, "Bip01 R Foot"); } if (iTagR != -1) { if (cg_shadows->integer == 2) { alpha *= 0.6f; } CG_CastSimpleFeetShadow(&trace, fWidth, alpha, iTagR, iTagL, model->tiki, model); return qtrue; } } cgi.R_ModelBounds(model->hModel, vMins, vMaxs); VectorSubtract(vMaxs, vMins, vSize); VectorScale(vSize, 0.6f, vSize); // add the mark as a temporary, so it goes directly to the renderer // without taking a spot in the cg_marks array CG_ImpactMark( cgs.media.shadowMarkShader, trace.endpos, trace.plane.normal, cent->lerpAngles[YAW], vSize[1], vSize[0], alpha, alpha, alpha, 1, qfalse, qtrue, qfalse, qfalse, 0.5f, 0.5f ); return qtrue; } // // // NEW ANIMATION AND THREE PART MODEL SYSTEM // // //================= //CG_AnimationDebugMessage //================= void CG_AnimationDebugMessage(int number, const char *fmt, ...) { #ifndef NDEBUG if (cg_debugAnim->integer) { va_list argptr; char msg[1024]; va_start(argptr, fmt); Q_vsnprintf(msg, sizeof(msg), fmt, argptr); va_end(argptr); if ((!cg_debugAnimWatch->integer) || ((cg_debugAnimWatch->integer - 1) == number)) { if (cg_debugAnim->integer == 2) { cgi.DebugPrintf(msg); } else { cgi.Printf(msg); } } } #endif } /* ====================== CG_AttachEntity Modifies the entities position and axis by the given tag location ====================== */ void CG_AttachEntity( refEntity_t *entity, refEntity_t *parent, dtiki_t *tiki, int tagnum, qboolean use_angles, vec3_t attach_offset ) { int i; orientation_t or ; vec3_t tempAxis[3]; vec3_t vOrigin; vec3_t vDeltaLightOrg; or = cgi.TIKI_Orientation(parent, tagnum); //cgi.Printf( "th = %d %.2f %.2f %.2f\n", tikihandle, or.origin[ 0 ], or.origin[ 1 ], or.origin[ 2 ] ); VectorSubtract(entity->lightingOrigin, entity->origin, vDeltaLightOrg); VectorCopy(parent->origin, entity->origin); for (i = 0; i < 3; i++) { VectorMA(entity->origin, or.origin[i], parent->axis[i], entity->origin); } if (attach_offset[0] || attach_offset[1] || attach_offset[2]) { MatrixMultiply(or.axis, parent->axis, tempAxis); for (i = 0; i < 3; i++) { VectorMA(entity->origin, attach_offset[i], tempAxis[i], entity->origin); } } VectorCopy(entity->origin, entity->oldorigin); if (use_angles) { MatrixMultiply(entity->axis, or.axis, tempAxis); MatrixMultiply(tempAxis, parent->axis, entity->axis); } entity->scale *= parent->scale; entity->renderfx |= (parent->renderfx & ~(RF_FLAGS_NOT_INHERITED | RF_LIGHTING_ORIGIN)); MatrixTransformVectorRight(entity->axis, vDeltaLightOrg, vOrigin); VectorAdd(entity->origin, vOrigin, entity->lightingOrigin); } /* =============== CG_AttachEyeEntity =============== */ void CG_AttachEyeEntity( refEntity_t *entity, refEntity_t *parent, dtiki_t *tiki, int tagnum, qboolean use_angles, vec_t *attach_offset ) { int i; VectorCopy(cg.refdef.vieworg, entity->origin); if (use_angles) { AnglesToAxis(cg.refdefViewAngles, entity->axis); } if (attach_offset[0] || attach_offset[1] || attach_offset[2]) { for (i = 0; i < 3; i++) { VectorMA(entity->origin, attach_offset[i], entity->axis[i], entity->origin); } } VectorCopy(entity->origin, entity->oldorigin); entity->scale *= parent->scale; entity->renderfx |= (parent->renderfx & ~(RF_FLAGS_NOT_INHERITED | RF_LIGHTING_ORIGIN)); VectorCopy(parent->lightingOrigin, entity->lightingOrigin); } /* =============== CG_IsValidServerModel =============== */ qboolean CG_IsValidServerModel(const char *modelpath) { const char *str; int i; for (i = 1; i < MAX_MODELS; i++) { str = CG_ConfigString(CS_MODELS + i); if (!Q_stricmp(str, modelpath)) { return qtrue; } } return qfalse; } /* =============== CG_CheckValidModels This verifies the allied player model and the german player model: - If they don't exist on the client, reset to the default allied player model - If they don't exist on the server, don't allow forceModel so the client explicitly know the skin isn't supported =============== */ void CG_CheckValidModels() { const char *modelpath; qboolean isDirty = qfalse; if (dm_playermodel->modified) { // Check for allied model modelpath = va("models/player/%s.tik", dm_playermodel->string); if (!cgi.R_RegisterModel(modelpath)) { cgi.Printf( "Allied model '%s' is invalid, resetting to '%s'\n", dm_playermodel->string, dm_playermodel->resetString ); cgi.Cvar_Set("dm_playermodel", dm_playermodel->resetString); modelpath = va("models/player/%s.tik", dm_playermodel->string); } cg.serverAlliedModelValid = CG_IsValidServerModel(modelpath); } if (dm_playergermanmodel->modified) { // Check for axis model modelpath = va("models/player/%s.tik", dm_playergermanmodel->string); if (!cgi.R_RegisterModel(modelpath)) { cgi.Printf( "Allied model '%s' is invalid, resetting to '%s'\n", dm_playergermanmodel->string, dm_playergermanmodel->resetString ); cgi.Cvar_Set("dm_playergermanmodel", dm_playergermanmodel->resetString); modelpath = va("models/player/%s.tik", dm_playergermanmodel->string); } cg.serverAxisModelValid = CG_IsValidServerModel(modelpath); } if (dm_playermodel->modified || dm_playergermanmodel->modified) { cg_forceModelAllowed = cg.serverAlliedModelValid && cg.serverAxisModelValid; } } /* =============== CG_ServerModelLoaded =============== */ void CG_ServerModelLoaded(const char *name, qhandle_t handle) { if (!Q_stricmpn(name, "models/player/", 14) && (!cg.serverAlliedModelValid || !cg.serverAxisModelValid)) { char modelName[MAX_QPATH]; COM_StripExtension(name + 14, modelName, sizeof(modelName)); // // The player model has been loaded on the server // so try again parsing // if (!Q_stricmp(modelName, dm_playermodel->string)) { dm_playermodel->modified = qtrue; } if (!Q_stricmp(modelName, dm_playergermanmodel->string)) { dm_playergermanmodel->modified = qtrue; } } } /* =============== CG_ServerModelUnloaded =============== */ void CG_ServerModelUnloaded(qhandle_t handle) { #if 0 if (cg.serverAlliedModelValid && handle == cg.hAlliedPlayerModelHandle) { dm_playermodel->modified = qtrue; } if (cg.serverAxisModelValid && handle == cg.hAxisPlayerModelHandle) { dm_playergermanmodel->modified = qtrue; } #endif } /* =============== CG_UpdateForceModels =============== */ void CG_UpdateForceModels() { qhandle_t hModel; char *pszAlliesPartial; char *pszAxisPartial; char szAlliesModel[256]; char szAxisModel[256]; qboolean isDirty; isDirty = dm_playermodel->modified || dm_playergermanmodel->modified || cg_forceModel->modified; if (!cg_forceModelAllowed) { if (isDirty) { cgi.Printf( "One or more of the selected players model don't exist on the server or are not loaded, using the " "default skin\n" ); } return; } if (cg.pAlliedPlayerModel && cg.pAxisPlayerModel && !isDirty) { return; } pszAlliesPartial = dm_playermodel->string; pszAxisPartial = dm_playergermanmodel->string; Com_sprintf(szAlliesModel, sizeof(szAlliesModel), "models/player/%s.tik", pszAlliesPartial); Com_sprintf(szAxisModel, sizeof(szAxisModel), "models/player/%s.tik", pszAxisPartial); hModel = cg.serverAlliedModelValid ? cgi.R_RegisterModel(szAlliesModel) : 0; if (!hModel) { Com_sprintf(szAlliesModel, sizeof(szAlliesModel), "models/player/%s.tik", dm_playermodel->resetString); hModel = cgi.R_RegisterModel(szAlliesModel); } if (hModel) { cg.hAlliedPlayerModelHandle = hModel; cg.pAlliedPlayerModel = cgi.R_Model_GetHandle(hModel); if (!cg.pAlliedPlayerModel) { cg.hAlliedPlayerModelHandle = 0; } } else { cg.hAlliedPlayerModelHandle = 0; cg.pAlliedPlayerModel = NULL; } hModel = cg.serverAxisModelValid ? cgi.R_RegisterModel(szAxisModel) : 0; if (!hModel) { Com_sprintf(szAxisModel, sizeof(szAxisModel), "models/player/%s.tik", dm_playergermanmodel->resetString); hModel = cgi.R_RegisterModel(szAxisModel); } if (hModel) { cg.hAxisPlayerModelHandle = hModel; cg.pAxisPlayerModel = cgi.R_Model_GetHandle(hModel); if (!cg.pAxisPlayerModel) { cg.hAxisPlayerModelHandle = 0; } } else { cg.hAxisPlayerModelHandle = 0; cg.pAxisPlayerModel = 0; } // Clear modified flag //dm_playermodel->modified = qfalse; //dm_playergermanmodel->modified = qfalse; } /* =============== CG_ProcessPlayerModel Checks player models, and update force models =============== */ void CG_ProcessPlayerModel() { CG_CheckValidModels(); if (cg_forceModel->integer) { CG_UpdateForceModels(); } // Clear modified flag dm_playermodel->modified = qfalse; dm_playergermanmodel->modified = qfalse; cg_forceModel->modified = qfalse; } /* =============== CG_ModelAnim =============== */ void CG_ModelAnim(centity_t *cent, qboolean bDoShaderTime) { entityState_t *s1; entityState_t *sNext = NULL; refEntity_t model; int i; vec3_t vMins, vMaxs, vTmp; const char *szTagName; int iAnimFlags; s1 = ¢->currentState; if ((cg.snap->ps.pm_flags & PMF_INTERMISSION) && s1->number == cg.snap->ps.clientNum && !cg_3rd_person->integer) { // don't render if in intermission and the client is self without 3rd person return; } memset(&model, 0, sizeof(model)); if (cent->interpolate) { sNext = ¢->nextState; } // add loop sound only if it is not attached if (s1->loopSound && (s1->parent == ENTITYNUM_NONE)) { cgi.S_AddLoopingSound( cent->lerpOrigin, vec3_origin, cgs.sound_precache[s1->loopSound], s1->loopSoundVolume, s1->loopSoundMinDist, s1->loopSoundMaxDist, s1->loopSoundPitch, s1->loopSoundFlags ); } if (cent->tikiLoopSound && (s1->parent == ENTITYNUM_NONE)) { cgi.S_AddLoopingSound( cent->lerpOrigin, vec3_origin, cent->tikiLoopSound, cent->tikiLoopSoundVolume, cent->tikiLoopSoundMinDist, cent->tikiLoopSoundMaxDist, cent->tikiLoopSoundPitch, cent->tikiLoopSoundFlags ); } if (s1->renderfx & RF_SKYORIGIN) { AnglesToAxis(cent->lerpAngles, cg.sky_axis); VectorCopy(cent->lerpOrigin, cg.sky_origin); } // if set to invisible, skip if (!s1->modelindex) { return; } // set the entity number model.entityNumber = s1->number; // take the results of CL_InterpolateEntities VectorCopy(cent->lerpOrigin, model.origin); VectorCopy(cent->lerpOrigin, model.oldorigin); IntegerToBoundingBox(s1->solid, vMins, vMaxs); // calculate the light origin VectorAdd(vMins, vMaxs, vTmp); VectorMA(model.origin, 0.5, vTmp, model.lightingOrigin); // calculate the radius VectorSubtract(vMins, vMaxs, vTmp); model.radius = VectorLength(vTmp) * 0.5; if (s1->number == cg.snap->ps.clientNum) { if (!cg_3rd_person->integer) { PmoveAdjustAngleSettings_Client( cg.refdefViewAngles, cent->lerpAngles, &cg.predicted_player_state, ¢->currentState ); } model.bone_quat = s1->bone_quat; model.bone_tag = s1->bone_tag; } else { for (i = 0; i < NUM_BONE_CONTROLLERS; i++) { if (s1->bone_tag[i] >= 0) { if ((cent->interpolate) && (cent->nextState.bone_tag[i] == s1->bone_tag[i])) { SlerpQuaternion( s1->bone_quat[i], cent->nextState.bone_quat[i], cg.frameInterpolation, cent->bone_quat[i] ); } else { cent->bone_quat[i][0] = s1->bone_quat[i][0]; cent->bone_quat[i][1] = s1->bone_quat[i][1]; cent->bone_quat[i][2] = s1->bone_quat[i][2]; cent->bone_quat[i][3] = s1->bone_quat[i][3]; } } } model.bone_quat = cent->bone_quat; model.bone_tag = s1->bone_tag; } // convert angles to axis AnglesToAxis(cent->lerpAngles, model.axis); // copy shader specific data if (s1->shader_data[0]) { model.shader_data[0] = s1->shader_data[0]; } else { model.shader_data[0] = s1->tag_num; } if (s1->shader_data[1]) { model.shader_data[1] = s1->shader_data[1]; } else { model.shader_data[1] = s1->skinNum; } if (bDoShaderTime) { if (cent->interpolate) { model.shaderTime = s1->shader_time + (sNext->shader_time - s1->shader_time) * cg.frameInterpolation + cg.time / 1000.0; } else { model.shaderTime = cg.time / 1000.0 + s1->shader_time; } } // Interpolated state variables if (cent->interpolate) { model.scale = s1->scale + cg.frameInterpolation * (cent->nextState.scale - s1->scale); } else { model.scale = s1->scale; } model.hOldModel = 0; model.tiki = cgi.R_Model_GetHandle(cgs.model_draw[s1->modelindex]); if (s1->number != cg.snap->ps.clientNum && (s1->eType == ET_PLAYER || (s1->eFlags & EF_DEAD))) { if (cg_forceModel->integer && cg_forceModelAllowed) { //CG_UpdateForceModels(); if (s1->eFlags & EF_AXIS) { model.hModel = cg.hAxisPlayerModelHandle; model.tiki = cg.pAxisPlayerModel; } else { model.hModel = cg.hAlliedPlayerModelHandle; model.tiki = cg.pAlliedPlayerModel; } if (model.hModel && model.tiki) { model.hOldModel = cgs.model_draw[s1->modelindex]; } else { // fallback to non-forced model model.tiki = cgi.R_Model_GetHandle(cgs.model_draw[s1->modelindex]); model.hModel = cgs.model_draw[s1->modelindex]; } } else { model.hModel = cgs.model_draw[s1->modelindex]; } if (!model.hModel || !model.tiki) { // Use a model in case it still doesn't exist if (s1->eFlags & EF_AXIS) { model.hModel = cgi.R_RegisterModel(CG_GetPlayerModelTiki(dm_playergermanmodel->resetString)); } else { model.hModel = cgi.R_RegisterModel(CG_GetPlayerModelTiki(dm_playermodel->resetString)); } model.tiki = cgi.R_Model_GetHandle(model.hModel); model.hOldModel = cgs.model_draw[s1->modelindex]; } } else { model.hModel = cgs.model_draw[s1->modelindex]; } if (!model.tiki) { // still no model return; } // set skin model.skinNum = s1->skinNum; model.renderfx |= s1->renderfx; cgi.TIKI_SetEyeTargetPos(model.tiki, model.entityNumber, s1->eyeVector); CG_InterpolateAnimParms(s1, sNext, &model); if (cent->currentState.parent != ENTITYNUM_NONE) { int iTagNum; refEntity_t *parent; dtiki_t *tiki; parent = cgi.R_GetRenderEntity(cent->currentState.parent); if (!parent) { if (developer->integer > 1) { cgi.DPrintf("CG_ModelAnim: Could not find parent entity\n"); } return; } if (s1->parent != cg.snap->ps.clientNum || cg_3rd_person->integer) { // attach the model to the world model if (parent->hOldModel) { tiki = cgi.R_Model_GetHandle(parent->hOldModel); szTagName = cgi.Tag_NameForNum(tiki, s1->tag_num & TAG_MASK); tiki = cgi.R_Model_GetHandle(parent->hModel); iTagNum = cgi.Tag_NumForName(tiki, szTagName); } else { tiki = cgi.R_Model_GetHandle(parent->hModel); iTagNum = s1->tag_num; } CG_AttachEntity(&model, parent, tiki, iTagNum & TAG_MASK, s1->attach_use_angles, s1->attach_offset); } else { tiki = cg.pPlayerFPSModel; // attach to the first person model if (cg.pLastPlayerWorldModel) { szTagName = cgi.Tag_NameForNum(cg.pLastPlayerWorldModel, s1->tag_num & TAG_MASK); } else { szTagName = cgi.Tag_NameForNum(tiki, s1->tag_num & TAG_MASK); } if (!Q_stricmp(szTagName, "eyes bone")) { iTagNum = cgi.Tag_NumForName(tiki, szTagName); CG_AttachEyeEntity(&model, parent, tiki, iTagNum & TAG_MASK, s1->attach_use_angles, s1->attach_offset); } else if (!Q_stricmp(szTagName, "tag_weapon_right") || !Q_stricmp(szTagName, "tag_weapon_left")) { iTagNum = cgi.Tag_NumForName(tiki, szTagName); CG_AttachEntity(&model, parent, tiki, iTagNum & TAG_MASK, s1->attach_use_angles, s1->attach_offset); } else { // Don't show the model at all return; } } if (s1->loopSound) { cgi.S_AddLoopingSound( model.origin, vec3_origin, cgs.sound_precache[s1->loopSound], s1->loopSoundVolume, s1->loopSoundMinDist, s1->loopSoundMaxDist, s1->loopSoundPitch, s1->loopSoundFlags ); } if (cent->tikiLoopSound) { cgi.S_AddLoopingSound( cent->lerpOrigin, vec3_origin, cent->tikiLoopSound, cent->tikiLoopSoundVolume, cent->tikiLoopSoundMinDist, cent->tikiLoopSoundMaxDist, cent->tikiLoopSoundPitch, cent->tikiLoopSoundFlags ); } // set the attached model to have the same render FX model.renderfx &= ~(RF_THIRD_PERSON | RF_THIRD_PERSON | RF_DEPTHHACK); model.renderfx |= parent->renderfx & (RF_THIRD_PERSON | RF_THIRD_PERSON | RF_DEPTHHACK); } for (i = 0; i < 3; i++) { model.shaderRGBA[i] = cent->color[i] * 255; } model.shaderRGBA[3] = s1->alpha * 255; // set surfaces memcpy(model.surfaces, s1->surfaces, MAX_MODEL_SURFACES); if (!(s1->renderfx & RF_VIEWMODEL) && s1->parent != ENTITYNUM_NONE && s1->parent == cg.snap->ps.clientNum && ((!cg_drawviewmodel->integer && !cg_3rd_person->integer) || cg.snap->ps.stats[STAT_INZOOM])) { // hide all surfaces while zooming or if the viewmodel shouldn't be shown for (i = 0; i < MAX_MODEL_SURFACES; i++) { model.surfaces[i] |= MDL_SURFACE_NODRAW; } } if (!(s1->renderfx & RF_DONTDRAW) && (model.renderfx & RF_SHADOW)) { // add the shadow CG_EntityShadow(cent, &model); } iAnimFlags = 0; // combine anim flags from all frame infos for (i = 0; i < MAX_FRAMEINFOS; i++) { if (model.frameInfo[i].weight && model.frameInfo[i].index >= 0) { iAnimFlags |= cgi.Anim_Flags(model.tiki, model.frameInfo[i].index); } } if (iAnimFlags & TAF_AUTOSTEPS) { int iTagNum; // Automatically calculate the footsteps sounds if (cent->bFootOnGround_Right) { iTagNum = cgi.Tag_NumForName(model.tiki, "Bip01 R Foot"); if (iTagNum >= 0) { cent->bFootOnGround_Right = cgi.TIKI_IsOnGround(&model, iTagNum, 13.653847f); } else { cent->bFootOnGround_Right = qtrue; } } else { iTagNum = cgi.Tag_NumForName(model.tiki, "Bip01 R Foot"); if (iTagNum >= 0) { if (cgi.TIKI_IsOnGround(&model, iTagNum, 13.461539f)) { CG_Footstep( "Bip01 R Foot", cent, &model, (iAnimFlags & TAF_AUTOSTEPS_RUNNING), (iAnimFlags & TAF_AUTOSTEPS_EQUIPMENT) ); cent->bFootOnGround_Right = qtrue; } } else { cent->bFootOnGround_Right = qtrue; } } if (cent->bFootOnGround_Left) { iTagNum = cgi.Tag_NumForName(model.tiki, "Bip01 L Foot"); if (iTagNum >= 0) { cent->bFootOnGround_Left = cgi.TIKI_IsOnGround(&model, iTagNum, 13.653847f); } else { cent->bFootOnGround_Left = qtrue; } } else { iTagNum = cgi.Tag_NumForName(model.tiki, "Bip01 L Foot"); if (iTagNum >= 0) { if (cgi.TIKI_IsOnGround(&model, iTagNum, 13.461539f)) { CG_Footstep( "Bip01 L Foot", cent, &model, (iAnimFlags & TAF_AUTOSTEPS_RUNNING), (iAnimFlags & TAF_AUTOSTEPS_EQUIPMENT) ); cent->bFootOnGround_Left = qtrue; } } else { cent->bFootOnGround_Left = qtrue; } } } else { cent->bFootOnGround_Left = qtrue; cent->bFootOnGround_Right = qtrue; } if (cent->currentState.eType == ET_PLAYER && !(cent->currentState.eFlags & EF_DEAD)) { CG_PlayerTeamIcon(&model, ¢->currentState); } if (s1->number == cg.snap->ps.clientNum) { if ((!cg.bFPSModelLastFrame && !cg_3rd_person->integer) || (cg.bFPSModelLastFrame && cg_3rd_person->integer)) { // reset the animations when toggling 3rd person for (i = 0; i < MAX_FRAMEINFOS; i++) { cent->animLast[i] = -1; } cent->animLastWeight = 0; cent->usageIndexLast = 0; cg.bFPSModelLastFrame = !cg_3rd_person->integer; } // player footsteps, walking/falling if (cg.bFPSOnGround != cg.predicted_player_state.walking) { cg.bFPSOnGround = cg.predicted_player_state.walking; if (cg.predicted_player_state.walking) { CG_LandingSound(cent, &model, 1.0, 1); } else { if (cent->iNextLandTime < cg.time) { CG_Footstep(0, cent, &model, 1, 1); } cent->iNextLandTime = cg.time + 200; } } if (!cg_3rd_person->integer) { // first person view if (!(cg.predicted_player_state.pm_flags & PMF_CAMERA_VIEW) && (cg.snap->ps.stats[STAT_HEALTH] <= 0 || cg_animationviewmodel->integer)) { // use world position for this case CG_OffsetFirstPersonView(&model, qtrue); } if (!cg.pLastPlayerWorldModel || cg.pLastPlayerWorldModel != model.tiki) { qhandle_t hModel; char fpsname[128]; COM_StripExtension(model.tiki->a->name, fpsname, sizeof(fpsname)); Q_strcat(fpsname, sizeof(fpsname), "_fps.tik"); hModel = cgi.R_RegisterModel(fpsname); if (hModel) { cg.hPlayerFPSModelHandle = hModel; cg.pPlayerFPSModel = cgi.R_Model_GetHandle(hModel); if (!cg.pPlayerFPSModel) { cg.pPlayerFPSModel = model.tiki; } } else { if (cg.snap->ps.stats[STAT_TEAM] == TEAM_AXIS) { hModel = cgi.R_RegisterModel(CG_GetPlayerLocalModelTiki(dm_playergermanmodel->resetString)); } else { hModel = cgi.R_RegisterModel(CG_GetPlayerLocalModelTiki(dm_playermodel->resetString)); } if (hModel) { cg.hPlayerFPSModelHandle = hModel; cg.pPlayerFPSModel = cgi.R_Model_GetHandle(hModel); if (!cg.pPlayerFPSModel) { cg.pPlayerFPSModel = model.tiki; } } else { cg.hPlayerFPSModelHandle = cgs.model_draw[s1->modelindex]; cg.pPlayerFPSModel = model.tiki; } } cg.pLastPlayerWorldModel = model.tiki; } model.tiki = cg.pPlayerFPSModel; model.hModel = cg.hPlayerFPSModelHandle; memset(model.surfaces, 0, sizeof(model.surfaces)); CG_ViewModelAnimation(&model); model.renderfx |= RF_FRAMELERP; cgi.ForceUpdatePose(&model); if ((cent->currentState.eFlags & EF_UNARMED) || cg_drawviewmodel->integer <= 1 || cg.snap->ps.stats[STAT_INZOOM] || cg.snap->ps.stats[STAT_HEALTH] <= 0) { // unarmed or zooming, hide the arms for (i = 0; i < MAX_MODEL_SURFACES; i++) { model.surfaces[i] |= MDL_SURFACE_NODRAW; } } else { // show/hide the garand hand depending if it's a rifle or not // so the hand can hold the rifle correctly const char *weaponstring = ""; int iSurfaceNum; if (cg.snap->ps.activeItems[1] >= 0) { weaponstring = CG_ConfigString(CS_WEAPONS + cg.snap->ps.activeItems[1]); } if (!Q_stricmp(weaponstring, "M1 Garand") || !Q_stricmp(weaponstring, "Springfield '03 Sniper") || !Q_stricmp(weaponstring, "Mauser KAR 98K") || !Q_stricmp(weaponstring, "KAR98 - Sniper")) { // show the garand hands iSurfaceNum = cgi.Surface_NameToNum(model.tiki, "lefthand"); if (iSurfaceNum >= 0) { model.surfaces[iSurfaceNum] |= MDL_SURFACE_NODRAW; } iSurfaceNum = cgi.Surface_NameToNum(model.tiki, "garandhand"); if (iSurfaceNum >= 0) { model.surfaces[iSurfaceNum] &= ~MDL_SURFACE_NODRAW; } } else { // hide the garand hands iSurfaceNum = cgi.Surface_NameToNum(model.tiki, "garandhand"); if (iSurfaceNum >= 0) { model.surfaces[iSurfaceNum] |= MDL_SURFACE_NODRAW; } iSurfaceNum = cgi.Surface_NameToNum(model.tiki, "lefthand"); if (iSurfaceNum >= 0) { model.surfaces[iSurfaceNum] &= ~MDL_SURFACE_NODRAW; } } } if (!(s1->eFlags & EF_CLIMBWALL)) { // when the player is not climbing ladders show the entity model.renderfx |= RF_DEPTHHACK; } if (!(cg.predicted_player_state.pm_flags & PMF_CAMERA_VIEW)) { if (cg.snap->ps.stats[STAT_HEALTH] > 0 && !cg_animationviewmodel->integer) { CG_OffsetFirstPersonView(&model, qfalse); } AnglesToAxis(cg.refdefViewAngles, cg.refdef.viewaxis); } model.renderfx &= ~(RF_FIRST_PERSON | RF_THIRD_PERSON); // set the first person render flag model.renderfx |= RF_FIRST_PERSON; } } model.reType = RT_MODEL; if (!(s1->renderfx & RF_DONTDRAW)) { cgi.R_Model_GetHandle(model.hModel); if (VectorCompare(model.origin, vec3_origin)) { VectorCopy(s1->origin, model.origin); AngleVectors(s1->angles, model.axis[0], model.axis[1], model.axis[2]); } // add to refresh list cgi.R_AddRefEntityToScene(&model, s1->parent); } CG_UpdateEntityEmitters(s1->number, &model, cent); if (s1->usageIndex == cent->usageIndexLast) { // process the exit commands of the last animations for (i = 0; i < MAX_FRAMEINFOS; i++) { if ((cent->animLastWeight >> i) & 1) { if (!model.frameInfo[i].weight || model.frameInfo[i].index != cent->animLast[i]) { CG_ProcessEntityCommands(TIKI_FRAME_EXIT, cent->animLast[i], s1->number, &model, cent); } } } } for (i = 0; i < MAX_FRAMEINFOS; i++) { // process the entry commands of the current anim if (model.frameInfo[i].weight) { if (!((cent->animLastWeight >> i) & 1) || model.frameInfo[i].index != cent->animLast[i]) { CG_ProcessEntityCommands(TIKI_FRAME_ENTRY, model.frameInfo[i].index, s1->number, &model, cent); if (cent->animLastTimes[i] == -1) { cent->animLast[i] = model.frameInfo[i].index; cent->animLastTimes[i] = model.frameInfo[i].time; } else { cent->animLastTimes[i] = 0; } } CG_ClientCommands(&model, cent, i); } cent->animLastTimes[i] = model.frameInfo[i].time; cent->animLast[i] = model.frameInfo[i].index; if (model.frameInfo[i].weight) { cent->animLastWeight |= 1 << i; } else { cent->animLastWeight &= ~(1 << i); } } cent->usageIndexLast = cent->currentState.usageIndex; }