This commit is contained in:
lifestorm
2024-08-04 23:12:27 +03:00
parent 8064ba84d8
commit 9c918c46e5
7081 changed files with 2173485 additions and 14 deletions

View File

@@ -0,0 +1,397 @@
--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
--[[--
Contains information about a player's current game state.
Characters are a fundamental object type in Helix. They are distinct from players, where players are the representation of a
person's existence in the server that owns a character, and their character is their currently selected persona. All the
characters that a player owns will be loaded into memory once they connect to the server. Characters are saved during a regular
interval, and during specific events (e.g when the owning player switches away from one character to another).
They contain all information that is not persistent with the player; names, descriptions, model, currency, etc. For the most
part, you'll want to keep all information stored on the character since it will probably be different or change if the
player switches to another character. An easy way to do this is to use `ix.char.RegisterVar` to easily create accessor functions
for variables that automatically save to the character object.
]]
-- @classmod Character
local CHAR = ix.meta.character or {}
CHAR.__index = CHAR
CHAR.id = CHAR.id or 0
CHAR.vars = CHAR.vars or {}
-- @todo not this
if (!ix.db) then
ix.util.Include("../libs/sv_database.lua")
end
--- Returns a string representation of this character
-- @realm shared
-- @treturn string String representation
-- @usage print(ix.char.loaded[1])
-- > "character[1]"
function CHAR:__tostring()
return "character["..(self.id or 0).."]"
end
--- Returns true if this character is equal to another character. Internally, this checks character IDs.
-- @realm shared
-- @char other Character to compare to
-- @treturn bool Whether or not this character is equal to the given character
-- @usage print(ix.char.loaded[1] == ix.char.loaded[2])
-- > false
function CHAR:__eq(other)
return self:GetID() == other:GetID()
end
--- Returns this character's database ID. This is guaranteed to be unique.
-- @realm shared
-- @treturn number Unique ID of character
function CHAR:GetID()
return self.id
end
if (SERVER) then
--- Saves this character's info to the database.
-- @realm server
-- @func[opt=nil] callback Function to call when the save has completed.
-- @usage ix.char.loaded[1]:Save(function()
-- print("done!")
-- end)
-- > done! -- after a moment
function CHAR:Save(callback)
-- Do not save if the character is for a bot.
if (self.isBot) then
return
end
-- Let plugins/schema determine if the character should be saved.
local shouldSave = hook.Run("CharacterPreSave", self)
if (shouldSave != false) then
-- Run a query to save the character to the database.
local query = mysql:Update("ix_characters")
-- update all character vars
for k, v in pairs(ix.char.vars) do
if (v.field and self.vars[k] != nil and !v.bSaveLoadInitialOnly) then
if (type(v.default) == "table") then
local tblQuery = mysql:Update("ix_characters_data")
tblQuery:Update("data", util.TableToJSON(self.vars[k]) or "NULL")
tblQuery:Where("id", self:GetID())
tblQuery:Where("key", v.field)
tblQuery:Execute()
else
local value = self.vars[k]
if (v.fieldType == ix.type.bool) then
value = value and 1 or 0
end
query:Update(v.field, tostring(value))
end
end
end
query:Where("id", self:GetID())
query:Callback(function()
if (callback) then
callback()
end
hook.Run("CharacterPostSave", self)
end)
query:Execute()
end
end
--- Networks this character's information to make the given player aware of this character's existence. If the receiver is
-- not the owner of this character, it will only be sent a limited amount of data (as it does not need anything else).
-- This is done automatically by the framework.
-- @internal
-- @realm server
-- @player[opt=nil] receiver Player to send the information to. This will sync to all connected players if set to `nil`.
function CHAR:Sync(receiver)
-- Broadcast the character information if receiver is not set.
if (receiver == nil) then
for _, v in ipairs(player.GetAll()) do
self:Sync(v)
end
-- Send all character information if the receiver is the character's owner.
elseif (receiver == self.player) then
local data = {}
for k, v in pairs(self.vars) do
if (ix.char.vars[k] != nil and !ix.char.vars[k].bNoNetworking) then
data[k] = v
end
end
net.Start("ixCharacterInfo")
ix.compnettable:Write(data)
net.WriteUInt(self:GetID(), 32)
net.WriteUInt(self.player:EntIndex(), 8)
net.Send(self.player)
else
local data = {}
for k, v in pairs(ix.char.vars) do
if (!v.bNoNetworking and !v.isLocal) then
data[k] = self.vars[k]
end
end
net.Start("ixCharacterInfo")
ix.compnettable:Write(data)
net.WriteUInt(self:GetID(), 32)
net.WriteUInt(self.player:EntIndex(), 8)
net.Send(receiver)
end
end
-- Sets up the "appearance" related inforomation for the character.
--- Applies the character's appearance and synchronizes information to the owning player.
-- @realm server
-- @internal
-- @bool[opt] bNoNetworking Whether or not to sync the character info to other players
function CHAR:Setup(bNoNetworking)
local client = self:GetPlayer()
if (IsValid(client)) then
-- Set the faction, model, and character index for the player.
local model = self:GetModel()
client:SetNetVar("char", self:GetID())
client:SetTeam(self:GetFaction())
client:SetModel(istable(model) and model[1] or model)
-- Apply saved body groups.
for k, v in pairs(self:GetData("groups", {})) do
client:SetBodygroup(k, v)
end
-- Apply a saved skin.
client:SetSkin(self:GetData("skin", 0))
-- Synchronize the character if we should.
if (!bNoNetworking) then
if (client:IsBot()) then
timer.Simple(0.33, function()
self:Sync()
end)
else
self:Sync()
end
for _, v in ipairs(self:GetInventory(true)) do
if (istable(v)) then
v:AddReceiver(client)
v:Sync(client)
end
end
end
local id = self:GetID()
hook.Run("CharacterLoaded", ix.char.loaded[id])
net.Start("ixCharacterLoaded")
net.WriteUInt(id, 32)
net.Send(client)
self.firstTimeLoaded = true
end
end
--- Forces a player off their current character, and sends them to the character menu to select a character.
-- @realm server
function CHAR:Kick()
-- Kill the player so they are not standing anywhere.
local client = self:GetPlayer()
client:KillSilent()
hook.Run("OnCharacterKicked", self)
local steamID = client:SteamID64()
local id = self:GetID()
local isCurrentChar = self and self:GetID() == id
-- Return the player to the character menu.
if (self and self.steamID == steamID) then
net.Start("ixCharacterKick")
net.WriteBool(isCurrentChar)
net.Send(client)
if (isCurrentChar) then
client:SetNetVar("char", nil)
client:Spawn()
end
end
end
--- Forces a player off their current character, and prevents them from using the character for the specified amount of time.
-- @realm server
-- @number[opt] time Amount of seconds to ban the character for. If left as `nil`, the character will be banned permanently
function CHAR:Ban(time)
time = tonumber(time)
hook.Run("OnCharacterBanned", self, time or true)
if (time) then
-- If time is provided, adjust it so it becomes the un-ban time.
time = os.time() + math.max(math.ceil(time), 60)
end
-- Mark the character as banned and kick the character back to menu.
self:SetData("banned", time or true)
self:Kick()
end
end
--- Returns the player that owns this character.
-- @realm shared
-- @treturn player Player that owns this character
function CHAR:GetPlayer()
-- Set the player from entity index.
if (isnumber(self.player)) then
local client = Entity(self.player)
if (IsValid(client)) then
self.player = client
return client
end
-- Return the player from cache.
elseif (IsValid(self.player)) then
return self.player
-- Search for which player owns this character.
elseif (self.steamID) then
local steamID = self.steamID
for _, v in ipairs(player.GetAll()) do
if (v:SteamID64() == steamID) then
self.player = v
return v
end
end
end
end
function CHAR:GetFactionVar(variable, default)
local faction = ix.faction.Get(self:GetFaction())
if (!faction) then return end
return faction[variable] or default
end
-- Sets up a new character variable.
function ix.char.RegisterVar(key, data)
-- Store information for the variable.
ix.char.vars[key] = data
data.index = data.index or table.Count(ix.char.vars)
local upperName = key:sub(1, 1):upper() .. key:sub(2)
if (SERVER) then
if (data.field) then
if (type(data.default) != "table") then
ix.db.AddToSchema("ix_characters", data.field, data.fieldType or ix.type.string)
end
end
-- Provide functions to change the variable if allowed.
if (!data.bNotModifiable) then
-- Overwrite the set function if desired.
if (data.OnSet) then
CHAR["Set"..upperName] = data.OnSet
-- Have the set function only set on the server if no networking.
elseif (data.bNoNetworking) then
CHAR["Set"..upperName] = function(self, value)
self.vars[key] = value
end
-- If the variable is a local one, only send the variable to the local player.
elseif (data.isLocal) then
CHAR["Set"..upperName] = function(self, value)
local oldVar = self.vars[key]
self.vars[key] = value
net.Start("ixCharacterVarChanged")
net.WriteUInt(self:GetID(), 32)
net.WriteString(key)
net.WriteType(value)
net.Send(self.player)
hook.Run("CharacterVarChanged", self, key, oldVar, value)
end
-- Otherwise network the variable to everyone.
else
CHAR["Set"..upperName] = function(self, value)
local oldVar = self.vars[key]
self.vars[key] = value
net.Start("ixCharacterVarChanged")
net.WriteUInt(self:GetID(), 32)
net.WriteString(key)
net.WriteType(value)
net.Broadcast()
hook.Run("CharacterVarChanged", self, key, oldVar, value)
end
end
end
end
-- The get functions are shared.
-- Overwrite the get function if desired.
if (data.OnGet) then
CHAR["Get"..upperName] = data.OnGet
-- Otherwise return the character variable or default if it does not exist.
else
CHAR["Get"..upperName] = function(self, default)
local value = self.vars[key]
if (value != nil) then
return value
end
if (default == nil) then
return ix.char.vars[key] and (istable(ix.char.vars[key].default) and table.Copy(ix.char.vars[key].default)
or ix.char.vars[key].default)
end
return default
end
end
local alias = data.alias
if (alias) then
if (istable(alias)) then
for _, v in ipairs(alias) do
local aliasName = v:sub(1, 1):upper()..v:sub(2)
CHAR["Get"..aliasName] = CHAR["Get"..upperName]
CHAR["Set"..aliasName] = CHAR["Set"..upperName]
end
elseif (isstring(alias)) then
local aliasName = alias:sub(1, 1):upper()..alias:sub(2)
CHAR["Get"..aliasName] = CHAR["Get"..upperName]
CHAR["Set"..aliasName] = CHAR["Set"..upperName]
end
end
-- Add the variable default to the character object.
CHAR.vars[key] = data.default
end
-- Allows access to the character metatable using ix.meta.character
ix.meta.character = CHAR

View File

