Last active
July 27, 2023 20:10
-
-
Save CheezusChrust/18845f7b41b9cceb171aa52a10d19855 to your computer and use it in GitHub Desktop.
Basic StarfallEx chip for placing vegetation on maps - this chip is old and probably buggy, be warned
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--@name Vegetation Painter | |
--@author Cheezus | |
--@shared | |
--[[ | |
Basic usage: | |
0. Start Hammer on the SAME VERSION of the map you're currently playing on; you may need to do a fresh compile | |
1. Define whatever foliage models you want placed using vegetation.models table below | |
2. Spawn the chip and hold your use key (default E) to place foliage and your walk key (default ALT) to erase | |
3. Type /export to export current vegetation models to Hammer | |
4. Type /clear to remove current vegetation models from Hammer (in case you've made a mistake) | |
5.1. This will NOT work if you despawn and respawn the chip, in that case you need to remove the vegetation | |
in hammer manually. Sorry! | |
--]] | |
local radius = 500 --Default radius to paint foliage in, in source units - change this with '/radius' | |
if SERVER then | |
local vegetation = {} | |
--[[ | |
Foliage model format: | |
model: What model to use | |
density: How often this model appears relative to all other models | |
radius: Distance from the center of the model to not spawn other foliage of the same type, in source units | |
heightOffset: Height offset, in source units | |
maxAngle: Don't spawn the model on terrain steeper than this angle, in degrees | |
useGroundAngle: Make this model conform to the angle of the terrain (good for grass and bushes) | |
type: models of dissimilar types will ignore eachother in terms of whether they can spawn in a certain | |
location or not | |
spawnOn: Table of physprops to spawn this type of vegetation on - see all at https://wiki.facepunch.com/gmod/Enums/MAT | |
(Optional) additionalKeyValues: Table of extra keyvalues to be added to the entities | |
EXAMPLE: | |
{ | |
model = "models/props_foliage/tree_poplar_01.mdl", | |
density = 150, | |
radius = 300, | |
heightOffset = 0, | |
maxAngle = 35, | |
useGroundAngle = false, | |
type = "tree", | |
spawnOn = {trace.MAT_GRASS}, | |
}, | |
Additional Information: | |
- You can define keyvalues to be set on ALL entities after the vegetation.models table | |
- A model's additional keyvalues override global keyvalues if two of the same type are specified | |
--]] | |
vegetation.models = { | |
{ | |
model = "models/foliage/tree05.mdl", | |
density = 150, | |
radius = 300, | |
heightOffset = 0, | |
maxAngle = 35, | |
useGroundAngle = false, | |
type = "tree", | |
spawnOn = {trace.MAT_GRASS} | |
}, | |
{ | |
model = "models/foliage/tree03b.mdl", | |
density = 50, | |
radius = 150, | |
heightOffset = 0, | |
maxAngle = 35, | |
useGroundAngle = false, | |
type = "tree", | |
spawnOn = {trace.MAT_GRASS} | |
}, | |
{ | |
model = "models/foliage/grass_01.mdl", | |
density = 50, | |
radius = 100, | |
heightOffset = 40, | |
maxAngle = 35, | |
useGroundAngle = false, | |
type = "grass", | |
spawnOn = {trace.MAT_GRASS} | |
} | |
} | |
vegetation.globalKeyValues = { | |
screenspacefade = 1, | |
fademindist = 100, | |
fademaxdist = 90, | |
disableselfshadowing = 1 | |
} | |
local maxCPU = 0.75 --Maximum CPU the chip is allowed to use, 0 being none and 1 being the max your current sf_timebuffer allows | |
--=======================End of Settings=======================-- | |
vegetation.data = {} --stupid workaround for stupid bug | |
vegetation.sumOfWeights = 0 | |
local processing = false | |
for _, model in pairs(vegetation.models) do | |
vegetation.sumOfWeights = vegetation.sumOfWeights + model.density | |
end | |
local function weightedSelection() | |
local randomWeight = math.rand(1, vegetation.sumOfWeights) | |
for _, model in pairs(vegetation.models) do | |
randomWeight = randomWeight - model.density | |
if randomWeight <= 0 then return model end | |
end | |
end | |
local function vecDiff(v1, v2) | |
return math.deg(math.acos(v1:dot(v2) / (v1:getLength() * v2:getLength()))) | |
end | |
local function canRun(quota) | |
if quotaAverage() > quotaMax() * quota or quotaUsed() > quotaMax() * quota then return false end | |
return true | |
end | |
local function print2(...) | |
print(Color(100, 255, 100), "[Vegetation Placer] ", Color(255, 255, 255), ...) | |
end | |
local function printProgress(current, final, color) | |
color = color or Color(255, 255, 255) | |
local progress = math.round((current / final) * 20) | |
print(color, "[" .. string.rep("=", progress) .. ">" .. string.rep(" ", (20 - progress) * 2.4) .. "] " .. math.floor((current / final) * 100) .. "%") | |
end | |
if not hammer then | |
print2("Hammer library not detected, get it at https://github.com/CheezusChrust/Starfall-Hammer-Library") | |
end | |
print2("Hold USE to paint") | |
print2("Hold WALK to erase") | |
print2("Type '/export' to export to hammer") | |
print2("Type '/radius' to change paint radius") | |
print2("Type '/clear' to cleanup previously exported foliage") | |
hook.add("think", "paint", function() | |
if processing then return end | |
if owner():keyDown(IN_KEY.USE) then | |
while canRun(maxCPU) and prop.propsLeft() > 0 and holograms.hologramsLeft() > 0 do | |
--Select foliage model at random | |
local model = weightedSelection() | |
--Get where owner is looking | |
local eyeTrace = trace.trace(owner():getEyePos(), owner():getEyePos() + owner():getEyeAngles():getForward() * 100000, find.all()) | |
--Select a random position and angle based on the above position | |
local pos = eyeTrace.HitPos + Vector(math.rand(-radius, radius), math.rand(-radius, radius), 0) | |
local ang = Angle(math.rand(-1, 1), math.rand(0, 359), math.rand(-1, 1)) | |
--Trace the position from above to determine its properties | |
local groundTrace = trace.trace(pos + Vector(0, 0, 500), pos + Vector(0, 0, -100)) | |
if model.useGroundAngle == true then | |
ang = groundTrace.HitNormal:getAngle() + Angle(90, 0, 0) | |
end | |
--Check radius of nearby foliage to prevent spawning inside eachother | |
local flag = false | |
find.inSphere(pos, model.radius, function(ent) | |
if not vegetation.data[ent] then return false end | |
if vegetation.data[ent].type == model.type and (ent:getPos() - pos):getLength() < model.radius then | |
flag = true | |
end | |
end) | |
if flag == true then continue end | |
--Ensure ground is one of the physprops to receive foliage | |
flag = true | |
for _, mat in pairs(model.spawnOn) do | |
if groundTrace.MatType == mat then | |
flag = false | |
end | |
end | |
if flag == true then continue end | |
--Ensure the ground isn't too steep for the foliage | |
if vecDiff(groundTrace.HitNormal, Vector(0, 0, 1)) > model.maxAngle then continue end | |
--Due to a recent change, prop.create can no longer place collisionless props ('effects') | |
--Need to create them as a hologram instead | |
local tree | |
local t = trace.trace(pos + Vector(0, 0, 750), pos + Vector(0, 0, -750)).HitPos + Vector(0, 0, model.heightOffset) | |
try(function() | |
tree = prop.create(t, ang, model.model, true) | |
end, function() | |
try(function() | |
tree = holograms.create(t, ang, model.model) | |
end, function(error) | |
print(Color(255, 0, 0), error.message .. " (" .. model.model .. ")") | |
end) | |
end) | |
if not tree then continue end | |
vegetation.data[tree] = {} | |
vegetation.data[tree].type = model.type | |
if model.additionalKeyValues then | |
vegetation.data[tree].additionalKeyValues = model.additionalKeyValues | |
end | |
end | |
end | |
if owner():keyDown(IN_KEY.WALK) then | |
ents = find.inSphere(owner():getEyeTrace().HitPos, radius) | |
for _, v in pairs(ents) do | |
if v:isValid() and vegetation.data[v] and vegetation.data[v].type ~= nil then | |
v:remove() | |
end | |
end | |
end | |
end) | |
hook.add("PlayerSay", "chatmsg", function(ply, text) | |
if ply ~= owner() then return end | |
if processing then return text end | |
chat = string.explode(" ", text) | |
if chat[1] == "/radius" then | |
if chat[2] then | |
radius = tonumber(chat[2]) | |
print2("Radius set to " .. chat[2] .. " units") | |
net.start("radius") | |
net.writeInt(radius, 16) | |
net.send() | |
else | |
print2("Please specify a radius") | |
end | |
return "" | |
end | |
if chat[1] == "/export" then | |
if not hammer then | |
print2("Could not export due to missing Hammer library - get it at https://github.com/CheezusChrust/Starfall-Hammer-Library") | |
return "" | |
end | |
processing = true | |
local queue = find.all(function(ent) | |
if vegetation.data[ent] then return true end | |
end) | |
hammer.startSession() | |
print2("Export started - THIS MAY TAKE A LONG TIME") | |
local total = #queue | |
hook.add("think", "processQueue", function() | |
if canRun(maxCPU) and #queue > 0 then | |
local ent = queue[#queue] | |
hammer.createProp("static", ent:getModel(), ent:getPos(), ent:getAngles()) | |
data = vegetation.data[ent] | |
if vegetation.globalKeyValues then | |
for k, v in pairs(vegetation.globalKeyValues) do | |
hammer.setKeyValue("prop_static", ent:getPos(), k, tostring(v)) | |
end | |
end | |
if data.additionalKeyValues then | |
for k, v in pairs(data.additionalKeyValues) do | |
hammer.setKeyValue("prop_static", ent:getPos(), k, tostring(v)) | |
end | |
end | |
table.remove(queue) | |
if #queue % 50 == 0 then | |
local perc = (total - #queue) / total | |
printProgress(perc, 1) | |
end | |
end | |
if #queue == 0 then | |
print2("Exported " .. total .. " entities") | |
hammer.endSession() | |
processing = false | |
hook.remove("think", "processQueue") | |
end | |
end) | |
return "" | |
end | |
if chat[1] == "/clear" then | |
if not hammer then | |
print2("Could not clear due to missing Hammer library - get it at https://github.com/CheezusChrust/Starfall-Hammer-Library") | |
return "" | |
end | |
processing = true | |
local queue = find.byClass("prop_physics", function(ent) | |
if vegetation.data[ent] then return true end | |
end) | |
hammer.startSession() | |
print2("Cleaning exported entities...") | |
local count = 0 | |
local total = #queue | |
hook.add("think", "processQueue", function() | |
if canRun(maxCPU) and #queue > 0 then | |
local ent = queue[#queue] | |
count = count + 1 | |
try(function() | |
hammer.removeEntity("prop_static", ent:getPos()) | |
ent:remove() | |
end) | |
table.remove(queue) | |
local perc = (total - #queue) / total | |
if #queue % 50 == 0 then | |
printProgress(perc, 1) | |
end | |
end | |
if #queue == 0 then | |
print2("Removed " .. count .. " entities") | |
hammer.endSession() | |
processing = false | |
hook.remove("think", "processQueue") | |
end | |
end) | |
return "" | |
end | |
end) | |
elseif player() == owner() then | |
net.receive("radius", function() | |
radius = net.readInt(16) | |
end) | |
render.setHUDActive(true) | |
hook.add("drawHud", "hud", function() | |
render.pushViewMatrix({ | |
type = "3D" | |
}) | |
local hitPos = trace.trace(owner():getEyePos(), owner():getEyePos() + owner():getEyeAngles():getForward() * 100000, find.all()).HitPos --player():getEyeTrace().HitPos | |
render.setColor(Color(0, 255, 0)) | |
render.draw3DWireframeBox(hitPos, Angle(0), Vector(radius):setZ(500), Vector(-radius):setZ(0)) | |
render.popViewMatrix() | |
end) | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment