This commit is contained in:
2026-07-07 21:08:52 +02:00
commit 4c20cfc716
2613 changed files with 318021 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
-- This resource is part of the default Cfx.re asset pack (cfx-server-data)
-- Altering or recreating for local use only is strongly discouraged.
version '1.0.0'
description 'An example money system fountain spawn point.'
repository 'https://github.com/citizenfx/cfx-server-data'
author 'Cfx.re <root@cfx.re>'
fx_version 'cerulean'
game 'gta5'
map 'map.lua'
dependency 'money-fountain'

View File

@@ -0,0 +1,4 @@
money_fountain 'test_fountain' {
vector3(97.334, -973.621, 29.36),
amount = 75
}

View File

@@ -0,0 +1,101 @@
-- add text entries for all the help types we have
AddTextEntry('FOUNTAIN_HELP', 'This fountain currently contains $~1~.~n~Press ~INPUT_PICKUP~ to obtain $~1~.~n~Press ~INPUT_DETONATE~ to place $~1~.')
AddTextEntry('FOUNTAIN_HELP_DRAINED', 'This fountain currently contains $~1~.~n~Press ~INPUT_DETONATE~ to place $~1~.')
AddTextEntry('FOUNTAIN_HELP_BROKE', 'This fountain currently contains $~1~.~n~Press ~INPUT_PICKUP~ to obtain $~1~.')
AddTextEntry('FOUNTAIN_HELP_BROKE_N_DRAINED', 'This fountain currently contains $~1~.')
AddTextEntry('FOUNTAIN_HELP_INUSE', 'This fountain currently contains $~1~.~n~You can use it again in ~a~.')
-- upvalue aliases so that we will be fast if far away
local Wait = Wait
local GetEntityCoords = GetEntityCoords
local PlayerPedId = PlayerPedId
-- timer, don't tick as frequently if we're far from any money fountain
local relevanceTimer = 500
CreateThread(function()
local pressing = false
while true do
Wait(relevanceTimer)
local coords = GetEntityCoords(PlayerPedId())
for _, data in pairs(moneyFountains) do
-- if we're near this fountain
local dist = #(coords - data.coords)
-- near enough to draw
if dist < 40 then
-- ensure per-frame tick
relevanceTimer = 0
DrawMarker(29, data.coords.x, data.coords.y, data.coords.z, 0, 0, 0, 0.0, 0, 0, 1.0, 1.0, 1.0, 0, 150, 0, 120, false, true, 2, false, nil, nil, false)
else
-- put the relevance timer back to the way it was
relevanceTimer = 500
end
-- near enough to use
if dist < 1 then
-- are we able to use it? if not, display appropriate help
local player = LocalPlayer
local nextUse = player.state['fountain_nextUse']
-- GetNetworkTime is synced for everyone
if nextUse and nextUse >= GetNetworkTime() then
BeginTextCommandDisplayHelp('FOUNTAIN_HELP_INUSE')
AddTextComponentInteger(GlobalState['fountain_' .. data.id])
AddTextComponentSubstringTime(math.tointeger(nextUse - GetNetworkTime()), 2 | 4) -- seconds (2), minutes (4)
EndTextCommandDisplayHelp(0, false, false, 1000)
else
-- handle inputs for pickup/place
if not pressing then
if IsControlPressed(0, 38 --[[ INPUT_PICKUP ]]) then
TriggerServerEvent('money_fountain:tryPickup', data.id)
pressing = true
elseif IsControlPressed(0, 47 --[[ INPUT_DETONATE ]]) then
TriggerServerEvent('money_fountain:tryPlace', data.id)
pressing = true
end
else
if not IsControlPressed(0, 38 --[[ INPUT_PICKUP ]]) and
not IsControlPressed(0, 47 --[[ INPUT_DETONATE ]]) then
pressing = false
end
end
-- decide the appropriate help message
local youCanSpend = (player.state['money_cash'] or 0) >= data.amount
local fountainCanSpend = GlobalState['fountain_' .. data.id] >= data.amount
local helpName
if youCanSpend and fountainCanSpend then
helpName = 'FOUNTAIN_HELP'
elseif youCanSpend and not fountainCanSpend then
helpName = 'FOUNTAIN_HELP_DRAINED'
elseif not youCanSpend and fountainCanSpend then
helpName = 'FOUNTAIN_HELP_BROKE'
else
helpName = 'FOUNTAIN_HELP_BROKE_N_DRAINED'
end
-- and print it
BeginTextCommandDisplayHelp(helpName)
AddTextComponentInteger(GlobalState['fountain_' .. data.id])
if fountainCanSpend then
AddTextComponentInteger(data.amount)
end
if youCanSpend then
AddTextComponentInteger(data.amount)
end
EndTextCommandDisplayHelp(0, false, false, 1000)
end
end
end
end
end)

View File

@@ -0,0 +1,22 @@
-- This resource is part of the default Cfx.re asset pack (cfx-server-data)
-- Altering or recreating for local use only is strongly discouraged.
version '1.0.0'
description 'An example money system client containing a money fountain.'
repository 'https://github.com/citizenfx/cfx-server-data'
author 'Cfx.re <root@cfx.re>'
fx_version 'bodacious'
game 'gta5'
client_script 'client.lua'
server_script 'server.lua'
shared_script 'mapdata.lua'
dependencies {
'mapmanager',
'money'
}
lua54 'yes'

View File

@@ -0,0 +1,28 @@
-- define the money fountain list (SHARED SCRIPT)
moneyFountains = {}
-- index to know what to remove
local fountainIdx = 1
AddEventHandler('getMapDirectives', function(add)
-- add a 'money_fountain' map directive
add('money_fountain', function(state, name)
return function(data)
local coords = data[1]
local amount = data.amount or 100
local idx = fountainIdx
fountainIdx += 1
moneyFountains[idx] = {
id = name,
coords = coords,
amount = amount
}
state.add('idx', idx)
end
end, function(state)
moneyFountains[state.idx] = nil
end)
end)

View File

