Make players tab public but advanced for admins

This commit is contained in:
2026-09-13 13:21:12 +02:00
parent e683249570
commit 568eb1520f
4 changed files with 442 additions and 197 deletions
+24 -7
View File
@@ -15,6 +15,8 @@ Score and therefore cannot lower a player's rank.
Player controls:
- `F3`, chat `!shop`, or client console `coop_shop`: open the rewards shop
- F3 `Players`: view the all-time rankings; right-click or double-click a row
to open that player's Steam profile
- Chat `!points`: print personal Score, Credits, rank, and total playtime
- Chat `!musicmute` or client console `coop_music_mute`: toggle purchased server music locally
- Client console `coop_music_volume 0.0` through `coop_music_volume 1.0`: set music volume
@@ -30,7 +32,9 @@ afterward. Each player can mute or adjust the music locally.
The permanent weapon catalogue contains all thirteen spawnable weapons from the
gamemode's `CO-OP` Q-menu category: Glock, P228, USP, Five-Seven, MAC-10, MP5,
Pump Shotgun, FAMAS, AK-47, M4A1, Desert Eagle, Automatic Shotgun, and M249. The
separate Workshop novelty weapons remain available at their higher prices.
Workshop novelty weapons are Nyan Gun, Lightsaber, HAAAX! SWEP, Popcorn
Spammable, Orbital Friendship Cannon, and Sexyness (2-bit). They are permanent
unlocks at their higher prices.
Music entries may set `spatial = true` plus `minimumDistance` and
`maximumDistance`. Spatial music follows the buyer's character while they move
@@ -83,14 +87,26 @@ cannot award points, submit a purchase price, or debit their own Credits.
Administrators get an `Admin Prices` tab in the F3 shop. Select an item, enter a
non-negative Credit price, and save it. The server permission-checks and persists
the change in SQLite, then broadcasts the new catalogue to all connected clients.
The adjacent `Admin Players` tab lists every SQLite player record, including
offline players, with Lifetime Score, Credits, playtime, equipped model, weapon,
trail, enabled behaviours, and permanent purchases. Connected players' newer
in-memory totals are merged into the view without forcing a synchronous save.
For testing, an administrator who cannot afford an item is allowed to unlock it
without a Credit charge; the server reports the shortfall and Credits remain at
their current non-negative value. Set `Progression.AdminShopTestingBypass` to
`false` to disable this behavior.
Every player gets one `Players` tab containing all SQLite player records,
including offline players, sorted by all-time Lifetime Score. The public payload
contains only SteamID64, name, online status, rank inputs, Score, Credits, and
playtime. It never sends equipped models, weapons, trails, behaviours, or
purchases. SteamID64 is used only for the explicit profile action; the list has
no automatic player-detail tooltip. Offline history is loaded once when F3 opens;
online rows and ranking then update automatically from the same networked Score,
Credits, and playtime fields used by the regular TAB scoreboard. There is no
manual refresh button, and the regular TAB scoreboard is unchanged. For admins,
the same table adds model, weapon, trail, and purchase-count columns plus hover
and selected-row details for enabled behaviours and permanent purchases. The
server appends these private fields only to responses for an authenticated admin;
it does not send them to guests and hide them in the UI. Connected players'
newer in-memory totals are merged without forcing a synchronous SQLite save.
The equivalent server/admin console command is:
`coop_setprice <model|weapon|music|behaviour|trail|entity|helper> <item_id> <credits>`
@@ -128,5 +144,6 @@ back it up separately.
Player-spawned NPCs and unknown/friendly NPC classes do not award points.
Shop refreshes preserve the selected F3 tab and catalogue scroll position, so
buying or equipping an item does not return the window to Player Models.
Purchase, equip, behaviour, and price responses update the existing F3 controls
in place. They do not recreate, close, move, or scroll the window, so there is no
visible frame flash and the selected tab and scroll position stay untouched.
+299 -159
View File
@@ -17,18 +17,19 @@ local shopState = {
enabledBehaviours = {},
prices = {},
isAdmin = false,
adminPlayers = {},
adminPlayersLoading = false
players = {},
playersLoading = false,
playersHaveDetails = false
}
local shopViewState = {
activeTab = "Player Models",
scroll = {}
activeTab = "Player Models"
}
local adminPlayersList
local adminPlayersStatus
local adminPlayerDetails
local adminPlayerRequestID = 0
local RefreshAdminPlayersView
local playersList
local playersStatus
local playerDetails
local playerRequestID = 0
local RefreshPlayersView
local RefreshShopFrame
local activeMusicChannel
local activeMusicSource
local activeMusicIsSpatial = false
@@ -92,17 +93,15 @@ local function RequestShop()
net.SendToServer()
end
local function RequestAdminPlayers()
if not shopState.isAdmin then return end
local function RequestPlayers()
playerRequestID = (playerRequestID + 1) % 65536
shopState.players = {}
shopState.playersLoading = true
if IsValid(playersStatus) then playersStatus:SetText("Loading all-time player records...") end
if IsValid(playersList) then playersList:Clear() end
adminPlayerRequestID = (adminPlayerRequestID + 1) % 65536
shopState.adminPlayers = {}
shopState.adminPlayersLoading = true
if IsValid(adminPlayersStatus) then adminPlayersStatus:SetText("Loading all player records...") end
if IsValid(adminPlayersList) then adminPlayersList:Clear() end
net.Start("CustomCoopProgressionRequestAdminPlayers")
net.WriteUInt(adminPlayerRequestID, 16)
net.Start("CustomCoopProgressionRequestPlayers")
net.WriteUInt(playerRequestID, 16)
net.SendToServer()
end
@@ -187,23 +186,12 @@ local function AddItemCard(layout, itemType, item)
local isHelper = itemType == "helper"
local isSpawnPurchase = isEntity or isHelper
local isConsumable = isBehaviour and item.consumable == true
local owned = isModel and shopState.ownedModels[item.id]
or isWeapon and shopState.ownedWeapons[item.id]
or isMusic and shopState.ownedMusic[item.id]
or isBehaviour and shopState.ownedBehaviours[item.id]
or isTrail and shopState.ownedTrails[item.id]
local selected = isModel and shopState.equippedModel == item.id
or isWeapon and shopState.equippedWeapon == item.id
or isBehaviour and shopState.enabledBehaviours[item.id]
or isTrail and shopState.equippedTrail == item.id
local currentPrice = CurrentPrice(itemType, item)
local card = layout:Add("DPanel")
card:SetSize(252, 292)
card.Paint = function(self, width, height)
local color = self:IsHovered() and colors.panelHover or colors.panel
draw.RoundedBox(8, 0, 0, width, height, color)
if selected then
if self.CustomCoopSelected then
surface.SetDrawColor(colors.success)
surface.DrawOutlinedRect(0, 0, width, height, 2)
end
@@ -256,8 +244,7 @@ local function AddItemCard(layout, itemType, item)
description:SetPos(12, 202)
description:SetSize(228, 38)
local priceText = currentPrice == 0 and "FREE" or (currentPrice .. " credits")
local price = AddText(card, priceText, "CustomCoopProgressionBody", currentPrice <= shopState.credits and colors.success or colors.warning)
local price = AddText(card, "", "CustomCoopProgressionBody", colors.warning)
price:SetPos(12, 239)
price:SetSize(100, 22)
@@ -265,31 +252,59 @@ local function AddItemCard(layout, itemType, item)
button:SetPos(112, 244)
button:SetSize(128, 36)
button:SetFont("CustomCoopProgressionBody")
local actionText
if isMusic then
actionText = (owned or currentPrice == 0) and "Play" or "Buy"
elseif isSpawnPurchase then
actionText = "Buy & Spawn"
elseif isBehaviour then
actionText = isConsumable and "Activate"
or owned and (selected and "Disable" or "Enable")
or "Buy & Enable"
else
actionText = selected and "Equipped" or (owned or currentPrice == 0) and "Equip" or "Buy"
local function RefreshCard(clearPending)
if clearPending then button.CustomCoopPending = false end
local owned = isModel and shopState.ownedModels[item.id]
or isWeapon and shopState.ownedWeapons[item.id]
or isMusic and shopState.ownedMusic[item.id]
or isBehaviour and shopState.ownedBehaviours[item.id]
or isTrail and shopState.ownedTrails[item.id]
local selected = isModel and shopState.equippedModel == item.id
or isWeapon and shopState.equippedWeapon == item.id
or isBehaviour and shopState.enabledBehaviours[item.id]
or isTrail and shopState.equippedTrail == item.id
local currentPrice = CurrentPrice(itemType, item)
local actionText
if isMusic then
actionText = (owned or currentPrice == 0) and "Play" or "Buy"
elseif isSpawnPurchase then
actionText = "Buy & Spawn"
elseif isBehaviour then
actionText = isConsumable and "Activate"
or owned and (selected and "Disable" or "Enable")
or "Buy & Enable"
else
actionText = selected and "Equipped" or (owned or currentPrice == 0) and "Equip" or "Buy"
end
card.CustomCoopSelected = selected == true
price:SetText(currentPrice == 0 and "FREE" or (currentPrice .. " credits"))
price:SetTextColor(currentPrice <= shopState.credits and colors.success or colors.warning)
button.CustomCoopActionText = actionText
if not button.CustomCoopPending then
button:SetText(actionText)
button:SetEnabled(isBehaviour or not selected)
end
end
card.CustomCoopRefresh = RefreshCard
if IsValid(shopFrame) then
shopFrame.CustomCoopCards[#shopFrame.CustomCoopCards + 1] = card
end
button:SetText(actionText)
button:SetEnabled(isBehaviour or not selected)
button.DoClick = function()
button.CustomCoopPending = true
button:SetEnabled(false)
button:SetText("Please wait...")
SelectItem(itemType, item.id)
timer.Simple(1, function()
if IsValid(button) then
button:SetEnabled(isBehaviour or not selected)
button:SetText(actionText)
button.CustomCoopPending = false
RefreshCard()
end
end)
end
RefreshCard()
end
local function CreateCataloguePage(sheet, title, itemType, items)
@@ -314,13 +329,6 @@ local function CreateCataloguePage(sheet, title, itemType, items)
page.CustomCoopScroll = scroll
sheet:AddSheet(title, page)
local savedScroll = shopViewState.scroll[title]
if savedScroll and savedScroll > 0 then
timer.Simple(0, function()
if IsValid(scroll) then scroll:GetVBar():SetScroll(savedScroll) end
end)
end
end
local function SendPriceChange(itemType, itemID, price)
@@ -389,6 +397,8 @@ local function CreateAdminPage(sheet)
local row = list:AddLine(catalogue.itemType, item.id, item.name, CurrentPrice(catalogue.itemType, item))
row.CustomCoopItemType = catalogue.itemType
row.CustomCoopItemID = item.id
row.CustomCoopItem = item
shopFrame.CustomCoopPriceRows[#shopFrame.CustomCoopPriceRows + 1] = row
end
end
@@ -406,6 +416,11 @@ local function CreateAdminPage(sheet)
save:SetText("Saving...")
end
shopFrame.CustomCoopPriceSave = save
shopFrame.CustomCoopSelectedPriceRow = function()
return selectedRow
end
sheet:AddSheet("Admin Prices", page)
end
@@ -451,34 +466,64 @@ local function DescribeAdminPlayer(record)
)
end
RefreshAdminPlayersView = function()
if not IsValid(adminPlayersList) then return end
local function OpenSteamProfile(record)
local steamID64 = tostring(record and record.steamID64 or "")
if #steamID64 ~= 17 or not string.match(steamID64, "^%d+$") then
notification.AddLegacy("That player has no valid Steam profile ID.", NOTIFY_ERROR, 4)
return
end
gui.OpenURL("https://steamcommunity.com/profiles/" .. steamID64)
end
adminPlayersList:Clear()
for _, record in ipairs(shopState.adminPlayers) do
local row = adminPlayersList:AddLine(
RefreshPlayersView = function()
if not IsValid(playersList) then return end
playersList:Clear()
for position, record in ipairs(shopState.players) do
local baseColumns = {
position,
record.playerName,
record.online and "Online" or "Offline",
record.steamID64,
Progression.GetRank(record.score),
record.score,
record.credits,
Progression.FormatPlaytime(record.playtime),
CatalogueItemName("model", record.equippedModel),
CatalogueItemName("weapon", record.equippedWeapon),
CatalogueItemName("trail", record.equippedTrail),
#(record.purchases or {})
)
row.CustomCoopAdminPlayer = record
row:SetTooltip(DescribeAdminPlayer(record))
Progression.FormatPlaytime(record.playtime)
}
local row
if shopState.playersHaveDetails then
row = playersList:AddLine(
position,
record.playerName,
record.online and "Online" or "Offline",
Progression.GetRank(record.score),
record.score,
record.credits,
Progression.FormatPlaytime(record.playtime),
record.steamID64,
CatalogueItemName("model", record.equippedModel),
CatalogueItemName("weapon", record.equippedWeapon),
CatalogueItemName("trail", record.equippedTrail),
#(record.purchases or {})
)
row:SetTooltip(DescribeAdminPlayer(record))
else
row = playersList:AddLine(unpack(baseColumns))
end
row.CustomCoopPlayer = record
end
if IsValid(adminPlayersStatus) then
local suffix = shopState.adminPlayersLoading and " (loading...)" or ""
adminPlayersStatus:SetText(string.format("%d player record%s%s", #shopState.adminPlayers, #shopState.adminPlayers == 1 and "" or "s", suffix))
if IsValid(playersStatus) then
local suffix = shopState.playersLoading and " (loading...)" or ""
playersStatus:SetText(string.format(
"%d all-time player%s%s — live ranking; right-click a row to open its Steam profile",
#shopState.players,
#shopState.players == 1 and "" or "s",
suffix
))
end
end
local function CreateAdminPlayersPage(sheet)
local function CreatePlayersPage(sheet)
local page = vgui.Create("DPanel", sheet)
page:DockPadding(10, 10, 10, 10)
page.Paint = function(_, width, height)
@@ -491,85 +536,168 @@ local function CreateAdminPlayersPage(sheet)
toolbar:DockMargin(0, 0, 0, 8)
toolbar.Paint = nil
local refresh = vgui.Create("DButton", toolbar)
refresh:Dock(RIGHT)
refresh:SetWide(150)
refresh:SetText("Refresh players")
refresh:SetFont("CustomCoopProgressionBody")
refresh.DoClick = RequestAdminPlayers
playersStatus = AddText(toolbar, "Loading all-time player records...", "CustomCoopProgressionSmall", colors.muted)
playersStatus:Dock(FILL)
playersStatus:SetContentAlignment(4)
adminPlayersStatus = AddText(toolbar, "Loading all player records...", "CustomCoopProgressionSmall", colors.muted)
adminPlayersStatus:Dock(FILL)
adminPlayersStatus:SetContentAlignment(4)
if shopState.isAdmin then
local detailsPanel = vgui.Create("DPanel", page)
detailsPanel:Dock(BOTTOM)
detailsPanel:SetTall(92)
detailsPanel:DockMargin(0, 8, 0, 0)
detailsPanel:DockPadding(10, 8, 10, 8)
detailsPanel.Paint = function(_, width, height)
draw.RoundedBox(5, 0, 0, width, height, colors.panel)
end
local detailsPanel = vgui.Create("DPanel", page)
detailsPanel:Dock(BOTTOM)
detailsPanel:SetTall(92)
detailsPanel:DockMargin(0, 8, 0, 0)
detailsPanel:DockPadding(10, 8, 10, 8)
detailsPanel.Paint = function(_, width, height)
draw.RoundedBox(5, 0, 0, width, height, colors.panel)
playerDetails = AddText(
detailsPanel,
"Select a player to inspect equipped items, enabled behaviours, and permanent purchases.",
"CustomCoopProgressionSmall",
colors.muted
)
playerDetails:Dock(FILL)
end
adminPlayerDetails = AddText(
detailsPanel,
"Select a player to inspect equipped items, enabled behaviours, and permanent purchases.",
"CustomCoopProgressionSmall",
colors.muted
)
adminPlayerDetails:Dock(FILL)
adminPlayersList = vgui.Create("DListView", page)
adminPlayersList:Dock(FILL)
adminPlayersList:SetMultiSelect(false)
adminPlayersList:AddColumn("Name")
adminPlayersList:AddColumn("Status"):SetFixedWidth(58)
adminPlayersList:AddColumn("SteamID64"):SetFixedWidth(145)
adminPlayersList:AddColumn("XP / Score"):SetFixedWidth(78)
adminPlayersList:AddColumn("Credits"):SetFixedWidth(68)
adminPlayersList:AddColumn("Played"):SetFixedWidth(78)
adminPlayersList:AddColumn("Model"):SetFixedWidth(115)
adminPlayersList:AddColumn("Weapon"):SetFixedWidth(125)
adminPlayersList:AddColumn("Trail"):SetFixedWidth(110)
adminPlayersList:AddColumn("Owned"):SetFixedWidth(55)
adminPlayersList.OnRowSelected = function(_, _, row)
if IsValid(adminPlayerDetails) and row.CustomCoopAdminPlayer then
adminPlayerDetails:SetText(DescribeAdminPlayer(row.CustomCoopAdminPlayer))
playersList = vgui.Create("DListView", page)
playersList:Dock(FILL)
playersList:SetMultiSelect(false)
playersList:AddColumn("#"):SetFixedWidth(36)
playersList:AddColumn("Name")
playersList:AddColumn("Status"):SetFixedWidth(65)
playersList:AddColumn("Rank"):SetFixedWidth(105)
playersList:AddColumn("XP / Score"):SetFixedWidth(90)
playersList:AddColumn("Credits"):SetFixedWidth(75)
playersList:AddColumn("Played"):SetFixedWidth(90)
if shopState.isAdmin then
playersList:AddColumn("SteamID64"):SetFixedWidth(145)
playersList:AddColumn("Model"):SetFixedWidth(115)
playersList:AddColumn("Weapon"):SetFixedWidth(125)
playersList:AddColumn("Trail"):SetFixedWidth(110)
playersList:AddColumn("Owned"):SetFixedWidth(55)
playersList.OnRowSelected = function(_, _, row)
if IsValid(playerDetails) and row.CustomCoopPlayer then
playerDetails:SetText(DescribeAdminPlayer(row.CustomCoopPlayer))
end
end
end
playersList.OnRowRightClick = function(_, _, row)
local record = row.CustomCoopPlayer
if not record then return end
sheet:AddSheet("Admin Players", page)
RefreshAdminPlayersView()
-- The shop-open request has a short server-side rate limit. Wait until it
-- expires before requesting the larger administrator-only dataset.
timer.Simple(0.3, function()
if IsValid(page) then
RequestAdminPlayers()
end
end)
local menu = DermaMenu()
menu:AddOption("Open Steam profile / add friend", function()
OpenSteamProfile(record)
end)
menu:AddOption("Copy SteamID64", function()
SetClipboardText(record.steamID64)
end)
menu:Open()
end
playersList.DoDoubleClick = function(_, _, row)
if row.CustomCoopPlayer then OpenSteamProfile(row.CustomCoopPlayer) end
end
sheet:AddSheet("Players", page)
RefreshPlayersView()
timer.Simple(0.3, function()
if IsValid(page) then RequestPlayers() end
end)
end
local function CaptureShopViewState()
if not IsValid(shopFrame) or not IsValid(shopFrame.CustomCoopSheet) then return end
local nextLivePlayerUpdate = 0
hook.Add("Think", "CustomCoopProgressionLivePlayerList", function()
if not IsValid(playersList) or RealTime() < nextLivePlayerUpdate then return end
nextLivePlayerUpdate = RealTime() + 0.25
local sheet = shopFrame.CustomCoopSheet
local activeTab = sheet:GetActiveTab()
if IsValid(activeTab) then shopViewState.activeTab = activeTab:GetText() end
local recordsBySteamID = {}
for _, record in ipairs(shopState.players) do
recordsBySteamID[record.steamID64] = record
end
for _, item in ipairs(sheet.Items or {}) do
if IsValid(item.Tab) and IsValid(item.Panel) and IsValid(item.Panel.CustomCoopScroll) then
shopViewState.scroll[item.Tab:GetText()] = item.Panel.CustomCoopScroll:GetVBar():GetScroll()
local onlineSteamIDs = {}
local changed = false
for _, ply in ipairs(player.GetAll()) do
if IsValid(ply) and not ply:IsBot() then
local steamID64 = ply:SteamID64()
if steamID64 and steamID64 ~= "" and steamID64 ~= "0" then
onlineSteamIDs[steamID64] = true
local record = recordsBySteamID[steamID64]
if not record then
record = {
steamID64 = steamID64,
playerName = ply:Nick(),
online = true,
score = 0,
credits = 0,
playtime = 0
}
shopState.players[#shopState.players + 1] = record
recordsBySteamID[steamID64] = record
changed = true
end
local playerName = ply:Nick()
local score = ply:GetNW2Int("CustomCoopScore", record.score)
local credits = ply:GetNW2Int("CustomCoopCredits", record.credits)
local playtime = ply:GetNW2Int("CustomCoopPlaytime", record.playtime)
if record.playerName ~= playerName or not record.online
or record.score ~= score or record.credits ~= credits or record.playtime ~= playtime then
record.playerName = playerName
record.online = true
record.score = score
record.credits = credits
record.playtime = playtime
changed = true
end
end
end
end
for _, record in ipairs(shopState.players) do
if record.online and not onlineSteamIDs[record.steamID64] then
record.online = false
changed = true
end
end
if not changed then return end
table.sort(shopState.players, function(left, right)
if left.score ~= right.score then return left.score > right.score end
if left.playtime ~= right.playtime then return left.playtime > right.playtime end
return string.lower(left.playerName) < string.lower(right.playerName)
end)
RefreshPlayersView()
end)
RefreshShopFrame = function()
if not IsValid(shopFrame) then return end
for _, card in ipairs(shopFrame.CustomCoopCards or {}) do
if IsValid(card) and card.CustomCoopRefresh then card.CustomCoopRefresh(true) end
end
for _, row in ipairs(shopFrame.CustomCoopPriceRows or {}) do
if IsValid(row) and row.CustomCoopItem then
row:SetColumnText(4, CurrentPrice(row.CustomCoopItemType, row.CustomCoopItem))
end
end
if IsValid(shopFrame.CustomCoopPriceSave) then
shopFrame.CustomCoopPriceSave:SetText("Save selected price")
shopFrame.CustomCoopPriceSave:SetEnabled(
IsValid(shopFrame.CustomCoopSelectedPriceRow and shopFrame.CustomCoopSelectedPriceRow())
)
end
end
local function OpenShop()
CaptureShopViewState()
if IsValid(shopFrame) then shopFrame:Remove() end
if IsValid(shopFrame) then
RefreshShopFrame()
return
end
adminPlayersList = nil
adminPlayersStatus = nil
adminPlayerDetails = nil
playersList = nil
playersStatus = nil
playerDetails = nil
shopFrame = vgui.Create("DFrame")
shopFrame:SetSize(math.min(ScrW() - 40, shopState.isAdmin and 1240 or 850), math.min(ScrH() - 40, 650))
@@ -577,6 +705,8 @@ local function OpenShop()
shopFrame:SetTitle("")
shopFrame:ShowCloseButton(true)
shopFrame:MakePopup()
shopFrame.CustomCoopCards = {}
shopFrame.CustomCoopPriceRows = {}
shopFrame.OnKeyCodePressed = function(self, keyCode)
if keyCode == KEY_F3 then
ToggleShop()
@@ -644,9 +774,9 @@ local function OpenShop()
CreateCataloguePage(sheet, "Player Trails", "trail", Progression.Trails)
CreateCataloguePage(sheet, "Entities", "entity", Progression.Entities)
CreateCataloguePage(sheet, "Helping NPCs", "helper", Progression.Helpers)
CreatePlayersPage(sheet)
if shopState.isAdmin then
CreateAdminPage(sheet)
CreateAdminPlayersPage(sheet)
end
sheet.OnActiveTabChanged = function(_, _, newTab)
@@ -707,23 +837,31 @@ net.Receive("CustomCoopProgressionOpenShop", function()
shopState.isAdmin = net.ReadBool()
shopState.prices = ReadShopPrices()
OpenShop()
if IsValid(shopFrame) then
RefreshShopFrame()
else
OpenShop()
end
end)
net.Receive("CustomCoopProgressionPricesChanged", function()
shopState.prices = ReadShopPrices()
if IsValid(shopFrame) then OpenShop() end
RefreshShopFrame()
notification.AddLegacy("Shop prices were updated by an administrator.", NOTIFY_GENERIC, 4)
end)
net.Receive("CustomCoopProgressionAdminPlayers", function()
net.Receive("CustomCoopProgressionPlayers", function()
local requestID = net.ReadUInt(16)
if requestID ~= adminPlayerRequestID then return end
if requestID ~= playerRequestID then return end
local reset = net.ReadBool()
local finished = net.ReadBool()
local hasDetails = net.ReadBool()
local count = net.ReadUInt(8)
if reset then shopState.adminPlayers = {} end
if reset then
shopState.players = {}
shopState.playersHaveDetails = hasDetails
end
for _ = 1, count do
local record = {
@@ -732,28 +870,30 @@ net.Receive("CustomCoopProgressionAdminPlayers", function()
online = net.ReadBool(),
score = net.ReadUInt(32),
credits = net.ReadUInt(32),
playtime = net.ReadUInt(32),
equippedModel = net.ReadString(),
equippedWeapon = net.ReadString(),
equippedTrail = net.ReadString(),
purchases = {},
enabledBehaviours = {}
playtime = net.ReadUInt(32)
}
for _ = 1, net.ReadUInt(8) do
record.purchases[#record.purchases + 1] = {
itemType = net.ReadString(),
itemID = net.ReadString(),
purchasedAt = net.ReadUInt(32)
}
if hasDetails then
record.equippedModel = net.ReadString()
record.equippedWeapon = net.ReadString()
record.equippedTrail = net.ReadString()
record.purchases = {}
record.enabledBehaviours = {}
for _ = 1, net.ReadUInt(8) do
record.purchases[#record.purchases + 1] = {
itemType = net.ReadString(),
itemID = net.ReadString(),
purchasedAt = net.ReadUInt(32)
}
end
for _ = 1, net.ReadUInt(8) do
record.enabledBehaviours[#record.enabledBehaviours + 1] = net.ReadString()
end
end
for _ = 1, net.ReadUInt(8) do
record.enabledBehaviours[#record.enabledBehaviours + 1] = net.ReadString()
end
shopState.adminPlayers[#shopState.adminPlayers + 1] = record
shopState.players[#shopState.players + 1] = record
end
shopState.adminPlayersLoading = not finished
RefreshAdminPlayersView()
shopState.playersLoading = not finished
RefreshPlayersView()
end)
net.Receive("CustomCoopProgressionNotice", function()
+14
View File
@@ -211,6 +211,20 @@ Progression.Weapons = {
price = 1000,
description = "A permanent weapon from a galaxy far, far away."
},
{
id = "haax_gun",
name = "HAAAX! SWEP",
weaponClass = "weapon_haxgun",
price = 2000,
description = "Weaponized error spam and terminal trouble. Unlocks permanently."
},
{
id = "popcorn_spammable",
name = "Popcorn Spammable",
weaponClass = "weapon_popcorn_spam",
price = 750,
description = "Cover the battlefield in an unreasonable amount of popcorn."
},
{
id = "orbital_friendship_cannon",
name = "Orbital Friendship Cannon",
+105 -31
View File
@@ -13,6 +13,7 @@ end
runtime.playerStates = runtime.playerStates or setmetatable({}, { __mode = "k" })
runtime.disconnectedStates = runtime.disconnectedStates or {}
runtime.requestCooldowns = runtime.requestCooldowns or setmetatable({}, { __mode = "k" })
runtime.publicPlayerRequestCooldowns = runtime.publicPlayerRequestCooldowns or setmetatable({}, { __mode = "k" })
runtime.purchasedSpawns = runtime.purchasedSpawns or setmetatable({}, { __mode = "k" })
runtime.shopPrices = runtime.shopPrices or {}
runtime.dirtyShopPrices = runtime.dirtyShopPrices or {}
@@ -21,6 +22,7 @@ runtime.availableMusic = runtime.availableMusic or {}
local playerStates = runtime.playerStates
local disconnectedStates = runtime.disconnectedStates
local requestCooldowns = runtime.requestCooldowns
local publicPlayerRequestCooldowns = runtime.publicPlayerRequestCooldowns
local purchasedSpawns = runtime.purchasedSpawns
local shopPrices = runtime.shopPrices
local dirtyShopPrices = runtime.dirtyShopPrices
@@ -43,8 +45,8 @@ util.AddNetworkString("CustomCoopProgressionSetPrice")
util.AddNetworkString("CustomCoopProgressionPricesChanged")
util.AddNetworkString("CustomCoopProgressionPlayMusic")
util.AddNetworkString("CustomCoopProgressionToggleMusicMute")
util.AddNetworkString("CustomCoopProgressionRequestAdminPlayers")
util.AddNetworkString("CustomCoopProgressionAdminPlayers")
util.AddNetworkString("CustomCoopProgressionRequestPlayers")
util.AddNetworkString("CustomCoopProgressionPlayers")
local function Query(query, context)
local startedAt = SysTime()
@@ -916,18 +918,83 @@ local function LoadAdminPlayerRecords()
end
table.sort(records, function(left, right)
if left.score ~= right.score then return left.score > right.score end
if left.credits ~= right.credits then return left.credits > right.credits end
if left.playtime ~= right.playtime then return left.playtime > right.playtime end
return string.lower(left.playerName) < string.lower(right.playerName)
end)
return records
end
local function SendAdminPlayerBatch(ply, requestID, records, firstIndex, lastIndex)
local function LoadPublicPlayerRecords()
local recordsBySteamID = {}
local records = {}
local function GetRecord(steamID64)
steamID64 = tostring(steamID64 or "")
if steamID64 == "" then return end
if recordsBySteamID[steamID64] then return recordsBySteamID[steamID64] end
local record = {
steamID64 = steamID64,
playerName = steamID64,
online = false,
score = 0,
credits = 0,
playtime = 0
}
recordsBySteamID[steamID64] = record
records[#records + 1] = record
return record
end
local rows = Query(
"SELECT steamid64, player_name, lifetime_score, credits, playtime_seconds " ..
"FROM custom_coop_players",
"could not load public player list"
)
if istable(rows) then
for _, row in ipairs(rows) do
local record = GetRecord(row.steamid64)
if record then
record.playerName = tostring(row.player_name or record.steamID64)
record.score = math.max(math.floor(tonumber(row.lifetime_score) or 0), 0)
record.credits = math.max(math.floor(tonumber(row.credits) or 0), 0)
record.playtime = math.max(math.floor(tonumber(row.playtime_seconds) or 0), 0)
end
end
end
local function ApplyState(record, state, playerName, online)
if not record or not state then return end
record.playerName = tostring(playerName or state.playerName or record.playerName)
record.online = online == true
record.score = math.max(math.floor(tonumber(state.score) or 0), 0)
record.credits = math.max(math.floor(tonumber(state.credits) or 0), 0)
record.playtime = math.max(math.floor(tonumber(state.playtimeSeconds) or 0), 0)
end
for ply, state in pairs(playerStates) do
if IsValid(ply) then AccruePlaytime(ply, state) end
ApplyState(GetRecord(state.steamID64), state, IsValid(ply) and ply:Nick() or nil, IsValid(ply))
end
for steamID64, state in pairs(disconnectedStates) do
ApplyState(GetRecord(steamID64), state, nil, false)
end
table.sort(records, function(left, right)
if left.score ~= right.score then return left.score > right.score end
if left.playtime ~= right.playtime then return left.playtime > right.playtime end
return string.lower(left.playerName) < string.lower(right.playerName)
end)
return records
end
local function SendPlayerBatch(ply, requestID, records, firstIndex, lastIndex, includeDetails)
local count = math.max(lastIndex - firstIndex + 1, 0)
net.Start("CustomCoopProgressionAdminPlayers")
net.Start("CustomCoopProgressionPlayers")
net.WriteUInt(requestID, 16)
net.WriteBool(firstIndex == 1)
net.WriteBool(lastIndex >= #records)
net.WriteBool(includeDetails)
net.WriteUInt(math.min(count, 255), 8)
for index = firstIndex, lastIndex do
local record = records[index]
@@ -937,34 +1004,44 @@ local function SendAdminPlayerBatch(ply, requestID, records, firstIndex, lastInd
net.WriteUInt(math.min(record.score, 4294967295), 32)
net.WriteUInt(math.min(record.credits, 4294967295), 32)
net.WriteUInt(math.min(record.playtime, 4294967295), 32)
net.WriteString(record.equippedModel)
net.WriteString(record.equippedWeapon)
net.WriteString(record.equippedTrail)
net.WriteUInt(math.min(#record.purchases, 255), 8)
for purchaseIndex = 1, math.min(#record.purchases, 255) do
local purchase = record.purchases[purchaseIndex]
net.WriteString(purchase.itemType)
net.WriteString(purchase.itemID)
net.WriteUInt(math.min(purchase.purchasedAt, 4294967295), 32)
end
net.WriteUInt(math.min(#record.enabledBehaviours, 255), 8)
for behaviourIndex = 1, math.min(#record.enabledBehaviours, 255) do
net.WriteString(record.enabledBehaviours[behaviourIndex])
if includeDetails then
net.WriteString(record.equippedModel)
net.WriteString(record.equippedWeapon)
net.WriteString(record.equippedTrail)
net.WriteUInt(math.min(#record.purchases, 255), 8)
for purchaseIndex = 1, math.min(#record.purchases, 255) do
local purchase = record.purchases[purchaseIndex]
net.WriteString(purchase.itemType)
net.WriteString(purchase.itemID)
net.WriteUInt(math.min(purchase.purchasedAt, 4294967295), 32)
end
net.WriteUInt(math.min(#record.enabledBehaviours, 255), 8)
for behaviourIndex = 1, math.min(#record.enabledBehaviours, 255) do
net.WriteString(record.enabledBehaviours[behaviourIndex])
end
end
end
net.Send(ply)
end
local function SendAdminPlayers(ply, requestID)
local records = LoadAdminPlayerRecords()
local function SendPlayers(ply, requestID)
local includeDetails = ply:IsAdmin()
local records = includeDetails and LoadAdminPlayerRecords() or LoadPublicPlayerRecords()
if #records == 0 then
SendAdminPlayerBatch(ply, requestID, records, 1, 0)
SendPlayerBatch(ply, requestID, records, 1, 0, includeDetails)
return
end
local batchSize = 25
local batchSize = includeDetails and 25 or 50
for firstIndex = 1, #records, batchSize do
SendAdminPlayerBatch(ply, requestID, records, firstIndex, math.min(firstIndex + batchSize - 1, #records))
SendPlayerBatch(
ply,
requestID,
records,
firstIndex,
math.min(firstIndex + batchSize - 1, #records),
includeDetails
)
end
end
@@ -1611,15 +1688,12 @@ net.Receive("CustomCoopProgressionRequestShop", function(_, ply)
SendShop(ply)
end)
net.Receive("CustomCoopProgressionRequestAdminPlayers", function(_, ply)
net.Receive("CustomCoopProgressionRequestPlayers", function(_, ply)
if not RequestAllowed(ply) then return end
local requestID = net.ReadUInt(16)
if not ply:IsAdmin() then
SendNotice(ply, false, "Only server administrators can view all player records.")
return
end
SendAdminPlayers(ply, requestID)
local now = CurTime()
if (publicPlayerRequestCooldowns[ply] or 0) > now then return end
publicPlayerRequestCooldowns[ply] = now + 2
SendPlayers(ply, net.ReadUInt(16))
end)
net.Receive("CustomCoopProgressionSelectItem", function(_, ply)