]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blobdiff - qcsrc/server/items/items.qc
items: remove blind nudging of items during spawn
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / items / items.qc
index 3aef870e00a8b775257803ef20ce1270fc941067..f4853c5c7203c9d36c57ebcce6d2520c1b80a310 100644 (file)
@@ -100,14 +100,7 @@ bool have_pickup_item(entity this)
        if (this.itemdef.spawnflags & ITEM_FLAG_MUTATORBLOCKED)
                return false;
 
-       if(this.itemdef.instanceOfPowerup)
-       {
-               if(autocvar_g_powerups > 0)
-                       return true;
-               if(autocvar_g_powerups == 0)
-                       return false;
-       }
-       else
+       if(!this.itemdef.instanceOfPowerup)
        {
                if(autocvar_g_pickup_items > 0)
                        return true;
@@ -184,7 +177,7 @@ void Item_Show(entity e, int mode)
 
 void Item_Think(entity this)
 {
-       if (Item_IsLoot(this))
+       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
@@ -205,6 +198,11 @@ void Item_Think(entity this)
                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);
@@ -579,8 +577,8 @@ bool Item_GiveTo(entity item, entity player)
                        Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_ITEM_JETPACK_GOT);
        }
 
-       int its;
-       if((its = (item.items - (item.items & player.items)) & IT_PICKUPMASK))
+       int its = (item.items - (item.items & player.items)) & IT_PICKUPMASK;
+       if (its)
        {
                pickedup = true;
                player.items |= its;
@@ -670,10 +668,11 @@ 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())
                {
+                       this.SendFlags |= ISF_REMOVEFX;
                        RemoveItem(this);
                        return;
                }
@@ -695,7 +694,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);
@@ -706,7 +705,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;
@@ -735,9 +734,10 @@ LABEL(pickup)
                return;
        }
 
-       if (Item_IsLoot(this))
+       if (ITEM_IS_LOOT(this))
        {
-               delete(this);
+               this.SendFlags |= ISF_REMOVEFX;
+               RemoveItem(this);
                return;
        }
        if (!this.spawnshieldtime)
@@ -769,7 +769,7 @@ void Item_Reset(entity this)
 {
        Item_Show(this, !this.state);
 
-       if (Item_IsLoot(this))
+       if (ITEM_IS_LOOT(this))
        {
                return;
        }
@@ -827,7 +827,7 @@ void Item_CopyFields(entity this, entity to)
 {
        setorigin(to, this.origin);
        to.spawnflags = this.spawnflags;
-       to.noalign = Item_ShouldKeepPosition(this);
+       to.noalign = ITEM_SHOULD_KEEP_POSITION(this);
        to.cnt = this.cnt;
        to.team = this.team;
        to.spawnfunc_checked = true;
@@ -842,7 +842,16 @@ void RemoveItem(entity this)
        if(wasfreed(this) || !this) { return; }
        if(this.waypointsprite_attached)
                WaypointSprite_Kill(this.waypointsprite_attached);
-       delete(this);
+
+       if (this.SendFlags & ISF_REMOVEFX)
+       {
+               // delay removal until ISF_REMOVEFX has been sent
+               setthink(this, RemoveItem);
+               this.nextthink = time + 2 * autocvar_sys_ticrate; // micro optimisation: next frame will be too soon
+               this.solid = SOLID_NOT; // untouchable
+       }
+       else
+               delete(this);
 }
 
 // pickup evaluation functions
@@ -956,55 +965,62 @@ void item_use(entity this, entity actor, entity trigger)
        gettouch(this)(this, actor);
 }
 
-// if defaultrespawntime is 0 get respawntime from the item definition
-// if defaultrespawntimejitter is 0 get respawntimejitter from the item definition
-void _StartItem(entity this, entity def, float defaultrespawntime, float defaultrespawntimejitter)
+void StartItem(entity this, entity def)
 {
-       string itemname = def.m_name;
-       float(entity player, entity item) pickupevalfunc = def.m_pickupevalfunc;
-       float pickupbasevalue = def.m_botvalue;
-
-       startitem_failed = false;
+       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;
 
-       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);
+       startitem_failed = true; // early return means failure
 
