Make players tab public but advanced for admins
This commit is contained in:
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user