Music fixes and admin price override
This commit is contained in:
@@ -24,6 +24,11 @@ Music category. Buying a song unlocks it permanently; selecting Play broadcasts
|
||||
it to all connected players. One song can play at a time, with a short cooldown
|
||||
afterward. Each player can mute or adjust the music locally.
|
||||
|
||||
Music entries may set `spatial = true` plus `minimumDistance` and
|
||||
`maximumDistance`. Spatial music follows the buyer's character while they move
|
||||
and fades over distance. Supply spatial audio as 44.1 kHz mono MP3 or WAV for
|
||||
reliable in-game positioning.
|
||||
|
||||
The server checks all kills, balances, live prices, ownership, equipment, and
|
||||
music playback. A purchase request contains only the item type and ID: clients
|
||||
cannot award points, submit a purchase price, or debit their own Credits.
|
||||
@@ -31,6 +36,10 @@ 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.
|
||||
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.
|
||||
The equivalent server/admin console command is:
|
||||
|
||||
`coop_setprice <model|weapon|music> <item_id> <credits>`
|
||||
@@ -52,8 +61,9 @@ restarts.
|
||||
|
||||
Music files are not bundled with this repository. Every server owner must supply
|
||||
the audio files referenced by their `Progression.Music` entries, place them
|
||||
under `garrysmod/sound/`, and register their virtual paths with
|
||||
`resource.AddFile` so connecting clients receive them. Keep locally supplied
|
||||
audio outside the gamemode source tree and distribute or back it up separately.
|
||||
under `garrysmod/sound/`, and reconnect clients so the server can send them. The
|
||||
gamemode automatically registers every configured file with `resource.AddFile`.
|
||||
Keep locally supplied audio outside the gamemode source tree and distribute or
|
||||
back it up separately.
|
||||
|
||||
Player-spawned NPCs and unknown/friendly NPC classes do not award points.
|
||||
|
||||
@@ -15,6 +15,8 @@ local shopState = {
|
||||
isAdmin = false
|
||||
}
|
||||
local activeMusicChannel
|
||||
local activeMusicSource
|
||||
local activeMusicIsSpatial = false
|
||||
local previousMusicVolume = 0.7
|
||||
local musicVolume = CreateClientConVar(
|
||||
"coop_music_volume", "0.7", true, false,
|
||||
@@ -419,16 +421,30 @@ net.Receive("CustomCoopProgressionToggleMusicMute", ToggleMusicMute)
|
||||
|
||||
net.Receive("CustomCoopProgressionPlayMusic", function()
|
||||
local itemID = net.ReadString()
|
||||
local sourcePlayer = net.ReadEntity()
|
||||
local buyerName = net.ReadString()
|
||||
local item = Progression.GetMusic(itemID)
|
||||
if not item then return end
|
||||
|
||||
local soundPath = "sound/" .. item.sound
|
||||
if not file.Exists(soundPath, "GAME") then
|
||||
notification.AddLegacy(
|
||||
"Music file missing on this client. Reconnect once to download server music.",
|
||||
NOTIFY_ERROR,
|
||||
8
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
if IsValid(activeMusicChannel) then
|
||||
activeMusicChannel:Stop()
|
||||
activeMusicChannel = nil
|
||||
end
|
||||
activeMusicSource = nil
|
||||
activeMusicIsSpatial = item.spatial == true
|
||||
|
||||
sound.PlayFile("sound/" .. item.sound, "noplay", function(channel, errorID, errorName)
|
||||
local flags = activeMusicIsSpatial and "3d noplay" or "noplay"
|
||||
sound.PlayFile(soundPath, flags, function(channel, errorID, errorName)
|
||||
if not IsValid(channel) then
|
||||
notification.AddLegacy(
|
||||
"Could not play " .. item.name .. ": " .. tostring(errorName or errorID),
|
||||
@@ -439,12 +455,34 @@ net.Receive("CustomCoopProgressionPlayMusic", function()
|
||||
end
|
||||
|
||||
activeMusicChannel = channel
|
||||
channel:SetVolume(math.Clamp(musicVolume:GetFloat(), 0, 1))
|
||||
activeMusicSource = sourcePlayer
|
||||
local volume = math.Clamp(musicVolume:GetFloat(), 0, 1)
|
||||
channel:SetVolume(volume)
|
||||
if activeMusicIsSpatial and IsValid(activeMusicSource) then
|
||||
local minimumDistance = math.max(tonumber(item.minimumDistance) or 120, 1)
|
||||
local maximumDistance = math.max(tonumber(item.maximumDistance) or 2200, minimumDistance + 1)
|
||||
channel:Set3DFadeDistance(minimumDistance, maximumDistance)
|
||||
channel:SetPos(activeMusicSource:WorldSpaceCenter())
|
||||
end
|
||||
channel:Play()
|
||||
notification.AddLegacy(buyerName .. " is playing " .. item.name .. ".", NOTIFY_GENERIC, 5)
|
||||
if volume <= 0.001 then
|
||||
notification.AddLegacy(
|
||||
buyerName .. " is playing " .. item.name .. ", but your shop music is muted. Type !musicmute to hear it.",
|
||||
NOTIFY_ERROR,
|
||||
7
|
||||
)
|
||||
else
|
||||
notification.AddLegacy(buyerName .. " is playing " .. item.name .. ".", NOTIFY_GENERIC, 5)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
hook.Add("Think", "CustomCoopProgressionMoveMusicSource", function()
|
||||
if not activeMusicIsSpatial or not IsValid(activeMusicChannel) then return end
|
||||
if not IsValid(activeMusicSource) then return end
|
||||
activeMusicChannel:SetPos(activeMusicSource:WorldSpaceCenter())
|
||||
end)
|
||||
|
||||
local previousScore
|
||||
local gainedScore = 0
|
||||
local gainExpires = 0
|
||||
|
||||
@@ -21,4 +21,3 @@ include( "maprotation.lua" )
|
||||
include( "sv_progression.lua" )
|
||||
|
||||
resource.AddFile( "sound/weapons/universal/pap.wav" )
|
||||
resource.AddFile( "sound/custom_coop_music/trololo.mp3" )
|
||||
|
||||
@@ -11,6 +11,7 @@ Progression.DefaultModelID = "kleiner"
|
||||
Progression.DefaultWeaponID = "none"
|
||||
Progression.MaximumShopPrice = 100000000
|
||||
Progression.MusicCooldownPadding = 10
|
||||
Progression.AdminShopTestingBypass = true
|
||||
|
||||
Progression.Ranks = {
|
||||
{ score = 0, name = "Newbie" },
|
||||
@@ -154,7 +155,10 @@ Progression.Music = {
|
||||
sound = "custom_coop_music/trololo.mp3",
|
||||
price = 250,
|
||||
duration = 162,
|
||||
description = "Play the full song for everybody currently on the server. Unlocks permanently."
|
||||
spatial = true,
|
||||
minimumDistance = 120,
|
||||
maximumDistance = 2200,
|
||||
description = "Your character plays this song as moving, positional audio. Unlocks permanently."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+41
-11
@@ -68,6 +68,15 @@ local catalogues = {
|
||||
{ itemType = "music", items = Progression.Music }
|
||||
}
|
||||
|
||||
for _, item in ipairs(Progression.Music) do
|
||||
local soundPath = "sound/" .. item.sound
|
||||
if file.Exists(soundPath, "GAME") then
|
||||
resource.AddFile(soundPath)
|
||||
else
|
||||
ErrorNoHalt("[custom_coop progression] Missing configured music file " .. soundPath .. "\n")
|
||||
end
|
||||
end
|
||||
|
||||
local function PriceKey(itemType, itemID)
|
||||
return itemType .. ":" .. itemID
|
||||
end
|
||||
@@ -337,7 +346,7 @@ local function SetShopPrice(actor, itemType, itemID, requestedPrice)
|
||||
return true
|
||||
end
|
||||
|
||||
local function PlayMusic(ply, item)
|
||||
local function PlayMusic(ply, item, successMessage)
|
||||
local now = CurTime()
|
||||
if musicBusyUntil > now then
|
||||
SendNotice(ply, false, string.format(
|
||||
@@ -357,11 +366,13 @@ local function PlayMusic(ply, item)
|
||||
|
||||
net.Start("CustomCoopProgressionPlayMusic")
|
||||
net.WriteString(item.id)
|
||||
net.WriteEntity(ply)
|
||||
net.WriteString(ply:Nick())
|
||||
net.Broadcast()
|
||||
|
||||
PrintMessage(HUD_PRINTTALK, string.format("[CO-OP] %s is playing %s.", ply:Nick(), item.name))
|
||||
SendNotice(ply, true, "Playing " .. item.name .. " for everyone.")
|
||||
local playbackTarget = item.spatial and " from your character." or " for everyone."
|
||||
SendNotice(ply, true, successMessage or ("Playing " .. item.name .. playbackTarget))
|
||||
end
|
||||
|
||||
local function PurchaseOrEquip(ply, itemType, itemID)
|
||||
@@ -387,6 +398,7 @@ local function PurchaseOrEquip(ply, itemType, itemID)
|
||||
local price = GetShopPrice(itemType, item)
|
||||
local alreadyOwned = ownedItems[itemID] == true or price == 0
|
||||
local previousWeaponID = state.equippedWeapon
|
||||
local adminCreditBypass = false
|
||||
|
||||
if itemType == "music" and alreadyOwned then
|
||||
PlayMusic(ply, item)
|
||||
@@ -396,8 +408,16 @@ local function PurchaseOrEquip(ply, itemType, itemID)
|
||||
|
||||
if not alreadyOwned then
|
||||
if state.credits < price then
|
||||
SendNotice(ply, false, "You need " .. (price - state.credits) .. " more credits.")
|
||||
return
|
||||
adminCreditBypass = Progression.AdminShopTestingBypass and ply:IsAdmin()
|
||||
if not adminCreditBypass then
|
||||
SendNotice(ply, false, "You need " .. (price - state.credits) .. " more credits.")
|
||||
return
|
||||
end
|
||||
|
||||
SendNotice(ply, false, string.format(
|
||||
"You need %d more credits. Admin testing override: no credits will be charged.",
|
||||
price - state.credits
|
||||
))
|
||||
end
|
||||
|
||||
sql.Begin()
|
||||
@@ -407,10 +427,13 @@ local function PurchaseOrEquip(ply, itemType, itemID)
|
||||
SQLString(ply:SteamID64()), SQLString(itemType), SQLString(itemID), os.time()
|
||||
), "could not store purchase")
|
||||
|
||||
local updateResult = insertResult ~= false and Query(string.format(
|
||||
"UPDATE custom_coop_players SET credits = credits - %d WHERE steamid64 = %s AND credits >= %d",
|
||||
price, SQLString(ply:SteamID64()), price
|
||||
), "could not debit purchase")
|
||||
local updateResult = true
|
||||
if insertResult ~= false and not adminCreditBypass then
|
||||
updateResult = Query(string.format(
|
||||
"UPDATE custom_coop_players SET credits = credits - %d WHERE steamid64 = %s AND credits >= %d",
|
||||
price, SQLString(ply:SteamID64()), price
|
||||
), "could not debit purchase")
|
||||
end
|
||||
|
||||
if insertResult == false or updateResult == false then
|
||||
sql.Query("ROLLBACK")
|
||||
@@ -419,13 +442,17 @@ local function PurchaseOrEquip(ply, itemType, itemID)
|
||||
end
|
||||
sql.Commit()
|
||||
|
||||
state.credits = state.credits - price
|
||||
if not adminCreditBypass then state.credits = state.credits - price end
|
||||
ownedItems[itemID] = true
|
||||
end
|
||||
|
||||
if itemType == "music" then
|
||||
SyncPlayer(ply, state)
|
||||
SendNotice(ply, true, "Purchased " .. item.name .. ". Select it again to play it for everyone.")
|
||||
local playbackTarget = item.spatial and " from your character." or " for everyone."
|
||||
local message = adminCreditBypass
|
||||
and ("Admin test-unlocked and playing " .. item.name .. playbackTarget)
|
||||
or ("Purchased and playing " .. item.name .. playbackTarget)
|
||||
PlayMusic(ply, item, message)
|
||||
SendShop(ply)
|
||||
return
|
||||
end
|
||||
@@ -452,7 +479,10 @@ local function PurchaseOrEquip(ply, itemType, itemID)
|
||||
end
|
||||
|
||||
SyncPlayer(ply, state)
|
||||
SendNotice(ply, true, (alreadyOwned and "Equipped " or "Purchased and equipped ") .. item.name .. ".")
|
||||
local action = alreadyOwned and "Equipped "
|
||||
or adminCreditBypass and "Admin test-unlocked and equipped "
|
||||
or "Purchased and equipped "
|
||||
SendNotice(ply, true, action .. item.name .. ".")
|
||||
SendShop(ply)
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user