+       // some mutators check for resources set by m_iteminit in FilterItem
        if(def.m_iteminit)
                def.m_iteminit(def, this);
 
+       // also checked by some mutators in FilterItem
+       this.items = def.m_itemid;
+       this.weapon = def.instanceOfWeaponPickup ? def.m_weapon.m_id : 0;
+       if(this.weapon)
+               STAT(WEAPONS, this) = WepSet_FromWeapon(REGISTRY_GET(Weapons, this.weapon));
+       this.flags = FL_ITEM | def.m_itemflags;
+
+       // FilterItem may change any field of a specific instance of an item, but
+       // it must not change any itemdef field (would cause mutators to break other mutators),
+       // and must not convert items into different ones (StartItem could be refactored to support that).
+       if(MUTATOR_CALLHOOK(FilterItem, this))
+       {
+               delete(this);
+               return;
+       }
+
+       if (!this.item_model_ent)
+               this.item_model_ent = def.m_model;
+
+       if (!this.item_pickupsound_ent)
+               this.item_pickupsound_ent = def.m_sound;
+       if (!this.item_pickupsound && this.item_pickupsound_ent)
+               this.item_pickupsound = Sound_fixpath(this.item_pickupsound_ent);
+       if (this.item_pickupsound == "")
+               LOG_WARNF("No pickup sound set for a %s", this.classname);
+
        if(!this.pickup_anyway && def.m_pickupanyway)
                this.pickup_anyway = def.m_pickupanyway();
 
-       int itemid = def.m_itemid;
-       this.items = itemid;
-       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_end = autocvar_g_items_maxdist;
 
-       if(weaponid)
-               STAT(WEAPONS, this) = WepSet_FromWeapon(REGISTRY_GET(Weapons, weaponid));
-
-       this.flags = FL_ITEM | def.m_itemflags;
+       // bones_was_here TODO: can we do this after we're sure the entity won't be deleted?
        IL_PUSH(g_items, this);
 
-       if(MUTATOR_CALLHOOK(FilterItem, this)) // error means we do not want the item
-       {
-               startitem_failed = true;
-               delete(this);
-               return;
-       }
-
        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;
 
@@ -1024,27 +1040,30 @@ 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);
                }
 
+               // most loot items have a bigger horizontal size than a player
+               nudgeoutofsolid(this);
+
                // don't drop if in a NODROP zone (such as lava)
                traceline(this.origin, this.origin, MOVE_NORMAL, this);
                if (trace_dpstartcontents & DPCONTENTS_NODROP)
                {
-                       startitem_failed = true;
                        delete(this);
                        return;
                }
        }
        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;
                        delete(this);
                        return;
                }
@@ -1052,18 +1071,33 @@ void _StartItem(entity this, entity def, float defaultrespawntime, float default
                // 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
                {
-                       this.respawntime = defaultrespawntime ? defaultrespawntime : def.m_respawntime();
-                       this.respawntimejitter = defaultrespawntimejitter ? defaultrespawntimejitter : def.m_respawntimejitter();
+                       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;
 
-               if(q3compat && !this.team)
+               if(q3compat)
                {
-                       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);
+                       if (!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);
+                       }
+
+                       // In Q3 the origin is in the middle of the bbox ("radius" 15), in Xon it's at the bottom
+                       // so we need to offset vertically (only for items placed by the mapper).
+                       this.origin.z += -15 - this.mins.z;
+                       setorigin(this, this.origin);
                }
 
                // it's a level item
