]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blobdiff - qcsrc/server/items/items.qc
items: merge _StartItem() and StartItem(), warn instead of crashing if defaultrespawn...
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / items / items.qc
index e68d858a8e72e55d9ad4a156cd0b81400cafbd8e..e06c6f074549e0d6976df8a9cf63ddd1b389fed7 100644 (file)
@@ -59,7 +59,6 @@ bool ItemSend(entity this, entity to, int sf)
        if(sf & ISF_MODEL)
        {
                WriteShort(MSG_ENTITY, bound(0, this.fade_end, 32767));
-               WriteShort(MSG_ENTITY, bound(0, this.fade_start, 32767));
 
                if(this.mdl == "")
                        LOG_TRACE("^1WARNING!^7 this.mdl is unset for item ", this.classname, "expect a crash just about now");
@@ -98,6 +97,9 @@ void UpdateItemAfterTeleport(entity this)
 
 bool have_pickup_item(entity this)
 {
+       if (this.itemdef.spawnflags & ITEM_FLAG_MUTATORBLOCKED)
+               return false;
+
        if(this.itemdef.instanceOfPowerup)
        {
                if(autocvar_g_powerups > 0)
@@ -155,10 +157,7 @@ void Item_Show(entity e, int mode)
                }
                else
                {
-                       //setmodel(e, "null");
                        e.solid = SOLID_NOT;
-                       e.colormod = '0 0 0';
-                       //e.glowmod = e.colormod;
                        e.spawnshieldtime = 1;
                        e.ItemStatus &= ~ITS_AVAILABLE;
                }
@@ -185,9 +184,45 @@ void Item_Show(entity e, int mode)
 
 void Item_Think(entity this)
 {
-       this.nextthink = time;
-       if(this.origin != this.oldorigin)
+       if (ITEM_IS_LOOT(this))
+       {
+               if (time < this.wait - IT_DESPAWNFX_TIME)
+                       this.nextthink = min(time + IT_UPDATE_INTERVAL, this.wait - IT_DESPAWNFX_TIME); // ensuring full time for effects
+               else
+               {
+                       // despawning soon, start effects
+                       this.ItemStatus |= ITS_EXPIRING;
+                       this.SendFlags |= ISF_STATUS;
+                       if (time < this.wait - IT_UPDATE_INTERVAL)
+                               this.nextthink = time + IT_UPDATE_INTERVAL;
+                       else
+                       {
+                               setthink(this, RemoveItem);
+                               this.nextthink = this.wait;
+                       }
+               }
+
+               if (this.itemdef.instanceOfPowerup)
+                       powerups_DropItem_Think(this);
+
+               // caution: kludge FIXME (with sv_legacy_bbox_expand)
+               // this works around prediction errors caused by bbox discrepancy between SVQC and CSQC
+               if (this.velocity == '0 0 0' && IS_ONGROUND(this))
+                       this.gravity = 0; // don't send ISF_DROP anymore
+
+               // send slow updates even if the item didn't move
+               // recovers prediction desyncs where server thinks item stopped, client thinks it didn't
                ItemUpdate(this);
+       }
+       else
+       {
+               // bones_was_here: TODO: predict movers, enable client prediction of items with a groundentity,
+               // and then send those less often too (and not all on the same frame)
+               this.nextthink = time;
+
+               if(this.origin != this.oldorigin)
+                       ItemUpdate(this);
+       }
 }
 
 bool Item_ItemsTime_SpectatorOnly(GameItem it);
@@ -210,9 +245,6 @@ void Item_Respawn(entity this)
 
        setthink(this, Item_Think);
        this.nextthink = time;
-
-       //Send_Effect(EFFECT_ITEM_RESPAWN, this.origin + this.mins_z * '0 0 1' + '0 0 48', '0 0 0', 1);
-       Send_Effect(EFFECT_ITEM_RESPAWN, CENTER_OR_VIEWOFS(this), '0 0 0', 1);
 }
 
 void Item_RespawnCountdown(entity this)
@@ -643,7 +675,7 @@ bool Item_GiveTo(entity item, entity player)
 void Item_Touch(entity this, entity toucher)
 {
        // remove the item if it's currnetly in a NODROP brush or hits a NOIMPACT surface (such as sky)
-       if (Item_IsLoot(this))
+       if (ITEM_IS_LOOT(this))
        {
                if (ITEM_TOUCH_NEEDKILL())
                {
@@ -668,7 +700,7 @@ void Item_Touch(entity this, entity toucher)
 
        toucher = M_ARGV(1, entity);
 
-       if (Item_IsExpiring(this))
+       if (ITEM_IS_EXPIRING(this))
        {
                this.strength_finished = max(0, this.strength_finished - time);
                this.invincible_finished = max(0, this.invincible_finished - time);
@@ -679,7 +711,7 @@ void Item_Touch(entity this, entity toucher)
        bool gave = ITEM_HANDLE(Pickup, this.itemdef, this, toucher);
        if (!gave)
        {
-               if (Item_IsExpiring(this))
+               if (ITEM_IS_EXPIRING(this))
                {
                        // undo what we did above
                        this.strength_finished += time;
@@ -698,8 +730,9 @@ LABEL(pickup)
 
        STAT(LAST_PICKUP, toucher) = time;
 
-       Send_Effect(EFFECT_ITEM_PICKUP, CENTER_OR_VIEWOFS(this), '0 0 0', 1);
-       _sound (toucher, (this.itemdef.instanceOfPowerup ? CH_TRIGGER_SINGLE : CH_TRIGGER), (this.item_pickupsound ? this.item_pickupsound : Sound_fixpath(this.item_pickupsound_ent)), VOL_BASE, ATTEN_NORM);
+       GameItem def = this.itemdef;
+       int ch = ((def.instanceOfPowerup && def.m_itemid != IT_RESOURCE) ? CH_TRIGGER_SINGLE : CH_TRIGGER);
+       _sound(toucher, ch, this.item_pickupsound, VOL_BASE, ATTEN_NORM);
 
        MUTATOR_CALLHOOK(ItemTouched, this, toucher);
        if (wasfreed(this))
@@ -707,7 +740,7 @@ LABEL(pickup)
                return;
        }
 
-       if (Item_IsLoot(this))
+       if (ITEM_IS_LOOT(this))
        {
                delete(this);
                return;
@@ -741,7 +774,7 @@ void Item_Reset(entity this)
 {
        Item_Show(this, !this.state);
 
-       if (Item_IsLoot(this))
+       if (ITEM_IS_LOOT(this))
        {
                return;
        }
@@ -795,13 +828,25 @@ void Item_FindTeam(entity this)
        });
 }
 
+void Item_CopyFields(entity this, entity to)
+{
+       setorigin(to, this.origin);
+       to.spawnflags = this.spawnflags;
+       to.noalign = ITEM_SHOULD_KEEP_POSITION(this);
+       to.cnt = this.cnt;
+       to.team = this.team;
+       to.spawnfunc_checked = true;
+       // TODO: copy respawn times? this may not be desirable in some cases
+       //to.respawntime = this.respawntime;
+       //to.respawntimejitter = this.respawntimejitter;
+}
+
 // Savage: used for item garbage-collection
 void RemoveItem(entity this)
 {
        if(wasfreed(this) || !this) { return; }
        if(this.waypointsprite_attached)
                WaypointSprite_Kill(this.waypointsprite_attached);
-       Send_Effect(EFFECT_ITEM_PICKUP, CENTER_OR_VIEWOFS(this), '0 0 0', 1);
        delete(this);
 }
 
@@ -916,29 +961,28 @@ void item_use(entity this, entity actor, entity trigger)
        gettouch(this)(this, actor);
 }
 
-void _StartItem(entity this, entity def, float defaultrespawntime, float defaultrespawntimejitter)
+void StartItem(entity this, entity def)
 {
+       if (def.m_spawnfunc_hookreplace)
+               def = def.m_spawnfunc_hookreplace(def, this);
+       this.itemdef = def;
+       if (def.m_canonical_spawnfunc != "") // FIXME why do weapons set itemdef to an entity that doesn't have this?
+               this.classname = def.m_canonical_spawnfunc;
+
        string itemname = def.m_name;
-       Model itemmodel = def.m_model;
-       Sound pickupsound = def.m_sound;
        float(entity player, entity item) pickupevalfunc = def.m_pickupevalfunc;
        float pickupbasevalue = def.m_botvalue;
-       int itemflags = def.m_itemflags;
 
        startitem_failed = false;
 
-       this.item_model_ent = itemmodel;
-       this.item_pickupsound_ent = pickupsound;
+       this.item_model_ent = def.m_model;
+       this.item_pickupsound_ent = def.m_sound;
+       if (!this.item_pickupsound)
+               this.item_pickupsound = Sound_fixpath(this.item_pickupsound_ent);
 
        if(def.m_iteminit)
                def.m_iteminit(def, this);
 
-       if(!this.respawntime) // both need to be set
-       {
-               this.respawntime = defaultrespawntime;
-               this.respawntimejitter = defaultrespawntimejitter;
-       }
-
        if(!this.pickup_anyway && def.m_pickupanyway)
                this.pickup_anyway = def.m_pickupanyway();
 
@@ -947,16 +991,14 @@ void _StartItem(entity this, entity def, float defaultrespawntime, float default
        int weaponid = def.instanceOfWeaponPickup ? def.m_weapon.m_id : 0;
        this.weapon = weaponid;
 
+       // bones_was_here TODO: implement sv_cullentities_dist and replace g_items_maxdist with it
        if(!this.fade_end)
-       {
-               this.fade_start = autocvar_g_items_mindist;
                this.fade_end = autocvar_g_items_maxdist;
-       }
 
        if(weaponid)
                STAT(WEAPONS, this) = WepSet_FromWeapon(REGISTRY_GET(Weapons, weaponid));
 
-       this.flags = FL_ITEM | itemflags;
+       this.flags = FL_ITEM | def.m_itemflags;
        IL_PUSH(g_items, this);
 
        if(MUTATOR_CALLHOOK(FilterItem, this)) // error means we do not want the item
@@ -966,24 +1008,24 @@ void _StartItem(entity this, entity def, float defaultrespawntime, float default
                return;
        }
 
-       precache_model(this.model);
-       precache_sound(this.item_pickupsound);
-
-       if(q3compat && !this.team)
-       {
-               string t = GetField_fullspawndata(this, "team", false);
-               // bones_was_here: this hack is cheaper than changing to a .string strcmp()
-               if(t) this.team = crc16(false, t);
-       }
+       this.mdl = this.model ? this.model : strzone(this.item_model_ent.model_str());
+       setmodel(this, MDL_Null); // precision set below
+       // set item size before we spawn a waypoint or droptofloor or MoveOutOfSolid
+       setsize (this, this.pos1 = def.m_mins, this.pos2 = def.m_maxs);
 
-       if (Item_IsLoot(this))
+       if (ITEM_IS_LOOT(this))
        {
                this.reset = RemoveItem;
+
                set_movetype(this, MOVETYPE_TOSS);
+               this.gravity = 1;
+
+               setthink(this, Item_Think);
+               this.nextthink = time + IT_UPDATE_INTERVAL;
+               this.wait = time + autocvar_g_items_dropped_lifetime;
 
-               // Savage: remove thrown items after a certain period of time ("garbage collection")
-               setthink(this, RemoveItem);
-               this.nextthink = time + autocvar_g_items_dropped_lifetime;
+               this.owner = NULL; // anyone can pick this up, including the player who threw it
+               this.item_spawnshieldtime = time + 0.5; // but not straight away
 
                this.takedamage = DAMAGE_YES;
                this.event_damage = Item_Damage;
@@ -991,7 +1033,7 @@ void _StartItem(entity this, entity def, float defaultrespawntime, float default
                //this.damagedbycontents = true;
                //IL_PUSH(g_damagedbycontents, this);
 
-               if (Item_IsExpiring(this))
+               if (ITEM_IS_EXPIRING(this))
                {
                        // if item is worthless after a timer, have it expire then
                        this.nextthink = max(this.strength_finished, this.invincible_finished, this.superweapons_finished);
@@ -1008,6 +1050,9 @@ void _StartItem(entity this, entity def, float defaultrespawntime, float default
        }
        else
        {
+               this.reset = Item_Reset;
+
+               // must be done after def.m_iteminit() as that may set ITEM_FLAG_MUTATORBLOCKED
                if(!have_pickup_item(this))
                {
                        startitem_failed = true;
@@ -1015,10 +1060,30 @@ void _StartItem(entity this, entity def, float defaultrespawntime, float default
                        return;
                }
 
+               // must be done before Item_Reset() and after MUTATORBLOCKED check (blocked items may have null func ptrs)
+               if(!this.respawntime) // both need to be set
+               {
+                       if (def.m_respawntime)
+                               this.respawntime = def.m_respawntime();
+                       else
+                               LOG_WARNF("Default respawntime for a %s is unavailable from its itemdef", this.classname);
+
+                       if (def.m_respawntimejitter)
+                               this.respawntimejitter = def.m_respawntimejitter();
+                       else
+                               LOG_WARNF("Default respawntimejitter for a %s is unavailable from its itemdef", this.classname);
+               }
+
                if(this.angles != '0 0 0')
                        this.SendFlags |= ISF_ANGLES;
 
-               this.reset = this.team ? Item_FindTeam : Item_Reset;
+               if(q3compat && !this.team)
+               {
+                       string t = GetField_fullspawndata(this, "team");
+                       // bones_was_here: this hack is cheaper than changing to a .string strcmp()
+                       if(t) this.team = crc16(false, t);
+               }
+
                // it's a level item
                if(this.spawnflags & 1)
                        this.noalign = 1;
@@ -1031,9 +1096,6 @@ void _StartItem(entity this, entity def, float defaultrespawntime, float default
                {
                        // first nudge it off the floor a little bit to avoid math errors
                        setorigin(this, this.origin + '0 0 1');
-                       // set item size before we spawn a spawnfunc_waypoint
-                       setsize(this, def.m_mins, def.m_maxs);
-                       this.SendFlags |= ISF_SIZE;
                        // note droptofloor returns false if stuck/or would fall too far
                        if (!this.noalign)
                                droptofloor(this);
@@ -1082,25 +1144,21 @@ void _StartItem(entity this, entity def, float defaultrespawntime, float default
                }
 
                Item_ItemsTime_SetTime(this, 0);
+
+               this.glowmod = def.m_color;
        }
 
        this.bot_pickup = true;
        this.bot_pickupevalfunc = pickupevalfunc;
        this.bot_pickupbasevalue = pickupbasevalue;
-       this.mdl = this.model ? this.model : strzone(this.item_model_ent.model_str());
        this.netname = itemname;
        settouch(this, Item_Touch);
-       setmodel(this, MDL_Null); // precision set below
        //this.effects |= EF_LOWPRECISION;
 
        // support skinned models for powerups
        if(!this.skin)
                this.skin = def.m_skin;
 
-       setsize (this, this.pos1 =  def.m_mins, this.pos2 = def.m_maxs);
-
-       this.SendFlags |= ISF_SIZE;
-
        if (!(this.spawnflags & 1024)) {
                if(def.instanceOfPowerup)
                        this.ItemStatus |= ITS_ANIMATE1;
@@ -1109,14 +1167,9 @@ void _StartItem(entity this, entity def, float defaultrespawntime, float default
                        this.ItemStatus |= ITS_ANIMATE2;
        }
 
-       if(Item_IsLoot(this))
-               this.gravity = 1;
-       else
-               this.glowmod = def.m_color;
-
        if(def.instanceOfWeaponPickup)
        {
-               if (!Item_IsLoot(this)) // if dropped, colormap is already set up nicely
+               if (!ITEM_IS_LOOT(this)) // if dropped, colormap is already set up nicely
                        this.colormap = 1024; // color shirt=0 pants=0 grey
                if (!(this.spawnflags & 1024))
                        this.ItemStatus |= ITS_ANIMATE1;
@@ -1131,6 +1184,7 @@ void _StartItem(entity this, entity def, float defaultrespawntime, float default
 
                this.effects |= EF_NOGUNBOB; // marker for item team search
                InitializeEntity(this, Item_FindTeam, INITPRIO_FINDTARGET);
+               this.reset = Item_FindTeam;
        }
        else
                Item_Reset(this);
@@ -1145,27 +1199,12 @@ void _StartItem(entity this, entity def, float defaultrespawntime, float default
                return;
        }
 
-       setItemGroup(this);
-}
-
-void StartItem(entity this, GameItem def)
-{
-       def = def.m_spawnfunc_hookreplace(def, this);
-
-       if (def.spawnflags & ITEM_FLAG_MUTATORBLOCKED)
-       {
-               delete(this);
-               return; // TODO does not set startitem_failed
-       }
-
-       this.classname = def.m_canonical_spawnfunc;
+       // we should be sure this item will spawn before loading its assets
+       // CSQC handles model precaching: it may not use this model (eg simple items) and may not have connected yet
+       //precache_model(this.mdl);
+       precache_sound(this.item_pickupsound);
 
-       _StartItem(
-               this,
-               this.itemdef = def,
-               def.m_respawntime(), // defaultrespawntime
-               def.m_respawntimejitter() // defaultrespawntimejitter
-       );
+       setItemGroup(this);
 }
 
 #define IS_SMALL(def) ((def.instanceOfHealth && def == ITEM_HealthSmall) || (def.instanceOfArmor && def == ITEM_ArmorSmall))
@@ -1173,7 +1212,7 @@ int group_count = 1;
 
 void setItemGroup(entity this)
 {
-       if(!IS_SMALL(this.itemdef) || Item_IsLoot(this))
+       if(!IS_SMALL(this.itemdef) || ITEM_IS_LOOT(this))
                return;
 
        FOREACH_ENTITY_RADIUS(this.origin, 120, (it != this) && IS_SMALL(it.itemdef),
@@ -1218,7 +1257,7 @@ void setItemGroupCount()
 
 void target_items_use(entity this, entity actor, entity trigger)
 {
-       if(Item_IsLoot(actor))
+       if(ITEM_IS_LOOT(actor))
        {
                EXACTTRIGGER_TOUCH(this, trigger);
                delete(actor);
@@ -1233,7 +1272,7 @@ void target_items_use(entity this, entity actor, entity trigger)
                EXACTTRIGGER_TOUCH(this, trigger);
        }
 
-       IL_EACH(g_items, it.enemy == actor && Item_IsLoot(it),
+       IL_EACH(g_items, it.enemy == actor && ITEM_IS_LOOT(it),
        {
                delete(it);
        });