Files
wnsrc/gamemodes/terrortown/gamemode/ent_replace.lua
lifestorm 94063e4369 Upload
2024-08-04 22:55:00 +03:00

610 lines
16 KiB
Lua

--[[
| 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/
--]]
---- Replace old and boring ents with new and shiny SENTs
ents.TTT = {}
local table = table
local math = math
local pairs = pairs
local function ReplaceSingle(ent, newname)
-- Ammo that has been mapper-placed will not have a pos yet at this point for
-- reasons that have to do with being really annoying. So don't touch those
-- so we can replace them later. Grumble grumble.
if ent:GetPos() == vector_origin then
return
end
ent:SetSolid(SOLID_NONE)
local rent = ents.Create(newname)
rent:SetPos(ent:GetPos())
rent:SetAngles(ent:GetAngles())
rent:Spawn()
rent:Activate()
rent:PhysWake()
ent:Remove()
end
local hl2_ammo_replace = {
["item_ammo_pistol"] = "item_ammo_pistol_ttt",
["item_box_buckshot"] = "item_box_buckshot_ttt",
["item_ammo_smg1"] = "item_ammo_smg1_ttt",
["item_ammo_357"] = "item_ammo_357_ttt",
["item_ammo_357_large"] = "item_ammo_357_ttt",
["item_ammo_revolver"] = "item_ammo_revolver_ttt", -- zm
["item_ammo_ar2"] = "item_ammo_pistol_ttt",
["item_ammo_ar2_large"] = "item_ammo_smg1_ttt",
["item_ammo_smg1_grenade"] = "weapon_zm_pistol",
["item_battery"] = "item_ammo_357_ttt",
["item_healthkit"] = "weapon_zm_shotgun",
["item_suitcharger"] = "weapon_zm_mac10",
["item_ammo_ar2_altfire"] = "weapon_zm_mac10",
["item_rpg_round"] = "item_ammo_357_ttt",
["item_ammo_crossbow"] = "item_box_buckshot_ttt",
["item_healthvial"] = "weapon_zm_molotov",
["item_healthcharger"] = "item_ammo_revolver_ttt",
["item_ammo_crate"] = "weapon_ttt_confgrenade",
["item_item_crate"] = "ttt_random_ammo"
};
-- Replace an ammo entity with the TTT version
-- Optional cls param is the classname, if the caller already has it handy
local function ReplaceAmmoSingle(ent, cls)
if cls == nil then cls = ent:GetClass() end
local rpl = hl2_ammo_replace[cls]
if rpl then
ReplaceSingle(ent, rpl)
end
end
local function ReplaceAmmo()
for _, ent in ipairs(ents.FindByClass("item_*")) do
ReplaceAmmoSingle(ent)
end
end
local hl2_weapon_replace = {
["weapon_smg1"] = "weapon_zm_mac10",
["weapon_shotgun"] = "weapon_zm_shotgun",
["weapon_ar2"] = "weapon_ttt_m16",
["weapon_357"] = "weapon_zm_rifle",
["weapon_crossbow"] = "weapon_zm_pistol",
["weapon_rpg"] = "weapon_zm_sledge",
["weapon_slam"] = "item_ammo_pistol_ttt",
["weapon_frag"] = "weapon_zm_revolver",
["weapon_crowbar"] = "weapon_zm_molotov"
};
local function ReplaceWeaponSingle(ent, cls)
-- Loadout weapons immune
-- we use a SWEP-set property because at this state all SWEPs identify as weapon_swep
if ent.AllowDelete == false then
return
else
if cls == nil then cls = ent:GetClass() end
local rpl = hl2_weapon_replace[cls]
if rpl then
ReplaceSingle(ent, rpl)
end
end
end
local function ReplaceWeapons()
for _, ent in ipairs(ents.FindByClass("weapon_*")) do
ReplaceWeaponSingle(ent)
end
end
-- Remove ZM ragdolls that don't work, AND old player ragdolls.
-- Exposed because it's also done at BeginRound
function ents.TTT.RemoveRagdolls(player_only)
for k, ent in ipairs(ents.FindByClass("prop_ragdoll")) do
if IsValid(ent) then
if not player_only and string.find(ent:GetModel(), "zm_", 6, true) then
ent:Remove()
elseif ent.player_ragdoll then
-- cleanup ought to catch these but you know
ent:Remove()
end
end
end
end
-- People spawn with these, so remove any pickups (ZM maps have them)
local function RemoveCrowbars()
for k, ent in ipairs(ents.FindByClass("weapon_zm_improvised")) do
ent:Remove()
end
end
function ents.TTT.ReplaceEntities()
ReplaceAmmo()
ReplaceWeapons()
RemoveCrowbars()
ents.TTT.RemoveRagdolls()
end
local cls = "" -- avoid allocating
local sub = string.sub
local function ReplaceOnCreated(s, ent)
-- Invalid ents are of no use anyway
if not ent:IsValid() then return end
cls = ent:GetClass()
if sub(cls, 1, 4) == "item" then
ReplaceAmmoSingle(ent, cls)
elseif sub(cls, 1, 6) == "weapon" then
ReplaceWeaponSingle(ent, cls)
end
end
local noop = util.noop
GM.OnEntityCreated = ReplaceOnCreated
-- Helper so we can easily turn off replacement stuff when we don't need it
function ents.TTT.SetReplaceChecking(state)
if state then
GAMEMODE.OnEntityCreated = ReplaceOnCreated
else
GAMEMODE.OnEntityCreated = noop
end
end
-- GMod's game.CleanUpMap destroys rope entities that are parented. This is an
-- experimental fix where the rope is unparented, the map cleaned, and then the
-- rope reparented.
-- Same happens for func_brush.
local broken_parenting_ents = {
"move_rope",
"keyframe_rope",
"info_target",
"func_brush"
}
function ents.TTT.FixParentedPreCleanup()
for _, rcls in pairs(broken_parenting_ents) do
for k,v in ipairs(ents.FindByClass(rcls)) do
if v.GetParent and IsValid(v:GetParent()) then
v.CachedParentName = v:GetParent():GetName()
v:SetParent(nil)
if not v.OrigPos then
v.OrigPos = v:GetPos()
end
end
end
end
end
function ents.TTT.FixParentedPostCleanup()
for _, rcls in pairs(broken_parenting_ents) do
for k,v in ipairs(ents.FindByClass(rcls)) do
if v.CachedParentName then
if v.OrigPos then
v:SetPos(v.OrigPos)
end
local parents = ents.FindByName(v.CachedParentName)
if #parents == 1 then
local parent = parents[1]
v:SetParent(parent)
end
end
end
end
end
function ents.TTT.TriggerRoundStateOutputs(r, param)
r = r or GetRoundState()
for _, ent in ipairs(ents.FindByClass("ttt_map_settings")) do
if IsValid(ent) then
ent:RoundStateTrigger(r, param)
end
end
end
-- CS:S and TF2 maps have a bunch of ents we'd like to abuse for weapon spawns,
-- but to do that we need to register a SENT with their class name, else they
-- will just error out and we can't do anything with them.
local dummify = {
-- CS:S
"hostage_entity",
-- TF2
"item_ammopack_full",
"item_ammopack_medium",
"item_ammopack_small",
"item_healthkit_full",
"item_healthkit_medium",
"item_healthkit_small",
"item_teamflag",
"game_intro_viewpoint",
"info_observer_point",
"team_control_point",
"team_control_point_master",
"team_control_point_round",
-- ZM
"item_ammo_revolver"
};
for k, cls in pairs(dummify) do
scripted_ents.Register({Type="point", IsWeaponDummy=true}, cls)
end
-- Cache this, every ttt_random_weapon uses it in its Init
local SpawnableSWEPs = nil
function ents.TTT.GetSpawnableSWEPs()
if not SpawnableSWEPs then
local tbl = {}
for k,v in pairs(weapons.GetList()) do
if v and v.AutoSpawnable and (not WEPS.IsEquipment(v)) then
table.insert(tbl, v)
end
end
SpawnableSWEPs = tbl
end
return SpawnableSWEPs
end
local SpawnableAmmoClasses = nil
function ents.TTT.GetSpawnableAmmo()
if not SpawnableAmmoClasses then
local tbl = {}
for k,v in pairs(scripted_ents.GetList()) do
if v and (v.AutoSpawnable or (v.t and v.t.AutoSpawnable)) then
table.insert(tbl, k)
end
end
SpawnableAmmoClasses = tbl
end
return SpawnableAmmoClasses
end
local function PlaceWeapon(swep, pos, ang)
local cls = swep and WEPS.GetClass(swep)
if not cls then return end
-- Create the weapon, somewhat in the air in case the spot hugs the ground.
local ent = ents.Create(cls)
pos.z = pos.z + 3
ent:SetPos(pos)
ent:SetAngles(VectorRand():Angle())
ent:Spawn()
-- Create some associated ammo (if any)
if ent.AmmoEnt then
for i=1, math.random(0,3) do
local ammo = ents.Create(ent.AmmoEnt)
if IsValid(ammo) then
pos.z = pos.z + 2
ammo:SetPos(pos)
ammo:SetAngles(VectorRand():Angle())
ammo:Spawn()
ammo:PhysWake()
end
end
end
return ent
end
-- Spawns a bunch of guns (scaling with maxplayers count or
-- by ttt_weapon_spawn_max cvar) at randomly selected
-- entities of the classes given the table
local function PlaceWeaponsAtEnts(spots_classes)
local spots = {}
for _, s in pairs(spots_classes) do
for _, e in ipairs(ents.FindByClass(s)) do
table.insert(spots, e)
end
end
local spawnables = ents.TTT.GetSpawnableSWEPs()
local max = GetConVar( "ttt_weapon_spawn_count" ):GetInt()
if max == 0 then
max = game.MaxPlayers()
max = max + math.max(3, 0.33 * max)
end
local num = 0
local w = nil
for k, v in RandomPairs(spots) do
w = table.Random(spawnables)
if w and IsValid(v) and util.IsInWorld(v:GetPos()) then
local spawned = PlaceWeapon(w, v:GetPos(), v:GetAngles())
num = num + 1
-- People with only a grenade are sad pandas. To get IsGrenade here,
-- we need the spawned ent that has inherited the goods from the
-- basegrenade swep.
if spawned and spawned.IsGrenade then
w = table.Random(spawnables)
if w then
PlaceWeapon(w, v:GetPos(), v:GetAngles())
end
end
end
if num > max then
return
end
end
end
local function PlaceExtraWeaponsForCSS()
MsgN("Weaponless CS:S-like map detected. Placing extra guns.")
local spots_classes = {
"info_player_terrorist",
"info_player_counterterrorist",
"hostage_entity"
};
PlaceWeaponsAtEnts(spots_classes)
end
-- TF2 actually has ammo ents and such, but unlike HL2DM there are not enough
-- different entities to do replacement.
local function PlaceExtraWeaponsForTF2()
MsgN("Weaponless TF2-like map detected. Placing extra guns.")
local spots_classes = {
"info_player_teamspawn",
"team_control_point",
"team_control_point_master",
"team_control_point_round",
"item_ammopack_full",
"item_ammopack_medium",
"item_ammopack_small",
"item_healthkit_full",
"item_healthkit_medium",
"item_healthkit_small",
"item_teamflag",
"game_intro_viewpoint",
"info_observer_point"
};
PlaceWeaponsAtEnts(spots_classes)
end
-- If there are no guns on the map, see if this looks like a TF2/CS:S map and
-- act appropriately
function ents.TTT.PlaceExtraWeapons()
-- If ents.FindByClass is constructed lazily or is an iterator, doing a
-- single loop should be faster than checking the table size.
-- Get out of here if there exists any weapon at all
for k,v in ipairs(ents.FindByClass("weapon_*")) do
-- See if it's the kind of thing we would spawn, to avoid the carry weapon
-- and such. Owned weapons are leftovers on players that will go away.
if IsValid(v) and v.AutoSpawnable and not IsValid(v:GetOwner()) then
return
end
end
-- All current TTT mappers use these, so if we find one we're good
for k,v in ipairs(ents.FindByClass("info_player_deathmatch")) do return end
-- CT spawns on the other hand are unlikely to be seen outside CS:S maps
for k,v in ipairs(ents.FindByClass("info_player_counterterrorist")) do
PlaceExtraWeaponsForCSS()
return
end
-- And same for TF2 team spawns
for k,v in ipairs(ents.FindByClass("info_player_teamspawn")) do
PlaceExtraWeaponsForTF2()
return
end
end
---- Weapon/ammo placement script importing
local function RemoveReplaceables()
-- This could be transformed into lots of FindByClass searches, one for every
-- key in the replace tables. Hopefully this is faster as more of the work is
-- done on the C side. Hard to measure.
for _, ent in ipairs(ents.FindByClass("item_*")) do
if hl2_ammo_replace[ent:GetClass()] then
ent:Remove()
end
end
for _, ent in ipairs(ents.FindByClass("weapon_*")) do
if hl2_weapon_replace[ent:GetClass()] then
ent:Remove()
end
end
end
local function RemoveWeaponEntities()
RemoveReplaceables()
for _, cls in pairs(ents.TTT.GetSpawnableAmmo()) do
for k, ent in ipairs(ents.FindByClass(cls)) do
ent:Remove()
end
end
for _, sw in pairs(ents.TTT.GetSpawnableSWEPs()) do
local cn = WEPS.GetClass(sw)
for k, ent in ipairs(ents.FindByClass(cn)) do
ent:Remove()
end
end
ents.TTT.RemoveRagdolls(false)
RemoveCrowbars()
end
local function RemoveSpawnEntities()
for k, ent in pairs(GetSpawnEnts(false, true)) do
ent.BeingRemoved = true -- they're not gone til next tick
ent:Remove()
end
end
local function CreateImportedEnt(cls, pos, ang, kv)
if not cls or not pos or not ang or not kv then return false end
local ent = ents.Create(cls)
if not IsValid(ent) then return false end
ent:SetPos(pos)
ent:SetAngles(ang)
for k,v in pairs(kv) do
ent:SetKeyValue(k, v)
end
ent:Spawn()
ent:PhysWake()
return true
end
function ents.TTT.CanImportEntities(map)
if not tostring(map) then return false end
if not GetConVar("ttt_use_weapon_spawn_scripts"):GetBool() then return false end
local fname = "maps/" .. map .. "_ttt.txt"
return file.Exists(fname, "GAME")
end
local function ImportSettings(map)
if not ents.TTT.CanImportEntities(map) then return end
local fname = "maps/" .. map .. "_ttt.txt"
local buf = file.Read(fname, "GAME")
local settings = {}
local lines = string.Explode("\n", buf)
for k, line in pairs(lines) do
if string.match(line, "^setting") then
local key, val = string.match(line, "^setting:\t(%w*) ([0-9]*)")
val = tonumber(val)
if key and val then
settings[key] = val
else
ErrorNoHalt("Invalid setting line " .. k .. " in " .. fname .. "\n")
end
end
end
return settings
end
local classremap = {
ttt_playerspawn = "info_player_deathmatch"
};
local function ImportEntities(map)
if not ents.TTT.CanImportEntities(map) then return end
local fname = "maps/" .. map .. "_ttt.txt"
local num = 0
for k, line in ipairs(string.Explode("\n", file.Read(fname, "GAME"))) do
if (not string.match(line, "^#")) and (not string.match(line, "^setting")) and line != "" and string.byte(line) != 0 then
local data = string.Explode("\t", line)
local fail = true -- pessimism
if data[2] and data[3] then
local cls = data[1]
local ang = nil
local pos = nil
local posraw = string.Explode(" ", data[2])
pos = Vector(tonumber(posraw[1]), tonumber(posraw[2]), tonumber(posraw[3]))
local angraw = string.Explode(" ", data[3])
ang = Angle(tonumber(angraw[1]), tonumber(angraw[2]), tonumber(angraw[3]))
-- Random weapons have a useful keyval
local kv = {}
if data[4] then
local kvraw = string.Explode(" ", data[4])
local key = kvraw[1]
local val = tonumber(kvraw[2])
if key and val then
kv[key] = val
end
end
-- Some dummy ents remap to different, real entity names
cls = classremap[cls] or cls
fail = not CreateImportedEnt(cls, pos, ang, kv)
end
if fail then
ErrorNoHalt("Invalid line " .. k .. " in " .. fname .. "\n")
else
num = num + 1
end
end
end
MsgN("Spawned " .. num .. " entities found in script.")
return true
end
function ents.TTT.ProcessImportScript(map)
MsgN("Weapon/ammo placement script found, attempting import...")
MsgN("Reading settings from script...")
local settings = ImportSettings(map)
if tobool(settings.replacespawns) then
MsgN("Removing existing player spawns")
RemoveSpawnEntities()
end
MsgN("Removing existing weapons/ammo")
RemoveWeaponEntities()
MsgN("Importing entities...")
local result = ImportEntities(map)
if result then
MsgN("Weapon placement script import successful!")
else
ErrorNoHalt("Weapon placement script import failed!\n")
end
end