@@ -1076,8 +1110,6 @@ void _StartItem(entity this, entity def, float defaultrespawntime, float default
                // do item filtering according to game mode and other things
                if (this.noalign <= 0)
                {
-                       // first nudge it off the floor a little bit to avoid math errors
-                       setorigin(this, this.origin + '0 0 1');
                        // note droptofloor returns false if stuck/or would fall too far
                        if (!this.noalign)
                                droptofloor(this);
@@ -1092,7 +1124,6 @@ void _StartItem(entity this, entity def, float defaultrespawntime, float default
                {
                        // target_give not yet supported; maybe later
                        print("removed targeted ", this.classname, "\n");
-                       startitem_failed = true;
                        delete(this);
                        return;
                }
@@ -1105,20 +1136,20 @@ void _StartItem(entity this, entity def, float defaultrespawntime, float default
                {
                        // why not flags & fl_item?
                        FOREACH_ENTITY_RADIUS(this.origin, 3, it.is_item, {
-                               LOG_TRACE("XXX Found duplicated item: ", itemname, vtos(this.origin));
+                               LOG_TRACE("XXX Found duplicated item: ", def.m_name, vtos(this.origin));
                                LOG_TRACE(" vs ", it.netname, vtos(it.origin));
                                error("Mapper sucks.");
                        });
                        this.is_item = true;
                }
 
-               weaponsInMap |= WepSet_FromWeapon(REGISTRY_GET(Weapons, weaponid));
+               weaponsInMap |= WepSet_FromWeapon(REGISTRY_GET(Weapons, this.weapon));
 
                if (        def.instanceOfPowerup
                        ||  def.instanceOfWeaponPickup
                        || (def.instanceOfHealth && def != ITEM_HealthSmall)
                        || (def.instanceOfArmor && def != ITEM_ArmorSmall)
-                       || (itemid & (IT_KEY1 | IT_KEY2))
+                       || (def.m_itemid & (IT_KEY1 | IT_KEY2))
                )
                {
                        if(!this.target || this.target == "")
@@ -1131,9 +1162,9 @@ void _StartItem(entity this, entity def, float defaultrespawntime, float default
        }
 
        this.bot_pickup = true;
-       this.bot_pickupevalfunc = pickupevalfunc;
-       this.bot_pickupbasevalue = pickupbasevalue;
-       this.netname = itemname;
+       this.bot_pickupevalfunc = def.m_pickupevalfunc;
+       this.bot_pickupbasevalue = def.m_botvalue;
+       this.netname = def.m_name;
        settouch(this, Item_Touch);
        //this.effects |= EF_LOWPRECISION;
 
@@ -1151,7 +1182,7 @@ void _StartItem(entity this, entity def, float defaultrespawntime, float default
 
        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;
@@ -1169,17 +1200,13 @@ void _StartItem(entity this, entity def, float defaultrespawntime, float default
                this.reset = Item_FindTeam;
        }
        else
-       {
-               this.reset = Item_Reset;
                Item_Reset(this);
-       }
 
        Net_LinkEntity(this, !(def.instanceOfPowerup || def.instanceOfHealth || def.instanceOfArmor), 0, ItemSend);
 
        // call this hook after everything else has been done
        if (MUTATOR_CALLHOOK(Item_Spawn, this))
        {
-               startitem_failed = true;
                delete(this);
                return;
        }
@@ -1190,16 +1217,8 @@ void _StartItem(entity this, entity def, float defaultrespawntime, float default
        precache_sound(this.item_pickupsound);
 
        setItemGroup(this);
-}
-
-void StartItem(entity this, GameItem def)
-{
-       def = def.m_spawnfunc_hookreplace(def, this);
 
-       this.classname = def.m_canonical_spawnfunc;
-
-       this.itemdef = def;
-       _StartItem(this, this.itemdef, 0, 0);
+       startitem_failed = false;
 }
 
 #define IS_SMALL(def) ((def.instanceOfHealth && def == ITEM_HealthSmall) || (def.instanceOfArmor && def == ITEM_ArmorSmall))
@@ -1207,7 +1226,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),
@@ -1252,7 +1271,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);
@@ -1267,7 +1286,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);
        });