@@ -0,0 +1,227 @@
--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
--[[--
Physical object in the game world.
Entities are physical representations of objects in the game world. Helix extends the functionality of entities to interface
between Helix's own classes, and to reduce boilerplate code.
See the [Garry's Mod Wiki](https://wiki.garrysmod.com/page/Category:Entity) for all other methods that the `Player` class has.
]]
-- @classmod Entity
local meta = FindMetaTable("Entity")
local CHAIR_CACHE = {}
-- Add chair models to the cache by checking if its vehicle category is a class.
for _, v in pairs(list.Get("Vehicles")) do
if (v.Category == "Chairs") then
CHAIR_CACHE[v.Model] = true
end
end
--- Returns `true` if this entity is a chair.
-- @realm shared
-- @treturn bool Whether or not this entity is a chair
function meta:IsChair()
return CHAIR_CACHE[self:GetModel()]
end
--- Returns `true` if this entity is a door. Internally, this checks to see if the entity's class has `door` in its name.
-- @realm shared
-- @treturn bool Whether or not the entity is a door
function meta:IsDoor()
local class = self:GetClass()
return (class and class:find("door") != nil)
end
if (SERVER) then
--- Returns `true` if the given entity is a button or door and is locked.
-- @realm server
-- @treturn bool Whether or not this entity is locked; `false` if this entity cannot be locked at all
-- (e.g not a button or door)
function meta:IsLocked()
if (self:IsVehicle()) then
local datatable = self:GetSaveTable()
if (datatable) then
return datatable.VehicleLocked
end
else
local datatable = self:GetSaveTable()
if (datatable) then
return datatable.m_bLocked
end
end
return false
end
--- Returns the neighbouring door entity for double doors.
-- @realm shared
-- @treturn[1] Entity This door's partner
-- @treturn[2] nil If the door does not have a partner
function meta:GetDoorPartner()
return self.ixPartner
end
--- Returns the entity that is blocking this door from opening.
-- @realm server
-- @treturn[1] Entity Entity that is blocking this door
-- @treturn[2] nil If this entity is not a door, or there is no blocking entity
function meta:GetBlocker()
local datatable = self:GetSaveTable()
return datatable.pBlocker
end
--- Blasts a door off its hinges. Internally, this hides the door entity, spawns a physics prop with the same model, and
-- applies force to the prop.
-- @realm server
-- @vector velocity Velocity to apply to the door
-- @number lifeTime How long to wait in seconds before the door is put back on its hinges
-- @bool bIgnorePartner Whether or not to ignore the door's partner in the case of double doors
-- @treturn[1] Entity The physics prop created for the door
-- @treturn nil If the entity is not a door
function meta:BlastDoor(velocity, lifeTime, bIgnorePartner)
if (!self:IsDoor()) then
return
end
if (IsValid(self.ixDummy)) then
self.ixDummy:Remove()
end
velocity = velocity or VectorRand()*100
lifeTime = lifeTime or 120
local partner = self:GetDoorPartner()
if (IsValid(partner) and !bIgnorePartner) then
partner:BlastDoor(velocity, lifeTime, true)
end
local color = self:GetColor()
local dummy = ents.Create("prop_physics")
dummy:SetModel(self:GetModel())
dummy:SetPos(self:GetPos())
dummy:SetAngles(self:GetAngles())
dummy:Spawn()
dummy:SetColor(color)
dummy:SetMaterial(self:GetMaterial())
dummy:SetSkin(self:GetSkin() or 0)
dummy:SetRenderMode(RENDERMODE_TRANSALPHA)
dummy:CallOnRemove("restoreDoor", function()
if (IsValid(self)) then
self:SetNotSolid(false)
self:SetNoDraw(false)
self:DrawShadow(true)
self.ignoreUse = false
self.ixIsMuted = false
for _, v in ipairs(ents.GetAll()) do
if (v:GetParent() == self) then
v:SetNotSolid(false)
v:SetNoDraw(false)
if (v.OnDoorRestored) then
v:OnDoorRestored(self)
end
end
end
end
end)
dummy:SetOwner(self)
dummy:SetCollisionGroup(COLLISION_GROUP_WEAPON)
self:Fire("unlock")
self:Fire("open")
self:SetNotSolid(true)
self:SetNoDraw(true)
self:DrawShadow(false)
self.ignoreUse = true
self.ixDummy = dummy
self.ixIsMuted = true
self:DeleteOnRemove(dummy)
for _, v in ipairs(self:GetBodyGroups() or {}) do
dummy:SetBodygroup(v.id, self:GetBodygroup(v.id))
end
for _, v in ipairs(ents.GetAll()) do
if (v:GetParent() == self) then
v:SetNotSolid(true)
v:SetNoDraw(true)
if (v.OnDoorBlasted) then
v:OnDoorBlasted(self)
end
end
end
dummy:GetPhysicsObject():SetVelocity(velocity)
local uniqueID = "doorRestore"..self:EntIndex()
local uniqueID2 = "doorOpener"..self:EntIndex()
timer.Create(uniqueID2, 1, 0, function()
if (IsValid(self) and IsValid(self.ixDummy)) then
self:Fire("open")
else
timer.Remove(uniqueID2)
end
end)
timer.Create(uniqueID, lifeTime, 1, function()
if (IsValid(self) and IsValid(dummy)) then
uniqueID = "dummyFade"..dummy:EntIndex()
local alpha = 255
timer.Create(uniqueID, 0.1, 255, function()
if (IsValid(dummy)) then
alpha = alpha - 1
dummy:SetColor(ColorAlpha(color, alpha))
if (alpha <= 0) then
dummy:Remove()
end
else
timer.Remove(uniqueID)
end
end)
end
end)
return dummy
end
else
-- Returns the door's slave entity.
function meta:GetDoorPartner()
local owner = self:GetOwner() or self.ixDoorOwner
if (IsValid(owner) and owner:IsDoor()) then
return owner
end
for _, v in ipairs(ents.FindByClass("prop_door_rotating")) do
if (v:GetOwner() == self) then
self.ixDoorOwner = v
return v
end
end
end
end

View File

@@ -0,0 +1,953 @@
--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
--[[--
Holds items within a grid layout.
Inventories are an object that contains `Item`s in a grid layout. Every `Character` will have exactly one inventory attached to
it, which is the only inventory that is allowed to hold bags - any item that has its own inventory (i.e a suitcase). Inventories
can be owned by a character, or it can be individually interacted with as a standalone object. For example, the container plugin
attaches inventories to props, allowing for items to be stored outside of any character inventories and remain "in the world".
You may be looking for the following common functions:
`Add` Which adds an item to the inventory.
`GetItems` Which gets all of the items inside the inventory.
`GetItemByID` Which gets an item in the inventory by it's item ID.
`GetItemAt` Which gets an item in the inventory by it's x and y
`GetID` Which gets the inventory's ID.
`HasItem` Which checks if the inventory has an item.
]]
-- @classmod Inventory
local META = ix.meta.inventory or ix.middleclass("ix_inventory")
META.slots = META.slots or {}
META.w = META.w or 4
META.h = META.h or 4
META.vars = META.vars or {}
META.receivers = META.receivers or {}
--- Returns a string representation of this inventory
-- @realm shared
-- @treturn string String representation
-- @usage print(ix.item.inventories[1])
-- > "inventory[1]"
function META:__tostring()
return "inventory["..(self.id or 0).."]"
end
--- Initializes the inventory with the provided arguments.
-- @realm shared
-- @internal
-- @number id The `Inventory`'s database ID.
-- @number width The inventory's width.
-- @number height The inventory's height.
function META:Initialize(id, width, height)
self.id = id
self.w = width
self.h = height
self.slots = {}
self.vars = {}
self.receivers = {}
end
--- Returns this inventory's database ID. This is guaranteed to be unique.
-- @realm shared
-- @treturn number Unique ID of inventory
function META:GetID()
return self.id or 0
end
--- Sets the grid size of this inventory.
-- @realm shared
-- @internal
-- @number width New width of inventory
-- @number height New height of inventory
function META:SetSize(width, height)
self.w = width
self.h = height
end
--- Returns the grid size of this inventory.
-- @realm shared
-- @treturn number Width of inventory
-- @treturn number Height of inventory
function META:GetSize()
return self.w, self.h
end
-- this is pretty good to debug/develop function to use.
function META:Print(printPos)
for k, v in pairs(self:GetItems()) do
local str = k .. ": " .. v.name
if (printPos) then
str = str .. " (" .. v.gridX .. ", " .. v.gridY .. ")"
end
print(str)
end
end
--- Searches the inventory to find any stacked items.
-- A common problem with developing, is that items will sometimes error out, or get corrupt.
-- Sometimes, the server knows things you don't while developing live
-- This function can be helpful for getting rid of those pesky errors.
-- @realm shared
function META:FindError()
for _, v in pairs(self:GetItems()) do
if (v.width == 1 and v.height == 1) then
continue
end
print("Finding error: " .. v.name)
print("Item Position: " .. v.gridX, v.gridY)
for x = v.gridX, v.gridX + v.width - 1 do
for y = v.gridY, v.gridY + v.height - 1 do
local item = self.slots[x][y]
if (item and item.id != v.id) then
print("Error Found: ".. item.name)
end
end
end
end
end
--- Prints out the id, width, height, slots and each item in each slot of an `Inventory`, used for debugging.
-- @realm shared
function META:PrintAll()
print("------------------------")
print("INVID", self:GetID())
print("INVSIZE", self:GetSize())
if (self.slots) then
for x = 1, self.w do
for y = 1, self.h do
local item = self.slots[x] and self.slots[x][y]
if (item and item.id) then
print(item.name .. "(" .. item.id .. ")", x, y)
end
end
end
end
print("INVVARS")
PrintTable(self.vars or {})
print("------------------------")
end
--- Returns the player that owns this inventory.
-- @realm shared
-- @treturn[1] Player Owning player
-- @treturn[2] nil If no connected player owns this inventory
function META:GetOwner()
for _, v in ipairs(player.GetAll()) do
if (v:GetCharacter() and v:GetCharacter().id == self.owner) then
return v
end
end
end
--- Sets the player that owns this inventory.
-- @realm shared
-- @player owner The player to take control over the inventory.
-- @bool fullUpdate Whether or not to update the inventory immediately to the new owner.
function META:SetOwner(owner, fullUpdate)
if (type(owner) == "Player" and owner:GetNetVar("char")) then
owner = owner:GetNetVar("char")
else
owner = tonumber(owner) or 0 // previously if owner variable wasn't a number, function ended, which caused bags to have and owner all the time
end
if (SERVER) then
if (fullUpdate) then
for _, v in ipairs(player.GetAll()) do
if (v:GetNetVar("char") == owner) then
self:Sync(v, true)
break
end
end
end
local query = mysql:Update("ix_inventories")
query:Update("character_id", owner)
query:Where("inventory_id", self:GetID())
query:Execute()
end
self.owner = owner
end
--- Checks whether a player has access to an inventory
-- @realm shared
-- @internal
-- @player client Player to check access for
-- @treturn bool Whether or not the player has access to the inventory
function META:OnCheckAccess(client)
local bAccess = false
for _, v in ipairs(self:GetReceivers()) do
if (v == client) then
bAccess = true
break
end
end
return bAccess
end
--- Checks whether or not an `Item` can fit into the `Inventory` starting from `x` and `y`.
-- Internally used by FindEmptySlot, in most cases you are better off using that.
-- This function will search if all of the slots within `x + width` and `y + width` are empty,
-- ignoring any space the `Item` itself already occupies.
-- @realm shared
-- @internal
-- @number x The beginning x coordinate to search for.
-- @number y The beginning y coordiate to search for.
-- @number w The `Item`'s width.
-- @number h The `Item`'s height.
-- @item[opt=nil] item2 An `Item`, if any, to ignore when searching.
function META:CanItemFit(x, y, w, h, item2)
local canFit = true
for x2 = 0, w - 1 do
for y2 = 0, h - 1 do
local item = (self.slots[x + x2] or {})[y + y2]
if ((x + x2) > self.w or item) then
if (item2) then
if (item and item.id == item2.id) then
continue
end
end
canFit = false
break
end
end
if (!canFit) then
break
end
end
return canFit
end
--- Returns the amount of slots currently filled in the Inventory.
-- @realm shared
-- @treturn number The amount of slots currently filled.
function META:GetFilledSlotCount()
local count = 0
for x = 1, self.w do
for y = 1, self.h do
if ((self.slots[x] or {})[y]) then
count = count + 1
end
end
end
return count
end
--- Finds an empty slot of a specified width and height.
-- In most cases, to check if an `Item` can actually fit in the `Inventory`,
-- as if it can't, it will just return `nil`.
--
-- FindEmptySlot will loop through all the slots for you, as opposed to `CanItemFit`
-- which you specify an `x` and `y` for.
-- this will call CanItemFit anyway.
-- If you need to check if an item will fit *exactly* at a position, you want CanItemFit instead.
-- @realm shared
-- @number w The width of the `Item` you are trying to fit.
-- @number h The height of the `Item` you are trying to fit.
-- @bool onlyMain Whether or not to search any bags connected to this `Inventory`
-- @treturn[1] number x The `x` coordinate that the `Item` can fit into.
-- @treturn[1] number y The `y` coordinate that the `Item` can fit into.
-- @treturn[2] number x The `x` coordinate that the `Item` can fit into.
-- @treturn[2] number y The `y` coordinate that the `Item` can fit into.
-- @treturn[2] Inventory bagInv If the item was in a bag, it will return the inventory it was in.
-- @see CanItemFit
function META:FindEmptySlot(w, h, onlyMain)
w = w or 1
h = h or 1
if (w > self.w or h > self.h) then
return
end
for y = 1, self.h - (h - 1) do
for x = 1, self.w - (w - 1) do
if (self:CanItemFit(x, y, w, h)) then
return x, y
end
end
end
if (onlyMain != true) then
local bags = self:GetBags()
if (#bags > 0) then
for _, invID in ipairs(bags) do
local bagInv = ix.item.inventories[invID]
if (bagInv) then
local x, y = bagInv:FindEmptySlot(w, h)
if (x and y) then
return x, y, bagInv
end
end
end
end
end
end
--- Returns the item that currently exists within `x` and `y` in the `Inventory`.
-- Items that have a width or height greater than 0 occupy more than 1 x and y.
-- @realm shared
-- @number x The `x` coordindate to search in.
-- @number y The `y` coordinate to search in.
-- @treturn number x The `x` coordinate that the `Item` is located at.
-- @treturn number y The `y` coordinate that the `Item` is located at.
function META:GetItemAt(x, y)
if (self.slots and self.slots[x]) then
return self.slots[x][y]
end
end
--- Removes an item from the inventory.
-- @realm shared
-- @number id The item instance ID to remove
-- @bool[opt=false] bNoReplication Whether or not the item's removal should not be replicated
-- @bool[opt=false] bNoDelete Whether or not the item should not be fully deleted
-- @bool[opt=false] bTransferring Whether or not the item is being transferred to another inventory
-- @treturn number The X position that the item was removed from
-- @treturn number The Y position that the item was removed from
function META:Remove(id, bNoReplication, bNoDelete, bTransferring)
local x2, y2
for x = 1, self.w do
if (self.slots[x]) then
for y = 1, self.h do
local item = self.slots[x][y]
if (item and item.id == id) then
self.slots[x][y] = nil
x2 = x2 or x
y2 = y2 or y
end
end
end
end
if (SERVER and !bNoReplication) then
local receivers = self:GetReceivers()
if (istable(receivers)) then
net.Start("ixInventoryRemove")
net.WriteUInt(id, 32)
net.WriteUInt(self:GetID(), 32)
net.Send(receivers)
end
-- we aren't removing the item - we're transferring it to another inventory
if (!bTransferring) then
hook.Run("InventoryItemRemoved", self, ix.item.instances[id])
end
if (!bNoDelete) then
local item = ix.item.instances[id]
if (item and item.OnRemoved) then
item:OnRemoved()
end
local query = mysql:Delete("ix_items")
query:Where("item_id", id)
query:Execute()
ix.item.instances[id] = nil
end
end
return x2, y2
end
--- Adds a player as a receiver on this `Inventory`
-- Receivers are players who will be networked the items inside the inventory.
--
-- Calling this will *not* automatically sync it's current contents to the client.
-- All future contents will be synced, but not anything that was not synced before this is called.
--
-- This function does not check the validity of `client`, therefore if `client` doesn't exist, it will error.
-- @realm shared
-- @player client The player to add as a receiver.
function META:AddReceiver(client)
self.receivers[client] = true
end
--- The opposite of `AddReceiver`.
-- This function does not check the validity of `client`, therefore if `client` doesn't exist, it will error.
-- @realm shared
-- @player client The player to remove from the receiver list.
function META:RemoveReceiver(client)
self.receivers[client] = nil
end
--- Get all of the receivers this `Inventory` has.
-- Receivers are players who will be networked the items inside the inventory.
--
-- This function will automatically sort out invalid players for you.
-- @realm shared
-- @treturn table result The players who are on the server and allowed to see this table.
function META:GetReceivers()
local result = {}
if (self.receivers) then
for k, _ in pairs(self.receivers) do
if (IsValid(k) and k:IsPlayer()) then
result[#result + 1] = k
end
end
end
return result
end
--- Returns a count of a *specific* `Item`s in the `Inventory`
-- @realm shared
-- @string uniqueID The Unique ID of the item.
-- @bool onlyMain Whether or not to exclude bags that are present from the search.
-- @treturn number The amount of `Item`s this inventory has.
-- @usage local curHighest, winner = 0, false
-- for client, character in ix.util.GetCharacters() do
-- local itemCount = character:GetInventory():GetItemCount('water', false)
-- if itemCount > curHighest then
-- curHighest = itemCount
-- winner = character
-- end
-- end
-- -- Finds the thirstiest character on the server and returns their Character ID or false if no character has water.
function META:GetItemCount(uniqueID, onlyMain)
local i = 0
for _, v in pairs(self:GetItems(onlyMain)) do
if (v.uniqueID == uniqueID) then
i = i + 1
end
end
return i
end
--- Returns a table of all `Item`s in the `Inventory` by their Unique ID.
-- Not to be confused with `GetItemsByID` or `GetItemByID` which take in an Item Instance's ID instead.
-- @realm shared
-- @string uniqueID The Unique ID of the item.
-- @bool onlyMain Whether or not to exclude bags that are present from the search.
-- @treturn number The table of specified `Item`s this inventory has.
function META:GetItemsByUniqueID(uniqueID, onlyMain)
local items = {}
for _, v in pairs(self:GetItems(onlyMain)) do
if (v.uniqueID == uniqueID) then
items[#items + 1] = v
end
end
return items
end
--- Returns a table of `Item`s by their base.
-- @realm shared
-- @string baseID The base to search for.
-- @bool bOnlyMain Whether or not to exclude bags that are present from the search.
function META:GetItemsByBase(baseID, bOnlyMain)
local items = {}
for _, v in pairs(self:GetItems(bOnlyMain)) do
if (v.base == baseID) then
items[#items + 1] = v
end
end
return items
end
--- Get an item by it's specific Database ID.
-- @realm shared
-- @number id The ID to search for.
-- @bool onlyMain Whether or not to exclude bags that are present from the search.
-- @treturn item The item if it exists.
function META:GetItemByID(id, onlyMain)
for _, v in pairs(self:GetItems(onlyMain)) do
if (v.id == id) then
return v
end
end
end
--- Get a table of `Item`s by their specific Database ID.
-- It's important to note that while in 99% of cases,
-- items will have a unique Database ID, developers or random GMod weirdness could
-- cause a second item with the same ID to appear, even though, `ix.item.instances` will only store one of those.
-- The inventory only stores a reference to the `ix.item.instance` ID, not the memory reference itself.
-- @realm shared
-- @number id The ID to search for.
-- @bool onlyMain Whether or not to exclude bags that are present from the search.
-- @treturn item The item if it exists.
function META:GetItemsByID(id, onlyMain)
local items = {}
for _, v in pairs(self:GetItems(onlyMain)) do
if (v.id == id) then
items[#items + 1] = v
end
end
return items
end
-- This function may pretty heavy.
--- Returns a table of all the items that an `Inventory` has.
-- @realm shared
-- @bool onlyMain Whether or not to exclude bags from this search.
-- @treturn table The items this `Inventory` has.
function META:GetItems(onlyMain)
local items = {}
for _, v in pairs(self.slots) do
for _, v2 in pairs(v) do
if (istable(v2) and !items[v2.id]) then
items[v2.id] = v2
v2.data = v2.data or {}
local isBag = v2.data.id
if (isBag and isBag != self:GetID() and onlyMain != true) then
local bagInv = ix.item.inventories[isBag]
if (bagInv) then
local bagItems = bagInv:GetItems()
table.Merge(items, bagItems)
end
end
end
end
end
return items
end
-- This function may pretty heavy.
--- Returns a table of all the items that an `Inventory` has.
-- @realm shared
-- @bool onlyMain Whether or not to exclude bags from this search.
-- @treturn table The items this `Inventory` has.
function META:GetBags()
local invs = {}
for _, v in pairs(self.slots) do
for _, v2 in pairs(v) do
if (istable(v2) and v2.data) then
local isBag = (((v2.base == "base_bags") or v2.isBag) and v2.data.id)
if (!table.HasValue(invs, isBag)) then
if (isBag and isBag != self:GetID()) then
invs[#invs + 1] = isBag
end
end
end
end
end
return invs
end
--- Returns the item with the given unique ID (e.g `"handheld_radio"`) if it exists in this inventory.
-- This method checks both
-- this inventory, and any bags that this inventory has inside of it.
-- @realm shared
-- @string targetID Unique ID of the item to look for
-- @tab[opt] data Item data to check for
-- @treturn[1] Item Item that belongs to this inventory with the given criteria
-- @treturn[2] bool `false` if the item does not exist
-- @see HasItems
-- @see HasItemOfBase
-- @usage local item = inventory:HasItem("handheld_radio")
--
-- if (item) then
-- -- do something with the item table
-- end
function META:HasItem(targetID, data)
local items = self:GetItems()
for _, v in pairs(items) do
if (v.uniqueID == targetID) then
if (data) then
local itemData = v.data
local bFound = true
for dataKey, dataVal in pairs(data) do
if (itemData[dataKey] != dataVal) then
bFound = false
break
end
end
if (!bFound) then
continue
end
end
return v
end
end
return false
end
--- Checks whether or not the `Inventory` has a table of items.
-- This function takes a table with **no** keys and runs in order of first item > last item,
--this is due to the usage of the `#` operator in the function.
--
-- @realm shared
-- @tab targetIDs A table of `Item` Unique ID's.
-- @treturn[1] bool true Whether or not the `Inventory` has all of the items.
-- @treturn[1] table targetIDs Your provided targetIDs table, but it will be empty.
-- @treturn[2] bool false
-- @treturn[2] table targetIDs Table consisting of the items the `Inventory` did **not** have.
-- @usage local itemFilter = {'water', 'water_sparkling'}
-- if not Entity(1):GetCharacter():GetInventory():HasItems(itemFilter) then return end
-- -- Filters out if this player has both a water, and a sparkling water.
function META:HasItems(targetIDs)
local items = self:GetItems()
local count = #targetIDs -- assuming array
targetIDs = table.Copy(targetIDs)
for _, v in pairs(items) do
for k, targetID in ipairs(targetIDs) do
if (v.uniqueID == targetID) then
table.remove(targetIDs, k)
count = count - 1
break
end
end
end
return count <= 0, targetIDs
end
--- Whether or not an `Inventory` has an item of a base, optionally with specified data.
-- This function has an optional `data` argument, which will take a `table`.
-- it will match if the data of the item is correct or not.
--
-- Items which are a base will automatically have base_ prefixed to their Unique ID, if you are having
-- trouble finding your base, that is probably why.
-- @realm shared
-- @string baseID The Item Base's Unique ID.
-- @tab[opt] data The Item's data to compare against.
-- @treturn[1] item The first `Item` of `baseID` that is found and there is no `data` argument or `data` was matched.
-- @treturn[2] false If no `Item`s of `baseID` is found or the `data` argument, if specified didn't match.
-- @usage local bHasWeaponEquipped = Entity(1):GetCharacter():GetInventory():HasItemOfBase('base_weapons', {['equip'] = true})
-- if bHasWeaponEquipped then
-- Entity(1):Notify('One gun is fun, two guns is Woo-tastic.')
-- end
-- -- Notifies the player that they should get some more guns.
function META:HasItemOfBase(baseID, data)
local items = self:GetItems()
for _, v in pairs(items) do
if (v.base == baseID) then
if (data) then
local itemData = v.data
local bFound = true
for dataKey, dataVal in pairs(data) do
if (itemData[dataKey] != dataVal) then
bFound = false
break
end
end
if (!bFound) then
continue
end
end
return v
end
end
return false
end
if (SERVER) then
--- Sends a specific slot to a character.
-- This will *not* send all of the slots of the `Item` to the character, items can occupy multiple slots.
--
-- This will call `OnSendData` on the Item using all of the `Inventory`'s receivers.
--
-- This function should *not* be used to sync an entire inventory, if you need to do that, use `AddReceiver` and `Sync`.
-- @realm server
-- @internal
-- @number x The Inventory x position to send.
-- @number y The Inventory y position to send.
-- @item[opt] item The item to send, if any.
-- @see AddReceiver
-- @see Sync
function META:SendSlot(x, y, item)
local receivers = self:GetReceivers()
local sendData = item and item.data and !table.IsEmpty(item.data) and item.data or {}
net.Start("ixInventorySet")
net.WriteUInt(self:GetID(), 32)
net.WriteUInt(x, 6)
net.WriteUInt(y, 6)
net.WriteString(item and item.uniqueID or "")
net.WriteUInt(item and item.id or 0, 32)
net.WriteUInt(self.owner or 0, 32)
net.WriteTable(sendData)
net.Send(receivers)
if (item) then
for _, v in pairs(receivers) do
item:Call("OnSendData", v)
end
end
end
--- Sets whether or not an `Inventory` should save.
-- This will prevent an `Inventory` from updating in the Database, if the inventory is already saved,
-- it will not be deleted when unloaded.
-- @realm server
-- @bool bNoSave Whether or not the Inventory should save.
function META:SetShouldSave(bNoSave)
self.noSave = bNoSave
end
--- Gets whether or not an `Inventory` should save.
-- Inventories that are marked to not save will not update in the Database, if they inventory is already saved,
-- it will not be deleted when unloaded.
-- @realm server
-- @treturn[1] bool Returns the field `noSave`.
-- @treturn[2] bool Returns true if the field `noSave` is not registered to this inventory.
function META:GetShouldSave()
return self.noSave or true
end
--- Add an item to the inventory.
-- @realm server
-- @param uniqueID The item unique ID (e.g `"handheld_radio"`) or instance ID (e.g `1024`) to add to the inventory
-- @number[opt=1] quantity The quantity of the item to add
-- @tab data Item data to add to the item
-- @number[opt=nil] x The X position for the item
-- @number[opt=nil] y The Y position for the item
-- @bool[opt=false] noReplication Whether or not the item's addition should not be replicated
-- @treturn[1] bool Whether the add was successful or not
-- @treturn[1] string The error, if applicable
-- @treturn[2] number The X position that the item was added to
-- @treturn[2] number The Y position that the item was added to
-- @treturn[2] number The inventory ID that the item was added to
function META:Add(uniqueID, quantity, data, x, y, noReplication)
quantity = quantity or 1
if (quantity < 1) then
return false, "noOwner"
end
if (!isnumber(uniqueID) and quantity > 1) then
for _ = 1, quantity do
local bSuccess, error = self:Add(uniqueID, 1, data)
if (!bSuccess) then
return false, error
end
end
return true
end
local client = self.GetOwner and self:GetOwner() or nil
local item = isnumber(uniqueID) and ix.item.instances[uniqueID] or ix.item.list[uniqueID]
local targetInv = self
local bagInv
if (!item) then
return false, "invalidItem"
end
if (isnumber(uniqueID)) then
local oldInvID = item.invID
if (!x and !y) then
x, y, bagInv = self:FindEmptySlot(item.width, item.height)
end
if (bagInv) then
targetInv = bagInv
end
-- we need to check for owner since the item instance already exists
if (!item.bAllowMultiCharacterInteraction and IsValid(client) and client:GetCharacter() and
item:GetPlayerID() == client:SteamID64() and item:GetCharacterID() != client:GetCharacter():GetID()) then
return false, "itemOwned"
end
if (hook.Run("CanTransferItem", item, ix.item.inventories[0], targetInv, x, y) == false) then
return false, "notAllowed"
end
if (x and y) then
targetInv.slots[x] = targetInv.slots[x] or {}
targetInv.slots[x][y] = true
item.gridX = x
item.gridY = y
item.invID = targetInv:GetID()
for x2 = 0, item.width - 1 do
local index = x + x2
for y2 = 0, item.height - 1 do
targetInv.slots[index] = targetInv.slots[index] or {}
targetInv.slots[index][y + y2] = item
end
end
if (!noReplication) then
targetInv:SendSlot(x, y, item)
end
if (!self.noSave) then
local query = mysql:Update("ix_items")
query:Update("inventory_id", targetInv:GetID())
query:Update("x", x)
query:Update("y", y)
query:Where("item_id", item.id)
query:Execute()
end
hook.Run("InventoryItemAdded", ix.item.inventories[oldInvID], targetInv, item)
return x, y, targetInv:GetID()
else
return false, "noFit"
end
else
if (!x and !y) then
x, y, bagInv = self:FindEmptySlot(item.width, item.height)
end
if (bagInv) then
targetInv = bagInv
end
if (hook.Run("CanTransferItem", item, ix.item.inventories[0], targetInv, x, y) == false) then
return false, "notAllowed"
end
if (x and y) then
for x2 = 0, item.width - 1 do
local index = x + x2
for y2 = 0, item.height - 1 do
targetInv.slots[index] = targetInv.slots[index] or {}
targetInv.slots[index][y + y2] = true
end
end
local characterID
local playerID
if (self.owner) then
local character = ix.char.loaded[self.owner]
if (character) then
characterID = character.id
playerID = character.steamID
end
end
ix.item.Instance(targetInv:GetID(), uniqueID, data, x, y, function(newItem)
newItem.gridX = x
newItem.gridY = y
for x2 = 0, newItem.width - 1 do
local index = x + x2
for y2 = 0, newItem.height - 1 do
targetInv.slots[index] = targetInv.slots[index] or {}
targetInv.slots[index][y + y2] = newItem
end
end
if (!noReplication) then
targetInv:SendSlot(x, y, newItem)
end
hook.Run("InventoryItemAdded", nil, targetInv, newItem)
end, characterID, playerID)
return x, y, targetInv:GetID()
else
return false, "noFit"
end
end
end
--- Syncs the `Inventory` to the receiver.
-- This will call Item.OnSendData on every item in the `Inventory`.
-- @realm server
-- @player receiver The player to
function META:Sync(receiver)
local slots = {}
for x, items in pairs(self.slots) do
for y, item in pairs(items) do
if (istable(item) and item.gridX == x and item.gridY == y) then
slots[#slots + 1] = {x, y, item.uniqueID, item.id, item.data}
end
end
end
net.Start("ixInventorySync")
net.WriteTable(slots)
net.WriteUInt(self:GetID(), 32)
net.WriteUInt(self.w, 6)
net.WriteUInt(self.h, 6)
net.WriteType(self.owner)
net.WriteTable(self.vars or {})
net.Send(receiver)
for _, v in pairs(self:GetItems()) do
v:Call("OnSendData", receiver)
end
end
end
ix.meta.inventory = META

View File

@@ -0,0 +1,709 @@
--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
--[[--
Interactable entities that can be held in inventories.
Items are objects that are contained inside of an `Inventory`, or as standalone entities if they are dropped in the world. They
usually have functionality that provides more gameplay aspects to the schema. For example, the zipties in the HL2 RP schema
allow a player to tie up and search a player.
For an item to have an actual presence, they need to be instanced (usually with `ix.item.Instance`). Items describe the
properties, while instances are a clone of these properties that can have their own unique data (e.g an ID card will have the
same name but different numerical IDs). You can think of items as the class, while instances are objects of the `Item` class.
## Creating item classes (`ItemStructure`)
Item classes are defined in their own file inside of your schema or plugin's `items/` folder. In these item class files you
specify how instances of the item behave. This includes default values for basic things like the item's name and description,
to more advanced things by overriding extra methods from an item base. See `ItemStructure` for information on how to define
a basic item class.
Item classes in this folder are automatically loaded by Helix when the server starts up.
## Item bases
If many items share the same functionality (i.e a can of soda and a bottle of water can both be consumed), then you might want
to consider using an item base to reduce the amount of duplication for these items. Item bases are defined the same way as
regular item classes, but they are placed in the `items/base/` folder in your schema or plugin. For example, a `consumables`
base would be in `items/base/sh_consumables.lua`.
Any items that you want to use this base must be placed in a subfolder that has the name of the base you want that item to use.
For example, for a bottled water item to use the consumable base, it must be placed in `items/consumables/sh_bottled_water.lua`.
This also means that you cannot place items into subfolders as you wish, since the framework will try to use an item base that
doesn't exist.
The default item bases that come with Helix are:
- `ammo` - provides ammo to any items with the `weapons` base
- `bags` - holds an inventory that other items can be stored inside of
- `outfit` - changes the appearance of the player that wears it
- `pacoutfit` - changes the appearance of the player that wears it using PAC3
- `weapons` - makes any SWEP into an item that can be equipped
These item bases usually come with extra values and methods that you can define/override in order to change their functionality.
You should take a look at the source code for these bases to see their capabilities.
## Item functions (`ItemFunctionStructure`)
Requiring players to interact with items in order for them to do something is quite common. As such, there is already a built-in
mechanism to allow players to right-click items and show a list of available options. Item functions are defined in your item
class file in the `ITEM.functions` table. See `ItemFunctionStructure` on how to define them.
Helix comes with `drop`, `take`, and `combine` item functions by default that allows items to be dropped from a player's
inventory, picked up from the world, and combining items together. These can be overridden by defining an item function
in your item class file with the same name. See the `bags` base for example usage of the `combine` item function.
## Item icons (`ItemIconStructure`)
Icons for items sometimes don't line up quite right, in which case you can modify an item's `iconCam` value and line up the
rendered model as needed. See `ItemIconStructure` for more details.
]]
-- @classmod Item
--[[--
All item functions live inside of an item's `functions` table. An item function entry includes a few methods and fields you can
use to customize the functionality and appearance of the item function. An example item function is below:
-- this item function's unique ID is "MyFunction"
ITEM.functions.MyFunction = {
name = "myFunctionPhrase", -- uses the "myFunctionPhrase" language phrase when displaying in the UI
tip = "myFunctionDescription", -- uses the "myFunctionDescription" language phrase when displaying in the UI
icon = "icon16/add.png", -- path to the icon material
OnRun = function(item)
local client = item.player
local entity = item.entity -- only set if this is function is being ran while the item is in the world
if (IsValid(client)) then
client:ChatPrint("This is a test.")
if (IsValid(entity)) then
client:ChatPrint(entity:GetName())
end
end
-- do not remove this item from the owning player's inventory
return false
end,
OnCanRun = function(item)
-- only allow admins to run this item function
local client = item.player
return IsValid(client) and client:IsAdmin()
end
}
]]
-- @table ItemFunctionStructure
-- @realm shared
-- @field[type=string,opt] name Language phrase to use when displaying this item function's name in the UI. If not specified,
-- then it will use the unique ID of the item function
-- @field[type=string,opt] tip Language phrase to use when displaying this item function's detailed description in the UI
-- @field[type=string,opt] icon Path to the material to use when displaying this item function's icon
-- @field[type=function] OnRun Function to call when the item function is ran. This function is **ONLY** ran on the server.
--
-- The only argument passed into this function is the instance of the item being called. The instance will have its `player`
-- field set if the item function is being ran by a player (which it should be most of the time). It will also have its `entity`
-- field set if the item function is being ran while the item is in the world, and not in a player's inventory.
--
-- The item will be removed after the item function is ran. If you want to prevent this behaviour, then you can return `false`
-- in this function. See the example above.
-- @field[type=function] OnCanRun Function to call when checking whether or not this item function can be ran. This function is
-- ran **BOTH** on the client and server.
--
-- The arguments are the same as `OnCanRun`, and the `player` and `entity` fields will be set on the item instance accordingly.
-- Returning `true` will allow the item function to be ran. Returning `false` will prevent it from running and additionally
-- hide it from the UI. See the example above.
-- @field[type=function,opt] OnClick This function is called when the player clicks on this item function's entry in the UI.
-- This function is ran **ONLY** on the client, and is only ran if `OnCanRun` succeeds.
--
-- The same arguments from `OnCanRun` and `OnRun` apply to this function.
--[[--
Changing the way an item's icon is rendered is done by modifying the location and angle of the model, as well as the FOV of the
camera. You can tweak the values in code, or use the `ix_dev_icon` console command to visually position the model and camera. An
example entry for an item's icon is below:
ITEM.iconCam = {
pos = Vector(0, 0, 60),
ang = Angle(90, 0, 0),
fov = 45
}
Note that this will probably not work for your item's specific model, since every model has a different size, origin, etc. All
item icons need to be tweaked individually.
]]
-- @table ItemIconStructure
-- @realm client
-- @field[type=vector] pos Location of the model relative to the camera. +X is forward, +Z is up
-- @field[type=angle] ang Angle of the model
-- @field[type=number] fov FOV of the camera
--[[--
When creating an item class, the file will have a global table `ITEM` set that you use to define the item's values/methods. An
example item class is below:
`items/sh_brick.lua`
ITEM.name = "Brick"
ITEM.description = "A brick. Pretty self-explanatory. You can eat it but you'll probably lose some teeth."
ITEM.model = Model("models/props_debris/concrete_cynderblock001.mdl")
ITEM.width = 1
ITEM.height = 1
ITEM.price = 25
Note that the below list only includes the default fields available for *all* items, and not special ones defined in custom
item bases.
]]
-- @table ItemStructure
-- @realm shared
-- @field[type=string] name Display name of the item
-- @field[type=string] description Detailed description of the item
-- @field[type=string] model Model to use for the item's icon and when it's dropped in the world
-- @field[type=number,opt=1] width Width of the item in grid cells
-- @field[type=number,opt=1] height Height of the item in grid cells
-- @field[type=number,opt=0] price How much money it costs to purchase this item in the business menu
-- @field[type=string,opt] category Name of the category this item belongs to - mainly used for the business menu
-- @field[type=boolean,opt=false] noBusiness Whether or not to disallow purchasing this item in the business menu
-- @field[type=table,opt] factions List of factions allowed to purchase this item in the business menu
-- @field[type=table,opt] classes List of character classes allowed to purchase this item in the business menu. Classes are
-- checked after factions, so the character must also be in an allowed faction
-- @field[type=string,opt] flag List of flags (as a string - e.g `"a"` or `"abc"`) allowed to purchase this item in the
-- business menu. Flags are checked last, so the character must also be in an allowed faction and class
-- @field[type=ItemIconStructure,opt] iconCam How to render this item's icon
-- @field[type=table,opt] functions List of all item functions that this item has. See `ItemFunctionStructure` on how to define
-- new item functions
local ITEM = ix.meta.item or {}
ITEM.__index = ITEM
ITEM.name = "Undefined"
ITEM.description = ITEM.description or "An item that is undefined."
ITEM.id = ITEM.id or 0
ITEM.uniqueID = "undefined"
--- Returns a string representation of this item.
-- @realm shared
-- @treturn string String representation
-- @usage print(ix.item.instances[1])
-- > "item[1]"
function ITEM:__tostring()
return "item["..self.uniqueID.."]["..self.id.."]"
end
--- Returns true if this item is equal to another item. Internally, this checks item IDs.
-- @realm shared
-- @item other Item to compare to
-- @treturn bool Whether or not this item is equal to the given item
-- @usage print(ix.item.instances[1] == ix.item.instances[2])
-- > false
function ITEM:__eq(other)
return self:GetID() == other:GetID()
end
--- Returns this item's database ID. This is guaranteed to be unique.
-- @realm shared
-- @treturn number Unique ID of item
function ITEM:GetID()
return self.id
end
--- Returns the name of the item.
-- @realm shared
-- @treturn string The name of the item
function ITEM:GetName()
return (CLIENT and L(self.name) or self.name)
end
--- Returns the description of the item.
-- @realm shared
-- @treturn string The description of the item
function ITEM:GetDescription()
if (!self.description) then return "ERROR" end
return L(self.description or "noDesc")
end
--- Returns the model of the item.
-- @realm shared
-- @treturn string The model of the item
function ITEM:GetModel()
return self.model
end
--- Returns the skin of the item.
-- @realm shared
-- @treturn number The skin of the item
function ITEM:GetSkin()
return self.skin or 0
end
--- Returns the bodygroup of the item model.
-- @realm shared
-- @return The body groups to set. Each single-digit number in the string represents a separate bodygroup.
function ITEM:GetModelBodygroups()
return self.modelBodygroups or ""
end
function ITEM:GetMaterial()
return nil
end
--- Returns the ID of the owning character, if one exists.
-- @realm shared
-- @treturn number The owning character's ID
function ITEM:GetCharacterID()
return self.characterID
end
--- Returns the SteamID64 of the owning player, if one exists.
-- @realm shared
-- @treturn number The owning player's SteamID64
function ITEM:GetPlayerID()
return self.playerID
end
--- A utility function which prints the item's details.
-- @realm shared
-- @bool[opt=false] detail Whether additional detail should be printed or not(Owner, X position, Y position)
function ITEM:Print(detail)
if (detail == true) then
print(Format("%s[%s]: >> [%s](%s,%s)", self.uniqueID, self.id, self.owner, self.gridX, self.gridY))
else
print(Format("%s[%s]", self.uniqueID, self.id))
end
end
--- A utility function printing the item's stored data.
-- @realm shared
function ITEM:PrintData()
self:Print(true)
print("ITEM DATA:")
for k, v in pairs(self.data) do
print(Format("[%s] = %s", k, v))
end
end
--- Calls one of the item's methods.
-- @realm shared
-- @string method The method to be called
-- @player client The client to pass when calling the method, if applicable
-- @entity entity The eneity to pass when calling the method, if applicable
-- @param ... Arguments to pass to the method
-- @return The values returned by the method
function ITEM:Call(method, client, entity, ...)
local oldPlayer, oldEntity = self.player, self.entity
self.player = client or self.player
self.entity = entity or self.entity
if (isfunction(self[method])) then
local results = {self[method](self, ...)}
self.player = nil
self.entity = nil
return unpack(results)
end
self.player = oldPlayer
self.entity = oldEntity
end
--- Returns the player that owns this item.
-- @realm shared
-- @treturn player Player owning this item
function ITEM:GetOwner()
local inventory = ix.item.inventories[self.invID]
if (inventory) then
return inventory.GetOwner and inventory:GetOwner()
end
local id = self:GetID()
for _, v in ipairs(player.GetAll()) do
local character = v:GetCharacter()
if (character and character:GetInventory():GetItemByID(id)) then
return v
end
end
end
--- Sets a key within the item's data.
-- @realm shared
-- @string key The key to store the value within
-- @param[opt=nil] value The value to store within the key
-- @tab[opt=nil] receivers The players to replicate the data on
-- @bool[opt=false] noSave Whether to disable saving the data on the database or not
-- @bool[opt=false] noCheckEntity Whether to disable setting the data on the entity, if applicable
function ITEM:SetData(key, value, receivers, noSave, noCheckEntity)
self.data = self.data or {}
self.data[key] = value
if (SERVER) then
if (!noCheckEntity) then
local ent = self:GetEntity()
if (IsValid(ent)) then
local data = ent:GetNetVar("data", {})
data[key] = value
ent:SetNetVar("data", data)
end
end
end
if (receivers != false and (receivers or self:GetOwner())) then
net.Start("ixInventoryData")
net.WriteUInt(self:GetID(), 32)
net.WriteString(key)
net.WriteType(value)
net.Send(receivers or self:GetOwner())
end
if (!noSave and ix.db) then
local query = mysql:Update("ix_items")
query:Update("data", util.TableToJSON(self.data))
query:Where("item_id", self:GetID())
query:Execute()
end
end
--- Returns the value stored on a key within the item's data.
-- @realm shared
-- @string key The key in which the value is stored
-- @param[opt=nil] default The value to return in case there is no value stored in the key
-- @return The value stored within the key
function ITEM:GetData(key, default)
self.data = self.data or {}
if (self.data) then
if (key == true) then
return self.data
end
local value = self.data[key]
if (value != nil) then
return value
elseif (IsValid(self.entity)) then
local data = self.entity:GetNetVar("data", {})
value = data[key]
if (value != nil) then
return value
end
end
else
self.data = {}
end
if (default != nil) then
return default
end
return
end
--- Changes the function called on specific events for the item.
-- @realm shared
-- @string name The name of the hook
-- @func func The function to call once the event occurs
function ITEM:Hook(name, func)
if (name) then
self.hooks[name] = func
end
end
--- Changes the function called after hooks for specific events for the item.
-- @realm shared
-- @string name The name of the hook
-- @func func The function to call after the original hook was called
function ITEM:PostHook(name, func)
if (name) then
self.postHooks[name] = func
end
end
--- Removes the item.
-- @realm shared
-- @bool bNoReplication Whether or not the item's removal should not be replicated.
-- @bool bNoDelete Whether or not the item should not be fully deleted
-- @treturn bool Whether the item was successfully deleted or not
function ITEM:Remove(bNoReplication, bNoDelete)
local inv = ix.item.inventories[self.invID]
if (self.invID > 0 and inv) then
local failed = false
for x = self.gridX, self.gridX + (self.width - 1) do
if (inv.slots[x]) then
for y = self.gridY, self.gridY + (self.height - 1) do
local item = inv.slots[x][y]
if (item and item.id == self.id) then
inv.slots[x][y] = nil
else
failed = true
end
end
end
end
if (failed) then
local items = inv:GetItems()
inv.slots = {}
for _, v in pairs(items) do
if (v.invID == inv:GetID()) then
for x = self.gridX, self.gridX + (self.width - 1) do
for y = self.gridY, self.gridY + (self.height - 1) do
inv.slots[x][y] = v.id
end
end
end
end
if (IsValid(inv.owner) and inv.owner:IsPlayer()) then
inv:Sync(inv.owner, true)
end
return false
end
else
-- @todo definition probably isn't needed
inv = ix.item.inventories[self.invID]
if (inv) then
ix.item.inventories[self.invID][self.id] = nil
end
end
if (SERVER and !bNoReplication) then
local entity = self:GetEntity()
if (IsValid(entity)) then
entity:Remove()
end
local receivers = inv.GetReceivers and inv:GetReceivers()
if (self.invID != 0 and istable(receivers)) then
net.Start("ixInventoryRemove")
net.WriteUInt(self.id, 32)
net.WriteUInt(self.invID, 32)
net.Send(receivers)
end
if (!bNoDelete) then
local item = ix.item.instances[self.id]
if (item and item.OnRemoved) then
item:OnRemoved()
end
local query = mysql:Delete("ix_items")
query:Where("item_id", self.id)
query:Execute()
ix.item.instances[self.id] = nil
end
end
return true
end
if (SERVER) then
--- Returns the item's entity.
-- @realm server
-- @treturn entity The entity of the item
function ITEM:GetEntity()
local id = self:GetID()
for _, v in ipairs(ents.FindByClass("ix_item")) do
if (v.ixItemID == id) then
return v
end
end
end
--- Spawn an item entity based off the item table.
-- @realm server
-- @param[type=vector] position The position in which the item's entity will be spawned
-- @param[type=angle] angles The angles at which the item's entity will spawn
-- @treturn entity The spawned entity
function ITEM:Spawn(position, angles)
-- Check if the item has been created before.
if (ix.item.instances[self.id]) then
local client
-- Spawn the actual item entity.
local entity = ents.Create("ix_item")
entity:Spawn()
entity:SetAngles(angles or Angle(0, 0, 0))
entity:SetItem(self.id)
-- If the first argument is a player, then we will find a position to drop
-- the item based off their aim.
if (type(position) == "Player") then
client = position
position = position:GetItemDropPos(entity)
end
entity:SetPos(position)
if (IsValid(client)) then
entity.ixSteamID = client:SteamID()
entity.ixCharID = client:GetCharacter():GetID()
entity:SetNetVar("owner", entity.ixCharID)
end
hook.Run("OnItemSpawned", entity)
return entity
end
end
--- Transfers an item to a specific inventory.
-- @realm server
-- @number invID The inventory to transfer the item to
-- @number x The X position to which the item should be transferred on the new inventory
-- @number y The Y position to which the item should be transferred on the new inventory
-- @player client The player to which the item is being transferred
-- @bool noReplication Whether there should be no replication of the transferral
-- @bool isLogical Whether or not an entity should spawn if the item is transferred to the world
-- @treturn[1] bool Whether the transfer was successful or not
-- @treturn[1] string The error, if applicable
function ITEM:Transfer(invID, x, y, client, noReplication, isLogical)
invID = invID or 0
if (self.invID == invID) then
return false, "same inv"
end
local inventory = ix.item.inventories[invID]
local curInv = ix.item.inventories[self.invID or 0]
if (curInv and !IsValid(client)) then
client = curInv.GetOwner and curInv:GetOwner() or nil
end
-- check if this item doesn't belong to another one of this player's characters
local itemPlayerID = self:GetPlayerID()
local itemCharacterID = self:GetCharacterID()
if (!self.bAllowMultiCharacterInteraction and IsValid(client) and client:GetCharacter()) then
local playerID = client:SteamID64()
local characterID = client:GetCharacter():GetID()
if (itemPlayerID and itemCharacterID) then
if (itemPlayerID == playerID and itemCharacterID != characterID) then
return false, "itemOwned"
end
else
self.characterID = characterID
self.playerID = playerID
local query = mysql:Update("ix_items")
query:Update("character_id", characterID)
query:Update("player_id", playerID)
query:Where("item_id", self:GetID())
query:Execute()
end
end
if (hook.Run("CanTransferItem", self, curInv, inventory, x, y) == false) then
return false, "notAllowed"
end
local authorized = false
if (inventory and inventory.OnAuthorizeTransfer and inventory:OnAuthorizeTransfer(client, curInv, self)) then
authorized = true
end
if (!authorized and self.CanTransfer and self:CanTransfer(curInv, inventory) == false) then
return false, "notAllowed"
end
if (curInv) then
if (invID and invID > 0 and inventory) then
local targetInv = inventory
local bagInv
if (!x and !y) then
x, y, bagInv = inventory:FindEmptySlot(self.width, self.height)
end
if (bagInv) then
targetInv = bagInv
end
if (!x or !y) then
return false, "noFit"
end
local prevID = self.invID
local status, result = targetInv:Add(self.id, nil, nil, x, y, noReplication)
if (status) then
if (self.invID > 0 and prevID != 0) then
-- we are transferring this item from one inventory to another
curInv:Remove(self.id, false, true, true)
if (self.OnTransferred) then
self:OnTransferred(curInv, inventory)
end
hook.Run("OnItemTransferred", self, curInv, inventory)
return true
elseif (self.invID > 0 and prevID == 0) then
-- we are transferring this item from the world to an inventory
ix.item.inventories[0][self.id] = nil
if (self.OnTransferred) then
self:OnTransferred(curInv, inventory)
end
hook.Run("OnItemTransferred", self, curInv, inventory)
return true
end
else
return false, result
end
elseif (IsValid(client)) then
-- we are transferring this item from an inventory to the world
self.invID = 0
curInv:Remove(self.id, false, true)
local query = mysql:Update("ix_items")
query:Update("inventory_id", 0)
query:Where("item_id", self.id)
query:Execute()
inventory = ix.item.inventories[0]
inventory[self:GetID()] = self
if (self.OnTransferred) then
self:OnTransferred(curInv, inventory)
end
hook.Run("OnItemTransferred", self, curInv, inventory)
if (!isLogical) then
return self:Spawn(client)
end
return true
else
return false, "noOwner"
end
else
return false, "invalidInventory"
end
end
end
ix.meta.item = ITEM

View File

@@ -0,0 +1,690 @@
--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
--[[--
Physical representation of connected player.
`Player`s are a type of `Entity`. They are a physical representation of a `Character` - and can possess at most one `Character`
object at a time that you can interface with.
See the [Garry's Mod Wiki](https://wiki.garrysmod.com/page/Category:Player) for all other methods that the `Player` class has.
]]
-- @classmod Player
local meta = FindMetaTable("Player")
if (SERVER) then
--- Returns the amount of time the player has played on the server.
-- @realm shared
-- @treturn number Number of seconds the player has played on the server
function meta:GetPlayTime()
return self.ixPlayTime + (RealTime() - (self.ixJoinTime or RealTime()))
end
else
ix.playTime = ix.playTime or 0
function meta:GetPlayTime()
return ix.playTime + (RealTime() - ix.joinTime or 0)
end
end
--- Returns `true` if the player has their weapon raised.
-- @realm shared
-- @treturn bool Whether or not the player has their weapon raised
function meta:IsWepRaised()
return self:GetNetVar("raised", false)
end
--- Returns `true` if the player is restricted - that is to say that they are considered "bound" and cannot interact with
-- objects normally (e.g hold weapons, use items, etc). An example of this would be a player in handcuffs.
-- @realm shared
-- @treturn bool Whether or not the player is restricted
function meta:IsRestricted()
return self:GetNetVar("restricted", false)
end
--- Returns `true` if the player is able to shoot their weapon.
-- @realm shared
-- @treturn bool Whether or not the player can shoot their weapon
function meta:CanShootWeapon()
return self:GetNetVar("canShoot", true)
end
local vectorLength2D = FindMetaTable("Vector").Length2D
--- Returns `true` if the player is running. Running in this case means that their current speed is greater than their
-- regularly set walk speed.
-- @realm shared
-- @treturn bool Whether or not the player is running
function meta:IsRunning()
return vectorLength2D(self:GetVelocity()) > (self:GetWalkSpeed() + 10)
end
--- Returns `true` if the player currently has a female model. This checks if the model has `female`, `alyx` or `mossman` in its
-- name, or if the player's model class is `citizen_female`.
-- @realm shared
-- @treturn bool Whether or not the player has a female model
function meta:IsFemale()
local model = self:GetModel():lower()
return (model:find("female") or model:find("alyx") or model:find("mossman")) != nil or
ix.anim.GetModelClass(model) == "citizen_female"
end
--- Whether or not this player is stuck and cannot move.
-- @realm shared
-- @treturn bool Whether or not this player is stuck
function meta:IsStuck()
return util.TraceEntity({
start = self:GetPos(),
endpos = self:GetPos(),
filter = self
}, self).StartSolid
end
--- Returns a good position in front of the player for an entity to be placed. This is usually used for item entities.
-- @realm shared
-- @entity entity Entity to get a position for
-- @treturn vector Best guess for a good drop position in front of the player
-- @usage local position = client:GetItemDropPos(entity)
-- entity:SetPos(position)
function meta:GetItemDropPos(entity)
local data = {}
local trace
data.start = self:GetShootPos()
data.endpos = self:GetShootPos() + self:GetAimVector() * 86
data.filter = self
if (IsValid(entity)) then
-- use a hull trace if there's a valid entity to avoid collisions
local mins, maxs = entity:GetRotatedAABB(entity:OBBMins(), entity:OBBMaxs())
data.mins = mins
data.maxs = maxs
data.filter = {entity, self}
trace = util.TraceHull(data)
else
-- trace along the normal for a few units so we can attempt to avoid a collision
trace = util.TraceLine(data)
data.start = trace.HitPos
data.endpos = data.start + trace.HitNormal * 48
trace = util.TraceLine(data)
end
return trace.HitPos
end
--- Performs a time-delay action that requires this player to look at an entity. If this player looks away from the entity
-- before the action timer completes, the action is cancelled. This is usually used in conjunction with `SetAction` to display
-- progress to the player.
-- @realm shared
-- @entity entity that this player must look at
-- @func callback Function to call when the timer completes
-- @number time How much time in seconds this player must look at the entity for
-- @func[opt=nil] onCancel Function to call when the timer has been cancelled
-- @number[opt=96] distance Maximum distance a player can move away from the entity before the action is cancelled
-- @see SetAction
-- @usage client:SetAction("Searching...", 4) -- for displaying the progress bar
-- client:DoStaredAction(entity, function()
-- print("hello!")
-- end)
-- -- prints "hello!" after looking at the entity for 4 seconds
function meta:DoStaredAction(entity, callback, time, onCancel, distance)
local uniqueID = "ixStare"..self:SteamID64()
local data = {}
data.filter = self
timer.Create(uniqueID, 0.1, time / 0.1, function()
if (IsValid(self) and IsValid(entity)) then
data.start = self:GetShootPos()
data.endpos = data.start + self:GetAimVector()*(distance or 96)
if (util.TraceLine(data).Entity != entity) then
timer.Remove(uniqueID)
if (onCancel) then
onCancel()
end
elseif (callback and timer.RepsLeft(uniqueID) == 0) then
callback()
end
else
timer.Remove(uniqueID)
if (onCancel) then
onCancel()
end
end
end)
end
--- Resets all bodygroups this player's model has to their defaults (`0`).
-- @realm shared
function meta:ResetBodygroups()
for i = 0, (self:GetNumBodyGroups() - 1) do
self:SetBodygroup(i, 0)
end
end
function meta:GetFactionVar(variable, default)
if (!self:GetCharacter()) then return end
local faction = ix.faction.Get(self:Team())
if (!faction) then return end
return faction[variable] or default
end
if (SERVER) then
util.AddNetworkString("ixActionBar")
util.AddNetworkString("ixActionBarReset")
util.AddNetworkString("ixStringRequest")
util.AddNetworkString("ixConfirmationRequest")
--- Sets whether or not this player's current weapon is raised.
-- @realm server
-- @bool bState Whether or not the raise the weapon
-- @entity[opt=GetActiveWeapon()] weapon Weapon to raise or lower. You should pass this argument if you already have a
-- reference to this player's current weapon to avoid an expensive lookup for this player's current weapon.
function meta:SetWepRaised(bState, weapon)
weapon = weapon or self:GetActiveWeapon()
if (IsValid(weapon)) then
local bCanShoot = !bState and weapon.FireWhenLowered or bState
self:SetNetVar("raised", bState)
if (bCanShoot) then
-- delay shooting while the raise animation is playing
timer.Create("ixWeaponRaise" .. self:SteamID64(), 1, 1, function()
if (IsValid(self)) then
self:SetNetVar("canShoot", true)
end
end)
else
timer.Remove("ixWeaponRaise" .. self:SteamID64())
self:SetNetVar("canShoot", false)
end
else
timer.Remove("ixWeaponRaise" .. self:SteamID64())
self:SetNetVar("raised", false)
self:SetNetVar("canShoot", false)
end
end
--- Inverts this player's weapon raised state. You should use `SetWepRaised` instead of this if you already have a reference
-- to this player's current weapon.
-- @realm server
function meta:ToggleWepRaised()
local weapon = self:GetActiveWeapon()
if (!IsValid(weapon) or
weapon.IsAlwaysRaised or ALWAYS_RAISED[weapon:GetClass()] or
weapon.IsAlwaysLowered or weapon.NeverRaised) then
return
end
self:SetWepRaised(!self:IsWepRaised(), weapon)
if (self:IsWepRaised() and weapon.OnRaised) then
weapon:OnRaised()
elseif (!self:IsWepRaised() and weapon.OnLowered) then
weapon:OnLowered()
end
end
--- Performs a delayed action that requires this player to hold use on an entity. This is displayed to this player as a
-- closing ring over their crosshair.
-- @realm server
-- @number time How much time in seconds this player has to hold use for
-- @entity entity Entity that this player must be looking at
-- @func callback Function to run when the timer completes. It will be ran right away if `time` is `0`. Returning `false` in
-- the callback will not mark this interaction as dirty if you're managing the interaction state manually.
function meta:PerformInteraction(time, entity, callback)
if (!IsValid(entity) or entity.ixInteractionDirty) then
return
end
if (time > 0) then
self.ixInteractionTarget = entity
self.ixInteractionCharacter = self:GetCharacter():GetID()
timer.Create("ixCharacterInteraction" .. self:SteamID(), time, 1, function()
if (IsValid(self) and IsValid(entity) and IsValid(self.ixInteractionTarget) and
self.ixInteractionCharacter == self:GetCharacter():GetID()) then
local data = {}
data.start = self:GetShootPos()
data.endpos = data.start + self:GetAimVector() * 96
data.filter = self
local traceEntity = util.TraceLine(data).Entity
if (IsValid(traceEntity) and traceEntity == self.ixInteractionTarget and !traceEntity.ixInteractionDirty) then
if (callback(self) != false) then
traceEntity.ixInteractionDirty = true
end
end
end
end)
else
if (callback(self) != false) then
entity.ixInteractionDirty = true
end
end
end
--- Displays a progress bar for this player that takes the given amount of time to complete.
-- @realm server
-- @string text Text to display above the progress bar
-- @number[opt=5] time How much time in seconds to wait before the timer completes
-- @func callback Function to run once the timer completes
-- @number[opt=CurTime()] startTime Game time in seconds that the timer started. If you are using `time`, then you shouldn't
-- use this argument
-- @number[opt=startTime + time] finishTime Game time in seconds that the timer should complete at. If you are using `time`,
-- then you shouldn't use this argument
function meta:SetAction(text, time, callback, startTime, finishTime)
if (time and time <= 0) then
if (callback) then
callback(self)
end
return
end
-- Default the time to five seconds.
time = time or 5
startTime = startTime or CurTime()
finishTime = finishTime or (startTime + time)
if (text == false) then
timer.Remove("ixAct"..self:SteamID64())
net.Start("ixActionBarReset")
net.Send(self)
return
end
if (!text) then
net.Start("ixActionBarReset")
net.Send(self)
else
net.Start("ixActionBar")
net.WriteFloat(startTime)
net.WriteFloat(finishTime)
net.WriteString(text)
net.Send(self)
end
-- If we have provided a callback, run it delayed.
if (callback) then
-- Create a timer that runs once with a delay.
timer.Create("ixAct"..self:SteamID64(), time, 1, function()
-- Call the callback if the player is still valid.
if (IsValid(self)) then
callback(self)
end
end)
end
end
--- Opens up a text box on this player's screen for input and returns the result. Remember to sanitize the user's input if
-- it's needed!
-- @realm server
-- @string title Title to display on the panel
-- @string subTitle Subtitle to display on the panel
-- @func callback Function to run when this player enters their input. Callback is ran with the user's input string.
-- @string[opt=nil] default Default value to put in the text box.
-- @usage client:RequestString("Hello", "Please enter your name", function(text)
-- client:ChatPrint("Hello, " .. text)
-- end)
-- -- prints "Hello, <text>" in the player's chat
function meta:RequestString(title, subTitle, callback, default)
local time = math.floor(os.time())
self.ixStrReqs = self.ixStrReqs or {}
self.ixStrReqs[time] = callback
net.Start("ixStringRequest")
net.WriteUInt(time, 32)
net.WriteString(title)
net.WriteString(subTitle)
net.WriteString(default)
net.Send(self)
end
--- Opens up a confirmation box on this player's screen for input and returns the result.
-- @realm server
-- @string title Title to display on the panel
-- @string subTitle Subtitle to display on the panel
-- @func callback Function to run when this player enters their input.
-- @usage client:RequestString("Confirm", "Are you sure you want to do this?", function(confirmation)
-- client:ChatPrint(confirmation and "Confirmed." or "Cancelled.")
-- end)
-- -- prints whether the player confirmed or cancelled.
function meta:RequestConfirmation(title, subTitle, callback)
local time = math.floor(os.time())
self.ixConfReqs = self.ixConfReqs or {}
self.ixConfReqs[time] = callback
net.Start("ixConfirmationRequest")
net.WriteUInt(time, 32)
net.WriteString(title)
net.WriteString(subTitle)
net.Send(self)
end
--- Sets this player's restricted status.
-- @realm server
-- @bool bState Whether or not to restrict this player
-- @bool bNoMessage Whether or not to suppress the restriction notification
function meta:SetRestricted(bState, bNoMessage)
if (bState) then
self:SetNetVar("restricted", true)
if (bNoMessage) then
self:SetLocalVar("restrictNoMsg", true)
end
self.ixRestrictWeps = self.ixRestrictWeps or {}
for _, v in ipairs(self:GetWeapons()) do
self.ixRestrictWeps[#self.ixRestrictWeps + 1] = v:GetClass()
v:Remove()
end
hook.Run("OnPlayerRestricted", self)
else
self:SetNetVar("restricted")
if (self:GetLocalVar("restrictNoMsg")) then
self:SetLocalVar("restrictNoMsg")
end
if (self.ixRestrictWeps) then
for _, v in ipairs(self.ixRestrictWeps) do
self:Give(v)
end
self.ixRestrictWeps = nil
end
hook.Run("OnPlayerUnRestricted", self)
end
end
--- Creates a ragdoll entity of this player that will be synced with clients. This does **not** affect the player like
-- `SetRagdolled` does.
-- @realm server
-- @bool[opt=false] bDontSetPlayer Whether or not to avoid setting the ragdoll's owning player
-- @treturn entity Created ragdoll entity
function meta:CreateServerRagdoll(bDontSetPlayer)
local entity = ents.Create("prop_ragdoll")
entity:SetPos(self:GetPos())
entity:SetAngles(self:EyeAngles())
entity:SetModel(self:GetModel())
entity:SetSkin(self:GetSkin())
for i = 0, (self:GetNumBodyGroups() - 1) do
entity:SetBodygroup(i, self:GetBodygroup(i))
end
entity:Spawn()
if (!bDontSetPlayer) then
entity:SetNetVar("player", self)
end
entity:SetCollisionGroup(COLLISION_GROUP_WEAPON)
entity:Activate()
local velocity = self:GetVelocity()
for i = 0, entity:GetPhysicsObjectCount() - 1 do
local physObj = entity:GetPhysicsObjectNum(i)
if (IsValid(physObj)) then
physObj:SetVelocity(velocity)
local index = entity:TranslatePhysBoneToBone(i)
if (index) then
local position, angles = self:GetBonePosition(index)
physObj:SetPos(position)
physObj:SetAngles(angles)
end
end
end
return entity
end
--- Sets this player's ragdoll status.
-- @realm server
-- @bool bState Whether or not to ragdoll this player
-- @number[opt=0] time How long this player should stay ragdolled for. Set to `0` if they should stay ragdolled until they
-- get back up manually
-- @number[opt=5] getUpGrace How much time in seconds to wait before the player is able to get back up manually. Set to
-- the same number as `time` to disable getting up manually entirely
function meta:SetRagdolled(bState, time, getUpGrace)
if (!self:Alive()) then
return
end
getUpGrace = getUpGrace or time or 5
if (bState) then
if (IsValid(self.ixRagdoll)) then
self.ixRagdoll:Remove()
end
local entity = self:CreateServerRagdoll()
entity:CallOnRemove("fixer", function()
if (IsValid(self)) then
self:SetLocalVar("blur", nil)
self:SetLocalVar("ragdoll", nil)
if (!entity.ixNoReset) then
self:SetPos(entity:GetPos())
end
self:SetNoDraw(false)
self:SetNotSolid(false)
self:SetMoveType(MOVETYPE_WALK)
self:SetLocalVelocity(IsValid(entity) and entity.ixLastVelocity or vector_origin)
end
if (IsValid(self) and !entity.ixIgnoreDelete) then
if (entity.ixWeapons) then
local active
for _, v in ipairs(entity.ixWeapons) do
if (v.active) then
active = v
elseif (v.class) then
local weapon = self:Give(v.class, true)
if (IsValid(weapon)) then
if (v.item) then
weapon.ixItem = v.item
end
self:SetAmmo(v.ammo, weapon:GetPrimaryAmmoType())
weapon:SetClip1(v.clip)
end
elseif (v.item and v.invID == v.item.invID) then
v.item:Equip(self, true, true)
self:SetAmmo(v.ammo, self.carryWeapons[v.item.weaponCategory]:GetPrimaryAmmoType())
end
end
if (active) then
if (active.class) then
local weapon = self:Give(active.class, true)
if (IsValid(weapon)) then
if (active.item) then
weapon.ixItem = active.item
end
self:SetAmmo(active.ammo, weapon:GetPrimaryAmmoType())
weapon:SetClip1(active.clip)
end
elseif (active.item and active.invID == active.item.invID) then
active.item:Equip(self, true, true)
self:SetAmmo(active.ammo, self.carryWeapons[active.item.weaponCategory]:GetPrimaryAmmoType())
end
end
end
--[[
if (entity.ixActiveWeapon) then
local weapon = entity.ixActiveWeapon
timer.Simple(1, function()
if (self:HasWeapon(weapon)) then
self:SetActiveWeapon(self:GetWeapon(weapon))
else
local weapons = self:GetWeapons()
if (#weapons > 0) then
self:SetActiveWeapon(weapons[1])
end
end
end)
timer.Simple(0.5, function()
if (self:HasWeapon("ix_hands")) then
self:SetActiveWeapon(self:GetWeapon("ix_hands"))
end
end)
end
--]]
if (self:IsStuck()) then
entity:DropToFloor()
self:SetPos(entity:GetPos() + Vector(0, 0, 16))
local positions = ix.util.FindEmptySpace(self, {entity, self})
for _, v in ipairs(positions) do
self:SetPos(v)
if (!self:IsStuck()) then
return
end
end
end
end
end)
self:SetLocalVar("blur", 25)
self.ixRagdoll = entity
entity.ixWeapons = {}
entity.ixPlayer = self
if (getUpGrace) then
entity.ixGrace = CurTime() + getUpGrace
end
if (time and time > 0) then
entity.ixStart = CurTime()
entity.ixFinish = entity.ixStart + time
self:SetAction("@wakingUp", nil, nil, entity.ixStart, entity.ixFinish)
end
if (IsValid(self:GetActiveWeapon())) then
entity.ixActiveWeapon = self:GetActiveWeapon():GetClass()
end
for _, v in ipairs(self:GetWeapons()) do
if (v.ixItem and v.ixItem.Equip and v.ixItem.Unequip) then
entity.ixWeapons[#entity.ixWeapons + 1] = {
item = v.ixItem,
invID = v.ixItem.invID,
ammo = self:GetAmmoCount(v:GetPrimaryAmmoType()),
active = v:GetClass() == entity.ixActiveWeapon
}
v.ixItem:Unequip(self, false)
else
local clip = v:Clip1()
local reserve = self:GetAmmoCount(v:GetPrimaryAmmoType())
entity.ixWeapons[#entity.ixWeapons + 1] = {
class = v:GetClass(),
item = v.ixItem,
clip = clip,
ammo = reserve,
active = v:GetClass() == entity.ixActiveWeapon
}
end
end
self:GodDisable()
self:StripWeapons()
self:SetMoveType(MOVETYPE_OBSERVER)
self:SetNoDraw(true)
self:SetNotSolid(true)
local uniqueID = "ixUnRagdoll" .. self:SteamID()
if (time) then
timer.Create(uniqueID, 0.33, 0, function()
if (IsValid(entity) and IsValid(self) and self.ixRagdoll == entity) then
local velocity = entity:GetVelocity()
entity.ixLastVelocity = velocity
self:SetPos(entity:GetPos())
if (velocity:Length2D() >= 8) then
if (!entity.ixPausing) then
self:SetAction()
entity.ixPausing = true
end
return
elseif (entity.ixPausing) then
self:SetAction("@wakingUp", time)
entity.ixPausing = false
end
time = time - 0.33
if (time <= 0) then
entity:Remove()
end
else
timer.Remove(uniqueID)
end
end)
else
timer.Create(uniqueID, 0.33, 0, function()
if (IsValid(entity) and IsValid(self) and self.ixRagdoll == entity) then
self:SetPos(entity:GetPos())
else
timer.Remove(uniqueID)
end
end)
end
self:SetLocalVar("ragdoll", entity:EntIndex())
hook.Run("OnCharacterFallover", self, entity, true)
elseif (IsValid(self.ixRagdoll)) then
-- Anti-Exploit measure. Just silently fail if near a door.
for _, entity in ipairs(ents.FindInSphere(self:GetPos(), 50)) do
if (entity:GetClass() == "func_door" or entity:GetClass() == "func_door_rotating" or entity:GetClass() == "prop_door_rotating") then return end
end
self.ixRagdoll:Remove()
hook.Run("OnCharacterFallover", self, nil, false)
end
end
end

View File

@@ -0,0 +1,140 @@
--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
local TOOL = ix.meta.tool or {}
-- code replicated from gamemodes/sandbox/entities/weapons/gmod_tool/stool.lua
function TOOL:Create()
local object = {}
setmetatable(object, self)
self.__index = self
object.Mode = nil
object.SWEP = nil
object.Owner = nil
object.ClientConVar = {}
object.ServerConVar = {}
object.Objects = {}
object.Stage = 0
object.Message = "start"
object.LastMessage = 0
object.AllowedCVar = 0
return object
end
function TOOL:CreateConVars()
local mode = self:GetMode()
if (CLIENT) then
for cvar, default in pairs(self.ClientConVar) do
CreateClientConVar(mode .. "_" .. cvar, default, true, true)
end
return
end
-- Note: I changed this from replicated because replicated convars don't work when they're created via Lua.
if (SERVER) then
self.AllowedCVar = CreateConVar("toolmode_allow_" .. mode, 1, FCVAR_NOTIFY)
end
end
function TOOL:GetServerInfo(property)
local mode = self:GetMode()
return GetConVarString(mode .. "_" .. property)
end
function TOOL:BuildConVarList()
local mode = self:GetMode()
local convars = {}
for k, v in pairs(self.ClientConVar) do
convars[mode .. "_" .. k] = v
end
return convars
end
function TOOL:GetClientInfo(property)
return self:GetOwner():GetInfo(self:GetMode() .. "_" .. property)
end
function TOOL:GetClientNumber(property, default)
return self:GetOwner():GetInfoNum(self:GetMode() .. "_" .. property, tonumber(default) or 0)
end
function TOOL:Allowed()
if (CLIENT) then
return true
end
return self.AllowedCVar:GetBool()
end
-- Now for all the TOOL redirects
function TOOL:Init()
end
function TOOL:GetMode()
return self.Mode
end
function TOOL:GetSWEP()
return self.SWEP
end
function TOOL:GetOwner()
return self:GetSWEP().Owner or self.Owner
end
function TOOL:GetWeapon()
return self:GetSWEP().Weapon or self.Weapon
end
function TOOL:LeftClick()
return false
end
function TOOL:RightClick()
return false
end
function TOOL:Reload()
self:ClearObjects()
end
function TOOL:Deploy()
self:ReleaseGhostEntity()
return
end
function TOOL:Holster()
self:ReleaseGhostEntity()
return
end
function TOOL:Think()
self:ReleaseGhostEntity()
end
-- Checks the objects before any action is taken
-- This is to make sure that the entities haven't been removed
function TOOL:CheckObjects()
for _, v in pairs(self.Objects) do
if (!v.Ent:IsWorld() and !v.Ent:IsValid()) then
self:ClearObjects()
end
end
end
ix.meta.tool = TOOL