@@ -0,0 +1,107 @@
-- track down what we've added to global state
local sentState = {}
-- money system
local ms = exports['money']
-- get the fountain content from storage
local function getMoneyForId(fountainId)
return GetResourceKvpInt(('money:%s'):format(fountainId)) / 100.0
end
-- set the fountain content in storage + state
local function setMoneyForId(fountainId, money)
GlobalState['fountain_' .. fountainId] = math.tointeger(money)
return SetResourceKvpInt(('money:%s'):format(fountainId), math.tointeger(money * 100.0))
end
-- get the nearest fountain to the player + ID
local function getMoneyFountain(id, source)
local coords = GetEntityCoords(GetPlayerPed(source))
for _, v in pairs(moneyFountains) do
if v.id == id then
if #(v.coords - coords) < 2.5 then
return v
end
end
end
return nil
end
-- generic function for events
local function handleFountainStuff(source, id, pickup)
-- if near the fountain we specify
local fountain = getMoneyFountain(id, source)
if fountain then
-- and we can actually use the fountain already
local player = Player(source)
local nextUse = player.state['fountain_nextUse']
if not nextUse then
nextUse = 0
end
-- GetGameTimer ~ GetNetworkTime on client
if nextUse <= GetGameTimer() then
-- not rate limited
local success = false
local money = getMoneyForId(fountain.id)
-- decide the op
if pickup then
-- if the fountain is rich enough to get the per-use amount
if money >= fountain.amount then
-- give the player money
if ms:addMoney(source, 'cash', fountain.amount) then
money -= fountain.amount
success = true
end
end
else
-- if the player is rich enough
if ms:removeMoney(source, 'cash', fountain.amount) then
-- add to the fountain
money += fountain.amount
success = true
end
end
-- save it and set the player's cooldown
if success then
setMoneyForId(fountain.id, money)
player.state['fountain_nextUse'] = GetGameTimer() + GetConvarInt('moneyFountain_cooldown', 5000)
end
end
end
end
-- event for picking up fountain->player
RegisterNetEvent('money_fountain:tryPickup')
AddEventHandler('money_fountain:tryPickup', function(id)
handleFountainStuff(source, id, true)
end)
-- event for donating player->fountain
RegisterNetEvent('money_fountain:tryPlace')
AddEventHandler('money_fountain:tryPlace', function(id)
handleFountainStuff(source, id, false)
end)
-- listener: if a new fountain is added, set its current money in state
CreateThread(function()
while true do
Wait(500)
for _, fountain in pairs(moneyFountains) do
if not sentState[fountain.id] then
GlobalState['fountain_' .. fountain.id] = math.tointeger(getMoneyForId(fountain.id))
sentState[fountain.id] = true
end
end
end
end)

View File

@@ -0,0 +1,30 @@
local moneyTypes = {
cash = `MP0_WALLET_BALANCE`,
bank = `BANK_BALANCE`,
}
RegisterNetEvent('money:displayUpdate')
AddEventHandler('money:displayUpdate', function(type, money)
local stat = moneyTypes[type]
if not stat then return end
StatSetInt(stat, math.floor(money))
end)
TriggerServerEvent('money:requestDisplay')
CreateThread(function()
while true do
Wait(0)
if IsControlJustPressed(0, 20) then
SetMultiplayerBankCash()
SetMultiplayerWalletCash()
Wait(4350)
RemoveMultiplayerBankCash()
RemoveMultiplayerWalletCash()
end
end
end)

View File

@@ -0,0 +1,16 @@
-- This resource is part of the default Cfx.re asset pack (cfx-server-data)
-- Altering or recreating for local use only is strongly discouraged.
version '1.0.0'
description 'An example money system using KVS.'
repository 'https://github.com/citizenfx/cfx-server-data'
author 'Cfx.re <root@cfx.re>'
fx_version 'bodacious'
game 'gta5'
client_script 'client.lua'
server_script 'server.lua'
--dependency 'cfx.re/playerData.v1alpha1'
lua54 'yes'

View File

@@ -0,0 +1,119 @@
local playerData = exports['cfx.re/playerData.v1alpha1']
local validMoneyTypes = {
bank = true,
cash = true,
}
local function getMoneyForId(playerId, moneyType)
return GetResourceKvpInt(('money:%s:%s'):format(playerId, moneyType)) / 100.0
end
local function setMoneyForId(playerId, moneyType, money)
local s = playerData:getPlayerById(playerId)
TriggerEvent('money:updated', {
dbId = playerId,
source = s,
moneyType = moneyType,
money = money
})
return SetResourceKvpInt(('money:%s:%s'):format(playerId, moneyType), math.tointeger(money * 100.0))
end
local function addMoneyForId(playerId, moneyType, amount)
local curMoney = getMoneyForId(playerId, moneyType)
curMoney += amount
if curMoney >= 0 then
setMoneyForId(playerId, moneyType, curMoney)
return true, curMoney
end
return false, 0
end
exports('addMoney', function(playerIdx, moneyType, amount)
amount = tonumber(amount)
if amount <= 0 or amount > (1 << 30) then
return false
end
if not validMoneyTypes[moneyType] then
return false
end
local playerId = playerData:getPlayerId(playerIdx)
local success, money = addMoneyForId(playerId, moneyType, amount)
if success then
Player(playerIdx).state['money_' .. moneyType] = money
end
return true
end)
exports('removeMoney', function(playerIdx, moneyType, amount)
amount = tonumber(amount)
if amount <= 0 or amount > (1 << 30) then
return false
end
if not validMoneyTypes[moneyType] then
return false
end
local playerId = playerData:getPlayerId(playerIdx)
local success, money = addMoneyForId(playerId, moneyType, -amount)
if success then
Player(playerIdx).state['money_' .. moneyType] = money
end
return success
end)
exports('getMoney', function(playerIdx, moneyType)
local playerId = playerData:getPlayerId(playerIdx)
return getMoneyForId(playerId, moneyType)
end)
-- player display bits
AddEventHandler('money:updated', function(data)
if data.source then
TriggerClientEvent('money:displayUpdate', data.source, data.moneyType, data.money)
end
end)
RegisterNetEvent('money:requestDisplay')
AddEventHandler('money:requestDisplay', function()
local source = source
local playerId = playerData:getPlayerId(source)
for type, _ in pairs(validMoneyTypes) do
local amount = getMoneyForId(playerId, type)
TriggerClientEvent('money:displayUpdate', source, type, amount)
Player(source).state['money_' .. type] = amount
end
end)
RegisterCommand('earn', function(source, args)
local type = args[1]
local amount = tonumber(args[2])
exports['money']:addMoney(source, type, amount)
end, true)
RegisterCommand('spend', function(source, args)
local type = args[1]
local amount = tonumber(args[2])
if not exports['money']:removeMoney(source, type, amount) then
print('you are broke??')
end
end, true)

