The repository carried two different servers side by side - the early
`justrp` prototype with its Python auth API, and a snapshot of the real
one under `server-code/`. Only the second one is a server anyone should
start from, so the prototype is gone and the real one moved to the root.
Taken from the live box, so the UI is the finished version (the tokens,
the county-records sheets and the variable fonts landed after the last
snapshot was pushed):
resources/[rp]/ rp_db, rp_core, rp_session, rp_loading, rp_ui,
rp_selftest, rp_dbtest
bin/ supervise.sh (keep-alive + console FIFO), rcon, init script
etc/schema.sql accounts, characters, transactions, inventory, vehicles
assets/fonts/ the bundled subsets
docs/SERVER.md how it is put together, resource by resource
install.sh turns a clean Ubuntu/Debian box into this server in one
command: recommended FXServer build, MariaDB with the schema, resources,
server.cfg + a generated database password, boot entry (systemd or
init.d), then it waits for the Cfx registration. --name/--port/--db-name
let a second server live on the same machine. Tested end to end on a
spare install root: build 25770 fetched, schema applied, boot entry
written, FXServer started and stopped exactly where a wrong licence key
should stop it.
No secrets travel with it: the licence key and the database password live
in data/secrets.cfg on the machine, and .gitignore now names it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
189 lines
5.6 KiB
Lua
189 lines
5.6 KiB
Lua
-- ---------------------------------------------------------------------------
|
|
-- Scripted cameras for the pre-spawn screens.
|
|
--
|
|
-- One camera object is reused throughout. Everything is driven from a single
|
|
-- render thread that eases the live values towards target values, so a change
|
|
-- requested by the UI never snaps.
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
Cam = {}
|
|
|
|
local cam = nil
|
|
local mode = 'off' -- 'off' | 'shots' | 'orbit' | 'fly'
|
|
local renderThread = false
|
|
|
|
-- live and target orbit values
|
|
local orbit = {
|
|
angle = 180.0, targetAngle = 180.0,
|
|
radius = 1.6, targetRadius = 1.6,
|
|
height = 0.65, targetHeight = 0.65,
|
|
focus = nil, -- entity to orbit
|
|
pitch = 0.0, targetPitch = 0.0,
|
|
}
|
|
|
|
local shots = { list = {}, index = 0, startedAt = 0, duration = 22000 }
|
|
local fly = { from = nil, to = nil, lookFrom = nil, lookTo = nil, startedAt = 0, duration = 3000, done = nil }
|
|
|
|
local function ensureCam()
|
|
if cam and DoesCamExist(cam) then return cam end
|
|
cam = CreateCam('DEFAULT_SCRIPTED_CAMERA', true)
|
|
SetCamActive(cam, true)
|
|
RenderScriptCams(true, false, 0, true, true)
|
|
return cam
|
|
end
|
|
|
|
local function lerp(a, b, t) return a + (b - a) * t end
|
|
|
|
local function vlerp(a, b, t)
|
|
return vector3(lerp(a.x, b.x, t), lerp(a.y, b.y, t), lerp(a.z, b.z, t))
|
|
end
|
|
|
|
-- smoothstep, so fly-throughs start and end at rest
|
|
local function ease(t)
|
|
t = math.max(0.0, math.min(1.0, t))
|
|
return t * t * (3.0 - 2.0 * t)
|
|
end
|
|
|
|
local function startRenderThread()
|
|
if renderThread then return end
|
|
renderThread = true
|
|
|
|
CreateThread(function()
|
|
while mode ~= 'off' do
|
|
local c = ensureCam()
|
|
|
|
if mode == 'shots' then
|
|
local shot = shots.list[shots.index]
|
|
if shot then
|
|
local t = (GetGameTimer() - shots.startedAt) / shots.duration
|
|
if t >= 1.0 then
|
|
shots.index = (shots.index % #shots.list) + 1
|
|
shots.startedAt = GetGameTimer()
|
|
shot = shots.list[shots.index]
|
|
t = 0.0
|
|
end
|
|
-- a slow linear dolly reads as a helicopter, not a spline
|
|
local pos = vlerp(shot.from, shot.to, t)
|
|
SetCamCoord(c, pos.x, pos.y, pos.z)
|
|
PointCamAtCoord(c, shot.look.x, shot.look.y, shot.look.z)
|
|
end
|
|
|
|
elseif mode == 'orbit' then
|
|
orbit.angle = lerp(orbit.angle, orbit.targetAngle, 0.14)
|
|
orbit.radius = lerp(orbit.radius, orbit.targetRadius, 0.10)
|
|
orbit.height = lerp(orbit.height, orbit.targetHeight, 0.10)
|
|
orbit.pitch = lerp(orbit.pitch, orbit.targetPitch, 0.10)
|
|
|
|
local ent = orbit.focus
|
|
if ent and DoesEntityExist(ent) then
|
|
local base = GetEntityCoords(ent)
|
|
local rad = math.rad(orbit.angle)
|
|
local px = base.x + math.sin(rad) * orbit.radius
|
|
local py = base.y + math.cos(rad) * orbit.radius
|
|
local pz = base.z + orbit.height
|
|
SetCamCoord(c, px, py, pz)
|
|
PointCamAtCoord(c, base.x, base.y, base.z + orbit.height - orbit.pitch)
|
|
end
|
|
|
|
elseif mode == 'fly' then
|
|
local t = ease((GetGameTimer() - fly.startedAt) / fly.duration)
|
|
local pos = vlerp(fly.from, fly.to, t)
|
|
local look = vlerp(fly.lookFrom, fly.lookTo, t)
|
|
SetCamCoord(c, pos.x, pos.y, pos.z)
|
|
PointCamAtCoord(c, look.x, look.y, look.z)
|
|
if t >= 1.0 then
|
|
mode = 'idle'
|
|
if fly.done then
|
|
local fn = fly.done
|
|
fly.done = nil
|
|
fn()
|
|
end
|
|
end
|
|
end
|
|
|
|
Wait(0)
|
|
end
|
|
renderThread = false
|
|
end)
|
|
end
|
|
|
|
--- Slow establishing shots over the city, cycled forever.
|
|
function Cam.Shots(list)
|
|
shots.list = list
|
|
shots.index = 1
|
|
shots.startedAt = GetGameTimer()
|
|
mode = 'shots'
|
|
ensureCam()
|
|
SetCamFov(cam, 42.0)
|
|
startRenderThread()
|
|
end
|
|
|
|
--- Orbit an entity. `snap` places the camera immediately instead of easing in.
|
|
function Cam.Orbit(entity, radius, height, angle, snap)
|
|
orbit.focus = entity
|
|
orbit.targetRadius = radius or orbit.targetRadius
|
|
orbit.targetHeight = height or orbit.targetHeight
|
|
if angle then orbit.targetAngle = angle end
|
|
if snap then
|
|
orbit.radius = orbit.targetRadius
|
|
orbit.height = orbit.targetHeight
|
|
orbit.angle = orbit.targetAngle
|
|
end
|
|
mode = 'orbit'
|
|
ensureCam()
|
|
SetCamFov(cam, 34.0)
|
|
startRenderThread()
|
|
end
|
|
|
|
--- Framing presets used by the creator's section tabs.
|
|
function Cam.OrbitFrame(radius, height, pitch)
|
|
orbit.targetRadius = radius
|
|
orbit.targetHeight = height
|
|
orbit.targetPitch = pitch or 0.0
|
|
end
|
|
|
|
function Cam.Nudge(deltaDegrees)
|
|
orbit.targetAngle = orbit.targetAngle + deltaDegrees
|
|
end
|
|
|
|
function Cam.SetAngle(deg)
|
|
orbit.targetAngle = deg
|
|
end
|
|
|
|
function Cam.Angle() return orbit.targetAngle end
|
|
|
|
--- Fly from wherever we are to a point looking at a target, then call `done`.
|
|
function Cam.FlyTo(fromPos, fromLook, toPos, toLook, duration, done)
|
|
fly.from = fromPos
|
|
fly.to = toPos
|
|
fly.lookFrom = fromLook
|
|
fly.lookTo = toLook
|
|
fly.duration = duration or 3000
|
|
fly.startedAt = GetGameTimer()
|
|
fly.done = done
|
|
mode = 'fly'
|
|
ensureCam()
|
|
startRenderThread()
|
|
end
|
|
|
|
function Cam.SetFov(fov)
|
|
if cam and DoesCamExist(cam) then SetCamFov(cam, fov + 0.0) end
|
|
end
|
|
|
|
function Cam.Position()
|
|
if cam and DoesCamExist(cam) then return GetCamCoord(cam) end
|
|
return GetEntityCoords(PlayerPedId())
|
|
end
|
|
|
|
--- Hand control back to the gameplay camera. With a duration the engine
|
|
--- interpolates between the two, which hides the cut entirely.
|
|
function Cam.Release(duration)
|
|
mode = 'off'
|
|
duration = duration or 0
|
|
RenderScriptCams(false, duration > 0, duration, true, true)
|
|
if cam and DoesCamExist(cam) then
|
|
DestroyCam(cam, true)
|
|
end
|
|
cam = nil
|
|
end
|