This commit is contained in:
lifestorm
2024-08-04 23:54:45 +03:00
parent 8064ba84d8
commit 6a58f406b1
7522 changed files with 4011896 additions and 15 deletions

View File

@@ -0,0 +1,131 @@
--[[
| 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/
--]]
ix.bar = ix.bar or {}
ix.bar.list = {}
ix.bar.delta = ix.bar.delta or {}
ix.bar.actionText = ""
ix.bar.actionStart = 0
ix.bar.actionEnd = 0
ix.bar.totalHeight = 0
-- luacheck: globals BAR_HEIGHT
BAR_HEIGHT = 10
function ix.bar.Get(identifier)
for _, v in ipairs(ix.bar.list) do
if (v.identifier == identifier) then
return v
end
end
end
function ix.bar.Remove(identifier)
local bar = ix.bar.Get(identifier)
if (bar) then
table.remove(ix.bar.list, bar.index)
if (IsValid(ix.gui.bars)) then
ix.gui.bars:RemoveBar(bar.panel)
end
end
end
function ix.bar.Add(getValue, color, priority, identifier)
if (identifier) then
ix.bar.Remove(identifier)
end
local index = #ix.bar.list + 1
color = color or Color(math.random(150, 255), math.random(150, 255), math.random(150, 255))
priority = priority or index
ix.bar.list[index] = {
index = index,
color = color,
priority = priority,
GetValue = getValue,
identifier = identifier,
panel = IsValid(ix.gui.bars) and ix.gui.bars:AddBar(index, color, priority)
}
return priority
end
local gradientD = ix.util.GetMaterial("vgui/gradient-d")
local TEXT_COLOR = Color(240, 240, 240)
local SHADOW_COLOR = Color(20, 20, 20)
function ix.bar.DrawAction()
local start, finish = ix.bar.actionStart, ix.bar.actionEnd
local curTime = CurTime()
local scrW, scrH = ScrW(), ScrH()
if (finish > curTime) then
local fraction = 1 - math.TimeFraction(start, finish, curTime)
local alpha = fraction * 255
if (alpha > 0) then
local w, h = scrW * 0.35, 28
local x, y = (scrW * 0.5) - (w * 0.5), (scrH * 0.725) - (h * 0.5)
ix.util.DrawBlurAt(x, y, w, h)
surface.SetDrawColor(35, 35, 35, 100)
surface.DrawRect(x, y, w, h)
surface.SetDrawColor(0, 0, 0, 120)
surface.DrawOutlinedRect(x, y, w, h)
surface.SetDrawColor(ix.config.Get("color"))
surface.DrawRect(x + 4, y + 4, math.max(w * fraction, 8) - 8, h - 8)
surface.SetDrawColor(200, 200, 200, 20)
surface.SetMaterial(gradientD)
surface.DrawTexturedRect(x + 4, y + 4, math.max(w * fraction, 8) - 8, h - 8)
draw.SimpleText(ix.bar.actionText, "ixMediumFont", x + 2, y - 22, SHADOW_COLOR)
draw.SimpleText(ix.bar.actionText, "ixMediumFont", x, y - 24, TEXT_COLOR)
end
end
end
do
ix.bar.Add(function()
return math.max(LocalPlayer():Health() / LocalPlayer():GetMaxHealth(), 0)
end, Color(200, 50, 40), nil, "health")
ix.bar.Add(function()
return math.min(LocalPlayer():Armor() / 100, 1)
end, Color(30, 70, 180), nil, "armor")
end
net.Receive("ixActionBar", function()
local start, finish = net.ReadFloat(), net.ReadFloat()
local text = net.ReadString()
if (text:sub(1, 1) == "@") then
text = L2(text:sub(2)) or text
end
ix.bar.actionStart = start
ix.bar.actionEnd = finish
ix.bar.actionText = text:utf8upper()
end)
net.Receive("ixActionBarReset", function()
ix.bar.actionStart = 0
ix.bar.actionEnd = 0
ix.bar.actionText = ""
end)

View File

@@ -0,0 +1,146 @@
--[[
| 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/
--]]
ix.hud = {}
local owner, w, h, ceil, ft, clmp
ceil = math.ceil
clmp = math.Clamp
local aprg, aprg2 = 0, 0
function ix.hud.DrawDeath()
owner = LocalPlayer()
ft = FrameTime()
w, h = ScrW(), ScrH()
if (owner:GetCharacter()) then
if (owner:Alive()) then
if (aprg != 0) then
aprg2 = clmp(aprg2 - ft*1.3, 0, 1)
if (aprg2 == 0) then
aprg = clmp(aprg - ft*.7, 0, 1)
end
end
else
if (aprg2 != 1) then
aprg = clmp(aprg + ft*.5, 0, 1)
if (aprg == 1) then
aprg2 = clmp(aprg2 + ft*.4, 0, 1)
end
end
end
end
if (IsValid(ix.gui.characterMenu) and ix.gui.characterMenu:IsVisible() or !owner:GetCharacter()) then
return
end
surface.SetDrawColor(0, 0, 0, ceil((aprg^.5) * 255))
surface.DrawRect(-1, -1, w+2, h+2)
ix.util.DrawText(
string.utf8upper(L"youreDead"), w/2, h/2, ColorAlpha(color_white, aprg2 * 255), 1, 1, "ixMenuButtonHugeFont", aprg2 * 255
)
end
function ix.hud.DrawItemPickup()
local pickupTime = ix.config.Get("itemPickupTime", 0.5)
if (pickupTime == 0) then
return
end
local client = LocalPlayer()
local entity = client.ixInteractionTarget
local startTime = client.ixInteractionStartTime
if (IsValid(entity) and startTime) then
local sysTime = SysTime()
local endTime = startTime + pickupTime
if (sysTime >= endTime or client:GetEyeTrace().Entity != entity) then
client.ixInteractionTarget = nil
client.ixInteractionStartTime = nil
return
end
local fraction = math.min((endTime - sysTime) / pickupTime, 1)
local x, y = ScrW() / 2, ScrH() / 2
local radius, thickness = 32, 6
local startAngle = 90
local endAngle = startAngle + (1 - fraction) * 360
local color = ColorAlpha(color_white, fraction * 255)
ix.util.DrawArc(x, y, radius, thickness, startAngle, endAngle, 2, color)
end
end
function ix.hud.PopulateItemTooltip(tooltip, item)
local name = tooltip:AddRow("name")
name:SetImportant()
name:SetText(item.GetName and item:GetName() or L(item.name))
name:SetMaxWidth(math.max(name:GetMaxWidth(), ScrW() * 0.5))
name:SizeToContents()
local description = tooltip:AddRow("description")
description:SetText(item:GetDescription() or "")
description:SizeToContents()
if (item.PopulateTooltip) then
item:PopulateTooltip(tooltip)
end
hook.Run("PopulateItemTooltip", tooltip, item)
end
function ix.hud.PopulatePlayerTooltip(tooltip, client)
local name = tooltip:AddRow("name")
name:SetImportant()
name:SetText(client:SteamName())
name:SetBackgroundColor(team.GetColor(client:Team()))
name:SizeToContents()
local nameHeight = name:GetTall()
name:SetTextInset(nameHeight + 4, 0)
name:SetWide(name:GetWide() + nameHeight + 4)
local avatar = name:Add("AvatarImage")
avatar:Dock(LEFT)
avatar:SetPlayer(client, nameHeight)
avatar:SetSize(name:GetTall(), name:GetTall())
local currentPing = client:Ping()
local ping = tooltip:AddRow("ping")
ping:SetText(L("ping", currentPing))
ping.Paint = function(_, width, height)
surface.SetDrawColor(ColorAlpha(derma.GetColor(
currentPing < 110 and "Success" or (currentPing < 165 and "Warning" or "Error")
, tooltip), 22))
surface.DrawRect(0, 0, width, height)
end
ping:SizeToContents()
hook.Run("PopulatePlayerTooltip", client, tooltip)
end
hook.Add("GetCrosshairAlpha", "ixCrosshair", function(alpha)
return alpha * (1 - aprg)
end)
function ix.hud.DrawAll(postHook)
if (postHook) then
ix.hud.DrawDeath()
end
ix.hud.DrawItemPickup()
end

View File

@@ -0,0 +1,506 @@
--[[
| 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/
--]]
-- luacheck: ignore
ix.markup = ix.markup or {}
-- Temporary information used when building text frames.
local colour_stack = { {r=255,g=255,b=255,a=255} }
local font_stack = { "DermaDefault" }
local curtag = nil
local blocks = {}
local colourmap = {
-- it's all black and white
["black"] = { r=0, g=0, b=0, a=255 },
["white"] = { r=255, g=255, b=255, a=255 },
-- it's greys
["dkgrey"] = { r=64, g=64, b=64, a=255 },
["grey"] = { r=128, g=128, b=128, a=255 },
["ltgrey"] = { r=192, g=192, b=192, a=255 },
-- account for speeling mistakes
["dkgray"] = { r=64, g=64, b=64, a=255 },
["gray"] = { r=128, g=128, b=128, a=255 },
["ltgray"] = { r=192, g=192, b=192, a=255 },
-- normal colours
["red"] = { r=255, g=0, b=0, a=255 },
["green"] = { r=0, g=255, b=0, a=255 },
["blue"] = { r=0, g=0, b=255, a=255 },
["yellow"] = { r=255, g=255, b=0, a=255 },
["purple"] = { r=255, g=0, b=255, a=255 },
["cyan"] = { r=0, g=255, b=255, a=255 },
["turq"] = { r=0, g=255, b=255, a=255 },
-- dark variations
["dkred"] = { r=128, g=0, b=0, a=255 },
["dkgreen"] = { r=0, g=128, b=0, a=255 },
["dkblue"] = { r=0, g=0, b=128, a=255 },
["dkyellow"] = { r=128, g=128, b=0, a=255 },
["dkpurple"] = { r=128, g=0, b=128, a=255 },
["dkcyan"] = { r=0, g=128, b=128, a=255 },
["dkturq"] = { r=0, g=128, b=128, a=255 },
-- light variations
["ltred"] = { r=255, g=128, b=128, a=255 },
["ltgreen"] = { r=128, g=255, b=128, a=255 },
["ltblue"] = { r=128, g=128, b=255, a=255 },
["ltyellow"] = { r=255, g=255, b=128, a=255 },
["ltpurple"] = { r=255, g=128, b=255, a=255 },
["ltcyan"] = { r=128, g=255, b=255, a=255 },
["ltturq"] = { r=128, g=255, b=255, a=255 },
}
--[[
Name: colourMatch(c)
Desc: Match a colour name to an rgb value.
Usage: ** INTERNAL ** Do not use!
]]
local function colourMatch(c)
c = string.lower(c)
return colourmap[c]
end
--[[
Name: ExtractParams(p1,p2,p3)
Desc: This function is used to extract the tag information.
Usage: ** INTERNAL ** Do not use!
]]
local function ExtractParams(p1,p2,p3)
if (string.utf8sub(p1, 1, 1) == "/") then
local tag = string.utf8sub(p1, 2)
if (tag == "color" or tag == "colour") then
table.remove(colour_stack)
elseif (tag == "font" or tag == "face") then
table.remove(font_stack)
end
else
if (p1 == "color" or p1 == "colour") then
local rgba = colourMatch(p2)
if (rgba == nil) then
rgba = {}
local x = { "r", "g", "b", "a" }
n = 1
for k, v in string.gmatch(p2, "(%d+),?") do
rgba[ x[n] ] = k
n = n + 1
end
end
table.insert(colour_stack, rgba)
elseif (p1 == "font" or p1 == "face") then
table.insert(font_stack, tostring(p2))
elseif (p1 == "img" and p2) then
local exploded = string.Explode(",", p2)
local material = exploded[1] or p2
local p3 = exploded[2]
local found = file.Find("materials/"..material..".*", "GAME")
if (found[1] and found[1]:find("%.png")) then
material = material..".png"
end
local texture = Material(material)
local sizeData = string.Explode("x", p3 or "16x16")
w = tonumber(sizeData[1]) or 16
h = tonumber(sizeData[2]) or 16
if (texture) then
table.insert(blocks, {
texture = texture,
w = w,
h = h
})
end
end
end
end
--[[
Name: CheckTextOrTag(p)
Desc: This function places data in the "blocks" table
depending of if p is a tag, or some text
Usage: ** INTERNAL ** Do not use!
]]
local function CheckTextOrTag(p)
if (p == "") then return end
if (p == nil) then return end
if (string.utf8sub(p, 1, 1) == "<") then
string.gsub(p, "<([/%a]*)=?([^>]*)", ExtractParams)
else
local text_block = {}
text_block.text = p
text_block.colour = colour_stack[#colour_stack]
text_block.font = font_stack[#font_stack]
table.insert(blocks, text_block)
end
end
--[[
Name: ProcessMatches(p1,p2,p3)
Desc: CheckTextOrTag for 3 parameters. Called by string.gsub
Usage: ** INTERNAL ** Do not use!
]]
local function ProcessMatches(p1,p2,p3)
if (p1) then CheckTextOrTag(p1) end
if (p2) then CheckTextOrTag(p2) end
if (p3) then CheckTextOrTag(p3) end
end
local MarkupObject = {}
--[[
Name: MarkupObject:Create()
Desc: Called by Parse. Creates a new table, and setups the
metatable.
Usage: ** INTERNAL ** Do not use!
]]
function MarkupObject:create()
local o = {}
setmetatable(o, self)
self.__index = self
return o
end
--[[
Name: MarkupObject:GetWidth()
Desc: Returns the width of a markup block
Usage: ml:GetWidth()
]]
function MarkupObject:GetWidth()
return self.totalWidth
end
--[[
Name: MarkupObject:GetHeight()
Desc: Returns the height of a markup block
Usage: ml:GetHeight()
]]
function MarkupObject:GetHeight()
return self.totalHeight
end
function MarkupObject:size()
return self.totalWidth, self.totalHeight
end
--[[
Name: MarkupObject:Draw(xOffset, yOffset, halign, valign, alphaoverride)
Desc: Draw the markup text to the screen as position
xOffset, yOffset. Halign and Valign can be used
to align the text. Alphaoverride can be used to override
the alpha value of the text-colour.
Usage: MarkupObject:Draw(100, 100)
]]
function MarkupObject:draw(xOffset, yOffset, halign, valign, alphaoverride)
for i = 1, #self.blocks do
local blk = self.blocks[i]
if (blk.texture) then
local y = yOffset + blk.offset.y
local x = xOffset + blk.offset.x
if (halign == TEXT_ALIGN_CENTER) then
x = x - (self.totalWidth * 0.5)
elseif (halign == TEXT_ALIGN_RIGHT) then
x = x - (self.totalWidth)
end
surface.SetDrawColor(blk.colour.r, blk.colour.g, blk.colour.b, alphaoverride or blk.colour.a or 255)
surface.SetMaterial(blk.texture)
surface.DrawTexturedRect(x, y, blk.w, blk.h)
else
local y = yOffset + (blk.height - blk.thisY) + blk.offset.y
local x = xOffset
if (halign == TEXT_ALIGN_CENTER) then x = x - (self.totalWidth / 2)
elseif (halign == TEXT_ALIGN_RIGHT) then x = x - (self.totalWidth)
end
x = x + blk.offset.x
if (self.onDrawText) then
self.onDrawText(blk.text, blk.font, x, y, blk.colour, halign, valign, alphaoverride, blk)
else
if (valign == TEXT_ALIGN_CENTER) then y = y - (self.totalHeight / 2)
elseif (valign == TEXT_ALIGN_BOTTOM) then y = y - (self.totalHeight)
end
local alpha = blk.colour.a
if (alphaoverride) then alpha = alphaoverride end
surface.SetFont( blk.font )
surface.SetTextColor( blk.colour.r, blk.colour.g, blk.colour.b, alpha )
surface.SetTextPos( x, y )
surface.DrawText( blk.text )
end
end
end
end
--[[
Name: Parse(ml, maxwidth)
Desc: Parses the pseudo-html markup language, and creates a
MarkupObject, which can be used to the draw the
text to the screen. Valid tags are: font and colour.
\n and \t are also available to move to the next line,
or insert a tab character.
Maxwidth can be used to make the text wrap to a specific
width.
Usage: markup.Parse("<font=Default>changed font</font>\n<colour=255,0,255,255>changed colour</colour>")
]]
function ix.markup.Parse(ml, maxwidth)
ml = utf8.force(ml)
colour_stack = { {r=255,g=255,b=255,a=255} }
font_stack = { "DermaDefault" }
blocks = {}
if (not string.find(ml, "<")) then
ml = ml .. "<nop>"
end
string.gsub(ml, "([^<>]*)(<[^>]+.)([^<>]*)", ProcessMatches)
local xOffset = 0
local yOffset = 0
local xSize = 0
local xMax = 0
local thisMaxY = 0
local new_block_list = {}
local ymaxes = {}
local texOffset = 0
local lineHeight = 0
for i = 1, #blocks do
local block = blocks[i]
if (block.text) then
surface.SetFont(block.font)
local thisY = 0
local curString = ""
block.text = string.gsub(block.text, "&gt;", ">")
block.text = string.gsub(block.text, "&lt;", "<")
block.text = string.gsub(block.text, "&amp;", "&")
for j=1,string.utf8len(block.text) do
local ch = string.utf8sub(block.text,j,j)
if (ch == "\n") then
if (thisY == 0) then
thisY = lineHeight + texOffset;
thisMaxY = lineHeight + texOffset;
else
lineHeight = thisY + texOffset
end
if (string.utf8len(curString) > 0) then
local x1,y1 = surface.GetTextSize(curString)
local new_block = {}
new_block.text = curString
new_block.font = block.font
new_block.colour = block.colour
new_block.thisY = thisY
new_block.thisX = x1
new_block.offset = {}
new_block.offset.x = xOffset
new_block.offset.y = yOffset
table.insert(new_block_list, new_block)
if (xOffset + x1 > xMax) then
xMax = xOffset + x1
end
end
xOffset = 0
xSize = 0
yOffset = yOffset + thisMaxY;
thisY = 0
curString = ""
thisMaxY = 0
elseif (ch == "\t") then
if (string.utf8len(curString) > 0) then
local x1,y1 = surface.GetTextSize(curString)
local new_block = {}
new_block.text = curString
new_block.font = block.font
new_block.colour = block.colour
new_block.thisY = thisY
new_block.thisX = x1
new_block.offset = {}
new_block.offset.x = xOffset
new_block.offset.y = yOffset
table.insert(new_block_list, new_block)
if (xOffset + x1 > xMax) then
xMax = xOffset + x1
end
end
local xOldSize = xSize
xSize = 0
curString = ""
local xOldOffset = xOffset
xOffset = math.ceil( (xOffset + xOldSize) / 50 ) * 50
if (xOffset == xOldOffset) then
xOffset = xOffset + 50
end
else
local x,y = surface.GetTextSize(ch)
if (x == nil) then return end
if (maxwidth and maxwidth > x) then
if (xOffset + xSize + x >= maxwidth) then
-- need to: find the previous space in the curString
-- if we can't find one, take off the last character
-- and add a -. add the character to ch
-- and insert as a new block, incrementing the y etc
local lastSpacePos = string.utf8len(curString)
for k=1,string.utf8len(curString) do
local chspace = string.utf8sub(curString,k,k)
if (chspace == " ") then
lastSpacePos = k
end
end
if (lastSpacePos == string.utf8len(curString)) then
ch = string.utf8sub(curString,lastSpacePos,lastSpacePos) .. ch
j = lastSpacePos
curString = string.utf8sub(curString, 1, lastSpacePos-1)
else
ch = string.utf8sub(curString,lastSpacePos+1) .. ch
j = lastSpacePos+1
curString = string.utf8sub(curString, 1, lastSpacePos)
end
local m = 1
while string.utf8sub(ch, m, m) == " " and m <= string.utf8len(ch) do
m = m + 1
end
ch = string.utf8sub(ch, m)
local x1,y1 = surface.GetTextSize(curString)
if (y1 > thisMaxY) then thisMaxY = y1; ymaxes[yOffset] = thisMaxY; lineHeight = y1; end
local new_block = {}
new_block.text = curString
new_block.font = block.font
new_block.colour = block.colour
new_block.thisY = thisY
new_block.thisX = x1
new_block.offset = {}
new_block.offset.x = xOffset
new_block.offset.y = yOffset
table.insert(new_block_list, new_block)
if (xOffset + x1 > xMax) then
xMax = xOffset + x1
end
xOffset = 0
xSize = 0
x,y = surface.GetTextSize(ch)
yOffset = yOffset + thisMaxY;
thisY = 0
curString = ""
thisMaxY = 0
end
end
curString = curString .. ch
thisY = y
xSize = xSize + x
if (y > thisMaxY) then thisMaxY = y; ymaxes[yOffset] = thisMaxY; lineHeight = y; end
end
end
if (string.utf8len(curString) > 0) then
local x1,y1 = surface.GetTextSize(curString)
local new_block = {}
new_block.text = curString
new_block.font = block.font
new_block.colour = block.colour
new_block.thisY = thisY
new_block.thisX = x1
new_block.offset = {}
new_block.offset.x = xOffset
new_block.offset.y = yOffset
table.insert(new_block_list, new_block)
lineHeight = thisY
if (xOffset + x1 > xMax) then
xMax = xOffset + x1
end
xOffset = xOffset + x1
end
xSize = 0
elseif (block.texture) then
local newBlock = table.Copy(block)
newBlock.colour = block.colour or {r = 255, g = 255, b = 255, a = 255}
newBlock.thisX = block.w
newBlock.thisY = block.h
newBlock.offset = {
x = xOffset,
y = 0
}
table.insert(new_block_list, newBlock)
xOffset = xOffset + block.w + 1
texOffset = block.h / 2
end
end
local totalHeight = 0
for i = 1, #new_block_list do
local block = new_block_list[i]
block.height = ymaxes[block.offset.y]
if (block.texture) then
block.offset.y = ymaxes[0] * 0.5 - block.h * 0.5
end
if (block.height and block.offset.y + block.height > totalHeight) then
totalHeight = block.offset.y + block.height
end
end
local newObject = MarkupObject:create()
newObject.totalHeight = totalHeight
newObject.totalWidth = xMax
newObject.blocks = new_block_list
return newObject
end

View File

@@ -0,0 +1,68 @@
--[[
| 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 entityMeta = FindMetaTable("Entity")
local playerMeta = FindMetaTable("Player")
ix.net = ix.net or {}
ix.net.globals = ix.net.globals or {}
net.Receive("ixGlobalVarSet", function()
ix.net.globals[net.ReadString()] = net.ReadType()
end)
net.Receive("ixNetVarSet", function()
local index = net.ReadUInt(16)
ix.net[index] = ix.net[index] or {}
ix.net[index][net.ReadString()] = net.ReadType()
end)
net.Receive("ixNetStatics", function()
for _ = 1, net.ReadUInt(16) do
local id = net.ReadUInt(16)
if (id == 0) then continue end
ix.net[id] = ix.net[id] or {}
ix.net[id].Persistent = true
end
end)
net.Receive("ixNetVarDelete", function()
ix.net[net.ReadUInt(16)] = nil
end)
net.Receive("ixLocalVarSet", function()
local key = net.ReadString()
local var = net.ReadType()
ix.net[LocalPlayer():EntIndex()] = ix.net[LocalPlayer():EntIndex()] or {}
ix.net[LocalPlayer():EntIndex()][key] = var
hook.Run("OnLocalVarSet", key, var)
end)
function GetNetVar(key, default) -- luacheck: globals GetNetVar
local value = ix.net.globals[key]
return value != nil and value or default
end
function entityMeta:GetNetVar(key, default)
local index = self:EntIndex()
if (ix.net[index] and ix.net[index][key] != nil) then
return ix.net[index][key]
end
return default
end
playerMeta.GetLocalVar = entityMeta.GetNetVar

View File

@@ -0,0 +1,160 @@
--[[
| 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/
--]]
function ix.util.InstallAnimationMethods(meta)
local function TweenAnimationThink(object)
for k, v in pairs(object.tweenAnimations) do
if (!v.bShouldPlay) then
continue
end
local bComplete = v:update(FrameTime())
if (v.Think) then
v:Think(object)
end
if (bComplete) then
v.bShouldPlay = nil
v:ForceComplete()
if (v.OnComplete) then
v:OnComplete(object)
end
if (v.bRemoveOnComplete) then
object.tweenAnimations[k] = nil
end
end
end
end
function meta:GetTweenAnimation(index, bNoPlay)
-- if we don't need to check if the animation is playing we can just return the animation
if (bNoPlay) then
return self.tweenAnimations[index]
else
for k, v in pairs(self.tweenAnimations or {}) do
if (k == index and v.bShouldPlay) then
return v
end
end
end
end
function meta:IsPlayingTweenAnimation(index)
for k, v in pairs(self.tweenAnimations or {}) do
if (v.bShouldPlay and index == k) then
return true
end
end
return false
end
function meta:StopAnimations(bRemove)
for k, v in pairs(self.tweenAnimations or {}) do
if (v.bShouldPlay) then
v:ForceComplete()
if (bRemove) then
self.tweenAnimations[k] = nil
end
end
end
end
function meta:CreateAnimation(length, data)
local animations = self.tweenAnimations or {}
self.tweenAnimations = animations
if (self.SetAnimationEnabled) then
self:SetAnimationEnabled(true)
end
local index = data.index or 1
local bCancelPrevious = data.bCancelPrevious == nil and false or data.bCancelPrevious
local bIgnoreConfig = SERVER or (data.bIgnoreConfig == nil and false or data.bIgnoreConfig)
if (bCancelPrevious and self:IsPlayingTweenAnimation()) then
for _, v in pairs(animations) do
v:set(v.duration)
end
end
local animation = ix.tween.new(
((length == 0 and 1 or length) or 1) * (bIgnoreConfig and 1 or ix.option.Get("animationScale", 1)),
data.subject or self,
data.target or {},
data.easing or "linear"
)
animation.index = index
animation.bIgnoreConfig = bIgnoreConfig
animation.bAutoFire = (data.bAutoFire == nil and true or data.bAutoFire)
animation.bRemoveOnComplete = (data.bRemoveOnComplete == nil and true or data.bRemoveOnComplete)
animation.Think = data.Think
animation.OnComplete = data.OnComplete
animation.ForceComplete = function(anim)
anim:set(anim.duration)
end
-- @todo don't use ridiculous method chaining
animation.CreateAnimation = function(currentAnimation, newLength, newData)
newData.bAutoFire = false
newData.index = currentAnimation.index + 1
local oldOnComplete = currentAnimation.OnComplete
local newAnimation = currentAnimation.subject:CreateAnimation(newLength, newData)
currentAnimation.OnComplete = function(...)
if (oldOnComplete) then
oldOnComplete(...)
end
newAnimation:Fire()
end
return newAnimation
end
if (length == 0 or (!animation.bIgnoreConfig and ix.option.Get("disableAnimations", false))) then
animation.Fire = function(anim)
anim:set(anim.duration)
anim.bShouldPlay = true
end
else
animation.Fire = function(anim)
anim:set(0)
anim.bShouldPlay = true
end
end
-- we can assume if we're using this library, we're not going to use the built-in
-- AnimationTo functions, so override AnimationThink with our own
self.AnimationThink = TweenAnimationThink
-- fire right away if autofire is enabled
if (animation.bAutoFire) then
animation:Fire()
end
self.tweenAnimations[index] = animation
return animation
end
end
if (CLIENT) then
local panelMeta = FindMetaTable("Panel")
ix.util.InstallAnimationMethods(panelMeta)
end

View File

@@ -0,0 +1,571 @@
--[[
| 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/
--]]
--[[--
Player model animation.
Helix comes with support for using NPC animations/models as regular player models by manually translating animations. There are
a few standard animation sets that are built-in that should cover most non-player models:
citizen_male
citizen_female
metrocop
overwatch
vortigaunt
player
zombie
fastZombie
If you find that your models are T-posing when they work elsewhere, you'll probably need to set the model class for your
model with `ix.anim.SetModelClass` in order for the correct animations to be used. If you'd like to add your own animation
class, simply add to the `ix.anim` table with a model class name and the required animation translation table.
]]
-- @module ix.anim
ix.anim = ix.anim or {}
ix.anim.citizen_male = {
normal = {
[ACT_MP_STAND_IDLE] = {ACT_IDLE, ACT_IDLE_ANGRY_SMG1},
[ACT_MP_CROUCH_IDLE] = {ACT_COVER_LOW, ACT_COVER_LOW},
[ACT_MP_WALK] = {ACT_WALK, ACT_WALK_AIM_RIFLE_STIMULATED},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH, ACT_WALK_CROUCH_AIM_RIFLE},
[ACT_MP_RUN] = {ACT_RUN, ACT_RUN_AIM_RIFLE_STIMULATED},
[ACT_LAND] = {ACT_RESET, ACT_RESET}
},
pistol = {
[ACT_MP_STAND_IDLE] = {ACT_IDLE_PISTOL, ACT_IDLE_ANGRY_PISTOL},
[ACT_MP_CROUCH_IDLE] = {ACT_COVER_LOW, ACT_RANGE_ATTACK_PISTOL_LOW},
[ACT_MP_WALK] = {ACT_WALK, ACT_WALK_AIM_PISTOL},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH, ACT_WALK_CROUCH_AIM_RIFLE},
[ACT_MP_RUN] = {ACT_RUN, ACT_RUN_AIM_PISTOL},
[ACT_LAND] = {ACT_RESET, ACT_RESET},
attack = ACT_GESTURE_RANGE_ATTACK_PISTOL,
reload = ACT_RELOAD_PISTOL
},
smg = {
[ACT_MP_STAND_IDLE] = {ACT_IDLE_SMG1_RELAXED, ACT_IDLE_ANGRY_SMG1},
[ACT_MP_CROUCH_IDLE] = {ACT_COVER_LOW, ACT_RANGE_AIM_SMG1_LOW},
[ACT_MP_WALK] = {ACT_WALK_RIFLE_RELAXED, ACT_WALK_AIM_RIFLE_STIMULATED},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH_RIFLE, ACT_WALK_CROUCH_AIM_RIFLE},
[ACT_MP_RUN] = {ACT_RUN_RIFLE_RELAXED, ACT_RUN_AIM_RIFLE_STIMULATED},
[ACT_LAND] = {ACT_RESET, ACT_RESET},
attack = ACT_GESTURE_RANGE_ATTACK_SMG1,
reload = ACT_GESTURE_RELOAD_SMG1
},
shotgun = {
[ACT_MP_STAND_IDLE] = {ACT_IDLE_SHOTGUN_RELAXED, ACT_IDLE_ANGRY_SMG1},
[ACT_MP_CROUCH_IDLE] = {ACT_COVER_LOW, ACT_RANGE_AIM_SMG1_LOW},
[ACT_MP_WALK] = {ACT_WALK_RIFLE_RELAXED, ACT_WALK_AIM_RIFLE_STIMULATED},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH_RIFLE, ACT_WALK_CROUCH_RIFLE},
[ACT_MP_RUN] = {ACT_RUN_RIFLE_RELAXED, ACT_RUN_AIM_RIFLE_STIMULATED},
[ACT_LAND] = {ACT_RESET, ACT_RESET},
attack = ACT_GESTURE_RANGE_ATTACK_SHOTGUN
},
grenade = {
[ACT_MP_STAND_IDLE] = {ACT_IDLE, ACT_IDLE_MANNEDGUN},
[ACT_MP_CROUCH_IDLE] = {ACT_COVER_LOW, ACT_RANGE_AIM_SMG1_LOW},
[ACT_MP_WALK] = {ACT_WALK, ACT_WALK_AIM_RIFLE_STIMULATED},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH, ACT_WALK_CROUCH_AIM_RIFLE},
[ACT_MP_RUN] = {ACT_RUN, ACT_RUN_RIFLE_STIMULATED},
[ACT_LAND] = {ACT_RESET, ACT_RESET},
attack = ACT_RANGE_ATTACK_THROW
},
melee = {
[ACT_MP_STAND_IDLE] = {ACT_IDLE, ACT_IDLE_ANGRY_MELEE},
[ACT_MP_CROUCH_IDLE] = {ACT_COVER_LOW, ACT_COVER_LOW},
[ACT_MP_WALK] = {ACT_WALK, ACT_WALK_AIM_RIFLE},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH, ACT_WALK_CROUCH},
[ACT_MP_RUN] = {ACT_RUN, ACT_RUN},
[ACT_LAND] = {ACT_RESET, ACT_RESET},
attack = ACT_MELEE_ATTACK_SWING
},
glide = ACT_GLIDE,
vehicle = {
["prop_vehicle_prisoner_pod"] = {"podpose", Vector(-3, 0, 0)},
["prop_vehicle_jeep"] = {ACT_BUSY_SIT_CHAIR, Vector(14, 0, -14)},
["prop_vehicle_airboat"] = {ACT_BUSY_SIT_CHAIR, Vector(8, 0, -20)},
chair = {ACT_BUSY_SIT_CHAIR, Vector(1, 0, -23)}
},
}
ix.anim.citizen_female = {
normal = {
[ACT_MP_STAND_IDLE] = {ACT_IDLE, ACT_IDLE_ANGRY_SMG1},
[ACT_MP_CROUCH_IDLE] = {ACT_COVER_LOW, ACT_COVER_LOW},
[ACT_MP_WALK] = {ACT_WALK, ACT_WALK_AIM_RIFLE_STIMULATED},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH, ACT_WALK_CROUCH_AIM_RIFLE},
[ACT_MP_RUN] = {ACT_RUN, ACT_RUN_AIM_RIFLE_STIMULATED},
[ACT_LAND] = {ACT_RESET, ACT_RESET}
},
pistol = {
[ACT_MP_STAND_IDLE] = {ACT_IDLE_PISTOL, ACT_IDLE_ANGRY_PISTOL},
[ACT_MP_CROUCH_IDLE] = {ACT_COVER_LOW, ACT_RANGE_AIM_SMG1_LOW},
[ACT_MP_WALK] = {ACT_WALK, ACT_WALK_AIM_PISTOL},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH, ACT_WALK_CROUCH_AIM_RIFLE},
[ACT_MP_RUN] = {ACT_RUN, ACT_RUN_AIM_PISTOL},
[ACT_LAND] = {ACT_RESET, ACT_RESET},
attack = ACT_GESTURE_RANGE_ATTACK_PISTOL,
reload = ACT_RELOAD_PISTOL
},
smg = {
[ACT_MP_STAND_IDLE] = {ACT_IDLE_SMG1_RELAXED, ACT_IDLE_ANGRY_SMG1},
[ACT_MP_CROUCH_IDLE] = {ACT_COVER_LOW, ACT_RANGE_AIM_SMG1_LOW},
[ACT_MP_WALK] = {ACT_WALK_RIFLE_RELAXED, ACT_WALK_AIM_RIFLE_STIMULATED},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH_RIFLE, ACT_WALK_CROUCH_AIM_RIFLE},
[ACT_MP_RUN] = {ACT_RUN_RIFLE_RELAXED, ACT_RUN_AIM_RIFLE_STIMULATED},
[ACT_LAND] = {ACT_RESET, ACT_RESET},
attack = ACT_GESTURE_RANGE_ATTACK_SMG1,
reload = ACT_GESTURE_RELOAD_SMG1
},
shotgun = {
[ACT_MP_STAND_IDLE] = {ACT_IDLE_SHOTGUN_RELAXED, ACT_IDLE_ANGRY_SMG1},
[ACT_MP_CROUCH_IDLE] = {ACT_COVER_LOW, ACT_RANGE_AIM_SMG1_LOW},
[ACT_MP_WALK] = {ACT_WALK_RIFLE_RELAXED, ACT_WALK_AIM_RIFLE_STIMULATED},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH_RIFLE, ACT_WALK_CROUCH_AIM_RIFLE},
[ACT_MP_RUN] = {ACT_RUN_RIFLE_RELAXED, ACT_RUN_AIM_RIFLE_STIMULATED},
[ACT_LAND] = {ACT_RESET, ACT_RESET},
attack = ACT_GESTURE_RANGE_ATTACK_SHOTGUN
},
grenade = {
[ACT_MP_STAND_IDLE] = {ACT_IDLE, ACT_IDLE_MANNEDGUN},
[ACT_MP_CROUCH_IDLE] = {ACT_COVER_LOW, ACT_RANGE_AIM_SMG1_LOW},
[ACT_MP_WALK] = {ACT_WALK, ACT_WALK_AIM_PISTOL},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH, ACT_WALK_CROUCH_AIM_RIFLE},
[ACT_MP_RUN] = {ACT_RUN, ACT_RUN_AIM_PISTOL},
[ACT_LAND] = {ACT_RESET, ACT_RESET},
attack = ACT_RANGE_ATTACK_THROW
},
melee = {
[ACT_MP_STAND_IDLE] = {ACT_IDLE, ACT_IDLE_MANNEDGUN},
[ACT_MP_CROUCH_IDLE] = {ACT_COVER_LOW, ACT_COVER_LOW},
[ACT_MP_WALK] = {ACT_WALK, ACT_WALK_AIM_RIFLE},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH, ACT_WALK_CROUCH},
[ACT_MP_RUN] = {ACT_RUN, ACT_RUN},
[ACT_LAND] = {ACT_RESET, ACT_RESET},
attack = ACT_MELEE_ATTACK_SWING
},
glide = ACT_GLIDE,
vehicle = ix.anim.citizen_male.vehicle
}
ix.anim.metrocop = {
normal = {
[ACT_MP_STAND_IDLE] = {ACT_IDLE, ACT_IDLE_ANGRY_SMG1},
[ACT_MP_CROUCH_IDLE] = {ACT_COVER_PISTOL_LOW, ACT_COVER_SMG1_LOW},
[ACT_MP_WALK] = {ACT_WALK, ACT_WALK_AIM_RIFLE},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH, ACT_WALK_CROUCH},
[ACT_MP_RUN] = {ACT_RUN, ACT_RUN},
[ACT_LAND] = {ACT_RESET, ACT_RESET}
},
pistol = {
[ACT_MP_STAND_IDLE] = {ACT_IDLE_PISTOL, ACT_IDLE_ANGRY_PISTOL},
[ACT_MP_CROUCH_IDLE] = {ACT_COVER_PISTOL_LOW, ACT_COVER_PISTOL_LOW},
[ACT_MP_WALK] = {ACT_WALK_PISTOL, ACT_WALK_AIM_PISTOL},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH, ACT_WALK_CROUCH},
[ACT_MP_RUN] = {ACT_RUN_PISTOL, ACT_RUN_AIM_PISTOL},
[ACT_LAND] = {ACT_RESET, ACT_RESET},
attack = ACT_GESTURE_RANGE_ATTACK_PISTOL,
reload = ACT_GESTURE_RELOAD_PISTOL
},
smg = {
[ACT_MP_STAND_IDLE] = {ACT_IDLE_SMG1, ACT_IDLE_ANGRY_SMG1},
[ACT_MP_CROUCH_IDLE] = {ACT_COVER_SMG1_LOW, ACT_COVER_SMG1_LOW},
[ACT_MP_WALK] = {ACT_WALK_RIFLE, ACT_WALK_AIM_RIFLE},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH, ACT_WALK_CROUCH},
[ACT_MP_RUN] = {ACT_RUN_RIFLE, ACT_RUN_AIM_RIFLE},
[ACT_LAND] = {ACT_RESET, ACT_RESET}
},
shotgun = {
[ACT_MP_STAND_IDLE] = {ACT_IDLE_SMG1, ACT_IDLE_ANGRY_SMG1},
[ACT_MP_CROUCH_IDLE] = {ACT_COVER_SMG1_LOW, ACT_COVER_SMG1_LOW},
[ACT_MP_WALK] = {ACT_WALK_RIFLE, ACT_WALK_AIM_RIFLE},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH, ACT_WALK_CROUCH},
[ACT_MP_RUN] = {ACT_RUN_RIFLE, ACT_RUN_AIM_RIFLE},
[ACT_LAND] = {ACT_RESET, ACT_RESET}
},
grenade = {
[ACT_MP_STAND_IDLE] = {ACT_IDLE, ACT_IDLE_ANGRY_MELEE},
[ACT_MP_CROUCH_IDLE] = {ACT_COVER_PISTOL_LOW, ACT_COVER_PISTOL_LOW},
[ACT_MP_WALK] = {ACT_WALK, ACT_WALK_ANGRY},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH, ACT_WALK_CROUCH},
[ACT_MP_RUN] = {ACT_RUN, ACT_RUN},
[ACT_LAND] = {ACT_RESET, ACT_RESET},
attack = ACT_COMBINE_THROW_GRENADE
},
melee = {
[ACT_MP_STAND_IDLE] = {ACT_IDLE, ACT_IDLE_ANGRY_MELEE},
[ACT_MP_CROUCH_IDLE] = {ACT_COVER_PISTOL_LOW, ACT_COVER_PISTOL_LOW},
[ACT_MP_WALK] = {ACT_WALK, ACT_WALK_ANGRY},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH, ACT_WALK_CROUCH},
[ACT_MP_RUN] = {ACT_RUN, ACT_RUN},
[ACT_LAND] = {ACT_RESET, ACT_RESET},
attack = ACT_MELEE_ATTACK_SWING_GESTURE
},
glide = ACT_GLIDE,
vehicle = {
chair = {ACT_COVER_PISTOL_LOW, Vector(5, 0, -5)},
["prop_vehicle_airboat"] = {ACT_COVER_PISTOL_LOW, Vector(10, 0, 0)},
["prop_vehicle_jeep"] = {ACT_COVER_PISTOL_LOW, Vector(18, -2, 4)},
["prop_vehicle_prisoner_pod"] = {ACT_IDLE, Vector(-4, -0.5, 0)}
}
}
ix.anim.metrocop_female = {
normal = {
[ACT_MP_STAND_IDLE] = {ACT_IDLE, ACT_IDLE_ANGRY_SMG1},
[ACT_MP_CROUCH_IDLE] = {ACT_COVER_LOW, ACT_COVER_LOW},
[ACT_MP_WALK] = {ACT_WALK, ACT_WALK_AIM_RIFLE_STIMULATED},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH, ACT_WALK_CROUCH_AIM_RIFLE},
[ACT_MP_RUN] = {ACT_RUN, ACT_RUN_AIM_RIFLE_STIMULATED},
[ACT_LAND] = {ACT_RESET, ACT_RESET}
},
pistol = {
[ACT_MP_STAND_IDLE] = {ACT_IDLE_PISTOL, ACT_IDLE_ANGRY_PISTOL},
[ACT_MP_CROUCH_IDLE] = {ACT_COVER_LOW, ACT_RANGE_AIM_SMG1_LOW},
[ACT_MP_WALK] = {ACT_WALK, ACT_WALK_AIM_PISTOL},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH, ACT_WALK_CROUCH_AIM_RIFLE},
[ACT_MP_RUN] = {ACT_RUN, ACT_RUN_AIM_PISTOL},
[ACT_LAND] = {ACT_RESET, ACT_RESET},
attack = ACT_GESTURE_RANGE_ATTACK_PISTOL,
reload = ACT_RELOAD_PISTOL
},
smg = {
[ACT_MP_STAND_IDLE] = {ACT_IDLE_SMG1_RELAXED, ACT_IDLE_ANGRY_SMG1},
[ACT_MP_CROUCH_IDLE] = {ACT_COVER_LOW, ACT_RANGE_AIM_SMG1_LOW},
[ACT_MP_WALK] = {ACT_WALK_RIFLE_RELAXED, ACT_WALK_AIM_RIFLE_STIMULATED},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH_RIFLE, ACT_WALK_CROUCH_AIM_RIFLE},
[ACT_MP_RUN] = {ACT_RUN_RIFLE_RELAXED, ACT_RUN_AIM_RIFLE_STIMULATED},
[ACT_LAND] = {ACT_RESET, ACT_RESET},
attack = ACT_GESTURE_RANGE_ATTACK_SMG1,
reload = ACT_GESTURE_RELOAD_SMG1
},
shotgun = {
[ACT_MP_STAND_IDLE] = {ACT_IDLE_SHOTGUN_RELAXED, ACT_IDLE_ANGRY_SMG1},
[ACT_MP_CROUCH_IDLE] = {ACT_COVER_LOW, ACT_RANGE_AIM_SMG1_LOW},
[ACT_MP_WALK] = {ACT_WALK_RIFLE_RELAXED, ACT_WALK_AIM_RIFLE_STIMULATED},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH_RIFLE, ACT_WALK_CROUCH_AIM_RIFLE},
[ACT_MP_RUN] = {ACT_RUN_RIFLE_RELAXED, ACT_RUN_AIM_RIFLE_STIMULATED},
[ACT_LAND] = {ACT_RESET, ACT_RESET},
attack = ACT_GESTURE_RANGE_ATTACK_SHOTGUN
},
grenade = {
[ACT_MP_STAND_IDLE] = {ACT_IDLE, ACT_IDLE_MANNEDGUN},
[ACT_MP_CROUCH_IDLE] = {ACT_COVER_LOW, ACT_RANGE_AIM_SMG1_LOW},
[ACT_MP_WALK] = {ACT_WALK, ACT_WALK_AIM_PISTOL},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH, ACT_WALK_CROUCH_AIM_RIFLE},
[ACT_MP_RUN] = {ACT_RUN, ACT_RUN_AIM_PISTOL},
[ACT_LAND] = {ACT_RESET, ACT_RESET},
attack = ACT_RANGE_ATTACK_THROW
},
melee = {
[ACT_MP_STAND_IDLE] = {ACT_IDLE, ACT_IDLE_MANNEDGUN},
[ACT_MP_CROUCH_IDLE] = {ACT_COVER_LOW, ACT_COVER_LOW},
[ACT_MP_WALK] = {ACT_WALK, ACT_WALK_AIM_RIFLE},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH, ACT_WALK_CROUCH},
[ACT_MP_RUN] = {ACT_RUN, ACT_RUN},
[ACT_LAND] = {ACT_RESET, ACT_RESET},
attack = ACT_MELEE_ATTACK_SWING
},
glide = ACT_GLIDE,
vehicle = ix.anim.citizen_male.vehicle
}
ix.anim.overwatch = {
normal = {
[ACT_MP_STAND_IDLE] = {"idle_unarmed", ACT_IDLE_ANGRY},
[ACT_MP_CROUCH_IDLE] = {ACT_CROUCHIDLE, ACT_CROUCHIDLE},
[ACT_MP_WALK] = {"walkunarmed_all", ACT_WALK_RIFLE},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH_RIFLE, ACT_WALK_CROUCH_RIFLE},
[ACT_MP_RUN] = {ACT_RUN_AIM_RIFLE, ACT_RUN_AIM_RIFLE},
[ACT_LAND] = {ACT_RESET, ACT_RESET}
},
pistol = {
[ACT_MP_STAND_IDLE] = {"idle_unarmed", ACT_IDLE_ANGRY_SMG1},
[ACT_MP_CROUCH_IDLE] = {ACT_CROUCHIDLE, ACT_CROUCHIDLE},
[ACT_MP_WALK] = {"walkunarmed_all", ACT_WALK_RIFLE},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH_RIFLE, ACT_WALK_CROUCH_RIFLE},
[ACT_MP_RUN] = {ACT_RUN_AIM_RIFLE, ACT_RUN_AIM_RIFLE},
[ACT_LAND] = {ACT_RESET, ACT_RESET}
},
smg = {
[ACT_MP_STAND_IDLE] = {ACT_IDLE_SMG1, ACT_IDLE_ANGRY_SMG1},
[ACT_MP_CROUCH_IDLE] = {ACT_CROUCHIDLE, ACT_CROUCHIDLE},
[ACT_MP_WALK] = {ACT_WALK_RIFLE, ACT_WALK_AIM_RIFLE},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH_RIFLE, ACT_WALK_CROUCH_RIFLE},
[ACT_MP_RUN] = {ACT_RUN_RIFLE, ACT_RUN_AIM_RIFLE},
[ACT_LAND] = {ACT_RESET, ACT_RESET}
},
shotgun = {
[ACT_MP_STAND_IDLE] = {ACT_IDLE_SMG1, ACT_IDLE_ANGRY_SHOTGUN},
[ACT_MP_CROUCH_IDLE] = {ACT_CROUCHIDLE, ACT_CROUCHIDLE},
[ACT_MP_WALK] = {ACT_WALK_RIFLE, ACT_WALK_AIM_SHOTGUN},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH_RIFLE, ACT_WALK_CROUCH_RIFLE},
[ACT_MP_RUN] = {ACT_RUN_RIFLE, ACT_RUN_AIM_SHOTGUN},
[ACT_LAND] = {ACT_RESET, ACT_RESET}
},
grenade = {
[ACT_MP_STAND_IDLE] = {"idle_unarmed", ACT_IDLE_ANGRY},
[ACT_MP_CROUCH_IDLE] = {ACT_CROUCHIDLE, ACT_CROUCHIDLE},
[ACT_MP_WALK] = {"walkunarmed_all", ACT_WALK_RIFLE},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH_RIFLE, ACT_WALK_CROUCH_RIFLE},
[ACT_MP_RUN] = {ACT_RUN_AIM_RIFLE, ACT_RUN_AIM_RIFLE},
[ACT_LAND] = {ACT_RESET, ACT_RESET}
},
melee = {
[ACT_MP_STAND_IDLE] = {"idle_unarmed", ACT_IDLE_ANGRY},
[ACT_MP_CROUCH_IDLE] = {ACT_CROUCHIDLE, ACT_CROUCHIDLE},
[ACT_MP_WALK] = {"walkunarmed_all", ACT_WALK_RIFLE},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH_RIFLE, ACT_WALK_CROUCH_RIFLE},
[ACT_MP_RUN] = {ACT_RUN_AIM_RIFLE, ACT_RUN_AIM_RIFLE},
[ACT_LAND] = {ACT_RESET, ACT_RESET},
attack = ACT_MELEE_ATTACK_SWING_GESTURE
},
glide = ACT_GLIDE
}
ix.anim.vortigaunt = {
melee = {
["attack"] = ACT_MELEE_ATTACK1,
[ACT_MP_STAND_IDLE] = {ACT_IDLE, "ActionIdle"},
[ACT_MP_CROUCH_IDLE] = {ACT_CROUCHIDLE, ACT_RANGE_ATTACK_PISTOL_LOW},
[ACT_MP_RUN] = {ACT_RUN, ACT_RUN_AIM},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH, ACT_WALK_CROUCH_RIFLE},
[ACT_MP_WALK] = {ACT_WALK, ACT_WALK_AIM},
},
grenade = {
["attack"] = ACT_MELEE_ATTACK1,
[ACT_MP_STAND_IDLE] = {ACT_IDLE, "ActionIdle"},
[ACT_MP_CROUCH_IDLE] = {ACT_CROUCHIDLE, ACT_RANGE_ATTACK_PISTOL_LOW},
[ACT_MP_RUN] = {ACT_RUN, ACT_RUN_AIM},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH, ACT_WALK_CROUCH_RIFLE},
[ACT_MP_WALK] = {ACT_WALK, ACT_WALK_AIM}
},
normal = {
[ACT_MP_STAND_IDLE] = {ACT_IDLE, ACT_IDLE_ANGRY},
[ACT_MP_CROUCH_IDLE] = {ACT_CROUCHIDLE, ACT_RANGE_ATTACK_PISTOL_LOW},
[ACT_MP_RUN] = {ACT_RUN, ACT_RUN_AIM},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH, ACT_WALK_CROUCH_RIFLE},
[ACT_MP_WALK] = {ACT_WALK, ACT_WALK_AIM},
["attack"] = ACT_MELEE_ATTACK1
},
pistol = { -- beam
[ACT_MP_STAND_IDLE] = {ACT_IDLE, ACT_IDLE_ANGRY},
[ACT_MP_CROUCH_IDLE] = {ACT_CROUCHIDLE, ACT_RANGE_ATTACK_PISTOL_LOW},
[ACT_MP_RUN] = {ACT_RUN, ACT_RUN_AIM},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH, ACT_WALK_CROUCH_RIFLE},
[ACT_MP_WALK] = {ACT_WALK, ACT_WALK_AIM},
["attack"] = ACT_GESTURE_RANGE_ATTACK_PISTOL,
["reload"] = ACT_IDLE,
["glide"] = {ACT_RUN, ACT_RUN}
},
shotgun = { -- broom
[ACT_MP_STAND_IDLE] = {ACT_IDLE, "sweep_idle"},
[ACT_MP_CROUCH_IDLE] = {ACT_CROUCHIDLE, ACT_RANGE_ATTACK_PISTOL_LOW},
[ACT_MP_RUN] = {ACT_RUN, ACT_RUN},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH, ACT_WALK_CROUCH_RIFLE},
[ACT_MP_WALK] = {"Walk_all_HoldBroom", "Walk_all_HoldBroom"},
["attack"] = ACT_IDLE,
["reload"] = ACT_IDLE,
["glide"] = {ACT_RUN, ACT_RUN}
},
smg = { -- heal
[ACT_MP_STAND_IDLE] = {ACT_IDLE, ACT_IDLE_ANGRY},
[ACT_MP_CROUCH_IDLE] = {ACT_CROUCHIDLE, ACT_RANGE_ATTACK_PISTOL_LOW},
[ACT_MP_RUN] = {ACT_RUN, ACT_RUN_AIM},
[ACT_MP_CROUCHWALK] = {ACT_WALK_CROUCH, ACT_WALK_CROUCH_RIFLE},
[ACT_MP_WALK] = {ACT_WALK, ACT_WALK_AIM},
["attack"] = ACT_IDLE,
["reload"] = ACT_IDLE,
["glide"] = {ACT_RUN, ACT_RUN}
},
glide = "jump_holding_glide"
}
ix.anim.player = {
normal = {
[ACT_MP_STAND_IDLE] = ACT_HL2MP_IDLE,
[ACT_MP_CROUCH_IDLE] = ACT_HL2MP_IDLE_CROUCH,
[ACT_MP_WALK] = ACT_HL2MP_WALK,
[ACT_MP_RUN] = ACT_HL2MP_RUN,
[ACT_LAND] = {ACT_RESET, ACT_RESET}
},
passive = {
[ACT_MP_STAND_IDLE] = ACT_HL2MP_IDLE_PASSIVE,
[ACT_MP_WALK] = ACT_HL2MP_WALK_PASSIVE,
[ACT_MP_CROUCHWALK] = ACT_HL2MP_WALK_CROUCH_PASSIVE,
[ACT_MP_RUN] = ACT_HL2MP_RUN_PASSIVE,
[ACT_LAND] = {ACT_RESET, ACT_RESET}
}
}
ix.anim.zombie = {
[ACT_MP_STAND_IDLE] = ACT_HL2MP_IDLE_ZOMBIE,
[ACT_MP_CROUCH_IDLE] = ACT_HL2MP_IDLE_CROUCH_ZOMBIE,
[ACT_MP_CROUCHWALK] = ACT_HL2MP_WALK_CROUCH_ZOMBIE_01,
[ACT_MP_WALK] = ACT_HL2MP_WALK_ZOMBIE_02,
[ACT_MP_RUN] = ACT_HL2MP_RUN_ZOMBIE,
[ACT_LAND] = {ACT_RESET, ACT_RESET}
}
ix.anim.fastZombie = {
[ACT_MP_STAND_IDLE] = ACT_HL2MP_WALK_ZOMBIE,
[ACT_MP_CROUCH_IDLE] = ACT_HL2MP_IDLE_CROUCH_ZOMBIE,
[ACT_MP_CROUCHWALK] = ACT_HL2MP_WALK_CROUCH_ZOMBIE_05,
[ACT_MP_WALK] = ACT_HL2MP_WALK_ZOMBIE_06,
[ACT_MP_RUN] = ACT_HL2MP_RUN_ZOMBIE_FAST,
[ACT_LAND] = {ACT_RESET, ACT_RESET}
}
local translations = {}
--- Sets a model's animation class.
-- @realm shared
-- @string model Model name to set the animation class for
-- @string class Animation class to assign to the model
-- @usage ix.anim.SetModelClass("models/police.mdl", "metrocop")
function ix.anim.SetModelClass(model, class)
if (!ix.anim[class]) then
error("'" .. tostring(class) .. "' is not a valid animation class!")
end
translations[model:lower()] = class
end
--- Gets a model's animation class.
-- @realm shared
-- @string model Model to get the animation class for
-- @treturn[1] string Animation class of the model
-- @treturn[2] nil If there was no animation associated with the given model
-- @usage ix.anim.GetModelClass("models/police.mdl")
-- > metrocop
function ix.anim.GetModelClass(model)
model = string.lower(model)
local class = translations[model]
if (!class and string.find(model, "/player")) then
return "player"
end
class = class or "citizen_male"
if (class == "citizen_male" and (
string.find(model, "female") or
string.find(model, "alyx") or
string.find(model, "mossman"))) then
class = "citizen_female"
end
return class
end
ix.anim.SetModelClass("models/police.mdl", "metrocop")
ix.anim.SetModelClass("models/combine_super_soldier.mdl", "overwatch")
ix.anim.SetModelClass("models/combine_soldier_prisonGuard.mdl", "overwatch")
ix.anim.SetModelClass("models/combine_soldier.mdl", "overwatch")
ix.anim.SetModelClass("models/vortigaunt.mdl", "vortigaunt")
ix.anim.SetModelClass("models/vortigaunt_blue.mdl", "vortigaunt")
ix.anim.SetModelClass("models/vortigaunt_doctor.mdl", "vortigaunt")
ix.anim.SetModelClass("models/vortigaunt_slave.mdl", "vortigaunt")
if (SERVER) then
util.AddNetworkString("ixSequenceSet")
util.AddNetworkString("ixSequenceReset")
local playerMeta = FindMetaTable("Player")
--- Player anim methods
-- @classmod Player
--- Forces this player's model to play an animation sequence. It also prevents the player from firing their weapon while the
-- animation is playing.
-- @realm server
-- @string sequence Name of the animation sequence to play
-- @func[opt=nil] callback Function to call when the animation finishes. This is also called immediately if the animation
-- fails to play
-- @number[opt=nil] time How long to play the animation for. This defaults to the duration of the animation
-- @bool[opt=false] bNoFreeze Whether or not to avoid freezing this player in place while the animation is playing
-- @see LeaveSequence
function playerMeta:ForceSequence(sequence, callback, time, bNoFreeze)
hook.Run("PlayerEnterSequence", self, sequence, callback, time, bNoFreeze)
if (!sequence) then
net.Start("ixSequenceReset")
net.WriteEntity(self)
net.Broadcast()
return
end
sequence = self:LookupSequence(tostring(sequence))
if (sequence and sequence > 0) then
time = time or self:SequenceDuration(sequence)
self.ixCouldShoot = self:GetNetVar("canShoot", false)
self.ixSeqCallback = callback
self:SetCycle(0)
self:SetPlaybackRate(1)
self:SetNetVar("forcedSequence", sequence)
self:SetNetVar("canShoot", false)
if (!bNoFreeze) then
self:SetMoveType(MOVETYPE_NONE)
end
if (time > 0) then
timer.Create("ixSeq"..self:EntIndex(), time, 1, function()
if (IsValid(self)) then
self:LeaveSequence()
end
end)
end
net.Start("ixSequenceSet")
net.WriteEntity(self)
net.Broadcast()
return time
elseif (callback) then
callback()
end
return false
end
--- Forcefully stops this player's model from playing an animation that was started by `ForceSequence`.
-- @realm server
function playerMeta:LeaveSequence()
hook.Run("PlayerLeaveSequence", self)
net.Start("ixSequenceReset")
net.WriteEntity(self)
net.Broadcast()
self:SetNetVar("canShoot", self.ixCouldShoot)
self:SetNetVar("forcedSequence", nil)
self:SetMoveType(MOVETYPE_WALK)
self.ixCouldShoot = nil
if (self.ixSeqCallback) then
self:ixSeqCallback()
end
end
else
net.Receive("ixSequenceSet", function()
local entity = net.ReadEntity()
if (IsValid(entity)) then
hook.Run("PlayerEnterSequence", entity)
end
end)
net.Receive("ixSequenceReset", function()
local entity = net.ReadEntity()
if (IsValid(entity)) then
hook.Run("PlayerLeaveSequence", entity)
end
end)
end

View File

@@ -0,0 +1,201 @@
--[[
| 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/
--]]
-- @module ix.attributes
if (!ix.char) then
include("sh_character.lua")
end
ix.attributes = ix.attributes or {}
ix.attributes.list = ix.attributes.list or {}
function ix.attributes.LoadFromDir(directory)
for _, v in ipairs(file.Find(directory.."/*.lua", "LUA")) do
local niceName = v:sub(4, -5)
ATTRIBUTE = ix.attributes.list[niceName] or {}
if (PLUGIN) then
ATTRIBUTE.plugin = PLUGIN.uniqueID
end
ix.util.Include(directory.."/"..v)
ATTRIBUTE.name = ATTRIBUTE.name or "Unknown"
ATTRIBUTE.description = ATTRIBUTE.description or "No description availalble."
ix.attributes.list[niceName] = ATTRIBUTE
ATTRIBUTE = nil
end
end
function ix.attributes.Setup(client)
local character = client:GetCharacter()
if (character) then
for k, v in pairs(ix.attributes.list) do
if (v.OnSetup) then
v:OnSetup(client, character:GetAttribute(k, 0))
end
end
end
end
do
--- Character attribute methods
-- @classmod Character
local charMeta = ix.meta.character
if (SERVER) then
util.AddNetworkString("ixAttributeUpdate")
--- Increments one of this character's attributes by the given amount.
-- @realm server
-- @string key Name of the attribute to update
-- @number value Amount to add to the attribute
function charMeta:UpdateAttrib(key, value)
local attribute = ix.attributes.list[key]
local client = self:GetPlayer()
if (attribute) then
local attrib = self:GetAttributes()
attrib[key] = math.min((attrib[key] or 0) + value, attribute.maxValue or ix.config.Get("maxAttributes", 100))
if (IsValid(client)) then
net.Start("ixAttributeUpdate")
net.WriteUInt(self:GetID(), 32)
net.WriteString(key)
net.WriteFloat(attrib[key])
net.Send(client)
if (attribute.Setup) then
attribute.Setup(attrib[key])
end
end
self:SetAttributes(attrib)
end
hook.Run("CharacterAttributeUpdated", client, self, key, value)
end
--- Sets the value of an attribute for this character.
-- @realm server
-- @string key Name of the attribute to update
-- @number value New value for the attribute
function charMeta:SetAttrib(key, value)
local attribute = ix.attributes.list[key]
local client = self:GetPlayer()
if (attribute) then
local attrib = self:GetAttributes()
attrib[key] = value
if (IsValid(client)) then
net.Start("ixAttributeUpdate")
net.WriteUInt(self:GetID(), 32)
net.WriteString(key)
net.WriteFloat(attrib[key])
net.Send(client)
if (attribute.Setup) then
attribute.Setup(attrib[key])
end
end
self:SetAttributes(attrib)
end
hook.Run("CharacterAttributeUpdated", client, self, key, value)
end
--- Temporarily increments one of this character's attributes. Useful for things like consumable items.
-- @realm server
-- @string boostID Unique ID to use for the boost to remove it later
-- @string attribID Name of the attribute to boost
-- @number boostAmount Amount to increase the attribute by
function charMeta:AddBoost(boostID, attribID, boostAmount)
local boosts = self:GetVar("boosts", {})
boosts[attribID] = boosts[attribID] or {}
boosts[attribID][boostID] = boostAmount
hook.Run("CharacterAttributeBoosted", self:GetPlayer(), self, attribID, boostID, boostAmount)
return self:SetVar("boosts", boosts, nil, self:GetPlayer())
end
--- Removes a temporary boost from this character.
-- @realm server
-- @string boostID Unique ID of the boost to remove
-- @string attribID Name of the attribute that was boosted
function charMeta:RemoveBoost(boostID, attribID)
local boosts = self:GetVar("boosts", {})
boosts[attribID] = boosts[attribID] or {}
boosts[attribID][boostID] = nil
hook.Run("CharacterAttributeBoosted", self:GetPlayer(), self, attribID, boostID, true)
return self:SetVar("boosts", boosts, nil, self:GetPlayer())
end
else
net.Receive("ixAttributeUpdate", function()
local id = net.ReadUInt(32)
local character = ix.char.loaded[id]
if (character) then
local key = net.ReadString()
local value = net.ReadFloat()
character:GetAttributes()[key] = value
end
end)
end
--- Returns all boosts that this character has for the given attribute. This is only valid on the server and owning client.
-- @realm shared
-- @string attribID Name of the attribute to find boosts for
-- @treturn[1] table Table of boosts that this character has for the attribute
-- @treturn[2] nil If the character has no boosts for the given attribute
function charMeta:GetBoost(attribID)
local boosts = self:GetBoosts()
return boosts[attribID]
end
--- Returns all boosts that this character has. This is only valid on the server and owning client.
-- @realm shared
-- @treturn table Table of boosts this character has
function charMeta:GetBoosts()
return self:GetVar("boosts", {})
end
--- Returns the current value of an attribute. This is only valid on the server and owning client.
-- @realm shared
-- @string key Name of the attribute to get
-- @number default Value to return if the attribute doesn't exist
-- @treturn number Value of the attribute
function charMeta:GetAttribute(key, default)
local att = self:GetAttributes()[key] or default
local boosts = self:GetBoosts()[key]
if (boosts) then
for _, v in pairs(boosts) do
att = att + v
end
end
return att
end
end

View File

@@ -0,0 +1,161 @@
--[[
| 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/
--]]
if (SERVER) then
util.AddNetworkString("ixBusinessBuy")
util.AddNetworkString("ixBusinessResponse")
util.AddNetworkString("ixShipmentUse")
util.AddNetworkString("ixShipmentOpen")
util.AddNetworkString("ixShipmentClose")
net.Receive("ixBusinessBuy", function(length, client)
if (client.ixNextBusiness and client.ixNextBusiness > CurTime()) then
client:NotifyLocalized("businessTooFast")
return
end
local char = client:GetCharacter()
if (!char) then
return
end
local indicies = net.ReadUInt(8)
local items = {}
for _ = 1, indicies do
items[net.ReadString()] = net.ReadUInt(8)
end
if (table.IsEmpty(items)) then
return
end
local cost = 0
for k, v in pairs(items) do
local itemTable = ix.item.list[k]
if (itemTable and hook.Run("CanPlayerUseBusiness", client, k) != false) then
local amount = math.Clamp(tonumber(v) or 0, 0, 10)
items[k] = amount
if (amount == 0) then
items[k] = nil
else
cost = cost + (amount * (itemTable.price or 0))
end
else
items[k] = nil
end
end
if (table.IsEmpty(items)) then
return
end
if (char:HasMoney(cost)) then
char:TakeMoney(cost)
local entity = ents.Create("ix_shipment")
entity:Spawn()
entity:SetPos(client:GetItemDropPos(entity))
entity:SetItems(items)
entity:SetNetVar("owner", char:GetID())
local shipments = char:GetVar("charEnts") or {}
table.insert(shipments, entity)
char:SetVar("charEnts", shipments, true)
net.Start("ixBusinessResponse")
net.Send(client)
hook.Run("CreateShipment", client, entity)
client.ixNextBusiness = CurTime() + 0.5
end
end)
net.Receive("ixShipmentUse", function(length, client)
local uniqueID = net.ReadString()
local drop = net.ReadBool()
local entity = client.ixShipment
local itemTable = ix.item.list[uniqueID]
if (itemTable and IsValid(entity)) then
if (entity:GetPos():Distance(client:GetPos()) > 128) then
client.ixShipment = nil
return
end
local amount = entity.items[uniqueID]
if (amount and amount > 0) then
if (entity.items[uniqueID] <= 0) then
entity.items[uniqueID] = nil
end
if (drop) then
ix.item.Spawn(uniqueID, entity:GetPos() + Vector(0, 0, 16), function(item, itemEntity)
if (IsValid(client)) then
itemEntity.ixSteamID = client:SteamID()
itemEntity.ixCharID = client:GetCharacter():GetID()
end
end)
else
if entity.itemData then
local item, _ = client:GetCharacter():GetInventory():Add(uniqueID, 1, {
data = entity.itemData
})
if (!item) then
return client:NotifyLocalized("noFit")
end
else
local status, _ = client:GetCharacter():GetInventory():Add(uniqueID)
if (!status) then
return client:NotifyLocalized("noFit")
end
end
end
hook.Run("ShipmentItemTaken", client, uniqueID, amount)
entity.items[uniqueID] = entity.items[uniqueID] - 1
if (entity:GetItemCount() < 1) then
entity:GibBreakServer(Vector(0, 0, 0.5))
entity:Remove()
end
end
end
end)
net.Receive("ixShipmentClose", function(length, client)
local entity = client.ixShipment
if (IsValid(entity)) then
entity.ixInteractionDirty = false
client.ixShipment = nil
end
end)
else
net.Receive("ixShipmentOpen", function()
local entity = net.ReadEntity()
local items = net.ReadTable()
ix.gui.shipment = vgui.Create("ixShipment")
ix.gui.shipment:SetItems(entity, items)
end)
end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,758 @@
--[[
| 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/
--]]
--[[--
Chat manipulation and helper functions.
Chat messages are a core part of the framework - it's takes up a good chunk of the gameplay, and is also used to interact with
the framework. Chat messages can have types or "classes" that describe how the message should be interpreted. All chat messages
will have some type of class: `ic` for regular in-character speech, `me` for actions, `ooc` for out-of-character, etc. These
chat classes can affect how the message is displayed in each player's chatbox. See `ix.chat.Register` and `ChatClassStructure`
to create your own chat classes.
]]
-- @module ix.chat
ix.chat = ix.chat or {}
--- List of all chat classes that have been registered by the framework, where each key is the name of the chat class, and value
-- is the chat class data. Accessing a chat class's data is useful for when you want to copy some functionality or properties
-- to use in your own. Note that if you're accessing this table, you should do so inside of the `InitializedChatClasses` hook.
-- @realm shared
-- @table ix.chat.classes
-- @usage print(ix.chat.classes.ic.format)
-- > "%s says \"%s\""
ix.chat.classes = ix.chat.classes or {}
if (!ix.command) then
include("sh_command.lua")
end
CAMI.RegisterPrivilege({
Name = "Helix - Bypass OOC Timer",
MinAccess = "admin"
})
CAMI.RegisterPrivilege({
Name = "Helix - OOC See IC Name",
MinAccess = "admin"
})
CAMI.RegisterPrivilege({
Name = "Helix - Icon Incognito Mode",
MinAccess = "admin"
})
-- note we can't use commas in the "color" field's default value since the metadata is separated by commas which will break the
-- formatting for that field
--- Chat messages can have different classes or "types" of messages that have different properties. This can include how the
-- text is formatted, color, hearing distance, etc.
-- @realm shared
-- @table ChatClassStructure
-- @see ix.chat.Register
-- @field[type=string] prefix What the player must type before their message in order to use this chat class. For example,
-- having a prefix of `/Y` will require to type `/Y I am yelling` in order to send a message with this chat class. This can also
-- be a table of strings if you want to allow multiple prefixes, such as `{"//", "/OOC"}`.
--
-- **NOTE:** the prefix should usually start with a `/` to be consistent with the rest of the framework. However, you are able
-- to use something different like the `LOOC` chat class where the prefixes are `.//`, `[[`, and `/LOOC`.
-- @field[type=bool,opt=false] noSpaceAfter Whether or not the `prefix` can be used without a space after it. For example, the
-- `OOC` chat class allows you to type `//my message` instead of `// my message`. **NOTE:** this only works if the last
-- character in the prefix is non-alphanumeric (i.e `noSpaceAfter` with `/Y` will not work, but `/!` will).
-- @field[type=string,opt] description Description to show to the user in the chatbox when they're using this chat class
-- @field[type=string,opt="%s: \"%s\""] format How to format a message with this chat class. The first `%s` will be the speaking
-- player's name, and the second one will be their message
-- @field[type=color,opt=Color(242 230 160)] color Color to use when displaying a message with this chat class
-- @field[type=string,opt="chatTyping"] indicator Language phrase to use when displaying the typing indicator above the
-- speaking player's head
-- @field[type=bool,opt=false] bNoIndicator Whether or not to avoid showing the typing indicator above the speaking player's
-- head
-- @field[type=string,opt=ixChatFont] font Font to use for displaying a message with this chat class
-- @field[type=bool,opt=false] deadCanChat Whether or not a dead player can send a message with this chat class
-- @field[type=number] CanHear This can be either a `number` representing how far away another player can hear this message.
-- IC messages will use the `chatRange` config, for example. This can also be a function, which returns `true` if the given
-- listener can hear the message emitted from a speaker.
-- -- message can be heard by any player 1000 units away from the speaking player
-- CanHear = 1000
-- OR
-- CanHear = function(self, speaker, listener)
-- -- the speaking player will be heard by everyone
-- return true
-- end
-- @field[type=function,opt] CanSay Function to run to check whether or not a player can send a message with this chat class.
-- By default, it will return `false` if the player is dead and `deadCanChat` is `false`. Overriding this function will prevent
-- `deadCanChat` from working, and you must implement this functionality manually.
-- CanSay = function(self, speaker, text)
-- -- the speaker will never be able to send a message with this chat class
-- return false
-- end
-- @field[type=function,opt] GetColor Function to run to set the color of a message with this chat class. You should generally
-- stick to using `color`, but this is useful for when you want the color of the message to change with some criteria.
-- GetColor = function(self, speaker, text)
-- -- each message with this chat class will be colored a random shade of red
-- return Color(math.random(120, 200), 0, 0)
-- end
-- @field[type=function,opt] OnChatAdd Function to run when a message with this chat class should be added to the chatbox. If
-- using this function, make sure you end the function by calling `chat.AddText` in order for the text to show up.
--
-- **NOTE:** using your own `OnChatAdd` function will prevent `color`, `GetColor`, or `format` from being used since you'll be
-- overriding the base function that uses those properties. In such cases you'll need to add that functionality back in
-- manually. In general, you should avoid overriding this function where possible. The `data` argument in the function is
-- whatever is passed into the same `data` argument in `ix.chat.Send`.
--
-- OnChatAdd = function(self, speaker, text, bAnonymous, data)
-- -- adds white text in the form of "Player Name: Message contents"
-- chat.AddText(color_white, speaker:GetName(), ": ", text)
-- end
--- Registers a new chat type with the information provided. Chat classes should usually be created inside of the
-- `InitializedChatClasses` hook.
-- @realm shared
-- @string chatType Name of the chat type
-- @tparam ChatClassStructure data Properties and functions to assign to this chat class
-- @usage -- this is the "me" chat class taken straight from the framework as an example
-- ix.chat.Register("me", {
-- format = "** %s %s",
-- color = Color(255, 50, 50),
-- CanHear = ix.config.Get("chatRange", 280) * 2,
-- prefix = {"/Me", "/Action"},
-- description = "@cmdMe",
-- indicator = "chatPerforming",
-- deadCanChat = true
-- })
-- @see ChatClassStructure
function ix.chat.Register(chatType, data)
chatType = string.lower(chatType)
if (!data.CanHear) then
-- Have a substitute if the canHear property is not found.
function data:CanHear(speaker, listener)
-- The speaker will be heard by everyone.
return true
end
elseif (isnumber(data.CanHear)) then
-- Use the value as a range and create a function to compare distances.
local range = data.CanHear * data.CanHear
data.range = range
function data:CanHear(speaker, listener)
-- Length2DSqr is faster than Length2D, so just check the squares.
return (speaker:GetPos() - listener:GetPos()):LengthSqr() <= self.range
end
end
-- Allow players to use this chat type by default.
if (!data.CanSay) then
function data:CanSay(speaker, text)
if (!self.deadCanChat and !speaker:Alive()) then
speaker:NotifyLocalized("noPerm")
return false
end
return true
end
end
-- Chat text color.
data.color = data.color or Color(242, 230, 160)
if (!data.OnChatAdd) then
data.format = data.format or "%s: \"%s\""
data.icon = data.icon or nil
function data:OnChatAdd(speaker, text, anonymous, info)
local color = self.color
local name = anonymous and
L"someone" or hook.Run("GetCharacterName", speaker, chatType) or
(IsValid(speaker) and speaker:Name() or "Console")
if (self.GetColor) then
color = self:GetColor(speaker, text, info)
end
if self.bigfont then
local oldFont = self.font
local font = hook.Run("GetSpeakerYellFont", speaker)
self.font = font
elseif self.specialfont then
local oldFont = self.font
local font = hook.Run("GetSpeakerFont", speaker)
self.font = font
else
local oldFont = self.font
local font = "ixChatFont"
self.font = font
end
local translated = L2(chatType.."Format", name, text)
if self.icon and ix.option.Get("standardIconsEnabled") then
chat.AddText(ix.util.GetMaterial(self.icon), color, translated or string.format(self.format, name, text))
else
chat.AddText(color, translated or string.format(self.format, name, text))
end
if self.font then
self.font = oldFont
end
end
end
if (CLIENT and data.prefix) then
if (istable(data.prefix)) then
for _, v in ipairs(data.prefix) do
if (v:utf8sub(1, 1) == "/") then
ix.command.Add(v:utf8sub(2), {
description = data.description,
arguments = ix.type.text,
indicator = data.indicator,
bNoIndicator = data.bNoIndicator,
chatClass = data,
OnCheckAccess = function() return true end,
OnRun = function(self, client, message) end
})
end
end
else
ix.command.Add(isstring(data.prefix) and data.prefix:utf8sub(2) or chatType, {
description = data.description,
arguments = ix.type.text,
indicator = data.indicator,
bNoIndicator = data.bNoIndicator,
chatClass = data,
OnCheckAccess = function() return true end,
OnRun = function(self, client, message) end
})
end
end
data.uniqueID = chatType
ix.chat.classes[chatType] = data
end
--- Identifies which chat mode should be used.
-- @realm shared
-- @player client Player who is speaking
-- @string message Message to parse
-- @bool[opt=false] bNoSend Whether or not to send the chat message after parsing
-- @treturn string Name of the chat type
-- @treturn string Message that was parsed
-- @treturn bool Whether or not the speaker should be anonymous
function ix.chat.Parse(client, message, bNoSend)
local anonymous = false
local chatType = "ic"
-- Loop through all chat classes and see if the message contains their prefix.
for k, v in pairs(ix.chat.classes) do
local isChosen = false
local chosenPrefix = ""
local noSpaceAfter = v.noSpaceAfter
-- Check through all prefixes if the chat type has more than one.
if (istable(v.prefix)) then
for _, prefix in ipairs(v.prefix) do
prefix = prefix:utf8lower()
local fullPrefix = prefix .. (noSpaceAfter and "" or " ")
-- Checking if the start of the message has the prefix.
if (message:utf8sub(1, prefix:utf8len() + (noSpaceAfter and 0 or 1)):utf8lower() == fullPrefix:utf8lower()) then
isChosen = true
chosenPrefix = fullPrefix
break
end
end
-- Otherwise the prefix itself is checked.
elseif (isstring(v.prefix)) then
local prefix = v.prefix:utf8lower()
local fullPrefix = prefix .. (noSpaceAfter and "" or " ")
isChosen = message:utf8sub(1, prefix:utf8len() + (noSpaceAfter and 0 or 1)):utf8lower() == fullPrefix:utf8lower()
chosenPrefix = fullPrefix
end
-- If the checks say we have the proper chat type, then the chat type is the chosen one!
-- If this is not chosen, the loop continues. If the loop doesn't find the correct chat
-- type, then it falls back to IC chat as seen by the chatType variable above.
if (isChosen) then
-- Set the chat type to the chosen one.
chatType = k
-- Remove the prefix from the chat type so it does not show in the message.
message = message:utf8sub(chosenPrefix:utf8len() + 1)
if (ix.chat.classes[k].noSpaceAfter and message:utf8sub(1, 1):match("%s")) then
message = message:utf8sub(2)
end
break
end
end
if (!message:find("%S")) then
return
end
-- Only send if needed.
if (SERVER and !bNoSend) then
-- Send the correct chat type out so other player see the message.
ix.chat.Send(client, chatType, hook.Run("PlayerMessageSend", client, chatType, message, anonymous) or message, anonymous)
end
-- Return the chosen chat type and the message that was sent if needed for some reason.
-- This would be useful if you want to send the message on your own.
return chatType, message, anonymous
end
--- Formats a string to fix basic grammar - removing extra spacing at the beginning and end, capitalizing the first character,
-- and making sure it ends in punctuation.
-- @realm shared
-- @string text String to format
-- @treturn string Formatted string
-- @usage print(ix.chat.Format("hello"))
-- > Hello.
-- @usage print(ix.chat.Format("wow!"))
-- > Wow!
function ix.chat.Format(text)
text = string.Trim(text)
local last = text:utf8sub(-1)
if (last != "." and last != "?" and last != "!" and last != "-" and last != "\"") then
text = text .. "."
end
return text:utf8sub(1, 1):utf8upper() .. text:utf8sub(2)
end
if (SERVER) then
util.AddNetworkString("ixChatMessage")
--- Send a chat message using the specified chat type.
-- @realm server
-- @player speaker Player who is speaking
-- @string chatType Name of the chat type
-- @string text Message to send
-- @bool[opt=false] bAnonymous Whether or not the speaker should be anonymous
-- @tab[opt=nil] receivers The players to replicate send the message to
-- @tab[opt=nil] data Additional data for this chat message
function ix.chat.Send(speaker, chatType, text, bAnonymous, receivers, data)
if (!chatType) then
return
end
data = data or {}
chatType = string.lower(chatType)
if (IsValid(speaker) and hook.Run("PrePlayerMessageSend", speaker, chatType, text, bAnonymous, receivers, data) == false) then
return
end
local class = ix.chat.classes[chatType]
if (class and class:CanSay(speaker, text, data) != false) then
if (class.CanHear and !receivers) then
receivers = {}
for _, v in ipairs(player.GetAll()) do
if (v:GetCharacter() and class:CanHear(speaker, v, data) != false) then
receivers[#receivers + 1] = v
end
end
if (#receivers == 0) then
return
end
end
-- Format the message if needed before we run the hook.
local rawText = text
local maxLength = ix.config.Get("chatMax")
if (text:utf8len() > maxLength) then
text = text:utf8sub(0, maxLength)
end
if (ix.config.Get("chatAutoFormat") and hook.Run("CanAutoFormatMessage", speaker, chatType, text)) then
text = ix.chat.Format(text)
end
local iconIncognitoMode = false
if speaker and IsValid(speaker) and !speaker:IsBot() then
iconIncognitoMode = ix.option.Get(speaker, "iconIncognitoMode", false)
end
text = hook.Run("PlayerMessageSend", speaker, chatType, text, bAnonymous, receivers, rawText, data) or text
net.Start("ixChatMessage")
net.WriteEntity(speaker)
net.WriteString(chatType)
net.WriteString(text)
net.WriteBool(bAnonymous or false)
net.WriteBool(iconIncognitoMode)
net.WriteTable(data or {})
net.Send(receivers)
return text
end
end
else
function ix.chat.Send(speaker, chatType, text, anonymous, data, iconIncognitoMode)
local class = ix.chat.classes[chatType]
if (class) then
-- luacheck: globals CHAT_CLASS
CHAT_CLASS = class
class:OnChatAdd(speaker, text, anonymous, data, iconIncognitoMode)
CHAT_CLASS = nil
end
end
-- Call OnChatAdd for the appropriate chatType.
net.Receive("ixChatMessage", function()
local client = net.ReadEntity()
local chatType = net.ReadString()
local text = net.ReadString()
local anonymous = net.ReadBool()
local iconIncognitoMode = net.ReadBool()
local data = net.ReadTable()
local info = {
chatType = chatType,
text = text,
anonymous = anonymous,
iconIncognitoMode = iconIncognitoMode,
data = data
}
if (IsValid(client)) then
hook.Run("MessageReceived", client, info)
ix.chat.Send(client, info.chatType or chatType, info.text or text, info.anonymous or anonymous, info.data,
info.iconIncognitoMode)
else
if client and client.IsWorld and client:IsWorld() then
hook.Run("MessageReceived", nil, info)
end
ix.chat.Send(nil, chatType, text, anonymous, data)
end
end)
end
-- Add the default chat types here.
do
-- Load the chat types after the configs so we can access changed configs.
hook.Add("InitializedConfig", "ixChatTypes", function()
-- The default in-character chat.
ix.chat.Register("ic", {
format = "%s says \"%s\"",
icon = "willardnetworks/chat/message_icon.png",
indicator = "chatTalking",
color = Color(255, 254, 153, 255),
CanHear = ix.config.Get("chatRange", 280)
})
ix.option.Add("iconIncognitoMode", ix.type.bool, false, {
bNetworked = true,
category = "chat",
hidden = function()
return !CAMI.PlayerHasAccess(LocalPlayer(), "Helix - Icon Incognito Mode")
end
})
if (CLIENT) then
ix.option.Add("standardIconsEnabled", ix.type.bool, true, {
category = "chat"
})
ix.option.Add("seeGlobalOOC", ix.type.bool, true, {
category = "chat"
})
ix.option.Add("enablePrivateMessageSound", ix.type.bool, true, {
category = "chat"
})
end
-- Actions and such.
ix.chat.Register("me", {
format = "*** %s %s",
color = Color(214, 254, 137, 255),
CanHear = ix.config.Get("chatRange", 280) * 2,
prefix = {"/Me", "/Action"},
description = "@cmdMe",
indicator = "chatPerforming",
CanSay = function(self, speaker, text)
if (!speaker:Alive() and speaker.lastMeExpended) then
speaker:NotifyLocalized("noPerm")
return false
end
end
})
-- Actions and such.
ix.chat.Register("it", {
OnChatAdd = function(self, speaker, text)
chat.AddText(ix.config.Get("chatColor"), "***' "..text)
end,
CanHear = ix.config.Get("chatRange", 280) * 2,
prefix = {"/It"},
description = "@cmdIt",
indicator = "chatPerforming",
deadCanChat = true
})
-- Whisper chat.
ix.chat.Register("w", {
format = "%s whispers \"%s\"",
icon = "willardnetworks/chat/whisper_icon.png",
color = Color(158, 162, 191, 255),
CanHear = ix.config.Get("chatRange", 280) * 0.25,
prefix = {"/W", "/Whisper"},
description = "@cmdW",
indicator = "chatWhispering",
specialfont = true
})
-- Yelling out loud.
ix.chat.Register("y", {
format = "%s yells \"%s\"",
color = Color(254, 171, 103, 255),
icon = "willardnetworks/chat/yell_icon.png",
CanHear = ix.config.Get("chatRange", 280) * 2,
prefix = {"/Y", "/Yell"},
description = "@cmdY",
indicator = "chatYelling",
bigfont = true
})
-- REMEMBER TO UPDATE THESE WHEN WE CHANGE RANKS IN SAM
local chatIconMap = {
["willardnetworks/chat/star.png"] = {
["community_manager"] = true,
["head_of_staff"] = true,
["server_council"] = true,
["willard_management"] = true,
["superadmin"] = true
},
["willardnetworks/chat/hammer.png"] = {
["senior_admin"] = true,
["server_admin"] = true,
["trial_admin"] = true,
["admin"] = true
},
["willardnetworks/chat/paintbrush.png"] = {
["gamemaster"] = true
},
["willardnetworks/chat/wrench.png"] = {
["developer"] = true
},
["willardnetworks/chat/leaf.png"] = {
["mentor"] = true
},
["willardnetworks/chat/heart.png"] = {
["premium1"] = true,
["premium2"] = true,
["premium3"] = true
}
}
local function GetIcon(speaker, iconIncognitoMode)
local icon = "willardnetworks/chat/ooc_icon.png"
if !speaker or speaker and !IsValid(speaker) then
return ix.util.GetMaterial(icon)
end
if iconIncognitoMode then return ix.util.GetMaterial(icon) end
for mat, rankGroup in pairs(chatIconMap) do
if (rankGroup[speaker:GetUserGroup()]) then
icon = mat
end
end
return ix.util.GetMaterial(hook.Run("GetPlayerIcon", speaker) or icon)
end
-- Out of character.
ix.chat.Register("ooc", {
CanSay = function(self, speaker, text)
if (!ix.config.Get("allowGlobalOOC")) then
speaker:NotifyLocalized("GOOCIsDisabled")
return false
else
local delay = ix.config.Get("oocDelay", 10)
-- Only need to check the time if they have spoken in OOC chat before.
if (delay > 0 and speaker.ixLastOOC) then
local lastOOC = CurTime() - speaker.ixLastOOC
-- Use this method of checking time in case the oocDelay config changes.
if (lastOOC <= delay and !CAMI.PlayerHasAccess(speaker, "Helix - Bypass OOC Timer", nil)) then
speaker:NotifyLocalized("oocDelay", delay - math.ceil(lastOOC))
return false
end
end
-- Save the last time they spoke in OOC.
speaker.ixLastOOC = CurTime()
end
end,
OnChatAdd = function(self, speaker, text, _, _, iconIncognitoMode)
-- @todo remove and fix actual cause of speaker being nil
if (!IsValid(speaker) or !ix.option.Get("seeGlobalOOC", true)) then
return
end
local icon = GetIcon(speaker, iconIncognitoMode)
if (CAMI.PlayerHasAccess(LocalPlayer(), "Helix - OOC See IC Name")) then
chat.AddText(icon, Color(255, 66, 66), "[OOC] ", Color(192, 192, 196), speaker:SteamName()
, " (", speaker:Name(), ")", color_white, ": ", text)
else
chat.AddText(icon, Color(255, 66, 66), "[OOC] ", Color(192, 192, 196), speaker:SteamName(), color_white, ": ", text)
end
end,
prefix = {"//", "/OOC"},
description = "@cmdOOC",
noSpaceAfter = true
})
-- Local out of character.
ix.chat.Register("looc", {
CanSay = function(self, speaker, text)
local delay = ix.config.Get("loocDelay", 0)
-- Only need to check the time if they have spoken in OOC chat before.
if (delay > 0 and speaker.ixLastLOOC) then
local lastLOOC = CurTime() - speaker.ixLastLOOC
-- Use this method of checking time in case the oocDelay config changes.
if (lastLOOC <= delay and !CAMI.PlayerHasAccess(speaker, "Helix - Bypass OOC Timer", nil)) then
speaker:NotifyLocalized("loocDelay", delay - math.ceil(lastLOOC))
return false
end
end
-- Save the last time they spoke in OOC.
speaker.ixLastLOOC = CurTime()
end,
OnChatAdd = function(self, speaker, text, _, _, iconIncognitoMode)
local icon = GetIcon(speaker, iconIncognitoMode)
local name = hook.Run("GetCharacterName", speaker, "ic") or
(IsValid(speaker) and speaker:Name() or "Console")
chat.AddText(icon, Color(255, 66, 66), "[LOOC] ", Color(255, 254, 153, 255), name..": "..text)
end,
CanHear = ix.config.Get("chatRange", 280),
prefix = {".//", "[[", "/LOOC"},
description = "@cmdLOOC",
noSpaceAfter = true
})
-- Roll information in chat.
ix.chat.Register("roll", {
format = "*** %s has rolled %s out of %s.",
color = Color(155, 111, 176),
CanHear = ix.config.Get("chatRange", 280),
deadCanChat = true,
OnChatAdd = function(self, speaker, text, bAnonymous, data)
local max = data.max or 100
local translated = L2(self.uniqueID.."Format", speaker:Name(), text, max)
chat.AddText(self.color, translated and "*** "..translated or string.format(self.format,
speaker:Name(), text, max
))
end
})
-- run a hook after we add the basic chat classes so schemas/plugins can access their info as soon as possible if needed
hook.Run("InitializedChatClasses")
end)
end
-- Private messages between players.
ix.chat.Register("pm", {
format = "%s (%s): %s",
color = Color(255, 255, 239, 61),
deadCanChat = true,
OnChatAdd = function(self, speaker, text, bAnonymous, data)
local client = LocalPlayer()
if (ix.option.Get("standardIconsEnabled")) then
if (speaker and client == speaker) then
if !data.target or data.target and !IsValid(data.target) then return end
chat.AddText(ix.util.GetMaterial("willardnetworks/chat/pm_icon.png"), Color(254, 238, 60), "[PM] »"
, self.color, string.format(self.format, data.target:GetName(), data.target:SteamName(), text))
else
chat.AddText(ix.util.GetMaterial("willardnetworks/chat/pm_icon.png"), Color(254, 238, 60), "[PM] "
, self.color, string.format(self.format, speaker:GetName(), speaker:SteamName(), text))
end
else
if (client == speaker) then
chat.AddText(Color(254, 238, 60), "[PM] »"
, self.color, string.format(self.format, data.target:GetName(), data.target:SteamName(), text))
else
chat.AddText(Color(254, 238, 60), "[PM] "
, self.color, string.format(self.format, speaker:GetName(), speaker:SteamName(), text))
end
end
if (client != speaker and ix.option.Get("enablePrivateMessageSound", true)) then
surface.PlaySound("hl1/fvox/bell.wav")
end
end
})
-- Global events.
ix.chat.Register("event", {
CanHear = 1000000,
OnChatAdd = function(self, speaker, text)
chat.AddText(Color(254, 138, 0), text)
end,
indicator = "chatPerforming"
})
ix.chat.Register("connect", {
CanSay = function(self, speaker, text)
return !IsValid(speaker)
end,
OnChatAdd = function(self, speaker, text)
local icon = ix.util.GetMaterial("willardnetworks/chat/connected_icon.png")
chat.AddText(icon, Color(151, 153, 152), L("playerConnected", text))
end,
noSpaceAfter = true
})
ix.chat.Register("disconnect", {
CanSay = function(self, speaker, text)
return !IsValid(speaker)
end,
OnChatAdd = function(self, speaker, text)
local icon = ix.util.GetMaterial("willardnetworks/chat/disconnected_icon.png")
chat.AddText(icon, Color(151, 153, 152), L("playerDisconnected", text))
end,
noSpaceAfter = true
})
ix.chat.Register("notice", {
CanSay = function(self, speaker, text)
return !IsValid(speaker)
end,
OnChatAdd = function(self, speaker, text, bAnonymous, data)
local icon = ix.util.GetMaterial(data.bError and "icon16/comment_delete.png" or "icon16/comment.png")
chat.AddText(icon, data.bError and Color(200, 175, 200, 255) or Color(175, 200, 255), text)
end,
noSpaceAfter = true
})
-- Why does ULX even have a /me command?
hook.Remove("PlayerSay", "ULXMeCheck")

View File

@@ -0,0 +1,212 @@
--[[
| 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/
--]]
--[[--
Helper library for loading/getting class information.
Classes are temporary assignments for characters - analogous to a "job" in a faction. For example, you may have a police faction
in your schema, and have "police recruit" and "police chief" as different classes in your faction. Anyone can join a class in
their faction by default, but you can restrict this as you need with `CLASS.CanSwitchTo`.
]]
-- @module ix.class
if (SERVER) then
util.AddNetworkString("ixClassUpdate")
end
ix.class = ix.class or {}
ix.class.list = {}
local charMeta = ix.meta.character
--- Loads classes from a directory.
-- @realm shared
-- @internal
-- @string directory The path to the class files.
function ix.class.LoadFromDir(directory)
for _, v in ipairs(file.Find(directory.."/*.lua", "LUA")) do
-- Get the name without the "sh_" prefix and ".lua" suffix.
local niceName = v:sub(4, -5)
-- Determine a numeric identifier for this class.
local index = #ix.class.list + 1
local halt
for _, v2 in ipairs(ix.class.list) do
if (v2.uniqueID == niceName) then
halt = true
end
end
if (halt == true) then
continue
end
-- Set up a global table so the file has access to the class table.
CLASS = {index = index, uniqueID = niceName}
CLASS.name = "Unknown"
CLASS.description = "No description available."
CLASS.limit = 0
-- For future use with plugins.
if (PLUGIN) then
CLASS.plugin = PLUGIN.uniqueID
end
ix.util.Include(directory.."/"..v, "shared")
-- Why have a class without a faction?
if (!CLASS.faction or !team.Valid(CLASS.faction)) then
ErrorNoHalt("Class '"..niceName.."' does not have a valid faction!\n")
CLASS = nil
continue
end
-- Allow classes to be joinable by default.
if (!CLASS.CanSwitchTo) then
CLASS.CanSwitchTo = function(client)
return true
end
end
ix.class.list[index] = CLASS
CLASS = nil
end
end
--- Determines if a player is allowed to join a specific class.
-- @realm shared
-- @player client Player to check
-- @number class Index of the class
-- @treturn bool Whether or not the player can switch to the class
function ix.class.CanSwitchTo(client, class)
-- Get the class table by its numeric identifier.
local info = ix.class.list[class]
-- See if the class exists.
if (!info) then
return false, "no info"
end
-- If the player's faction matches the class's faction.
if (client:Team() != info.faction) then
return false, "not correct team"
end
if (client:GetCharacter():GetClass() == class) then
return false, "same class request"
end
if (info.limit > 0) then
if (#ix.class.GetPlayers(info.index) >= info.limit) then
return false, "class is full"
end
end
if (hook.Run("CanPlayerJoinClass", client, class, info) == false) then
return false
end
-- See if the class allows the player to join it.
return info:CanSwitchTo(client)
end
--- Retrieves a class table.
-- @realm shared
-- @number identifier Index of the class
-- @treturn table Class table
function ix.class.Get(identifier)
return ix.class.list[identifier]
end
--- Retrieves the players in a class
-- @realm shared
-- @number class Index of the class
-- @treturn table Table of players in the class
function ix.class.GetPlayers(class)
local players = {}
for _, v in ipairs(player.GetAll()) do
local char = v:GetCharacter()
if (char and char:GetClass() == class) then
table.insert(players, v)
end
end
return players
end
if (SERVER) then
--- Character class methods
-- @classmod Character
--- Makes this character join a class. This automatically calls `KickClass` for you.
-- @realm server
-- @number class Index of the class to join
-- @treturn bool Whether or not the character has successfully joined the class
function charMeta:JoinClass(class)
if (!class) then
self:KickClass()
return false
end
local oldClass = self:GetClass()
local client = self:GetPlayer()
if (ix.class.CanSwitchTo(client, class)) then
self:SetClass(class)
hook.Run("PlayerJoinedClass", client, class, oldClass)
return true
end
return false
end
--- Kicks this character out of the class they are currently in.
-- @realm server
function charMeta:KickClass()
local client = self:GetPlayer()
if (!client) then return end
local goClass
for k, v in pairs(ix.class.list) do
if (v.faction == client:Team() and v.isDefault) then
goClass = k
break
end
end
self:JoinClass(goClass)
hook.Run("PlayerJoinedClass", client, goClass)
end
function GM:PlayerJoinedClass(client, class, oldClass)
local info = ix.class.list[class]
local info2 = ix.class.list[oldClass]
if (info.OnSet) then
info:OnSet(client)
end
if (info2 and info2.OnLeave) then
info2:OnLeave(client)
end
net.Start("ixClassUpdate")
net.WriteEntity(client)
net.Broadcast()
end
end

View File

@@ -0,0 +1,636 @@
--[[
| 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/
--]]
--[[--
Registration, parsing, and handling of commands.
Commands can be ran through the chat with slash commands or they can be executed through the console. Commands can be manually
restricted to certain usergroups using a [CAMI](https://github.com/glua/CAMI)-compliant admin mod.
]]
-- @module ix.command
--- When registering commands with `ix.command.Add`, you'll need to pass in a valid command structure. This is simply a table
-- with various fields defined to describe the functionality of the command.
-- @realm shared
-- @table CommandStructure
-- @field[type=function] OnRun This function is called when the command has passed all the checks and can execute. The first two
-- arguments will be the running command table and the calling player. If the arguments field has been specified, the arguments
-- will be passed as regular function parameters rather than in a table.
--
-- When the arguments field is defined: `OnRun(self, client, target, length, message)`
--
-- When the arguments field is NOT defined: `OnRun(self, client, arguments)`
-- @field[type=string,opt="@noDesc"] description The help text that appears when the user types in the command. If the string is
-- prefixed with `"@"`, it will use a language phrase.
-- @field[type=table,opt=nil] argumentNames An array of strings corresponding to each argument of the command. This ignores the
-- name that's specified in the `OnRun` function arguments and allows you to use any string to change the text that displays
-- in the command's syntax help. When using this field, make sure that the amount is equal to the amount of arguments, as such:
-- COMMAND.arguments = {ix.type.character, ix.type.number}
-- COMMAND.argumentNames = {"target char", "cash (1-1000)"}
-- @field[type=table,opt] arguments If this field is defined, then additional checks will be performed to ensure that the
-- arguments given to the command are valid. This removes extra boilerplate code since all the passed arguments are guaranteed
-- to be valid. See `CommandArgumentsStructure` for more information.
-- @field[type=boolean,opt=false] adminOnly Provides an additional check to see if the user is an admin before running.
-- @field[type=boolean,opt=false] superAdminOnly Provides an additional check to see if the user is a superadmin before running.
-- @field[type=string,opt=nil] privilege Manually specify a privilege name for this command. It will always be prefixed with
-- `"Helix - "`. This is used in the case that you want to group commands under the same privilege, or use a privilege that
-- you've already defined (i.e grouping `/CharBan` and `/CharUnban` into the `Helix - Ban Character` privilege).
-- @field[type=function,opt=nil] OnCheckAccess This callback checks whether or not the player is allowed to run the command.
-- This callback should **NOT** be used in conjunction with `adminOnly` or `superAdminOnly`, as populating those
-- fields create a custom a `OnCheckAccess` callback for you internally. This is used in cases where you want more fine-grained
-- access control for your command.
--
-- Keep in mind that this is a **SHARED** callback; the command will not show up the client if the callback returns `false`.
--- Rather than checking the validity for arguments in your command's `OnRun` function, you can have Helix do it for you to
-- reduce the amount of boilerplate code that needs to be written. This can be done by populating the `arguments` field.
--
-- When using the `arguments` field in your command, you are specifying specific types that you expect to receive when the
-- command is ran successfully. This means that before `OnRun` is called, the arguments passed to the command from a user will
-- be verified to be valid. Each argument is an `ix.type` entry that specifies the expected type for that argument. Optional
-- arguments can be specified by using a bitwise OR with the special `ix.type.optional` type. When specified as optional, the
-- argument can be `nil` if the user has not entered anything for that argument - otherwise it will be valid.
--
-- Note that optional arguments must always be at the end of a list of arguments - or rather, they must not follow a required
-- argument. The `syntax` field will be automatically populated when using strict arguments, which means you shouldn't fill out
-- the `syntax` field yourself. The arguments you specify will have the same names as the arguments in your OnRun function.
--
-- Consider this example command:
-- ix.command.Add("CharSlap", {
-- description = "Slaps a character with a large trout.",
-- adminOnly = true,
-- arguments = {
-- ix.type.character,
-- bit.bor(ix.type.number, ix.type.optional)
-- },
-- OnRun = function(self, client, target, damage)
-- -- WHAM!
-- end
-- })
-- Here, we've specified the first argument called `target` to be of type `character`, and the second argument called `damage`
-- to be of type `number`. The `damage` argument is optional, meaning that the command will still run if the user has not
-- specified any value for the damage. In this case, we'll need to check if it was specified by doing a simple
-- `if (damage) then`. The syntax field will be automatically populated with the value `"<target: character> [damage: number]"`.
-- @realm shared
-- @table CommandArgumentsStructure
ix.command = ix.command or {}
ix.command.list = ix.command.list or {}
local COMMAND_PREFIX = "/"
local function ArgumentCheckStub(command, client, given)
local arguments = command.arguments
local result = {}
for i = 1, #arguments do
local bOptional = bit.band(arguments[i], ix.type.optional) == ix.type.optional
local argType = bOptional and bit.bxor(arguments[i], ix.type.optional) or arguments[i]
local argument = given[i]
if (!argument and !bOptional) then
return L("invalidArg", client, i)
end
if (argType == ix.type.string) then
if (!argument and bOptional) then
result[#result + 1] = nil
else
result[#result + 1] = tostring(argument)
end
elseif (argType == ix.type.text) then
result[#result + 1] = table.concat(given, " ", i) or ""
break
elseif (argType == ix.type.number) then
local value = tonumber(argument)
if (!bOptional and !value) then
return L("invalidArg", client, i)
end
result[#result + 1] = value
elseif (argType == ix.type.player or argType == ix.type.character) then
local bPlayer = argType == ix.type.player
local value
if argument == "^" then
value = client
else
value = ix.util.FindPlayer(argument)
end
-- FindPlayer emits feedback for us
if (!value and !bOptional) then
return L(bPlayer and "plyNoExist" or "charNoExist", client)
end
-- check for the character if we're using the character type
if (!bPlayer) then
local character = value:GetCharacter()
if (!character) then
return L("charNoExist", client)
end
value = character
end
result[#result + 1] = value
elseif (argType == ix.type.steamid) then
local value = argument:match("STEAM_(%d+):(%d+):(%d+)")
if (!value and bOptional) then
return L("invalidArg", client, i)
end
result[#result + 1] = value
elseif (argType == ix.type.bool) then
if (argument == nil and bOptional) then
result[#result + 1] = nil
else
result[#result + 1] = tobool(argument)
end
end
end
return result
end
--- Creates a new command.
-- @realm shared
-- @string command Name of the command (recommended in UpperCamelCase)
-- @tparam CommandStructure data Data describing the command
-- @see CommandStructure
-- @see CommandArgumentsStructure
function ix.command.Add(command, data)
data.name = string.gsub(command, "%s", "")
data.description = data.description or ""
command = command:lower()
data.uniqueID = command
-- Why bother adding a command if it doesn't do anything.
if (!data.OnRun) then
return ErrorNoHalt("Command '"..command.."' does not have a callback, not adding!\n")
end
-- Add a function to get the description that can be overridden.
if (!data.GetDescription) then
-- Check if the description is using a language string.
if (data.description:sub(1, 1) == "@") then
function data:GetDescription()
return L(self.description:sub(2))
end
else
-- Otherwise just return the raw description.
function data:GetDescription()
return self.description
end
end
end
-- OnCheckAccess by default will rely on CAMI for access information with adminOnly/superAdminOnly being fallbacks
if (!data.OnCheckAccess) then
if (data.group) then
ErrorNoHalt("Command '" .. data.name .. "' tried to use the deprecated field 'group'!\n")
return
end
local privilege = "Helix - " .. (isstring(data.privilege) and data.privilege or data.name)
-- we could be using a previously-defined privilege
if (!CAMI.GetPrivilege(privilege)) then
CAMI.RegisterPrivilege({
Name = privilege,
MinAccess = data.superAdminOnly and "superadmin" or (data.adminOnly and "admin" or "user"),
Description = data.description
})
end
function data:OnCheckAccess(client)
local bHasAccess, _ = CAMI.PlayerHasAccess(client, privilege, nil)
return bHasAccess
end
end
-- if we have an arguments table, then we're using the new command format
if (data.arguments) then
local bFirst = true
local bLastOptional = false
local bHasArgumentNames = istable(data.argumentNames)
data.syntax = "" -- @todo deprecate this in favour of argumentNames
data.argumentNames = bHasArgumentNames and data.argumentNames or {}
-- if one argument is supplied by itself, put it into a table
if (!istable(data.arguments)) then
data.arguments = {data.arguments}
end
if (bHasArgumentNames and #data.argumentNames != #data.arguments) then
return ErrorNoHalt(string.format(
"Command '%s' doesn't have argument names that correspond to each argument\n", command
))
end
-- check the arguments table to see if its entries are valid
for i = 1, #data.arguments do
local argument = data.arguments[i]
local argumentName = debug.getlocal(data.OnRun, 2 + i)
if (argument == ix.type.optional) then
return ErrorNoHalt(string.format(
"Command '%s' tried to use an optional argument for #%d without specifying type\n", command, i
))
elseif (!isnumber(argument)) then
return ErrorNoHalt(string.format(
"Command '%s' tried to use an invalid type for argument #%d\n", command, i
))
elseif (argument == ix.type.array or bit.band(argument, ix.type.array) > 0) then
return ErrorNoHalt(string.format(
"Command '%s' tried to use an unsupported type 'array' for argument #%d\n", command, i
))
end
local bOptional = bit.band(argument, ix.type.optional) > 0
argument = bOptional and bit.bxor(argument, ix.type.optional) or argument
if (!ix.type[argument]) then
return ErrorNoHalt(string.format(
"Command '%s' tried to use an invalid type for argument #%d\n", command, i
))
elseif (!isstring(argumentName)) then
return ErrorNoHalt(string.format(
"Command '%s' is missing function argument for command argument #%d\n", command, i
))
elseif (argument == ix.type.text and i != #data.arguments) then
return ErrorNoHalt(string.format(
"Command '%s' tried to use a text argument outside of the last argument\n", command
))
elseif (!bOptional and bLastOptional) then
return ErrorNoHalt(string.format(
"Command '%s' tried to use an required argument after an optional one\n", command
))
end
-- text is always optional and will return an empty string if nothing is specified, rather than nil
if (argument == ix.type.text) then
data.arguments[i] = bit.bor(ix.type.text, ix.type.optional)
bOptional = true
end
if (!bHasArgumentNames) then
data.argumentNames[i] = argumentName
end
data.syntax = data.syntax .. (bFirst and "" or " ") ..
string.format((bOptional and "[%s: %s]" or "<%s: %s>"), argumentName, ix.type[argument])
bFirst = false
bLastOptional = bOptional
end
if (data.syntax:utf8len() == 0) then
data.syntax = "<none>"
end
else
data.syntax = data.syntax or "<none>"
end
-- Add the command to the list of commands.
local alias = data.alias
if (alias) then
if (istable(alias)) then
for _, v in ipairs(alias) do
ix.command.list[v:lower()] = data
end
elseif (isstring(alias)) then
ix.command.list[alias:lower()] = data
end
end
ix.command.list[command] = data
end
--- Returns true if a player is allowed to run a certain command.
-- @realm shared
-- @player client Player to check access for
-- @string command Name of the command to check access for
-- @treturn bool Whether or not the player is allowed to run the command
function ix.command.HasAccess(client, command)
command = ix.command.list[command:lower()]
if (command) then
if (command.OnCheckAccess) then
return command:OnCheckAccess(client)
else
return true
end
end
return false
end
--- Returns a table of arguments from a given string.
-- Words separated by spaces will be considered one argument. To have an argument containing multiple words, they must be
-- contained within quotation marks.
-- @realm shared
-- @string text String to extract arguments from
-- @treturn table Arguments extracted from string
-- @usage PrintTable(ix.command.ExtractArgs("these are \"some arguments\""))
-- > 1 = these
-- > 2 = are
-- > 3 = some arguments
function ix.command.ExtractArgs(text)
local skip = 0
local arguments = {}
local curString = ""
for i = 1, text:utf8len() do
if (i <= skip) then continue end
local c = text:utf8sub(i, i)
if (c == "\"") then
local match = text:utf8sub(i):match("%b\"\"")
if (match) then
curString = ""
skip = i + match:utf8len()
arguments[#arguments + 1] = match:utf8sub(2, -2)
else
curString = curString..c
end
elseif (c == " " and curString != "") then
arguments[#arguments + 1] = curString
curString = ""
else
if (c == " " and curString == "") then
continue
end
curString = curString..c
end
end
if (curString != "") then
arguments[#arguments + 1] = curString
end
return arguments
end
--- Returns an array of potential commands by unique id.
-- When bSorted is true, the commands will be sorted by name. When bReorganize is true, it will move any exact match to the top
-- of the array. When bRemoveDupes is true, it will remove any commands that have the same NAME.
-- @realm shared
-- @string identifier Search query
-- @bool[opt=false] bSorted Whether or not to sort the commands by name
-- @bool[opt=false] bReorganize Whether or not any exact match will be moved to the top of the array
-- @bool[opt=false] bRemoveDupes Whether or not to remove any commands that have the same name
-- @treturn table Array of command tables whose name partially or completely matches the search query
function ix.command.FindAll(identifier, bSorted, bReorganize, bRemoveDupes)
local result = {}
local iterator = bSorted and SortedPairs or pairs
local fullMatch
identifier = identifier:lower()
if (identifier == "/") then
-- we don't simply copy because we need numeric indices
for _, v in iterator(ix.command.list) do
result[#result + 1] = v
end
return result
elseif (identifier:utf8sub(1, 1) == "/") then
identifier = identifier:utf8sub(2)
end
for k, v in iterator(ix.command.list) do
if (k:match(identifier)) then
local index = #result + 1
result[index] = v
if (k == identifier) then
fullMatch = index
end
end
end
if (bReorganize and fullMatch and fullMatch != 1) then
result[1], result[fullMatch] = result[fullMatch], result[1]
end
if (bRemoveDupes) then
local commandNames = {}
-- using pairs intead of ipairs because we might remove from array
for k, v in pairs(result) do
if (commandNames[v.name]) then
table.remove(result, k)
end
commandNames[v.name] = true
end
end
return result
end
if (SERVER) then
util.AddNetworkString("ixCommand")
--- Attempts to find a player by an identifier. If unsuccessful, a notice will be displayed to the specified player. The
-- search criteria is derived from `ix.util.FindPlayer`.
-- @realm server
-- @player client Player to give a notification to if the player could not be found
-- @string name Search query
-- @treturn[1] player Player that matches the given search query
-- @treturn[2] nil If a player could not be found
-- @see ix.util.FindPlayer
function ix.command.FindPlayer(client, name)
local target = isstring(name) and ix.util.FindPlayer(name) or NULL
if (IsValid(target)) then
return target
else
client:NotifyLocalized("plyNoExist")
end
end
--- Forces a player to execute a command by name.
-- @realm server
-- @player client Player who is executing the command
-- @string command Full name of the command to be executed. This string gets lowered, but it's good practice to stick with
-- the exact name of the command
-- @tab arguments Array of arguments to be passed to the command
-- @usage ix.command.Run(player.GetByID(1), "Roll", {10})
function ix.command.Run(client, command, arguments)
if ((client.ixCommandCooldown or 0) > RealTime()) then
return
end
command = ix.command.list[tostring(command):lower()]
if (!command) then
return
end
-- we throw it into a table since arguments get unpacked and only
-- the arguments table gets passed in by default
local argumentsTable = arguments
arguments = {argumentsTable}
-- if feedback is non-nil, we can assume that the command failed
-- and is a phrase string
local feedback
-- check for group access
if (command.OnCheckAccess) then
local bSuccess, phrase = command:OnCheckAccess(client)
feedback = !bSuccess and L(phrase and phrase or "noPerm", client) or nil
end
-- check for strict arguments
if (!feedback and command.arguments) then
arguments = ArgumentCheckStub(command, client, argumentsTable)
if (isstring(arguments)) then
feedback = arguments
end
end
-- run the command if all the checks passed
if (!feedback) then
local results = {command:OnRun(client, unpack(arguments))}
local phrase = results[1]
-- check to see if the command has returned a phrase string and display it
if (isstring(phrase)) then
if (IsValid(client)) then
if (phrase:sub(1, 1) == "@") then
client:NotifyLocalized(phrase:sub(2), unpack(results, 2))
else
client:Notify(phrase)
end
else
-- print message since we're running from the server console
print(phrase)
end
end
client.ixCommandCooldown = RealTime() + 0.5
if (IsValid(client)) then
ix.log.Add(client, "command", COMMAND_PREFIX .. command.name, argumentsTable and table.concat(argumentsTable, " "))
end
else
client:Notify(feedback)
end
end
--- Parses a chat string and runs the command if one is found. Specifically, it checks for commands in a string with the
-- format `/CommandName some arguments`
-- @realm server
-- @player client Player who is executing the command
-- @string text Input string to search for the command format
-- @string[opt] realCommand Specific command to check for. If this is specified, it will not try to run any command that's
-- found at the beginning - only if it matches `realCommand`
-- @tab[opt] arguments Array of arguments to pass to the command. If not specified, it will try to extract it from the
-- string specified in `text` using `ix.command.ExtractArgs`
-- @treturn bool Whether or not a command has been found
-- @usage ix.command.Parse(player.GetByID(1), "/roll 10")
function ix.command.Parse(client, text, realCommand, arguments)
if (realCommand or text:utf8sub(1, 1) == COMMAND_PREFIX) then
-- See if the string contains a command.
local match = realCommand or text:utf8lower():match(COMMAND_PREFIX.."([_%w]+)")
-- is it unicode text?
if (!match) then
local post = string.Explode(" ", text)
local len = string.len(post[1])
match = post[1]:utf8sub(2, len)
end
match = match:utf8lower()
local command = ix.command.list[match]
-- We have a valid, registered command.
if (command) then
-- Get the arguments like a console command.
if (!arguments) then
arguments = ix.command.ExtractArgs(text:utf8sub(match:utf8len() + 3))
end
-- Runs the actual command.
ix.command.Run(client, match, arguments)
else
if (IsValid(client)) then
client:NotifyLocalized("cmdNoExist")
else
print("Sorry, that command does not exist.")
end
end
return true
end
return false
end
concommand.Add("ix", function(client, _, arguments)
local command = arguments[1]
table.remove(arguments, 1)
ix.command.Parse(client, nil, command or "", arguments)
end)
net.Receive("ixCommand", function(length, client)
if ((client.ixNextCmd or 0) < CurTime()) then
local command = net.ReadString()
local indices = net.ReadUInt(4)
local arguments = {}
for _ = 1, indices do
local value = net.ReadType()
if (isstring(value) or isnumber(value)) then
arguments[#arguments + 1] = tostring(value)
end
end
ix.command.Parse(client, nil, command, arguments)
client.ixNextCmd = CurTime() + 0.2
end
end)
else
--- Request the server to run a command. This mimics similar functionality to the client typing `/CommandName` in the chatbox.
-- @realm client
-- @string command Unique ID of the command
-- @param ... Arguments to pass to the command
-- @usage ix.command.Send("roll", 10)
function ix.command.Send(command, ...)
local arguments = {...}
net.Start("ixCommand")
net.WriteString(command)
net.WriteUInt(#arguments, 4)
for _, v in ipairs(arguments) do
net.WriteType(v)
end
net.SendToServer()
end
end

View File

@@ -0,0 +1,166 @@
--[[
| 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/
--]]
ix.compnettable = ix.compnettable or {}
-- size of chunks
ix.compnettable.chunkSize = 60000
do
if (CLIENT) then
ix.compnettable.doExpoBackOff = true
-- expo backoff and jitter vars
ix.compnettable.maxBackOffTime = 20 -- tenth of a second
ix.compnettable.baseBackOffTime = 1 -- tenth of a second
ix.compnettable.attemptsInWindow = 0
ix.compnettable.windowSize = 1 -- seconds
ix.compnettable.lastRequest = CurTime()
else
-- don't do backoffs on the server (that'd break a lot of shit)
ix.compnettable.doExpobackOff = false
end
end
-- writes a compressed and chunked table with the following mechanism:
--[[
1) sends an int of the number of chunks to expect
2) writes the chunks as compressed strings
3) returns number of chunks to caller
IMPORTANT
THIS MUST BE USED IN THE CORRECT CONTEXT. EX:
net.Send("YourNetHook", function()
ix.compnettable:Write(myTable)
end)
net.Receive("YourNetHook", function()
local yourTable = ix.compnettable:Read()
end)
OTHERWISE IT WILL BREAK YOUR SHIT
]]
function ix.compnettable:Write(tbl)
if (!istable(tbl)) then
return nil
end
local str = util.TableToJSON(tbl)
local basestrlen = string.len(str)
if (!str or basestrlen < 1) then
error("Provided table could not be converted to JSON!")
end
local nchunks = math.ceil(basestrlen / self.chunkSize)
net.WriteInt(nchunks, 17)
for i=0, nchunks + 1, 1 do
local stringslice = string.sub(
str,
i * self.chunkSize,
(i + 1) *self.chunkSize
)
local chunk = util.Compress(stringslice)
if (!chunk) then
error(
"Chunk "
..tostring(i)
.." cannot be compressed! Attempted to compress: "
..stringslice
)
end
net.WriteInt(#chunk, 17)
net.WriteData(chunk, #chunk)
end
return nchunks
end
-- attempts to read a table compressed by ix.compnettable.Write()
--[[ IMPORTANT
THIS MUST BE USED IN THE CORRECT CONTEXT. EX:
net.Send("YourNetHook", function()
ix.compnettable:Write(myTable)
end)
net.Receive("YourNetHook", function()
local yourTable = ix.compnettable:Read()
end)
OTHERWISE IT WILL BREAK YOUR SHIT
]]
function ix.compnettable:Read()
local nchunks = net.ReadInt(17)
if (nchunks < 1) then
error("No chunks to read!")
end
local fullstr = ""
for i=0, nchunks + 1, 1 do
local chunksize = net.ReadInt(17)
local strcomp = net.ReadData(chunksize)
if (#strcomp == 1 and strcomp[0] == 0) then
error("Expected substring at idx ("..tostring(i)..") out of ("..tostring(nchunks)..") is empty or nil!")
end
local strchunk = util.Decompress(strcomp)
if (!strchunk) then
error("Substring ("..tostring(i)..") out of ("..tostring(nchunks)..") failed to decompress!")
end
fullstr = fullstr..strchunk
end
return util.JSONToTable(fullstr)
end
-- reads with a brief backoff to help with overflow issues
-- https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
-- ^^ a cheap (and wildly simplified) version of this:
function ix.compnettable:ReadWithBackoff()
if (CLIENT) then
local backOffTime = 0
local reqStart = SysTime()
if (self.doExpoBackOff) then
if ((reqStart - self.lastRequest) < self.windowSize) then
self.attemptsInWindow = self.attemptsInWindow + 1
local minBackOffTime = math.min(
math.pow(
self.baseBackOffTime,
self.attemptsInWindow
),
self.maxBackOffTime
)
local maxBackOffTime = minBackOffTime + (self.baseBackOffTime * 10)
backOffTime = math.Rand(minBackOffTime, maxBackOffTime) / 10
else
self.attemptsInWindow = 0
end
end
if (backOffTime > 0) then
print("Backing off for "..tostring(backOffTime).." seconds...")
-- ^^ leave this in for now just in case ;)
end
local sec = tonumber(SysTime() + backOffTime);
while (SysTime() < sec) do
-- wait for backOffTime in seconds...
-- NOTE: This might cause some freezing and things
-- BUT A FREEZE IS BETTER THAN AN OVERFLOW!!!!!!!!
end
self.lastRequest = reqStart
end
return self:Read()
end

View File

@@ -0,0 +1,121 @@
--[[
| 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/
--]]
--- A library representing the server's currency system.
-- @module ix.currency
ix.currency = ix.currency or {}
ix.currency.symbol = ix.currency.symbol or "$"
ix.currency.singular = ix.currency.singular or "dollar"
ix.currency.plural = ix.currency.plural or "dollars"
ix.currency.model = ix.currency.model or "models/props_lab/box01a.mdl"
--- Sets the currency type.
-- @realm shared
-- @string symbol The symbol of the currency.
-- @string singular The name of the currency in it's singular form.
-- @string plural The name of the currency in it's plural form.
-- @string model The model of the currency entity.
function ix.currency.Set(symbol, singular, plural, model)
ix.currency.symbol = symbol
ix.currency.singular = singular
ix.currency.plural = plural
ix.currency.model = model
end
--- Returns a formatted string according to the current currency.
-- @realm shared
-- @number amount The amount of cash being formatted.
-- @treturn string The formatted string.
function ix.currency.Get(amount)
if (amount == 1) then
return ix.currency.symbol.."1 "..ix.currency.singular
else
return ix.currency.symbol..amount.." "..ix.currency.plural
end
end
--- Spawns an amount of cash at a specific location on the map.
-- @realm shared
-- @vector pos The position of the money to be spawned.
-- @number amount The amount of cash being spawned.
-- @angle[opt=angle_zero] angle The angle of the entity being spawned.
-- @treturn entity The spawned money entity.
function ix.currency.Spawn(pos, amount, angle)
if (!amount or amount < 0) then
print("[Helix] Can't create currency entity: Invalid Amount of money")
return
end
local money = ents.Create("ix_money")
money:Spawn()
if (IsValid(pos) and pos:IsPlayer()) then
pos = pos:GetItemDropPos(money)
elseif (!isvector(pos)) then
print("[Helix] Can't create currency entity: Invalid Position")
money:Remove()
return
end
money:SetPos(pos)
-- double check for negative.
money:SetAmount(math.Round(math.abs(amount)))
money:SetAngles(angle or angle_zero)
money:Activate()
return money
end
function GM:OnPickupMoney(client, moneyEntity)
if (IsValid(moneyEntity)) then
local amount = moneyEntity:GetAmount()
client:GetCharacter():GiveMoney(amount)
end
end
do
local character = ix.meta.character
function character:HasMoney(amount)
if (amount < 0) then
print("Negative Money Check Received.")
end
return self:GetMoney() >= amount
end
function character:GiveMoney(amount, bNoLog)
amount = math.abs(amount)
if (!bNoLog) then
ix.log.Add(self:GetPlayer(), "money", amount)
end
self:SetMoney(self:GetMoney() + amount)
return true
end
function character:TakeMoney(amount, bNoLog)
amount = math.abs(amount)
if (!bNoLog) then
ix.log.Add(self:GetPlayer(), "money", -amount)
end
self:SetMoney(self:GetMoney() - amount)
return true
end
end

View File

@@ -0,0 +1,156 @@
--[[
| 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/
--]]
--[[--
Persistent date and time handling.
All of Lua's time functions are dependent on the Unix epoch, which means we can't have dates that go further than 1970. This
library remedies this problem. Time/date is represented by a `date` object that is queried, instead of relying on the seconds
since the epoch.
## Futher documentation
This library makes use of a third-party date library found at https://github.com/Tieske/date - you can find all documentation
regarding the `date` object and its methods there.
]]
-- @module ix.date
ix.date = ix.date or {}
ix.date.lib = ix.date.lib or include("thirdparty/sh_date.lua")
ix.date.timeScale = ix.date.timeScale or ix.config.Get("secondsPerMinute", 60) -- seconds per minute
ix.date.current = ix.date.current or ix.date.lib() -- current in-game date/time
ix.date.start = ix.date.start or CurTime() -- arbitrary start time for calculating date/time offset
if (SERVER) then
util.AddNetworkString("ixDateSync")
--- Loads the date from disk.
-- @realm server
-- @internal
function ix.date.Initialize()
local currentDate = ix.data.Get("date", nil, false, true)
-- construct new starting date if we don't have it saved already
if (!currentDate) then
currentDate = {
year = ix.config.Get("year"),
month = ix.config.Get("month"),
day = ix.config.Get("day"),
hour = tonumber(os.date("%H")) or 0,
min = tonumber(os.date("%M")) or 0,
sec = tonumber(os.date("%S")) or 0
}
currentDate = ix.date.lib.serialize(ix.date.lib(currentDate))
ix.data.Set("date", currentDate, false, true)
end
ix.date.timeScale = ix.config.Get("secondsPerMinute", 60)
ix.date.current = ix.date.lib.construct(currentDate)
end
--- Updates the internal in-game date/time representation and resets the offset.
-- @realm server
-- @internal
function ix.date.ResolveOffset()
ix.date.current = ix.date.Get()
ix.date.start = CurTime()
end
--- Updates the time scale of the in-game date/time. The time scale is given in seconds per minute (i.e how many real life
-- seconds it takes for an in-game minute to pass). You should avoid using this function and use the in-game config menu to
-- change the time scale instead.
-- @realm server
-- @internal
-- @number secondsPerMinute New time scale
function ix.date.UpdateTimescale(secondsPerMinute)
ix.date.ResolveOffset()
ix.date.timeScale = secondsPerMinute
end
--- Sends the current date to a player. This is done automatically when the player joins the server.
-- @realm server
-- @internal
-- @player[opt=nil] client Player to send the date to, or `nil` to send to everyone
function ix.date.Send(client)
net.Start("ixDateSync")
net.WriteFloat(ix.date.timeScale)
net.WriteTable(ix.date.current)
net.WriteFloat(ix.date.start)
if (client) then
net.Send(client)
else
net.Broadcast()
end
end
--- Saves the current in-game date to disk.
-- @realm server
-- @internal
function ix.date.Save()
ix.date.bSaving = true
ix.date.ResolveOffset() -- resolve offset so we save the actual time to disk
ix.data.Set("date", ix.date.lib.serialize(ix.date.current), false, true)
-- update config to reflect current saved date
ix.config.Set("year", ix.date.current:getyear())
ix.config.Set("month", ix.date.current:getmonth())
ix.config.Set("day", ix.date.current:getday())
ix.date.bSaving = nil
end
else
net.Receive("ixDateSync", function()
local timeScale = net.ReadFloat()
local currentDate = ix.date.lib.construct(net.ReadTable())
local startTime = net.ReadFloat()
ix.date.timeScale = timeScale
ix.date.current = currentDate
ix.date.start = startTime
end)
end
--- Returns the currently set date.
-- @realm shared
-- @treturn date Current in-game date
function ix.date.Get()
local minutesSinceStart = (CurTime() - ix.date.start) / ix.date.timeScale
return ix.date.current:copy():addminutes(minutesSinceStart)
end
--- Returns a string formatted version of a date.
-- @realm shared
-- @string format Format string
-- @date[opt=nil] currentDate Date to format. If nil, it will use the currently set date
-- @treturn string Formatted date
function ix.date.GetFormatted(format, currentDate)
return (currentDate or ix.date.Get()):fmt(format)
end
--- Returns a serialized version of a date. This is useful when you need to network a date to clients, or save a date to disk.
-- @realm shared
-- @date[opt=nil] currentDate Date to serialize. If nil, it will use the currently set date
-- @treturn table Serialized date
function ix.date.GetSerialized(currentDate)
return ix.date.lib.serialize(currentDate or ix.date.Get())
end
--- Returns a date object from a table or serialized date.
-- @realm shared
-- @param currentDate Date to construct
-- @treturn date Constructed date object
function ix.date.Construct(currentDate)
return ix.date.lib.construct(currentDate)
end

View File

@@ -0,0 +1,135 @@
--[[
| 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/
--]]
--- Helper library for loading/getting faction information.
-- @module ix.faction
ix.faction = ix.faction or {}
ix.faction.teams = ix.faction.teams or {}
ix.faction.indices = ix.faction.indices or {}
local CITIZEN_MODELS = {
"models/humans/group01/male_01.mdl",
"models/humans/group01/male_02.mdl",
"models/humans/group01/male_04.mdl",
"models/humans/group01/male_05.mdl",
"models/humans/group01/male_06.mdl",
"models/humans/group01/male_07.mdl",
"models/humans/group01/male_08.mdl",
"models/humans/group01/male_09.mdl",
"models/humans/group02/male_01.mdl",
"models/humans/group02/male_03.mdl",
"models/humans/group02/male_05.mdl",
"models/humans/group02/male_07.mdl",
"models/humans/group02/male_09.mdl",
"models/humans/group01/female_01.mdl",
"models/humans/group01/female_02.mdl",
"models/humans/group01/female_03.mdl",
"models/humans/group01/female_06.mdl",
"models/humans/group01/female_07.mdl",
"models/humans/group02/female_01.mdl",
"models/humans/group02/female_03.mdl",
"models/humans/group02/female_06.mdl",
"models/humans/group01/female_04.mdl"
}
--- Loads factions from a directory.
-- @realm shared
-- @string directory The path to the factions files.
function ix.faction.LoadFromDir(directory)
for _, v in ipairs(file.Find(directory.."/*.lua", "LUA")) do
local niceName = v:sub(4, -5)
FACTION = ix.faction.teams[niceName] or {index = table.Count(ix.faction.teams) + 1, isDefault = false}
if (PLUGIN) then
FACTION.plugin = PLUGIN.uniqueID
end
ix.util.Include(directory.."/"..v, "shared")
if (!FACTION.name) then
FACTION.name = "Unknown"
ErrorNoHalt("Faction '"..niceName.."' is missing a name. You need to add a FACTION.name = \"Name\"\n")
end
if (!FACTION.color) then
FACTION.color = Color(150, 150, 150)
ErrorNoHalt("Faction '"..niceName.."' is missing a color. You need to add FACTION.color = Color(1, 2, 3)\n")
end
team.SetUp(FACTION.index, FACTION.name or "Unknown", FACTION.color or Color(125, 125, 125))
FACTION.models = FACTION.models or CITIZEN_MODELS
FACTION.uniqueID = FACTION.uniqueID or niceName
for _, v2 in pairs(FACTION.models) do
if (isstring(v2)) then
util.PrecacheModel(v2)
elseif (istable(v2)) then
util.PrecacheModel(v2[1])
end
end
if (!FACTION.GetModels) then
function FACTION:GetModels(client)
return self.models
end
end
ix.faction.indices[FACTION.index] = FACTION
ix.faction.teams[niceName] = FACTION
FACTION = nil
end
end
--- Retrieves a faction table.
-- @realm shared
-- @param identifier Index or name of the faction
-- @treturn table Faction table
-- @usage print(ix.faction.Get(Entity(1):Team()).name)
-- > "Citizen"
function ix.faction.Get(identifier)
return ix.faction.indices[identifier] or ix.faction.teams[identifier]
end
--- Retrieves a faction index.
-- @realm shared
-- @string uniqueID Unique ID of the faction
-- @treturn number Faction index
function ix.faction.GetIndex(uniqueID)
for k, v in ipairs(ix.faction.indices) do
if (v.uniqueID == uniqueID) then
return k
end
end
end
if (CLIENT) then
--- Returns true if a faction requires a whitelist.
-- @realm client
-- @number faction Index of the faction
-- @treturn bool Whether or not the faction requires a whitelist
function ix.faction.HasWhitelist(faction)
local data = ix.faction.indices[faction]
if (data) then
if (data.isDefault) then
return true
end
local ixData = ix.localData and ix.localData.whitelists or {}
return ixData[Schema.folder] and ixData[Schema.folder][data.uniqueID] == true or false
end
return false
end
end

View File

@@ -0,0 +1,213 @@
--[[
| 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/
--]]
--[[--
Grants abilities to characters.
Flags are a simple way of adding/removing certain abilities to players on a per-character basis. Helix comes with a few flags
by default, for example to restrict spawning of props, usage of the physgun, etc. All flags will be listed in the
`Flags` section of the `Help` menu. Flags are usually used when server validation is required to allow a player to do something
on their character. However, it's usually preferable to use in-character methods over flags when possible (i.e restricting
the business menu to characters that have a permit item, rather than using flags to determine availability).
Flags are a single alphanumeric character that can be checked on the server. Serverside callbacks can be used to provide
functionality whenever the flag is added or removed. For example:
ix.flag.Add("z", "Access to some cool stuff.", function(client, bGiven)
print("z flag given:", bGiven)
end)
Entity(1):GetCharacter():GiveFlags("z")
> z flag given: true
Entity(1):GetCharacter():TakeFlags("z")
> z flag given: false
print(Entity(1):GetCharacter():HasFlags("z"))
> false
Check out `Character:GiveFlags` and `Character:TakeFlags` for additional info.
]]
-- @module ix.flag
ix.flag = ix.flag or {}
ix.flag.list = ix.flag.list or {}
--- Creates a flag. This should be called shared in order for the client to be aware of the flag's existence.
-- @realm shared
-- @string flag Alphanumeric character to use for the flag
-- @string description Description of the flag
-- @func callback Function to call when the flag is given or taken from a player
function ix.flag.Add(flag, description, callback)
ix.flag.list[flag] = {
description = description,
callback = callback
}
end
if (SERVER) then
-- Called to apply flags when a player has spawned.
-- @realm server
-- @internal
-- @player client Player to setup flags for
function ix.flag.OnSpawn(client)
-- Check if they have a valid character.
if (client:GetCharacter()) then
-- Get all of the character's flags.
local flags = client:GetCharacter():GetFlags()
for i = 1, #flags do
-- Get each individual flag.
local flag = flags[i]
local info = ix.flag.list[flag]
-- Check if the flag has a callback.
if (info and info.callback) then
-- Run the callback, passing the player and true so they get whatever benefits.
info.callback(client, true)
end
end
end
end
end
do
local character = ix.meta.character
if (SERVER) then
--- Flag util functions for character
-- @classmod Character
--- Sets this character's accessible flags. Note that this method overwrites **all** flags instead of adding them.
-- @realm server
-- @string flags Flag(s) this charater is allowed to have
-- @see GiveFlags
function character:SetFlags(flags)
self:SetData("f", flags)
end
--- Adds a flag to the list of this character's accessible flags. This does not overwrite existing flags.
-- @realm server
-- @string flags Flag(s) this character should be given
-- @usage character:GiveFlags("pet")
-- -- gives p, e, and t flags to the character
-- @see HasFlags
function character:GiveFlags(flags)
local addedFlags = ""
-- Get the individual flags within the flag string.
for i = 1, #flags do
local flag = flags[i]
local info = ix.flag.list[flag]
if (info) then
if (!self:HasFlags(flag)) then
addedFlags = addedFlags..flag
end
if (info.callback) then
-- Pass the player and true (true for the flag being given.)
info.callback(self:GetPlayer(), true)
end
end
end
-- Only change the flag string if it is different.
if (addedFlags != "") then
self:SetFlags(self:GetFlags()..addedFlags)
end
end
--- Removes this character's access to the given flags.
-- @realm server
-- @string flags Flag(s) to remove from this character
-- @usage -- for a character with "pet" flags
-- character:TakeFlags("p")
-- -- character now has e, and t flags
function character:TakeFlags(flags)
local oldFlags = self:GetFlags()
local newFlags = oldFlags
-- Get the individual flags within the flag string.
for i = 1, #flags do
local flag = flags[i]
local info = ix.flag.list[flag]
-- Call the callback if the flag has been registered.
if (info and info.callback) then
-- Pass the player and false (false since the flag is being taken)
info.callback(self:GetPlayer(), false)
end
newFlags = newFlags:gsub(flag, "")
end
if (newFlags != oldFlags) then
self:SetFlags(newFlags)
end
end
end
--- Returns all of the flags this character has.
-- @realm shared
-- @treturn string Flags this character has represented as one string. You can access individual flags by iterating through
-- the string letter by letter
function character:GetFlags()
return self:GetData("f", "")
end
--- Returns `true` if the character has the given flag(s).
-- @realm shared
-- @string flags Flag(s) to check access for
-- @treturn bool Whether or not this character has access to the given flag(s)
function character:HasFlags(flags)
local bHasFlag = hook.Run("CharacterHasFlags", self, flags)
if (bHasFlag == true) then
return true
end
local flagList = self:GetFlags()
for i = 1, #flags do
if (flagList:find(flags[i], 1, true)) then
return true
end
end
return false
end
end
do
ix.flag.Add("p", "Access to the physgun.", function(client, isGiven)
if (isGiven) then
client:Give("weapon_physgun")
--client:SelectWeapon("weapon_physgun")
else
client:StripWeapon("weapon_physgun")
end
end)
ix.flag.Add("t", "Access to the toolgun", function(client, isGiven)
if (isGiven) then
client:Give("gmod_tool")
--client:SelectWeapon("gmod_tool")
else
client:StripWeapon("gmod_tool")
end
end)
ix.flag.Add("c", "Access to spawn chairs.")
ix.flag.Add("C", "Access to spawn vehicles.")
ix.flag.Add("r", "Access to spawn ragdolls.")
ix.flag.Add("e", "Access to spawn props.")
ix.flag.Add("n", "Access to spawn NPCs.")
end

View File

@@ -0,0 +1,186 @@
--[[
| 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/
--]]
--[[--
Inventory manipulation and helper functions.
]]
-- @module ix.inventory
ix.inventory = ix.inventory or {}
ix.util.Include("helix/gamemode/core/meta/sh_inventory.lua")
--- Retrieves an inventory table.
-- @realm shared
-- @number invID Index of the inventory
-- @treturn Inventory Inventory table
function ix.inventory.Get(invID)
return ix.item.inventories[invID]
end
function ix.inventory.Create(width, height, id)
local inventory = ix.meta.inventory:New(id, width, height)
ix.item.inventories[id] = inventory
return inventory
end
--- Loads an inventory and associated items from the database into memory. If you are passing a table into `invID`, it
-- requires a table where the key is the inventory ID, and the value is a table of the width and height values. See below
-- for an example.
-- @realm server
-- @param invID Inventory ID or table of inventory IDs
-- @number width Width of inventory (this is not used when passing a table to `invID`)
-- @number height Height of inventory (this is not used when passing a table to `invID`)
-- @func callback Function to call when inventory is restored
-- @usage ix.inventory.Restore({
-- [10] = {5, 5},
-- [11] = {7, 4}
-- })
-- -- inventories 10 and 11 with sizes (5, 5) and (7, 4) will be loaded
function ix.inventory.Restore(invID, width, height, callback)
local inventories = {}
if (!istable(invID)) then
if (!isnumber(invID) or invID < 0) then
error("Attempt to restore inventory with an invalid ID!")
end
inventories[invID] = {width, height}
ix.inventory.Create(width, height, invID)
else
for k, v in pairs(invID) do
inventories[k] = {v[1], v[2]}
ix.inventory.Create(v[1], v[2], k)
end
end
local query = mysql:Select("ix_items")
query:Select("item_id")
query:Select("inventory_id")
query:Select("unique_id")
query:Select("data")
query:Select("character_id")
query:Select("player_id")
query:Select("x")
query:Select("y")
query:WhereIn("inventory_id", table.GetKeys(inventories))
query:Callback(function(result)
if (istable(result) and #result > 0) then
local invSlots = {}
for _, item in ipairs(result) do
local itemInvID = tonumber(item.inventory_id)
local invInfo = inventories[itemInvID]
if (!itemInvID or !invInfo) then
-- don't restore items with an invalid inventory id or type
continue
end
local inventory = ix.item.inventories[itemInvID]
local x, y = tonumber(item.x), tonumber(item.y)
local itemID = tonumber(item.item_id)
local data = util.JSONToTable(item.data or "[]")
local characterID, playerID = tonumber(item.character_id), tostring(item.player_id)
if (x and y and itemID) then
if (x <= inventory.w and x > 0 and y <= inventory.h and y > 0) then
local item2 = ix.item.New(item.unique_id, itemID)
if (item2) then
invSlots[itemInvID] = invSlots[itemInvID] or {}
local slots = invSlots[itemInvID]
item2.data = {}
if (data) then
item2.data = data
end
item2.gridX = x
item2.gridY = y
item2.invID = itemInvID
item2.characterID = characterID
item2.playerID = (playerID == "" or playerID == "NULL") and nil or playerID
for x2 = 0, item2.width - 1 do
for y2 = 0, item2.height - 1 do
slots[x + x2] = slots[x + x2] or {}
slots[x + x2][y + y2] = item2
end
end
if (item2.OnRestored) then
item2:OnRestored(item2, itemInvID)
end
end
end
end
end
for k, v in pairs(invSlots) do
ix.item.inventories[k].slots = v
end
end
if (callback) then
for k, _ in pairs(inventories) do
callback(ix.item.inventories[k])
end
end
end)
query:Execute()
end
function ix.inventory.New(owner, invType, callback)
local invData = ix.item.inventoryTypes[invType] or {w = 1, h = 1}
local query = mysql:Insert("ix_inventories")
query:Insert("inventory_type", invType)
query:Insert("character_id", owner)
query:Callback(function(result, status, lastID)
local inventory = ix.inventory.Create(invData.w, invData.h, lastID)
if (invType) then
if invType == "equipInventory" then
inventory.vars.isBag = false
else
inventory.vars.isBag = invType
end
end
if (isnumber(owner) and owner > 0) then
local character = ix.char.loaded[owner]
local client = character:GetPlayer()
inventory:SetOwner(owner)
if (IsValid(client)) then
inventory:Sync(client)
end
end
if (callback) then
callback(inventory)
end
end)
query:Execute()
end
function ix.inventory.Register(invType, w, h, isBag)
ix.item.inventoryTypes[invType] = {w = w, h = h}
if (isBag) then
ix.item.inventoryTypes[invType].isBag = invType
end
return ix.item.inventoryTypes[invType]
end

View File

@@ -0,0 +1,879 @@
--[[
| 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/
--]]
--[[--
Item manipulation and helper functions.
]]
-- @module ix.item
ix.item = ix.item or {}
ix.item.list = ix.item.list or {}
ix.item.base = ix.item.base or {}
ix.item.instances = ix.item.instances or {}
ix.item.inventories = ix.item.inventories or {
[0] = {}
}
ix.item.inventoryTypes = ix.item.inventoryTypes or {}
ix.util.Include("helix/gamemode/core/meta/sh_item.lua")
-- Declare some supports for logic inventory
local zeroInv = ix.item.inventories[0]
function zeroInv:GetID()
return 0
end
function zeroInv:OnCheckAccess(client)
return true
end
-- WARNING: You have to manually sync the data to client if you're trying to use item in the logical inventory in the vgui.
function zeroInv:Add(uniqueID, quantity, data, x, y)
quantity = quantity or 1
if (quantity > 0) then
if (!isnumber(uniqueID)) then
if (quantity > 1) then
for _ = 1, quantity do
self:Add(uniqueID, 1, data)
end
return
end
local itemTable = ix.item.list[uniqueID]
if (!itemTable) then
return false, "invalidItem"
end
ix.item.Instance(0, uniqueID, data, x, y, function(item)
self[item:GetID()] = item
end)
return nil, nil, 0
end
else
return false, "notValid"
end
end
function ix.item.Instance(index, uniqueID, itemData, x, y, callback, characterID, playerID)
if (!uniqueID or ix.item.list[uniqueID]) then
itemData = istable(itemData) and itemData or {}
local query = mysql:Insert("ix_items")
query:Insert("inventory_id", index)
query:Insert("unique_id", uniqueID)
query:Insert("data", util.TableToJSON(itemData))
query:Insert("x", x)
query:Insert("y", y)
if (characterID) then
query:Insert("character_id", characterID)
end
if (playerID) then
query:Insert("player_id", playerID)
end
query:Callback(function(result, status, lastID)
local item = ix.item.New(uniqueID, lastID)
if (item) then
item.data = table.Copy(itemData)
item.invID = index
item.characterID = characterID
item.playerID = playerID
if (callback) then
callback(item)
end
if (item.OnInstanced) then
item:OnInstanced(index, x, y, item)
end
end
end)
query:Execute()
else
ErrorNoHalt("[Helix] Attempt to give an invalid item! (" .. (uniqueID or "nil") .. ")\n")
end
end
--- Retrieves an item table.
-- @realm shared
-- @string identifier Unique ID of the item
-- @treturn item Item table
-- @usage print(ix.item.Get("example"))
-- > "item[example][0]"
function ix.item.Get(identifier)
return ix.item.base[identifier] or ix.item.list[identifier]
end
function ix.item.Load(path, baseID, isBaseItem)
local uniqueID = path:match("sh_([_%w]+)%.lua")
if (uniqueID) then
uniqueID = (isBaseItem and "base_" or "")..uniqueID
ix.item.Register(uniqueID, baseID, isBaseItem, path)
else
if (!path:find(".txt")) then
ErrorNoHalt("[Helix] Item at '"..path.."' follows invalid naming convention!\n")
end
end
end
function ix.item.Register(uniqueID, baseID, isBaseItem, path, luaGenerated)
local meta = ix.meta.item
if (uniqueID) then
ITEM = (isBaseItem and ix.item.base or ix.item.list)[uniqueID] or setmetatable({}, meta)
ITEM.uniqueID = uniqueID
ITEM.base = baseID or ITEM.base
ITEM.isBase = isBaseItem
ITEM.hooks = ITEM.hooks or {}
ITEM.postHooks = ITEM.postHooks or {}
ITEM.functions = ITEM.functions or {}
ITEM.functions.drop = ITEM.functions.drop or {
tip = "dropTip",
icon = "icon16/world.png",
OnRun = function(item)
local bSuccess, error = item:Transfer(nil, nil, nil, item.player)
if (!bSuccess and isstring(error)) then
item.player:NotifyLocalized(error)
else
item.player:EmitSound("npc/zombie/foot_slide" .. math.random(1, 3) .. ".wav", 75, math.random(90, 120), 1)
end
return false
end,
OnCanRun = function(item)
return !IsValid(item.entity) and !item.noDrop
end
}
ITEM.functions.take = ITEM.functions.take or {
tip = "takeTip",
icon = "icon16/box.png",
OnRun = function(item)
local client = item.player
local bSuccess, error = item:Transfer(client:GetCharacter():GetInventory():GetID(), nil, nil, client)
if (!bSuccess) then
client:NotifyLocalized(error or "unknownError")
return false
else
client:EmitSound("npc/zombie/foot_slide" .. math.random(1, 3) .. ".wav", 75, math.random(90, 120), 1)
if (item.data) then -- I don't like it but, meh...
for k, v in pairs(item.data) do
item:SetData(k, v)
end
end
end
return true
end,
OnCanRun = function(item)
return IsValid(item.entity)
end
}
ITEM.functions.removeLabel = ITEM.functions.removeLabel or {
name = "Remove Label",
icon = "icon16/tag_blue_delete.png",
OnRun = function(item)
local client = item.player
local inventory = client:GetCharacter():GetInventory()
inventory:Add("itemlabel", 1, {
labelInfoName = item:GetData("labelName"),
labelInfoDesc = item:GetData("labelDescription")
})
item:SetData("labelName", false)
item:SetData("labelDescription", false)
client:Notify("Etiketi şundan kaldırdınız: " .. item.name .. ".")
return false
end,
OnCanRun = function(item)
return item:GetData("labelName", false) or item:GetData("labelDescription", false)
end
}
local oldBase = ITEM.base
if (ITEM.base) then
local baseTable = ix.item.base[ITEM.base]
if (baseTable) then
for k, v in pairs(baseTable) do
if (ITEM[k] == nil) then
ITEM[k] = v
end
ITEM.baseTable = baseTable
end
local mergeTable = table.Copy(baseTable)
ITEM = table.Merge(mergeTable, ITEM)
else
ErrorNoHalt("[Helix] Item '"..ITEM.uniqueID.."' has a non-existent base! ("..ITEM.base..")\n")
end
end
if (PLUGIN) then
ITEM.plugin = PLUGIN.uniqueID
end
if (!luaGenerated and path) then
ix.util.Include(path)
end
if (ITEM.base and oldBase != ITEM.base) then
local baseTable = ix.item.base[ITEM.base]
if (baseTable) then
for k, v in pairs(baseTable) do
if (ITEM[k] == nil) then
ITEM[k] = v
end
ITEM.baseTable = baseTable
end
local mergeTable = table.Copy(baseTable)
ITEM = table.Merge(mergeTable, ITEM)
else
ErrorNoHalt("[Helix] Item '"..ITEM.uniqueID.."' has a non-existent base! ("..ITEM.base..")\n")
end
end
ITEM.description = ITEM.description or "noDesc"
ITEM.width = ITEM.width or 1
ITEM.height = ITEM.height or 1
ITEM.category = ITEM.category or "misc"
if (ITEM.OnRegistered) then
ITEM:OnRegistered()
end
(isBaseItem and ix.item.base or ix.item.list)[ITEM.uniqueID] = ITEM
if (IX_RELOADED) then
-- we don't know which item was actually edited, so we'll refresh all of them
for _, v in pairs(ix.item.instances) do
if (v.uniqueID == uniqueID) then
table.Merge(v, ITEM)
end
end
end
if (luaGenerated) then
return ITEM
else
ITEM = nil
end
else
ErrorNoHalt("[Helix] You tried to register an item without uniqueID!\n")
end
end
function ix.item.LoadFromDir(directory)
local files, folders
files = file.Find(directory.."/base/*.lua", "LUA")
for _, v in ipairs(files) do
ix.item.Load(directory.."/base/"..v, nil, true)
end
files, folders = file.Find(directory.."/*", "LUA")
for _, v in ipairs(folders) do
if (v == "base") then
continue
end
for _, v2 in ipairs(file.Find(directory.."/"..v.."/*.lua", "LUA")) do
ix.item.Load(directory.."/"..v .. "/".. v2, "base_"..v)
end
end
for _, v in ipairs(files) do
ix.item.Load(directory.."/"..v)
end
end
function ix.item.New(uniqueID, id)
if (ix.item.instances[id] and ix.item.instances[id].uniqueID == uniqueID) then
return ix.item.instances[id]
end
local stockItem = ix.item.list[uniqueID]
if (stockItem) then
local item = setmetatable({id = id, data = {}}, {
__index = stockItem,
__eq = stockItem.__eq,
__tostring = stockItem.__tostring
})
ix.item.instances[id] = item
return item
else
ErrorNoHalt("[Helix] Attempt to index unknown item '"..uniqueID.."'\n")
end
end
do
function ix.item.GetInv(invID)
ErrorNoHalt("ix.item.GetInv is deprecated. Use ix.inventory.Get instead!\n")
return ix.inventory.Get(invID)
end
function ix.item.RegisterInv(invType, w, h, isBag)
ErrorNoHalt("ix.item.RegisterInv is deprecated. Use ix.inventory.Register instead!\n")
return ix.inventory.Register(invType, w, h, isBag)
end
function ix.item.NewInv(owner, invType, callback)
ErrorNoHalt("ix.item.NewInv is deprecated. Use ix.inventory.New instead!\n")
return ix.inventory.New(owner, invType, callback)
end
function ix.item.CreateInv(width, height, id)
ErrorNoHalt("ix.item.CreateInv is deprecated. Use ix.inventory.Create instead!\n")
return ix.inventory.Create(width, height, id)
end
function ix.item.RestoreInv(invID, width, height, callback)
ErrorNoHalt("ix.item.RestoreInv is deprecated. Use ix.inventory.Restore instead!\n")
return ix.inventory.Restore(invID, width, height, callback)
end
if (CLIENT) then
net.Receive("ixInventorySync", function()
local slots = net.ReadTable()
local id = net.ReadUInt(32)
local w, h = net.ReadUInt(6), net.ReadUInt(6)
local owner = net.ReadType()
local vars = net.ReadTable()
if (!LocalPlayer():GetCharacter()) then
return
end
local character = owner and ix.char.loaded[owner]
local inventory = ix.inventory.Create(w, h, id)
inventory.slots = {}
inventory.vars = vars
local x, y
for _, v in ipairs(slots) do
x, y = v[1], v[2]
inventory.slots[x] = inventory.slots[x] or {}
local item = ix.item.New(v[3], v[4])
item.data = {}
if (v[5]) then
item.data = v[5]
end
item.invID = item.invID or id
inventory.slots[x][y] = item
end
if (character) then
inventory:SetOwner(character:GetID())
character.vars.inv = character.vars.inv or {}
for k, v in ipairs(character:GetInventory(true)) do
if (v:GetID() == id) then
character:GetInventory(true)[k] = inventory
return
end
end
table.insert(character.vars.inv, inventory)
end
end)
net.Receive("ixInventoryData", function()
local id = net.ReadUInt(32)
local item = ix.item.instances[id]
if (item) then
local key = net.ReadString()
local value = net.ReadType()
item.data = item.data or {}
item.data[key] = value
local invID = item.invID == LocalPlayer():GetCharacter():GetInventory():GetID() and 1 or item.invID
local panel = ix.gui["inv" .. invID]
if (panel and panel.panels) then
local icon = panel.panels[id]
if (icon) then
icon:SetHelixTooltip(function(tooltip)
ix.hud.PopulateItemTooltip(tooltip, item)
end)
end
end
end
end)
net.Receive("ixInventorySet", function()
local invID = net.ReadUInt(32)
local x, y = net.ReadUInt(6), net.ReadUInt(6)
local uniqueID = net.ReadString()
local id = net.ReadUInt(32)
local owner = net.ReadUInt(32)
local data = net.ReadTable()
local character = owner != 0 and ix.char.loaded[owner] or LocalPlayer():GetCharacter()
if (character) then
local inventory = ix.item.inventories[invID]
if (inventory) then
local item = (uniqueID != "" and id != 0) and ix.item.New(uniqueID, id) or nil
item.invID = invID
item.data = {}
if (data) then
item.data = data
end
inventory.slots[x] = inventory.slots[x] or {}
inventory.slots[x][y] = item
invID = invID == LocalPlayer():GetCharacter():GetInventory():GetID() and 1 or invID
local panel = ix.gui["inv" .. invID]
if (IsValid(panel)) then
local icon = panel:AddIcon(item, item:GetModel() or "models/props_junk/popcan01a.mdl",
x, y, item.width, item.height, item:GetSkin(), item:GetModelBodygroups(), item.color, item.material, item.rotate, item.OnInventoryDraw)
if (IsValid(icon)) then
icon:SetHelixTooltip(function(tooltip)
ix.hud.PopulateItemTooltip(tooltip, item)
end)
icon.itemID = item.id
panel.panels[item.id] = icon
end
end
end
end
end)
net.Receive("ixInventoryMove", function()
local invID = net.ReadUInt(32)
local inventory = ix.item.inventories[invID]
if (!inventory) then
return
end
local itemID = net.ReadUInt(32)
local oldX = net.ReadUInt(6)
local oldY = net.ReadUInt(6)
local x = net.ReadUInt(6)
local y = net.ReadUInt(6)
invID = invID == LocalPlayer():GetCharacter():GetInventory():GetID() and 1 or invID
local item = ix.item.instances[itemID]
local panel = ix.gui["inv" .. invID]
-- update inventory UI if it's open
if (IsValid(panel)) then
local icon = panel.panels[itemID]
if (IsValid(icon)) then
icon:Move(x, y, panel, true)
end
end
-- update inventory slots
if (item) then
inventory.slots[oldX][oldY] = nil
inventory.slots[x] = inventory.slots[x] or {}
inventory.slots[x][y] = item
end
end)
net.Receive("ixInventoryRemove", function()
local id = net.ReadUInt(32)
local invID = net.ReadUInt(32)
local inventory = ix.item.inventories[invID]
if (!inventory) then
return
end
inventory:Remove(id)
invID = invID == LocalPlayer():GetCharacter():GetInventory():GetID() and 1 or invID
local panel = ix.gui["inv" .. invID]
if (IsValid(panel)) then
local icon = panel.panels[id]
if (IsValid(icon)) then
for _, v in ipairs(icon.slots or {}) do
if (v.item == icon) then
v.item = nil
end
end
icon:Remove()
end
end
local item = ix.item.instances[id]
if (!item) then
return
end
-- we need to close any bag windows that are open because of this item
if (item.isBag) then
local itemInv = item:GetInventory()
if (itemInv) then
local frame = ix.gui["inv" .. itemInv:GetID()]
if (IsValid(frame)) then
frame:Remove()
end
end
end
end)
else
util.AddNetworkString("ixInventorySync")
util.AddNetworkString("ixInventorySet")
util.AddNetworkString("ixInventoryMove")
util.AddNetworkString("ixInventoryRemove")
util.AddNetworkString("ixInventoryData")
util.AddNetworkString("ixInventoryAction")
function ix.item.LoadItemByID(itemIndex, recipientFilter, callback)
local query = mysql:Select("ix_items")
query:Select("item_id")
query:Select("unique_id")
query:Select("data")
query:Select("character_id")
query:Select("player_id")
query:WhereIn("item_id", itemIndex)
query:Callback(function(result)
if (istable(result)) then
for _, v in ipairs(result) do
local itemID = tonumber(v.item_id)
local data = util.JSONToTable(v.data or "[]")
local uniqueID = v.unique_id
local itemTable = ix.item.list[uniqueID]
local characterID = tonumber(v.character_id)
local playerID = tostring(v.player_id)
if (itemTable and itemID) then
local item = ix.item.New(uniqueID, itemID)
item.data = data or {}
item.invID = 0
item.characterID = characterID
item.playerID = (playerID == "" or playerID == "NULL") and nil or playerID
if (callback) then
callback(item)
end
end
end
end
end)
query:Execute()
end
function ix.item.PerformInventoryAction(client, action, item, invID, data)
local character = client:GetCharacter()
if (!character) then
return
end
local inventory = ix.item.inventories[invID or 0]
if (hook.Run("CanPlayerInteractItem", client, action, item, data) == false) then
return
end
if (!inventory:OnCheckAccess(client)) then
return
end
if (isentity(item)) then
if (IsValid(item)) then
local entity = item
local itemID = item.ixItemID
item = ix.item.instances[itemID]
if (!item) then
return
end
item.entity = entity
item.player = client
else
return
end
elseif (isnumber(item)) then
item = ix.item.instances[item]
if (!item) then
return
end
item.player = client
end
if (item.entity) then
if (item.entity:GetPos():Distance(client:GetPos()) > 96) then
return
end
elseif (!inventory:GetItemByID(item.id)) then
return
end
if (!item.bAllowMultiCharacterInteraction and IsValid(client) and client:GetCharacter()) then
local itemPlayerID = item:GetPlayerID()
local itemCharacterID = item:GetCharacterID()
local playerID = client:SteamID64()
local characterID = client:GetCharacter():GetID()
if (itemPlayerID and itemCharacterID and itemPlayerID == playerID and itemCharacterID != characterID) then
client:NotifyLocalized("itemOwned")
item.player = nil
item.entity = nil
return
end
end
local callback = item.functions[action]
if (callback) then
if (callback.OnCanRun and callback.OnCanRun(item, data) == false) then
item.entity = nil
item.player = nil
return
end
hook.Run("PlayerInteractItem", client, action, item)
local entity = item.entity
local result
if (item.hooks[action]) then
result = item.hooks[action](item, data)
end
if (result == nil) then
result = callback.OnRun(item, data)
end
if (item.postHooks[action]) then
-- Posthooks shouldn't override the result from OnRun
item.postHooks[action](item, result, data)
end
if (result != false) then
if (IsValid(entity)) then
entity.ixIsSafe = true
entity:Remove()
else
item:Remove()
end
end
item.entity = nil
item.player = nil
return result != false
end
end
local function NetworkInventoryMove(receiver, invID, itemID, oldX, oldY, x, y)
net.Start("ixInventoryMove")
net.WriteUInt(invID, 32)
net.WriteUInt(itemID, 32)
net.WriteUInt(oldX, 6)
net.WriteUInt(oldY, 6)
net.WriteUInt(x, 6)
net.WriteUInt(y, 6)
net.Send(receiver)
end
net.Receive("ixInventoryMove", function(length, client)
local oldX, oldY, x, y = net.ReadUInt(6), net.ReadUInt(6), net.ReadUInt(6), net.ReadUInt(6)
local invID, newInvID = net.ReadUInt(32), net.ReadUInt(32)
local character = client:GetCharacter()
if (character) then
local inventory = ix.item.inventories[invID]
if (!inventory or inventory == nil) then
inventory:Sync(client)
end
if ((!inventory.owner or (inventory.owner and inventory.owner == character:GetID())) or
inventory:OnCheckAccess(client)) then
local item = inventory:GetItemAt(oldX, oldY)
if (item) then
if (newInvID and invID != newInvID) then
local inventory2 = ix.item.inventories[newInvID]
if (inventory2) then
local bStatus, error = item:Transfer(newInvID, x, y, client)
if (!bStatus) then
NetworkInventoryMove(
client, item.invID, item:GetID(), item.gridX, item.gridY, item.gridX, item.gridY
)
client:NotifyLocalized(error or "unknownError")
end
end
return
end
if hook.Run("CanMoveItemSameInv", item, inventory, x, y) == false then return end
if (inventory:CanItemFit(x, y, item.width, item.height, item)) then
item.gridX = x
item.gridY = y
for x2 = 0, item.width - 1 do
for y2 = 0, item.height - 1 do
local previousX = inventory.slots[oldX + x2]
if (previousX) then
previousX[oldY + y2] = nil
end
end
end
for x2 = 0, item.width - 1 do
for y2 = 0, item.height - 1 do
inventory.slots[x + x2] = inventory.slots[x + x2] or {}
inventory.slots[x + x2][y + y2] = item
end
end
local receivers = inventory:GetReceivers()
if (istable(receivers)) then
local filtered = {}
for _, v in ipairs(receivers) do
if (v != client) then
filtered[#filtered + 1] = v
end
end
if (#filtered > 0) then
NetworkInventoryMove(
filtered, invID, item:GetID(), oldX, oldY, x, y
)
end
end
if (!inventory.noSave) then
local query = mysql:Update("ix_items")
query:Update("x", x)
query:Update("y", y)
query:Where("item_id", item.id)
query:Execute()
end
else
NetworkInventoryMove(
client, item.invID, item:GetID(), item.gridX, item.gridY, item.gridX, item.gridY
)
end
end
else
local item = inventory:GetItemAt(oldX, oldY)
if (item) then
NetworkInventoryMove(
client, item.invID, item.invID, item:GetID(), item.gridX, item.gridY, item.gridX, item.gridY
)
end
end
end
end)
net.Receive("ixInventoryAction", function(length, client)
ix.item.PerformInventoryAction(client, net.ReadString(), net.ReadUInt(32), net.ReadUInt(32), net.ReadTable())
end)
end
--- Instances and spawns a given item type.
-- @realm server
-- @string uniqueID Unique ID of the item
-- @vector position The position in which the item's entity will be spawned
-- @func[opt=nil] callback Function to call when the item entity is created
-- @angle[opt=angle_zero] angles The angles at which the item's entity will spawn
-- @tab[opt=nil] data Additional data for this item instance
function ix.item.Spawn(uniqueID, position, callback, angles, data)
ix.item.Instance(0, uniqueID, data or {}, 1, 1, function(item)
local entity = item:Spawn(position, angles)
if (callback) then
callback(item, entity)
end
end)
end
end
--- Inventory util functions for character
-- @classmod Character
--- Returns this character's associated `Inventory` object.
-- @function GetInventory
-- @realm shared
-- @treturn Inventory This character's inventory
ix.char.RegisterVar("Inventory", {
bNoNetworking = true,
bNoDisplay = true,
OnGet = function(character, index)
if (index and !isnumber(index)) then
return character.vars.inv or {}
end
return character.vars.inv and character.vars.inv[index or 1]
end,
alias = "Inv"
})

View File

@@ -0,0 +1,138 @@
--[[
| 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/
--]]
--[[--
Multi-language phrase support.
Helix has support for multiple languages, and you can easily leverage this system for use in your own schema, plugins, etc.
Languages will be loaded from the schema and any plugins in `languages/sh_languagename.lua`, where `languagename` is the id of a
language (`english` for English, `french` for French, etc). The structure of a language file is a table of phrases with the key
as its phrase ID and the value as its translation for that language. For example, in `plugins/area/sh_english.lua`:
LANGUAGE = {
area = "Area",
areas = "Areas",
areaEditMode = "Area Edit Mode",
-- etc.
}
The phrases defined in these language files can be used with the `L` global function:
print(L("areaEditMode"))
> Area Edit Mode
All phrases are formatted with `string.format`, so if you wish to add some info in a phrase you can use standard Lua string
formatting arguments:
print(L("areaDeleteConfirm", "Test"))
> Are you sure you want to delete the area "Test"?
Phrases are also usable on the server, but only when trying to localize a phrase based on a client's preferences. The server
does not have a set language. An example:
Entity(1):ChatPrint(L("areaEditMode"))
> -- "Area Edit Mode" will print in the player's chatbox
]]
-- @module ix.lang
ix.lang = ix.lang or {}
ix.lang.stored = ix.lang.stored or {}
ix.lang.names = ix.lang.names or {}
--- Loads language files from a directory.
-- @realm shared
-- @internal
-- @string directory Directory to load language files from
function ix.lang.LoadFromDir(directory)
for _, v in ipairs(file.Find(directory.."/sh_*.lua", "LUA")) do
local niceName = v:sub(4, -5):lower()
ix.util.Include(directory.."/"..v, "shared")
if (LANGUAGE) then
if (NAME) then
ix.lang.names[niceName] = NAME
NAME = nil
end
ix.lang.AddTable(niceName, LANGUAGE)
LANGUAGE = nil
end
end
end
--- Adds phrases to a language. This is used when you aren't adding entries through the files in the `languages/` folder. A
-- common use case is adding language phrases in a single-file plugin.
-- @realm shared
-- @string language The ID of the language
-- @tab data Language data to add to the given language
-- @usage ix.lang.AddTable("english", {
-- myPhrase = "My Phrase"
-- })
function ix.lang.AddTable(language, data)
language = tostring(language):lower()
ix.lang.stored[language] = table.Merge(ix.lang.stored[language] or {}, data)
end
if (SERVER) then
-- luacheck: globals L
function L(key, arg, ...)
local defaultLang = ix.config.language or "english"
local languages = ix.lang.stored
if (IsEntity(arg) and arg:IsPlayer()) then
local langKey = ix.option.Get(arg, "language", defaultLang)
local info = languages[langKey] or languages[defaultLang]
return string.format(info and info[key] or languages[defaultLang][key] or languages.english[key] or key, ...)
else
local info = languages[defaultLang]
return string.format(info and info[key] or languages[defaultLang][key] or languages.english[key] or key, arg, ...)
end
end
-- luacheck: globals L2
function L2(key, arg, ...)
local defaultLang = ix.config.language or "english"
local languages = ix.lang.stored
if (IsEntity(arg) and arg:IsPlayer()) then
local langKey = ix.option.Get(arg, "language", defaultLang)
local info = languages[langKey] or languages[defaultLang]
if (info and info[key]) then
return string.format(info[key], ...)
end
else
local info = ix.lang.stored[defaultLang]
if (info and info[key]) then
return string.format(info[key], arg, ...)
end
end
end
else
function L(key, ...)
local defaultLang = ix.config.language or "english"
local languages = ix.lang.stored
local langKey = ix.option.Get("language", defaultLang)
local info = languages[langKey] or languages[defaultLang]
return string.format(info and info[key] or languages[defaultLang][key] or languages.english[key] or key, ...)
end
function L2(key, ...)
local defaultLang = ix.config.language or "english"
local langKey = ix.option.Get("language", defaultLang)
local info = ix.lang.stored[langKey]
if (info and info[key]) then
return string.format(info[key], ...)
end
end
end

View File

@@ -0,0 +1,169 @@
--[[
| 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/
--]]
--[[--
Logging helper functions.
Predefined flags:
FLAG_NORMAL
FLAG_SUCCESS
FLAG_WARNING
FLAG_DANGER
FLAG_SERVER
FLAG_DEV
]]
-- @module ix.log
-- luacheck: globals FLAG_NORMAL FLAG_SUCCESS FLAG_WARNING FLAG_DANGER FLAG_SERVER FLAG_DEV
FLAG_NORMAL = 0
FLAG_SUCCESS = 1
FLAG_WARNING = 2
FLAG_DANGER = 3
FLAG_SERVER = 4
FLAG_DEV = 5
ix.log = ix.log or {}
ix.log.color = {
[FLAG_NORMAL] = Color(200, 200, 200),
[FLAG_SUCCESS] = Color(50, 200, 50),
[FLAG_WARNING] = Color(255, 255, 0),
[FLAG_DANGER] = Color(255, 50, 50),
[FLAG_SERVER] = Color(200, 200, 220),
[FLAG_DEV] = Color(200, 200, 220),
}
CAMI.RegisterPrivilege({
Name = "Helix - Logs",
MinAccess = "admin"
})
local consoleColor = Color(50, 200, 50)
if (SERVER) then
if (!ix.db) then
include("sv_database.lua")
end
util.AddNetworkString("ixLogStream")
function ix.log.LoadTables()
ix.log.CallHandler("Load")
end
ix.log.types = ix.log.types or {}
--- Adds a log type
-- @realm server
-- @string logType Log category
-- @string format The string format that log messages should use
-- @number flag Log level
function ix.log.AddType(logType, format, flag)
ix.log.types[logType] = {format = format, flag = flag}
end
function ix.log.Parse(client, logType, ...)
local info = ix.log.types[logType]
if (!info) then
ErrorNoHalt("attempted to add entry to non-existent log type \"" .. tostring(logType) .. "\"")
return
end
local text = info and info.format
if (text) then
if (isfunction(text)) then
text = text(client, ...)
end
else
text = -1
end
return text, info.flag
end
function ix.log.AddRaw(logString, bNoSave)
CAMI.GetPlayersWithAccess("Helix - Logs", function(receivers)
ix.log.Send(receivers, logString)
end)
Msg("[LOG] ", logString .. "\n")
if (!bNoSave) then
ix.log.CallHandler("Write", nil, logString)
end
end
--- Add a log message
-- @realm server
-- @player client Player who instigated the log
-- @string logType Log category
-- @param ... Arguments to pass to the log
function ix.log.Add(client, logType, ...)
local logString, logFlag = ix.log.Parse(client, logType, ...)
if (logString == -1) then return end
CAMI.GetPlayersWithAccess("Helix - Logs", function(receivers)
ix.log.Send(receivers, logString, logFlag)
end)
Msg("[LOG] ", logString .. "\n")
ix.log.CallHandler("Write", client, logString, logFlag, logType, {...})
end
function ix.log.Send(client, logString, flag)
net.Start("ixLogStream")
net.WriteString(logString)
net.WriteUInt(flag or 0, 4)
net.Send(client)
end
ix.log.handlers = ix.log.handlers or {}
function ix.log.CallHandler(event, ...)
for _, v in pairs(ix.log.handlers) do
if (isfunction(v[event])) then
v[event](...)
end
end
end
function ix.log.RegisterHandler(name, data)
data.name = string.gsub(name, "%s", "")
name = name:lower()
data.uniqueID = name
ix.log.handlers[name] = data
end
do
local HANDLER = {}
function HANDLER.Load()
file.CreateDir("helix/logs")
end
function HANDLER.Write(client, message)
file.Append("helix/logs/" .. os.date("%x"):gsub("/", "-") .. ".txt", "[" .. os.date("%X") .. "]\t" .. message .. "\r\n")
end
ix.log.RegisterHandler("File", HANDLER)
end
else
net.Receive("ixLogStream", function(length)
local logString = net.ReadString()
local flag = net.ReadUInt(4)
if (isstring(logString) and isnumber(flag)) then
MsgC(consoleColor, "[SERVER] ", ix.log.color[flag], logString .. "\n")
end
end)
end

View File

@@ -0,0 +1,119 @@
--[[
| 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/
--]]
--[[--
Entity menu manipulation.
The `menu` library allows you to open up a context menu of arbitrary options whose callbacks will be ran when they are selected
from the panel that shows up for the player.
]]
-- @module ix.menu
--- You'll need to pass a table of options to `ix.menu.Open` to populate the menu. This table consists of strings as its keys
-- and functions as its values. These correspond to the text displayed in the menu and the callback to run, respectively.
--
-- Example usage:
-- ix.menu.Open({
-- Drink = function()
-- print("Drink option selected!")
-- end,
-- Take = function()
-- print("Take option selected!")
-- end
-- }, ents.GetByIndex(1))
-- This opens a menu with the options `"Drink"` and `"Take"` which will print a message when you click on either of the options.
-- @realm client
-- @table MenuOptionsStructure
ix.menu = ix.menu or {}
if (CLIENT) then
--- Opens up a context menu for the given entity.
-- @realm client
-- @tparam MenuOptionsStructure options Data describing what options to display
-- @entity[opt] entity Entity to send commands to
-- @treturn boolean Whether or not the menu opened successfully. It will fail when there is already a menu open.
function ix.menu.Open(options, entity)
if (IsValid(ix.menu.panel)) then
return false
end
local panel = vgui.Create("ixEntityMenu")
panel:SetEntity(entity)
panel:SetOptions(options)
return true
end
--- Checks whether or not an entity menu is currently open.
-- @realm client
-- @treturn boolean Whether or not an entity menu is open
function ix.menu.IsOpen()
return IsValid(ix.menu.panel)
end
--- Notifies the server of an option that was chosen for the given entity.
-- @realm client
-- @entity entity Entity to call option on
-- @string choice Option that was chosen
-- @param data Extra data to send to the entity
function ix.menu.NetworkChoice(entity, choice, data)
if (IsValid(entity)) then
net.Start("ixEntityMenuSelect")
net.WriteEntity(entity)
net.WriteString(choice)
net.WriteType(data)
net.SendToServer()
end
end
else
util.AddNetworkString("ixEntityMenuSelect")
net.Receive("ixEntityMenuSelect", function(length, client)
local entity = net.ReadEntity()
local option = net.ReadString()
local data = net.ReadType()
if (!IsValid(entity) or !isstring(option) or
hook.Run("CanPlayerInteractEntity", client, entity, option, data) == false or
entity:GetPos():Distance(client:GetPos()) > 96) then
return
end
hook.Run("PlayerInteractEntity", client, entity, option, data)
local callbackName = "OnSelect" .. option:gsub("%s", "")
if (entity[callbackName]) then
entity[callbackName](entity, client, data)
else
entity:OnOptionSelected(client, option, data)
end
end)
end
do
local PLAYER = FindMetaTable("Player")
if (CLIENT) then
function PLAYER:GetEntityMenu()
local options = {}
hook.Run("GetPlayerEntityMenu", self, options)
return options
end
else
function PLAYER:OnOptionSelected(client, option)
hook.Run("OnPlayerOptionSelected", self, client, option)
end
end
end

View File

@@ -0,0 +1,154 @@
--[[
| 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/
--]]
--- Notification helper functions
-- @module ix.notice
if (SERVER) then
util.AddNetworkString("ixNotify")
util.AddNetworkString("ixNotifyLocalized")
--- Sends a notification to a specified recipient.
-- @realm server
-- @string message Message to notify
-- @player[opt=nil] recipient Player to be notified
function ix.util.Notify(message, recipient)
net.Start("ixNotify")
net.WriteString(message)
if (recipient == nil) then
net.Broadcast()
else
net.Send(recipient)
end
end
--- Sends a translated notification to a specified recipient.
-- @realm server
-- @string message Message to notify
-- @player[opt=nil] recipient Player to be notified
-- @param ... Arguments to pass to the translated message
function ix.util.NotifyLocalized(message, recipient, ...)
net.Start("ixNotifyLocalized")
net.WriteString(message)
net.WriteTable({...})
if (recipient == nil) then
net.Broadcast()
else
net.Send(recipient)
end
end
do
--- Notification util functions for players
-- @classmod Player
local playerMeta = FindMetaTable("Player")
--- Displays a prominent notification in the top-right of this player's screen.
-- @realm shared
-- @string message Text to display in the notification
function playerMeta:Notify(message)
ix.util.Notify(message, self)
end
--- Displays a notification for this player with the given language phrase.
-- @realm shared
-- @string message ID of the phrase to display to the player
-- @param ... Arguments to pass to the phrase
-- @usage client:NotifyLocalized("mapRestarting", 10)
-- -- displays "The map will restart in 10 seconds!" if the player's language is set to English
-- @see ix.lang
function playerMeta:NotifyLocalized(message, ...)
ix.util.NotifyLocalized(message, self, ...)
end
--- Displays a notification for this player in the chatbox.
-- @realm shared
-- @string message Text to display in the notification
function playerMeta:ChatNotify(message)
ix.chat.Send(nil, "notice", message, false, {self})
end
--- Displays a notification for this player in the chatbox with the given language phrase.
-- @realm shared
-- @string message ID of the phrase to display to the player
-- @param ... Arguments to pass to the phrase
-- @see NotifyLocalized
function playerMeta:ChatNotifyLocalized(message, ...)
ix.chat.Send(nil, "notice", L(message, self, ...), false, {self})
end
end
else
-- Create a notification panel.
function ix.util.Notify(message)
if (ix.option.Get("chatNotices", false)) then
local messageLength = message:utf8len()
ix.chat.Send(LocalPlayer(), "notice", message, false, {
bError = message:utf8sub(messageLength, messageLength) == "!"
})
return
end
if (IsValid(ix.gui.notices)) then
ix.gui.notices:AddNotice(message)
end
MsgC(Color(0, 255, 255), message .. "\n")
end
-- Creates a translated notification.
function ix.util.NotifyLocalized(message, ...)
ix.util.Notify(L(message, ...))
end
-- shortcut notify functions
do
local playerMeta = FindMetaTable("Player")
function playerMeta:Notify(message)
if (self == LocalPlayer()) then
ix.util.Notify(message)
end
end
function playerMeta:NotifyLocalized(message, ...)
if (self == LocalPlayer()) then
ix.util.NotifyLocalized(message, ...)
end
end
function playerMeta:ChatNotify(message)
if (self == LocalPlayer()) then
ix.chat.Send(LocalPlayer(), "notice", message)
end
end
function playerMeta:ChatNotifyLocalized(message, ...)
if (self == LocalPlayer()) then
ix.chat.Send(LocalPlayer(), "notice", L(message, ...))
end
end
end
-- Receives a notification from the server.
net.Receive("ixNotify", function()
ix.util.Notify(net.ReadString())
end)
-- Receives a notification from the server.
net.Receive("ixNotifyLocalized", function()
ix.util.NotifyLocalized(net.ReadString(), unpack(net.ReadTable()))
end)
end

View File

@@ -0,0 +1,381 @@
--[[
| 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/
--]]
--[[--
Client-side configuration management.
The `option` library provides a cleaner way to manage any arbitrary data on the client without the hassle of managing CVars. It
is analagous to the `ix.config` library, but it only deals with data that needs to be stored on the client.
To get started, you'll need to define an option in a client realm so the framework can be aware of its existence. This can be
done in the `cl_init.lua` file of your schema, or in an `if (CLIENT) then` statement in the `sh_plugin.lua` file of your plugin:
ix.option.Add("headbob", ix.type.bool, true)
If you need to get the value of an option on the server, you'll need to specify `true` for the `bNetworked` argument in
`ix.option.Add`. *NOTE:* You also need to define your option in a *shared* realm, since the server now also needs to be aware
of its existence. This makes it so that the client will send that option's value to the server whenever it changes, which then
means that the server can now retrieve the value that the client has the option set to. For example, if you need to get what
language a client is using, you can simply do the following:
ix.option.Get(player.GetByID(1), "language", "english")
This will return the language of the player, or `"english"` if one isn't found. Note that `"language"` is a networked option
that is already defined in the framework, so it will always be available. All options will show up in the options menu on the
client, unless `hidden` returns `true` when using `ix.option.Add`.
Note that the labels for each option in the menu will use a language phrase to show the name. For example, if your option is
named `headbob`, then you'll need to define a language phrase called `optHeadbob` that will be used as the option title.
]]
-- @module ix.option
ix.option = ix.option or {}
ix.option.stored = ix.option.stored or {}
ix.option.categories = ix.option.categories or {}
--- Creates a client-side configuration option with the given information.
-- @realm shared
-- @string key Unique ID for this option
-- @ixtype optionType Type of this option
-- @param default Default value that this option will have - this can be nil if needed
-- @tparam OptionStructure data Additional settings for this option
-- @usage ix.option.Add("animationScale", ix.type.number, 1, {
-- category = "appearance",
-- min = 0.3,
-- max = 2,
-- decimals = 1
-- })
function ix.option.Add(key, optionType, default, data)
assert(isstring(key) and key:find("%S"), "expected a non-empty string for the key")
data = data or {}
local categories = ix.option.categories
local category = data.category or "misc"
local upperName = key:sub(1, 1):upper() .. key:sub(2)
categories[category] = categories[category] or {}
categories[category][key] = true
--- You can specify additional optional arguments for `ix.option.Add` by passing in a table of specific fields as the fourth
-- argument.
-- @table OptionStructure
-- @realm shared
-- @field[type=string,opt="opt" .. key] phrase The phrase to use when displaying in the UI. The default value is your option
-- key in UpperCamelCase, prefixed with `"opt"`. For example, if your key is `"exampleOption"`, the default phrase will be
-- `"optExampleOption"`.
-- @field[type=string,opt="optd" .. key] description The phrase to use in the tooltip when hovered in the UI. The default
-- value is your option key in UpperCamelCase, prefixed with `"optd"`. For example, if your key is `"exampleOption"`, the
-- default phrase will be `"optdExampleOption"`.
-- @field[type=string,opt="misc"] category The category that this option should reside in. This is purely for
-- aesthetic reasons when displaying the options in the options menu. When displayed in the UI, it will take the form of
-- `L("category name")`. This means that you must create a language phrase for the category name - otherwise it will only
-- show as the exact string you've specified. If no category is set, it will default to `"misc"`.
-- @field[type=number,opt=0] min The minimum allowed amount when setting this option. This field is not
-- applicable to any type other than `ix.type.number`.
-- @field[type=number,opt=10] max The maximum allowed amount when setting this option. This field is not
-- applicable to any type other than `ix.type.number`.
-- @field[type=number,opt=0] decimals How many decimals to constrain to when using a number type. This field is not
-- applicable to any type other than `ix.type.number`.
-- @field[type=boolean,opt=false] bNetworked Whether or not the server should be aware of this option for each client.
-- @field[type=function,opt] OnChanged The function to run when this option is changed - this includes whether it was set
-- by the player, or through code using `ix.option.Set`.
-- OnChanged = function(oldValue, value)
-- print("new value is", value)
-- end
-- @field[type=function,opt] hidden The function to check whether the option should be hidden from the options menu.
-- @field[type=function,opt] populate The function to run when the option needs to be added to the menu. This is a required
-- field for any array options. It should return a table of entries where the key is the value to set in `ix.option.Set`,
-- and the value is the display name for the entry. An example:
--
-- populate = function()
-- return {
-- ["english"] = "English",
-- ["french"] = "French",
-- ["spanish"] = "Spanish"
-- }
-- end
ix.option.stored[key] = {
key = key,
phrase = "opt" .. upperName,
description = "optd" .. upperName,
type = optionType,
default = default,
min = data.min or 0,
max = data.max or 10,
decimals = data.decimals or 0,
category = data.category or "misc",
bNetworked = data.bNetworked and true or false,
hidden = data.hidden or nil,
populate = data.populate or nil,
OnChanged = data.OnChanged or nil
}
end
--- Loads all saved options from disk.
-- @realm shared
-- @internal
function ix.option.Load()
ix.util.Include("helix/gamemode/config/sh_options.lua")
if (CLIENT) then
local options = ix.data.Get("options", nil, true, true)
if (options) then
for k, v in pairs(options) do
ix.option.client[k] = v
end
end
ix.option.Sync()
end
end
--- Returns all of the available options. Note that this does contain the actual values of the options, just their properties.
-- @realm shared
-- @treturn table Table of all options
-- @usage PrintTable(ix.option.GetAll())
-- > language:
-- > bNetworked = true
-- > default = english
-- > type = 512
-- -- etc.
function ix.option.GetAll()
return ix.option.stored
end
--- Returns all of the available options grouped by their categories. The returned table contains category tables, that contain
-- all the options in that category as an array (this is so you can sort them if you'd like).
-- @realm shared
-- @bool[opt=false] bRemoveHidden Remove entries that are marked as hidden
-- @treturn table Table of all options
-- @usage PrintTable(ix.option.GetAllByCategories())
-- > general:
-- > 1:
-- > key = language
-- > bNetworked = true
-- > default = english
-- > type = 512
-- -- etc.
function ix.option.GetAllByCategories(bRemoveHidden)
local result = {}
for k, v in pairs(ix.option.categories) do
for k2, _ in pairs(v) do
local option = ix.option.stored[k2]
if (bRemoveHidden and isfunction(option.hidden) and option.hidden()) then
continue
end
-- we create the category table here because it could contain all hidden options which makes the table empty
result[k] = result[k] or {}
result[k][#result[k] + 1] = option
end
end
return result
end
if (CLIENT) then
ix.option.client = ix.option.client or {}
--- Sets an option value for the local player.
-- This function will error when an invalid key is passed.
-- @realm client
-- @string key Unique ID of the option
-- @param value New value to assign to the option
-- @bool[opt=false] bNoSave Whether or not to avoid saving
-- @bool[opt=false] bNoNetwork Whether or not to avoid networking regardless of whether the option is networked or not
function ix.option.Set(key, value, bNoSave, bNoNetwork)
local option = assert(ix.option.stored[key], "invalid option key \"" .. tostring(key) .. "\"")
if (option.type == ix.type.number) then
value = math.Clamp(math.Round(value, option.decimals), option.min, option.max)
end
local oldValue = ix.option.client[key]
ix.option.client[key] = value
if (option.bNetworked and !bNoNetwork) then
net.Start("ixOptionSet")
net.WriteString(key)
net.WriteType(value)
net.SendToServer()
end
if (!bNoSave) then
ix.option.Save()
end
if (isfunction(option.OnChanged)) then
option.OnChanged(oldValue, value)
end
end
net.Receive("ixOptionSet", function()
local key = net.ReadString()
local value = net.ReadType()
local bNoSave = net.ReadBool()
ix.option.Set(key, value, bNoSave, true)
end)
--- Retrieves an option value for the local player. If it is not set, it'll return the default that you've specified.
-- @realm client
-- @string key Unique ID of the option
-- @param default Default value to return if the option is not set
-- @return[1] Value associated with the key
-- @return[2] The given default if the option is not set
function ix.option.Get(key, default)
local option = ix.option.stored[key]
if (option) then
local localValue = ix.option.client[key]
if (localValue != nil) then
return localValue
end
return option.default
end
return default
end
--- Saves all options to disk.
-- @realm client
-- @internal
function ix.option.Save()
ix.data.Set("options", ix.option.client, true, true)
end
--- Syncs all networked options to the server.
-- @realm client
function ix.option.Sync()
local options = {}
for k, v in pairs(ix.option.stored) do
if (v.bNetworked) then
options[#options + 1] = {k, ix.option.client[k]}
end
end
if (#options > 0) then
net.Start("ixOptionSync")
net.WriteUInt(#options, 8)
for _, v in ipairs(options) do
net.WriteString(v[1])
net.WriteType(v[2])
end
net.SendToServer()
end
end
else
util.AddNetworkString("ixOptionSet")
util.AddNetworkString("ixOptionSync")
ix.option.clients = ix.option.clients or {}
--- Retrieves an option value from the specified player. If it is not set, it'll return the default that you've specified.
-- This function will error when an invalid player is passed.
-- @realm server
-- @player client Player to retrieve option value from
-- @string key Unique ID of the option
-- @param default Default value to return if the option is not set
-- @return[1] Value associated with the key
-- @return[2] The given default if the option is not set
function ix.option.Get(client, key, default)
assert(IsValid(client) and client:IsPlayer(), "expected valid player for argument #1")
local option = ix.option.stored[key]
if (option) then
local clientOptions = ix.option.clients[client:SteamID64()]
if (clientOptions) then
local clientOption = clientOptions[key]
if (clientOption != nil) then
return clientOption
end
end
return option.default
end
return default
end
function ix.option.Set(client, key, value, bNoSave)
local steamID = client:SteamID64()
local option = ix.option.stored[key]
if (option) then
if (option.bNetworked) then
ix.option.clients[steamID] = ix.option.clients[steamID] or {}
ix.option.clients[steamID][key] = value
end
net.Start("ixOptionSet")
net.WriteString(key)
net.WriteType(value)
net.WriteBool(bNoSave)
net.Send(client)
else
ErrorNoHalt(string.format("Attempted to set option with invalid key '%s' for '%s'\n", key, tostring(client) .. client:SteamID()))
end
end
-- sent whenever a client's networked option has changed
net.Receive("ixOptionSet", function(length, client)
local key = net.ReadString()
local value = net.ReadType()
local steamID = client:SteamID64()
local option = ix.option.stored[key]
if (option) then
ix.option.clients[steamID] = ix.option.clients[steamID] or {}
ix.option.clients[steamID][key] = value
else
ErrorNoHalt(string.format(
"'%s' attempted to set option with invalid key '%s'\n", tostring(client) .. client:SteamID(), key
))
end
end)
-- sent on first load to sync all networked option values
net.Receive("ixOptionSync", function(length, client)
local indices = net.ReadUInt(8)
local data = {}
for _ = 1, indices do
data[net.ReadString()] = net.ReadType()
end
local steamID = client:SteamID64()
ix.option.clients[steamID] = ix.option.clients[steamID] or {}
for k, v in pairs(data) do
local option = ix.option.stored[k]
if (option) then
ix.option.clients[steamID][k] = v
else
return ErrorNoHalt(string.format(
"'%s' attempted to sync option with invalid key '%s'\n", tostring(client) .. client:SteamID(), k
))
end
end
end)
end

View File

@@ -0,0 +1,153 @@
--[[
| 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 playerMeta = FindMetaTable("Player")
-- ixData information for the player.
do
if (SERVER) then
function playerMeta:GetData(key, default)
if (key == true) then
return self.ixData
end
local data = self.ixData and self.ixData[key]
if (data == nil) then
return default
else
return data
end
end
else
function playerMeta:GetData(key, default)
local data = ix.localData and ix.localData[key]
if (data == nil) then
return default
else
return data
end
end
net.Receive("ixDataSync", function()
ix.localData = net.ReadTable()
ix.playTime = net.ReadUInt(32)
end)
net.Receive("ixData", function()
ix.localData = ix.localData or {}
ix.localData[net.ReadString()] = net.ReadType()
end)
end
end
-- Whitelist networking information here.
do
function playerMeta:HasWhitelist(faction)
local data = ix.faction.indices[faction]
if (data) then
if (data.isDefault) then
return true
end
local ixData = self:GetData("whitelists", {})
return ixData[Schema.folder] and ixData[Schema.folder][data.uniqueID] == true or false
end
return false
end
function playerMeta:GetItems()
local char = self:GetCharacter()
if (char) then
local inv = char:GetInventory()
if (inv) then
return inv:GetItems()
end
end
end
function playerMeta:GetClassData()
local char = self:GetCharacter()
if (char) then
local class = char:GetClass()
if (class) then
local classData = ix.class.list[class]
return classData
end
end
end
end
do
if (SERVER) then
util.AddNetworkString("PlayerModelChanged")
util.AddNetworkString("PlayerSelectWeapon")
local entityMeta = FindMetaTable("Entity")
entityMeta.ixSetModel = entityMeta.ixSetModel or entityMeta.SetModel
playerMeta.ixSelectWeapon = playerMeta.ixSelectWeapon or playerMeta.SelectWeapon
function entityMeta:SetModel(model)
local oldModel = self:GetModel()
if (self:IsPlayer()) then
hook.Run("PlayerModelChanged", self, model, oldModel)
net.Start("PlayerModelChanged")
net.WriteEntity(self)
net.WriteString(model)
net.WriteString(oldModel)
net.Broadcast()
end
return self:ixSetModel(model)
end
function playerMeta:SelectWeapon(className)
net.Start("PlayerSelectWeapon")
net.WriteEntity(self)
net.WriteString(className)
net.Broadcast()
return self:ixSelectWeapon(className)
end
else
net.Receive("PlayerModelChanged", function(length)
hook.Run("PlayerModelChanged", net.ReadEntity(), net.ReadString(), net.ReadString())
end)
net.Receive("PlayerSelectWeapon", function(length)
local client = net.ReadEntity()
local className = net.ReadString()
if (!IsValid(client)) then
hook.Run("PlayerWeaponChanged", client, NULL)
return
end
for _, v in ipairs(client:GetWeapons()) do
if (v:GetClass() == className) then
hook.Run("PlayerWeaponChanged", client, v)
break
end
end
end)
end
end

View File

@@ -0,0 +1,526 @@
--[[
| 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/
--]]
ix.plugin = ix.plugin or {}
ix.plugin.list = ix.plugin.list or {}
ix.plugin.unloaded = ix.plugin.unloaded or {}
ix.util.Include("helix/gamemode/core/meta/sh_tool.lua")
-- luacheck: globals HOOKS_CACHE
HOOKS_CACHE = {}
local function insertSorted(tbl, plugin, func, priority)
if (IX_RELOADED) then
-- Clean out the old function from the table
for i = 1, #tbl do
if (tbl[i][1] == plugin) then
table.remove(tbl, i)
break
end
end
end
-- Attempt to insert into an empty table or at the end first
if (#tbl == 0 or tbl[#tbl][3] >= priority) then
tbl[#tbl + 1] = {plugin, func, priority}
return
end
-- Find where to insert
for i = #tbl - 1, 1, -1 do
if (tbl[i][3] >= priority) then
table.insert(tbl, i + 1, {plugin, func, priority})
return
end
end
-- Insert at the front
table.insert(tbl, 1, {plugin, func, priority})
end
function ix.plugin.Load(uniqueID, path, isSingleFile, variable)
if (hook.Run("PluginShouldLoad", uniqueID) == false) then return end
variable = variable or "PLUGIN"
-- Plugins within plugins situation?
local oldPlugin = PLUGIN
local PLUGIN = {
folder = path,
plugin = oldPlugin,
uniqueID = uniqueID,
name = "Unknown",
description = "Description not available",
author = "Anonymous"
}
if (uniqueID == "schema") then
if (Schema) then
PLUGIN = Schema
end
variable = "Schema"
PLUGIN.folder = engine.ActiveGamemode()
elseif (ix.plugin.list[uniqueID]) then
PLUGIN = ix.plugin.list[uniqueID]
end
_G[variable] = PLUGIN
PLUGIN.loading = true
if (!isSingleFile) then
ix.lang.LoadFromDir(path.."/languages")
ix.util.IncludeDir(path.."/libs", true)
ix.attributes.LoadFromDir(path.."/attributes")
ix.faction.LoadFromDir(path.."/factions")
ix.class.LoadFromDir(path.."/classes")
ix.item.LoadFromDir(path.."/items")
ix.plugin.LoadFromDir(path.."/plugins")
ix.util.IncludeDir(path.."/derma", true)
ix.plugin.LoadEntities(path.."/entities")
hook.Run("DoPluginIncludes", path, PLUGIN)
end
ix.util.Include(isSingleFile and path or path.."/sh_"..variable:lower()..".lua", "shared")
PLUGIN.loading = false
local uniqueID2 = uniqueID
if (uniqueID2 == "schema") then
uniqueID2 = PLUGIN.name
end
function PLUGIN:SetData(value, global, ignoreMap)
ix.data.Set(uniqueID2, value, global, ignoreMap)
end
function PLUGIN:GetData(default, global, ignoreMap, refresh)
return ix.data.Get(uniqueID2, default, global, ignoreMap, refresh) or {}
end
hook.Run("PluginLoaded", uniqueID, PLUGIN)
if (uniqueID != "schema") then
PLUGIN.name = PLUGIN.name or "Unknown"
PLUGIN.description = PLUGIN.description or "No description available."
PLUGIN.hookCallPriority = PLUGIN.hookCallPriority or 1000
for k, v in pairs(PLUGIN) do
if (isfunction(v)) then
HOOKS_CACHE[k] = HOOKS_CACHE[k] or {}
insertSorted(HOOKS_CACHE[k], PLUGIN, v,
(PLUGIN.GetHookCallPriority and PLUGIN:GetHookCallPriority(k))
or PLUGIN.hookCallPriority)
end
end
ix.plugin.list[uniqueID] = PLUGIN
_G[variable] = nil
end
if (PLUGIN.OnLoaded) then
PLUGIN:OnLoaded()
end
end
function ix.plugin.GetHook(pluginName, hookName)
local h = HOOKS_CACHE[hookName]
if (h) then
local p = ix.plugin.list[pluginName]
if (p) then
for _, v in ipairs(h) do
if (v[1] == p) then
return v[2]
end
end
end
end
return
end
function ix.plugin.LoadEntities(path)
local bLoadedTools
local files, folders
local function IncludeFiles(path2, bClientOnly)
if (SERVER and !bClientOnly) then
if (file.Exists(path2.."init.lua", "LUA")) then
ix.util.Include(path2.."init.lua", "server")
elseif (file.Exists(path2.."shared.lua", "LUA")) then
ix.util.Include(path2.."shared.lua")
end
if (file.Exists(path2.."cl_init.lua", "LUA")) then
ix.util.Include(path2.."cl_init.lua", "client")
end
elseif (file.Exists(path2.."cl_init.lua", "LUA")) then
ix.util.Include(path2.."cl_init.lua", "client")
elseif (file.Exists(path2.."shared.lua", "LUA")) then
ix.util.Include(path2.."shared.lua")
end
end
local function HandleEntityInclusion(folder, variable, register, default, clientOnly, create, complete)
files, folders = file.Find(path.."/"..folder.."/*", "LUA")
default = default or {}
for _, v in ipairs(folders) do
local path2 = path.."/"..folder.."/"..v.."/"
v = ix.util.StripRealmPrefix(v)
_G[variable] = table.Copy(default)
if (!isfunction(create)) then
_G[variable].ClassName = v
else
create(v)
end
IncludeFiles(path2, clientOnly)
if (clientOnly) then
if (CLIENT) then
register(_G[variable], v)
end
else
register(_G[variable], v)
end
if (isfunction(complete)) then
complete(_G[variable])
end
_G[variable] = nil
end
for _, v in ipairs(files) do
local niceName = ix.util.StripRealmPrefix(string.StripExtension(v))
_G[variable] = table.Copy(default)
if (!isfunction(create)) then
_G[variable].ClassName = niceName
else
create(niceName)
end
ix.util.Include(path.."/"..folder.."/"..v, clientOnly and "client" or "shared")
if (clientOnly) then
if (CLIENT) then
register(_G[variable], niceName)
end
else
register(_G[variable], niceName)
end
if (isfunction(complete)) then
complete(_G[variable])
end
_G[variable] = nil
end
end
local function RegisterTool(tool, className)
local gmodTool = weapons.GetStored("gmod_tool")
if (className:sub(1, 3) == "sh_") then
className = className:sub(4)
end
if (gmodTool) then
gmodTool.Tool[className] = tool
else
-- this should never happen
ErrorNoHalt(string.format("attempted to register tool '%s' with invalid gmod_tool weapon", className))
end
bLoadedTools = true
end
-- Include entities.
HandleEntityInclusion("entities", "ENT", scripted_ents.Register, {
Type = "anim",
Base = "base_gmodentity",
Spawnable = true
}, false, nil, function(ent)
if (SERVER and ent.Holdable == true) then
ix.allowedHoldableClasses[ent.ClassName] = true
end
end)
-- Include weapons.
HandleEntityInclusion("weapons", "SWEP", weapons.Register, {
Primary = {},
Secondary = {},
Base = "weapon_base"
})
HandleEntityInclusion("tools", "TOOL", RegisterTool, {}, false, function(className)
if (className:sub(1, 3) == "sh_") then
className = className:sub(4)
end
TOOL = ix.meta.tool:Create()
TOOL.Mode = className
TOOL:CreateConVars()
end)
-- Include effects.
HandleEntityInclusion("effects", "EFFECT", effects and effects.Register, nil, true)
-- only reload spawn menu if any new tools were registered
if (CLIENT and bLoadedTools) then
RunConsoleCommand("spawnmenu_reload")
end
end
function ix.plugin.Initialize()
ix.plugin.unloaded = ix.data.Get("unloaded", {}, true, true)
ix.plugin.LoadFromDir("helix/plugins")
ix.plugin.Load("schema", engine.ActiveGamemode().."/schema")
hook.Run("InitializedSchema")
ix.plugin.LoadFromDir(engine.ActiveGamemode().."/plugins")
hook.Run("InitializedPlugins")
end
function ix.plugin.Get(identifier)
return ix.plugin.list[identifier]
end
function ix.plugin.LoadFromDir(directory)
local files, folders = file.Find(directory.."/*", "LUA")
for _, v in ipairs(folders) do
ix.plugin.Load(v, directory.."/"..v)
end
for _, v in ipairs(files) do
ix.plugin.Load(string.StripExtension(v), directory.."/"..v, true)
end
end
function ix.plugin.SetUnloaded(uniqueID, state, bNoSave)
local plugin = ix.plugin.list[uniqueID]
if (state) then
if (plugin and plugin.OnUnload) then
plugin:OnUnload()
end
ix.plugin.unloaded[uniqueID] = true
elseif (ix.plugin.unloaded[uniqueID]) then
ix.plugin.unloaded[uniqueID] = nil
else
return false
end
if (SERVER and !bNoSave) then
local status
if (state) then
status = true
end
local unloaded = ix.data.Get("unloaded", {}, true, true)
unloaded[uniqueID] = status
ix.data.Set("unloaded", unloaded, true, true)
end
if (state) then
hook.Run("PluginUnloaded", uniqueID)
end
return true
end
if (SERVER) then
--- Runs the `LoadData` and `PostLoadData` hooks for the gamemode, schema, and plugins. Any plugins that error during the
-- hook will have their `SaveData` and `PostLoadData` hooks removed to prevent them from saving junk data.
-- @internal
-- @realm server
function ix.plugin.RunLoadData()
local errors = hook.SafeRun("LoadData")
-- remove the SaveData and PostLoadData hooks for any plugins that error during LoadData since they would probably be
-- saving bad data. this doesn't prevent plugins from saving data via other means, but there's only so much we can do
for _, v in pairs(errors or {}) do
if (v.plugin) then
local plugin = ix.plugin.Get(v.plugin)
if (plugin) then
local saveDataHooks = HOOKS_CACHE["SaveData"] or {}
saveDataHooks[plugin] = nil
local postLoadDataHooks = HOOKS_CACHE["PostLoadData"] or {}
postLoadDataHooks[plugin] = nil
end
end
end
hook.Run("PostLoadData")
end
end
do
-- luacheck: globals hook
hook.ixCall = hook.ixCall or hook.Call
local hookCall
if (CLIENT) then
hookCall = function(name, gm, ...)
local cache = HOOKS_CACHE[name]
if (cache) then
for i = 1, #cache do
local a, b, c, d, e, f = cache[i][2](cache[i][1], ...)
if (a != nil) then
return a, b, c, d, e, f
end
end
end
if (Schema and Schema[name]) then
local a, b, c, d, e, f = Schema[name](Schema, ...)
if (a != nil) then
return a, b, c, d, e, f
end
end
--luacheck: ignore global SwiftAC__N
if (SwiftAC__N) then
SwiftAC__N.hook.Call(name, gm, ...)
else
return hook.ixCall(name, gm, ...)
end
end
--SwiftAC compatibility
hook.Run = function(name, ...)
return hookCall(name, gmod and gmod.GetGamemode(), ...)
end
else
hookCall = function(name, gm, ...)
local cache = HOOKS_CACHE[name]
if (cache) then
for i = 1, #cache do
local a, b, c, d, e, f = cache[i][2](cache[i][1], ...)
if (a != nil) then
return a, b, c, d, e, f
end
end
end
if (Schema and Schema[name]) then
local a, b, c, d, e, f = Schema[name](Schema, ...)
if (a != nil) then
return a, b, c, d, e, f
end
end
return hook.ixCall(name, gm, ...)
end
end
hook.Call = hookCall
--- Runs the given hook in a protected call so that the calling function will continue executing even if any errors occur
-- while running the hook. This function is much more expensive to call than `hook.Run`, so you should avoid using it unless
-- you absolutely need to avoid errors from stopping the execution of your function.
-- @internal
-- @realm shared
-- @string name Name of the hook to run
-- @param ... Arguments to pass to the hook functions
-- @treturn[1] table Table of error data if an error occurred while running
-- @treturn[1] ... Any arguments returned by the hook functions
-- @usage local errors, bCanSpray = hook.SafeRun("PlayerSpray", Entity(1))
-- if (!errors) then
-- -- do stuff with bCanSpray
-- else
-- PrintTable(errors)
-- end
function hook.SafeRun(name, ...)
local errors = {}
local gm = gmod and gmod.GetGamemode() or nil
local cache = HOOKS_CACHE[name]
if (cache) then
for i = 1, #cache do
local bSuccess, a, b, c, d, e, f = pcall(cache[i][2], cache[i][1], ...)
if (bSuccess) then
if (a != nil) then
return errors, a, b, c, d, e, f
end
else
local plugin = cache[i][1]
ErrorNoHalt(string.format("[Helix] hook.SafeRun error for plugin hook \"%s:%s\":\n\t%s\n%s\n",
tostring(plugin and plugin.uniqueID or nil), tostring(name), tostring(a), debug.traceback()))
errors[#errors + 1] = {
name = name,
plugin = plugin and plugin.uniqueID or nil,
errorMessage = tostring(a)
}
end
end
end
if (Schema and Schema[name]) then
local bSuccess, a, b, c, d, e, f = pcall(Schema[name], Schema, ...)
if (bSuccess) then
if (a != nil) then
return errors, a, b, c, d, e, f
end
else
ErrorNoHalt(string.format("[Helix] hook.SafeRun error for schema hook \"%s\":\n\t%s\n%s\n",
tostring(name), tostring(a), debug.traceback()))
errors[#errors + 1] = {
name = name,
schema = Schema.name,
errorMessage = tostring(a)
}
end
end
local bSuccess, a, b, c, d, e, f = pcall(hook.ixCall, name, gm, ...)
if (bSuccess) then
return errors, a, b, c, d, e, f
else
ErrorNoHalt(string.format("[Helix] hook.SafeRun error for gamemode hook \"%s\":\n\t%s\n%s\n",
tostring(name), tostring(a), debug.traceback()))
errors[#errors + 1] = {
name = name,
gamemode = "gamemode",
errorMessage = tostring(a)
}
return errors
end
end
end

View File

@@ -0,0 +1,491 @@
--[[
| 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/
--]]
--[[--
Player manipulation of inventories.
This library provides an easy way for players to manipulate other inventories. The only functions that you should need are
`ix.storage.Open` and `ix.storage.Close`. When opening an inventory as a storage item, it will display both the given inventory
and the player's inventory in the player's UI, which allows them to drag items to and from the given inventory.
Example usage:
ix.storage.Open(client, inventory, {
name = "Filing Cabinet",
entity = ents.GetByIndex(3),
bMultipleUsers = true,
searchText = "Rummaging...",
searchTime = 4
})
]]
-- @module ix.storage
--- There are some parameters you can customize when opening an inventory as a storage object with `ix.storage.Open`.
-- @realm server
-- @table StorageInfoStructure
-- @field[type=entity] entity Entity to "attach" the inventory to. This is used to provide a location for the inventory for
-- things like making sure the player doesn't move too far away from the inventory, etc. This can also be a `player` object.
-- @field[type=number,opt=inventory id] id The ID of the nventory. This defaults to the inventory passed into `ix.Storage.Open`.
-- @field[type=string,opt="Storage"] name Title to display in the UI when the inventory is open.
-- @field[type=boolean,opt=false] bMultipleUsers Whether or not multiple players are allowed to view this inventory at the
-- same time.
-- @field[type=number,opt=0] searchTime How long the player has to wait before the inventory is opened.
-- @field[type=string,opt="@storageSearching"] text Text to display to the user while opening the inventory. If prefixed with
-- `"@"`, it will display a language phrase.
-- @field[type=function,opt] OnPlayerClose Called when a player who was accessing the inventory has closed it. The
-- argument passed to the callback is the player who closed it.
-- @field[type=table,opt={}] data Table of arbitrary data to send to the client when the inventory has been opened.
ix.storage = ix.storage or {}
if (SERVER) then
util.AddNetworkString("ixStorageOpen")
util.AddNetworkString("ixStorageClose")
util.AddNetworkString("ixStorageExpired")
util.AddNetworkString("ixStorageMoneyTake")
util.AddNetworkString("ixStorageMoneyGive")
util.AddNetworkString("ixStorageMoneyUpdate")
--- Returns whether or not the given inventory has a storage context and is being looked at by other players.
-- @realm server
-- @inventory inventory Inventory to check
-- @treturn bool Whether or not `inventory` is in use
function ix.storage.InUse(inventory)
if (inventory.storageInfo) then
for _, v in pairs(inventory:GetReceivers()) do
if (IsValid(v) and v:IsPlayer() and v != inventory.storageInfo.entity) then
return true
end
end
end
return false
end
--- Returns whether or not an inventory is in use by a specific player.
-- @realm server
-- @inventory inventory Inventory to check
-- @player client Player to check
-- @treturn bool Whether or not the player is using the given `inventory`
function ix.storage.InUseBy(inventory, client)
if (inventory.storageInfo) then
for _, v in pairs(inventory:GetReceivers()) do
if (IsValid(v) and v:IsPlayer() and v == client) then
return true
end
end
end
return false
end
--- Creates a storage context on the given inventory.
-- @realm server
-- @internal
-- @inventory inventory Inventory to create a storage context for
-- @tab info Information to store on the context
function ix.storage.CreateContext(inventory, info)
info = info or {}
info.id = inventory:GetID()
info.name = info.name or "Storage"
info.entity = assert(IsValid(info.entity), "expected valid entity in info table") and info.entity
info.bMultipleUsers = info.bMultipleUsers == nil and false or info.bMultipleUsers
info.searchTime = tonumber(info.searchTime) or 0
info.searchText = info.searchText or "@storageSearching"
info.data = info.data or {}
inventory.storageInfo = info
-- remove context from any bags this inventory might have
for _, v in pairs(inventory:GetItems()) do
if (v.isBag and v:GetInventory()) then
ix.storage.CreateContext(v:GetInventory(), table.Copy(info))
end
end
end
--- Removes a storage context from an inventory if it exists.
-- @realm server
-- @internal
-- @inventory inventory Inventory to remove a storage context from
function ix.storage.RemoveContext(inventory)
inventory.storageInfo = nil
-- remove context from any bags this inventory might have
for _, v in pairs(inventory:GetItems()) do
if (v.isBag and v:GetInventory()) then
ix.storage.RemoveContext(v:GetInventory())
end
end
end
--- Synchronizes an inventory with a storage context to the given client.
-- @realm server
-- @internal
-- @player client Player to sync storage for
-- @inventory inventory Inventory to sync storage for
function ix.storage.Sync(client, inventory)
local info = inventory.storageInfo
-- we'll retrieve the money value as we're syncing because it may have changed while
-- we were waiting for the timer to finish
if (info.entity.GetMoney) then
info.data.money = info.entity:GetMoney()
elseif (info.entity:IsPlayer() and info.entity:GetCharacter()) then
info.data.money = info.entity:GetCharacter():GetMoney()
end
-- bags are automatically sync'd when the owning inventory is sync'd
inventory:Sync(client)
net.Start("ixStorageOpen")
net.WriteUInt(info.id, 32)
net.WriteEntity(info.entity)
net.WriteString(info.name)
net.WriteTable(info.data)
net.Send(client)
end
--- Adds a receiver to a given inventory with a storage context.
-- @realm server
-- @internal
-- @player client Player to sync storage for
-- @inventory inventory Inventory to sync storage for
-- @bool bDontSync Whether or not to skip syncing the storage to the client. If this is `true`, the storage panel will not
-- show up for the player
function ix.storage.AddReceiver(client, inventory, bDontSync)
local info = inventory.storageInfo
if (info) then
inventory:AddReceiver(client)
client.ixOpenStorage = inventory
-- update receivers for any bags this inventory might have
for _, v in pairs(inventory:GetItems()) do
if (v.isBag and v:GetInventory()) then
v:GetInventory():AddReceiver(client)
end
end
if (isfunction(info.OnPlayerOpen)) then
info.OnPlayerOpen(client)
end
if (!bDontSync) then
ix.storage.Sync(client, inventory)
end
return true
end
return false
end
--- Removes a storage receiver and removes the context if there are no more receivers.
-- @realm server
-- @internal
-- @player client Player to remove from receivers
-- @inventory inventory Inventory with storage context to remove receiver from
-- @bool bDontRemove Whether or not to skip removing the storage context if there are no more receivers
function ix.storage.RemoveReceiver(client, inventory, bDontRemove)
local info = inventory.storageInfo
if (info) then
inventory:RemoveReceiver(client)
-- update receivers for any bags this inventory might have
for _, v in pairs(inventory:GetItems()) do
if (v.isBag and v:GetInventory()) then
v:GetInventory():RemoveReceiver(client)
end
end
if (isfunction(info.OnPlayerClose)) then
info.OnPlayerClose(client)
end
if (!bDontRemove and !ix.storage.InUse(inventory)) then
ix.storage.RemoveContext(inventory)
end
client.ixOpenStorage = nil
hook.Run("OnStorageReceiverRemoved", client, inventory)
return true
end
return false
end
--- Makes a player open an inventory that they can interact with. This can be called multiple times on the same inventory,
-- if the info passed allows for multiple users.
-- @realm server
-- @player client Player to open the inventory for
-- @inventory inventory Inventory to open
-- @tab info `StorageInfoStructure` describing the storage properties
function ix.storage.Open(client, inventory, info)
assert(IsValid(client) and client:IsPlayer(), "expected valid player")
assert(type(inventory) == "table" and inventory:IsInstanceOf(ix.meta.inventory), "expected valid inventory")
-- create storage context if one isn't already created
if (!inventory.storageInfo) then
info = info or {}
ix.storage.CreateContext(inventory, info)
end
local storageInfo = inventory.storageInfo
-- add the client to the list of receivers if we're allowed to have multiple users
-- or if nobody else is occupying this inventory, otherwise nag the player
if (storageInfo.bMultipleUsers or !ix.storage.InUse(inventory)) then
ix.storage.AddReceiver(client, inventory, true)
else
client:NotifyLocalized("storageInUse")
return
end
if (storageInfo.searchTime > 0) then
client:SetAction(storageInfo.searchText, storageInfo.searchTime)
client:DoStaredAction(storageInfo.entity, function()
if (IsValid(client) and IsValid(storageInfo.entity) and inventory.storageInfo) then
ix.storage.Sync(client, inventory)
end
end, storageInfo.searchTime, function()
if (IsValid(client)) then
ix.storage.RemoveReceiver(client, inventory)
client:SetAction()
end
end)
else
ix.storage.Sync(client, inventory)
end
end
--- Forcefully makes clients close this inventory if they have it open.
-- @realm server
-- @inventory inventory Inventory to close
function ix.storage.Close(inventory)
local receivers = inventory:GetReceivers()
if (#receivers > 0) then
net.Start("ixStorageExpired")
net.WriteUInt(inventory.storageInfo.id, 32)
net.Send(receivers)
end
ix.storage.RemoveContext(inventory)
end
net.Receive("ixStorageClose", function(length, client)
local inventory = client.ixOpenStorage
if (inventory) then
ix.storage.RemoveReceiver(client, inventory)
end
end)
net.Receive("ixStorageMoneyTake", function(length, client)
if (CurTime() < (client.ixStorageMoneyTimer or 0)) then
return
end
local character = client:GetCharacter()
if (!character) then
return
end
local storageID = net.ReadUInt(32)
local amount = net.ReadUInt(32)
local inventory = client.ixOpenStorage
if (!inventory or !inventory.storageInfo or storageID != inventory:GetID()) then
return
end
local entity = inventory.storageInfo.entity
if (hook.Run("CanTakeMoney", client, entity, amount) == false) then
client:NotifyLocalized("notAllowed")
return
end
local total
if (inventory.storageInfo.OnMoneyTake) then
amount, total = inventory.storageInfo.OnMoneyTake(client, inventory, amount)
if (!amount) then
return
end
else
if (!IsValid(entity) or
(!entity:IsPlayer() and (!isfunction(entity.GetMoney) or !isfunction(entity.SetMoney))) or
(entity:IsPlayer() and !entity:GetCharacter())) then
return
end
entity = entity:IsPlayer() and entity:GetCharacter() or entity
amount = math.Clamp(math.Round(tonumber(amount) or 0), 0, entity:GetMoney())
if (amount == 0) then
return
end
character:SetMoney(character:GetMoney() + amount)
total = entity:GetMoney() - amount
entity:SetMoney(total)
ix.log.Add(client, "storageMoneyTake", entity, amount, total)
end
net.Start("ixStorageMoneyUpdate")
net.WriteUInt(storageID, 32)
net.WriteUInt(total, 32)
net.Send(inventory:GetReceivers())
client.ixStorageMoneyTimer = CurTime() + 0.5
end)
net.Receive("ixStorageMoneyGive", function(length, client)
if (CurTime() < (client.ixStorageMoneyTimer or 0)) then
return
end
local character = client:GetCharacter()
if (!character) then
return
end
local storageID = net.ReadUInt(32)
local amount = net.ReadUInt(32)
local inventory = client.ixOpenStorage
if (!inventory or !inventory.storageInfo or storageID != inventory:GetID()) then
return
end
local entity = inventory.storageInfo.entity
if (hook.Run("CanGiveMoney", client, entity, amount) == false) then
client:NotifyLocalized("notAllowed")
return
end
local total
if (inventory.storageInfo.OnMoneyGive) then
amount, total = inventory.storageInfo.OnMoneyGive(client, inventory, amount)
if (!amount) then
return
end
else
if (!IsValid(entity) or
(!entity:IsPlayer() and (!isfunction(entity.GetMoney) or !isfunction(entity.SetMoney))) or
(entity:IsPlayer() and !entity:GetCharacter())) then
return
end
entity = entity:IsPlayer() and entity:GetCharacter() or entity
amount = math.Clamp(math.Round(tonumber(amount) or 0), 0, character:GetMoney())
if (amount == 0) then
return
end
character:SetMoney(character:GetMoney() - amount)
total = entity:GetMoney() + amount
entity:SetMoney(total)
ix.log.Add(client, "storageMoneyGive", entity, amount, total)
end
net.Start("ixStorageMoneyUpdate")
net.WriteUInt(storageID, 32)
net.WriteUInt(total, 32)
net.Send(inventory:GetReceivers())
client.ixStorageMoneyTimer = CurTime() + 0.5
end)
else
net.Receive("ixStorageOpen", function()
if (IsValid(ix.gui.menu)) then
net.Start("ixStorageClose")
net.SendToServer()
return
end
local id = net.ReadUInt(32)
local entity = net.ReadEntity()
local name = net.ReadString()
local data = net.ReadTable()
local inventory = ix.item.inventories[id]
if (IsValid(entity) and inventory and inventory.slots) then
local char = LocalPlayer().GetCharacter and LocalPlayer():GetCharacter()
local localInventory = char and char:GetInventory()
local equipInvID = char and char.GetEquipInventory and char:GetEquipInventory()
local equipInv = equipInvID and ix.item.inventories[equipInvID]
local panel = vgui.Create("ixStorageView")
panel:CreateContents(inventory, entity)
if (localInventory) then
panel:SetLocalInventory(localInventory)
end
if (equipInv) then
panel:SetEquipInv(equipInv)
end
panel:SetStorageID(id)
panel:SetStorageTitle(name)
panel:SetStorageInventory(inventory)
if (data.money) then
if (localInventory) then
panel:SetLocalMoney(LocalPlayer():GetCharacter():GetMoney())
end
panel:SetStorageMoney(data.money)
end
end
end)
net.Receive("ixStorageExpired", function()
if (IsValid(ix.gui.openedStorage)) then
ix.gui.openedStorage:Remove()
end
local id = net.ReadUInt(32)
if (id != 0) then
ix.item.inventories[id] = nil
end
end)
net.Receive("ixStorageMoneyUpdate", function()
local storageID = net.ReadUInt(32)
local amount = net.ReadUInt(32)
local panel = ix.gui.openedStorage
if (!IsValid(panel) or panel:GetStorageID() != storageID) then
return
end
panel:SetStorageMoney(amount)
panel:SetLocalMoney(LocalPlayer():GetCharacter():GetMoney())
end)
end

View File

@@ -0,0 +1,204 @@
--[[
| 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/
--]]
ix.db = ix.db or {
schema = {},
schemaQueue = {},
type = {
-- TODO: more specific types, lengths, and defaults
-- i.e INT(11) UNSIGNED, SMALLINT(4), LONGTEXT, VARCHAR(350), NOT NULL, DEFAULT NULL, etc
[ix.type.string] = "VARCHAR(255)",
[ix.type.text] = "TEXT",
[ix.type.number] = "INT(11)",
[ix.type.steamid] = "VARCHAR(20)",
[ix.type.bool] = "TINYINT(1)"
}
}
ix.db.config = ix.config.server.database or {}
function ix.db.Connect()
ix.db.config.adapter = ix.db.config.adapter or "sqlite"
local dbmodule = ix.db.config.adapter
local hostname = ix.db.config.hostname
local username = ix.db.config.username
local password = ix.db.config.password
local database = ix.db.config.database
local port = ix.db.config.port
mysql:SetModule(dbmodule)
mysql:Connect(hostname, username, password, database, port)
end
function ix.db.AddToSchema(schemaType, field, fieldType)
if (!ix.db.type[fieldType]) then
error(string.format("attempted to add field in schema with invalid type '%s'", fieldType))
return
end
if (!mysql:IsConnected() or !ix.db.schema[schemaType]) then
ix.db.schemaQueue[#ix.db.schemaQueue + 1] = {schemaType, field, fieldType}
return
end
ix.db.InsertSchema(schemaType, field, fieldType)
end
-- this is only ever used internally
function ix.db.InsertSchema(schemaType, field, fieldType)
local schema = ix.db.schema[schemaType]
if (!schema) then
error(string.format("attempted to insert into schema with invalid schema type '%s'", schemaType))
return
end
if (!schema[field]) then
schema[field] = true
local query = mysql:Update("ix_schema")
query:Update("columns", util.TableToJSON(schema))
query:Execute()
query = mysql:Alter(schemaType)
query:Add(field, ix.db.type[fieldType])
query:Execute()
end
end
function ix.db.LoadTables()
local query
query = mysql:Create("ix_schema")
query:Create("table", "VARCHAR(64) NOT NULL")
query:Create("columns", "TEXT NOT NULL")
query:PrimaryKey("table")
query:Execute()
-- table structure will be populated with more fields when vars
-- are registered using ix.char.RegisterVar
query = mysql:Create("ix_characters")
query:Create("id", "INT(11) UNSIGNED NOT NULL AUTO_INCREMENT")
query:PrimaryKey("id")
query:Execute()
query = mysql:Create("ix_characters_data")
query:Create("id", "INT(11) UNSIGNED NOT NULL")
query:Create("key", "VARCHAR(60) NOT NULL")
query:Create("data", "TEXT DEFAULT NULL")
query:PrimaryKey("id`,`key")
query:Execute()
query = mysql:Create("ix_inventories")
query:Create("inventory_id", "INT(11) UNSIGNED NOT NULL AUTO_INCREMENT")
query:Create("character_id", "INT(11) UNSIGNED NOT NULL")
query:Create("inventory_type", "VARCHAR(150) DEFAULT NULL")
query:PrimaryKey("inventory_id")
query:Execute()
query = mysql:Create("ix_items")
query:Create("item_id", "INT(11) UNSIGNED NOT NULL AUTO_INCREMENT")
query:Create("inventory_id", "INT(11) UNSIGNED NOT NULL")
query:Create("unique_id", "VARCHAR(60) NOT NULL")
query:Create("character_id", "INT(11) UNSIGNED DEFAULT NULL")
query:Create("player_id", "VARCHAR(20) DEFAULT NULL")
query:Create("data", "TEXT DEFAULT NULL")
query:Create("x", "SMALLINT(4) NOT NULL")
query:Create("y", "SMALLINT(4) NOT NULL")
query:PrimaryKey("item_id")
query:Execute()
query = mysql:Create("ix_players")
query:Create("steamid", "VARCHAR(20) NOT NULL")
query:Create("steam_name", "VARCHAR(32) NOT NULL")
query:Create("play_time", "INT(11) UNSIGNED DEFAULT NULL")
query:Create("address", "VARCHAR(15) DEFAULT NULL")
query:Create("last_join_time", "INT(11) UNSIGNED DEFAULT NULL")
query:Create("data", "TEXT")
query:PrimaryKey("steamid")
query:Execute()
-- populate schema table if rows don't exist
query = mysql:InsertIgnore("ix_schema")
query:Insert("table", "ix_characters")
query:Insert("columns", util.TableToJSON({}))
query:Execute()
-- load schema from database
query = mysql:Select("ix_schema")
query:Callback(function(result)
if (!istable(result)) then
return
end
for _, v in pairs(result) do
ix.db.schema[v.table] = util.JSONToTable(v.columns)
end
-- update schema if needed
for i = 1, #ix.db.schemaQueue do
local entry = ix.db.schemaQueue[i]
ix.db.InsertSchema(entry[1], entry[2], entry[3])
end
end)
query:Execute()
end
function ix.db.WipeTables(callback)
local query
query = mysql:Drop("ix_schema")
query:Execute()
query = mysql:Drop("ix_characters")
query:Execute()
query = mysql:Drop("ix_inventories")
query:Execute()
query = mysql:Drop("ix_items")
query:Execute()
query = mysql:Drop("ix_players")
query:Callback(callback)
query:Execute()
end
hook.Add("InitPostEntity", "ixDatabaseConnect", function()
-- Connect to the database using SQLite, mysqoo, or tmysql4.
ix.db.Connect()
end)
local resetCalled = 0
concommand.Add("ix_wipedb", function(client, cmd, arguments)
-- can only be ran through the server's console
if (!IsValid(client)) then
if (resetCalled < RealTime()) then
resetCalled = RealTime() + 3
MsgC(Color(255, 0, 0),
"[Helix] WIPING THE DATABASE WILL PERMENANTLY REMOVE ALL PLAYER, CHARACTER, ITEM, AND INVENTORY DATA.\n")
MsgC(Color(255, 0, 0), "[Helix] THE SERVER WILL RESTART TO APPLY THESE CHANGES WHEN COMPLETED.\n")
MsgC(Color(255, 0, 0), "[Helix] TO CONFIRM DATABASE RESET, RUN 'ix_wipedb' AGAIN WITHIN 3 SECONDS.\n")
else
resetCalled = 0
MsgC(Color(255, 0, 0), "[Helix] DATABASE WIPE IN PROGRESS...\n")
hook.Run("OnWipeTables")
ix.db.WipeTables(function()
MsgC(Color(255, 255, 0), "[Helix] DATABASE WIPE COMPLETED!\n")
RunConsoleCommand("changelevel", game.GetMap())
end)
end
end
end)

View File

@@ -0,0 +1,245 @@
--[[
| 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/
--]]
-- @module ix.net
local entityMeta = FindMetaTable("Entity")
local playerMeta = FindMetaTable("Player")
ix.net = ix.net or {}
ix.net.list = ix.net.list or {}
ix.net.locals = ix.net.locals or {}
ix.net.globals = ix.net.globals or {}
util.AddNetworkString("ixGlobalVarSet")
util.AddNetworkString("ixLocalVarSet")
util.AddNetworkString("ixNetVarSet")
util.AddNetworkString("ixNetStatics")
util.AddNetworkString("ixNetVarDelete")
-- Check if there is an attempt to send a function. Can't send those.
local function CheckBadType(name, object)
if (isfunction(object)) then
ErrorNoHalt("Net var '" .. name .. "' contains a bad object type!")
return true
elseif (istable(object)) then
for k, v in pairs(object) do
-- Check both the key and the value for tables, and has recursion.
if (CheckBadType(name, k) or CheckBadType(name, v)) then
return true
end
end
end
end
function GetNetVar(key, default) -- luacheck: globals GetNetVar
local value = ix.net.globals[key]
return value != nil and value or default
end
function SetNetVar(key, value, receiver) -- luacheck: globals SetNetVar
if (CheckBadType(key, value)) then return end
if (GetNetVar(key) == value) then return end
ix.net.globals[key] = value
net.Start("ixGlobalVarSet")
net.WriteString(key)
net.WriteType(value)
if (receiver == nil) then
net.Broadcast()
else
net.Send(receiver)
end
end
--- Player networked variable functions
-- @classmod Player
--- Synchronizes networked variables to the client.
-- @realm server
-- @internal
local division = 100
function playerMeta:SyncVars()
for k, v in pairs(ix.net.globals) do
net.Start("ixGlobalVarSet")
net.WriteString(k)
net.WriteType(v)
net.Send(self)
end
for k, v in pairs(ix.net.locals[self] or {}) do
net.Start("ixLocalVarSet")
net.WriteString(k)
net.WriteType(v)
net.Send(self)
end
local statics = {}
local toWrite = {}
for entity, data in pairs(ix.net.list) do
if (IsValid(entity)) then
local index = entity:EntIndex()
if (table.Count(data) == 1 and data.Persistent == true) then
statics[#statics + 1] = index
continue
end
for k, v in pairs(data) do
toWrite[#toWrite + 1] = {index, k, v}
end
end
end
for i = 0, math.floor(#toWrite / division) do
timer.Simple(i * 0.5, function()
if (!IsValid(self)) then return end
for j = 0, division - 1 do
if (!toWrite[i * division + j]) then continue end
net.Start("ixNetVarSet")
net.WriteUInt(toWrite[i * division + j][1], 16)
net.WriteString(toWrite[i * division + j][2])
net.WriteType(toWrite[i * division + j][3])
net.Send(self)
end
end)
end
timer.Simple(30, function()
if (!IsValid(self)) then return end
net.Start("ixNetStatics")
net.WriteUInt(#statics, 16)
for _, v in ipairs(statics) do
local entity = Entity(v)
if (IsValid(entity) and ix.net.list[entity] and ix.net.list[entity].Persistent == true) then
net.WriteUInt(v, 16)
else
net.WriteUInt(0, 16)
end
end
net.Send(self)
end)
end
--- Retrieves a local networked variable. If it is not set, it'll return the default that you've specified.
-- Locally networked variables can only be retrieved from the owning player when used from the client.
-- @realm shared
-- @string key Identifier of the local variable
-- @param default Default value to return if the local variable is not set
-- @return Value associated with the key, or the default that was given if it doesn't exist
-- @usage print(client:GetLocalVar("secret"))
-- > 12345678
-- @see SetLocalVar
function playerMeta:GetLocalVar(key, default)
if (ix.net.locals[self] and ix.net.locals[self][key] != nil) then
return ix.net.locals[self][key]
end
return default
end
--- Sets the value of a local networked variable.
-- @realm server
-- @string key Identifier of the local variable
-- @param value New value to assign to the local variable
-- @usage client:SetLocalVar("secret", 12345678)
-- @see GetLocalVar
function playerMeta:SetLocalVar(key, value)
if (CheckBadType(key, value)) then return end
ix.net.locals[self] = ix.net.locals[self] or {}
ix.net.locals[self][key] = value
net.Start("ixLocalVarSet")
net.WriteString(key)
net.WriteType(value)
net.Send(self)
end
--- Entity networked variable functions
-- @classmod Entity
--- Retrieves a networked variable. If it is not set, it'll return the default that you've specified.
-- @realm shared
-- @string key Identifier of the networked variable
-- @param default Default value to return if the networked variable is not set
-- @return Value associated with the key, or the default that was given if it doesn't exist
-- @usage print(client:GetNetVar("example"))
-- > Hello World!
-- @see SetNetVar
function entityMeta:GetNetVar(key, default)
if (ix.net.list[self] and ix.net.list[self][key] != nil) then
return ix.net.list[self][key]
end
return default
end
--- Sets the value of a networked variable.
-- @realm server
-- @string key Identifier of the networked variable
-- @param value New value to assign to the networked variable
-- @tab[opt=nil] receiver The players to send the networked variable to
-- @usage client:SetNetVar("example", "Hello World!")
-- @see GetNetVar
function entityMeta:SetNetVar(key, value, receiver)
if (CheckBadType(key, value)) then return end
ix.net.list[self] = ix.net.list[self] or {}
if (ix.net.list[self][key] != value) then
ix.net.list[self][key] = value
end
self:SendNetVar(key, receiver)
end
--- Sends a networked variable.
-- @realm server
-- @internal
-- @string key Identifier of the networked variable
-- @tab[opt=nil] receiver The players to send the networked variable to
function entityMeta:SendNetVar(key, receiver)
net.Start("ixNetVarSet")
net.WriteUInt(self:EntIndex(), 16)
net.WriteString(key)
net.WriteType(ix.net.list[self] and ix.net.list[self][key])
if (receiver == nil) then
net.Broadcast()
else
net.Send(receiver)
end
end
--- Clears all of the networked variables.
-- @realm server
-- @internal
-- @tab[opt=nil] receiver The players to clear the networked variable for
function entityMeta:ClearNetVars(receiver)
ix.net.list[self] = nil
ix.net.locals[self] = nil
net.Start("ixNetVarDelete")
net.WriteUInt(self:EntIndex(), 16)
if (receiver == nil) then
net.Broadcast()
else
net.Send(receiver)
end
end

View File

@@ -0,0 +1,125 @@
--[[
| 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 playerMeta = FindMetaTable("Player")
-- Player data (outside of characters) handling.
do
util.AddNetworkString("ixData")
util.AddNetworkString("ixDataSync")
function playerMeta:LoadData(callback)
local name = self:SteamName()
local steamID64 = self:SteamID64()
local timestamp = math.floor(os.time())
local ip = self:IPAddress():match("%d+%.%d+%.%d+%.%d+")
local query = mysql:Select("ix_players")
query:Select("data")
query:Select("play_time")
query:Where("steamid", steamID64)
query:Callback(function(result)
if (IsValid(self) and istable(result) and #result > 0 and result[1].data) then
local updateQuery = mysql:Update("ix_players")
updateQuery:Update("last_join_time", timestamp)
updateQuery:Update("address", ip)
updateQuery:Where("steamid", steamID64)
updateQuery:Execute()
self.ixPlayTime = tonumber(result[1].play_time) or 0
self.ixData = util.JSONToTable(result[1].data)
hook.Run("PlayerDataRestored", self)
if (callback) then
callback(self.ixData)
end
else
local insertQuery = mysql:Insert("ix_players")
insertQuery:Insert("steamid", steamID64)
insertQuery:Insert("steam_name", name)
insertQuery:Insert("play_time", 0)
insertQuery:Insert("address", ip)
insertQuery:Insert("last_join_time", timestamp)
insertQuery:Insert("data", util.TableToJSON({}))
insertQuery:Execute()
hook.Run("PlayerDataRestored", self)
if (callback) then
callback({})
end
end
end)
query:Execute()
end
function playerMeta:SaveData()
local name = self:SteamName()
local steamID64 = self:SteamID64()
local query = mysql:Update("ix_players")
query:Update("steam_name", name)
query:Update("play_time", math.floor((self.ixPlayTime or 0) + (RealTime() - (self.ixJoinTime or RealTime() - 1))))
query:Update("data", util.TableToJSON(self.ixData))
query:Where("steamid", steamID64)
query:Execute()
end
function playerMeta:SetData(key, value, bNoNetworking)
self.ixData = self.ixData or {}
self.ixData[key] = value
if (!bNoNetworking) then
net.Start("ixData")
net.WriteString(key)
net.WriteType(value)
net.Send(self)
end
end
end
-- Whitelisting information for the player.
do
function playerMeta:SetWhitelisted(faction, whitelisted)
if (!whitelisted) then
whitelisted = nil
end
local data = ix.faction.indices[faction]
if (data) then
local whitelists = self:GetData("whitelists", {})
whitelists[Schema.folder] = whitelists[Schema.folder] or {}
whitelists[Schema.folder][data.uniqueID] = whitelisted and true or nil
self:SetData("whitelists", whitelists)
self:SaveData()
return true
end
return false
end
end
do
playerMeta.ixGive = playerMeta.ixGive or playerMeta.Give
function playerMeta:Give(className, bNoAmmo)
local weapon
self.ixWeaponGive = true
weapon = self:ixGive(className, bNoAmmo)
self.ixWeaponGive = nil
return weapon
end
end

View File

@@ -0,0 +1,362 @@
--[[
| 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/
--]]
--[[
BLACK TEA ICON LIBRARY FOR NUTSCRIPT 1.1
The MIT License (MIT)
Copyright (c) 2017, Kyu Yeon Lee(Black Tea Za rebel1324)
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so, subject
to the following conditions:
The above copyright notice and thispermission notice shall be included in all copies
or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
TL;DR: https://tldrlegal.com/license/mit-license
OK -
Commercial Use
Modify
Distribute
Sublicense
Private Use
NOT OK -
Hold Liable
MUST -
Include Copyright
Include License
]]--
--[[
Default Tables.
]]--
ikon = ikon or {}
ikon.cache = ikon.cache or {}
ikon.requestList = ikon.requestList or {}
ikon.dev = false
ikon.maxSize = 8 -- 8x8 (512^2) is max icon size.
IKON_BUSY = 1
IKON_PROCESSING = 0
IKON_SOMETHINGWRONG = -1
local schemaName = schemaName or (Schema and Schema.folder)
--[[
Initialize hooks and RT Screens.
returns nothing
]]--
function ikon:init()
if (self.dev) then
hook.Add("HUDPaint", "ikon_dev2", ikon.showResult)
else
hook.Remove("HUDPaint", "ikon_dev2")
end
--[[
Being good at gmod is knowing all of stinky hacks
- Black Tea (2017)
]]--
ikon.haloAdd = ikon.haloAdd or halo.Add
function halo.Add(...)
if (ikon.rendering != true) then
ikon.haloAdd(...)
end
end
ikon.haloRender = ikon.haloRender or halo.Render
function halo.Render(...)
if (ikon.rendering != true) then
ikon.haloRender(...)
end
end
file.CreateDir("helix/icons")
file.CreateDir("helix/icons/" .. schemaName)
end
--[[
IKON Library Essential Material/Texture Declare
]]--
local TEXTURE_FLAGS_CLAMP_S = 0x0004
local TEXTURE_FLAGS_CLAMP_T = 0x0008
ikon.max = ikon.maxSize * 64
ikon.RT = GetRenderTargetEx("ixIconRendered",
ikon.max,
ikon.max,
RT_SIZE_NO_CHANGE,
MATERIAL_RT_DEPTH_SHARED,
bit.bor(TEXTURE_FLAGS_CLAMP_S, TEXTURE_FLAGS_CLAMP_T),
CREATERENDERTARGETFLAGS_UNFILTERABLE_OK,
IMAGE_FORMAT_RGBA8888
)
local tex_effect = GetRenderTarget("ixIconRenderedOutline", ikon.max, ikon.max)
local mat_outline = CreateMaterial("ixIconRenderedTemp", "UnlitGeneric", {
["$basetexture"] = tex_effect:GetName(),
["$translucent"] = 1
})
local lightPositions = {
BOX_TOP = Color(255, 255, 255),
BOX_FRONT = Color(255, 255, 255),
}
function ikon:renderHook()
local entity = ikon.renderEntity
if (halo.RenderedEntity() == entity) then
return
end
local w, h = ikon.curWidth * 64, ikon.curHeight * 64
local x, y = 0, 0
local tab
if (ikon.info) then
tab = {
origin = ikon.info.pos,
angles = ikon.info.ang,
fov = ikon.info.fov,
outline = ikon.info.outline,
outCol = ikon.info.outlineColor
}
if (!tab.origin and !tab.angles and !tab.fov) then
table.Merge(tab, PositionSpawnIcon(entity, entity:GetPos(), true))
end
else
tab = PositionSpawnIcon(entity, entity:GetPos(), true)
end
-- Taking MDave's Tip
xpcall(function()
render.OverrideAlphaWriteEnable(true, true) -- some playermodel eyeballs will not render without this
render.SetWriteDepthToDestAlpha(false)
render.OverrideBlend(true, BLEND_ONE, BLEND_ONE, BLENDFUNC_ADD, BLEND_ONE, BLEND_ONE, BLENDFUNC_ADD)
render.SuppressEngineLighting(true)
render.Clear(0, 0, 0, 0, true, true)
render.SetLightingOrigin(vector_origin)
render.ResetModelLighting(200 / 255, 200 / 255, 200 / 255)
render.SetColorModulation(1, 1, 1)
for i = 0, 6 do
local col = lightPositions[i]
if (col) then
render.SetModelLighting(i, col.r / 255, col.g / 255, col.b / 255)
end
end
if (tab.outline) then
ix.util.ResetStencilValues()
render.SetStencilEnable(true)
render.SetStencilWriteMask(137) -- yeah random number to avoid confliction
render.SetStencilCompareFunction(STENCILCOMPARISONFUNCTION_ALWAYS)
render.SetStencilPassOperation(STENCILOPERATION_REPLACE)
render.SetStencilFailOperation(STENCILOPERATION_REPLACE)
end
--[[
Add more effects on the Models!
]]--
if (ikon.info and ikon.info.drawHook) then
ikon.info.drawHook(entity)
end
cam.Start3D(tab.origin, tab.angles, tab.fov, 0, 0, w, h)
render.SetBlend(1)
entity:DrawModel()
cam.End3D()
if (tab.outline) then
render.PushRenderTarget(tex_effect)
render.Clear(0, 0, 0, 0)
render.ClearDepth()
cam.Start2D()
cam.Start3D(tab.origin, tab.angles, tab.fov, 0, 0, w, h)
render.SetBlend(0)
entity:DrawModel()
render.SetStencilWriteMask(138) -- could you please?
render.SetStencilTestMask(1)
render.SetStencilReferenceValue(1)
render.SetStencilCompareFunction(STENCILCOMPARISONFUNCTION_EQUAL)
render.SetStencilPassOperation(STENCILOPERATION_KEEP)
render.SetStencilFailOperation(STENCILOPERATION_KEEP)
cam.Start2D()
surface.SetDrawColor(tab.outCol or color_white)
surface.DrawRect(0, 0, ScrW(), ScrH())
cam.End2D()
cam.End3D()
cam.End2D()
render.PopRenderTarget()
render.SetBlend(1)
render.SetStencilCompareFunction(STENCILCOMPARISONFUNCTION_NOTEQUAL)
--[[
Thanks for Noiwex
NxServ.eu
]]--
cam.Start2D()
surface.SetMaterial(mat_outline)
surface.DrawTexturedRectUV(-2, 0, w, h, 0, 0, w / ikon.max, h / ikon.max)
surface.DrawTexturedRectUV(2, 0, w, h, 0, 0, w / ikon.max, h / ikon.max)
surface.DrawTexturedRectUV(0, 2, w, h, 0, 0, w / ikon.max, h / ikon.max)
surface.DrawTexturedRectUV(0, -2, w, h, 0, 0, w / ikon.max, h / ikon.max)
cam.End2D()
render.SetStencilEnable(false)
end
render.SuppressEngineLighting(false)
render.SetWriteDepthToDestAlpha(true)
render.OverrideAlphaWriteEnable(false)
end, function(message)
print(message)
end)
end
function ikon:showResult()
local x, y = ScrW() / 2, ScrH() / 2
local w, h = ikon.curWidth * 64, ikon.curHeight * 64
surface.SetDrawColor(255, 255, 255, 255)
surface.DrawOutlinedRect(x, 0, w, h)
surface.SetMaterial(mat_outline)
surface.DrawTexturedRect(x, 0, w, h)
end
--[[
Renders the Icon with given arguments.
returns nothing
]]--
function ikon:renderIcon(name, w, h, mdl, camInfo, updateCache)
if (#ikon.requestList > 0) then return IKON_BUSY end
if (ikon.requestList[name]) then return IKON_PROCESSING end
if (!w or !h or !mdl) then return IKON_SOMETHINGWRONG end
local capturedIcon
ikon.curWidth = w or 1
ikon.curHeight = h or 1
ikon.renderModel = mdl
if (camInfo) then
ikon.info = camInfo
end
local w, h = ikon.curWidth * 64, ikon.curHeight * 64
local sw, sh = ScrW(), ScrH()
if (ikon.renderModel) then
if (!IsValid(ikon.renderEntity)) then
ikon.renderEntity = ClientsideModel(ikon.renderModel, RENDERGROUP_BOTH)
ikon.renderEntity:SetNoDraw(true)
end
end
ikon.renderEntity:SetModel(ikon.renderModel)
local bone = ikon.renderEntity:LookupBone("ValveBiped.Bip01_Head1")
if (bone) then
ikon.renderEntity:SetEyeTarget(ikon.renderEntity:GetBonePosition(bone) + ikon.renderEntity:GetForward() * 32)
end
local oldRT = render.GetRenderTarget()
render.PushRenderTarget(ikon.RT)
ikon.rendering = true
ikon:renderHook()
ikon.rendering = nil
capturedIcon = render.Capture({
format = "png",
alpha = true,
x = 0,
y = 0,
w = w,
h = h
})
file.Write("helix/icons/" .. schemaName .. "/" .. name .. ".png", capturedIcon)
ikon.info = nil
render.PopRenderTarget()
if (updateCache) then
local materialID = tostring(os.time())
file.Write(materialID .. ".png", capturedIcon)
timer.Simple(0, function()
local material = Material("../data/".. materialID ..".png")
ikon.cache[name] = material
file.Delete(materialID .. ".png")
end)
end
ikon.requestList[name] = nil
return true
end
--[[
Gets rendered icon with given unique name.
returns IMaterial
]]--
function ikon:GetIcon(name)
if (ikon.cache[name]) then
return ikon.cache[name] -- yeah return cache
end
if (file.Exists("helix/icons/" .. schemaName .. "/" .. name .. ".png", "DATA")) then
ikon.cache[name] = Material("../data/helix/icons/" .. schemaName .. "/".. name ..".png")
return ikon.cache[name] -- yeah return cache
else
return false -- retryd
end
end
concommand.Add("ix_flushicon", function()
local root = "helix/icons/" .. schemaName
for _, v in pairs(file.Find(root .. "/*.png", "DATA")) do
file.Delete(root .. "/" .. v)
end
ikon.cache = {}
end)
hook.Add("InitializedSchema", "updatePath", function()
schemaName = Schema.folder
ikon:init()
end)
if (schemaName) then
ikon:init()
end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,560 @@
--[[
| 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/
--]]
--[[
CAMI - Common Admin Mod Interface.
Makes admin mods intercompatible and provides an abstract privilege interface
for third party addons.
Follows the specification on this page:
https://docs.google.com/document/d/1QIRVcAgZfAYf1aBl_dNV_ewR6P25wze2KmUVzlbFgMI
Structures:
CAMI_USERGROUP, defines the charactaristics of a usergroup:
{
Name
string
The name of the usergroup
Inherits
string
The name of the usergroup this usergroup inherits from
}
CAMI_PRIVILEGE, defines the charactaristics of a privilege:
{
Name
string
The name of the privilege
MinAccess
string
One of the following three: user/admin/superadmin
Description
string
optional
A text describing the purpose of the privilege
HasAccess
function(
privilege :: CAMI_PRIVILEGE,
actor :: Player,
target :: Player
) :: bool
optional
Function that decides whether a player can execute this privilege,
optionally on another player (target).
}
]]
-- Version number in YearMonthDay format.
local version = 20190102
if CAMI and CAMI.Version >= version then return end
CAMI = CAMI or {}
CAMI.Version = version
--[[
usergroups
Contains the registered CAMI_USERGROUP usergroup structures.
Indexed by usergroup name.
]]
local usergroups = CAMI.GetUsergroups and CAMI.GetUsergroups() or {
user = {
Name = "user",
Inherits = "user"
},
admin = {
Name = "admin",
Inherits = "user"
},
superadmin = {
Name = "superadmin",
Inherits = "admin"
}
}
--[[
privileges
Contains the registered CAMI_PRIVILEGE privilege structures.
Indexed by privilege name.
]]
local privileges = CAMI.GetPrivileges and CAMI.GetPrivileges() or {}
--[[
CAMI.RegisterUsergroup
Registers a usergroup with CAMI.
Parameters:
usergroup
CAMI_USERGROUP
(see CAMI_USERGROUP structure)
source
any
Identifier for your own admin mod. Can be anything.
Use this to make sure CAMI.RegisterUsergroup function and the
CAMI.OnUsergroupRegistered hook don't cause an infinite loop
Return value:
CAMI_USERGROUP
The usergroup given as argument.
]]
function CAMI.RegisterUsergroup(usergroup, source)
usergroups[usergroup.Name] = usergroup
hook.Call("CAMI.OnUsergroupRegistered", nil, usergroup, source)
return usergroup
end
--[[
CAMI.UnregisterUsergroup
Unregisters a usergroup from CAMI. This will call a hook that will notify
all other admin mods of the removal.
Call only when the usergroup is to be permanently removed.
Parameters:
usergroupName
string
The name of the usergroup.
source
any
Identifier for your own admin mod. Can be anything.
Use this to make sure CAMI.UnregisterUsergroup function and the
CAMI.OnUsergroupUnregistered hook don't cause an infinite loop
Return value:
bool
Whether the unregistering succeeded.
]]
function CAMI.UnregisterUsergroup(usergroupName, source)
if not usergroups[usergroupName] then return false end
local usergroup = usergroups[usergroupName]
usergroups[usergroupName] = nil
hook.Call("CAMI.OnUsergroupUnregistered", nil, usergroup, source)
return true
end
--[[
CAMI.GetUsergroups
Retrieves all registered usergroups.
Return value:
Table of CAMI_USERGROUP, indexed by their names.
]]
function CAMI.GetUsergroups()
return usergroups
end
--[[
CAMI.GetUsergroup
Receives information about a usergroup.
Return value:
CAMI_USERGROUP
Returns nil when the usergroup does not exist.
]]
function CAMI.GetUsergroup(usergroupName)
return usergroups[usergroupName]
end
--[[
CAMI.UsergroupInherits
Returns true when usergroupName1 inherits usergroupName2.
Note that usergroupName1 does not need to be a direct child.
Every usergroup trivially inherits itself.
Parameters:
usergroupName1
string
The name of the usergroup that is queried.
usergroupName2
string
The name of the usergroup of which is queried whether usergroupName
inherits from.
Return value:
bool
Whether usergroupName1 inherits usergroupName2.
]]
function CAMI.UsergroupInherits(usergroupName1, usergroupName2)
repeat
if usergroupName1 == usergroupName2 then return true end
usergroupName1 = usergroups[usergroupName1] and
usergroups[usergroupName1].Inherits or
usergroupName1
until not usergroups[usergroupName1] or
usergroups[usergroupName1].Inherits == usergroupName1
-- One can only be sure the usergroup inherits from user if the
-- usergroup isn't registered.
return usergroupName1 == usergroupName2 or usergroupName2 == "user"
end
--[[
CAMI.InheritanceRoot
All usergroups must eventually inherit either user, admin or superadmin.
Regardless of what inheritance mechism an admin may or may not have, this
always applies.
This method always returns either user, admin or superadmin, based on what
usergroups eventually inherit.
Parameters:
usergroupName
string
The name of the usergroup of which the root of inheritance is
requested
Return value:
string
The name of the root usergroup (either user, admin or superadmin)
]]
function CAMI.InheritanceRoot(usergroupName)
if not usergroups[usergroupName] then return end
local inherits = usergroups[usergroupName].Inherits
while inherits ~= usergroups[usergroupName].Inherits do
usergroupName = usergroups[usergroupName].Inherits
end
return usergroupName
end
--[[
CAMI.RegisterPrivilege
Registers a privilege with CAMI.
Note: do NOT register all your admin mod's privileges with this function!
This function is for third party addons to register privileges
with admin mods, not for admin mods sharing the privileges amongst one
another.
Parameters:
privilege
CAMI_PRIVILEGE
See CAMI_PRIVILEGE structure.
Return value:
CAMI_PRIVILEGE
The privilege given as argument.
]]
function CAMI.RegisterPrivilege(privilege)
privileges[privilege.Name] = privilege
hook.Call("CAMI.OnPrivilegeRegistered", nil, privilege)
return privilege
end
--[[
CAMI.UnregisterPrivilege
Unregisters a privilege from CAMI. This will call a hook that will notify
all other admin mods of the removal.
Call only when the privilege is to be permanently removed.
Parameters:
privilegeName
string
The name of the privilege.
Return value:
bool
Whether the unregistering succeeded.
]]
function CAMI.UnregisterPrivilege(privilegeName)
if not privileges[privilegeName] then return false end
local privilege = privileges[privilegeName]
privileges[privilegeName] = nil
hook.Call("CAMI.OnPrivilegeUnregistered", nil, privilege)
return true
end
--[[
CAMI.GetPrivileges
Retrieves all registered privileges.
Return value:
Table of CAMI_PRIVILEGE, indexed by their names.
]]
function CAMI.GetPrivileges()
return privileges
end
--[[
CAMI.GetPrivilege
Receives information about a privilege.
Return value:
CAMI_PRIVILEGE when the privilege exists.
nil when the privilege does not exist.
]]
function CAMI.GetPrivilege(privilegeName)
return privileges[privilegeName]
end
--[[
CAMI.PlayerHasAccess
Queries whether a certain player has the right to perform a certain action.
Parameters:
actorPly
Player
The player of which is requested whether they have the privilege.
privilegeName
string
The name of the privilege.
callback
function(bool, string) or nil
This function will be called with the answer. The bool signifies the
yes or no answer as to whether the player is allowed. The string
will optionally give a reason.
Give an explicit nil here to get an answer immediately
Important note: May throw an error when the admin mod doesn't
give an answer immediately!
targetPly
Optional.
The player on which the privilege is executed.
extraInfoTbl
Optional.
Table containing extra information.
Officially supported members:
Fallback
string
Either of user/admin/superadmin. When no admin mod replies,
the decision is based on the admin status of the user.
Defaults to admin if not given.
IgnoreImmunity
bool
Ignore any immunity mechanisms an admin mod might have.
CommandArguments
table
Extra arguments that were given to the privilege command.
Return value:
If callback is specified:
None
Otherwise:
hasAccess
bool
Whether the player has access
reason
Optional.
The reason why a player does or does not have access.
]]
-- Default access handler
local defaultAccessHandler = {["CAMI.PlayerHasAccess"] =
function(_, actorPly, privilegeName, callback, _, extraInfoTbl)
-- The server always has access in the fallback
if not IsValid(actorPly) then return callback(true, "Fallback.") end
local priv = privileges[privilegeName]
local fallback = extraInfoTbl and (
not extraInfoTbl.Fallback and actorPly:IsAdmin() or
extraInfoTbl.Fallback == "user" and true or
extraInfoTbl.Fallback == "admin" and actorPly:IsAdmin() or
extraInfoTbl.Fallback == "superadmin" and actorPly:IsSuperAdmin())
if not priv then return callback(fallback, "Fallback.") end
callback(
priv.MinAccess == "user" or
priv.MinAccess == "admin" and actorPly:IsAdmin() or
priv.MinAccess == "superadmin" and actorPly:IsSuperAdmin()
, "Fallback.")
end,
["CAMI.SteamIDHasAccess"] =
function(_, _, _, callback)
callback(false, "No information available.")
end
}
function CAMI.PlayerHasAccess(actorPly, privilegeName, callback, targetPly,
extraInfoTbl)
local hasAccess, reason = nil, nil
local callback_ = callback or function(hA, r) hasAccess, reason = hA, r end
hook.Call("CAMI.PlayerHasAccess", defaultAccessHandler, actorPly,
privilegeName, callback_, targetPly, extraInfoTbl)
if callback ~= nil then return end
if hasAccess == nil then
local err = [[The function CAMI.PlayerHasAccess was used to find out
whether Player %s has privilege "%s", but an admin mod did not give an
immediate answer!]]
error(string.format(err,
actorPly:IsPlayer() and actorPly:Nick() or tostring(actorPly),
privilegeName))
end
return hasAccess, reason
end
--[[
CAMI.GetPlayersWithAccess
Finds the list of currently joined players who have the right to perform a
certain action.
NOTE: this function will NOT return an immediate result!
The result is in the callback!
Parameters:
privilegeName
string
The name of the privilege.
callback
function(players)
This function will be called with the list of players with access.
targetPly
Optional.
The player on which the privilege is executed.
extraInfoTbl
Optional.
Table containing extra information.
Officially supported members:
Fallback
string
Either of user/admin/superadmin. When no admin mod replies,
the decision is based on the admin status of the user.
Defaults to admin if not given.
IgnoreImmunity
bool
Ignore any immunity mechanisms an admin mod might have.
CommandArguments
table
Extra arguments that were given to the privilege command.
]]
function CAMI.GetPlayersWithAccess(privilegeName, callback, targetPly,
extraInfoTbl)
local allowedPlys = {}
local allPlys = player.GetAll()
local countdown = #allPlys
local function onResult(ply, hasAccess, _)
countdown = countdown - 1
if hasAccess then table.insert(allowedPlys, ply) end
if countdown == 0 then callback(allowedPlys) end
end
for _, ply in ipairs(allPlys) do
CAMI.PlayerHasAccess(ply, privilegeName,
function(...) onResult(ply, ...) end,
targetPly, extraInfoTbl)
end
end
--[[
CAMI.SteamIDHasAccess
Queries whether a player with a steam ID has the right to perform a certain
action.
Note: the player does not need to be in the server for this to
work.
Note: this function does NOT return an immediate result!
The result is in the callback!
Parameters:
actorSteam
Player
The SteamID of the player of which is requested whether they have
the privilege.
privilegeName
string
The name of the privilege.
callback
function(bool, string)
This function will be called with the answer. The bool signifies the
yes or no answer as to whether the player is allowed. The string
will optionally give a reason.
targetSteam
Optional.
The SteamID of the player on which the privilege is executed.
extraInfoTbl
Optional.
Table containing extra information.
Officially supported members:
IgnoreImmunity
bool
Ignore any immunity mechanisms an admin mod might have.
CommandArguments
table
Extra arguments that were given to the privilege command.
Return value:
None, the answer is given in the callback function in order to allow
for the admin mod to perform e.g. a database lookup.
]]
function CAMI.SteamIDHasAccess(actorSteam, privilegeName, callback,
targetSteam, extraInfoTbl)
hook.Call("CAMI.SteamIDHasAccess", defaultAccessHandler, actorSteam,
privilegeName, callback, targetSteam, extraInfoTbl)
end
--[[
CAMI.SignalUserGroupChanged
Signify that your admin mod has changed the usergroup of a player. This
function communicates to other admin mods what it thinks the usergroup
of a player should be.
Listen to the hook to receive the usergroup changes of other admin mods.
Parameters:
ply
Player
The player for which the usergroup is changed
old
string
The previous usergroup of the player.
new
string
The new usergroup of the player.
source
any
Identifier for your own admin mod. Can be anything.
]]
function CAMI.SignalUserGroupChanged(ply, old, new, source)
hook.Call("CAMI.PlayerUsergroupChanged", nil, ply, old, new, source)
end
--[[
CAMI.SignalSteamIDUserGroupChanged
Signify that your admin mod has changed the usergroup of a disconnected
player. This communicates to other admin mods what it thinks the usergroup
of a player should be.
Listen to the hook to receive the usergroup changes of other admin mods.
Parameters:
ply
string
The steam ID of the player for which the usergroup is changed
old
string
The previous usergroup of the player.
new
string
The new usergroup of the player.
source
any
Identifier for your own admin mod. Can be anything.
]]
function CAMI.SignalSteamIDUserGroupChanged(steamId, old, new, source)
hook.Call("CAMI.SteamIDUsergroupChanged", nil, steamId, old, new, source)
end

View File

@@ -0,0 +1,791 @@
--[[
| 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/
--]]
---------------------------------------------------------------------------------------
-- Module for date and time calculations
--
-- Version 2.1.1
-- Copyright (C) 2006, by Jas Latrix (jastejada@yahoo.com)
-- Copyright (C) 2013-2014, by Thijs Schreijer
-- Licensed under MIT, http://opensource.org/licenses/MIT
-- https://github.com/Tieske/date
--[[
The MIT License (MIT) http://opensource.org/licenses/MIT
Copyright (c) 2013-2017 Thijs Schreijer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
]]
--[[ CONSTANTS ]]--
local HOURPERDAY = 24
local MINPERHOUR = 60
local MINPERDAY = 1440 -- 24*60
local SECPERMIN = 60
local SECPERHOUR = 3600 -- 60*60
local SECPERDAY = 86400 -- 24*60*60
local TICKSPERSEC = 1000000
local TICKSPERDAY = 86400000000
local TICKSPERHOUR = 3600000000
local TICKSPERMIN = 60000000
local DAYNUM_MAX = 365242500 -- Sat Jan 01 1000000 00:00:00
local DAYNUM_MIN = -365242500 -- Mon Jan 01 1000000 BCE 00:00:00
local DAYNUM_DEF = 0 -- Mon Jan 01 0001 00:00:00
local _;
--[[ LOCAL ARE FASTER ]]--
local type = type
local pairs = pairs
local error = error
local assert = assert
local tonumber = tonumber
local tostring = tostring
local string = string
local math = math
local os = os
local unpack = unpack or table.unpack
local pack = table.pack or function(...) return { n = select('#', ...), ... } end
local setmetatable = setmetatable
local getmetatable = getmetatable
--[[ EXTRA FUNCTIONS ]]--
local fmt = string.format
local lwr = string.lower
local upr = string.upper
local rep = string.rep
local len = string.len
local sub = string.sub
local gsub = string.gsub
local gmatch = string.gmatch or string.gfind
local find = string.find
local ostime = os.time
local osdate = os.date
local floor = math.floor
local ceil = math.ceil
local abs = math.abs
-- removes the decimal part of a number
local function fix(n) n = tonumber(n) return n and ((n > 0 and floor or ceil)(n)) end
-- returns the modulo n % d;
local function mod(n,d) return n - d*floor(n/d) end
-- rounds a number;
local function round(n, d) d=d^10 return floor((n*d)+.5)/d end
-- rounds a number to whole;
local function whole(n)return floor(n+.5)end
-- is `str` in string list `tbl`, `ml` is the minimun len
local function inlist(str, tbl, ml, tn)
local sl = len(str)
if sl < (ml or 0) then return nil end
str = lwr(str)
for k, v in pairs(tbl) do
if str == lwr(sub(v, 1, sl)) then
if tn then tn[0] = k end
return k
end
end
end
local function fnil() end
local function fret(x)return x;end
--[[ DATE FUNCTIONS ]]--
local DATE_EPOCH -- to be set later
local sl_weekdays = {
[0]="Sunday",[1]="Monday",[2]="Tuesday",[3]="Wednesday",[4]="Thursday",[5]="Friday",[6]="Saturday",
[7]="Sun",[8]="Mon",[9]="Tue",[10]="Wed",[11]="Thu",[12]="Fri",[13]="Sat",
}
local sl_meridian = {[-1]="AM", [1]="PM"}
local sl_months = {
[00]="January", [01]="February", [02]="March",
[03]="April", [04]="May", [05]="June",
[06]="July", [07]="August", [08]="September",
[09]="October", [10]="November", [11]="December",
[12]="Jan", [13]="Feb", [14]="Mar",
[15]="Apr", [16]="May", [17]="Jun",
[18]="Jul", [19]="Aug", [20]="Sep",
[21]="Oct", [22]="Nov", [23]="Dec",
}
-- added the '.2' to avoid collision, use `fix` to remove
local sl_timezone = {
[000]="utc", [0.2]="gmt",
[300]="est", [240]="edt",
[360]="cst", [300.2]="cdt",
[420]="mst", [360.2]="mdt",
[480]="pst", [420.2]="pdt",
}
-- set the day fraction resolution
local function setticks(t)
TICKSPERSEC = t;
TICKSPERDAY = SECPERDAY*TICKSPERSEC
TICKSPERHOUR= SECPERHOUR*TICKSPERSEC
TICKSPERMIN = SECPERMIN*TICKSPERSEC
end
-- is year y leap year?
local function isleapyear(y) -- y must be int!
return (mod(y, 4) == 0 and (mod(y, 100) ~= 0 or mod(y, 400) == 0))
end
-- day since year 0
local function dayfromyear(y) -- y must be int!
return 365*y + floor(y/4) - floor(y/100) + floor(y/400)
end
-- day number from date, month is zero base
local function makedaynum(y, m, d)
local mm = mod(mod(m,12) + 10, 12)
return dayfromyear(y + floor(m/12) - floor(mm/10)) + floor((mm*306 + 5)/10) + d - 307
--local yy = y + floor(m/12) - floor(mm/10)
--return dayfromyear(yy) + floor((mm*306 + 5)/10) + (d - 1)
end
-- date from day number, month is zero base
local function breakdaynum(g)
local g = g + 306
local y = floor((10000*g + 14780)/3652425)
local d = g - dayfromyear(y)
if d < 0 then y = y - 1; d = g - dayfromyear(y) end
local mi = floor((100*d + 52)/3060)
return (floor((mi + 2)/12) + y), mod(mi + 2,12), (d - floor((mi*306 + 5)/10) + 1)
end
--[[ for floats or int32 Lua Number data type
local function breakdaynum2(g)
local g, n = g + 306;
local n400 = floor(g/DI400Y);n = mod(g,DI400Y);
local n100 = floor(n/DI100Y);n = mod(n,DI100Y);
local n004 = floor(n/DI4Y); n = mod(n,DI4Y);
local n001 = floor(n/365); n = mod(n,365);
local y = (n400*400) + (n100*100) + (n004*4) + n001 - ((n001 == 4 or n100 == 4) and 1 or 0)
local d = g - dayfromyear(y)
local mi = floor((100*d + 52)/3060)
return (floor((mi + 2)/12) + y), mod(mi + 2,12), (d - floor((mi*306 + 5)/10) + 1)
end
]]
-- day fraction from time
local function makedayfrc(h,r,s,t)
return ((h*60 + r)*60 + s)*TICKSPERSEC + t
end
-- time from day fraction
local function breakdayfrc(df)
return
mod(floor(df/TICKSPERHOUR),HOURPERDAY),
mod(floor(df/TICKSPERMIN ),MINPERHOUR),
mod(floor(df/TICKSPERSEC ),SECPERMIN),
mod(df,TICKSPERSEC)
end
-- weekday sunday = 0, monday = 1 ...
local function weekday(dn) return mod(dn + 1, 7) end
-- yearday 0 based ...
local function yearday(dn)
return dn - dayfromyear((breakdaynum(dn))-1)
end
-- parse v as a month
local function getmontharg(v)
local m = tonumber(v);
return (m and fix(m - 1)) or inlist(tostring(v) or "", sl_months, 2)
end
-- get daynum of isoweek one of year y
local function isow1(y)
local f = makedaynum(y, 0, 4) -- get the date for the 4-Jan of year `y`
local d = weekday(f)
d = d == 0 and 7 or d -- get the ISO day number, 1 == Monday, 7 == Sunday
return f + (1 - d)
end
local function isowy(dn)
local w1;
local y = (breakdaynum(dn))
if dn >= makedaynum(y, 11, 29) then
w1 = isow1(y + 1);
if dn < w1 then
w1 = isow1(y);
else
y = y + 1;
end
else
w1 = isow1(y);
if dn < w1 then
w1 = isow1(y-1)
y = y - 1
end
end
return floor((dn-w1)/7)+1, y
end
local function isoy(dn)
local y = (breakdaynum(dn))
return y + (((dn >= makedaynum(y, 11, 29)) and (dn >= isow1(y + 1))) and 1 or (dn < isow1(y) and -1 or 0))
end
local function makedaynum_isoywd(y,w,d)
return isow1(y) + 7*w + d - 8 -- simplified: isow1(y) + ((w-1)*7) + (d-1)
end
--[[ THE DATE MODULE ]]--
local fmtstr = "%x %X";
--#if not DATE_OBJECT_AFX then
local date = {}
setmetatable(date, date)
-- Version: VMMMRRRR; V-Major, M-Minor, R-Revision; e.g. 5.45.321 == 50450321
date.version = 20010001 -- 2.1.1
--#end -- not DATE_OBJECT_AFX
--[[ THE DATE OBJECT ]]--
local dobj = {}
dobj.__index = dobj
dobj.__metatable = dobj
-- shout invalid arg
local function date_error_arg() return error("invalid argument(s)",0) end
-- create new date object
local function date_new(dn, df)
return setmetatable({daynum=dn, dayfrc=df}, dobj)
end
-- is `v` a date object?
local function date_isdobj(v)
return (istable(v) and getmetatable(v) == dobj) and v
end
--#if not NO_LOCAL_TIME_SUPPORT then
-- magic year table
local date_epoch, yt;
local function getequivyear(y)
assert(not yt)
yt = {}
local de, dw, dy = date_epoch:copy()
for i = 0, 3000 do
de:setyear(de:getyear() + 1, 1, 1)
dy = de:getyear()
dw = de:getweekday() * (isleapyear(dy) and -1 or 1)
if not yt[dw] then yt[dw] = dy end --print(de)
if yt[1] and yt[2] and yt[3] and yt[4] and yt[5] and yt[6] and yt[7] and yt[-1] and yt[-2] and yt[-3] and yt[-4] and yt[-5] and yt[-6] and yt[-7] then
getequivyear = function(y) return yt[ (weekday(makedaynum(y, 0, 1)) + 1) * (isleapyear(y) and -1 or 1) ] end
return getequivyear(y)
end
end
end
-- TimeValue from daynum and dayfrc
local function dvtotv(dn, df)
return fix(dn - DATE_EPOCH) * SECPERDAY + (df/1000)
end
-- TimeValue from date and time
local function totv(y,m,d,h,r,s)
return (makedaynum(y, m, d) - DATE_EPOCH) * SECPERDAY + ((h*60 + r)*60 + s)
end
-- TimeValue from TimeTable
local function tmtotv(tm)
return tm and totv(tm.year, tm.month - 1, tm.day, tm.hour, tm.min, tm.sec)
end
-- Returns the bias in seconds of utc time daynum and dayfrc
local function getbiasutc2(self)
local y,m,d = breakdaynum(self.daynum)
local h,r,s = breakdayfrc(self.dayfrc)
local tvu = totv(y,m,d,h,r,s) -- get the utc TimeValue of date and time
local tml = osdate("*t", tvu) -- get the local TimeTable of tvu
if (not tml) or (tml.year > (y+1) or tml.year < (y-1)) then -- failed try the magic
y = getequivyear(y)
tvu = totv(y,m,d,h,r,s)
tml = osdate("*t", tvu)
end
local tvl = tmtotv(tml)
if tvu and tvl then
return tvu - tvl, tvu, tvl
else
return error("failed to get bias from utc time")
end
end
-- Returns the bias in seconds of local time daynum and dayfrc
local function getbiasloc2(daynum, dayfrc)
local tvu
-- extract date and time
local y,m,d = breakdaynum(daynum)
local h,r,s = breakdayfrc(dayfrc)
-- get equivalent TimeTable
local tml = {year=y, month=m+1, day=d, hour=h, min=r, sec=s}
-- get equivalent TimeValue
local tvl = tmtotv(tml)
local function chkutc()
tml.isdst = nil; local tvug = ostime(tml) if tvug and (tvl == tmtotv(osdate("*t", tvug))) then tvu = tvug return end
tml.isdst = true; local tvud = ostime(tml) if tvud and (tvl == tmtotv(osdate("*t", tvud))) then tvu = tvud return end
tvu = tvud or tvug
end
chkutc()
if not tvu then
tml.year = getequivyear(y)
tvl = tmtotv(tml)
chkutc()
end
return ((tvu and tvl) and (tvu - tvl)) or error("failed to get bias from local time"), tvu, tvl
end
--#end -- not NO_LOCAL_TIME_SUPPORT
--#if not DATE_OBJECT_AFX then
-- the date parser
local strwalker = {} -- ^Lua regular expression is not as powerful as Perl$
strwalker.__index = strwalker
local function newstrwalker(s)return setmetatable({s=s, i=1, e=1, c=len(s)}, strwalker) end
function strwalker:aimchr() return "\n" .. self.s .. "\n" .. rep(".",self.e-1) .. "^" end
function strwalker:finish() return self.i > self.c end
function strwalker:back() self.i = self.e return self end
function strwalker:restart() self.i, self.e = 1, 1 return self end
function strwalker:match(s) return (find(self.s, s, self.i)) end
function strwalker:__call(s, f)-- print("strwalker:__call "..s..self:aimchr())
local is, ie; is, ie, self[1], self[2], self[3], self[4], self[5] = find(self.s, s, self.i)
if is then self.e, self.i = self.i, 1+ie; if f then f(unpack(self)) end return self end
end
local function date_parse(str)
local y,m,d, h,r,s, z, w,u, j, e, k, x,v,c, chkfin, dn,df;
local sw = newstrwalker(gsub(gsub(str, "(%b())", ""),"^(%s*)","")) -- remove comment, trim leading space
--local function error_out() print(y,m,d,h,r,s) end
local function error_dup(q) --[[error_out()]] error("duplicate value: " .. (q or "") .. sw:aimchr()) end
local function error_syn(q) --[[error_out()]] error("syntax error: " .. (q or "") .. sw:aimchr()) end
local function error_inv(q) --[[error_out()]] error("invalid date: " .. (q or "") .. sw:aimchr()) end
local function sety(q) y = y and error_dup() or tonumber(q); end
local function setm(q) m = (m or w or j) and error_dup(m or w or j) or tonumber(q) end
local function setd(q) d = d and error_dup() or tonumber(q) end
local function seth(q) h = h and error_dup() or tonumber(q) end
local function setr(q) r = r and error_dup() or tonumber(q) end
local function sets(q) s = s and error_dup() or tonumber(q) end
local function adds(q) s = s + tonumber(q) end
local function setj(q) j = (m or w or j) and error_dup() or tonumber(q); end
local function setz(q) z = (z ~= 0 and z) and error_dup() or q end
local function setzn(zs,zn) zn = tonumber(zn); setz( ((zn<24) and (zn*60) or (mod(zn,100) + floor(zn/100) * 60))*( zs=='+' and -1 or 1) ) end
local function setzc(zs,zh,zm) setz( ((tonumber(zh)*60) + tonumber(zm))*( zs=='+' and -1 or 1) ) end
if not (sw("^(%d%d%d%d)",sety) and (sw("^(%-?)(%d%d)%1(%d%d)",function(_,a,b) setm(tonumber(a)); setd(tonumber(b)) end) or sw("^(%-?)[Ww](%d%d)%1(%d?)",function(_,a,b) w, u = tonumber(a), tonumber(b or 1) end) or sw("^%-?(%d%d%d)",setj) or sw("^%-?(%d%d)",function(a) setm(a);setd(1) end))
and ((sw("^%s*[Tt]?(%d%d):?",seth) and sw("^(%d%d):?",setr) and sw("^(%d%d)",sets) and sw("^(%.%d+)",adds))
or sw:finish() or (sw"^%s*$" or sw"^%s*[Zz]%s*$" or sw("^%s-([%+%-])(%d%d):?(%d%d)%s*$",setzc) or sw("^%s*([%+%-])(%d%d)%s*$",setzn))
) )
then --print(y,m,d,h,r,s,z,w,u,j)
sw:restart(); y,m,d,h,r,s,z,w,u,j = nil;
repeat -- print(sw:aimchr())
if sw("^[tT:]?%s*(%d%d?):",seth) then --print("$Time")
_ = sw("^%s*(%d%d?)",setr) and sw("^%s*:%s*(%d%d?)",sets) and sw("^(%.%d+)",adds)
elseif sw("^(%d+)[/\\%s,-]?%s*") then --print("$Digits")
x, c = tonumber(sw[1]), len(sw[1])
if (x >= 70) or (m and d and (not y)) or (c > 3) then
sety( x + ((x >= 100 or c>3)and 0 or 1900) )
else
if m then setd(x) else m = x end
end
elseif sw("^(%a+)[/\\%s,-]?%s*") then --print("$Words")
x = sw[1]
if inlist(x, sl_months, 2, sw) then
if m and (not d) and (not y) then d, m = m, false end
setm(mod(sw[0],12)+1)
elseif inlist(x, sl_timezone, 2, sw) then
c = fix(sw[0]) -- ignore gmt and utc
if c ~= 0 then setz(c, x) end
elseif inlist(x, sl_weekdays, 2, sw) then
k = sw[0]
else
sw:back()
-- am pm bce ad ce bc
if sw("^([bB])%s*(%.?)%s*[Cc]%s*(%2)%s*[Ee]%s*(%2)%s*") or sw("^([bB])%s*(%.?)%s*[Cc]%s*(%2)%s*") then
e = e and error_dup() or -1
elseif sw("^([aA])%s*(%.?)%s*[Dd]%s*(%2)%s*") or sw("^([cC])%s*(%.?)%s*[Ee]%s*(%2)%s*") then
e = e and error_dup() or 1
elseif sw("^([PApa])%s*(%.?)%s*[Mm]?%s*(%2)%s*") then
x = lwr(sw[1]) -- there should be hour and it must be correct
if (not h) or (h > 12) or (h < 0) then return error_inv() end
if x == 'a' and h == 12 then h = 0 end -- am
if x == 'p' and h ~= 12 then h = h + 12 end -- pm
else error_syn() end
end
elseif not(sw("^([+-])(%d%d?):(%d%d)",setzc) or sw("^([+-])(%d+)",setzn) or sw("^[Zz]%s*$")) then -- sw{"([+-])",{"(%d%d?):(%d%d)","(%d+)"}}
error_syn("?")
end
sw("^%s*") until sw:finish()
--else print("$Iso(Date|Time|Zone)")
end
-- if date is given, it must be complete year, month & day
if (not y and not h) or ((m and not d) or (d and not m)) or ((m and w) or (m and j) or (j and w)) then return error_inv("!") end
-- fix month
if m then m = m - 1 end
-- fix year if we are on BCE
if e and e < 0 and y > 0 then y = 1 - y end
-- create date object
dn = (y and ((w and makedaynum_isoywd(y,w,u)) or (j and makedaynum(y, 0, j)) or makedaynum(y, m, d))) or DAYNUM_DEF
df = makedayfrc(h or 0, r or 0, s or 0, 0) + ((z or 0)*TICKSPERMIN)
--print("Zone",h,r,s,z,m,d,y,df)
return date_new(dn, df) -- no need to :normalize();
end
local function date_fromtable(v)
local y, m, d = fix(v.year), getmontharg(v.month), fix(v.day)
local h, r, s, t = tonumber(v.hour), tonumber(v.min), tonumber(v.sec), tonumber(v.ticks)
-- atleast there is time or complete date
if (y or m or d) and (not(y and m and d)) then return error("incomplete table") end
return (y or h or r or s or t) and date_new(y and makedaynum(y, m, d) or DAYNUM_DEF, makedayfrc(h or 0, r or 0, s or 0, t or 0))
end
local tmap = {
['number'] = function(v) return date_epoch:copy():addseconds(v) end,
['string'] = function(v) return date_parse(v) end,
['boolean']= function(v) return date_fromtable(osdate(v and "!*t" or "*t")) end,
['table'] = function(v) local ref = getmetatable(v) == dobj; return ref and v or date_fromtable(v), ref end
}
local function date_getdobj(v)
local o, r = (tmap[type(v)] or fnil)(v);
return (o and o:normalize() or error"invalid date time value"), r -- if r is true then o is a reference to a date obj
end
--#end -- not DATE_OBJECT_AFX
local function date_from(...)
local arg = pack(...)
local y, m, d = fix(arg[1]), getmontharg(arg[2]), fix(arg[3])
local h, r, s, t = tonumber(arg[4] or 0), tonumber(arg[5] or 0), tonumber(arg[6] or 0), tonumber(arg[7] or 0)
if y and m and d and h and r and s and t then
return date_new(makedaynum(y, m, d), makedayfrc(h, r, s, t)):normalize()
else
return date_error_arg()
end
end
--[[ THE DATE OBJECT METHODS ]]--
function dobj:normalize()
local dn, df = fix(self.daynum), self.dayfrc
self.daynum, self.dayfrc = dn + floor(df/TICKSPERDAY), mod(df, TICKSPERDAY)
return (dn >= DAYNUM_MIN and dn <= DAYNUM_MAX) and self or error("date beyond imposed limits:"..self)
end
function dobj:getdate() local y, m, d = breakdaynum(self.daynum) return y, m+1, d end
function dobj:gettime() return breakdayfrc(self.dayfrc) end
function dobj:getclockhour() local h = self:gethours() return h>12 and mod(h,12) or (h==0 and 12 or h) end
function dobj:getyearday() return yearday(self.daynum) + 1 end
function dobj:getweekday() return weekday(self.daynum) + 1 end -- in lua weekday is sunday = 1, monday = 2 ...
function dobj:getyear() local r,_,_ = breakdaynum(self.daynum) return r end
function dobj:getmonth() local _,r,_ = breakdaynum(self.daynum) return r+1 end-- in lua month is 1 base
function dobj:getday() local _,_,r = breakdaynum(self.daynum) return r end
function dobj:gethours() return mod(floor(self.dayfrc/TICKSPERHOUR),HOURPERDAY) end
function dobj:getminutes() return mod(floor(self.dayfrc/TICKSPERMIN), MINPERHOUR) end
function dobj:getseconds() return mod(floor(self.dayfrc/TICKSPERSEC ),SECPERMIN) end
function dobj:getfracsec() return mod(floor(self.dayfrc/TICKSPERSEC ),SECPERMIN)+(mod(self.dayfrc,TICKSPERSEC)/TICKSPERSEC) end
function dobj:getticks(u) local x = mod(self.dayfrc,TICKSPERSEC) return u and ((x*u)/TICKSPERSEC) or x end
function dobj:getweeknumber(wdb)
local wd, yd = weekday(self.daynum), yearday(self.daynum)
if wdb then
wdb = tonumber(wdb)
if wdb then
wd = mod(wd-(wdb-1),7)-- shift the week day base
else
return date_error_arg()
end
end
return (yd < wd and 0) or (floor(yd/7) + ((mod(yd, 7)>=wd) and 1 or 0))
end
function dobj:getisoweekday() return mod(weekday(self.daynum)-1,7)+1 end -- sunday = 7, monday = 1 ...
function dobj:getisoweeknumber() return (isowy(self.daynum)) end
function dobj:getisoyear() return isoy(self.daynum) end
function dobj:getisodate()
local w, y = isowy(self.daynum)
return y, w, self:getisoweekday()
end
function dobj:setisoyear(y, w, d)
local cy, cw, cd = self:getisodate()
if y then cy = fix(tonumber(y))end
if w then cw = fix(tonumber(w))end
if d then cd = fix(tonumber(d))end
if cy and cw and cd then
self.daynum = makedaynum_isoywd(cy, cw, cd)
return self:normalize()
else
return date_error_arg()
end
end
function dobj:setisoweekday(d) return self:setisoyear(nil, nil, d) end
function dobj:setisoweeknumber(w,d) return self:setisoyear(nil, w, d) end
function dobj:setyear(y, m, d)
local cy, cm, cd = breakdaynum(self.daynum)
if y then cy = fix(tonumber(y))end
if m then cm = getmontharg(m) end
if d then cd = fix(tonumber(d))end
if cy and cm and cd then
self.daynum = makedaynum(cy, cm, cd)
return self:normalize()
else
return date_error_arg()
end
end
function dobj:setmonth(m, d)return self:setyear(nil, m, d) end
function dobj:setday(d) return self:setyear(nil, nil, d) end
function dobj:sethours(h, m, s, t)
local ch,cm,cs,ck = breakdayfrc(self.dayfrc)
ch, cm, cs, ck = tonumber(h or ch), tonumber(m or cm), tonumber(s or cs), tonumber(t or ck)
if ch and cm and cs and ck then
self.dayfrc = makedayfrc(ch, cm, cs, ck)
return self:normalize()
else
return date_error_arg()
end
end
function dobj:setminutes(m,s,t) return self:sethours(nil, m, s, t) end
function dobj:setseconds(s, t) return self:sethours(nil, nil, s, t) end
function dobj:setticks(t) return self:sethours(nil, nil, nil, t) end
function dobj:spanticks() return (self.daynum*TICKSPERDAY + self.dayfrc) end
function dobj:spanseconds() return (self.daynum*TICKSPERDAY + self.dayfrc)/TICKSPERSEC end
function dobj:spanminutes() return (self.daynum*TICKSPERDAY + self.dayfrc)/TICKSPERMIN end
function dobj:spanhours() return (self.daynum*TICKSPERDAY + self.dayfrc)/TICKSPERHOUR end
function dobj:spandays() return (self.daynum*TICKSPERDAY + self.dayfrc)/TICKSPERDAY end
function dobj:addyears(y, m, d)
local cy, cm, cd = breakdaynum(self.daynum)
if y then y = fix(tonumber(y))else y = 0 end
if m then m = fix(tonumber(m))else m = 0 end
if d then d = fix(tonumber(d))else d = 0 end
if y and m and d then
self.daynum = makedaynum(cy+y, cm+m, cd+d)
return self:normalize()
else
return date_error_arg()
end
end
function dobj:addmonths(m, d)
return self:addyears(nil, m, d)
end
local function dobj_adddayfrc(self,n,pt,pd)
n = tonumber(n)
if n then
local x = floor(n/pd);
self.daynum = self.daynum + x;
self.dayfrc = self.dayfrc + (n-x*pd)*pt;
return self:normalize()
else
return date_error_arg()
end
end
function dobj:adddays(n) return dobj_adddayfrc(self,n,TICKSPERDAY,1) end
function dobj:addhours(n) return dobj_adddayfrc(self,n,TICKSPERHOUR,HOURPERDAY) end
function dobj:addminutes(n) return dobj_adddayfrc(self,n,TICKSPERMIN,MINPERDAY) end
function dobj:addseconds(n) return dobj_adddayfrc(self,n,TICKSPERSEC,SECPERDAY) end
function dobj:addticks(n) return dobj_adddayfrc(self,n,1,TICKSPERDAY) end
local tvspec = {
-- Abbreviated weekday name (Sun)
['%a']=function(self) return sl_weekdays[weekday(self.daynum) + 7] end,
-- Full weekday name (Sunday)
['%A']=function(self) return sl_weekdays[weekday(self.daynum)] end,
-- Abbreviated month name (Dec)
['%b']=function(self) return sl_months[self:getmonth() - 1 + 12] end,
-- Full month name (December)
['%B']=function(self) return sl_months[self:getmonth() - 1] end,
-- Year/100 (19, 20, 30)
['%C']=function(self) return fmt("%.2d", fix(self:getyear()/100)) end,
-- The day of the month as a number (range 1 - 31)
['%d']=function(self) return fmt("%.2d", self:getday()) end,
-- year for ISO 8601 week, from 00 (79)
['%g']=function(self) return fmt("%.2d", mod(self:getisoyear() ,100)) end,
-- year for ISO 8601 week, from 0000 (1979)
['%G']=function(self) return fmt("%.4d", self:getisoyear()) end,
-- same as %b
['%h']=function(self) return self:fmt0("%b") end,
-- hour of the 24-hour day, from 00 (06)
['%H']=function(self) return fmt("%.2d", self:gethours()) end,
-- The hour as a number using a 12-hour clock (01 - 12)
['%I']=function(self) return fmt("%.2d", self:getclockhour()) end,
-- The day of the year as a number (001 - 366)
['%j']=function(self) return fmt("%.3d", self:getyearday()) end,
-- Month of the year, from 01 to 12
['%m']=function(self) return fmt("%.2d", self:getmonth()) end,
-- Minutes after the hour 55
['%M']=function(self) return fmt("%.2d", self:getminutes())end,
-- AM/PM indicator (AM)
['%p']=function(self) return sl_meridian[self:gethours() > 11 and 1 or -1] end, --AM/PM indicator (AM)
-- The second as a number (59, 20 , 01)
['%S']=function(self) return fmt("%.2d", self:getseconds()) end,
-- ISO 8601 day of the week, to 7 for Sunday (7, 1)
['%u']=function(self) return self:getisoweekday() end,
-- Sunday week of the year, from 00 (48)
['%U']=function(self) return fmt("%.2d", self:getweeknumber()) end,
-- ISO 8601 week of the year, from 01 (48)
['%V']=function(self) return fmt("%.2d", self:getisoweeknumber()) end,
-- The day of the week as a decimal, Sunday being 0
['%w']=function(self) return self:getweekday() - 1 end,
-- Monday week of the year, from 00 (48)
['%W']=function(self) return fmt("%.2d", self:getweeknumber(2)) end,
-- The year as a number without a century (range 00 to 99)
['%y']=function(self) return fmt("%.2d", mod(self:getyear() ,100)) end,
-- Year with century (2000, 1914, 0325, 0001)
['%Y']=function(self) return fmt("%.4d", self:getyear()) end,
-- Time zone offset, the date object is assumed local time (+1000, -0230)
['%z']=function(self) local b = -self:getbias(); local x = abs(b); return fmt("%s%.4d", b < 0 and "-" or "+", fix(x/60)*100 + floor(mod(x,60))) end,
-- Time zone name, the date object is assumed local time
['%Z']=function(self) return self:gettzname() end,
-- Misc --
-- Year, if year is in BCE, prints the BCE Year representation, otherwise result is similar to "%Y" (1 BCE, 40 BCE)
['%\b']=function(self) local x = self:getyear() return fmt("%.4d%s", x>0 and x or (-x+1), x>0 and "" or " BCE") end,
-- Seconds including fraction (59.998, 01.123)
['%\f']=function(self) local x = self:getfracsec() return fmt("%s%.9f",x >= 10 and "" or "0", x) end,
-- percent character %
['%%']=function(self) return "%" end,
-- Group Spec --
-- 12-hour time, from 01:00:00 AM (06:55:15 AM); same as "%I:%M:%S %p"
['%r']=function(self) return self:fmt0("%I:%M:%S %p") end,
-- hour:minute, from 01:00 (06:55); same as "%I:%M"
['%R']=function(self) return self:fmt0("%I:%M") end,
-- 24-hour time, from 00:00:00 (06:55:15); same as "%H:%M:%S"
['%T']=function(self) return self:fmt0("%H:%M:%S") end,
-- month/day/year from 01/01/00 (12/02/79); same as "%m/%d/%y"
['%D']=function(self) return self:fmt0("%m/%d/%y") end,
-- year-month-day (1979-12-02); same as "%Y-%m-%d"
['%F']=function(self) return self:fmt0("%Y-%m-%d") end,
-- The preferred date and time representation; same as "%x %X"
['%c']=function(self) return self:fmt0("%x %X") end,
-- The preferred date representation, same as "%a %b %d %\b"
['%x']=function(self) return self:fmt0("%a %b %d %\b") end,
-- The preferred time representation, same as "%H:%M:%\f"
['%X']=function(self) return self:fmt0("%H:%M:%\f") end,
-- GroupSpec --
-- Iso format, same as "%Y-%m-%dT%T"
['${iso}'] = function(self) return self:fmt0("%Y-%m-%dT%T") end,
-- http format, same as "%a, %d %b %Y %T GMT"
['${http}'] = function(self) return self:fmt0("%a, %d %b %Y %T GMT") end,
-- ctime format, same as "%a %b %d %T GMT %Y"
['${ctime}'] = function(self) return self:fmt0("%a %b %d %T GMT %Y") end,
-- RFC850 format, same as "%A, %d-%b-%y %T GMT"
['${rfc850}'] = function(self) return self:fmt0("%A, %d-%b-%y %T GMT") end,
-- RFC1123 format, same as "%a, %d %b %Y %T GMT"
['${rfc1123}'] = function(self) return self:fmt0("%a, %d %b %Y %T GMT") end,
-- asctime format, same as "%a %b %d %T %Y"
['${asctime}'] = function(self) return self:fmt0("%a %b %d %T %Y") end,
}
function dobj:fmt0(str) return (gsub(str, "%%[%a%%\b\f]", function(x) local f = tvspec[x];return (f and f(self)) or x end)) end
function dobj:fmt(str)
str = str or self.fmtstr or fmtstr
return self:fmt0((gmatch(str, "${%w+}")) and (gsub(str, "${%w+}", function(x)local f=tvspec[x];return (f and f(self)) or x end)) or str)
end
dobj.format = dobj.fmt
function dobj.__lt(a, b) if (a.daynum == b.daynum) then return (a.dayfrc < b.dayfrc) else return (a.daynum < b.daynum) end end
function dobj.__le(a, b) if (a.daynum == b.daynum) then return (a.dayfrc <= b.dayfrc) else return (a.daynum <= b.daynum) end end
function dobj.__eq(a, b)return (a.daynum == b.daynum) and (a.dayfrc == b.dayfrc) end
function dobj.__sub(a,b)
local d1, d2 = date_getdobj(a), date_getdobj(b)
local d0 = d1 and d2 and date_new(d1.daynum - d2.daynum, d1.dayfrc - d2.dayfrc)
return d0 and d0:normalize()
end
function dobj.__add(a,b)
local d1, d2 = date_getdobj(a), date_getdobj(b)
local d0 = d1 and d2 and date_new(d1.daynum + d2.daynum, d1.dayfrc + d2.dayfrc)
return d0 and d0:normalize()
end
function dobj.__concat(a, b) return tostring(a) .. tostring(b) end
function dobj:__tostring() return self:fmt() end
function dobj:copy() return date_new(self.daynum, self.dayfrc) end
--[[ THE LOCAL DATE OBJECT METHODS ]]--
function dobj:tolocal()
local dn,df = self.daynum, self.dayfrc
local bias = getbiasutc2(self)
if bias then
-- utc = local + bias; local = utc - bias
self.daynum = dn
self.dayfrc = df - bias*TICKSPERSEC
return self:normalize()
else
return nil
end
end
function dobj:toutc()
local dn,df = self.daynum, self.dayfrc
local bias = getbiasloc2(dn, df)
if bias then
-- utc = local + bias;
self.daynum = dn
self.dayfrc = df + bias*TICKSPERSEC
return self:normalize()
else
return nil
end
end
function dobj:getbias() return (getbiasloc2(self.daynum, self.dayfrc))/SECPERMIN end
function dobj:gettzname()
local _, tvu, _ = getbiasloc2(self.daynum, self.dayfrc)
return tvu and osdate("%Z",tvu) or ""
end
--#if not DATE_OBJECT_AFX then
function date.time(h, r, s, t)
h, r, s, t = tonumber(h or 0), tonumber(r or 0), tonumber(s or 0), tonumber(t or 0)
if h and r and s and t then
return date_new(DAYNUM_DEF, makedayfrc(h, r, s, t))
else
return date_error_arg()
end
end
function date:__call(...)
local arg = pack(...)
if arg.n > 1 then return (date_from(...))
elseif arg.n == 0 then return (date_getdobj(false))
else local o, r = date_getdobj(arg[1]); return r and o:copy() or o end
end
date.diff = dobj.__sub
function date.isleapyear(v)
local y = fix(v);
if not y then
y = date_getdobj(v)
y = y and y:getyear()
end
return isleapyear(y+0)
end
function date.epoch() return date_epoch:copy() end
function date.isodate(y,w,d) return date_new(makedaynum_isoywd(y + 0, w and (w+0) or 1, d and (d+0) or 1), 0) end
-- Internal functions
function date.fmt(str) if str then fmtstr = str end; return fmtstr end
function date.daynummin(n) DAYNUM_MIN = (n and n < DAYNUM_MAX) and n or DAYNUM_MIN return n and DAYNUM_MIN or date_new(DAYNUM_MIN, 0):normalize()end
function date.daynummax(n) DAYNUM_MAX = (n and n > DAYNUM_MIN) and n or DAYNUM_MAX return n and DAYNUM_MAX or date_new(DAYNUM_MAX, 0):normalize()end
function date.ticks(t) if t then setticks(t) end return TICKSPERSEC end
--#end -- not DATE_OBJECT_AFX
local tm = osdate("!*t", 0);
if tm then
date_epoch = date_new(makedaynum(tm.year, tm.month - 1, tm.day), makedayfrc(tm.hour, tm.min, tm.sec, 0))
-- the distance from our epoch to os epoch in daynum
DATE_EPOCH = date_epoch and date_epoch:spandays()
else -- error will be raise only if called!
date_epoch = setmetatable({},{__index = function() error("failed to get the epoch date") end})
end
function date.serialize(object)
return {tostring(object.daynum), tostring(object.dayfrc)}
end
function date.construct(object)
return date_isdobj(object) or (object.daynum and date_new(object.daynum, object.dayfrc) or date_new(object[1], object[2]))
end
--#if not DATE_OBJECT_AFX then
return date
--#else
--$return date_from
--#end

View File

@@ -0,0 +1,193 @@
--[[
| 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 middleclass = {
_VERSION = 'middleclass v4.1.1',
_DESCRIPTION = 'Object Orientation for Lua',
_URL = 'https://github.com/kikito/middleclass',
_LICENSE = [[
MIT LICENSE
Copyright (c) 2011 Enrique García Cota
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
}
local function _createIndexWrapper(aClass, f)
if f == nil then
return aClass.__instanceDict
else
return function(self, name)
local value = aClass.__instanceDict[name]
if value ~= nil then
return value
elseif type(f) == "function" then
return (f(self, name))
else
return f[name]
end
end
end
end
local function _propagateInstanceMethod(aClass, name, f)
f = name == "__index" and _createIndexWrapper(aClass, f) or f
aClass.__instanceDict[name] = f
for subclass in pairs(aClass.subclasses) do
if rawget(subclass.__declaredMethods, name) == nil then
_propagateInstanceMethod(subclass, name, f)
end
end
end
local function _declareInstanceMethod(aClass, name, f)
aClass.__declaredMethods[name] = f
if f == nil and aClass.super then
f = aClass.super.__instanceDict[name]
end
_propagateInstanceMethod(aClass, name, f)
end
local function _tostring(self) return "class " .. self.name end
local function _call(self, ...) return self:New(...) end
local function _createClass(name, super)
local dict = {}
dict.__index = dict
local aClass = { name = name, super = super, static = {},
__instanceDict = dict, __declaredMethods = {},
subclasses = setmetatable({}, {__mode='k'}) }
if super then
setmetatable(aClass.static, {
__index = function(_,k)
local result = rawget(dict,k)
if result == nil then
return super.static[k]
end
return result
end
})
else
setmetatable(aClass.static, { __index = function(_,k) return rawget(dict,k) end })
end
setmetatable(aClass, { __index = aClass.static, __tostring = _tostring,
__call = _call, __newindex = _declareInstanceMethod })
return aClass
end
local function _includeMixin(aClass, mixin)
assert(type(mixin) == 'table', "mixin must be a table")
for name,method in pairs(mixin) do
if name ~= "Included" and name ~= "static" then aClass[name] = method end
end
for name,method in pairs(mixin.static or {}) do
aClass.static[name] = method
end
if type(mixin.Included)=="function" then mixin:Included(aClass) end
return aClass
end
local DefaultMixin = {
__tostring = function(self) return "instance of " .. tostring(self.class) end,
Initialize = function(self, ...) end,
IsInstanceOf = function(self, aClass)
return type(aClass) == 'table'
and type(self) == 'table'
and (self.class == aClass
or type(self.class) == 'table'
and type(self.class.IsSubclassOf) == 'function'
and self.class:IsSubclassOf(aClass))
end,
static = {
Allocate = function(self)
assert(type(self) == 'table', "Make sure that you are using 'Class:Allocate' instead of 'Class.Allocate'")
return setmetatable({ class = self }, self.__instanceDict)
end,
New = function(self, ...)
assert(type(self) == 'table', "Make sure that you are using 'Class:New' instead of 'Class.New'")
local instance = self:Allocate()
instance:Initialize(...)
return instance
end,
Subclass = function(self, name)
assert(type(self) == 'table', "Make sure that you are using 'Class:Subclass' instead of 'Class.Subclass'")
assert(type(name) == "string", "You must provide a name(string) for your class")
local subclass = _createClass(name, self)
for methodName, f in pairs(self.__instanceDict) do
_propagateInstanceMethod(subclass, methodName, f)
end
subclass.Initialize = function(instance, ...) return self.Initialize(instance, ...) end
self.subclasses[subclass] = true
self:Subclassed(subclass)
return subclass
end,
Subclassed = function(self, other) end,
IsSubclassOf = function(self, other)
return type(other) == 'table' and
type(self.super) == 'table' and
( self.super == other or self.super:IsSubclassOf(other) )
end,
Include = function(self, ...)
assert(type(self) == 'table', "Make sure you that you are using 'Class:Include' instead of 'Class.Include'")
for _,mixin in ipairs({...}) do _includeMixin(self, mixin) end
return self
end
}
}
function middleclass.class(name, super)
assert(type(name) == 'string', "A name (string) is needed for the new class")
return super and super:Subclass(name) or _includeMixin(_createClass(name), DefaultMixin)
end
setmetatable(middleclass, { __call = function(_, ...) return middleclass.class(...) end })
ix.middleclass = middleclass

View File

@@ -0,0 +1,174 @@
--[[
| 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/
--]]
--[[
NetStream - 2.0.0
Alexander Grist-Hucker
http://www.revotech.org
Credits to:
thelastpenguin for pON.
https://github.com/thelastpenguin/gLUA-Library/tree/master/pON
--]]
AddCSLuaFile();
local _player = player
netstream = netstream or {};
netstream.stored = netstream.stored or {};
-- A function to split data for a data stream.
function netstream.Split(data)
local index = 1;
local result = {};
local buffer = {};
for i = 0, string.len(data) do
buffer[#buffer + 1] = string.sub(data, i, i);
if (#buffer == 32768) then
result[#result + 1] = table.concat(buffer);
index = index + 1;
buffer = {};
end;
end;
result[#result + 1] = table.concat(buffer);
return result;
end;
-- A function to hook a data stream.
function netstream.Hook(name, Callback)
netstream.stored[name] = Callback;
end;
if (SERVER) then
util.AddNetworkString("NetStreamDS");
-- A function to start a net stream.
function netstream.Start(player, name, ...)
local recipients = {};
local bShouldSend = false;
local bSendPVS = false;
if (type(player) != "table") then
if (!player) then
player = _player.GetAll();
elseif (type(player) == "Vector") then
bSendPVS = true;
else
player = {player};
end;
end;
if (type(player) != "Vector") then
for k, v in pairs(player) do
if (type(v) == "Player") then
recipients[#recipients + 1] = v;
bShouldSend = true;
elseif (type(k) == "Player") then
recipients[#recipients + 1] = k;
bShouldSend = true;
end;
end;
else
bShouldSend = true;
end;
local dataTable = {...};
local encodedData = pon.encode(dataTable);
if (encodedData and #encodedData > 0 and bShouldSend) then
net.Start("NetStreamDS");
net.WriteString(name);
net.WriteUInt(#encodedData, 32);
net.WriteData(encodedData, #encodedData);
if (bSendPVS) then
net.SendPVS(player);
else
net.Send(recipients);
end;
end;
end;
net.Receive("NetStreamDS", function(length, player)
local NS_DS_NAME = net.ReadString();
local NS_DS_LENGTH = net.ReadUInt(32);
local NS_DS_DATA = net.ReadData(NS_DS_LENGTH);
if (NS_DS_NAME and NS_DS_DATA and NS_DS_LENGTH) then
player.nsDataStreamName = NS_DS_NAME;
player.nsDataStreamData = "";
if (player.nsDataStreamName and player.nsDataStreamData) then
player.nsDataStreamData = NS_DS_DATA;
if (netstream.stored[player.nsDataStreamName]) then
local bStatus, value = pcall(pon.decode, player.nsDataStreamData);
if (bStatus) then
netstream.stored[player.nsDataStreamName](player, unpack(value));
else
ErrorNoHalt("NetStream: '"..NS_DS_NAME.."'\n"..value.."\n");
end;
else
ErrorNoHalt("NetStream: Undefined hook for '"..NS_DS_NAME.."'\n")
end;
player.nsDataStreamName = nil;
player.nsDataStreamData = nil;
end;
end;
NS_DS_NAME, NS_DS_DATA, NS_DS_LENGTH = nil, nil, nil;
end);
else
-- A function to start a net stream.
function netstream.Start(name, ...)
local dataTable = {...};
local encodedData = pon.encode(dataTable);
if (encodedData and #encodedData > 0) then
net.Start("NetStreamDS");
net.WriteString(name);
net.WriteUInt(#encodedData, 32);
net.WriteData(encodedData, #encodedData);
net.SendToServer();
end;
end;
net.Receive("NetStreamDS", function(length)
local NS_DS_NAME = net.ReadString();
local NS_DS_LENGTH = net.ReadUInt(32);
local NS_DS_DATA = net.ReadData(NS_DS_LENGTH);
if (NS_DS_NAME and NS_DS_DATA and NS_DS_LENGTH) then
if (netstream.stored[NS_DS_NAME]) then
local bStatus, value = pcall(pon.decode, NS_DS_DATA);
if (bStatus) then
netstream.stored[NS_DS_NAME](unpack(value));
else
ErrorNoHalt("NetStream: '"..NS_DS_NAME.."'\n"..value.."\n");
end;
else
ErrorNoHalt("NetSteam: Undefined hook for '"..NS_DS_NAME.."'\n")
end;
end;
NS_DS_NAME, NS_DS_DATA, NS_DS_LENGTH = nil, nil, nil;
end);
end;

View File

@@ -0,0 +1,411 @@
--[[
| 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/
--]]
--[[
DEVELOPMENTAL VERSION;
VERSION 1.2.2
Copyright thelastpenguin™
You may use this for any purpose as long as:
- You don't remove this copyright notice.
- You don't claim this to be your own.
- You properly credit the author, thelastpenguin™, if you publish your work based on (and/or using) this.
If you modify the code for any purpose, the above still applies to the modified code.
The author is not held responsible for any damages incured from the use of pon, you use it at your own risk.
DATA TYPES SUPPORTED:
- tables - k,v - pointers
- strings - k,v - pointers
- numbers - k,v
- booleans- k,v
- Vectors - k,v
- Angles - k,v
- Entities- k,v
- Players - k,v
CHANGE LOG
V 1.1.0
- Added Vehicle, NPC, NextBot, Player, Weapon
V 1.2.0
- Added custom handling for k,v tables without any array component.
V 1.2.1
- fixed deserialization bug.
THANKS TO...
- VERCAS for the inspiration.
]]
local pon = {};
_G.pon = pon;
local type, count = type, table.Count ;
local tonumber = tonumber ;
local format = string.format;
do
local type, count = type, table.Count ;
local tonumber = tonumber ;
local format = string.format;
local encode = {};
local tryCache ;
local cacheSize = 0;
encode['table'] = function( self, tbl, output, cache )
if( cache[ tbl ] )then
output[ #output + 1 ] = format('(%x)', cache[tbl] );
return ;
else
cacheSize = cacheSize + 1;
cache[ tbl ] = cacheSize;
end
local first = next(tbl, nil)
local predictedNumeric = 1
local lastKey = nil
-- starts with a numeric dealio
if first == 1 then
output[#output + 1] = '{'
for k,v in next, tbl do
if k == predictedNumeric then
predictedNumeric = predictedNumeric + 1
local tv = type(v)
if tv == 'string' then
local pid = cache[v]
if pid then
output[#output + 1] = format('(%x)', pid)
else
cacheSize = cacheSize + 1
cache[v] = cacheSize
self.string(self, v, output, cache)
end
else
self[tv](self, v, output, cache)
end
else
break
end
end
predictedNumeric = predictedNumeric - 1
else
predictedNumeric = nil
end
if predictedNumeric == nil then
output[#output + 1] = '[' -- no array component
else
output[#output + 1] = '~' -- array component came first so shit needs to happen
end
for k, v in next, tbl, predictedNumeric do
local tk, tv = type(k), type(v)
-- WRITE KEY
if tk == 'string' then
local pid = cache[ k ];
if( pid )then
output[ #output + 1 ] = format('(%x)', pid );
else
cacheSize = cacheSize + 1;
cache[ k ] = cacheSize;
self.string( self, k, output, cache );
end
else
self[tk](self, k, output, cache)
end
-- WRITE VALUE
if( tv == 'string' )then
local pid = cache[ v ];
if( pid )then
output[ #output + 1 ] = format('(%x)', pid );
else
cacheSize = cacheSize + 1;
cache[ v ] = cacheSize;
self.string( self, v, output, cache );
end
else
self[ tv ]( self, v, output, cache );
end
end
output[#output + 1] = '}'
end
-- ENCODE STRING
local gsub = string.gsub ;
encode['string'] = function( self, str, output )
--if tryCache( str, output ) then return end
local estr, count = gsub( str, ";", "\\;");
if( count == 0 )then
output[ #output + 1 ] = '\''..str..';';
else
output[ #output + 1 ] = '"'..estr..'";';
end
end
-- ENCODE NUMBER
encode['number'] = function( self, num, output )
if num%1 == 0 then
if num < 0 then
output[ #output + 1 ] = format( 'x%x;', -num );
else
output[ #output + 1 ] = format('X%x;', num );
end
else
output[ #output + 1 ] = tonumber( num )..';';
end
end
-- ENCODE BOOLEAN
encode['boolean'] = function( self, val, output )
output[ #output + 1 ] = val and 't' or 'f'
end
-- ENCODE VECTOR
encode['Vector'] = function( self, val, output )
output[ #output + 1 ] = ('v'..val.x..','..val.y)..(','..val.z..';');
end
-- ENCODE ANGLE
encode['Angle'] = function( self, val, output )
output[ #output + 1 ] = ('a'..val.p..','..val.y)..(','..val.r..';');
end
encode['Entity'] = function( self, val, output )
output[ #output + 1] = 'E'..(IsValid( val ) and (val:EntIndex( )..';') or '#');
end
encode['Player'] = encode['Entity'];
encode['Vehicle'] = encode['Entity'];
encode['Weapon'] = encode['Entity'];
encode['NPC'] = encode['Entity'];
encode['NextBot'] = encode['Entity'];
encode['PhysObj'] = encode['Entity'];
encode['nil'] = function()
output[ #output + 1 ] = '?';
end
encode.__index = function( key )
ErrorNoHalt('Type: '..key..' can not be encoded. Encoded as as pass-over value.');
return encode['nil'];
end
do
local empty, concat = table.Empty, table.concat ;
function pon.encode( tbl )
local output = {};
cacheSize = 0;
encode[ 'table' ]( encode, tbl, output, {} );
local res = concat( output );
return res;
end
end
end
do
local tonumber = tonumber ;
local find, sub, gsub, Explode = string.find, string.sub, string.gsub, string.Explode ;
local Vector, Angle, Entity = Vector, Angle, Entity ;
local decode = {};
decode['{'] = function( self, index, str, cache )
local cur = {};
cache[ #cache + 1 ] = cur;
local k, v, tk, tv = 1, nil, nil, nil;
while( true )do
tv = sub( str, index, index );
if( not tv or tv == '~' )then
index = index + 1;
break ;
end
if( tv == '}' )then
return index + 1, cur;
end
-- READ THE VALUE
index = index + 1;
index, v = self[ tv ]( self, index, str, cache );
cur[ k ] = v;
k = k + 1;
end
while( true )do
tk = sub( str, index, index );
if( not tk or tk == '}' )then
index = index + 1;
break ;
end
-- READ THE KEY
index = index + 1;
index, k = self[ tk ]( self, index, str, cache );
-- READ THE VALUE
tv = sub( str, index, index );
index = index + 1;
index, v = self[ tv ]( self, index, str, cache );
cur[ k ] = v;
end
return index, cur;
end
decode['['] = function( self, index, str, cache )
local cur = {};
cache[ #cache + 1 ] = cur;
local k, v, tk, tv = 1, nil, nil, nil;
while( true )do
tk = sub( str, index, index );
if( not tk or tk == '}' )then
index = index + 1;
break ;
end
-- READ THE KEY
index = index + 1;
index, k = self[ tk ]( self, index, str, cache );
if not k then continue end
-- READ THE VALUE
tv = sub( str, index, index );
index = index + 1;
if not self[tv] then
print('did not find type: '..tv)
end
index, v = self[ tv ]( self, index, str, cache );
cur[ k ] = v;
end
return index, cur;
end
-- STRING
decode['"'] = function( self, index, str, cache )
local finish = find( str, '";', index, true );
local res = gsub( sub( str, index, finish - 1 ), '\\;', ';' );
index = finish + 2;
cache[ #cache + 1 ] = res;
return index, res;
end
-- STRING NO ESCAPING NEEDED
decode['\''] = function( self, index, str, cache )
local finish = find( str, ';', index, true );
local res = sub( str, index, finish - 1 )
index = finish + 1;
cache[ #cache + 1 ] = res;
return index, res;
end
-- NUMBER
decode['n'] = function( self, index, str, cache )
index = index - 1;
local finish = find( str, ';', index, true );
local num = tonumber( sub( str, index, finish - 1 ) );
index = finish + 1;
return index, num;
end
decode['0'] = decode['n'];
decode['1'] = decode['n'];
decode['2'] = decode['n'];
decode['3'] = decode['n'];
decode['4'] = decode['n'];
decode['5'] = decode['n'];
decode['6'] = decode['n'];
decode['7'] = decode['n'];
decode['8'] = decode['n'];
decode['9'] = decode['n'];
decode['-'] = decode['n'];
-- positive hex
decode['X'] = function( self, index, str, cache )
local finish = find( str, ';', index, true );
local num = tonumber( sub( str, index, finish - 1), 16 );
index = finish + 1;
return index, num;
end
-- negative hex
decode['x'] = function( self, index, str, cache )
local finish = find( str, ';', index, true );
local num = -tonumber( sub( str, index, finish - 1), 16 );
index = finish + 1;
return index, num;
end
-- POINTER
decode['('] = function( self, index, str, cache )
local finish = find( str, ')', index, true );
local num = tonumber( sub( str, index, finish - 1), 16 );
index = finish + 1;
return index, cache[ num ];
end
-- BOOLEAN. ONE DATA TYPE FOR YES, ANOTHER FOR NO.
decode[ 't' ] = function( self, index )
return index, true;
end
decode[ 'f' ] = function( self, index )
return index, false;
end
-- VECTOR
decode[ 'v' ] = function( self, index, str, cache )
local finish = find( str, ';', index, true );
local vecStr = sub( str, index, finish - 1 );
index = finish + 1; -- update the index.
local segs = Explode( ',', vecStr, false );
return index, Vector( tonumber( segs[1] ), tonumber( segs[2] ), tonumber( segs[3] ) );
end
-- ANGLE
decode[ 'a' ] = function( self, index, str, cache )
local finish = find( str, ';', index, true );
local angStr = sub( str, index, finish - 1 );
index = finish + 1; -- update the index.
local segs = Explode( ',', angStr, false );
return index, Angle( tonumber( segs[1] ), tonumber( segs[2] ), tonumber( segs[3] ) );
end
-- ENTITY
decode[ 'E' ] = function( self, index, str, cache )
if( str[index] == '#' )then
index = index + 1;
return index, NULL ;
else
local finish = find( str, ';', index, true );
local num = tonumber( sub( str, index, finish - 1 ) );
index = finish + 1;
return index, Entity( num );
end
end
-- PLAYER
decode[ 'P' ] = function( self, index, str, cache )
local finish = find( str, ';', index, true );
local num = tonumber( sub( str, index, finish - 1 ) );
index = finish + 1;
return index, Entity( num ) or NULL;
end
-- NIL
decode['?'] = function( self, index, str, cache )
return index + 1, nil;
end
function pon.decode( data )
local _, res = decode[sub(data,1,1)]( decode, 2, data, {});
return res;
end
end

View File

@@ -0,0 +1,385 @@
--[[
| 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 tween = {
_VERSION = 'tween 2.1.1',
_DESCRIPTION = 'tweening for lua',
_URL = 'https://github.com/kikito/tween.lua',
_LICENSE = [[
MIT LICENSE
Copyright (c) 2014 Enrique García Cota, Yuichi Tateno, Emmanuel Oga
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
}
-- easing
-- Adapted from https://github.com/EmmanuelOga/easing. See LICENSE.txt for credits.
-- For all easing functions:
-- t = time == how much time has to pass for the tweening to complete
-- b = begin == starting property value
-- c = change == ending - beginning
-- d = duration == running time. How much time has passed *right now*
local pow, sin, cos, pi, sqrt, abs, asin = math.pow, math.sin, math.cos, math.pi, math.sqrt, math.abs, math.asin
-- linear
local function linear(t, b, c, d) return c * t / d + b end
-- quad
local function inQuad(t, b, c, d) return c * pow(t / d, 2) + b end
local function outQuad(t, b, c, d)
t = t / d
return -c * t * (t - 2) + b
end
local function inOutQuad(t, b, c, d)
t = t / d * 2
if t < 1 then return c / 2 * pow(t, 2) + b end
return -c / 2 * ((t - 1) * (t - 3) - 1) + b
end
local function outInQuad(t, b, c, d)
if t < d / 2 then return outQuad(t * 2, b, c / 2, d) end
return inQuad((t * 2) - d, b + c / 2, c / 2, d)
end
-- cubic
local function inCubic (t, b, c, d) return c * pow(t / d, 3) + b end
local function outCubic(t, b, c, d) return c * (pow(t / d - 1, 3) + 1) + b end
local function inOutCubic(t, b, c, d)
t = t / d * 2
if t < 1 then return c / 2 * t * t * t + b end
t = t - 2
return c / 2 * (t * t * t + 2) + b
end
local function outInCubic(t, b, c, d)
if t < d / 2 then return outCubic(t * 2, b, c / 2, d) end
return inCubic((t * 2) - d, b + c / 2, c / 2, d)
end
-- quart
local function inQuart(t, b, c, d) return c * pow(t / d, 4) + b end
local function outQuart(t, b, c, d) return -c * (pow(t / d - 1, 4) - 1) + b end
local function inOutQuart(t, b, c, d)
t = t / d * 2
if t < 1 then return c / 2 * pow(t, 4) + b end
return -c / 2 * (pow(t - 2, 4) - 2) + b
end
local function outInQuart(t, b, c, d)
if t < d / 2 then return outQuart(t * 2, b, c / 2, d) end
return inQuart((t * 2) - d, b + c / 2, c / 2, d)
end
-- quint
local function inQuint(t, b, c, d) return c * pow(t / d, 5) + b end
local function outQuint(t, b, c, d) return c * (pow(t / d - 1, 5) + 1) + b end
local function inOutQuint(t, b, c, d)
t = t / d * 2
if t < 1 then return c / 2 * pow(t, 5) + b end
return c / 2 * (pow(t - 2, 5) + 2) + b
end
local function outInQuint(t, b, c, d)
if t < d / 2 then return outQuint(t * 2, b, c / 2, d) end
return inQuint((t * 2) - d, b + c / 2, c / 2, d)
end
-- sine
local function inSine(t, b, c, d) return -c * cos(t / d * (pi / 2)) + c + b end
local function outSine(t, b, c, d) return c * sin(t / d * (pi / 2)) + b end
local function inOutSine(t, b, c, d) return -c / 2 * (cos(pi * t / d) - 1) + b end
local function outInSine(t, b, c, d)
if t < d / 2 then return outSine(t * 2, b, c / 2, d) end
return inSine((t * 2) -d, b + c / 2, c / 2, d)
end
-- expo
local function inExpo(t, b, c, d)
if t == 0 then return b end
return c * pow(2, 10 * (t / d - 1)) + b - c * 0.001
end
local function outExpo(t, b, c, d)
if t == d then return b + c end
return c * 1.001 * (-pow(2, -10 * t / d) + 1) + b
end
local function inOutExpo(t, b, c, d)
if t == 0 then return b end
if t == d then return b + c end
t = t / d * 2
if t < 1 then return c / 2 * pow(2, 10 * (t - 1)) + b - c * 0.0005 end
return c / 2 * 1.0005 * (-pow(2, -10 * (t - 1)) + 2) + b
end
local function outInExpo(t, b, c, d)
if t < d / 2 then return outExpo(t * 2, b, c / 2, d) end
return inExpo((t * 2) - d, b + c / 2, c / 2, d)
end
-- circ
local function inCirc(t, b, c, d) return(-c * (sqrt(1 - pow(t / d, 2)) - 1) + b) end
local function outCirc(t, b, c, d) return(c * sqrt(1 - pow(t / d - 1, 2)) + b) end
local function inOutCirc(t, b, c, d)
t = t / d * 2
if t < 1 then return -c / 2 * (sqrt(1 - t * t) - 1) + b end
t = t - 2
return c / 2 * (sqrt(1 - t * t) + 1) + b
end
local function outInCirc(t, b, c, d)
if t < d / 2 then return outCirc(t * 2, b, c / 2, d) end
return inCirc((t * 2) - d, b + c / 2, c / 2, d)
end
-- elastic
local function calculatePAS(p,a,c,d)
p, a = p or d * 0.3, a or 0
if a < abs(c) then return p, c, p / 4 end -- p, a, s
return p, a, p / (2 * pi) * asin(c/a) -- p,a,s
end
local function inElastic(t, b, c, d, a, p)
local s
if t == 0 then return b end
t = t / d
if t == 1 then return b + c end
p,a,s = calculatePAS(p,a,c,d)
t = t - 1
return -(a * pow(2, 10 * t) * sin((t * d - s) * (2 * pi) / p)) + b
end
local function outElastic(t, b, c, d, a, p)
local s
if t == 0 then return b end
t = t / d
if t == 1 then return b + c end
p,a,s = calculatePAS(p,a,c,d)
return a * pow(2, -10 * t) * sin((t * d - s) * (2 * pi) / p) + c + b
end
local function inOutElastic(t, b, c, d, a, p)
local s
if t == 0 then return b end
t = t / d * 2
if t == 2 then return b + c end
p,a,s = calculatePAS(p,a,c,d)
t = t - 1
if t < 0 then return -0.5 * (a * pow(2, 10 * t) * sin((t * d - s) * (2 * pi) / p)) + b end
return a * pow(2, -10 * t) * sin((t * d - s) * (2 * pi) / p ) * 0.5 + c + b
end
local function outInElastic(t, b, c, d, a, p)
if t < d / 2 then return outElastic(t * 2, b, c / 2, d, a, p) end
return inElastic((t * 2) - d, b + c / 2, c / 2, d, a, p)
end
-- back
local function inBack(t, b, c, d, s)
s = s or 1.70158
t = t / d
return c * t * t * ((s + 1) * t - s) + b
end
local function outBack(t, b, c, d, s)
s = s or 1.70158
t = t / d - 1
return c * (t * t * ((s + 1) * t + s) + 1) + b
end
local function inOutBack(t, b, c, d, s)
s = (s or 1.70158) * 1.525
t = t / d * 2
if t < 1 then return c / 2 * (t * t * ((s + 1) * t - s)) + b end
t = t - 2
return c / 2 * (t * t * ((s + 1) * t + s) + 2) + b
end
local function outInBack(t, b, c, d, s)
if t < d / 2 then return outBack(t * 2, b, c / 2, d, s) end
return inBack((t * 2) - d, b + c / 2, c / 2, d, s)
end
-- bounce
local function outBounce(t, b, c, d)
t = t / d
if t < 1 / 2.75 then return c * (7.5625 * t * t) + b end
if t < 2 / 2.75 then
t = t - (1.5 / 2.75)
return c * (7.5625 * t * t + 0.75) + b
elseif t < 2.5 / 2.75 then
t = t - (2.25 / 2.75)
return c * (7.5625 * t * t + 0.9375) + b
end
t = t - (2.625 / 2.75)
return c * (7.5625 * t * t + 0.984375) + b
end
local function inBounce(t, b, c, d) return c - outBounce(d - t, 0, c, d) + b end
local function inOutBounce(t, b, c, d)
if t < d / 2 then return inBounce(t * 2, 0, c, d) * 0.5 + b end
return outBounce(t * 2 - d, 0, c, d) * 0.5 + c * .5 + b
end
local function outInBounce(t, b, c, d)
if t < d / 2 then return outBounce(t * 2, b, c / 2, d) end
return inBounce((t * 2) - d, b + c / 2, c / 2, d)
end
tween.easing = {
linear = linear,
inQuad = inQuad, outQuad = outQuad, inOutQuad = inOutQuad, outInQuad = outInQuad,
inCubic = inCubic, outCubic = outCubic, inOutCubic = inOutCubic, outInCubic = outInCubic,
inQuart = inQuart, outQuart = outQuart, inOutQuart = inOutQuart, outInQuart = outInQuart,
inQuint = inQuint, outQuint = outQuint, inOutQuint = inOutQuint, outInQuint = outInQuint,
inSine = inSine, outSine = outSine, inOutSine = inOutSine, outInSine = outInSine,
inExpo = inExpo, outExpo = outExpo, inOutExpo = inOutExpo, outInExpo = outInExpo,
inCirc = inCirc, outCirc = outCirc, inOutCirc = inOutCirc, outInCirc = outInCirc,
inElastic = inElastic, outElastic = outElastic, inOutElastic = inOutElastic, outInElastic = outInElastic,
inBack = inBack, outBack = outBack, inOutBack = inOutBack, outInBack = outInBack,
inBounce = inBounce, outBounce = outBounce, inOutBounce = inOutBounce, outInBounce = outInBounce
}
-- private stuff
local function copyTables(destination, keysTable, valuesTable)
valuesTable = valuesTable or keysTable
local mt = getmetatable(keysTable)
if mt and getmetatable(destination) == nil then
setmetatable(destination, mt)
end
for k,v in pairs(keysTable) do
if type(v) == 'table' then
destination[k] = copyTables({}, v, valuesTable[k])
else
destination[k] = valuesTable[k]
end
end
return destination
end
local function checkSubjectAndTargetRecursively(subject, target, path)
path = path or {}
local newPath
for k,targetValue in pairs(target) do
newPath = copyTables({}, path)
table.insert(newPath, tostring(k))
if isnumber(targetValue) then
assert(isnumber(subject[k]), "Parameter '" .. table.concat(newPath,'/') .. "' is missing from subject or isn't a number")
elseif istable(targetValue) then
checkSubjectAndTargetRecursively(subject[k], targetValue, newPath)
else
assert(isnumber(targetValue), "Parameter '" .. table.concat(newPath,'/') .. "' must be a number or table of numbers")
end
end
end
local function checkNewParams(duration, subject, target, easing)
assert(isnumber(duration) and duration > 0, "duration must be a positive number. Was " .. tostring(duration))
assert(istable(target), "target must be a table. Was " .. tostring(target))
assert(isfunction(easing), "easing must be a function. Was " .. tostring(easing))
checkSubjectAndTargetRecursively(subject, target)
end
local function getEasingFunction(easing)
easing = easing or "linear"
if isstring(easing) then
local name = easing
easing = tween.easing[name]
if not isfunction(easing) then
error("The easing function name '" .. name .. "' is invalid")
end
end
return easing
end
local function performEasingOnSubject(subject, target, initial, clock, duration, easing)
local t,b,c,d
for k,v in pairs(target) do
if istable(v) then
performEasingOnSubject(subject[k], v, initial[k], clock, duration, easing)
else
t,b,c,d = clock, initial[k], v - initial[k], duration
subject[k] = easing(t,b,c,d)
end
end
end
local function applyValues(subject, target)
for k, v in pairs(target) do
if (istable(v)) then
applyValues(subject[k], v)
else
subject[k] = v
end
end
end
-- Tween methods
local Tween = {}
local Tween_mt = {__index = Tween}
function Tween:set(clock)
assert(isnumber(clock), "clock must be a positive number or 0")
self.initial = self.initial or copyTables({}, self.target, self.subject)
self.clock = clock
if self.clock <= 0 then
self.clock = 0
applyValues(self.subject, self.initial)
elseif self.clock >= self.duration then -- the tween has expired
self.clock = self.duration
applyValues(self.subject, self.target)
else
performEasingOnSubject(self.subject, self.target, self.initial, self.clock, self.duration, self.easing)
end
return self.clock >= self.duration
end
function Tween:reset()
return self:set(0)
end
function Tween:update(dt)
assert(isnumber(dt), "dt must be a number")
return self:set(self.clock + dt)
end
-- Public interface
function tween.new(duration, subject, target, easing)
easing = getEasingFunction(easing)
checkNewParams(duration, subject, target, easing)
return setmetatable({
duration = duration,
subject = subject,
target = target,
easing = easing,
clock = 0
}, Tween_mt)
end
ix.tween = tween

View File

@@ -0,0 +1,338 @@
--[[
| 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/
--]]
-- $Id: utf8.lua 179 2009-04-03 18:10:03Z pasta $
--
-- Provides UTF-8 aware string functions implemented in pure lua:
-- * string.utf8len(s)
-- * string.utf8sub(s, i, j)
-- * string.utf8reverse(s)
--
-- If utf8data.lua (containing the lower<->upper case mappings) is loaded, these
-- additional functions are available:
-- * string.utf8upper(s)
-- * string.utf8lower(s)
--
-- All functions behave as their non UTF-8 aware counterparts with the exception
-- that UTF-8 characters are used instead of bytes for all units.
--[[
Copyright (c) 2006-2007, Kyle Smith
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the author nor the names of its contributors may be
used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--]]
-- ABNF from RFC 3629
--
-- UTF8-octets = *( UTF8-char )
-- UTF8-char = UTF8-1 / UTF8-2 / UTF8-3 / UTF8-4
-- UTF8-1 = %x00-7F
-- UTF8-2 = %xC2-DF UTF8-tail
-- UTF8-3 = %xE0 %xA0-BF UTF8-tail / %xE1-EC 2( UTF8-tail ) /
-- %xED %x80-9F UTF8-tail / %xEE-EF 2( UTF8-tail )
-- UTF8-4 = %xF0 %x90-BF 2( UTF8-tail ) / %xF1-F3 3( UTF8-tail ) /
-- %xF4 %x80-8F 2( UTF8-tail )
-- UTF8-tail = %x80-BF
--
ix.util.Include("data/sh_utf8_casemap.lua")
-- returns the number of bytes used by the UTF-8 character at byte i in s
-- also doubles as a UTF-8 character validator
local function utf8charbytes (s, i)
-- argument defaults
i = i or 1
-- argument checking
if not isstring(s) then
error("bad argument #1 to 'utf8charbytes' (string expected, got ".. type(s).. ")")
end
if not isnumber(i) then
error("bad argument #2 to 'utf8charbytes' (number expected, got ".. type(i).. ")")
end
local c = s:byte(i)
-- determine bytes needed for character, based on RFC 3629
-- validate byte 1
if c > 0 and c <= 127 then
-- UTF8-1
return 1
elseif c >= 194 and c <= 223 then
-- UTF8-2
local c2 = s:byte(i + 1)
if not c2 then
error("UTF-8 string terminated early")
end
-- validate byte 2
if c2 < 128 or c2 > 191 then
error("Invalid UTF-8 character")
end
return 2
elseif c >= 224 and c <= 239 then
-- UTF8-3
local c2 = s:byte(i + 1)
local c3 = s:byte(i + 2)
if not c2 or not c3 then
error("UTF-8 string terminated early")
end
-- validate byte 2
if c == 224 and (c2 < 160 or c2 > 191) then
error("Invalid UTF-8 character")
elseif c == 237 and (c2 < 128 or c2 > 159) then
error("Invalid UTF-8 character")
elseif c2 < 128 or c2 > 191 then
error("Invalid UTF-8 character")
end
-- validate byte 3
if c3 < 128 or c3 > 191 then
error("Invalid UTF-8 character")
end
return 3
elseif c >= 240 and c <= 244 then
-- UTF8-4
local c2 = s:byte(i + 1)
local c3 = s:byte(i + 2)
local c4 = s:byte(i + 3)
if not c2 or not c3 or not c4 then
error("UTF-8 string terminated early")
end
-- validate byte 2
if c == 240 and (c2 < 144 or c2 > 191) then
error("Invalid UTF-8 character")
elseif c == 244 and (c2 < 128 or c2 > 143) then
error("Invalid UTF-8 character")
elseif c2 < 128 or c2 > 191 then
error("Invalid UTF-8 character")
end
-- validate byte 3
if c3 < 128 or c3 > 191 then
error("Invalid UTF-8 character")
end
-- validate byte 4
if c4 < 128 or c4 > 191 then
error("Invalid UTF-8 character")
end
return 4
else
error("Invalid UTF-8 character")
end
end
-- returns the number of characters in a UTF-8 string
local function utf8len (s)
-- argument checking
if not isstring(s) then
error("bad argument #1 to 'utf8len' (string expected, got ".. type(s).. ")")
end
local pos = 1
local bytes = s:len()
local len = 0
while pos <= bytes do
len = len + 1
pos = pos + utf8charbytes(s, pos)
end
return len
end
-- install in the string library
if not string.utf8bytes then
string.utf8bytes = utf8charbytes
end
-- install in the string library
if not string.utf8len then
string.utf8len = utf8len
end
-- functions identically to string.sub except that i and j are UTF-8 characters
-- instead of bytes
local function utf8sub (s, i, j)
-- argument defaults
j = j or -1
-- argument checking
if not isstring(s) then
error("bad argument #1 to 'utf8sub' (string expected, got ".. type(s).. ")")
end
if not isnumber(i) then
error("bad argument #2 to 'utf8sub' (number expected, got ".. type(i).. ")")
end
if not isnumber(j) then
error("bad argument #3 to 'utf8sub' (number expected, got ".. type(j).. ")")
end
local pos = 1
local bytes = s:len()
local len = 0
-- only set l if i or j is negative
local l = (i >= 0 and j >= 0) or s:utf8len()
local startChar = (i >= 0) and i or l + i + 1
local endChar = (j >= 0) and j or l + j + 1
-- can't have start before end!
if startChar > endChar then
return ""
end
-- byte offsets to pass to string.sub
local startByte, endByte = 1, bytes
while pos <= bytes do
len = len + 1
if len == startChar then
startByte = pos
end
pos = pos + utf8charbytes(s, pos)
if len == endChar then
endByte = pos - 1
break
end
end
return s:sub(startByte, endByte)
end
-- install in the string library
if not string.utf8sub then
string.utf8sub = utf8sub
end
-- replace UTF-8 characters based on a mapping table
local function utf8replace (s, mapping)
-- argument checking
if not isstring(s) then
error("bad argument #1 to 'utf8replace' (string expected, got ".. type(s).. ")")
end
if not istable(mapping) then
error("bad argument #2 to 'utf8replace' (table expected, got ".. type(mapping).. ")")
end
local pos = 1
local bytes = s:len()
local charbytes
local newstr = ""
while pos <= bytes do
charbytes = utf8charbytes(s, pos)
local c = s:sub(pos, pos + charbytes - 1)
newstr = newstr .. (mapping[c] or c)
pos = pos + charbytes
end
return newstr
end
-- identical to string.upper except it knows about unicode simple case conversions
local function utf8upper (s)
return utf8replace(s, utf8_lc_uc)
end
-- install in the string library
if not string.utf8upper and utf8_lc_uc then
string.utf8upper = utf8upper
end
-- identical to string.lower except it knows about unicode simple case conversions
local function utf8lower (s)
return utf8replace(s, utf8_uc_lc)
end
-- install in the string library
if not string.utf8lower and utf8_uc_lc then
string.utf8lower = utf8lower
end
-- identical to string.reverse except that it supports UTF-8
local function utf8reverse (s)
-- argument checking
if not isstring(s) then
error("bad argument #1 to 'utf8reverse' (string expected, got ".. type(s).. ")")
end
local bytes = s:len()
local pos = bytes
local charbytes
local newstr = ""
while pos > 0 do
c = s:byte(pos)
while c >= 128 and c <= 191 do
pos = pos - 1
c = s:byte(pos)
end
charbytes = utf8charbytes(s, pos)
newstr = newstr .. s:sub(pos, pos + charbytes - 1)
pos = pos - 1
end
return newstr
end
-- install in the string library
if not string.utf8reverse then
string.utf8reverse = utf8reverse
end

View File

@@ -0,0 +1,615 @@
--[[
| 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/
--]]
--[[
(The MIT License)
Copyright (c) 2017 Dominic Letz dominicletz@exosite.com
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--]]
local table_print_value
table_print_value = function(value, indent, done)
indent = indent or 0
done = done or {}
if istable(value) and not done [value] then
done [value] = true
local list = {}
for key in pairs (value) do
list[#list + 1] = key
end
table.sort(list, function(a, b) return tostring(a) < tostring(b) end)
local last = list[#list]
local rep = "{\n"
local comma
for _, key in ipairs (list) do
if key == last then
comma = ''
else
comma = ','
end
local keyRep
if isnumber(key) then
keyRep = key
else
keyRep = string.format("%q", tostring(key))
end
rep = rep .. string.format(
"%s[%s] = %s%s\n",
string.rep(" ", indent + 2),
keyRep,
table_print_value(value[key], indent + 2, done),
comma
)
end
rep = rep .. string.rep(" ", indent) -- indent it
rep = rep .. "}"
done[value] = false
return rep
elseif isstring(value) then
return string.format("%q", value)
else
return tostring(value)
end
end
local table_print = function(tt)
print('return '..table_print_value(tt))
end
local table_clone = function(t)
local clone = {}
for k,v in pairs(t) do
clone[k] = v
end
return clone
end
local string_trim = function(s, what)
what = what or " "
return s:gsub("^[" .. what .. "]*(.-)["..what.."]*$", "%1")
end
local push = function(stack, item)
stack[#stack + 1] = item
end
local pop = function(stack)
local item = stack[#stack]
stack[#stack] = nil
return item
end
local context = function (str)
if not isstring(str) then
return ""
end
str = str:sub(0,25):gsub("\n","\\n"):gsub("\"","\\\"");
return ", near \"" .. str .. "\""
end
local Parser = {}
function Parser.new (self, tokens)
self.tokens = tokens
self.parse_stack = {}
self.refs = {}
self.current = 0
return self
end
local exports = {version = "1.2"}
local word = function(w) return "^("..w..")([%s$%c])" end
local tokens = {
{"comment", "^#[^\n]*"},
{"indent", "^\n( *)"},
{"space", "^ +"},
{"true", word("enabled"), const = true, value = true},
{"true", word("true"), const = true, value = true},
{"true", word("yes"), const = true, value = true},
{"true", word("on"), const = true, value = true},
{"false", word("disabled"), const = true, value = false},
{"false", word("false"), const = true, value = false},
{"false", word("no"), const = true, value = false},
{"false", word("off"), const = true, value = false},
{"null", word("null"), const = true, value = nil},
{"null", word("Null"), const = true, value = nil},
{"null", word("NULL"), const = true, value = nil},
{"null", word("~"), const = true, value = nil},
{"id", "^\"([^\"]-)\" *(:[%s%c])"},
{"id", "^'([^']-)' *(:[%s%c])"},
{"string", "^\"([^\"]-)\"", force_text = true},
{"string", "^'([^']-)'", force_text = true},
{"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d):(%d%d)%s+(%-?%d%d?):(%d%d)"},
{"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d):(%d%d)%s+(%-?%d%d?)"},
{"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d):(%d%d)"},
{"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d)"},
{"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?)"},
{"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)"},
{"doc", "^%-%-%-[^%c]*"},
{",", "^,"},
{"string", "^%b{} *[^,%c]+", noinline = true},
{"{", "^{"},
{"}", "^}"},
{"string", "^%b[] *[^,%c]+", noinline = true},
{"[", "^%["},
{"]", "^%]"},
{"-", "^%-"},
{":", "^:"},
{"pipe", "^(|)(%d*[+%-]?)", sep = "\n"},
{"pipe", "^(>)(%d*[+%-]?)", sep = " "},
{"id", "^([%w][%w %-_]*)(:[%s%c])"},
{"string", "^[^%c]+", noinline = true},
{"string", "^[^,%c ]+"}
};
exports.tokenize = function (str)
local token
local row = 0
local ignore
local indents = 0
local lastIndents
local stack = {}
local indentAmount = 0
local inline = false
str = str:gsub("\r\n","\010")
while #str > 0 do
for i in ipairs(tokens) do
local captures = {}
if not inline or tokens[i].noinline == nil then
captures = {str:match(tokens[i][2])}
end
if #captures > 0 then
captures.input = str:sub(0, 25)
token = table_clone(tokens[i])
token[2] = captures
local str2 = str:gsub(tokens[i][2], "", 1)
token.raw = str:sub(1, #str - #str2)
str = str2
if token[1] == "{" or token[1] == "[" then
inline = true
elseif token.const then
-- Since word pattern contains last char we're re-adding it
str = token[2][2] .. str
token.raw = token.raw:sub(1, #token.raw - #token[2][2])
elseif token[1] == "id" then
-- Since id pattern contains last semi-colon we're re-adding it
str = token[2][2] .. str
token.raw = token.raw:sub(1, #token.raw - #token[2][2])
-- Trim
token[2][1] = string_trim(token[2][1])
elseif token[1] == "string" then
-- Finding numbers
local snip = token[2][1]
if not token.force_text then
if snip:match("^(%d+%.%d+)$") or snip:match("^(%d+)$") then
token[1] = "number"
end
end
elseif token[1] == "comment" then
ignore = true;
elseif token[1] == "indent" then
row = row + 1
inline = false
lastIndents = indents
if indentAmount == 0 then
indentAmount = #token[2][1]
end
if indentAmount ~= 0 then
indents = (#token[2][1] / indentAmount);
else
indents = 0
end
if indents == lastIndents then
ignore = true;
elseif indents > lastIndents + 2 then
error("SyntaxError: invalid indentation, got " .. tostring(indents)
.. " instead of " .. tostring(lastIndents) .. context(token[2].input))
elseif indents > lastIndents + 1 then
push(stack, token)
elseif indents < lastIndents then
local input = token[2].input
token = {"dedent", {"", input = ""}}
token.input = input
while lastIndents > indents + 1 do
lastIndents = lastIndents - 1
push(stack, token)
end
end
end -- if token[1] == XXX
token.row = row
break
end -- if #captures > 0
end
if not ignore then
if token then
push(stack, token)
token = nil
else
error("SyntaxError " .. context(str))
end
end
ignore = false;
end
return stack
end
Parser.peek = function (self, offset)
offset = offset or 1
return self.tokens[offset + self.current]
end
Parser.advance = function (self)
self.current = self.current + 1
return self.tokens[self.current]
end
Parser.advanceValue = function (self)
return self:advance()[2][1]
end
Parser.accept = function (self, type)
if self:peekType(type) then
return self:advance()
end
end
Parser.expect = function (self, type, msg)
return self:accept(type) or
error(msg .. context(self:peek()[1].input))
end
Parser.expectDedent = function (self, msg)
return self:accept("dedent") or (self:peek() == nil) or
error(msg .. context(self:peek()[2].input))
end
Parser.peekType = function (self, val, offset)
return self:peek(offset) and self:peek(offset)[1] == val
end
Parser.ignore = function (self, items)
local advanced
repeat
advanced = false
for _,v in pairs(items) do
if self:peekType(v) then
self:advance()
advanced = true
end
end
until advanced == false
end
Parser.ignoreSpace = function (self)
self:ignore{"space"}
end
Parser.ignoreWhitespace = function (self)
self:ignore{"space", "indent", "dedent"}
end
Parser.parse = function (self)
local ref = nil
if self:peekType("string") and not self:peek().force_text then
local char = self:peek()[2][1]:sub(1,1)
if char == "&" then
ref = self:peek()[2][1]:sub(2)
self:advanceValue()
self:ignoreSpace()
elseif char == "*" then
ref = self:peek()[2][1]:sub(2)
return self.refs[ref]
end
end
local result
local c = {
indent = self:accept("indent") and 1 or 0,
token = self:peek()
}
push(self.parse_stack, c)
if c.token[1] == "doc" then
result = self:parseDoc()
elseif c.token[1] == "-" then
result = self:parseList()
elseif c.token[1] == "{" then
result = self:parseInlineHash()
elseif c.token[1] == "[" then
result = self:parseInlineList()
elseif c.token[1] == "id" then
result = self:parseHash()
elseif c.token[1] == "string" then
result = self:parseString("\n")
elseif c.token[1] == "timestamp" then
result = self:parseTimestamp()
elseif c.token[1] == "number" then
result = tonumber(self:advanceValue())
elseif c.token[1] == "pipe" then
result = self:parsePipe()
elseif c.token.const == true then
self:advanceValue();
result = c.token.value
else
error("ParseError: unexpected token '" .. c.token[1] .. "'" .. context(c.token.input))
end
pop(self.parse_stack)
while c.indent > 0 do
c.indent = c.indent - 1
local term = "term "..c.token[1]..": '"..c.token[2][1].."'"
self:expectDedent("last ".. term .." is not properly dedented")
end
if ref then
self.refs[ref] = result
end
return result
end
Parser.parseDoc = function (self)
self:accept("doc")
return self:parse()
end
Parser.inline = function (self)
local current = self:peek(0)
if not current then
return {}, 0
end
local inline = {}
local i = 0
while self:peek(i) and not self:peekType("indent", i) and current.row == self:peek(i).row do
inline[self:peek(i)[1]] = true
i = i - 1
end
return inline, -i
end
Parser.isInline = function (self)
local _, i = self:inline()
return i > 0
end
Parser.parent = function(self, level)
level = level or 1
return self.parse_stack[#self.parse_stack - level]
end
Parser.parentType = function(self, type, level)
return self:parent(level) and self:parent(level).token[1] == type
end
Parser.parseString = function (self)
if self:isInline() then
local result = self:advanceValue()
--[[
- a: this looks
flowing: but is
no: string
--]]
local types = self:inline()
if types["id"] and types["-"] then
if not self:peekType("indent") or not self:peekType("indent", 2) then
return result
end
end
--[[
a: 1
b: this is
a flowing string
example
c: 3
--]]
if self:peekType("indent") then
self:expect("indent", "text block needs to start with indent")
local addtl = self:accept("indent")
result = result .. "\n" .. self:parseTextBlock("\n")
self:expectDedent("text block ending dedent missing")
if addtl then
self:expectDedent("text block ending dedent missing")
end
end
return result
else
--[[
a: 1
b:
this is also
a flowing string
example
c: 3
--]]
return self:parseTextBlock("\n")
end
end
Parser.parsePipe = function (self)
local pipe = self:expect("pipe")
self:expect("indent", "text block needs to start with indent")
local result = self:parseTextBlock(pipe.sep)
self:expectDedent("text block ending dedent missing")
return result
end
Parser.parseTextBlock = function (self, sep)
local token = self:advance()
local result = string_trim(token.raw, "\n")
local indents = 0
while self:peek() ~= nil and ( indents > 0 or not self:peekType("dedent") ) do
local newtoken = self:advance()
while token.row < newtoken.row do
result = result .. sep
token.row = token.row + 1
end
if newtoken[1] == "indent" then
indents = indents + 1
elseif newtoken[1] == "dedent" then
indents = indents - 1
else
result = result .. string_trim(newtoken.raw, "\n")
end
end
return result
end
Parser.parseHash = function (self, hash)
hash = hash or {}
local indents = 0
if self:isInline() then
local id = self:advanceValue()
self:expect(":", "expected semi-colon after id")
self:ignoreSpace()
if self:accept("indent") then
indents = indents + 1
hash[id] = self:parse()
else
hash[id] = self:parse()
if self:accept("indent") then
indents = indents + 1
end
end
self:ignoreSpace();
end
while self:peekType("id") do
local id = self:advanceValue()
self:expect(":","expected semi-colon after id")
self:ignoreSpace()
hash[id] = self:parse()
self:ignoreSpace();
end
while indents > 0 do
self:expectDedent("expected dedent")
indents = indents - 1
end
return hash
end
Parser.parseInlineHash = function (self)
local id
local hash = {}
local i = 0
self:accept("{")
while not self:accept("}") do
self:ignoreSpace()
if i > 0 then
self:expect(",","expected comma")
end
self:ignoreWhitespace()
if self:peekType("id") then
id = self:advanceValue()
if id then
self:expect(":","expected semi-colon after id")
self:ignoreSpace()
hash[id] = self:parse()
self:ignoreWhitespace()
end
end
i = i + 1
end
return hash
end
Parser.parseList = function (self)
local list = {}
while self:accept("-") do
self:ignoreSpace()
list[#list + 1] = self:parse()
self:ignoreSpace()
end
return list
end
Parser.parseInlineList = function (self)
local list = {}
local i = 0
self:accept("[")
while not self:accept("]") do
self:ignoreSpace()
if i > 0 then
self:expect(",","expected comma")
end
self:ignoreSpace()
list[#list + 1] = self:parse()
self:ignoreSpace()
i = i + 1
end
return list
end
Parser.parseTimestamp = function (self)
local capture = self:advance()[2]
return os.time{
year = capture[1],
month = capture[2],
day = capture[3],
hour = capture[4] or 0,
min = capture[5] or 0,
sec = capture[6] or 0
}
end
exports.Eval = function (str)
return Parser:new(exports.tokenize(str)):parse()
end
exports.Read = function(file_name)
if file.Exists(file_name, 'GAME') then
local local_name = file_name:gsub('%.y([a]?)ml', '.local.y%1ml')
if file.Exists(local_name, 'GAME') then
file_name = local_name
end
return Parser:new(exports.tokenize(file.Read(file_name, 'GAME'))):parse()
end
end
exports.Dump = table_print
ix.yaml = exports

View File

@@ -0,0 +1,663 @@
--[[
| 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/
--]]
--[[
mysql - 1.0.3
A simple MySQL wrapper for Garry's Mod.
Alexander Grist-Hucker
http://www.alexgrist.com
The MIT License (MIT)
Copyright (c) 2014 Alex Grist-Hucker
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--]]
mysql = mysql or {
module = "sqlite"
}
local QueueTable = {}
local tostring = tostring
local table = table
--[[
Replacement tables
--]]
local Replacements = {
sqlite = {
Create = {
{"UNSIGNED ", ""},
{"NOT NULL AUTO_INCREMENT", ""}, -- assuming primary key
{"AUTO_INCREMENT", ""},
{"INT%(%d*%)", "INTEGER"},
{"INT ", "INTEGER"}
}
}
}
--[[
Phrases
--]]
local MODULE_NOT_EXIST = "[mysql] The %s module does not exist!\n"
--[[
Begin Query Class.
--]]
local QUERY_CLASS = {}
QUERY_CLASS.__index = QUERY_CLASS
function QUERY_CLASS:New(tableName, queryType)
local newObject = setmetatable({}, QUERY_CLASS)
newObject.queryType = queryType
newObject.tableName = tableName
newObject.selectList = {}
newObject.insertList = {}
newObject.updateList = {}
newObject.createList = {}
newObject.whereList = {}
newObject.orderByList = {}
return newObject
end
function QUERY_CLASS:Escape(text)
return mysql:Escape(tostring(text))
end
function QUERY_CLASS:ForTable(tableName)
self.tableName = tableName
end
function QUERY_CLASS:Where(key, value)
self:WhereEqual(key, value)
end
function QUERY_CLASS:WhereEqual(key, value)
self.whereList[#self.whereList + 1] = "`"..key.."` = '"..self:Escape(value).."'"
end
function QUERY_CLASS:WhereNotEqual(key, value)
self.whereList[#self.whereList + 1] = "`"..key.."` != '"..self:Escape(value).."'"
end
function QUERY_CLASS:WhereLike(key, value, format)
format = format or "%%%s%%"
self.whereList[#self.whereList + 1] = "`"..key.."` LIKE '"..string.format(format, self:Escape(value)).."'"
end
function QUERY_CLASS:WhereNotLike(key, value, format)
format = format or "%%%s%%"
self.whereList[#self.whereList + 1] = "`"..key.."` NOT LIKE '"..string.format(format, self:Escape(value)).."'"
end
function QUERY_CLASS:WhereGT(key, value)
self.whereList[#self.whereList + 1] = "`"..key.."` > '"..self:Escape(value).."'"
end
function QUERY_CLASS:WhereLT(key, value)
self.whereList[#self.whereList + 1] = "`"..key.."` < '"..self:Escape(value).."'"
end
function QUERY_CLASS:WhereGTE(key, value)
self.whereList[#self.whereList + 1] = "`"..key.."` >= '"..self:Escape(value).."'"
end
function QUERY_CLASS:WhereLTE(key, value)
self.whereList[#self.whereList + 1] = "`"..key.."` <= '"..self:Escape(value).."'"
end
function QUERY_CLASS:WhereIn(key, value)
value = istable(value) and value or {value}
local values = ""
local bFirst = true
for k, v in pairs(value) do
values = values .. (bFirst and "" or ", ") .. "'" .. self:Escape(v) .. "'"
bFirst = false
end
self.whereList[#self.whereList + 1] = "`"..key.."` IN ("..values..")"
end
function QUERY_CLASS:OrderByDesc(key)
self.orderByList[#self.orderByList + 1] = "`"..key.."` DESC"
end
function QUERY_CLASS:OrderByAsc(key)
self.orderByList[#self.orderByList + 1] = "`"..key.."` ASC"
end
function QUERY_CLASS:Callback(queryCallback)
self.callback = queryCallback
end
function QUERY_CLASS:Select(fieldName)
self.selectList[#self.selectList + 1] = "`"..fieldName.."`"
end
function QUERY_CLASS:Insert(key, value)
self.insertList[#self.insertList + 1] = {"`"..key.."`", "'"..self:Escape(value).."'"}
end
function QUERY_CLASS:Update(key, value)
self.updateList[#self.updateList + 1] = {"`"..key.."`", "'"..self:Escape(value).."'"}
end
function QUERY_CLASS:Create(key, value)
self.createList[#self.createList + 1] = {"`"..key.."`", value}
end
function QUERY_CLASS:Add(key, value)
self.add = {"`"..key.."`", value}
end
function QUERY_CLASS:Drop(key)
self.drop = "`"..key.."`"
end
function QUERY_CLASS:PrimaryKey(key)
self.primaryKey = "`"..key.."`"
end
function QUERY_CLASS:Limit(value)
self.limit = value
end
function QUERY_CLASS:Offset(value)
self.offset = value
end
local function ApplyQueryReplacements(mode, query)
if (!Replacements[mysql.module]) then
return query
end
local result = query
local entries = Replacements[mysql.module][mode]
for i = 1, #entries do
result = string.gsub(result, entries[i][1], entries[i][2])
end
return result
end
local function BuildSelectQuery(queryObj)
local queryString = {"SELECT"}
if (!istable(queryObj.selectList) or #queryObj.selectList == 0) then
queryString[#queryString + 1] = " *"
else
queryString[#queryString + 1] = " "..table.concat(queryObj.selectList, ", ")
end
if (isstring(queryObj.tableName)) then
queryString[#queryString + 1] = " FROM `"..queryObj.tableName.."` "
else
ErrorNoHalt("[mysql] No table name specified!\n")
return
end
if (istable(queryObj.whereList) and #queryObj.whereList > 0) then
queryString[#queryString + 1] = " WHERE "
queryString[#queryString + 1] = table.concat(queryObj.whereList, " AND ")
end
if (istable(queryObj.orderByList) and #queryObj.orderByList > 0) then
queryString[#queryString + 1] = " ORDER BY "
queryString[#queryString + 1] = table.concat(queryObj.orderByList, ", ")
end
if (isnumber(queryObj.limit)) then
queryString[#queryString + 1] = " LIMIT "
queryString[#queryString + 1] = queryObj.limit
if (isnumber(queryObj.offset)) then
queryString[#queryString + 1] = " OFFSET "
queryString[#queryString + 1] = queryObj.offset
end
end
return table.concat(queryString)
end
local function BuildInsertQuery(queryObj, bIgnore)
local suffix = (bIgnore and (mysql.module == "sqlite" and "INSERT OR IGNORE INTO" or "INSERT IGNORE INTO") or "INSERT INTO")
local queryString = {suffix}
local keyList = {}
local valueList = {}
if (isstring(queryObj.tableName)) then
queryString[#queryString + 1] = " `"..queryObj.tableName.."`"
else
ErrorNoHalt("[mysql] No table name specified!\n")
return
end
for i = 1, #queryObj.insertList do
keyList[#keyList + 1] = queryObj.insertList[i][1]
valueList[#valueList + 1] = queryObj.insertList[i][2]
end
if (#keyList == 0) then
return
end
queryString[#queryString + 1] = " ("..table.concat(keyList, ", ")..")"
queryString[#queryString + 1] = " VALUES ("..table.concat(valueList, ", ")..")"
return table.concat(queryString)
end
local function BuildUpdateQuery(queryObj)
local queryString = {"UPDATE"}
if (isstring(queryObj.tableName)) then
queryString[#queryString + 1] = " `"..queryObj.tableName.."`"
else
ErrorNoHalt("[mysql] No table name specified!\n")
return
end
if (istable(queryObj.updateList) and #queryObj.updateList > 0) then
local updateList = {}
queryString[#queryString + 1] = " SET"
for i = 1, #queryObj.updateList do
updateList[#updateList + 1] = queryObj.updateList[i][1].." = "..queryObj.updateList[i][2]
end
queryString[#queryString + 1] = " "..table.concat(updateList, ", ")
end
if (istable(queryObj.whereList) and #queryObj.whereList > 0) then
queryString[#queryString + 1] = " WHERE "
queryString[#queryString + 1] = table.concat(queryObj.whereList, " AND ")
end
if (isnumber(queryObj.offset)) then
queryString[#queryString + 1] = " OFFSET "
queryString[#queryString + 1] = queryObj.offset
end
return table.concat(queryString)
end
local function BuildDeleteQuery(queryObj)
local queryString = {"DELETE FROM"}
if (isstring(queryObj.tableName)) then
queryString[#queryString + 1] = " `"..queryObj.tableName.."`"
else
ErrorNoHalt("[mysql] No table name specified!\n")
return
end
if (istable(queryObj.whereList) and #queryObj.whereList > 0) then
queryString[#queryString + 1] = " WHERE "
queryString[#queryString + 1] = table.concat(queryObj.whereList, " AND ")
end
if (isnumber(queryObj.limit)) then
queryString[#queryString + 1] = " LIMIT "
queryString[#queryString + 1] = queryObj.limit
end
return table.concat(queryString)
end
local function BuildDropQuery(queryObj)
local queryString = {"DROP TABLE"}
if (isstring(queryObj.tableName)) then
queryString[#queryString + 1] = " `"..queryObj.tableName.."`"
else
ErrorNoHalt("[mysql] No table name specified!\n")
return
end
return table.concat(queryString)
end
local function BuildTruncateQuery(queryObj)
local queryString = {"TRUNCATE TABLE"}
if (isstring(queryObj.tableName)) then
queryString[#queryString + 1] = " `"..queryObj.tableName.."`"
else
ErrorNoHalt("[mysql] No table name specified!\n")
return
end
return table.concat(queryString)
end
local function BuildCreateQuery(queryObj)
local queryString = {"CREATE TABLE IF NOT EXISTS"}
if (isstring(queryObj.tableName)) then
queryString[#queryString + 1] = " `"..queryObj.tableName.."`"
else
ErrorNoHalt("[mysql] No table name specified!\n")
return
end
queryString[#queryString + 1] = " ("
if (istable(queryObj.createList) and #queryObj.createList > 0) then
local createList = {}
for i = 1, #queryObj.createList do
if (mysql.module == "sqlite") then
createList[#createList + 1] = queryObj.createList[i][1].." "..ApplyQueryReplacements("Create", queryObj.createList[i][2])
else
createList[#createList + 1] = queryObj.createList[i][1].." "..queryObj.createList[i][2]
end
end
queryString[#queryString + 1] = " "..table.concat(createList, ", ")
end
if (isstring(queryObj.primaryKey)) then
queryString[#queryString + 1] = ", PRIMARY KEY"
queryString[#queryString + 1] = " ("..queryObj.primaryKey..")"
end
queryString[#queryString + 1] = " )"
return table.concat(queryString)
end
local function BuildAlterQuery(queryObj)
local queryString = {"ALTER TABLE"}
if (isstring(queryObj.tableName)) then
queryString[#queryString + 1] = " `"..queryObj.tableName.."`"
else
ErrorNoHalt("[mysql] No table name specified!\n")
return
end
if (istable(queryObj.add)) then
queryString[#queryString + 1] = " ADD "..queryObj.add[1].." "..ApplyQueryReplacements("Create", queryObj.add[2])
elseif (isstring(queryObj.drop)) then
if (mysql.module == "sqlite") then
ErrorNoHalt("[mysql] Cannot drop columns in sqlite!\n")
return
end
queryString[#queryString + 1] = " DROP COLUMN "..queryObj.drop
end
return table.concat(queryString)
end
function QUERY_CLASS:Execute(bQueueQuery)
local queryString = nil
local queryType = string.lower(self.queryType)
if (queryType == "select") then
queryString = BuildSelectQuery(self)
elseif (queryType == "insert") then
queryString = BuildInsertQuery(self)
elseif (queryType == "insert ignore") then
queryString = BuildInsertQuery(self, true)
elseif (queryType == "update") then
queryString = BuildUpdateQuery(self)
elseif (queryType == "delete") then
queryString = BuildDeleteQuery(self)
elseif (queryType == "drop") then
queryString = BuildDropQuery(self)
elseif (queryType == "truncate") then
queryString = BuildTruncateQuery(self)
elseif (queryType == "create") then
queryString = BuildCreateQuery(self)
elseif (queryType == "alter") then
queryString = BuildAlterQuery(self)
end
if (isstring(queryString)) then
if (!bQueueQuery) then
return mysql:RawQuery(queryString, self.callback)
else
return mysql:Queue(queryString, self.callback)
end
end
end
--[[
End Query Class.
--]]
function mysql:Select(tableName)
return QUERY_CLASS:New(tableName, "SELECT")
end
function mysql:Insert(tableName)
return QUERY_CLASS:New(tableName, "INSERT")
end
function mysql:InsertIgnore(tableName)
return QUERY_CLASS:New(tableName, "INSERT IGNORE")
end
function mysql:Update(tableName)
return QUERY_CLASS:New(tableName, "UPDATE")
end
function mysql:Delete(tableName)
return QUERY_CLASS:New(tableName, "DELETE")
end
function mysql:Drop(tableName)
return QUERY_CLASS:New(tableName, "DROP")
end
function mysql:Truncate(tableName)
return QUERY_CLASS:New(tableName, "TRUNCATE")
end
function mysql:Create(tableName)
return QUERY_CLASS:New(tableName, "CREATE")
end
function mysql:Alter(tableName)
return QUERY_CLASS:New(tableName, "ALTER")
end
local UTF8MB4 = "ALTER DATABASE %s CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci"
-- A function to connect to the MySQL database.
function mysql:Connect(host, username, password, database, port, socket, flags)
port = port or 3306
if (self.module == "mysqloo") then
if (!istable(mysqloo)) then
require("mysqloo")
end
if (mysqloo) then
if (self.connection and self.connection:ping()) then
return
end
local clientFlag = flags or 0
if (!isstring(socket)) then
self.connection = mysqloo.connect(host, username, password, database, port)
else
self.connection = mysqloo.connect(host, username, password, database, port, socket, clientFlag)
end
self.connection.onConnected = function(connection)
local success, error_message = connection:setCharacterSet("utf8mb4")
if (!success) then
ErrorNoHalt("Failed to set MySQL encoding!\n")
ErrorNoHalt(error_message .. "\n")
else
self:RawQuery(string.format(UTF8MB4, database))
end
mysql:OnConnected()
end
self.connection.onConnectionFailed = function(database, errorText)
mysql:OnConnectionFailed(errorText)
end
self.connection:connect()
timer.Create("mysql.KeepAlive", 300, 0, function()
self.connection:ping()
end)
else
ErrorNoHalt(string.format(MODULE_NOT_EXIST, self.module))
end
elseif (self.module == "sqlite") then
timer.Simple(0, function()
mysql:OnConnected()
end)
end
end
-- A function to query the MySQL database.
function mysql:RawQuery(query, callback, flags, ...)
if (self.module == "mysqloo") then
local queryObj = self.connection:query(query)
queryObj:setOption(mysqloo.OPTION_NAMED_FIELDS)
queryObj.onSuccess = function(queryObj, result)
if (callback) then
local bStatus, value = pcall(callback, result, true, tonumber(queryObj:lastInsert()))
if (!bStatus) then
error(string.format("[mysql] MySQL Callback Error!\n%s\n", value))
end
end
end
queryObj.onError = function(queryObj, errorText)
ErrorNoHalt(string.format("[mysql] MySQL Query Error!\nQuery: %s\n%s\n", query, errorText))
end
queryObj:start()
elseif (self.module == "sqlite") then
local result = sql.Query(query)
if (result == false) then
error(string.format("[mysql] SQL Query Error!\nQuery: %s\n%s\n", query, sql.LastError()))
else
if (callback) then
local bStatus, value = pcall(callback, result, true, tonumber(sql.QueryValue("SELECT last_insert_rowid()")))
if (!bStatus) then
error(string.format("[mysql] SQL Callback Error!\n%s\n", value))
end
end
end
else
ErrorNoHalt(string.format("[mysql] Unsupported module \"%s\"!\n", self.module))
end
end
-- A function to add a query to the queue.
function mysql:Queue(queryString, callback)
if (isstring(queryString)) then
QueueTable[#QueueTable + 1] = {queryString, callback}
end
end
-- A function to escape a string for MySQL.
function mysql:Escape(text)
if (self.connection) then
if (self.module == "mysqloo") then
return self.connection:escape(text)
end
else
return sql.SQLStr(text, true)
end
end
-- A function to disconnect from the MySQL database.
function mysql:Disconnect()
if (self.connection) then
if (self.module == "mysqloo") then
self.connection:disconnect(true)
end
end
end
function mysql:Think()
if (#QueueTable > 0) then
if (istable(QueueTable[1])) then
local queueObj = QueueTable[1]
local queryString = queueObj[1]
local callback = queueObj[2]
if (isstring(queryString)) then
self:RawQuery(queryString, callback)
end
table.remove(QueueTable, 1)
end
end
end
-- A function to set the module that should be used.
function mysql:SetModule(moduleName)
self.module = moduleName
end
-- Called when the database connects sucessfully.
function mysql:OnConnected()
MsgC(Color(25, 235, 25), "[mysql] Connected to the database!\n")
hook.Run("DatabaseConnected")
end
-- Called when the database connection fails.
function mysql:OnConnectionFailed(errorText)
ErrorNoHalt(string.format("[mysql] Unable to connect to the database!\n%s\n", errorText))
hook.Run("DatabaseConnectionFailed", errorText)
end
-- A function to check whether or not the module is connected to a database.
function mysql:IsConnected()
return self.module == "mysqloo" and (self.connection and self.connection:ping()) or self.module == "sqlite"
end
return mysql