View File

@@ -0,0 +1,41 @@
AddEventHandler('gameEventTriggered', function(eventName, args)
if eventName == 'CEventNetworkEntityDamage' then
local victim = args[1]
local culprit = args[2]
local isDead = args[4] == 1
if isDead then
local origCoords = GetEntityCoords(victim)
local pickup = CreatePickupRotate(`PICKUP_MONEY_VARIABLE`, origCoords.x, origCoords.y, origCoords.z - 0.7, 0.0, 0.0, 0.0, 512, 0, false, 0)
local netId = PedToNet(victim)
local undoStuff = { false }
CreateThread(function()
local self = PlayerPedId()
while not undoStuff[1] do
Wait(50)
if #(GetEntityCoords(self) - origCoords) < 2.5 and HasPickupBeenCollected(pickup) then
TriggerServerEvent('money:tryPickup', netId)
RemovePickup(pickup)
break
end
end
undoStuff[1] = true
end)
SetTimeout(15000, function()
if not undoStuff[1] then
RemovePickup(pickup)
undoStuff[1] = true
end
end)
TriggerServerEvent('money:allowPickupNear', netId)
end
end
end)

View File

@@ -0,0 +1,15 @@
-- This resource is part of the default Cfx.re asset pack (cfx-server-data)
-- Altering or recreating for local use only is strongly discouraged.
version '1.0.0'
description 'An example money system client.'
author 'Cfx.re <root@cfx.re>'
repository 'https://github.com/citizenfx/cfx-server-data'
fx_version 'bodacious'
game 'gta5'
client_script 'client.lua'
server_script 'server.lua'
lua54 'yes'

View File

@@ -0,0 +1,42 @@
local safePositions = {}
RegisterNetEvent('money:allowPickupNear')
AddEventHandler('money:allowPickupNear', function(pedId)
local entity = NetworkGetEntityFromNetworkId(pedId)
Wait(250)
if not DoesEntityExist(entity) then
return
end
if GetEntityHealth(entity) > 100 then
return
end
local coords = GetEntityCoords(entity)
safePositions[pedId] = coords
end)
RegisterNetEvent('money:tryPickup')
AddEventHandler('money:tryPickup', function(entity)
if not safePositions[entity] then
return
end
local source = source
local playerPed = GetPlayerPed(source)
local coords = GetEntityCoords(playerPed)
if #(safePositions[entity] - coords) < 2.5 then
exports['money']:addMoney(source, 'cash', 40)
end
safePositions[entity] = nil
end)
AddEventHandler('entityRemoved', function(entity)
safePositions[entity] = nil
end)

View File

@@ -0,0 +1,21 @@
-- This resource is part of the default Cfx.re asset pack (cfx-server-data)
-- Altering or recreating for local use only is strongly discouraged.
version '1.0.0'
author 'Cfx.re <root@cfx.re>'
description 'A GTA Online-styled theme for the chat resource.'
repository 'https://github.com/citizenfx/cfx-server-data'
file 'style.css'
file 'shadow.js'
chat_theme 'gtao' {
styleSheet = 'style.css',
script = 'shadow.js',
msgTemplates = {
default = '<b>{0}</b><span>{1}</span>'
}
}
game 'common'
fx_version 'adamant'

View File

@@ -0,0 +1,74 @@
(function() {
var Filters = {}
var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("style", "display:block;width:0px;height:0px");
var defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
var blurFilter = document.createElementNS("http://www.w3.org/2000/svg", "filter");
blurFilter.setAttribute("id", "svgBlurFilter");
var feGaussianFilter = document.createElementNS("http://www.w3.org/2000/svg", "feGaussianBlur");
feGaussianFilter.setAttribute("stdDeviation", "0 0");
blurFilter.appendChild(feGaussianFilter);
defs.appendChild(blurFilter);
Filters._svgBlurFilter = feGaussianFilter;
// Drop Shadow Filter
var dropShadowFilter = document.createElementNS("http://www.w3.org/2000/svg", "filter");
dropShadowFilter.setAttribute("id", "svgDropShadowFilter");
var feGaussianFilter = document.createElementNS("http://www.w3.org/2000/svg", "feGaussianBlur");
feGaussianFilter.setAttribute("in", "SourceAlpha");
feGaussianFilter.setAttribute("stdDeviation", "3");
dropShadowFilter.appendChild(feGaussianFilter);
Filters._svgDropshadowFilterBlur = feGaussianFilter;
var feOffset = document.createElementNS("http://www.w3.org/2000/svg", "feOffset");
feOffset.setAttribute("dx", "0");
feOffset.setAttribute("dy", "0");
feOffset.setAttribute("result", "offsetblur");
dropShadowFilter.appendChild(feOffset);
Filters._svgDropshadowFilterOffset = feOffset;
var feFlood = document.createElementNS("http://www.w3.org/2000/svg", "feFlood");
feFlood.setAttribute("flood-color", "rgba(0,0,0,1)");
dropShadowFilter.appendChild(feFlood);
Filters._svgDropshadowFilterFlood = feFlood;
var feComposite = document.createElementNS("http://www.w3.org/2000/svg", "feComposite");
feComposite.setAttribute("in2", "offsetblur");
feComposite.setAttribute("operator", "in");
dropShadowFilter.appendChild(feComposite);
var feComposite = document.createElementNS("http://www.w3.org/2000/svg", "feComposite");
feComposite.setAttribute("in2", "SourceAlpha");
feComposite.setAttribute("operator", "out");
feComposite.setAttribute("result", "outer");
dropShadowFilter.appendChild(feComposite);
var feMerge = document.createElementNS("http://www.w3.org/2000/svg", "feMerge");
var feMergeNode = document.createElementNS("http://www.w3.org/2000/svg", "feMergeNode");
feMerge.appendChild(feMergeNode);
var feMergeNode = document.createElementNS("http://www.w3.org/2000/svg", "feMergeNode");
feMerge.appendChild(feMergeNode);
Filters._svgDropshadowMergeNode = feMergeNode;
dropShadowFilter.appendChild(feMerge);
defs.appendChild(dropShadowFilter);
svg.appendChild(defs);
document.documentElement.appendChild(svg);
const blurScale = 1;
const scale = (document.body.clientWidth / 1280);
Filters._svgDropshadowFilterBlur.setAttribute("stdDeviation",
1 * blurScale + " " +
1 * blurScale
);
Filters._svgDropshadowFilterOffset.setAttribute("dx",
String(Math.cos(45 * Math.PI / 180) * 1 * scale));
Filters._svgDropshadowFilterOffset.setAttribute("dy",
String(Math.sin(45 * Math.PI / 180) * 1 * scale));
Filters._svgDropshadowFilterFlood.setAttribute("flood-color",
'rgba(0, 0, 0, 1)');
Filters._svgDropshadowMergeNode.setAttribute("in",
"SourceGraphic");
})();

