Skip to content

Instantly share code, notes, and snippets.

@Earu
Created May 19, 2025 17:42
Show Gist options
  • Save Earu/67dc5d6a619faac90535580904ea0f81 to your computer and use it in GitHub Desktop.
Save Earu/67dc5d6a619faac90535580904ea0f81 to your computer and use it in GitHub Desktop.
Scan Garry's Mod maps for bodies of water for various things.
--[[
THIS IS A LIBRARY THAT SCANS FOR THE SURFACE OF BODIES OF WATER IN A SECLUDED AREA.
IT IS USED TO FIND RANDOM POSITIONS IN WATER FOR ENTITIES TO SPAWN IN.
OR TO CHECK IF A SPECIFIC POSITION IS IN WATER.
/!\ IT IS HIGHLY RECOMMENDED TO CALL THIS FUNCTION ONCE AND STORE THE RESULT AS IT A VERY INTENSIVE FUNCTION!
EXAMPLE USAGE:
local origin = landmark.get("land_fish1") -- The origin point of the scan
local scan_precision = 100 -- The precision of the scan, higher = more accurate but slower
WaterScanner.ScanArea(origin, scan_precision, function(result)
-- Check if the point is in the water
local pos = ply:GetPos()
local is_water = result:Contains(pos.x, pos.y)
-- Get a random position in the water
local random_pos = result:GetRandomPos()
ent:SetPos(random_pos)
MsgC(Color(255, 0, 0), "DONE SCANNING WATER\n")
end)
]]
local meta = {}
meta.__index = meta
_G.WaterScanner = meta
function meta:Contains(x, y)
if x < self.MinX or x > self.MaxX or y < self.MinY or y > self.MaxY then
return false
end
local diff = x - self.MinX
local n = math.floor(diff / self.ScanInterval)
local index = self.MinX + (self.ScanInterval * n)
if not self.Bounds[index] then
return false
end
for _, bound in ipairs(self.Bounds[index]) do
if y >= bound.start and y <= bound["end"] then
return true
end
end
return false
end
function meta:GetRandomPos()
local all_x = table.GetKeys(self.Map)
local random_x = all_x[math.random(1, #all_x)]
local all_y = table.GetKeys(self.Map[random_x])
local random_y = all_y[math.random(1, #all_y)]
return Vector(random_x, random_y, self.SurfaceZ)
end
local function get_world_bounds(origin_point)
-- Get the topmost point above the origin point
local top_most_point = util.TraceLine({
start = origin_point,
endpos = origin_point + Vector(0, 0, 2e9),
mask = MASK_SOLID_BRUSHONLY,
}).HitPos - Vector(0, 0, 50)
-- Get max and min x world bounds
local max_x, min_x = 0, 0
do
local x1 = util.TraceLine({
start = top_most_point,
endpos = top_most_point + Vector(2e9, 0, 0),
mask = MASK_SOLID_BRUSHONLY,
}).HitPos.x
local x2 = util.TraceLine({
start = top_most_point,
endpos = top_most_point + Vector(-2e9, 0, 0),
mask = MASK_SOLID_BRUSHONLY,
}).HitPos.x
max_x = math.max(x1, x2)
min_x = math.min(x1, x2)
end
-- Get max and min y world bounds
local max_y, min_y = 0, 0
do
local y1 = util.TraceLine({
start = top_most_point,
endpos = top_most_point + Vector(0, 2e9, 0),
mask = MASK_SOLID_BRUSHONLY,
}).HitPos.y
local y2 = util.TraceLine({
start = top_most_point,
endpos = top_most_point + Vector(0, -2e9, 0),
mask = MASK_SOLID_BRUSHONLY,
}).HitPos.y
max_y = math.max(y1, y2)
min_y = math.min(y1, y2)
end
return min_x, max_x, min_y, max_y, top_most_point.z
end
function meta.ScanArea(origin, scan_interval, callback)
scan_interval = scan_interval or 100
local min_x, max_x, min_y, max_y, top_most_z = get_world_bounds(origin)
local result = {
Map = {},
Bounds = {},
SurfaceZ = -1,
MinX = min_x,
MaxX = max_x,
MinY = min_y,
MaxY = max_y,
ScanInterval = scan_interval,
}
local co = coroutine.create(function()
local current_x = min_x
local current_y = min_y
local prev_state = "NOT_WATER"
local steps = 0
while current_x < max_x do
result.Bounds[current_x] = result.Bounds[current_x] or {}
while current_y < max_y do
local point = Vector(current_x, current_y, top_most_z)
local water_trace = util.TraceLine({
start = point,
endpos = point + Vector(0, 0, -2e9),
mask = MASK_WATER + MASK_SOLID_BRUSHONLY,
})
local is_water = water_trace.Hit and water_trace.MatType == MAT_SLOSH
if is_water and result.SurfaceZ == -1 then
result.SurfaceZ = water_trace.HitPos.z
end
-- Add to the map
result.Map[current_x] = result.Map[current_x] or {}
result.Map[current_x][current_y] = is_water
local state = is_water and "WATER" or "NOT_WATER"
if prev_state ~= state then
if prev_state == "NOT_WATER" then
table.insert(result.Bounds[current_x], { start = current_y, ["end"] = -1 })
else
local last_bound = result.Bounds[current_x][#result.Bounds[current_x]]
if last_bound["end"] == -1 then
last_bound["end"] = current_y
end
end
prev_state = state
end
current_y = current_y + scan_interval
steps = steps + 1
if steps % 100 == 0 then
coroutine.yield()
end
end
local last_bound = result.Bounds[current_x][#result.Bounds[current_x]]
if last_bound and last_bound["end"] == -1 then
last_bound["end"] = current_y
end
current_y = min_y
current_x = current_x + scan_interval
prev_state = "NOT_WATER"
steps = steps + 1
if steps % 100 == 0 then
coroutine.yield()
end
end
end)
local timer_name = "water_scanner_" .. FrameNumber()
timer.Create(timer_name, 0.25, 0, function()
if coroutine.status(co) == "dead" then
timer.Remove(timer_name)
callback(setmetatable(result, meta))
return
end
local status, err = coroutine.resume(co)
if not status then
print(err)
end
end)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment