ts/src/shared/player.qc

2140 lines
60 KiB
Plaintext

/*
* Copyright (c) 2016-2021 Marco Hladik <marco@icculus.org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
// little test
//var float otherTimer = 0;
//#define FORCE_NETWORK_ALL_INVENTORY
/* all potential SendFlags bits we can possibly send */
enumflags
{
PLAYER_KEEPALIVE,
PLAYER_MODELINDEX,
PLAYER_ORIGIN,
PLAYER_ORIGIN_Z,
PLAYER_ANGLES_X,
PLAYER_ANGLES_Y,
PLAYER_COLORMAP,
PLAYER_VELOCITY,
PLAYER_VELOCITY_Z,
PLAYER_FLAGS,
PLAYER_WEAPON,
PLAYER_ITEMS,
PLAYER_HEALTH,
PLAYER_ARMOR,
PLAYER_MOVETYPE,
PLAYER_VIEWOFS,
PLAYER_TOPFRAME,
PLAYER_BOTTOMFRAME,
PLAYER_AMMO1,
PLAYER_AMMO2,
PLAYER_AMMO3,
PLAYER_UNUSED1,
PLAYER_UNUSED2
};
// player class wuz here
//void player::dummyBufferMethod(void){}
#ifdef CLIENT
void Weapons_AmmoUpdate(entity);
/*
=================
player::ReceiveEntity
=================
*/
void
player::ReceiveEntity(float new, float fl)
{
/* the generic client attributes */
base_player::ReceiveEntity(new, fl);
/* animation */
if (fl & PLAYER_TOPFRAME) {
anim_top = readbyte();
anim_top_time = readfloat();
anim_top_delay = readfloat();
}
if (fl & PLAYER_BOTTOMFRAME) {
anim_bottom = readbyte();
anim_bottom_time = readfloat();
}
int i;
float temp_flViewModelFrame;
temp_flViewModelFrame = readfloat();
// TODO - any more sophistication needed? laser-lock versions? custom idle delays? etc.?
// do we need to do this here, I: forget, I don't think so
/*
int s = (float)getproperty(VF_ACTIVESEAT);
pSeat = &g_seats[s];
*/
/*
//pSeat->m_eViewModel.frame = (float)iSequence;
if(pSeat->m_eViewModel.frame != temp_flViewModelFrame){
// !!! Re-enable this to allow server-client viewmodel sequence corrections, in case
// they go out-of-sync compared to what the server expects the client to be doing.
// This still needs better support for other TS_Weapons_ViewAnimation calls
// (Bad to assume that the plain variant was always the one called)
printfline("***VIEWMODEL FRAME SYNC ISSUE CORRECTED: was:%d suggested:%d", pSeat->m_eViewModel.frame, temp_flViewModelFrame);
// NOTE - this var has been abandoned, Nuclide's w_idle_next is used instead.
//if(temp_nextViewModelAnimationTime <= 0 || temp_nextViewModelAnimationTime == 255){
// temp_nextViewModelAnimationTime = 1; // ???
//}else{
// temp_nextViewModelAnimationTime = temp_nextViewModelAnimationTime + time;
//}
TS_Weapons_ViewAnimation(temp_flViewModelFrame, w_idle_next);
}
*/
//TAGGG - REPLACED.
/*
if (fl & PLAYER_AMMO1) {
glock_mag = readbyte();
mp5_mag = readbyte();
python_mag = readbyte();
shotgun_mag = readbyte();
crossbow_mag = readbyte();
rpg_mag = readbyte();
satchel_chg = readbyte();
}
if (fl & PLAYER_AMMO2) {
ammo_9mm = readbyte();
ammo_357 = readbyte();
ammo_buckshot = readbyte();
ammo_bolt = readbyte();
ammo_rocket = readbyte();
ammo_uranium = readbyte();
ammo_handgrenade = readbyte();
ammo_satchel = readbyte();
ammo_tripmine = readbyte();
ammo_snark = readbyte();
ammo_hornet = readbyte();
}
if (fl & PLAYER_AMMO3) {
ammo_m203_grenade = readbyte();
ammo_gauss_volume = readbyte();
ammo_rpg_state = readbyte();
mode_tempstate = readbyte();
}
if (fl & PLAYER_AMMO1 || fl & PLAYER_AMMO2 || fl & PLAYER_AMMO3)
Weapons_AmmoUpdate(this);
*/
/////////////////////////////////////////////////////
iState = readfloat();
int inventoryEquippedIndex_temp = readbyte();
// Overflow can do funny things. -1 comes as 255 because thats how bits work
// Of course if we have over (or exactly) 255 weapons this would not be good.
// we'd run out of room for those 255+ weapons anyway.
if(inventoryEquippedIndex_temp == 255){
inventoryEquippedIndex_temp = -1;
}
//if(oldPlayerWeapEq != inventoryEquippedIndex){
// printfline("WELL hey WHAT:%i new:%i", oldPlayerWeapEq, inventoryEquippedIndex);
//}
// TODO! Test surrounding with this
if(!equippedWeaponWaitingForCallback){
}
// important to keep pl.activeweapon in sync
inventoryEquippedIndex = inventoryEquippedIndex_temp;
//setInventoryEquippedIndex(inventoryEquippedIndex_temp);
// Somehow this is atrocious.
/*
if(inventoryEquippedIndex_temp != inventoryEquippedIndex){
setInventoryEquippedIndex(inventoryEquippedIndex_temp);
}
*/
weaponEquippedAkimbo = readbyte();
w_attack_akimbo_next = readfloat();
isReloading = readbyte();
isChangingIronsight = readbyte();
if(fl & PLAYER_UNUSED2){
//float flZoomTarget_temp = readfloat();
//flZoomTarget = flZoomTarget_temp;
int iZoomLevel_temp = readbyte();
if(iZoomLevel_temp != iZoomLevel){
// printfline("!!! ZoomLevel change A. %i -> %i (%.2f)", iZoomLevel, iZoomLevel_temp, time);
}
if(!equippedWeaponWaitingForCallback){
iZoomLevel = iZoomLevel_temp;
}
}
nextAkimboAttackPreference = readbyte();
akimboDualFireToleranceTime = readfloat();
grenadeFireIndex = readbyte() - 1;
bGrenadeToss = readbyte();
armor = readbyte();
iTotalSlots = readbyte();
//flKarateBlockCooldown
iMeleeCycler = readbyte();
/*
vViewAngleOffsetTargetDir[0] = readcoord();
vViewAngleOffsetTargetDir[1] = readcoord();
vViewAngleOffsetTargetDir[2] = readcoord();
*/
vViewAngleOffsetTotalChange[0] = readcoord();
vViewAngleOffsetTotalChange[1] = readcoord();
vViewAngleOffsetTotalChange[2] = readcoord();
//TAGGG - vital to the viewangle-modifying kickback looking right
// with prediction!
setproperty(VF_CL_VIEWANGLES, GET_MY_VIEW_ANGLES + vViewAngleOffsetTotalChange);
vViewAngleOffsetTotalChange = [0,0,0];
fAccuracyKickback = readfloat();
fAccuracyKickbackStartCooldown = readfloat();
float fMoveBlockDelay_temp = readfloat();
float fUncrouchBlockDelay_temp = readfloat();
if(!equippedWeaponWaitingForCallback){
fMoveBlockDelay = fMoveBlockDelay_temp;
fUncrouchBlockDelay = fUncrouchBlockDelay_temp;
}
fKarateStamina = readfloat();
shotgunReloadIndex = readbyte();
shotgunAddAmmoTime = readfloat();
shotgunAddAmmoSoundTime = readfloat();
ary_myWeapons_softMax = readbyte();
this.completeInventorySend = readbyte();
#ifdef FORCE_NETWORK_ALL_INVENTORY
completeInventorySend = TRUE;
#endif
if(this.completeInventorySend){
for(i = 0; i < ary_myWeapons_softMax; i++){
ReceiveEntity_ary_myWeapons(i);
}
for(i = 0; i < AMMO_ID::LAST_ID; i++){
ary_ammoTotal[i] = readlong();
}
}else if(fl & PLAYER_UNUSED1){
//i = inventoryEquippedIndex;
i = readbyte() - 1;
/// could this ever be -1?
if(i != -1){
ReceiveEntity_ary_myWeapons(i);
//int myAmmoType = getAmmoTypeOfWeapon(this.activeweapon);
int betterID = ary_myWeapons[inventoryEquippedIndex].weaponID;
int myAmmoType = getAmmoTypeOfWeapon(betterID);
if(myAmmoType != -1){
i = myAmmoType;
//UNNECESSARY. This array is of fixed length, so known at all times.
//WriteByte(MSG_ENTITY, ary_ammoTotal_softMax);
//for(i = 0; i < AMMO_ID::LAST_ID; i++){
// See serverside equivalent, too much info was lost from some pools being over 255
// (well I guess that's all there is to it)
ary_ammoTotal[i] = readlong();
//}
}
}/*else{
// STUPID.
ReceiveEntity_ary_myWeapons(0);
ary_ammoTotal[0] = readlong();
}
*/
}
/////////////////////////////////////////////////////
// safe?
receivedViewAngles = this.v_angle;
// TODO! Check for any change in ammo values like this:
// Or maybe not, looks like Weapons_AmmoUpdate isn't needed for FreeTS, HUD draw methods
// already use the current weapon's ammo valus per weapon on the HUD without it
// if (fl & PLAYER_AMMO1 || fl & PLAYER_AMMO2 || fl & PLAYER_AMMO3)
//Weapons_AmmoUpdate(this);
setorigin(this, origin);
}
void player::ReceiveEntity_ary_myWeapons(int i){
// TODO: IDEA. Only send updates for the currently equipped weapon, maybe the previously equipped one
// a few frames too? Unsure.
ary_myWeapons[i].weaponID = readbyte();
ary_myWeapons[i].weaponTypeID = readbyte();
ary_myWeapons[i].iBitsUpgrade = readbyte();
ary_myWeapons[i].iCount = readbyte();
//ary_myWeapons[i].iPrice = readbyte();
//ary_myWeapons[i].iSlots = readbyte();
ary_myWeapons[i].iClipLeft = readbyte();
ary_myWeapons[i].iClipAkimboLeft = readbyte();
#if OTHER_PREDICTION_TEST != 1
ary_myWeapons[i].iBitsUpgrade_on = readbyte();
#endif
ary_myWeapons[i].iFireMode = readbyte();
ary_myWeapons[i].iFireModeAkimbo = readbyte();
ary_myWeapons[i].iIronSight = readbyte();
ary_myWeapons[i].iForceBodygroup1Submodel = readbyte();
ary_myWeapons[i].bNeedsPump = readbyte();
}
/*
=================
player::PredictPreFrame
Save the last valid server values away in the _net variants of each field
so we can roll them back later.
=================
*/
void
player::PredictPreFrame(void)
{
//printfline("---PREDICT PRE FRAME");
//sendevent("ViewOffsetR", "fff", vViewAngleOffsetTotalChange[0], vViewAngleOffsetTotalChange[1], vViewAngleOffsetTotalChange[2]);
/* the generic client attributes */
base_player::PredictPreFrame();
// client's way of calling prethink
preThink();
SAVE_STATE(anim_top);
SAVE_STATE(anim_top_delay);
SAVE_STATE(anim_top_time);
SAVE_STATE(anim_bottom);
SAVE_STATE(anim_bottom_time);
SAVE_STATE(iState);
SAVE_STATE(inventoryEquippedIndex);
SAVE_STATE(weaponEquippedAkimbo);
SAVE_STATE(w_attack_akimbo_next);
SAVE_STATE(isReloading);
SAVE_STATE(isChangingIronsight);
//SAVE_STATE(flZoomTarget);
SAVE_STATE(iZoomLevel);
SAVE_STATE(nextAkimboAttackPreference);
SAVE_STATE(akimboDualFireToleranceTime);
SAVE_STATE(grenadeFireIndex);
SAVE_STATE(bGrenadeToss);
//flKarateBlockCooldown
SAVE_STATE(iMeleeCycler);
SAVE_STATE(vViewAngleOffsetTargetDir);
SAVE_STATE(vViewAngleOffsetTotalChange);
SAVE_STATE(flViewAngleOffsetTarget);
SAVE_STATE(fAccuracyKickback);
SAVE_STATE(fAccuracyKickbackStartCooldown);
SAVE_STATE(fMoveBlockDelay);
SAVE_STATE(fUncrouchBlockDelay);
SAVE_STATE(fKarateStamina);
SAVE_STATE(shotgunReloadIndex);
SAVE_STATE(shotgunAddAmmoTime);
SAVE_STATE(shotgunAddAmmoSoundTime);
SAVE_STATE(ary_myWeapons_softMax);
for(int i = 0; i < ary_myWeapons_softMax; i++){
SAVE_STATE(ary_myWeapons[i].weaponID);
SAVE_STATE(ary_myWeapons[i].weaponTypeID);
SAVE_STATE(ary_myWeapons[i].iBitsUpgrade);
SAVE_STATE(ary_myWeapons[i].iCount);
//SAVE_STATE(ary_myWeapons[i].iPrice);
//SAVE_STATE(ary_myWeapons[i].iSlots);
SAVE_STATE(ary_myWeapons[i].iClipLeft);
SAVE_STATE(ary_myWeapons[i].iClipAkimboLeft);
SAVE_STATE(ary_myWeapons[i].iBitsUpgrade_on);
SAVE_STATE(ary_myWeapons[i].iFireMode);
SAVE_STATE(ary_myWeapons[i].iFireModeAkimbo);
SAVE_STATE(ary_myWeapons[i].iIronSight);
SAVE_STATE(ary_myWeapons[i].iForceBodygroup1Submodel);
SAVE_STATE(ary_myWeapons[i].bNeedsPump);
}
//UNNECESSARY. This array is of fixed length, so known at all times.
//WriteByte(MSG_ENTITY, ary_ammoTotal_softMax);
for(int i = 0; i < AMMO_ID::LAST_ID; i++){
// See serverside equivalent, too much info was lost from some pools being over 255
// (well I guess that's all there is to it)
SAVE_STATE_ARY(ary_ammoTotal, i);
}
// viewzoom must be set by the end of PrePredict or else it will not
// reach the Nuclide logic looking at it in time to be applied.
// No need here now I think?
//this.viewzoom = this.flZoomCurrent;
#if OTHER_PREDICTION_TEST == 1
Custom_Predict_PlayerPreFrame(this);
#endif
}
/*
=================
player::PredictPostFrame
Where we roll back our values to the ones last sent/verified by the server.
=================
*/
void
player::PredictPostFrame(void)
{
//printfline("---PREDICT POST FRAME");
sendevent("ViewOffsetR", "fff", vViewAngleOffsetTotalChange[0], vViewAngleOffsetTotalChange[1], vViewAngleOffsetTotalChange[2]);
/* the generic client attributes */
base_player::PredictPostFrame();
// client's way of calling postthink
postThink();
ROLL_BACK(anim_top);
ROLL_BACK(anim_top_delay);
ROLL_BACK(anim_top_time);
ROLL_BACK(anim_bottom);
ROLL_BACK(anim_bottom_time);
ROLL_BACK(iState);
ROLL_BACK(inventoryEquippedIndex);
ROLL_BACK(weaponEquippedAkimbo);
ROLL_BACK(w_attack_akimbo_next);
ROLL_BACK(isReloading);
ROLL_BACK(isChangingIronsight);
//ROLL_BACK(flZoomTarget);
ROLL_BACK(iZoomLevel);
ROLL_BACK(nextAkimboAttackPreference);
ROLL_BACK(akimboDualFireToleranceTime);
ROLL_BACK(grenadeFireIndex);
ROLL_BACK(bGrenadeToss);
//flKarateBlockCooldown
ROLL_BACK(iMeleeCycler);
ROLL_BACK(vViewAngleOffsetTargetDir);
ROLL_BACK(vViewAngleOffsetTotalChange);
ROLL_BACK(flViewAngleOffsetTarget);
ROLL_BACK(fAccuracyKickback);
ROLL_BACK(fAccuracyKickbackStartCooldown);
ROLL_BACK(fMoveBlockDelay);
ROLL_BACK(fUncrouchBlockDelay);
ROLL_BACK(fKarateStamina);
ROLL_BACK(shotgunReloadIndex);
ROLL_BACK(shotgunAddAmmoTime);
ROLL_BACK(shotgunAddAmmoSoundTime);
ROLL_BACK(ary_myWeapons_softMax);
for(int i = 0; i < ary_myWeapons_softMax; i++){
ROLL_BACK(ary_myWeapons[i].weaponID);
ROLL_BACK(ary_myWeapons[i].weaponTypeID);
ROLL_BACK(ary_myWeapons[i].iBitsUpgrade);
ROLL_BACK(ary_myWeapons[i].iCount);
//ROLL_BACK(ary_myWeapons[i].iPrice);
//ROLL_BACK(ary_myWeapons[i].iSlots);
ROLL_BACK(ary_myWeapons[i].iClipLeft);
ROLL_BACK(ary_myWeapons[i].iClipAkimboLeft);
ROLL_BACK(ary_myWeapons[i].iBitsUpgrade_on);
ROLL_BACK(ary_myWeapons[i].iFireMode);
ROLL_BACK(ary_myWeapons[i].iFireModeAkimbo);
ROLL_BACK(ary_myWeapons[i].iIronSight);
ROLL_BACK(ary_myWeapons[i].iForceBodygroup1Submodel);
ROLL_BACK(ary_myWeapons[i].bNeedsPump);
}
// UNNECESSARY. This array is of fixed length, so known at all times.
//WriteByte(MSG_ENTITY, ary_ammoTotal_softMax);
for(int i = 0; i < AMMO_ID::LAST_ID; i++){
// See serverside equivalent, too much info was lost from some pools being over 255
// (well I guess that's all there is to it)
ROLL_BACK_ARY(ary_ammoTotal, i);
}
#if OTHER_PREDICTION_TEST == 1
Custom_Predict_PlayerPostFrame(this);
#endif
}
#else
void
player::EvaluateEntity(void)
{
/* the generic client attributes */
base_player::EvaluateEntity();
int i;
int myAmmoType;
int betterID;
/* animation */
if (ATTR_CHANGED(anim_bottom) || ATTR_CHANGED(anim_bottom_time))
SendFlags |= PLAYER_BOTTOMFRAME;
if (ATTR_CHANGED(anim_top) || ATTR_CHANGED(anim_top_time) || ATTR_CHANGED(anim_top_delay))
SendFlags |= PLAYER_TOPFRAME;
// IDEA: do a full-send, all weapons stats on executing a buyconfig order?
// set some flag to convey that?
SAVE_STATE(ary_myWeapons_softMax);
i = inventoryEquippedIndex;
if(i != -1){
//for(i = 0; i < ary_myWeapons_softMax; i++){
if(ATTR_CHANGED(ary_myWeapons[i].weaponID)) SendFlags |= PLAYER_UNUSED1;
if(ATTR_CHANGED(ary_myWeapons[i].weaponTypeID)) SendFlags |= PLAYER_UNUSED1;
if(ATTR_CHANGED(ary_myWeapons[i].iBitsUpgrade))SendFlags |= PLAYER_UNUSED1;
if(ATTR_CHANGED(ary_myWeapons[i].iCount)) SendFlags |= PLAYER_UNUSED1;
//SAVE_STATE(ary_myWeapons[i].iPrice));
//SAVE_STATE(ary_myWeapons[i].iSlots));
if(ATTR_CHANGED(ary_myWeapons[i].iClipLeft)) SendFlags |= PLAYER_UNUSED1;
if(ATTR_CHANGED(ary_myWeapons[i].iClipAkimboLeft)) SendFlags |= PLAYER_UNUSED1;
if(ATTR_CHANGED(ary_myWeapons[i].iBitsUpgrade_on)) SendFlags |= PLAYER_UNUSED1;
if(ATTR_CHANGED(ary_myWeapons[i].iFireMode)) SendFlags |= PLAYER_UNUSED1;
if(ATTR_CHANGED(ary_myWeapons[i].iFireModeAkimbo)) SendFlags |= PLAYER_UNUSED1;
if(ATTR_CHANGED(ary_myWeapons[i].iIronSight)) SendFlags |= PLAYER_UNUSED1;
if(ATTR_CHANGED(ary_myWeapons[i].iForceBodygroup1Submodel)) SendFlags |= PLAYER_UNUSED1;
if(ATTR_CHANGED(ary_myWeapons[i].bNeedsPump)) SendFlags |= PLAYER_UNUSED1;
//}
//myAmmoType = getAmmoTypeOfWeapon(this.activeweapon);
betterID = ary_myWeapons[inventoryEquippedIndex].weaponID;
myAmmoType = getAmmoTypeOfWeapon(betterID);
if(myAmmoType != -1){
i = myAmmoType;
//UNNECESSARY. This array is of fixed length, so known at all times.
//WriteByte(MSG_ENTITY, ary_ammoTotal_softMax);
//for(i = 0; i < AMMO_ID::LAST_ID; i++){
// See serverside equivalent, too much info was lost from some pools being over 255
// (well I guess that's all there is to it)
if(ATTR_CHANGED_ARY(ary_ammoTotal, i)) SendFlags |= PLAYER_UNUSED1;
//}
}
}
// iZoomLevel or flZoomTarget ?
if(
//ATTR_CHANGED(flZoomTarget)
//||
ATTR_CHANGED(iZoomLevel)
){
SendFlags |= PLAYER_UNUSED2;
}
SAVE_STATE(anim_top);
SAVE_STATE(anim_top_delay);
SAVE_STATE(anim_top_time);
SAVE_STATE(anim_bottom);
SAVE_STATE(anim_bottom_time);
SAVE_STATE(iState);
SAVE_STATE(inventoryEquippedIndex);
SAVE_STATE(weaponEquippedAkimbo);
SAVE_STATE(w_attack_akimbo_next);
SAVE_STATE(isReloading);
SAVE_STATE(isChangingIronsight);
//SAVE_STATE(flZoomTarget);
SAVE_STATE(iZoomLevel);
SAVE_STATE(nextAkimboAttackPreference);
SAVE_STATE(akimboDualFireToleranceTime);
SAVE_STATE(grenadeFireIndex);
SAVE_STATE(bGrenadeToss);
//flKarateBlockCooldown
SAVE_STATE(iMeleeCycler);
SAVE_STATE(vViewAngleOffsetTargetDir);
SAVE_STATE(vViewAngleOffsetTotalChange);
SAVE_STATE(flViewAngleOffsetTarget);
SAVE_STATE(fAccuracyKickback);
SAVE_STATE(fAccuracyKickbackStartCooldown);
SAVE_STATE(fMoveBlockDelay);
SAVE_STATE(fUncrouchBlockDelay);
SAVE_STATE(fKarateStamina);
SAVE_STATE(shotgunReloadIndex);
SAVE_STATE(shotgunAddAmmoTime);
SAVE_STATE(shotgunAddAmmoSoundTime);
SAVE_STATE(ary_myWeapons_softMax);
i = inventoryEquippedIndex;
if(i != -1){
//for(i = 0; i < ary_myWeapons_softMax; i++){
SAVE_STATE(ary_myWeapons[i].weaponID);
SAVE_STATE(ary_myWeapons[i].weaponTypeID);
SAVE_STATE(ary_myWeapons[i].iBitsUpgrade);
SAVE_STATE(ary_myWeapons[i].iCount);
//SAVE_STATE(ary_myWeapons[i].iPrice);
//SAVE_STATE(ary_myWeapons[i].iSlots);
SAVE_STATE(ary_myWeapons[i].iClipLeft);
SAVE_STATE(ary_myWeapons[i].iClipAkimboLeft);
SAVE_STATE(ary_myWeapons[i].iBitsUpgrade_on);
SAVE_STATE(ary_myWeapons[i].iFireMode);
SAVE_STATE(ary_myWeapons[i].iFireModeAkimbo);
SAVE_STATE(ary_myWeapons[i].iIronSight);
SAVE_STATE(ary_myWeapons[i].iForceBodygroup1Submodel);
SAVE_STATE(ary_myWeapons[i].bNeedsPump);
//}
//myAmmoType = getAmmoTypeOfWeapon(this.activeweapon);
betterID = ary_myWeapons[inventoryEquippedIndex].weaponID;
myAmmoType = getAmmoTypeOfWeapon(betterID);
if(myAmmoType != -1){
i = myAmmoType;
//UNNECESSARY. This array is of fixed length, so known at all times.
//WriteByte(MSG_ENTITY, ary_ammoTotal_softMax);
//for(i = 0; i < AMMO_ID::LAST_ID; i++){
// See serverside equivalent, too much info was lost from some pools being over 255
// (well I guess that's all there is to it)
SAVE_STATE_ARY(ary_ammoTotal, i);
//}
}
}
#if OTHER_PREDICTION_TEST == 1
Custom_EvaluateEntity(this);
#endif
}
void CSEv_ViewAnglesR_fff(float arg_x, float arg_y, float arg_z){
player pl = (player)self;
pl.v_angle.x = arg_x;
pl.v_angle.y = arg_y;
pl.v_angle.z = arg_z;
pl.vViewAngleOffsetTotalChangeAlt = [0,0,0];
}
void CSEv_ViewOffsetR_fff(float arg_x, float arg_y, float arg_z){
player pl = (player)self;
pl.vViewAngleOffsetTotalChangeAlt.x = arg_x;
pl.vViewAngleOffsetTotalChangeAlt.y = arg_y;
pl.vViewAngleOffsetTotalChangeAlt.z = arg_z;
//pl.vViewAngleOffsetTotalChangeAlt = [0,0,0];
}
/*
=================
player::SendEntity
=================
*/
float
player::SendEntity(entity ePEnt, float fChanged)
{
/* remove our entity to other clients if we're dead */
if (health <= 0 && ePEnt != this) {
return (0);
}
/* target client isn't real, they have no client-side. abandon */
if (clienttype(ePEnt) != CLIENTTYPE_REAL) {
return (0);
}
WriteByte(MSG_ENTITY, ENT_PLAYER);
WriteFloat(MSG_ENTITY, fChanged);
/* the generic client attributes */
base_player::SendEntity(ePEnt, fChanged);
int i;
if (fChanged & PLAYER_TOPFRAME) {
WriteByte(MSG_ENTITY, anim_top);
WriteFloat(MSG_ENTITY, anim_top_time);
WriteFloat(MSG_ENTITY, anim_top_delay);
}
if (fChanged & PLAYER_BOTTOMFRAME) {
WriteByte(MSG_ENTITY, anim_bottom);
WriteFloat(MSG_ENTITY, anim_bottom_time);
}
WriteFloat(MSG_ENTITY, flViewModelFrame);
// TODO MAJOR!!!
// Only send updates if we shuffled the weapons around (update everything, all weapons),
// OR if a particular weapon states to update its features, perhaps sending itself
// through
WriteFloat(MSG_ENTITY, iState);
WriteByte(MSG_ENTITY, inventoryEquippedIndex );
WriteByte(MSG_ENTITY, weaponEquippedAkimbo );
WriteFloat(MSG_ENTITY, w_attack_akimbo_next );
WriteByte(MSG_ENTITY, isReloading );
WriteByte(MSG_ENTITY, isChangingIronsight );
if(fChanged & PLAYER_UNUSED2){
//WriteFloat(MSG_ENTITY, flZoomTarget);
WriteByte(MSG_ENTITY, iZoomLevel);
}
WriteByte(MSG_ENTITY, nextAkimboAttackPreference);
WriteFloat(MSG_ENTITY, akimboDualFireToleranceTime);
WriteByte(MSG_ENTITY, grenadeFireIndex + 1);
WriteByte(MSG_ENTITY, bGrenadeToss);
WriteByte(MSG_ENTITY, armor );
WriteByte(MSG_ENTITY, iTotalSlots );
//flKarateBlockCooldown
WriteByte(MSG_ENTITY, iMeleeCycler);
WriteCoord(MSG_ENTITY, vViewAngleOffsetTotalChange[0] );
WriteCoord(MSG_ENTITY, vViewAngleOffsetTotalChange[1] );
WriteCoord(MSG_ENTITY, vViewAngleOffsetTotalChange[2] );
vViewAngleOffsetTotalChange = [0,0,0];
WriteFloat(MSG_ENTITY, fAccuracyKickback);
WriteFloat(MSG_ENTITY, fAccuracyKickbackStartCooldown);
WriteFloat(MSG_ENTITY, fMoveBlockDelay);
WriteFloat(MSG_ENTITY, fUncrouchBlockDelay);
WriteFloat(MSG_ENTITY, fKarateStamina);
WriteByte(MSG_ENTITY, shotgunReloadIndex );
WriteFloat(MSG_ENTITY, shotgunAddAmmoTime );
WriteFloat(MSG_ENTITY, shotgunAddAmmoSoundTime );
//weapondynamic_t ary_myWeapons[ary_myWeapons_length];
WriteByte(MSG_ENTITY, ary_myWeapons_softMax );
WriteByte(MSG_ENTITY, this.completeInventorySend );
#ifdef FORCE_NETWORK_ALL_INVENTORY
completeInventorySend = TRUE;
#endif
if(this.completeInventorySend){
for(i = 0; i < ary_myWeapons_softMax; i++){
SendEntity_ary_myWeapons(i);
}
for(i = 0; i < AMMO_ID::LAST_ID; i++){
WriteLong(MSG_ENTITY, ary_ammoTotal[i] );
}
}else if(fChanged & PLAYER_UNUSED1){
i = (int)inventoryEquippedIndex;
WriteByte(MSG_ENTITY, i + 1);
/// could this ever be -1?
if(i != -1){
SendEntity_ary_myWeapons(i);
//int myAmmoType = getAmmoTypeOfWeapon(this.activeweapon);
int betterID = ary_myWeapons[inventoryEquippedIndex].weaponID;
int myAmmoType = getAmmoTypeOfWeapon(betterID);
if(myAmmoType != -1){
i = myAmmoType;
//UNNECESSARY. This array is of fixed length, so known at all times.
//WriteByte(MSG_ENTITY, ary_ammoTotal_softMax);
//for(i = 0; i < AMMO_ID::LAST_ID; i++){
// using 'WriteLong' instead of 'WriteByte', because SOME AMMO POOL just had to
// exceed 255, didn't it.
WriteLong(MSG_ENTITY, ary_ammoTotal[i] );
//}
}
}
}
// no need to send again until this flag is set freshly.
this.completeInventorySend = FALSE;
return (1);
}
void player::SendEntity_ary_myWeapons(int i){
WriteByte(MSG_ENTITY, ary_myWeapons[i].weaponID );
WriteByte(MSG_ENTITY, ary_myWeapons[i].weaponTypeID );
WriteByte(MSG_ENTITY, ary_myWeapons[i].iBitsUpgrade );
WriteByte(MSG_ENTITY, ary_myWeapons[i].iCount );
//WriteByte(MSG_ENTITY, ary_myWeapons[i].iPrice );
//WriteByte(MSG_ENTITY, ary_myWeapons[i].iSlots );
WriteByte(MSG_ENTITY, ary_myWeapons[i].iClipLeft );
WriteByte(MSG_ENTITY, ary_myWeapons[i].iClipAkimboLeft );
#if OTHER_PREDICTION_TEST != 1
WriteByte(MSG_ENTITY, ary_myWeapons[i].iBitsUpgrade_on );
#endif
WriteByte(MSG_ENTITY, ary_myWeapons[i].iFireMode );
WriteByte(MSG_ENTITY, ary_myWeapons[i].iFireModeAkimbo );
WriteByte(MSG_ENTITY, ary_myWeapons[i].iIronSight );
WriteByte(MSG_ENTITY, ary_myWeapons[i].iForceBodygroup1Submodel);
WriteByte(MSG_ENTITY, ary_myWeapons[i].bNeedsPump);
}
#endif
void
player::player(void){
printfline("CONSTRUCTOR: player, called");
// just in case?
this.classname = "player";
// reasonable default?
iState = PLAYER_STATE::NOCLIP;
completeInventorySend = FALSE;
#ifdef SERVER
money = 0; //safety?
//this.think = player_frameThink;
//this.nextthink = time + 0.0;
#endif
ary_myWeapons_softMax = 0;
for(int i = 0; i < ary_myWeapons_length; i++){
ary_myWeapons[i] = spawn(weapondynamic_t);
}
#ifdef CLIENT
weaponSelectHighlightID = -1;
weaponSelectHighlightAkimbo = FALSE;
flag_lastWeaponAkimbo = FALSE;
#endif
flKarateBlockCooldown = 0;
// Should we?
//setInventoryEquippedIndex(-1);
inventoryEquippedIndex_previous = -1;
weaponEquippedAkimbo = FALSE;
//safe default.
iTeam = TS_Team::TEAM_NONE;
reset(TRUE); //cover anything specified in here too
}// player constructor
// use TRUE for clean respawns,
// FALSE for only resetting some weapons-related vars between weapon changes, nothing too
// significant
void
player::reset(BOOL resetInventory){
INPUT_TAP_RESET(this)
// should this even make any assumptions about this?
//iState = ?;
resetZoom();
//this.flViewAngleOffsetLerp = 1.0f;
#ifdef CLIENT
// For safety, doing this.
// If anywhere that ever calls for this method clientside has this set
// properly before the call to here, (whatever event-method in base files leads
// to here most likely), this is not necessary.
// Also, no need for pSeatLocal yet, but add in if that changes.
int s = (float)getproperty(VF_ACTIVESEAT);
pSeat = &g_seats[s];
pSeat->m_flHUDWeaponSelectTime = -1;
// we don't want damage related to the death / between spawns carrying over
// at the start of the next spawn.
pSeat->m_flDamageAlpha = 0;
// doing this in reset too to avoid a little glitch.
weaponSelectHighlightID = -1;
w_freeze_idle_next = -1;
flRecentGroundTime = 0;
flViewShake = 0;
recentLaserHitPosSet = FALSE;
lasersightUnlockTime = FALSE;
forceViewModelUpdate = FALSE;
prev_iForceBodygroup1Submodel = 0;
#endif
#ifdef SERVER
switchToRecentlyAddedWeapon = FALSE;
nextUseCooldown = 0;
#endif
// Shared, but don't network! I think?
aryNextBurstShotTime_softLength = 0;
aryNextBurstShotTime_listenIndex = -1;
iMeleeCycler = 0;
//shotgun stuff.
//NOTICE - all networked now, testing.
shotgunReloadIndex = 0;
shotgunAddAmmoTime = -1;
shotgunAddAmmoSoundTime = -1;
//Grenade stuff
grenadeFireIndex = -1;
grenadeHeldDuration = -1;
grenadeSpawnTime = -1;
bGrenadeToss = FALSE;
isReloading = FALSE;
isChangingIronsight = FALSE;
nextAkimboAttackPreference = BITS_AKIMBOCHOICE_LEFT;
recentAttackHadAmmo = FALSE;
// don't reset the money. Carries between spawns. I think?
// TODO - FIGURE THIS OUT, for gamemodes that involve money.
// do a check. If we didn't die last time, we don't need to delete this stuff.
// is this good? See that this reliable to check at the time "reset" is called in the
// spawning method!
// or just send a parameter here (preserve weapons: yes/no BOOL) to determine that
// instead.
if(resetInventory){
#ifdef CLIENT
//TAGGG - BobNEW.
pViewBob = &g_viewBobVars[s];
pViewBob_reset();
#endif
// do this at spawn too. oh right, this 'reset' method is called from there.
//vViewAngleOffsetTarget = [0, 0, 0];
flViewAngleOffsetTarget = 0;
fAccuracyKickback = 0;
fAccuracyKickbackStartCooldown = -1;
fMoveBlockDelay = 0;
fUncrouchBlockDelay = 0;
fMoveBlockCapSpeed = 0;
fKarateStamina = 1.0;
flBaseSpeedMulti = 1;
flMoveSpeedMulti = 1;
flSoundSpeedMulti = 1;
flFireDelayMulti = 1;
flBulletSpeedMulti = 1;
flProjectileSpeedMulti = 1;
// could this be done for resets outside of resetInventory? unsure
flags = 0;
gflags = 0;
// sure? Not used for this game though.
g_items = 0x0;
printfline("setInventoryEquippedIndex: FLAG A");
setInventoryEquippedIndex(-1);
#ifdef CLIENT
equippedWeaponWaitingForCallback = FALSE;
#endif
#ifdef SERVER
// other projects do that so sure?
weapon = 0;
// TODO - is doing this in here a good idea? decide.
WriteByte( MSG_MULTICAST, SVC_CGAMEPACKET );
WriteByte( MSG_MULTICAST, EVENT_TS::RESET_VIEW_MODEL );
msg_entity = this;
multicast( [0,0,0], MULTICAST_ONE );
#endif
// effectively says, no weapons. Any other info in there is to be overwritten as
// needed.
ary_myWeapons_softMax = 0;
//All ammo counters, goodbye.
for(int i = 0; i < AMMO_ID::LAST_ID; i++){
ary_ammoTotal[i] = 0; //safe default
}
armor = 0;
iTotalSlots = 0;
iTotalPrice = 0;
}
}//reset
// little test
//var float input_timelengthSum = 0;
// Count down my custom timers, called by ts/src/shared/input.qc, which is called
// by Nuclide's PMove, same place that counts down w_attack_next.
// Best to put things here that involve input_timelength.
void
player::updateTimers(void){
// good place for this? Cloned from w_attack_next as a separate counter
// for akimbo firing to use.
w_attack_akimbo_next = max(0, w_attack_akimbo_next - input_timelength);
akimboDualFireToleranceTime = max(0, akimboDualFireToleranceTime - input_timelength);
fMoveBlockDelay = max(0, fMoveBlockDelay - input_timelength);
fUncrouchBlockDelay = max(0, fUncrouchBlockDelay - input_timelength);
/*
// little test. See input_timelengthSum shoot up with packet latency.
input_timelengthSum += input_timelength;
if(otherTimer >= 1.0){
otherTimer -= 1.0;
printfline("input_timelengthSum: %.2f", input_timelengthSum);
input_timelengthSum = 0;
}
*/
fAccuracyKickbackStartCooldown = max(0, fAccuracyKickbackStartCooldown - input_timelength);
if(fKarateStamina < 1.0){
//fKarateStamina = bound(0, fKarateStamina + frametime * 0.1667, 1);
fKarateStamina = bound(0, fKarateStamina + input_timelength * 0.1667, 1);
}
//printfline("KARATE STAMINA? %.2f", fKarateStamina);
if(fAccuracyKickbackStartCooldown != -1){
if(fAccuracyKickbackStartCooldown <= 0){
// begin reducing
//fAccuracyKickback -= frametime * 0.1;
fAccuracyKickback = max(0, fAccuracyKickback - input_timelength);
if(fAccuracyKickback <= 0){
// stop!
fAccuracyKickbackStartCooldown = -1;
}
}
}//kickbackStartCooldown check
/*
#ifdef CLIENT
#define my_frametime clframetime
#else
#define my_frametime frametime
#endif
*/
#define my_frametime input_timelength
//vector viewChange;
float flViewChange;
vector vViewChangeDir;
if(flViewAngleOffsetTarget <= 0.02){
flViewChange = this.flViewAngleOffsetTarget;
vViewChangeDir = this.vViewAngleOffsetTargetDir;
this.flViewAngleOffsetTarget = 0;
}else{
float lenOfVAOT = this.flViewAngleOffsetTarget;
float amountToChangeBy = this.flViewAngleOffsetTarget * 36 * my_frametime;
float amountToReduceBy = this.flViewAngleOffsetTarget * 18 * my_frametime;
//printfline("COMPARISON? cb:%.2f rb:%.2f - total:%.2f", amountToChangeBy, amountToReduceBy, lenOfVAOT);
// if changing the vViewOffsetTarget by this much would
// go past the offset into pushing it past what was intended,
// don't allow that! Might not even be possible
if(amountToReduceBy >= lenOfVAOT){
flViewChange = this.flViewAngleOffsetTarget;
vViewChangeDir = this.vViewAngleOffsetTargetDir;
this.flViewAngleOffsetTarget = 0;
}else{
this.flViewAngleOffsetTarget -= amountToReduceBy;
flViewChange = amountToChangeBy;
vViewChangeDir = this.vViewAngleOffsetTargetDir;
}
}
vector finalChange = flViewChange * vViewChangeDir;
this.vViewAngleOffsetTotalChange += finalChange;
this.vViewAngleOffsetTotalChangeAlt += finalChange;
// test? main one
#ifndef CLIENT
GET_MY_VIEW_ANGLES = GET_MY_VIEW_ANGLES + finalChange;
#else
#endif
}//updateTimers
// add kickback that pushes the camera by some angle.
// Cumulative with existing kickback not yet finished being applied.
void player::addViewAngleOffset(float arg_ang, float arg_len){
/*
#ifdef CLIENT
printfline("--- %d - %d", input_sequence, tempQueue2[input_sequence % 512]);
if(!tempQueue2[input_sequence % 512]){
return;
}else{
tempQueue2[input_sequence % 512] = FALSE;
}
printfline("!!! addViewAngleOffset !!! %d", input_sequence);
#endif
*/
if(flViewAngleOffsetTarget == 0){
// easy, replace completely
flViewAngleOffsetTarget = arg_len;
vViewAngleOffsetTargetDir = [-sin(arg_ang), cos(arg_ang), 0];
}else{
// Have to unpack the vector, add it, and re-do the split value/dir way.
// Might be a smarter way to do that math-wise but this works for now
vector tempVec = flViewAngleOffsetTarget * vViewAngleOffsetTargetDir;
vector newVec = arg_len * [-sin(arg_ang), cos(arg_ang), 0];
tempVec = tempVec + newVec;
flViewAngleOffsetTarget = vlen(tempVec);
if(flViewAngleOffsetTarget != 0){
vViewAngleOffsetTargetDir = tempVec / flViewAngleOffsetTarget;
}else{
// what.
// Nothing to do, leave flViewAngleOffsetTarget at 0 to be ignored
}
}
}
#ifdef CLIENT
// client only, check some other input-related CVars.
void
player::clientInputUpdate(void){
//printfline("---clientInputUpdate");
}
void
player::clientUseItemsCallback(void){
}
// - initially from The Wastes.
void player::handleZoom(void)
{
if(this.iState != PLAYER_STATE::SPAWNED){
// zoom is only allowed for ingame.
return;
}
// test
/*
this.flZoomCurrent = this.flZoomTarget;
this.viewzoom = this.flZoomTarget;
printfline("flZoomTarget: %.2f", this.flZoomTarget);
return;
*/
//printfline("WHATS GOING ON %.2f %.2f %.2f %.2f %.2f", this.flZoomEnd, this.flZoomTarget, this.flZoomStart, this.flZoomLerp, this.flZoomCurrent);
// - No other codebase refers to STAT_VIEWZOOM, best to replace anything
// involving that.
// in case of some unexpected change.
// TODO: this might have an issue if the player were to change weapons and end up
// on the same zoom level as a previous weapon, despite the two weapons having
// different target values (like 0.4 vs. 0.6) at their own zoom level #1's.
// A check for being a different equipped weapon type
// (this.activeweapon vs. this.activeweapon_zoomprev, set only by setZoomLevel)
// ought to work. But that would be a very strange case anyway
if(this.iZoomLevel != this.iZoomLevelPrev){
this.setZoomLevel(this.iZoomLevel);
}
if (this.flZoomEnd != this.flZoomTarget ) {
//printfline("NEW ZOOM DETECTED: %.2f -> %.2f (zoomlev: %i)", this.flZoomEnd, this.flZoomTarget, this.iZoomLevel);
// is setting flZoomStart to flZoomEnd or flZoomCurrent a better idea?
// at flZoomEnd means, on changing before the current lerp has finished, it jumps to beginning
// at the end point of the lerp in progress. But starting at flZoomCurrent might be smoother?
// if it isn't glitchy that could be nice. Trying that out.
this.flZoomStart = this.flZoomCurrent;
//this.flZoomStart = this.flZoomEnd;
this.flZoomEnd = this.flZoomTarget;
this.flZoomLerp = 0.0f;
}
if (this.flZoomLerp < 1.0f) {
// Slow this down for debugging purposes, this is meant to be fast.
// 0.8 can be safe.
// The Wastes default was 4.
// !!! clframetime, frametime, or input_timelength ?
this.flZoomLerp += clframetime * 5.7;
if(this.flZoomLerp >= 1.0){
//enforce the cap.
this.flZoomLerp = 1.0;
}
//this.flZoomCurrent = getstatf(STAT_VIEWZOOM);
this.flZoomCurrent = Math_Lerp(this.flZoomStart, this.flZoomEnd, this.flZoomLerp);
}
// Set this, since Nuclide will read it in and apply instantly.
// So same effect without having to edit Nuclide, this pipes it over for it
// to do the setproperty thing below to apply
//this.viewzoom = this.flZoomCurrent;
////setproperty(VF_AFOV, DEFAULT_FOV * this.flZoomCurrent);
////setsensitivityscaler(this.flZoomCurrent);
// here's the way old FreeCS did it
//setproperty(VF_AFOV, cvar("fov") * this.viewzoom);
//setsensitivityscaler(this.viewzoom);
}// TS_View_HandleZoom
#endif
void
player::setZoomLevel(int arg_iZoomLevel){
iZoomLevel = arg_iZoomLevel;
#ifdef CLIENT
iZoomLevelPrev = iZoomLevel;
#endif
weapondata_basic_t* myDat = getEquippedWeaponData();
if(myDat != NULL){
if(myDat.funOnSetZoomLevel != NULL){
myDat.funOnSetZoomLevel(this);
}
}
}
// forget any zoom-related settings instantly.
// Unsure if a message should be sent to the client if called from the server, or assume the
// calls coincide.
void
player::resetZoom(void)
{
iZoomLevel = 0;
viewzoom = 1.0f;
flZoomTarget = 1.0f;
#ifdef CLIENT
// IMPORTANT! ZoomLerp under 0 tries to involve flZoomOld, set or not
flZoomLerp = 1.0f;
/*
SAVE_STATE(iZoomLevel);
SAVE_STATE(viewzoom);
//SAVE_STATE(flZoomTarget);
*/
flZoomEnd = 1.0f;
flZoomCurrent = 1.0f;
#endif
}
// similar as above, but still go for the transition smoothness,
// not an instant change
void
player::resetZoomSoft(void)
{
iZoomLevel = 0;
flZoomTarget = 1.0f;
}
void
player::setInventoryEquippedIndex(int arg_newIndex)
{
setInventoryEquippedIndex_Akimbo(arg_newIndex, FALSE);
}
// Extra version supplied 'useAkimbo'.
void
player::setInventoryEquippedIndex_Akimbo(int arg_newIndex, BOOL useAkimbo)
{
printfline("setInventoryEquippedIndex_Akimbo: %i, %d", arg_newIndex, useAkimbo);
inventoryEquippedIndex = arg_newIndex;
if(arg_newIndex != -1){
// Let Nuclide be aware what element of g_weapons is relevant (set activeweapon).
// all we need is dynaRef.weaponID
weapondynamic_t dynaRef = ary_myWeapons[arg_newIndex];
weapondata_basic_t* basicP = (weapondata_basic_t*) ary_weaponData[dynaRef.weaponID];
//weapondata_basic_t basicRef = *(basicP);
//printfline("WELL. %d", useAkimbo);
//printfline("setInventoryEquippedIndex - newInvIndex:%i weaponID:%i\n", arg_newIndex, dynaRef.weaponID);
int myAkimboUpgradeID = (*ary_weaponData[dynaRef.weaponID]).iAkimboID;
// If I don't intend to use the akimbo version, the upgrade ID to get there is bad,
// or this weapon does not support akimbo, or simply isn't right now, do singular.
if(
!useAkimbo ||
myAkimboUpgradeID <= 0 ||
!((dynaRef.iBitsUpgrade & BITS_WEAPONOPT_AKIMBO) && ((*basicP).iBitsUpgrade & BITS_WEAPONOPT_AKIMBO))
){
weaponEquippedAkimbo = FALSE;
activeweapon = (float)dynaRef.weaponID;
activeweapon_singular = (float)dynaRef.weaponID;
}else{
weaponEquippedAkimbo = TRUE;
activeweapon = (float)ary_AKIMBO_UPGRADE_TO_WEAPON[myAkimboUpgradeID];
activeweapon_singular = (float)dynaRef.weaponID;
}
}else{
// it was -1? activeweapon must be 0, the 'none' choice.
activeweapon = 0;
activeweapon_singular = 0;
weaponEquippedAkimbo = FALSE; // I guess?
}
// - OLD COMMENT
// NOTE, IMPORTANT.
// Is this necessary?
// Makes a decent example for paying attention to SAVE and ROLLBACKs in player script
// (see activeweapon SAVE_STATE/ROLLBACK's in Nuclide's src/shared/player.qc).
// ALSO - for this demo to work, remove the karate call in 'deployConfig',
// so that having no weapons out leads to equipping the 'empty' weapon.
// When nothing is here instead (comment out), try throwing the last throwing knife.
// Often, you'll still hear the 'draw' noise because a rollback sets activeweapon back
// to the previous value, even though you want it to be nothing from having nothing
// equipped.
// But if this is enabled all the time (client and serverside), the weapon set by the
// server exclusively, such as player-spawn, does not get sent off..
// This seems to be the best case scenario: setting this clientside stops the rollback
// to undo setting the current equipped weapon to 'nothing' back to the throwing knife,
// which would not even be equipped anymore.
// If this is the wrong way to go thinking about this, I need to know more.
// ALSO - see other uses of SAVE_STATE, most are for both client & server,
// especially the w_attack_next, or w_next_attack, in ts/shared/weapons.qc.
/////////////////////////////////////
// - NEW COMMENTs
// Looks like we at least want to do SAVE_STATE(activeweapon) to stop a rollback
// from being undone that causes it to flicker back to what's about to be received.
// and we get 'iilegible server message' on very high pings.
// S T E L L A R . although it's happenin without this too.
// Although this can cause the weapon info to glich out, like a host of other stuff
// needs to be sent / know not to change just yet too I'm guessing, a clientside SAVE_STATE
// of all the next equipped weapon's stuff I'm guessing?
// Make a method and test, with equippedID set to the new
// (inventoryEquippedIndex and activeweapon as they are here)! TODO TODO TODO
// #ifdef CLIENT saveWeaponState(inventoryEquippedIndex) ... ary_myWeapons ... #endif
// And the network-crash is from ... not sending the entire inventory every single frame.
// ... Marvelous. But at least that's a completely separate issue, again with or without below.
// (force a sendoff, expect it area)
#ifdef SERVER
// Force a sendoff!
SendFlags |= PLAYER_WEAPON;
#else
SAVE_STATE(activeweapon);
// unsure if this one's needed
SAVE_STATE(inventoryEquippedIndex);
#endif
}
// get old FreeTS weapon info.
weapondata_basic_t*
player::getEquippedWeaponData(void){
//weapondynamic_t dynaRef;
//dynaRef = ary_myWeapons[inventoryEquippedIndex];
//dynaRef.weaponID
return getWeaponData((int)activeweapon, weaponEquippedAkimbo);
}
weapondata_basic_t*
player::getEquippedWeaponData_Singular(void){
//return getWeaponData_Singular(activeweapon_singular);
// no do this.
return ary_weaponData[activeweapon_singular];
}
// For choices other than the equipped weapon ID
weapondata_basic_t*
player::getInventoryWeaponData(int arg_invID, BOOL arg_preferAkimbo){
weapondynamic_t dynaRef;
dynaRef = ary_myWeapons[arg_invID];
return getWeaponData(dynaRef.weaponID, arg_preferAkimbo);
}
// As the akimbo versions of weapons are saved to g_weapons,
// the singular version may be desired sometimes.
weapondata_basic_t*
player::getInventoryWeaponData_Singular(int arg_invID){
weapondynamic_t dynaRef;
dynaRef = ary_myWeapons[arg_invID];
return getWeaponData_Singular(dynaRef.weaponID);
}
// Call me since changing a weapon's iCount by unusual means like throwing a weapon.
// Other methods, addWeaponToInventory and player::dropWeapon, either already call this
// or handle this on their own already.
// Should this be done for price too? I doubt there would be any benefit to preserving
// the value stored in weapons ingame.
void
player::updateSlotCountsForEquippedWeapon(void){
//weapondynamic_t arg_thisWeapon = ary_myWeapons[inventoryEquippedIndex];
weapondata_basic_t* basicP = getEquippedWeaponData();
// Record the old number of slots saved to the weapon at its iCount earlier
int iSlotsOld = ary_myWeapons[inventoryEquippedIndex].iSlots;
// Determine the new slot count from the (modified?) weapon's iCount.
int iSlotsNew = (*basicP).iSlots * ary_myWeapons[inventoryEquippedIndex].iCount;
ary_myWeapons[inventoryEquippedIndex].iSlots = iSlotsNew;
// Remove iSlotsOld, add iSlotsNew. If they match this means no change.
iTotalSlots = iTotalSlots - iSlotsOld + iSlotsNew;
}
// Returns whether the current weapon was removed.
// Mainly this exists to check if a weapon intended to be removed instead of dropped
// (count set to 0), but this handles deleting it from the player's inventory anyway.
BOOL
player::equippedWeaponDeleteCheck(void){
weapondynamic_t arg_thisWeapon = this.ary_myWeapons[this.inventoryEquippedIndex];
if(arg_thisWeapon.iCount == 0){
printfline("!!! Auto delete check, equipping a new weapon");
// remove this weapon, pick another one.
removeWeaponFromInventory(this, this.inventoryEquippedIndex);
playerEquipIdealSafe(this);
return TRUE;
}
return FALSE;
}
BOOL
player::inventoryWeaponDeleteCheck(int arg_invID){
weapondynamic_t arg_thisWeapon = this.ary_myWeapons[arg_invID];
if(arg_thisWeapon.iCount == 0){
// remove this weapon, pick another one.
removeWeaponFromInventory(this, arg_invID);
//if(arg_invID == inventoryEquippedIndex){
// playerEquipIdealSafe(this);
//}
return TRUE;
}
return FALSE;
}
void
player::callWeaponThink(void){
if(inventoryEquippedIndex != -1){
weapondynamic_t dynaRef = ary_myWeapons[inventoryEquippedIndex];
weapondata_basic_t* basicPointer = (weapondata_basic_t*) this.getEquippedWeaponData();
weapondata_basic_t basicRef = *(basicPointer);
basicRef.funOnThink(this, dynaRef);
}// weaponEquipped check
}
#ifdef SERVER
// (Comment may be out of date, verify if serverside postthink or setting think methods
// anywhere still has issues)
// runs every frame server-side. postthink, oddly enough, does not.
// Even cumulative 'frametime' readings do not at all add up to the real passage of time.
// Looks like we have to do this with server calling our think instead... it's own frame
// logic method does add up to something that resmebles the passage of time at least.
// The "frameThink_fromServer" further below is actually called from the server and runs
// every single logic frame of the server, as evidenced by its frametime's summing up to the
// passage of time.
// Strangely, setting the ".think" method of the player clientside, even with nextThink set,
// does nothing at all. Even though other clientside-programmed entities like the ts_powerup
// work fine with it... I. DONT. KNOW.
void
player::frameThink_fromServer(void){
// no, leave that to shared/input.qc
//updateTimers();
//preThinkShared();
}// frameThink_fromServer
#endif
// preThink & postThink are supporter client and serverside.
// They're not built-in methods per entity nor called by Nuclide.
// For clientside, preThink/postThink are called by prediction pre/post methods.
// For serverside, gamerules "Player(Pre/Post)Frame" events call these for every
// player.
// An above comment says there were problems with serverside postthink, but unsure
// if that is still the case.
#ifdef CLIENT
void
player::preThink(void){
weapondynamic_t dynaRef;
if(autocvar_cl_printoutspam == 1){
printfline("My state: %d", iState);
gFun_UI_EventGrabber_DebugMethod();
}
if(time >= equippedWeaponWaitingForCallback_maxWaitTime){
// stop then
equippedWeaponWaitingForCallback = FALSE;
}
//callWeaponThink();
//TAGGG - CRITICAL!!!
// Should the rest all be client-only anymore? See if it should be shared.
// TAGGG - TODO.
// maybe rename this to "viewModelAnimationDuration"
// and let "nextViewModelAnimationTime" be re-used if we ever support idle animations
// like HL does? doubt that really though.
// When a viewmodel's w_freeze_idle_next has expired, force to the frozen idle frame.
if(w_freeze_idle_next == 0){
w_freeze_idle_next = -1;
if(inventoryEquippedIndex != -1){
dynaRef = ary_myWeapons[inventoryEquippedIndex];
weapondata_basic_t* basePRef = this.getEquippedWeaponData();
weapondata_basic_t baseRef = *basePRef;
//printfline("THE IDLE HAPPENED");
if(dynaRef.weaponTypeID == WEAPONDATA_TYPEID_IRONSIGHT){
weapondata_ironsight_t ironsightRef = *((weapondata_ironsight_t*) basePRef);
BOOL usingIronSight;
// Usually we can depend on iIronSight, but not always.
// If using switch - ironsight calls, we need to be able to tell what the
// intended iIronSight is, since it changes at the end on the server and
// might not've reached us (the client) in time.
if(pSeat->m_eViewModel.frame == ironsightRef.ironsightdata.iAnim_Change_Index){
usingIronSight = TRUE; //assuming the end of this. Pick the opposite.
}else if(pSeat->m_eViewModel.frame == ironsightRef.ironsightdata.iAnim_ReChange_Index){
usingIronSight = FALSE; //assuming the end of this. Pick the opposite.
}else{
//neither? We should be able to trust this.
usingIronSight = (dynaRef.iIronSight == 1);
}
// idle anims don't really have any length. Let's just say "2" seconds if
// we ever care about some delay for this in the future.
if(!usingIronSight){
// use this one (plain)
TS_Weapons_ViewAnimation(ironsightRef.iAnim_Idle_Index, 2 );
}else{
// use this one
TS_Weapons_ViewAnimation(ironsightRef.ironsightdata.iAnim_Idle_Index, 2 );
}
}else{
//baseRef = *((weapondata_basic_t*) basePRef);
TS_Weapons_ViewAnimation(baseRef.iAnim_Idle_Index, 2 );
}
}// inventoryEquippedIndex
}// w_freeze_idle_next check
preThinkShared();
}//preThink
void
player::postThink(void){
float cltime_delta = cltime - clientTime_prev;
clientTime_prev = cltime;
// is that wise? There is also "frametime".
//printfline("-------postThink %.3f - %.3f------", cltime_delta, frametime);
/*
// if anything like this were ever needed?
w_attack_next_nonet -= cltime_delta;
if(w_attack_next_nonet < 0)w_attack_next_nonet = 0;
*/
// client-only, so it uses this.
// Why does this decrease way too fast if it uses input_timelength instead?
// Vars that are networked (see w_attack_akimbo_next in ts/src/shared/input.qc)
// don't appear to have that problem.
if(w_freeze_idle_next != -1){
w_freeze_idle_next = max(0, w_freeze_idle_next - cltime_delta);
}
}//postThink
#endif // CLIENT
#ifdef SERVER
void
player::preThink(void){
if(autocvar_sv_printoutspam == 1){
printfline("My state: %d", iState);
printfline("STATUS: %d, %i", activeweapon, inventoryEquippedIndex);
}
//self.angles += '0 0.3 0';
//self.v_angle = self.angles;
preThinkShared();
}// preThink
void
player::postThink(void){
}// postThink
#endif
// timer vars modified in here should use 'frametime'.
// could/should clframetime be used for clientside intead? Unsure.
void
player::preThinkShared(void){
// Empty, for now.
}
#ifdef SERVER
BOOL
player::attemptAddWeaponFromPickup(CTSWorldGun arg_pickup){
//int existingMatchingIndex = findWeaponInConfig((player)other, (int)arg_pickup.myInfo.weaponID);
// if the other player does not already have this weapon, we can take it.
// if the other player does already have this weapon, see if we can give them
// all our ammo. If so, despawn.
// If not (max pool capacity reached first), give what we can (if any) and then despawn.
//This method already handles already having this weapon in the inventory.
//...unfortunately we need a 2nd thing returned: whether we want to
// switch to the weapon we worked with.
// Using a temp var included with each player we assume this method sets:
// switchToRecentlyAddedWeapon.
int slotPlacedIn = addWeaponToInventory(this, arg_pickup);
//Assumption: if the player added this weapon to the inventory,
// it will have bumped the softMax compared to what it was before.
// Only switch to a weapon we picked up if it is completely new,
// OR turned an existing weapon akimbo.
// ...not good enough, we can't determine if akimbo was recently added
// to something that didn't use to have it.
//newWeaponAdded = (otherPlayerRef.ary_myWeapons_softMax != oldCap);
//We can equip the slot we made it to.
if(slotPlacedIn != -1){
if(switchToRecentlyAddedWeapon){
// We assume this is a new weapon or one that upgraded into akimbo.
// Play this sound too.
sound(this, CHAN_ITEM, "items/gunpickup2.wav", 1, ATTN_NORM, 100, SOUNDFLAG_PLAYER_COMMON );
TS_playerEquippedWeapon(this, slotPlacedIn, TRUE);
}
// picked up the weapon? decide whether we delete the pickup-able.
weapondata_basic_t* weaponPointer = ary_weaponData[arg_pickup.myInfo.weaponID];
weapondata_basic_t basicRef = *((weapondata_basic_t*) weaponPointer);
if(basicRef.typeID == WEAPONDATA_TYPEID_GUN || basicRef.typeID == WEAPONDATA_TYPEID_IRONSIGHT){
// assume we took the weapon. Akimbo was handled already.
return TRUE; //delete
}else if(basicRef.typeID == WEAPONDATA_TYPEID_THROWABLE){
//a throwable with all taken out of it? It disappears.
if(arg_pickup.myInfo.iCount == 0){
return TRUE; //delete
}
}else{
//melee? Just remove it.
return TRUE; //delete
}
}
return FALSE; //don't delete.
}// attemptAddWeaponFromPickup
// Drop this weapon (of that index). ID is kinda the improper term.
// the "completeDrop" parameter means auto-dropping akimbo. Player death does this
// for convenience.
void
player::dropWeapon(int arg_weaponID, BOOL completeDrop){
if(arg_weaponID == -1){
// ???
return;
}
// record the number of slots lost for removing from the total later,
// typically those of the 'eDrop' entity. Adjust for being more than one.
int iSlotsDropped = 0;
CTSWorldGun eDrop = NULL;
// there is a "TS_playerDropWeapon" method in event_custom.c we could call if needed.
// Looks like we never really intended for it to go anywhere though, no need for that.
// Not even hooked up in ts/client/event.c anyway (commented out).
// oh wait, we don't want to because the client initiates this whole thing. That's right.
// It sees the "drop" command called and so sends an event to the server that got to this "dropWeapon" method
// here in player. okay.
printfline("playerDrop. weaponID:%i completeDrop:%d", arg_weaponID, completeDrop);
weapondynamic_t dynaRefPRE = ary_myWeapons[arg_weaponID];
weapondata_basic_t* basicP = getInventoryWeaponData(arg_weaponID, TRUE);
weapondata_basic_t* basicPS = getInventoryWeaponData_Singular(arg_weaponID);
//printfline("ActiveWeap:%d InvEqIndx:%i", this.activeweapon, this.inventoryEquippedIndex);
// WARNING! This is still checking the possibly akimbo variant for having a
// worldmodel. The drops are still the singular variant in any case, but verify that.
if((*basicP).sWorldModelPath == ""){
// if the world model path is empty, we can't be dropped.
if(completeDrop){
// But if this is a completeDrop (dead player), delete it anyway.
removeWeaponFromInventory(this, arg_weaponID);
}
return;
}
BOOL deletedCurrentWeapon;
if(arg_weaponID == inventoryEquippedIndex){
// only if this is the currently equipped weapon.
Weapons_Holster();
if(!completeDrop){
deletedCurrentWeapon = equippedWeaponDeleteCheck();
}else{
// don't bother with the equip-ideal call that equippedWeaponDeleteCheck does.
deletedCurrentWeapon = inventoryWeaponDeleteCheck(arg_weaponID);
//setInventoryEquippedIndex(-1);
}
}else{
// Still, see if this weapon would prefer to disappear rather than
// be dropped. Throwables with a count of 0 (?) would prefer to be drop-less.
deletedCurrentWeapon = inventoryWeaponDeleteCheck(arg_weaponID);
}
// back to using the singular form unless otherwise noted.
// CRITICAL. Should still be able to use the akimbo version for stats all the same,
// adjust below to make usign the same basicP work.
// Until then, something simple.
// No, just use basicPS for below.
//basicP = getEquippedWeaponData_Singular();
printfline("-deletedCurrentWeapon? %d", deletedCurrentWeapon);
if(deletedCurrentWeapon){
// STOP. Apparently the player deleted this weapon in the holster call above,
// like a grenade that has 0 ammo and tried to be 'dropped'.
return;
}
// Check again, because maybe unequip above already removed the currently equipped
// weapon, leaving this -1 now. But it does say to equip the next best thing.
// it's safe.
//if(inventoryEquippedIndex != -1){
if((*basicPS).typeID == WEAPONDATA_TYPEID_GUN || (*basicPS).typeID == WEAPONDATA_TYPEID_IRONSIGHT){
// This works, but hte proper way to check for always-akimbo is:
// basicRefX.iAkimboID == WEAPON_AKIMBO_UPGRADE_ID::NONE
// for linked akimbo, alongside the same dynaRef check:
// basicRefX.iAkimboID > 0
if((*basicPS).iBitsUpgradeAuto & BITS_WEAPONOPT_AKIMBO){
// singular akimbo? dropping removes.
eDrop = CTSWorldGun::generate(this, arg_weaponID);
removeWeaponFromInventory(this, arg_weaponID);
if(!completeDrop){
playerEquipIdealSafe(this);
}
}else if(
((*basicPS).iBitsUpgrade & BITS_WEAPONOPT_AKIMBO) &&
(dynaRefPRE.iBitsUpgrade & BITS_WEAPONOPT_AKIMBO)
){
// TODO - CRITICAL! Does this need any special checks for akimbo-only weapons?
// Those can't be split into two singular pickups like an area further down does.
////////////////////////
// supports akimbo with singular form, and we happen to be akimbo? Don't delete the
// weapon, just downgrade to non-akimbo.
// WAIT NOT JUST YET. The weapon drop will think we're dropping the weapon while it
// didn't have akimbo.
if(!completeDrop){
eDrop = CTSWorldGun::generate(this, arg_weaponID);
if(weaponEquippedAkimbo){
// safety. dynaRefPRE.iClipLeft stays as it is, just enforcing that the akimbo one was dropped.
dynaRefPRE.iClipAkimboLeft = 0;
}else{
// Dropping the one used for singular? ok.
// Make the singular we retain's that of the akimbo clip count then.
dynaRefPRE.iClipLeft = dynaRefPRE.iClipAkimboLeft;
dynaRefPRE.iClipAkimboLeft = 0;
}
// playerEquipIdealSafe ? Not quite.
printfline("setInventoryEquippedIndex: FLAG B");
TS_resetViewModel(this);
setInventoryEquippedIndex(-1);
weaponEquippedAkimbo = FALSE;
//playerEquipIdeal(this);
ary_myWeapons[arg_weaponID].iCount = 1; //instead of 2.
// we just lost the slots worth of what was just dropped.
// Important since dropping didn't delete this weapon, in the meantime
// its own slot count must be accurate since losing one of the two guns.
ary_myWeapons[arg_weaponID].iSlots = (*basicPS).iSlots * 1;
// HERE WE GO.
dynaRefPRE.iBitsUpgrade &= ~ BITS_WEAPONOPT_AKIMBO;
// only FALSE on the akimbo part since we clearly just lost it.
TS_playerEquippedWeapon(this, arg_weaponID, FALSE);
}else{
// Player is dying? Remove this weapon and drop two copies.
// Pretty simple.
// ...but we need to override how they get ammo.
eDrop = CTSWorldGun::generate(this, arg_weaponID);
eDrop.myInfo.iClipLeft = dynaRefPRE.iClipLeft;
eDrop.myInfo.iClipAkimboLeft = 0;
eDrop = CTSWorldGun::generate(this, arg_weaponID);
eDrop.myInfo.iClipLeft = dynaRefPRE.iClipAkimboLeft;
eDrop.myInfo.iClipAkimboLeft = 0;
removeWeaponFromInventory(this, arg_weaponID);
// because further below assumes only one copy was dropped.
// Add the 2nd copy's slots in this way.
iSlotsDropped += eDrop.myInfo.iSlots;
}
}else{
// doesn't support akimbo or not akimbo? remove.
eDrop = CTSWorldGun::generate(this, arg_weaponID);
removeWeaponFromInventory(this, arg_weaponID);
if(!completeDrop){
playerEquipIdealSafe(this);
}
}
}else if((*basicPS).typeID == WEAPONDATA_TYPEID_THROWABLE){
//drop away
if(!completeDrop){
eDrop = CTSWorldGun::generate(this, arg_weaponID);
eDrop.myInfo.iCount = 1;
eDrop.myInfo.iSlots = (*basicPS).iSlots * 1;
ary_myWeapons[arg_weaponID].iCount -= 1;
ary_myWeapons[arg_weaponID].iSlots = (*basicPS).iSlots * ary_myWeapons[arg_weaponID].iCount;
if(ary_myWeapons[arg_weaponID].iCount <= 0){
// unequip this weapon
removeWeaponFromInventory(this, arg_weaponID);
playerEquipIdealSafe(this);
}else{
// still have it? undirty any blood effects since we dropped that.
ary_myWeapons[arg_weaponID].iForceBodygroup1Submodel = 0;
}
}else{
// combine them all, happens naturally.
eDrop = CTSWorldGun::generate(this, arg_weaponID);
removeWeaponFromInventory(this, arg_weaponID);
playerEquipIdealSafe(this);
}
}else{ // if((*basicPS).typeID == WEAPONDATA_TYPEID_MELEE)
eDrop = CTSWorldGun::generate(this, arg_weaponID);
removeWeaponFromInventory(this, arg_weaponID);
if(!completeDrop){
playerEquipIdealSafe(this);
}
}
printfline("playerDrop: end. Was there a drop? %d", (float)(eDrop != NULL));
if(eDrop != NULL){
iSlotsDropped += eDrop.myInfo.iSlots;
}
iTotalSlots -= iSlotsDropped;
sound(this, CHAN_WEAPON, "weapons/weapondrop.wav", 1, ATTN_NORM );
}// dropWeapon
BOOL
player::anyAmmoPoolNonEmpty(void){
//BOOL anyNonEmpty = FALSE;
for(int i = 0; i < AMMO_ID::LAST_ID; i++){
if(ary_ammoTotal[i] > 0){
//anyNonEmpty = TRUE;
return TRUE;
}
}
//return anyNonEmpty;
return FALSE; //made it this far? didn't have any.
}// anyAmmoPoolNonEmpty
// oh.. I guess that's it.
void
player::dropAmmo(void){
//CTSAmmoPack eDrop =
CTSAmmoPack::generate(this);
}// dropAmmo
#endif