View File

@@ -0,0 +1,141 @@
* {
font-family: inherit;
}
.chat-window {
--size: calc(((2.7vh * 1.2)) * 6);
position: absolute;
right: calc(2.77vh);
top: calc(50% - (var(--size) / 2));
height: var(--size) !important;
background: inherit !important;
text-align: right;
left: auto;
user-select: none;
}
@font-face {
font-family: 'Font2';
src: url(https://runtime.fivem.net/temp/ChaletLondonNineteenSixty.otf?a);
}
@font-face {
font-family: 'Font2_cond';
src: url(https://runtime.fivem.net/temp/chaletcomprime-colognesixty-webfont.ttf?a);
}
.msg {
font-family: Font2, sans-serif;
color: #fff;
font-size: calc(1.8vh); /* 13px in 720p, calc'd by width */
filter: url(#svgDropShadowFilter);
line-height: calc(2.7vh * 1.2);
margin-bottom: 0;
}
.chat-messages {
margin: 0;
height: 100%;
}
.msg > span > span > b {
font-family: Font2_cond, sans-serif;
font-weight: normal;
vertical-align: baseline;
padding-right: 11px;
line-height: 1;
font-size: calc(2.7vh);
}
.msg > span > span > span {
vertical-align: baseline;
}
.msg i:first-of-type {
font-style: normal;
color: #c0c0c0;
}
.chat-input {
position: absolute;
right: calc(2.77vh);
bottom: calc(2.77vh);
background: inherit !important;
text-align: right;
top: auto;
left: auto;
height: auto;
font-family: Font2, sans-serif;
}
.chat-input > div {
background-color: rgba(0, 0, 0, .6) !important;
border: calc(0.28vh / 2) solid rgba(180, 180, 180, .6);
outline: calc(0.28vh / 2) solid rgba(0, 0, 0, .8); /* to replace margin-background */
padding: calc(0.28vh / 2);
}
.chat-input .prefix {
margin: 0;
margin-left: 0.7%;
margin-top: -0.1%;
line-height: 2.8vh;
}
.chat-input .prefix.any {
opacity: 0.8;
}
.chat-input .prefix.any:before {
content: '[';
}
.chat-input .prefix.any:after {
content: ']';
}
.chat-input > div + div {
position: absolute;
bottom: calc(1.65vh + 0.28vh + 0.28vh + 0.28vh + (0.28vh / 2));
width: 99.6%;
text-align: left;
}
.suggestions {
border: calc(0.28vh / 2) solid rgba(180, 180, 180, .6);
background: transparent;
}
textarea {
background: transparent;
padding: 0.5vh;
}
@media screen and (min-aspect-ratio: 21/9) {
.chat-window, .chat-input {
right: calc(12.8vw);
}
}
@media screen and (min-aspect-ratio: 32/9) {
.chat-window, .chat-input {
right: calc(25vw);
}
}

View File

@@ -0,0 +1,16 @@
-- This resource is part of the default Cfx.re asset pack (cfx-server-data)
-- Altering or recreating for local use only is strongly discouraged.
version '1.0.0'
description 'A basic resource for storing player identifiers.'
author 'Cfx.re <root@cfx.re>'
repository 'https://github.com/citizenfx/cfx-server-data'
fx_version 'bodacious'
game 'common'
server_script 'server.lua'
provides {
'cfx.re/playerData.v1alpha1'
}

View File

@@ -0,0 +1,222 @@
--- player-data is a basic resource to showcase player identifier storage
--
-- it works in a fairly simple way: a set of identifiers is assigned to an account ID, and said
-- account ID is then returned/added as state bag
--
-- it also implements the `cfx.re/playerData.v1alpha1` spec, which is exposed through the following:
-- - getPlayerId(source: string)
-- - getPlayerById(dbId: string)
-- - getPlayerIdFromIdentifier(identifier: string)
-- - setting `cfx.re/playerData@id` state bag field on the player
-- identifiers that we'll ignore (e.g. IP) as they're low-trust/high-variance
local identifierBlocklist = {
ip = true
}
-- function to check if the identifier is blocked
local function isIdentifierBlocked(identifier)
-- Lua pattern to correctly split
local idType = identifier:match('([^:]+):')
-- ensure it's a boolean
return identifierBlocklist[idType] or false
end
-- our database schema, in hierarchical KVS syntax:
-- player:
-- <id>:
-- identifier:
-- <identifier>: 'true'
-- identifier:
-- <identifier>: <playerId>
-- list of player indices to data
local players = {}
-- list of player DBIDs to player indices
local playersById = {}
-- a sequence field using KVS
local function incrementId()
local nextId = GetResourceKvpInt('nextId')
nextId = nextId + 1
SetResourceKvpInt('nextId', nextId)
return nextId
end
-- gets the ID tied to an identifier in the schema, or nil
local function getPlayerIdFromIdentifier(identifier)
local str = GetResourceKvpString(('identifier:%s'):format(identifier))
if not str then
return nil
end
return msgpack.unpack(str).id
end
-- stores the identifier + adds to a logging list
local function setPlayerIdFromIdentifier(identifier, id)
local str = ('identifier:%s'):format(identifier)
SetResourceKvp(str, msgpack.pack({ id = id }))
SetResourceKvp(('player:%s:identifier:%s'):format(id, identifier), 'true')
end
-- stores any new identifiers for this player ID
local function storeIdentifiers(playerIdx, newId)
for _, identifier in ipairs(GetPlayerIdentifiers(playerIdx)) do
if not isIdentifierBlocked(identifier) then
-- TODO: check if the player already has an identifier of this type
setPlayerIdFromIdentifier(identifier, newId)
end
end
end
-- registers a new player (increments sequence, stores data, returns ID)
local function registerPlayer(playerIdx)
local newId = incrementId()
storeIdentifiers(playerIdx, newId)
return newId
end
-- initializes a player's data set
local function setupPlayer(playerIdx)
-- try getting the oldest-known identity from all the player's identifiers
local defaultId = 0xFFFFFFFFFF
local lowestId = defaultId
for _, identifier in ipairs(GetPlayerIdentifiers(playerIdx)) do
if not isIdentifierBlocked(identifier) then
local dbId = getPlayerIdFromIdentifier(identifier)
if dbId then
if dbId < lowestId then
lowestId = dbId
end
end
end
end
-- if this is the default ID, register. if not, update
local playerId
if lowestId == defaultId then
playerId = registerPlayer(playerIdx)
else
storeIdentifiers(playerIdx, lowestId)
playerId = lowestId
end
-- add state bag field
if Player then
Player(playerIdx).state['cfx.re/playerData@id'] = playerId
end
-- and add to our caching tables
players[playerIdx] = {
dbId = playerId
}
playersById[tostring(playerId)] = playerIdx
end
-- we want to add a player pretty early
AddEventHandler('playerConnecting', function()
local playerIdx = tostring(source)
setupPlayer(playerIdx)
end)
-- and migrate them to a 'joining' ID where possible
RegisterNetEvent('playerJoining')
AddEventHandler('playerJoining', function(oldIdx)
-- resource restart race condition
local oldPlayer = players[tostring(oldIdx)]
if oldPlayer then
players[tostring(source)] = oldPlayer
players[tostring(oldIdx)] = nil
else
setupPlayer(tostring(source))
end
end)
-- remove them if they're dropped
AddEventHandler('playerDropped', function()
local player = players[tostring(source)]
if player then
playersById[tostring(player.dbId)] = nil
end
players[tostring(source)] = nil
end)
-- and when the resource is restarted, set up all players that are on right now
for _, player in ipairs(GetPlayers()) do
setupPlayer(player)
end
-- also a quick command to get the current state
RegisterCommand('playerData', function(source, args)
if not args[1] then
print('Usage:')
print('\tplayerData getId <dbId>: gets identifiers for ID')
print('\tplayerData getIdentifier <identifier>: gets ID for identifier')
return
end
if args[1] == 'getId' then
local prefix = ('player:%s:identifier:'):format(args[2])
local handle = StartFindKvp(prefix)
local key
repeat
key = FindKvp(handle)
if key then
print('result:', key:sub(#prefix + 1))
end
until not key
EndFindKvp(handle)
elseif args[1] == 'getIdentifier' then
print('result:', getPlayerIdFromIdentifier(args[2]))
end
end, true)
-- COMPATIBILITY for server versions that don't export provide
local function getExportEventName(resource, name)
return string.format('__cfx_export_%s_%s', resource, name)
end
function AddExport(name, fn)
if not Citizen.Traits or not Citizen.Traits.ProvidesExports then
AddEventHandler(getExportEventName('cfx.re/playerData.v1alpha1', name), function(setCB)
setCB(fn)
end)
end
exports(name, fn)
end
-- exports
AddExport('getPlayerIdFromIdentifier', getPlayerIdFromIdentifier)
AddExport('getPlayerId', function(playerIdx)
local player = players[tostring(playerIdx)]
if not player then
return nil
end
return player.dbId
end)
AddExport('getPlayerById', function(playerId)
return playersById[tostring(playerId)]
end)

View File

@@ -0,0 +1,36 @@
-- This resource is part of the default Cfx.re asset pack (cfx-server-data)
-- Altering or recreating for local use only is strongly discouraged.
version '1.0.0'
author 'Cfx.re <root@cfx.re>'
description 'A basic resource for displaying player names.'
repository 'https://github.com/citizenfx/cfx-server-data'
-- add scripts
client_script 'playernames_api.lua'
server_script 'playernames_api.lua'
client_script 'playernames_cl.lua'
server_script 'playernames_sv.lua'
-- make exports
local exportList = {
'setComponentColor',
'setComponentAlpha',
'setComponentVisibility',
'setWantedLevel',
'setHealthBarColor',
'setNameTemplate'
}
exports(exportList)
server_exports(exportList)
-- add files
files {
'template/template.lua'
}
-- support the latest resource manifest
fx_version 'adamant'
game 'gta5'

View File

@@ -0,0 +1,80 @@
local ids = {}
local function getTriggerFunction(key)
return function(id, ...)
-- if on the client, it's easy
if not IsDuplicityVersion() then
TriggerEvent('playernames:configure', GetPlayerServerId(id), key, ...)
else
-- if on the server, save configuration
if not ids[id] then
ids[id] = {}
end
-- save the setting
ids[id][key] = table.pack(...)
-- broadcast to clients
TriggerClientEvent('playernames:configure', -1, id, key, ...)
end
end
end
if IsDuplicityVersion() then
function reconfigure(source)
for id, data in pairs(ids) do
for key, args in pairs(data) do
TriggerClientEvent('playernames:configure', source, id, key, table.unpack(args))
end
end
end
AddEventHandler('playerDropped', function()
ids[source] = nil
end)
end
setComponentColor = getTriggerFunction('setc')
setComponentAlpha = getTriggerFunction('seta')
setComponentVisibility = getTriggerFunction('tglc')
setWantedLevel = getTriggerFunction('setw')
setHealthBarColor = getTriggerFunction('sehc')
setNameTemplate = getTriggerFunction('tpl')
setName = getTriggerFunction('name')
if not io then
io = { write = nil, open = nil }
end
local template = load(LoadResourceFile(GetCurrentResourceName(), 'template/template.lua'))()
function formatPlayerNameTag(i, templateStr)
--return ('%s &lt;%d&gt;'):format(GetPlayerName(i), GetPlayerServerId(i))
local str = ''
template.print = function(txt)
str = str .. txt
end
local context = {
name = GetPlayerName(i),
i = i,
global = _G
}
if IsDuplicityVersion() then
context.id = i
else
context.id = GetPlayerServerId(i)
end
TriggerEvent('playernames:extendContext', i, function(k, v)
context[k] = v
end)
template.render(templateStr, context, nil, true)
template.print = print
return str
end

View File

@@ -0,0 +1,191 @@
local mpGamerTags = {}
local mpGamerTagSettings = {}
local gtComponent = {
GAMER_NAME = 0,
CREW_TAG = 1,
healthArmour = 2,
BIG_TEXT = 3,
AUDIO_ICON = 4,
MP_USING_MENU = 5,
MP_PASSIVE_MODE = 6,
WANTED_STARS = 7,
MP_DRIVER = 8,
MP_CO_DRIVER = 9,
MP_TAGGED = 10,
GAMER_NAME_NEARBY = 11,
ARROW = 12,
MP_PACKAGES = 13,
INV_IF_PED_FOLLOWING = 14,
RANK_TEXT = 15,
MP_TYPING = 16
}
local function makeSettings()
return {
alphas = {},
colors = {},
healthColor = false,
toggles = {},
wantedLevel = false
}
end
local templateStr
function updatePlayerNames()
-- re-run this function the next frame
SetTimeout(0, updatePlayerNames)
-- return if no template string is set
if not templateStr then
return
end
-- get local coordinates to compare to
local localCoords = GetEntityCoords(PlayerPedId())
-- for each valid player index
for _, i in ipairs(GetActivePlayers()) do
-- if the player exists
if i ~= PlayerId() then
-- get their ped
local ped = GetPlayerPed(i)
local pedCoords = GetEntityCoords(ped)
-- make a new settings list if needed
if not mpGamerTagSettings[i] then
mpGamerTagSettings[i] = makeSettings()
end
-- check the ped, because changing player models may recreate the ped
-- also check gamer tag activity in case the game deleted the gamer tag
if not mpGamerTags[i] or mpGamerTags[i].ped ~= ped or not IsMpGamerTagActive(mpGamerTags[i].tag) then
local nameTag = formatPlayerNameTag(i, templateStr)
-- remove any existing tag
if mpGamerTags[i] then
RemoveMpGamerTag(mpGamerTags[i].tag)
end
-- store the new tag
mpGamerTags[i] = {
tag = CreateMpGamerTag(GetPlayerPed(i), nameTag, false, false, '', 0),
ped = ped
}
end
-- store the tag in a local
local tag = mpGamerTags[i].tag
-- should the player be renamed? this is set by events
if mpGamerTagSettings[i].rename then
SetMpGamerTagName(tag, formatPlayerNameTag(i, templateStr))
mpGamerTagSettings[i].rename = nil
end
-- check distance
local distance = #(pedCoords - localCoords)
-- show/hide based on nearbyness/line-of-sight
-- nearby checks are primarily to prevent a lot of LOS checks
if distance < 250 and HasEntityClearLosToEntity(PlayerPedId(), ped, 17) then
SetMpGamerTagVisibility(tag, gtComponent.GAMER_NAME, true)
SetMpGamerTagVisibility(tag, gtComponent.healthArmour, IsPlayerTargettingEntity(PlayerId(), ped))
SetMpGamerTagVisibility(tag, gtComponent.AUDIO_ICON, NetworkIsPlayerTalking(i))
SetMpGamerTagAlpha(tag, gtComponent.AUDIO_ICON, 255)
SetMpGamerTagAlpha(tag, gtComponent.healthArmour, 255)
-- override settings
local settings = mpGamerTagSettings[i]
for k, v in pairs(settings.toggles) do
SetMpGamerTagVisibility(tag, gtComponent[k], v)
end
for k, v in pairs(settings.alphas) do
SetMpGamerTagAlpha(tag, gtComponent[k], v)
end
for k, v in pairs(settings.colors) do
SetMpGamerTagColour(tag, gtComponent[k], v)
end
if settings.wantedLevel then
SetMpGamerTagWantedLevel(tag, settings.wantedLevel)
end
if settings.healthColor then
SetMpGamerTagHealthBarColour(tag, settings.healthColor)
end
else
SetMpGamerTagVisibility(tag, gtComponent.GAMER_NAME, false)
SetMpGamerTagVisibility(tag, gtComponent.healthArmour, false)
SetMpGamerTagVisibility(tag, gtComponent.AUDIO_ICON, false)
end
elseif mpGamerTags[i] then
RemoveMpGamerTag(mpGamerTags[i].tag)
mpGamerTags[i] = nil
end
end
end
local function getSettings(id)
local i = GetPlayerFromServerId(tonumber(id))
if not mpGamerTagSettings[i] then
mpGamerTagSettings[i] = makeSettings()
end
return mpGamerTagSettings[i]
end
RegisterNetEvent('playernames:configure')
AddEventHandler('playernames:configure', function(id, key, ...)
local args = table.pack(...)
if key == 'tglc' then
getSettings(id).toggles[args[1]] = args[2]
elseif key == 'seta' then
getSettings(id).alphas[args[1]] = args[2]
elseif key == 'setc' then
getSettings(id).colors[args[1]] = args[2]
elseif key == 'setw' then
getSettings(id).wantedLevel = args[1]
elseif key == 'sehc' then
getSettings(id).healthColor = args[1]
elseif key == 'rnme' then
getSettings(id).rename = true
elseif key == 'name' then
getSettings(id).serverName = args[1]
getSettings(id).rename = true
elseif key == 'tpl' then
for _, v in pairs(mpGamerTagSettings) do
v.rename = true
end
templateStr = args[1]
end
end)
AddEventHandler('playernames:extendContext', function(i, cb)
cb('serverName', getSettings(GetPlayerServerId(i)).serverName)
end)
AddEventHandler('onResourceStop', function(name)
if name == GetCurrentResourceName() then
for _, v in pairs(mpGamerTags) do
RemoveMpGamerTag(v.tag)
end
end
end)
SetTimeout(0, function()
TriggerServerEvent('playernames:init')
end)
-- run this function every frame
SetTimeout(0, updatePlayerNames)

View File

@@ -0,0 +1,46 @@
local curTemplate
local curTags = {}
local activePlayers = {}
local function detectUpdates()
SetTimeout(500, detectUpdates)
local template = GetConvar('playerNames_template', '[{{id}}] {{name}}')
if curTemplate ~= template then
setNameTemplate(-1, template)
curTemplate = template
end
template = GetConvar('playerNames_svTemplate', '[{{id}}] {{name}}')
for v, _ in pairs(activePlayers) do
local newTag = formatPlayerNameTag(v, template)
if newTag ~= curTags[v] then
setName(v, newTag)
curTags[v] = newTag
end
end
for i, tag in pairs(curTags) do
if not activePlayers[i] then
curTags[i] = nil -- in case curTags doesnt get cleared when the player left, clear it now.
end
end
end
AddEventHandler('playerDropped', function()
curTags[source] = nil
activePlayers[source] = nil
end)
RegisterNetEvent('playernames:init')
AddEventHandler('playernames:init', function()
reconfigure(source)
activePlayers[source] = true
end)
detectUpdates()

View File

@@ -0,0 +1,27 @@
Copyright (c) 2014 - 2017 Aapo Talvensaari
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
* Neither the name of the {organization} nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,478 @@
local setmetatable = setmetatable
local loadstring = loadstring
local loadchunk
local tostring = tostring
local setfenv = setfenv
local require = require
local capture
local concat = table.concat
local assert = assert
local prefix
local write = io.write
local pcall = pcall
local phase
local open = io.open
local load = load
local type = type
local dump = string.dump
local find = string.find
local gsub = string.gsub
local byte = string.byte
local null
local sub = string.sub
local ngx = ngx
local jit = jit
local var
local _VERSION = _VERSION
local _ENV = _ENV
local _G = _G
local HTML_ENTITIES = {
["&"] = "&amp;",
["<"] = "&lt;",
[">"] = "&gt;",
['"'] = "&quot;",
["'"] = "&#39;",
["/"] = "&#47;"
}
local CODE_ENTITIES = {
["{"] = "&#123;",
["}"] = "&#125;",
["&"] = "&amp;",
["<"] = "&lt;",
[">"] = "&gt;",
['"'] = "&quot;",
["'"] = "&#39;",
["/"] = "&#47;"
}
local VAR_PHASES
local ok, newtab = pcall(require, "table.new")
if not ok then newtab = function() return {} end end
local caching = true
local template = newtab(0, 12)
template._VERSION = "1.9"
template.cache = {}
local function enabled(val)
if val == nil then return true end
return val == true or (val == "1" or val == "true" or val == "on")
end
local function trim(s)
return gsub(gsub(s, "^%s+", ""), "%s+$", "")
end
local function rpos(view, s)
while s > 0 do
local c = sub(view, s, s)
if c == " " or c == "\t" or c == "\0" or c == "\x0B" then
s = s - 1
else
break
end
end
return s
end
local function escaped(view, s)
if s > 1 and sub(view, s - 1, s - 1) == "\\" then
if s > 2 and sub(view, s - 2, s - 2) == "\\" then
return false, 1
else
return true, 1
end
end
return false, 0
end
local function readfile(path)
local file = open(path, "rb")
if not file then return nil end
local content = file:read "*a"
file:close()
return content
end
local function loadlua(path)
return readfile(path) or path
end
local function loadngx(path)
local vars = VAR_PHASES[phase()]
local file, location = path, vars and var.template_location
if sub(file, 1) == "/" then file = sub(file, 2) end
if location and location ~= "" then
if sub(location, -1) == "/" then location = sub(location, 1, -2) end
local res = capture(concat{ location, '/', file})
if res.status == 200 then return res.body end
end
local root = vars and (var.template_root or var.document_root) or prefix
if sub(root, -1) == "/" then root = sub(root, 1, -2) end
return readfile(concat{ root, "/", file }) or path
end
do
if ngx then
VAR_PHASES = {
set = true,
rewrite = true,
access = true,
content = true,
header_filter = true,
body_filter = true,
log = true
}
template.print = ngx.print or write
template.load = loadngx
prefix, var, capture, null, phase = ngx.config.prefix(), ngx.var, ngx.location.capture, ngx.null, ngx.get_phase
if VAR_PHASES[phase()] then
caching = enabled(var.template_cache)
end
else
template.print = write
template.load = loadlua
end
if _VERSION == "Lua 5.1" then
local context = { __index = function(t, k)
return t.context[k] or t.template[k] or _G[k]
end }
if jit then
loadchunk = function(view)
return assert(load(view, nil, nil, setmetatable({ template = template }, context)))
end
else
loadchunk = function(view)
local func = assert(loadstring(view))
setfenv(func, setmetatable({ template = template }, context))
return func
end
end
else
local context = { __index = function(t, k)
return t.context[k] or t.template[k] or _ENV[k]
end }
loadchunk = function(view)
return assert(load(view, nil, nil, setmetatable({ template = template }, context)))
end
end
end
function template.caching(enable)
if enable ~= nil then caching = enable == true end
return caching
end
function template.output(s)
if s == nil or s == null then return "" end
if type(s) == "function" then return template.output(s()) end
return tostring(s)
end
function template.escape(s, c)
if type(s) == "string" then
if c then return gsub(s, "[}{\">/<'&]", CODE_ENTITIES) end
return gsub(s, "[\">/<'&]", HTML_ENTITIES)
end
return template.output(s)
end
function template.new(view, layout)
assert(view, "view was not provided for template.new(view, layout).")
local render, compile = template.render, template.compile
if layout then
if type(layout) == "table" then
return setmetatable({ render = function(self, context)
local context = context or self
context.blocks = context.blocks or {}
context.view = compile(view)(context)
layout.blocks = context.blocks or {}
layout.view = context.view or ""
return layout:render()
end }, { __tostring = function(self)
local context = self
context.blocks = context.blocks or {}
context.view = compile(view)(context)
layout.blocks = context.blocks or {}
layout.view = context.view
return tostring(layout)
end })
else
return setmetatable({ render = function(self, context)
local context = context or self
context.blocks = context.blocks or {}
context.view = compile(view)(context)
return render(layout, context)
end }, { __tostring = function(self)
local context = self
context.blocks = context.blocks or {}
context.view = compile(view)(context)
return compile(layout)(context)
end })
end
end
return setmetatable({ render = function(self, context)
return render(view, context or self)
end }, { __tostring = function(self)
return compile(view)(self)
end })
end
function template.precompile(view, path, strip)
local chunk = dump(template.compile(view), strip ~= false)
if path then
local file = open(path, "wb")
file:write(chunk)
file:close()
end
return chunk
end
function template.compile(view, key, plain)
assert(view, "view was not provided for template.compile(view, key, plain).")
if key == "no-cache" then
return loadchunk(template.parse(view, plain)), false
end
key = key or view
local cache = template.cache
if cache[key] then return cache[key], true end
local func = loadchunk(template.parse(view, plain))
if caching then cache[key] = func end
return func, false
end
function template.parse(view, plain)
assert(view, "view was not provided for template.parse(view, plain).")
if not plain then
view = template.load(view)
if byte(view, 1, 1) == 27 then return view end
end
local j = 2
local c = {[[
context=... or {}
local function include(v, c) return template.compile(v)(c or context) end
local ___,blocks,layout={},blocks or {}
]] }
local i, s = 1, find(view, "{", 1, true)
while s do
local t, p = sub(view, s + 1, s + 1), s + 2
if t == "{" then
local e = find(view, "}}", p, true)
if e then
local z, w = escaped(view, s)
if i < s - w then
c[j] = "___[#___+1]=[=[\n"
c[j+1] = sub(view, i, s - 1 - w)
c[j+2] = "]=]\n"
j=j+3
end
if z then
i = s
else
c[j] = "___[#___+1]=template.escape("
c[j+1] = trim(sub(view, p, e - 1))
c[j+2] = ")\n"
j=j+3
s, i = e + 1, e + 2
end
end
elseif t == "*" then
local e = find(view, "*}", p, true)
if e then
local z, w = escaped(view, s)
if i < s - w then
c[j] = "___[#___+1]=[=[\n"
c[j+1] = sub(view, i, s - 1 - w)
c[j+2] = "]=]\n"
j=j+3
end
if z then
i = s
else
c[j] = "___[#___+1]=template.output("
c[j+1] = trim(sub(view, p, e - 1))
c[j+2] = ")\n"
j=j+3
s, i = e + 1, e + 2
end
end
elseif t == "%" then
local e = find(view, "%}", p, true)
if e then
local z, w = escaped(view, s)
if z then
if i < s - w then
c[j] = "___[#___+1]=[=[\n"
c[j+1] = sub(view, i, s - 1 - w)
c[j+2] = "]=]\n"
j=j+3
end
i = s
else
local n = e + 2
if sub(view, n, n) == "\n" then
n = n + 1
end
local r = rpos(view, s - 1)
if i <= r then
c[j] = "___[#___+1]=[=[\n"
c[j+1] = sub(view, i, r)
c[j+2] = "]=]\n"
j=j+3
end
c[j] = trim(sub(view, p, e - 1))
c[j+1] = "\n"
j=j+2
s, i = n - 1, n
end
end
elseif t == "(" then
local e = find(view, ")}", p, true)
if e then
local z, w = escaped(view, s)
if i < s - w then
c[j] = "___[#___+1]=[=[\n"
c[j+1] = sub(view, i, s - 1 - w)
c[j+2] = "]=]\n"
j=j+3
end
if z then
i = s
else
local f = sub(view, p, e - 1)
local x = find(f, ",", 2, true)
if x then
c[j] = "___[#___+1]=include([=["
c[j+1] = trim(sub(f, 1, x - 1))
c[j+2] = "]=],"
c[j+3] = trim(sub(f, x + 1))
c[j+4] = ")\n"
j=j+5
else
c[j] = "___[#___+1]=include([=["
c[j+1] = trim(f)
c[j+2] = "]=])\n"
j=j+3
end
s, i = e + 1, e + 2
end
end
elseif t == "[" then
local e = find(view, "]}", p, true)
if e then
local z, w = escaped(view, s)
if i < s - w then
c[j] = "___[#___+1]=[=[\n"
c[j+1] = sub(view, i, s - 1 - w)
c[j+2] = "]=]\n"
j=j+3
end
if z then
i = s
else
c[j] = "___[#___+1]=include("
c[j+1] = trim(sub(view, p, e - 1))
c[j+2] = ")\n"
j=j+3
s, i = e + 1, e + 2
end
end
elseif t == "-" then
local e = find(view, "-}", p, true)
if e then
local x, y = find(view, sub(view, s, e + 1), e + 2, true)
if x then
local z, w = escaped(view, s)
if z then
if i < s - w then
c[j] = "___[#___+1]=[=[\n"
c[j+1] = sub(view, i, s - 1 - w)
c[j+2] = "]=]\n"
j=j+3
end
i = s
else
y = y + 1
x = x - 1
if sub(view, y, y) == "\n" then
y = y + 1
end
local b = trim(sub(view, p, e - 1))
if b == "verbatim" or b == "raw" then
if i < s - w then
c[j] = "___[#___+1]=[=[\n"
c[j+1] = sub(view, i, s - 1 - w)
c[j+2] = "]=]\n"
j=j+3
end
c[j] = "___[#___+1]=[=["
c[j+1] = sub(view, e + 2, x)
c[j+2] = "]=]\n"
j=j+3
else
if sub(view, x, x) == "\n" then
x = x - 1
end
local r = rpos(view, s - 1)
if i <= r then
c[j] = "___[#___+1]=[=[\n"
c[j+1] = sub(view, i, r)
c[j+2] = "]=]\n"
j=j+3
end
c[j] = 'blocks["'
c[j+1] = b
c[j+2] = '"]=include[=['
c[j+3] = sub(view, e + 2, x)
c[j+4] = "]=]\n"
j=j+5
end
s, i = y - 1, y
end
end
end
elseif t == "#" then
local e = find(view, "#}", p, true)
if e then
local z, w = escaped(view, s)
if i < s - w then
c[j] = "___[#___+1]=[=[\n"
c[j+1] = sub(view, i, s - 1 - w)
c[j+2] = "]=]\n"
j=j+3
end
if z then
i = s
else
e = e + 2
if sub(view, e, e) == "\n" then
e = e + 1
end
s, i = e - 1, e
end
end
end
s = find(view, "{", s + 1, true)
end
s = sub(view, i)
if s and s ~= "" then
c[j] = "___[#___+1]=[=[\n"
c[j+1] = s
c[j+2] = "]=]\n"
j=j+3
end
c[j] = "return layout and include(layout,setmetatable({view=table.concat(___),blocks=blocks},{__index=context})) or table.concat(___)"
return concat(c)
end
function template.render(view, context, key, plain)
assert(view, "view was not provided for template.render(view, context, key, plain).")
return template.print(template.compile(view, key, plain)(context))
end
return template