Init commit Fivem Data

This commit is contained in:
jack
2026-06-20 13:18:14 +00:00
commit b8dd09ae66
137 changed files with 161716 additions and 0 deletions

0
.replxx_history Normal file
View File

38
README.md Normal file
View File

@@ -0,0 +1,38 @@
# cfx-server-data
_The data repository for Cfx.re servers_
## Deprecation notice
This repository itself is considered finalized/immutable and is currently archived. Pull requests or issue reports were not merged for a *while*, using the contents of the repository should still be fine (and this shouldn't change until mentioned otherwise).
In the future, relevant parts of this repository will be moved to [citizenfx/fivem](https://github.com/citizenfx/fivem) to be bundled with actual server builds, and the examples (+ legacy API-like resources) will be moved to a new repository.
If there's any critical issue in the code in this repository, posting on the [Cfx.re forums](https://forum.cfx.re/) may work.
## Usage
1. Make sure to `git clone`. Don't "Download ZIP", as that'll make it _much_ harder to update to newer versions.
2. Put custom resources in `resources/[local]/` if you don't want to be affected by any random messups.
### Advanced usage
You can also consider using the repository as a submodule + symlink for your own Git repository:
**Linux**:
```
$ git submodule add https://github.com/citizenfx/cfx-server-data.git vendor/server-data
$ ln -s ../vendor/server-data/resources/ 'resources/[base]/'
```
**Windows**:
```
> git submodule add https://github.com/citizenfx/cfx-server-data.git vendor/server-data
> mklink /d resources\[base] ..\vendor\server-data\resources
```
## Policy
You can make pull requests to propose changes that benefit _everyone_. Add new useful resources, change/improve
existing ones - anything goes, as long as you make sure to:
1. Not break existing users/APIs.
2. Not change default behavior without a toggle.
3. Use best practices (convars over config files, native commands wherever possible, etc.)
Modifying or rewriting existing resources in this repository for local use only is _strongly_ discouraged.

BIN
cache/files/hardcap/resource.rpf vendored Executable file

Binary file not shown.

1
cache/files/hardcap/resource.rpf.db vendored Executable file
View File

@@ -0,0 +1 @@
[{"n":"/config/resources//[system]/hardcap/client.lua","mt":1780096812,"s":169,"i":"qcUjAAAAAAAAAAAAAAAAAA=="},{"n":"/config/resources//[system]/hardcap/fxmanifest.lua","mt":1780096812,"s":593,"i":"q8UjAAAAAAAAAAAAAAAAAA=="}]

0
cache/files/hardcap/resource.sfl vendored Executable file
View File

1
cache/files/hardcap/resource.sfl.db vendored Executable file
View File

@@ -0,0 +1 @@
[]

BIN
cache/files/mapmanager/resource.rpf vendored Executable file

Binary file not shown.

1
cache/files/mapmanager/resource.rpf.db vendored Executable file
View File

@@ -0,0 +1 @@
[{"n":"/config/resources//[managers]/mapmanager/fxmanifest.lua","mt":1780096812,"s":872,"i":"gcUjAAAAAAAAAAAAAAAAAA=="},{"n":"/config/resources//[managers]/mapmanager/mapmanager_client.lua","mt":1780096812,"s":2815,"i":"fsUjAAAAAAAAAAAAAAAAAA=="},{"n":"/config/resources//[managers]/mapmanager/mapmanager_shared.lua","mt":1780096812,"s":2225,"i":"f8UjAAAAAAAAAAAAAAAAAA=="}]

0
cache/files/mapmanager/resource.sfl vendored Executable file
View File

1
cache/files/mapmanager/resource.sfl.db vendored Executable file
View File

@@ -0,0 +1 @@
[]

BIN
cache/files/sessionmanager/resource.rpf vendored Executable file

Binary file not shown.

1
cache/files/sessionmanager/resource.rpf.db vendored Executable file
View File

@@ -0,0 +1 @@
[{"n":"/config/resources//[system]/sessionmanager/client/empty.lua","mt":1780096812,"s":242,"i":"mMUjAAAAAAAAAAAAAAAAAA=="},{"n":"/config/resources//[system]/sessionmanager/fxmanifest.lua","mt":1780096812,"s":448,"i":"m8UjAAAAAAAAAAAAAAAAAA=="}]

0
cache/files/sessionmanager/resource.sfl vendored Executable file
View File

1
cache/files/sessionmanager/resource.sfl.db vendored Executable file
View File

@@ -0,0 +1 @@
[]

BIN
cache/files/spawnmanager/resource.rpf vendored Executable file

Binary file not shown.

1
cache/files/spawnmanager/resource.rpf.db vendored Executable file
View File

@@ -0,0 +1 @@
[{"n":"/config/resources//[managers]/spawnmanager/fxmanifest.lua","mt":1780096812,"s":602,"i":"hMUjAAAAAAAAAAAAAAAAAA=="},{"n":"/config/resources//[managers]/spawnmanager/spawnmanager.lua","mt":1780096812,"s":10483,"i":"g8UjAAAAAAAAAAAAAAAAAA=="}]

0
cache/files/spawnmanager/resource.sfl vendored Executable file
View File

1
cache/files/spawnmanager/resource.sfl.db vendored Executable file
View File

@@ -0,0 +1 @@
[]

BIN
cache/files/sw-rp/resource.rpf vendored Executable file

Binary file not shown.

1
cache/files/sw-rp/resource.rpf.db vendored Executable file
View File

@@ -0,0 +1 @@
[{"n":"/config/resources//sw-rp/dist/client/index.js","mt":1780105118,"s":24287,"i":"Udc2AAAAAAAAAAAAAAAAAA=="},{"n":"/config/resources//sw-rp/fxmanifest.lua","mt":1780093784,"s":436,"i":"txs1AAAAAAAAAAAAAAAAAA=="},{"n":"/config/resources//sw-rp/ui/assets/GoogleSans-VF.woff2","mt":1780100113,"s":1497036,"i":"VtQ2AAAAAAAAAAAAAAAAAA=="},{"n":"/config/resources//sw-rp/ui/assets/Inter-VF.woff2","mt":1780100113,"s":348956,"i":"WdQ2AAAAAAAAAAAAAAAAAA=="},{"n":"/config/resources//sw-rp/ui/assets/bg.jpg","mt":1780100113,"s":43548,"i":"WNQ2AAAAAAAAAAAAAAAAAA=="},{"n":"/config/resources//sw-rp/ui/assets/swrp-logo.png","mt":1780100113,"s":560566,"i":"V9Q2AAAAAAAAAAAAAAAAAA=="},{"n":"/config/resources//sw-rp/ui/chunk-P3AUIBQA.js","mt":1780100113,"s":1353192,"i":"jPg0AAAAAAAAAAAAAAAAAA=="},{"n":"/config/resources//sw-rp/ui/chunk-SDV3ZBF6.js","mt":1780100113,"s":270484,"i":"j/g0AAAAAAAAAAAAAAAAAA=="},{"n":"/config/resources//sw-rp/ui/index.html","mt":1780101082,"s":2483113,"i":"IBk1AAAAAAAAAAAAAAAAAA=="},{"n":"/config/resources//sw-rp/ui/main.js","mt":1780100113,"s":1123,"i":"jfg0AAAAAAAAAAAAAAAAAA=="},{"n":"/config/resources//sw-rp/ui/styles.css","mt":1780100113,"s":2471352,"i":"kvg0AAAAAAAAAAAAAAAAAA=="}]

0
cache/files/sw-rp/resource.sfl vendored Executable file
View File

1
cache/files/sw-rp/resource.sfl.db vendored Executable file
View File

@@ -0,0 +1 @@
[]

BIN
core.6 Normal file

Binary file not shown.

BIN
core.7 Normal file

Binary file not shown.

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'
author 'Cfx.re <root@cfx.re>'
description 'Example spawn points for FiveM with a "hipster" model.'
repository 'https://github.com/citizenfx/cfx-server-data'
resource_type 'map' { gameTypes = { ['basic-gamemode'] = true } }
map 'map.lua'
fx_version 'adamant'
game 'gta5'

View File

@@ -0,0 +1,63 @@
vehicle_generator "airtug" { -54.26639938354492, -1679.548828125, 28.4414, heading = 228.2736053466797 }
spawnpoint 'a_m_y_hipster_01' { x = -802.311, y = 175.056, z = 72.8446 }
spawnpoint 'a_m_y_hipster_02' { x = -9.96562, y = -1438.54, z = 31.1015 }
spawnpoint 'a_m_y_hipster_01' { x = 0.916756, y = 528.485, z = 174.628 }
spawnpoint 'a_m_y_hipster_01' { x = -181.615, y = 852.8, z = 232.701 }
spawnpoint 'a_m_y_hipster_02' { x = 657.723, y = 457.342, z = 144.641 }
spawnpoint 'a_m_y_hipster_01' { x = 134.387, y = 1150.31, z = 231.594 }
spawnpoint 'a_m_y_hipster_02' { x = 726.14, y = 1196.91, z = 326.262 }
spawnpoint 'a_m_y_hipster_01' { x = 740.792, y = 1283.62, z = 360.297 }
spawnpoint 'a_m_y_hipster_02' { x = -437.009, y = 1059.59, z = 327.331 }
spawnpoint 'a_m_y_hipster_01' { x = -428.771, y = 1596.8, z = 356.338 }
spawnpoint 'a_m_y_hipster_02' { x = -1348.78, y = 723.87, z = 186.45 }
spawnpoint 'a_m_y_hipster_01' { x = -1543.24, y = 830.069, z = 182.132 }
spawnpoint 'a_m_y_hipster_02' { x = -2150.48, y = 222.019, z = 184.602 }
spawnpoint 'a_m_y_hipster_01' { x = -3032.13, y = 22.2157, z = 10.1184 }
spawnpoint 'a_m_y_hipster_02' { x = 3063.97, y = 5608.88, z = 209.245 }
spawnpoint 'a_m_y_hipster_01' { x = -2614.35, y = 1872.49, z = 167.32 }
spawnpoint 'a_m_y_hipster_02' { x = -1873.94, y = 2088.73, z = 140.994 }
spawnpoint 'a_m_y_hipster_01' { x = -597.177, y = 2092.16, z = 131.413 }
spawnpoint 'a_m_y_hipster_02' { x = 967.126, y = 2226.99, z = 54.0588 }
spawnpoint 'a_m_y_hipster_01' { x = -338.043, y = 2829, z = 56.0871 }
spawnpoint 'a_m_y_hipster_02' { x = 1082.25, y = -696.921, z = 58.0099 }
spawnpoint 'a_m_y_hipster_01' { x = 1658.31, y = -13.9234, z = 169.992 }
spawnpoint 'a_m_y_hipster_02' { x = 2522.98, y = -384.436, z = 92.9928 }
spawnpoint 'a_m_y_hipster_01' { x = 2826.27, y = -656.489, z = 1.87841 }
spawnpoint 'a_m_y_hipster_02' { x = 2851.12, y = 1467.5, z = 24.5554 }
spawnpoint 'a_m_y_hipster_01' { x = 2336.33, y = 2535.39, z = 46.5177 }
spawnpoint 'a_m_y_hipster_02' { x = 2410.46, y = 3077.88, z = 48.1529 }
spawnpoint 'a_m_y_hipster_01' { x = 2451.15, y = 3768.37, z = 41.3477 }
spawnpoint 'a_m_y_hipster_02' { x = 3337.78, y = 5174.8, z = 18.2108 }
spawnpoint 'a_m_y_hipster_01' { x = -1119.33, y = 4978.52, z = 186.26 }
spawnpoint 'a_m_y_hipster_02' { x = 2877.3, y = 5911.57, z = 369.618 }
spawnpoint 'a_m_y_hipster_01' { x = 2942.1, y = 5306.73, z = 101.52 }
spawnpoint 'a_m_y_hipster_02' { x = 2211.29, y = 5577.94, z = 53.872 }
spawnpoint 'a_m_y_hipster_01' { x = 1602.39, y = 6623.02, z = 15.8417 }
spawnpoint 'a_m_y_hipster_02' { x = 66.0113, y = 7203.58, z = 3.16 }
spawnpoint 'a_m_y_hipster_01' { x = -219.201, y = 6562.82, z = 10.9706 }
spawnpoint 'a_m_y_hipster_02' { x = -45.1562, y = 6301.64, z = 31.6114 }
spawnpoint 'a_m_y_hipster_01' { x = -1004.77, y = 4854.32, z = 274.606 }
spawnpoint 'a_m_y_hipster_02' { x = -1580.01, y = 5173.3, z = 19.5813 }
spawnpoint 'a_m_y_hipster_01' { x = -1467.95, y = 5416.2, z = 23.5959 }
spawnpoint 'a_m_y_hipster_02' { x = -2359.31, y = 3243.83, z = 92.9037 }
spawnpoint 'a_m_y_hipster_01' { x = -2612.96, y = 3555.03, z = 4.85649 }
spawnpoint 'a_m_y_hipster_02' { x = -2083.27, y = 2616.94, z = 3.08396 }
spawnpoint 'a_m_y_hipster_01' { x = -524.471, y = 4195, z = 193.731 }
spawnpoint 'a_m_y_hipster_02' { x = -840.713, y = 4183.18, z = 215.29 }
spawnpoint 'a_m_y_hipster_01' { x = -1576.24, y = 2103.87, z = 67.576 }
spawnpoint 'a_m_y_hipster_02' { x = -1634.37, y = 209.816, z = 60.6413 }
spawnpoint 'a_m_y_hipster_01' { x = -1495.07, y = 142.697, z = 55.6527 }
spawnpoint 'a_m_y_hipster_02' { x = -1715.41, y = -197.722, z = 57.698 }
spawnpoint 'a_m_y_hipster_01' { x = -1181.07, y = -505.544, z = 35.5661 }
spawnpoint 'a_m_y_hipster_02' { x = -1712.37, y = -1082.91, z = 13.0801 }
spawnpoint 'a_m_y_hipster_01' { x = -1352.43, y = -1542.75, z = 4.42268 }
spawnpoint 'a_m_y_hipster_02' { x = -1756.89, y = 427.531, z = 127.685 }
spawnpoint 'a_m_y_hipster_01' { x = 3060.2, y = 2113.2, z = 1.6613 }
spawnpoint 'a_m_y_hipster_02' { x = 501.646, y = 5604.53, z = 797.91 }
spawnpoint 'a_m_y_hipster_01' { x = 714.109, y = 4151.15, z = 35.7792 }
spawnpoint 'a_m_y_hipster_02' { x = -103.651, y = -967.93, z = 296.52 }
spawnpoint 'a_m_y_hipster_01' { x = -265.333, y = -2419.35, z = 122.366 }
spawnpoint 'a_m_y_hipster_02' { x = 1788.25, y = 3890.34, z = 34.3849 }
--

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'
author 'Cfx.re <root@cfx.re>'
description 'Example spawn points for FiveM with a "skater" model.'
repository 'https://github.com/citizenfx/cfx-server-data'
resource_type 'map' { gameTypes = { ['basic-gamemode'] = true } }
map 'map.lua'
fx_version 'adamant'
game 'gta5'

View File

@@ -0,0 +1,62 @@
spawnpoint 'a_m_y_skater_01' { x = -802.311, y = 175.056, z = 72.8446 }
spawnpoint 'a_m_y_skater_02' { x = -9.96562, y = -1438.54, z = 31.1015 }
spawnpoint 'a_m_y_skater_01' { x = 0.916756, y = 528.485, z = 174.628 }
spawnpoint 'a_m_y_skater_02' { x = 1975.86, y = 3821.03, z = 33.4501 }
spawnpoint 'a_m_y_skater_01' { x = -181.615, y = 852.8, z = 232.701 }
spawnpoint 'a_m_y_skater_02' { x = 657.723, y = 457.342, z = 144.641 }
spawnpoint 'a_m_y_skater_01' { x = 134.387, y = 1150.31, z = 231.594 }
spawnpoint 'a_m_y_skater_02' { x = 726.14, y = 1196.91, z = 326.262 }
spawnpoint 'a_m_y_skater_01' { x = 740.792, y = 1283.62, z = 360.297 }
spawnpoint 'a_m_y_skater_02' { x = -437.009, y = 1059.59, z = 327.331 }
spawnpoint 'a_m_y_skater_01' { x = -428.771, y = 1596.8, z = 356.338 }
spawnpoint 'a_m_y_skater_02' { x = -1348.78, y = 723.87, z = 186.45 }
spawnpoint 'a_m_y_skater_01' { x = -1543.24, y = 830.069, z = 182.132 }
spawnpoint 'a_m_y_skater_02' { x = -2150.48, y = 222.019, z = 184.602 }
spawnpoint 'a_m_y_skater_01' { x = -3032.13, y = 22.2157, z = 10.1184 }
spawnpoint 'a_m_y_skater_02' { x = 3063.97, y = 5608.88, z = 209.245 }
spawnpoint 'a_m_y_skater_01' { x = -2614.35, y = 1872.49, z = 167.32 }
spawnpoint 'a_m_y_skater_02' { x = -1873.94, y = 2088.73, z = 140.994 }
spawnpoint 'a_m_y_skater_01' { x = -597.177, y = 2092.16, z = 131.413 }
spawnpoint 'a_m_y_skater_02' { x = 967.126, y = 2226.99, z = 54.0588 }
spawnpoint 'a_m_y_skater_01' { x = -338.043, y = 2829, z = 56.0871 }
spawnpoint 'a_m_y_skater_02' { x = 1082.25, y = -696.921, z = 58.0099 }
spawnpoint 'a_m_y_skater_01' { x = 1658.31, y = -13.9234, z = 169.992 }
spawnpoint 'a_m_y_skater_02' { x = 2522.98, y = -384.436, z = 92.9928 }
spawnpoint 'a_m_y_skater_01' { x = 2826.27, y = -656.489, z = 1.87841 }
spawnpoint 'a_m_y_skater_02' { x = 2851.12, y = 1467.5, z = 24.5554 }
spawnpoint 'a_m_y_skater_01' { x = 2336.33, y = 2535.39, z = 46.5177 }
spawnpoint 'a_m_y_skater_02' { x = 2410.46, y = 3077.88, z = 48.1529 }
spawnpoint 'a_m_y_skater_01' { x = 2451.15, y = 3768.37, z = 41.3477 }
spawnpoint 'a_m_y_skater_02' { x = 3337.78, y = 5174.8, z = 18.2108 }
spawnpoint 'a_m_y_skater_01' { x = -1119.33, y = 4978.52, z = 186.26 }
spawnpoint 'a_m_y_skater_02' { x = 2877.3, y = 5911.57, z = 369.618 }
spawnpoint 'a_m_y_skater_01' { x = 2942.1, y = 5306.73, z = 101.52 }
spawnpoint 'a_m_y_skater_02' { x = 2211.29, y = 5577.94, z = 53.872 }
spawnpoint 'a_m_y_skater_01' { x = 1602.39, y = 6623.02, z = 15.8417 }
spawnpoint 'a_m_y_skater_02' { x = 66.0113, y = 7203.58, z = 3.16 }
spawnpoint 'a_m_y_skater_01' { x = -219.201, y = 6562.82, z = 10.9706 }
spawnpoint 'a_m_y_skater_02' { x = -45.1562, y = 6301.64, z = 31.6114 }
spawnpoint 'a_m_y_skater_01' { x = -1004.77, y = 4854.32, z = 274.606 }
spawnpoint 'a_m_y_skater_02' { x = -1580.01, y = 5173.3, z = 19.5813 }
spawnpoint 'a_m_y_skater_01' { x = -1467.95, y = 5416.2, z = 23.5959 }
spawnpoint 'a_m_y_skater_02' { x = -2359.31, y = 3243.83, z = 92.9037 }
spawnpoint 'a_m_y_skater_01' { x = -2612.96, y = 3555.03, z = 4.85649 }
spawnpoint 'a_m_y_skater_02' { x = -2083.27, y = 2616.94, z = 3.08396 }
spawnpoint 'a_m_y_skater_01' { x = -524.471, y = 4195, z = 193.731 }
spawnpoint 'a_m_y_skater_02' { x = -840.713, y = 4183.18, z = 215.29 }
spawnpoint 'a_m_y_skater_01' { x = -1576.24, y = 2103.87, z = 67.576 }
spawnpoint 'a_m_y_skater_02' { x = -1634.37, y = 209.816, z = 60.6413 }
spawnpoint 'a_m_y_skater_01' { x = -1495.07, y = 142.697, z = 55.6527 }
spawnpoint 'a_m_y_skater_02' { x = -1715.41, y = -197.722, z = 57.698 }
spawnpoint 'a_m_y_skater_01' { x = -1181.07, y = -505.544, z = 35.5661 }
spawnpoint 'a_m_y_skater_02' { x = -1712.37, y = -1082.91, z = 13.0801 }
spawnpoint 'a_m_y_skater_01' { x = -1352.43, y = -1542.75, z = 4.42268 }
spawnpoint 'a_m_y_skater_02' { x = -1756.89, y = 427.531, z = 127.685 }
spawnpoint 'a_m_y_skater_01' { x = 3060.2, y = 2113.2, z = 1.6613 }
spawnpoint 'a_m_y_skater_02' { x = 501.646, y = 5604.53, z = 797.91 }
spawnpoint 'a_m_y_skater_01' { x = 714.109, y = 4151.15, z = 35.7792 }
spawnpoint 'a_m_y_skater_02' { x = -103.651, y = -967.93, z = 296.52 }
spawnpoint 'a_m_y_skater_01' { x = -265.333, y = -2419.35, z = 122.366 }
spawnpoint 'a_m_y_skater_02' { x = 1788.25, y = 3890.34, z = 34.3849 }
--

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'
author 'Cfx.re <root@cfx.re>'
description 'Example spawn points for RedM.'
repository 'https://github.com/citizenfx/cfx-server-data'
resource_type 'map' { gameTypes = { ['basic-gamemode'] = true } }
map 'map.lua'
fx_version 'adamant'
game 'rdr3'
rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.'

View File

@@ -0,0 +1,2 @@
spawnpoint 'player_three' { x = -262.849, y = 793.404, z = 118.087 }
spawnpoint 'player_zero' { x = -262.849, y = 793.404, z = 118.087 }

View File

@@ -0,0 +1,4 @@
AddEventHandler('onClientMapStart', function()
exports.spawnmanager:setAutoSpawn(true)
exports.spawnmanager:forceRespawn()
end)

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'
author 'Cfx.re <root@cfx.re>'
description 'A basic freeroam gametype that uses the default spawn logic from spawnmanager.'
repository 'https://github.com/citizenfx/cfx-server-data'
resource_type 'gametype' { name = 'Freeroam' }
client_script 'basic_client.lua'
game 'common'
fx_version 'adamant'

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);
}
}

4
resources/[gameplay]/chat/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules/
.yarn.installed
yarn-error.log
dist/

View File

@@ -0,0 +1 @@
# Chat

View File

@@ -0,0 +1,308 @@
local isRDR = not TerraingridActivate and true or false
local chatInputActive = false
local chatInputActivating = false
local chatLoaded = false
RegisterNetEvent('chatMessage')
RegisterNetEvent('chat:addTemplate')
RegisterNetEvent('chat:addMessage')
RegisterNetEvent('chat:addSuggestion')
RegisterNetEvent('chat:addSuggestions')
RegisterNetEvent('chat:addMode')
RegisterNetEvent('chat:removeMode')
RegisterNetEvent('chat:removeSuggestion')
RegisterNetEvent('chat:clear')
-- internal events
RegisterNetEvent('__cfx_internal:serverPrint')
RegisterNetEvent('_chat:messageEntered')
--deprecated, use chat:addMessage
AddEventHandler('chatMessage', function(author, color, text)
local args = { text }
if author ~= "" then
table.insert(args, 1, author)
end
SendNUIMessage({
type = 'ON_MESSAGE',
message = {
color = color,
multiline = true,
args = args
}
})
end)
AddEventHandler('__cfx_internal:serverPrint', function(msg)
print(msg)
SendNUIMessage({
type = 'ON_MESSAGE',
message = {
templateId = 'print',
multiline = true,
args = { msg },
mode = '_global'
}
})
end)
-- addMessage
local addMessage = function(message)
if type(message) == 'string' then
message = {
args = { message }
}
end
SendNUIMessage({
type = 'ON_MESSAGE',
message = message
})
end
exports('addMessage', addMessage)
AddEventHandler('chat:addMessage', addMessage)
-- addSuggestion
local addSuggestion = function(name, help, params)
SendNUIMessage({
type = 'ON_SUGGESTION_ADD',
suggestion = {
name = name,
help = help,
params = params or nil
}
})
end
exports('addSuggestion', addSuggestion)
AddEventHandler('chat:addSuggestion', addSuggestion)
AddEventHandler('chat:addSuggestions', function(suggestions)
for _, suggestion in ipairs(suggestions) do
SendNUIMessage({
type = 'ON_SUGGESTION_ADD',
suggestion = suggestion
})
end
end)
AddEventHandler('chat:removeSuggestion', function(name)
SendNUIMessage({
type = 'ON_SUGGESTION_REMOVE',
name = name
})
end)
AddEventHandler('chat:addMode', function(mode)
SendNUIMessage({
type = 'ON_MODE_ADD',
mode = mode
})
end)
AddEventHandler('chat:removeMode', function(name)
SendNUIMessage({
type = 'ON_MODE_REMOVE',
name = name
})
end)
AddEventHandler('chat:addTemplate', function(id, html)
SendNUIMessage({
type = 'ON_TEMPLATE_ADD',
template = {
id = id,
html = html
}
})
end)
AddEventHandler('chat:clear', function(name)
SendNUIMessage({
type = 'ON_CLEAR'
})
end)
RegisterNUICallback('chatResult', function(data, cb)
chatInputActive = false
SetNuiFocus(false)
if not data.canceled then
local id = PlayerId()
--deprecated
local r, g, b = 0, 0x99, 255
if data.message:sub(1, 1) == '/' then
ExecuteCommand(data.message:sub(2))
else
TriggerServerEvent('_chat:messageEntered', GetPlayerName(id), { r, g, b }, data.message, data.mode)
end
end
cb('ok')
end)
local function refreshCommands()
if GetRegisteredCommands then
local registeredCommands = GetRegisteredCommands()
local suggestions = {}
for _, command in ipairs(registeredCommands) do
if IsAceAllowed(('command.%s'):format(command.name)) and command.name ~= 'toggleChat' then
table.insert(suggestions, {
name = '/' .. command.name,
help = ''
})
end
end
TriggerEvent('chat:addSuggestions', suggestions)
end
end
local function refreshThemes()
local themes = {}
for resIdx = 0, GetNumResources() - 1 do
local resource = GetResourceByFindIndex(resIdx)
if GetResourceState(resource) == 'started' then
local numThemes = GetNumResourceMetadata(resource, 'chat_theme')
if numThemes > 0 then
local themeName = GetResourceMetadata(resource, 'chat_theme')
local themeData = json.decode(GetResourceMetadata(resource, 'chat_theme_extra') or 'null')
if themeName and themeData then
themeData.baseUrl = 'nui://' .. resource .. '/'
themes[themeName] = themeData
end
end
end
end
SendNUIMessage({
type = 'ON_UPDATE_THEMES',
themes = themes
})
end
AddEventHandler('onClientResourceStart', function(resName)
Wait(500)
refreshCommands()
refreshThemes()
end)
AddEventHandler('onClientResourceStop', function(resName)
Wait(500)
refreshCommands()
refreshThemes()
end)
RegisterNUICallback('loaded', function(data, cb)
TriggerServerEvent('chat:init')
refreshCommands()
refreshThemes()
chatLoaded = true
cb('ok')
end)
local CHAT_HIDE_STATES = {
SHOW_WHEN_ACTIVE = 0,
ALWAYS_SHOW = 1,
ALWAYS_HIDE = 2
}
local kvpEntry = GetResourceKvpString('hideState')
local chatHideState = kvpEntry and tonumber(kvpEntry) or CHAT_HIDE_STATES.SHOW_WHEN_ACTIVE
local isFirstHide = true
if not isRDR then
if RegisterKeyMapping then
RegisterKeyMapping('toggleChat', 'Toggle chat', 'keyboard', 'l')
end
RegisterCommand('toggleChat', function()
if chatHideState == CHAT_HIDE_STATES.SHOW_WHEN_ACTIVE then
chatHideState = CHAT_HIDE_STATES.ALWAYS_SHOW
elseif chatHideState == CHAT_HIDE_STATES.ALWAYS_SHOW then
chatHideState = CHAT_HIDE_STATES.ALWAYS_HIDE
elseif chatHideState == CHAT_HIDE_STATES.ALWAYS_HIDE then
chatHideState = CHAT_HIDE_STATES.SHOW_WHEN_ACTIVE
end
isFirstHide = false
SetResourceKvp('hideState', tostring(chatHideState))
end, false)
end
Citizen.CreateThread(function()
SetTextChatEnabled(false)
SetNuiFocus(false)
local lastChatHideState = -1
local origChatHideState = -1
while true do
Wait(0)
if not chatInputActive then
if IsControlPressed(0, isRDR and `INPUT_MP_TEXT_CHAT_ALL` or 245) --[[ INPUT_MP_TEXT_CHAT_ALL ]] then
chatInputActive = true
chatInputActivating = true
SendNUIMessage({
type = 'ON_OPEN'
})
end
end
if chatInputActivating then
if not IsControlPressed(0, isRDR and `INPUT_MP_TEXT_CHAT_ALL` or 245) then
SetNuiFocus(true)
chatInputActivating = false
end
end
if chatLoaded then
local forceHide = IsScreenFadedOut() or IsPauseMenuActive()
local wasForceHide = false
if chatHideState ~= CHAT_HIDE_STATES.ALWAYS_HIDE then
if forceHide then
origChatHideState = chatHideState
chatHideState = CHAT_HIDE_STATES.ALWAYS_HIDE
end
elseif not forceHide and origChatHideState ~= -1 then
chatHideState = origChatHideState
origChatHideState = -1
wasForceHide = true
end
if chatHideState ~= lastChatHideState then
lastChatHideState = chatHideState
SendNUIMessage({
type = 'ON_SCREEN_STATE_CHANGE',
hideState = chatHideState,
fromUserInteraction = not forceHide and not isFirstHide and not wasForceHide
})
isFirstHide = false
end
end
end
end)

View File

@@ -0,0 +1,30 @@
-- 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 'Provides baseline chat functionality using a NUI-based interface.'
repository 'https://github.com/citizenfx/cfx-server-data'
ui_page 'dist/ui.html'
client_script 'cl_chat.lua'
server_script 'sv_chat.lua'
files {
'dist/ui.html',
'dist/index.css',
'html/vendor/*.css',
'html/vendor/fonts/*.woff2',
}
fx_version 'adamant'
games { 'rdr3', 'gta5' }
rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.'
dependencies {
'yarn',
'webpack'
}
webpack_config 'webpack.config.js'

View File

@@ -0,0 +1,467 @@
import { post } from './utils';
import CONFIG from './config';
import Vue from 'vue';
import Suggestions from './Suggestions.vue';
import MessageV from './Message.vue';
import { Suggestion } from './Suggestions';
export interface Message {
args: string[];
template: string;
params?: { [key: string]: string };
multiline?: boolean;
color?: [ number, number, number ];
templateId?: number;
mode?: string;
modeData?: Mode;
id?: string;
}
export interface ThemeData {
style: string;
styleSheet: string;
baseUrl: string;
script: string;
templates: { [id: string]: string }; // not supported rn
msgTemplates: { [id: string]: string };
}
export interface Mode {
name: string;
displayName: string;
color: string;
hidden?: boolean;
isChannel?: boolean;
isGlobal?: boolean;
}
enum ChatHideStates {
ShowWhenActive = 0,
AlwaysShow = 1,
AlwaysHide = 2,
}
const defaultMode: Mode = {
name: 'all',
displayName: 'All',
color: '#fff'
};
const globalMode: Mode = {
name: '_global',
displayName: 'All',
color: '#fff',
isGlobal: true,
hidden: true
};
export default Vue.extend({
template: "#app_template",
name: "app",
components: {
Suggestions,
MessageV
},
data() {
return {
style: CONFIG.style,
showInput: false,
showWindow: false,
showHideState: false,
hideState: ChatHideStates.ShowWhenActive,
backingSuggestions: [] as Suggestion[],
removedSuggestions: [] as string[],
templates: { ...CONFIG.templates } as { [ key: string ]: string },
message: "",
messages: [] as Message[],
oldMessages: [] as string[],
oldMessagesIndex: -1,
tplBackups: [] as unknown as [ HTMLElement, string ][],
msgTplBackups: [] as unknown as [ string, string ][],
focusTimer: 0,
showWindowTimer: 0,
showHideStateTimer: 0,
listener: (event: MessageEvent) => {},
modes: [defaultMode, globalMode] as Mode[],
modeIdx: 0,
};
},
destroyed() {
clearInterval(this.focusTimer);
window.removeEventListener("message", this.listener);
},
mounted() {
post("http://chat/loaded", JSON.stringify({}));
this.listener = (event: MessageEvent) => {
const item: any = event.data || (<any>event).detail; //'detail' is for debugging via browsers
if (!item || !item.type) {
return;
}
const typeRef = item.type as
'ON_OPEN' | 'ON_SCREEN_STATE_CHANGE' | 'ON_MESSAGE' | 'ON_CLEAR' | 'ON_SUGGESTION_ADD' |
'ON_SUGGESTION_REMOVE' | 'ON_TEMPLATE_ADD' | 'ON_UPDATE_THEMES' | 'ON_MODE_ADD' | 'ON_MODE_REMOVE';
if (this[typeRef]) {
this[typeRef](item);
}
};
window.addEventListener("message", this.listener);
},
watch: {
messages() {
if (this.hideState !== ChatHideStates.AlwaysHide) {
if (this.showWindowTimer) {
clearTimeout(this.showWindowTimer);
}
this.showWindow = true;
this.resetShowWindowTimer();
}
const messagesObj = this.$refs.messages as HTMLDivElement;
this.$nextTick(() => {
messagesObj.scrollTop = messagesObj.scrollHeight;
});
}
},
computed: {
filteredMessages(): Message[] {
return this.messages.filter(
// show messages that are
// - (if the current mode is a channel) global, or in the current mode
// - (if the message is a channel) in the current mode
el => (el.modeData?.isChannel || this.modes[this.modeIdx].isChannel) ?
(el.mode === this.modes[this.modeIdx].name || el.modeData?.isGlobal) :
true
);
},
suggestions(): Suggestion[] {
return this.backingSuggestions.filter(
el => this.removedSuggestions.indexOf(el.name) <= -1
);
},
hideAnimated(): boolean {
return this.hideState !== ChatHideStates.AlwaysHide;
},
modeIdxGet(): number {
return (this.modeIdx >= this.modes.length) ? (this.modes.length - 1) : this.modeIdx;
},
modePrefix(): string {
if (this.modes.length === 2) {
return ``;
}
return this.modes[this.modeIdxGet].displayName;
},
modeColor(): string {
return this.modes[this.modeIdxGet].color;
},
hideStateString(): string {
// TODO: localization
switch (this.hideState) {
case ChatHideStates.AlwaysShow:
return 'Visible';
case ChatHideStates.AlwaysHide:
return 'Hidden';
case ChatHideStates.ShowWhenActive:
return 'When active';
}
}
},
methods: {
ON_SCREEN_STATE_CHANGE({ hideState, fromUserInteraction }: { hideState: ChatHideStates, fromUserInteraction: boolean }) {
this.hideState = hideState;
if (this.hideState === ChatHideStates.AlwaysHide) {
if (!this.showInput) {
this.showWindow = false;
}
} else if (this.hideState === ChatHideStates.AlwaysShow) {
this.showWindow = true;
if (this.showWindowTimer) {
clearTimeout(this.showWindowTimer);
}
} else {
this.resetShowWindowTimer();
}
if (fromUserInteraction) {
this.showHideState = true;
if (this.showHideStateTimer) {
clearTimeout(this.showHideStateTimer);
}
this.showHideStateTimer = window.setTimeout(() => {
this.showHideState = false;
}, 1500);
}
},
ON_OPEN() {
this.showInput = true;
this.showWindow = true;
if (this.showWindowTimer) {
clearTimeout(this.showWindowTimer);
}
this.focusTimer = window.setInterval(() => {
if (this.$refs.input) {
(this.$refs.input as HTMLInputElement).focus();
} else {
clearInterval(this.focusTimer);
}
}, 100);
},
ON_MESSAGE({ message }: { message: Message }) {
message.id = `${new Date().getTime()}${Math.random()}`;
message.modeData = this.modes.find(mode => mode.name === message.mode);
this.messages.push(message);
},
ON_CLEAR() {
this.messages = [];
this.oldMessages = [];
this.oldMessagesIndex = -1;
},
ON_SUGGESTION_ADD({ suggestion }: { suggestion: Suggestion }) {
this.removedSuggestions = this.removedSuggestions.filter(a => a !== suggestion.name);
const duplicateSuggestion = this.backingSuggestions.find(
a => a.name == suggestion.name
);
if (duplicateSuggestion) {
if (suggestion.help || suggestion.params) {
duplicateSuggestion.help = suggestion.help || "";
duplicateSuggestion.params = suggestion.params || [];
}
return;
}
if (!suggestion.params) {
suggestion.params = []; //TODO Move somewhere else
}
this.backingSuggestions.push(suggestion);
},
ON_SUGGESTION_REMOVE({ name }: { name: string }) {
if (this.removedSuggestions.indexOf(name) <= -1) {
this.removedSuggestions.push(name);
}
},
ON_MODE_ADD({ mode }: { mode: Mode }) {
this.modes = [
...this.modes.filter(a => a.name !== mode.name),
mode
];
},
ON_MODE_REMOVE({ name }: { name: string }) {
this.modes = this.modes.filter(a => a.name !== name);
if (this.modes.length === 0) {
this.modes = [defaultMode];
}
},
ON_TEMPLATE_ADD({ template }: { template: { id: string, html: string }}) {
if (this.templates[template.id]) {
this.warn(`Tried to add duplicate template '${template.id}'`);
} else {
this.templates[template.id] = template.html;
}
},
ON_UPDATE_THEMES({ themes }: { themes: { [key: string]: ThemeData } }) {
this.removeThemes();
this.setThemes(themes);
},
removeThemes() {
for (let i = 0; i < document.styleSheets.length; i++) {
const styleSheet = document.styleSheets[i];
const node = styleSheet.ownerNode as Element;
if (node.getAttribute("data-theme")) {
node.parentNode?.removeChild(node);
}
}
this.tplBackups.reverse();
for (const [elem, oldData] of this.tplBackups) {
elem.innerText = oldData;
}
this.tplBackups = [];
this.msgTplBackups.reverse();
for (const [id, oldData] of this.msgTplBackups) {
this.templates[id] = oldData;
}
this.msgTplBackups = [];
},
setThemes(themes: { [key: string]: ThemeData }) {
for (const [id, data] of Object.entries(themes)) {
if (data.style) {
const style = document.createElement("style");
style.type = "text/css";
style.setAttribute("data-theme", id);
style.appendChild(document.createTextNode(data.style));
document.head.appendChild(style);
}
if (data.styleSheet) {
const link = document.createElement("link");
link.rel = "stylesheet";
link.type = "text/css";
link.href = data.baseUrl + data.styleSheet;
link.setAttribute("data-theme", id);
document.head.appendChild(link);
}
if (data.templates) {
for (const [tplId, tpl] of Object.entries(data.templates)) {
const elem = document.getElementById(tplId);
if (elem) {
this.tplBackups.push([elem, elem.innerText]);
elem.innerText = tpl;
}
}
}
if (data.script) {
const script = document.createElement("script");
script.type = "text/javascript";
script.src = data.baseUrl + data.script;
document.head.appendChild(script);
}
if (data.msgTemplates) {
for (const [tplId, tpl] of Object.entries(data.msgTemplates)) {
this.msgTplBackups.push([tplId, this.templates[tplId]]);
this.templates[tplId] = tpl;
}
}
}
},
warn(msg: string) {
this.messages.push({
args: [msg],
template: "^3<b>CHAT-WARN</b>: ^0{0}"
});
},
clearShowWindowTimer() {
clearTimeout(this.showWindowTimer);
},
resetShowWindowTimer() {
this.clearShowWindowTimer();
this.showWindowTimer = window.setTimeout(() => {
if (this.hideState !== ChatHideStates.AlwaysShow && !this.showInput) {
this.showWindow = false;
}
}, CONFIG.fadeTimeout);
},
keyUp() {
this.resize();
},
keyDown(e: KeyboardEvent) {
if (e.which === 38 || e.which === 40) {
e.preventDefault();
this.moveOldMessageIndex(e.which === 38);
} else if (e.which == 33) {
var buf = document.getElementsByClassName("chat-messages")[0];
buf.scrollTop = buf.scrollTop - 100;
} else if (e.which == 34) {
var buf = document.getElementsByClassName("chat-messages")[0];
buf.scrollTop = buf.scrollTop + 100;
} else if (e.which === 9) { // tab
if (e.shiftKey || e.altKey) {
do {
--this.modeIdx;
if (this.modeIdx < 0) {
this.modeIdx = this.modes.length - 1;
}
} while (this.modes[this.modeIdx].hidden);
} else {
do {
this.modeIdx = (this.modeIdx + 1) % this.modes.length;
} while (this.modes[this.modeIdx].hidden);
}
const buf = document.getElementsByClassName('chat-messages')[0];
setTimeout(() => buf.scrollTop = buf.scrollHeight, 0);
}
this.resize();
},
moveOldMessageIndex(up: boolean) {
if (up && this.oldMessages.length > this.oldMessagesIndex + 1) {
this.oldMessagesIndex += 1;
this.message = this.oldMessages[this.oldMessagesIndex];
} else if (!up && this.oldMessagesIndex - 1 >= 0) {
this.oldMessagesIndex -= 1;
this.message = this.oldMessages[this.oldMessagesIndex];
} else if (!up && this.oldMessagesIndex - 1 === -1) {
this.oldMessagesIndex = -1;
this.message = "";
}
},
resize() {
const input = this.$refs.input as HTMLInputElement;
// scrollHeight includes padding, but content-box excludes padding
// remove padding before setting height on the element
const style = getComputedStyle(input);
const paddingRemove = parseFloat(style.paddingBottom) + parseFloat(style.paddingTop);
input.style.height = "5px";
input.style.height = `${input.scrollHeight - paddingRemove}px`;
},
send() {
if (this.message !== "") {
post(
"http://chat/chatResult",
JSON.stringify({
message: this.message,
mode: this.modes[this.modeIdxGet].name
})
);
this.oldMessages.unshift(this.message);
this.oldMessagesIndex = -1;
this.hideInput();
} else {
this.hideInput(true);
}
},
hideInput(canceled = false) {
setTimeout(() => {
const input = this.$refs.input as HTMLInputElement;
delete input.style.height;
}, 50);
if (canceled) {
post("http://chat/chatResult", JSON.stringify({ canceled }));
}
this.message = "";
this.showInput = false;
clearInterval(this.focusTimer);
if (this.hideState !== ChatHideStates.AlwaysHide) {
this.resetShowWindowTimer();
} else {
this.showWindow = false;
}
}
}
});

View File

@@ -0,0 +1,44 @@
<template>
<div id="app">
<div class="chat-window" :style="this.style" :class="{
'animated': !showWindow && hideAnimated,
'hidden': !showWindow
}">
<div class="chat-messages" ref="messages">
<message v-for="msg in filteredMessages"
:templates="templates"
:multiline="msg.multiline"
:args="msg.args"
:params="msg.params"
:color="msg.color"
:template="msg.template"
:template-id="msg.templateId"
:key="msg.id">
</message>
</div>
</div>
<div class="chat-input">
<div v-show="showInput" class="input">
<span class="prefix" :class="{ any: modes.length > 1 }" :style="{ color: modeColor }">{{modePrefix}}</span>
<textarea v-model="message"
ref="input"
type="text"
autofocus
spellcheck="false"
rows="1"
@keyup.esc="hideInput"
@keyup="keyUp"
@keydown="keyDown"
@keypress.enter.prevent="send">
</textarea>
</div>
<suggestions :message="message" :suggestions="suggestions">
</suggestions>
<div class="chat-hide-state" v-show="showHideState">
{{hideStateString}}
</div>
</div>
</div>
</template>
<script lang="ts" src="./App.ts"></script>

View File

@@ -0,0 +1,102 @@
import CONFIG from './config';
import Vue, { PropType } from 'vue';
export default Vue.component('message', {
data() {
return {};
},
computed: {
textEscaped(): string {
let s = this.template ? this.template : this.templates[this.templateId];
//This hack is required to preserve backwards compatability
if (!this.template && this.templateId == CONFIG.defaultTemplateId
&& this.args.length == 1) {
s = this.templates[CONFIG.defaultAltTemplateId] //Swap out default template :/
}
s = s.replace(`@default`, this.templates[this.templateId]);
s = s.replace(/{(\d+)}/g, (match, number) => {
const argEscaped = this.args[number] != undefined ? this.escape(this.args[number]) : match;
if (number == 0 && this.color) {
//color is deprecated, use templates or ^1 etc.
return this.colorizeOld(argEscaped);
}
return argEscaped;
});
// format variant args
s = s.replace(/\{\{([a-zA-Z0-9_\-]+?)\}\}/g, (match, id) => {
const argEscaped = this.params[id] != undefined ? this.escape(this.params[id]) : match;
return argEscaped;
});
return this.colorize(s);
},
},
methods: {
colorizeOld(str: string): string {
return `<span style="color: rgb(${this.color[0]}, ${this.color[1]}, ${this.color[2]})">${str}</span>`
},
colorize(str: string): string {
let s = "<span>" + colorTrans(str) + "</span>";
const styleDict: {[ key: string ]: string} = {
'*': 'font-weight: bold;',
'_': 'text-decoration: underline;',
'~': 'text-decoration: line-through;',
'=': 'text-decoration: underline line-through;',
'r': 'text-decoration: none;font-weight: normal;',
};
const styleRegex = /\^(\_|\*|\=|\~|\/|r)(.*?)(?=$|\^r|<\/em>)/;
while (s.match(styleRegex)) { //Any better solution would be appreciated :P
s = s.replace(styleRegex, (str, style, inner) => `<em style="${styleDict[style]}">${inner}</em>`)
}
return s.replace(/<span[^>]*><\/span[^>]*>/g, '');
function colorTrans(str: string) {
return str
.replace(/\^([0-9])/g, (str, color) => `</span><span class="color-${color}">`)
.replace(/\^#([0-9A-F]{3,6})/gi, (str, color) => `</span><span class="color" style="color: #${color}">`)
.replace(/~([a-z])~/g, (str, color) => `</span><span class="gameColor-${color}">`);
}
},
escape(unsafe: string): string {
return String(unsafe)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
},
},
props: {
templates: {
type: Object as PropType<{ [key: string]: string }>,
},
args: {
type: Array as PropType<string[]>,
},
params: {
type: Object as PropType<{ [ key: string]: string }>,
},
template: {
type: String,
default: null,
},
templateId: {
type: String,
default: CONFIG.defaultTemplateId,
},
multiline: {
type: Boolean,
default: false,
},
color: { //deprecated
type: Array as PropType<number[]>,
default: null,
},
},
});

View File

@@ -0,0 +1,7 @@
<template>
<div class="msg" :class="{ multiline }">
<span v-html="textEscaped"></span>
</div>
</template>
<script lang="ts" src="./Message.ts"></script>

View File

@@ -0,0 +1,63 @@
import CONFIG from './config';
import Vue, { PropType } from 'vue';
export interface Suggestion {
name: string;
help: string;
params: string[];
disabled: boolean;
}
export default Vue.component('suggestions', {
props: {
message: {
type: String
},
suggestions: {
type: Array as PropType<Suggestion[]>
}
},
data() {
return {};
},
computed: {
currentSuggestions(): Suggestion[] {
if (this.message === '') {
return [];
}
const currentSuggestions = this.suggestions.filter((s) => {
if (!s.name.startsWith(this.message)) {
const suggestionSplitted = s.name.split(' ');
const messageSplitted = this.message.split(' ');
for (let i = 0; i < messageSplitted.length; i += 1) {
if (i >= suggestionSplitted.length) {
return i < suggestionSplitted.length + s.params.length;
}
if (suggestionSplitted[i] !== messageSplitted[i]) {
return false;
}
}
}
return true;
}).slice(0, CONFIG.suggestionLimit);
currentSuggestions.forEach((s) => {
// eslint-disable-next-line no-param-reassign
s.disabled = !s.name.startsWith(this.message);
s.params.forEach((p, index) => {
const wType = (index === s.params.length - 1) ? '.' : '\\S';
const regex = new RegExp(`${s.name} (?:\\w+ ){${index}}(?:${wType}*)$`, 'g');
// eslint-disable-next-line no-param-reassign
// @ts-ignore
p.disabled = this.message.match(regex) == null;
});
});
return currentSuggestions;
},
},
methods: {},
});

View File

@@ -0,0 +1,29 @@
<template>
<div class="suggestions-wrap" v-show="currentSuggestions.length > 0">
<ul class="suggestions">
<li class="suggestion" v-for="s in currentSuggestions" :key="s.name">
<p>
<span :class="{ 'disabled': s.disabled }">
{{s.name}}
</span>
<span class="param"
v-for="p in s.params"
:class="{ 'disabled': p.disabled }"
:key="p.name">
[{{p.name}}]
</span>
</p>
<small class="help">
<template v-if="!s.disabled">
{{s.help}}
</template>
<template v-for="p in s.params" v-if="!p.disabled">
{{p.help}}
</template>
</small>
</li>
</ul>
</div>
</template>
<script lang="ts" src="./Suggestions.ts"></script>

View File

@@ -0,0 +1,17 @@
export default {
defaultTemplateId: 'default', //This is the default template for 2 args1
defaultAltTemplateId: 'defaultAlt', //This one for 1 arg
templates: { //You can add static templates here
'default': '<b>{0}</b>: {1}',
'defaultAlt': '{0}',
'print': '<pre>{0}</pre>',
'example:important': '<h1>^2{0}</h1>'
},
fadeTimeout: 7000,
suggestionLimit: 5,
style: {
background: 'rgba(52, 73, 94, 0.7)',
width: '38vw',
height: '22%',
}
};

View File

@@ -0,0 +1,160 @@
.color-0{color: #ffffff;}
.color-1{color: #ff4444;}
.color-2{color: #99cc00;}
.color-3{color: #ffbb33;}
.color-4{color: #0099cc;}
.color-5{color: #33b5e5;}
.color-6{color: #aa66cc;}
.color-8{color: #cc0000;}
.color-9{color: #cc0068;}
.gameColor-w{color: #ffffff;}
.gameColor-r{color: #ff4444;}
.gameColor-g{color: #99cc00;}
.gameColor-y{color: #ffbb33;}
.gameColor-b{color: #33b5e5;}
/* todo: more game colors */
* {
font-family: 'Lato', sans-serif;
margin: 0;
padding: 0;
}
.no-grow {
flex-grow: 0;
}
em {
font-style: normal;
}
#app {
font-family: 'Lato', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: white;
}
.chat-window {
position: absolute;
top: 1.5%;
left: 0.8%;
width: 38%;
height: 22%;
max-width: 1000px;
background-color: rgba(52, 73, 94, 0.7);
-webkit-animation-duration: 2s;
}
.chat-messages {
position: relative;
height: 95%;
font-size: 1.8vh;
margin: 1%;
overflow-x: hidden;
overflow-y: hidden;
}
.chat-input {
font-size: 1.65vh;
position: absolute;
top: 23.8%;
left: 0.8%;
width: 38%;
max-width: 1000px;
box-sizing: border-box;
}
.chat-input > div.input {
position: relative;
display: flex;
align-items: stretch;
width: 100%;
background-color: rgba(44, 62, 80, 1.0);
}
.chat-hide-state {
text-transform: uppercase;
margin-left: 0.05vw;
font-size: 1.65vh;
}
.prefix {
font-size: 1.8vh;
/*position: absolute;
top: 0%;*/
height: 100%;
vertical-align: middle;
line-height: calc(1vh + 1vh + 1.85vh);
padding-left: 0.5vh;
text-transform: uppercase;
font-weight: bold;
display: inline-block;
}
textarea {
font-size: 1.65vh;
line-height: 1.85vh;
display: block;
box-sizing: content-box;
padding: 1vh;
padding-left: 0.5vh;
color: white;
border-width: 0;
height: 3.15%;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
background-color: transparent;
}
textarea:focus, input:focus {
outline: none;
}
.msg {
margin-bottom: 0.28%;
}
.multiline {
margin-left: 4%;
text-indent: -1.2rem;
white-space: pre-line;
}
.suggestions {
list-style-type: none;
padding: 0.5%;
padding-left: 1.4%;
font-size: 1.65vh;
box-sizing: border-box;
color: white;
background-color: rgba(44, 62, 80, 1.0);
width: 100%;
}
.help {
color: #b0bbbd;
}
.disabled {
color: #b0bbbd;
}
.suggestion {
margin-bottom: 0.5%;
}
.hidden {
opacity: 0;
}
.hidden.animated {
transition: opacity 1s;
}

View File

@@ -0,0 +1,4 @@
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}

View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<link href="/html/vendor/latofonts.css" rel="stylesheet">
<link href="/html/vendor/flexboxgrid.6.3.1.min.css" rel="stylesheet"></link>
<link href="/html/vendor/animate.3.5.2.min.css" rel="stylesheet"></link>
<link href="index.css" rel="stylesheet"></link>
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@@ -0,0 +1,7 @@
import Vue from 'vue';
import App from './App.vue';
const instance = new Vue({
el: '#app',
render: h => h(App),
});

View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"outDir": "./",
"module": "es6",
"strict": true,
"moduleResolution": "node",
"target": "es6",
"allowJs": true,
"lib": [
"es2017",
"dom"
]
},
"include": [
"./**/*"
],
"exclude": []
}

View File

@@ -0,0 +1,31 @@
export function post(url: string, data: any) {
var request = new XMLHttpRequest();
request.open('POST', url, true);
request.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
request.send(data);
}
function emulate(type: string, detail = {}) {
const detailRef = {
type,
...detail
};
window.dispatchEvent(new CustomEvent('message', {
detail: detailRef
}));
}
(window as any)['emulate'] = emulate;
(window as any)['demo'] = () => {
emulate('ON_MESSAGE', {
message: {
args: [ 'me', 'hello!' ]
}
})
emulate('ON_SCREEN_STATE_CHANGE', {
shouldHide: false
});
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,48 @@
/* latin-ext */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 300;
src: local('Lato Light'), local('Lato-Light'), url(fonts/LatoLight.woff2);
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 300;
src: local('Lato Light'), local('Lato-Light'), url(fonts/LatoLight2.woff2);
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}
/* latin-ext */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 400;
src: local('Lato Regular'), local('Lato-Regular'), url(fonts/LatoRegular.woff2);
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 400;
src: local('Lato Regular'), local('Lato-Regular'), url(fonts/LatoRegular2.woff2);
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}
/* latin-ext */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 700;
src: local('Lato Bold'), local('Lato-Bold'), url(fonts/LatoBold.woff2);
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 700;
src: local('Lato Bold'), local('Lato-Bold'), url(fonts/LatoBold2.woff2);
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}

View File

@@ -0,0 +1,24 @@
{
"name": "chat",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"@types/vue": "^2.0.0",
"copy-webpack-plugin": "^5.1.1",
"css-loader": "^3.4.2",
"html-webpack-inline-source-plugin": "^0.0.10",
"html-webpack-plugin": "^3.2.0",
"ts-loader": "^6.2.1",
"typescript": "^3.8.3",
"vue": "^2.6.11",
"vue-loader": "^15.9.0",
"vue-template-compiler": "^2.6.11",
"webpack": "4"
},
"devDependencies": {
"command-line-args": "^5.1.1",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3"
}
}

View File

@@ -0,0 +1,293 @@
RegisterServerEvent('chat:init')
RegisterServerEvent('chat:addTemplate')
RegisterServerEvent('chat:addMessage')
RegisterServerEvent('chat:addSuggestion')
RegisterServerEvent('chat:removeSuggestion')
RegisterServerEvent('_chat:messageEntered')
RegisterServerEvent('chat:clear')
RegisterServerEvent('__cfx_internal:commandFallback')
-- this is a built-in event, but somehow needs to be registered
RegisterNetEvent('playerJoining')
exports('addMessage', function(target, message)
if not message then
message = target
target = -1
end
if not target or not message then return end
TriggerClientEvent('chat:addMessage', target, message)
end)
local hooks = {}
local hookIdx = 1
exports('registerMessageHook', function(hook)
local resource = GetInvokingResource()
hooks[hookIdx + 1] = {
fn = hook,
resource = resource
}
hookIdx = hookIdx + 1
end)
local modes = {}
local function getMatchingPlayers(seObject)
local players = GetPlayers()
local retval = {}
for _, v in ipairs(players) do
if IsPlayerAceAllowed(v, seObject) then
retval[#retval + 1] = v
end
end
return retval
end
exports('registerMode', function(modeData)
if not modeData.name or not modeData.displayName or not modeData.cb then
return false
end
local resource = GetInvokingResource()
modes[modeData.name] = modeData
modes[modeData.name].resource = resource
local clObj = {
name = modeData.name,
displayName = modeData.displayName,
color = modeData.color or '#fff',
isChannel = modeData.isChannel,
isGlobal = modeData.isGlobal,
}
if not modeData.seObject then
TriggerClientEvent('chat:addMode', -1, clObj)
else
for _, v in ipairs(getMatchingPlayers(modeData.seObject)) do
TriggerClientEvent('chat:addMode', v, clObj)
end
end
return true
end)
local function unregisterHooks(resource)
local toRemove = {}
for k, v in pairs(hooks) do
if v.resource == resource then
table.insert(toRemove, k)
end
end
for _, v in ipairs(toRemove) do
hooks[v] = nil
end
toRemove = {}
for k, v in pairs(modes) do
if v.resource == resource then
table.insert(toRemove, k)
end
end
for _, v in ipairs(toRemove) do
TriggerClientEvent('chat:removeMode', -1, {
name = v
})
modes[v] = nil
end
end
local function routeMessage(source, author, message, mode, fromConsole)
if source >= 1 then
author = GetPlayerName(source)
end
local outMessage = {
color = { 255, 255, 255 },
multiline = true,
args = { message },
mode = mode
}
if author ~= "" then
outMessage.args = { author, message }
end
if mode and modes[mode] then
local modeData = modes[mode]
if modeData.seObject and not IsPlayerAceAllowed(source, modeData.seObject) then
return
end
end
local messageCanceled = false
local routingTarget = -1
local hookRef = {
updateMessage = function(t)
-- shallow merge
for k, v in pairs(t) do
if k == 'template' then
outMessage['template'] = v:gsub('%{%}', outMessage['template'] or '@default')
elseif k == 'params' then
if not outMessage.params then
outMessage.params = {}
end
for pk, pv in pairs(v) do
outMessage.params[pk] = pv
end
else
outMessage[k] = v
end
end
end,
cancel = function()
messageCanceled = true
end,
setSeObject = function(object)
routingTarget = getMatchingPlayers(object)
end,
setRouting = function(target)
routingTarget = target
end
}
for _, hook in pairs(hooks) do
if hook.fn then
hook.fn(source, outMessage, hookRef)
end
end
if modes[mode] then
local m = modes[mode]
m.cb(source, outMessage, hookRef)
end
if messageCanceled then
return
end
TriggerEvent('chatMessage', source, #outMessage.args > 1 and outMessage.args[1] or '', outMessage.args[#outMessage.args])
if not WasEventCanceled() then
if type(routingTarget) ~= 'table' then
TriggerClientEvent('chat:addMessage', routingTarget, outMessage)
else
for _, id in ipairs(routingTarget) do
TriggerClientEvent('chat:addMessage', id, outMessage)
end
end
end
if not fromConsole then
print(author .. '^7' .. (modes[mode] and (' (' .. modes[mode].displayName .. ')') or '') .. ': ' .. message .. '^7')
end
end
AddEventHandler('_chat:messageEntered', function(author, color, message, mode)
if not message or not author then
return
end
local source = source
routeMessage(source, author, message, mode)
end)
AddEventHandler('__cfx_internal:commandFallback', function(command)
local name = GetPlayerName(source)
-- route the message as if it were a /command
routeMessage(source, name, '/' .. command, nil, true)
CancelEvent()
end)
-- player join messages
AddEventHandler('playerJoining', function()
if GetConvarInt('chat_showJoins', 1) == 0 then
return
end
TriggerClientEvent('chatMessage', -1, '', { 255, 255, 255 }, '^2* ' .. GetPlayerName(source) .. ' joined.')
end)
AddEventHandler('playerDropped', function(reason)
if GetConvarInt('chat_showQuits', 1) == 0 then
return
end
TriggerClientEvent('chatMessage', -1, '', { 255, 255, 255 }, '^2* ' .. GetPlayerName(source) ..' left (' .. reason .. ')')
end)
RegisterCommand('say', function(source, args, rawCommand)
routeMessage(source, (source == 0) and 'console' or GetPlayerName(source), rawCommand:sub(5), nil, true)
end)
-- command suggestions for clients
local function refreshCommands(player)
if GetRegisteredCommands then
local registeredCommands = GetRegisteredCommands()
local suggestions = {}
for _, command in ipairs(registeredCommands) do
if IsPlayerAceAllowed(player, ('command.%s'):format(command.name)) then
table.insert(suggestions, {
name = '/' .. command.name,
help = ''
})
end
end
TriggerClientEvent('chat:addSuggestions', player, suggestions)
end
end
AddEventHandler('chat:init', function()
local source = source
refreshCommands(source)
for _, modeData in pairs(modes) do
local clObj = {
name = modeData.name,
displayName = modeData.displayName,
color = modeData.color or '#fff',
isChannel = modeData.isChannel,
isGlobal = modeData.isGlobal,
}
if not modeData.seObject or IsPlayerAceAllowed(source, modeData.seObject) then
TriggerClientEvent('chat:addMode', source, clObj)
end
end
end)
AddEventHandler('onServerResourceStart', function(resName)
Wait(500)
for _, player in ipairs(GetPlayers()) do
refreshCommands(player)
end
end)
AddEventHandler('onResourceStop', function(resName)
unregisterHooks(resName)
end)

View File

@@ -0,0 +1,45 @@
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const CopyPlugin = require('copy-webpack-plugin');
module.exports = {
mode: 'production',
entry: './html/main.ts',
module: {
rules: [
{
test: /\.ts$/,
loader: 'ts-loader',
exclude: /node_modules/,
options: {
appendTsSuffixTo: [/\.vue$/],
}
},
{
test: /\.vue$/,
loader: 'vue-loader'
},
]
},
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
inlineSource: '.(js|css)$',
template: './html/index.html',
filename: 'ui.html'
}),
new HtmlWebpackInlineSourcePlugin(),
new CopyPlugin([
{ from: 'html/index.css', to: 'index.css' }
]),
],
resolve: {
extensions: [ '.ts', '.js' ]
},
output: {
filename: 'chat.js',
path: __dirname + '/dist/'
},
//devtool: 'inline-source-map'
};

File diff suppressed because it is too large Load Diff

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

View File

View File

@@ -0,0 +1,30 @@
-- 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 flexible handler for game type/map association.'
repository 'https://github.com/citizenfx/cfx-server-data'
client_scripts {
"mapmanager_shared.lua",
"mapmanager_client.lua"
}
server_scripts {
"mapmanager_shared.lua",
"mapmanager_server.lua"
}
fx_version 'adamant'
games { 'gta5', 'rdr3' }
server_export "getCurrentGameType"
server_export "getCurrentMap"
server_export "changeGameType"
server_export "changeMap"
server_export "doesMapSupportGameType"
server_export "getMaps"
server_export "roundEnded"
rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.'

View File

@@ -0,0 +1,108 @@
local maps = {}
local gametypes = {}
AddEventHandler('onClientResourceStart', function(res)
-- parse metadata for this resource
-- map files
local num = GetNumResourceMetadata(res, 'map')
if num > 0 then
for i = 0, num-1 do
local file = GetResourceMetadata(res, 'map', i)
if file then
addMap(file, res)
end
end
end
-- resource type data
local type = GetResourceMetadata(res, 'resource_type', 0)
if type then
local extraData = GetResourceMetadata(res, 'resource_type_extra', 0)
if extraData then
extraData = json.decode(extraData)
else
extraData = {}
end
if type == 'map' then
maps[res] = extraData
elseif type == 'gametype' then
gametypes[res] = extraData
end
end
-- handle starting
loadMap(res)
-- defer this to the next game tick to work around a lack of dependencies
Citizen.CreateThread(function()
Citizen.Wait(15)
if maps[res] then
TriggerEvent('onClientMapStart', res)
elseif gametypes[res] then
TriggerEvent('onClientGameTypeStart', res)
end
end)
end)
AddEventHandler('onResourceStop', function(res)
if maps[res] then
TriggerEvent('onClientMapStop', res)
elseif gametypes[res] then
TriggerEvent('onClientGameTypeStop', res)
end
unloadMap(res)
end)
AddEventHandler('getMapDirectives', function(add)
if not CreateScriptVehicleGenerator then
return
end
add('vehicle_generator', function(state, name)
return function(opts)
local x, y, z, heading
local color1, color2
if opts.x then
x = opts.x
y = opts.y
z = opts.z
else
x = opts[1]
y = opts[2]
z = opts[3]
end
heading = opts.heading or 1.0
color1 = opts.color1 or -1
color2 = opts.color2 or -1
CreateThread(function()
local hash = GetHashKey(name)
RequestModel(hash)
while not HasModelLoaded(hash) do
Wait(0)
end
local carGen = CreateScriptVehicleGenerator(x, y, z, heading, 5.0, 3.0, hash, color1, color2, -1, -1, true, false, false, true, true, -1)
SetScriptVehicleGenerator(carGen, true)
SetAllVehicleGeneratorsActive(true)
state.add('cargen', carGen)
end)
end
end, function(state, arg)
Citizen.Trace("deleting car gen " .. tostring(state.cargen) .. "\n")
DeleteScriptVehicleGenerator(state.cargen)
end)
end)

View File

@@ -0,0 +1,331 @@
-- loosely based on MTA's https://code.google.com/p/mtasa-resources/source/browse/trunk/%5Bmanagers%5D/mapmanager/mapmanager_main.lua
local maps = {}
local gametypes = {}
local function refreshResources()
local numResources = GetNumResources()
for i = 0, numResources - 1 do
local resource = GetResourceByFindIndex(i)
if GetNumResourceMetadata(resource, 'resource_type') > 0 then
local type = GetResourceMetadata(resource, 'resource_type', 0)
local params = json.decode(GetResourceMetadata(resource, 'resource_type_extra', 0))
local valid = false
local games = GetNumResourceMetadata(resource, 'game')
if games > 0 then
for j = 0, games - 1 do
local game = GetResourceMetadata(resource, 'game', j)
if game == GetConvar('gamename', 'gta5') or game == 'common' then
valid = true
end
end
end
if valid then
if type == 'map' then
maps[resource] = params
elseif type == 'gametype' then
gametypes[resource] = params
end
end
end
end
end
AddEventHandler('onResourceListRefresh', function()
refreshResources()
end)
refreshResources()
AddEventHandler('onResourceStarting', function(resource)
local num = GetNumResourceMetadata(resource, 'map')
if num then
for i = 0, num-1 do
local file = GetResourceMetadata(resource, 'map', i)
if file then
addMap(file, resource)
end
end
end
if maps[resource] then
if getCurrentMap() and getCurrentMap() ~= resource then
if doesMapSupportGameType(getCurrentGameType(), resource) then
print("Changing map from " .. getCurrentMap() .. " to " .. resource)
changeMap(resource)
else
-- check if there's only one possible game type for the map
local map = maps[resource]
local count = 0
local gt
for type, flag in pairs(map.gameTypes) do
if flag then
count = count + 1
gt = type
end
end
if count == 1 then
print("Changing map from " .. getCurrentMap() .. " to " .. resource .. " (gt " .. gt .. ")")
changeGameType(gt)
changeMap(resource)
end
end
CancelEvent()
end
elseif gametypes[resource] then
if getCurrentGameType() and getCurrentGameType() ~= resource then
print("Changing gametype from " .. getCurrentGameType() .. " to " .. resource)
changeGameType(resource)
CancelEvent()
end
end
end)
math.randomseed(GetInstanceId())
local currentGameType = nil
local currentMap = nil
AddEventHandler('onResourceStart', function(resource)
if maps[resource] then
if not getCurrentGameType() then
for gt, _ in pairs(maps[resource].gameTypes) do
changeGameType(gt)
break
end
end
if getCurrentGameType() and not getCurrentMap() then
if doesMapSupportGameType(currentGameType, resource) then
if TriggerEvent('onMapStart', resource, maps[resource]) then
if maps[resource].name then
print('Started map ' .. maps[resource].name)
SetMapName(maps[resource].name)
else
print('Started map ' .. resource)
SetMapName(resource)
end
currentMap = resource
else
currentMap = nil
end
end
end
elseif gametypes[resource] then
if not getCurrentGameType() then
if TriggerEvent('onGameTypeStart', resource, gametypes[resource]) then
currentGameType = resource
local gtName = gametypes[resource].name or resource
SetGameType(gtName)
print('Started gametype ' .. gtName)
SetTimeout(50, function()
if not currentMap then
local possibleMaps = {}
for map, data in pairs(maps) do
if data.gameTypes[currentGameType] then
table.insert(possibleMaps, map)
end
end
if #possibleMaps > 0 then
local rnd = math.random(#possibleMaps)
changeMap(possibleMaps[rnd])
end
end
end)
else
currentGameType = nil
end
end
end
-- handle starting
loadMap(resource)
end)
local function handleRoundEnd()
local possibleMaps = {}
for map, data in pairs(maps) do
if data.gameTypes[currentGameType] then
table.insert(possibleMaps, map)
end
end
if #possibleMaps > 1 then
local mapname = currentMap
while mapname == currentMap do
local rnd = math.random(#possibleMaps)
mapname = possibleMaps[rnd]
end
changeMap(mapname)
elseif #possibleMaps > 0 then
local rnd = math.random(#possibleMaps)
changeMap(possibleMaps[rnd])
end
end
AddEventHandler('mapmanager:roundEnded', function()
-- set a timeout as we don't want to return to a dead environment
SetTimeout(50, handleRoundEnd) -- not a closure as to work around some issue in neolua?
end)
function roundEnded()
SetTimeout(50, handleRoundEnd)
end
AddEventHandler('onResourceStop', function(resource)
if resource == currentGameType then
TriggerEvent('onGameTypeStop', resource)
currentGameType = nil
if currentMap then
StopResource(currentMap)
end
elseif resource == currentMap then
TriggerEvent('onMapStop', resource)
currentMap = nil
end
-- unload the map
unloadMap(resource)
end)
AddEventHandler('rconCommand', function(commandName, args)
if commandName == 'map' then
if #args ~= 1 then
RconPrint("usage: map [mapname]\n")
end
if not maps[args[1]] then
RconPrint('no such map ' .. args[1] .. "\n")
CancelEvent()
return
end
if currentGameType == nil or not doesMapSupportGameType(currentGameType, args[1]) then
local map = maps[args[1]]
local count = 0
local gt
for type, flag in pairs(map.gameTypes) do
if flag then
count = count + 1
gt = type
end
end
if count == 1 then
print("Changing map from " .. getCurrentMap() .. " to " .. args[1] .. " (gt " .. gt .. ")")
changeGameType(gt)
changeMap(args[1])
RconPrint('map ' .. args[1] .. "\n")
else
RconPrint('map ' .. args[1] .. ' does not support ' .. currentGameType .. "\n")
end
CancelEvent()
return
end
changeMap(args[1])
RconPrint('map ' .. args[1] .. "\n")
CancelEvent()
elseif commandName == 'gametype' then
if #args ~= 1 then
RconPrint("usage: gametype [name]\n")
end
if not gametypes[args[1]] then
RconPrint('no such gametype ' .. args[1] .. "\n")
CancelEvent()
return
end
changeGameType(args[1])
RconPrint('gametype ' .. args[1] .. "\n")
CancelEvent()
end
end)
function getCurrentGameType()
return currentGameType
end
function getCurrentMap()
return currentMap
end
function getMaps()
return maps
end
function changeGameType(gameType)
if currentMap and not doesMapSupportGameType(gameType, currentMap) then
StopResource(currentMap)
end
if currentGameType then
StopResource(currentGameType)
end
StartResource(gameType)
end
function changeMap(map)
if currentMap then
StopResource(currentMap)
end
StartResource(map)
end
function doesMapSupportGameType(gameType, map)
if not gametypes[gameType] then
return false
end
if not maps[map] then
return false
end
if not maps[map].gameTypes then
return true
end
return maps[map].gameTypes[gameType]
end

View File

@@ -0,0 +1,87 @@
-- shared logic file for map manager - don't call any subsystem-specific functions here
mapFiles = {}
function addMap(file, owningResource)
if not mapFiles[owningResource] then
mapFiles[owningResource] = {}
end
table.insert(mapFiles[owningResource], file)
end
undoCallbacks = {}
function loadMap(res)
if mapFiles[res] then
for _, file in ipairs(mapFiles[res]) do
parseMap(file, res)
end
end
end
function unloadMap(res)
if undoCallbacks[res] then
for _, cb in ipairs(undoCallbacks[res]) do
cb()
end
undoCallbacks[res] = nil
mapFiles[res] = nil
end
end
function parseMap(file, owningResource)
if not undoCallbacks[owningResource] then
undoCallbacks[owningResource] = {}
end
local env = {
math = math, pairs = pairs, ipairs = ipairs, next = next, tonumber = tonumber, tostring = tostring,
type = type, table = table, string = string, _G = env,
vector3 = vector3, quat = quat, vec = vec, vector2 = vector2
}
TriggerEvent('getMapDirectives', function(key, cb, undocb)
env[key] = function(...)
local state = {}
state.add = function(k, v)
state[k] = v
end
local result = cb(state, ...)
local args = table.pack(...)
table.insert(undoCallbacks[owningResource], function()
undocb(state)
end)
return result
end
end)
local mt = {
__index = function(t, k)
if rawget(t, k) ~= nil then return rawget(t, k) end
-- as we're not going to return nothing here (to allow unknown directives to be ignored)
local f = function()
return f
end
return function() return f end
end
}
setmetatable(env, mt)
local fileData = LoadResourceFile(owningResource, file)
local mapFunction, err = load(fileData, file, 't', env)
if not mapFunction then
Citizen.Trace("Couldn't load map " .. file .. ": " .. err .. " (type of fileData: " .. type(fileData) .. ")\n")
return
end
mapFunction()
end

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'
author 'Cfx.re <root@cfx.re>'
description 'Handles spawning a player in a unified fashion to prevent resources from having to implement custom spawn logic.'
repository 'https://github.com/citizenfx/cfx-server-data'
client_script 'spawnmanager.lua'
fx_version 'adamant'
games { 'rdr3', 'gta5' }
rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.'

View File

@@ -0,0 +1,386 @@
-- in-memory spawnpoint array for this script execution instance
local spawnPoints = {}
-- auto-spawn enabled flag
local autoSpawnEnabled = false
local autoSpawnCallback
-- support for mapmanager maps
AddEventHandler('getMapDirectives', function(add)
-- call the remote callback
add('spawnpoint', function(state, model)
-- return another callback to pass coordinates and so on (as such syntax would be [spawnpoint 'model' { options/coords }])
return function(opts)
local x, y, z, heading
local s, e = pcall(function()
-- is this a map or an array?
if opts.x then
x = opts.x
y = opts.y
z = opts.z
else
x = opts[1]
y = opts[2]
z = opts[3]
end
x = x + 0.0001
y = y + 0.0001
z = z + 0.0001
-- get a heading and force it to a float, or just default to null
heading = opts.heading and (opts.heading + 0.01) or 0
-- add the spawnpoint
addSpawnPoint({
x = x, y = y, z = z,
heading = heading,
model = model
})
-- recalculate the model for storage
if not tonumber(model) then
model = GetHashKey(model, _r)
end
-- store the spawn data in the state so we can erase it later on
state.add('xyz', { x, y, z })
state.add('model', model)
end)
if not s then
Citizen.Trace(e .. "\n")
end
end
-- delete callback follows on the next line
end, function(state, arg)
-- loop through all spawn points to find one with our state
for i, sp in ipairs(spawnPoints) do
-- if it matches...
if sp.x == state.xyz[1] and sp.y == state.xyz[2] and sp.z == state.xyz[3] and sp.model == state.model then
-- remove it.
table.remove(spawnPoints, i)
return
end
end
end)
end)
-- loads a set of spawn points from a JSON string
function loadSpawns(spawnString)
-- decode the JSON string
local data = json.decode(spawnString)
-- do we have a 'spawns' field?
if not data.spawns then
error("no 'spawns' in JSON data")
end
-- loop through the spawns
for i, spawn in ipairs(data.spawns) do
-- and add it to the list (validating as we go)
addSpawnPoint(spawn)
end
end
local spawnNum = 1
function addSpawnPoint(spawn)
-- validate the spawn (position)
if not tonumber(spawn.x) or not tonumber(spawn.y) or not tonumber(spawn.z) then
error("invalid spawn position")
end
-- heading
if not tonumber(spawn.heading) then
error("invalid spawn heading")
end
-- model (try integer first, if not, hash it)
local model = spawn.model
if not tonumber(spawn.model) then
model = GetHashKey(spawn.model)
end
-- is the model actually a model?
if not IsModelInCdimage(model) then
error("invalid spawn model")
end
-- is is even a ped?
-- not in V?
--[[if not IsThisModelAPed(model) then
error("this model ain't a ped!")
end]]
-- overwrite the model in case we hashed it
spawn.model = model
-- add an index
spawn.idx = spawnNum
spawnNum = spawnNum + 1
-- all OK, add the spawn entry to the list
table.insert(spawnPoints, spawn)
return spawn.idx
end
-- removes a spawn point
function removeSpawnPoint(spawn)
for i = 1, #spawnPoints do
if spawnPoints[i].idx == spawn then
table.remove(spawnPoints, i)
return
end
end
end
-- changes the auto-spawn flag
function setAutoSpawn(enabled)
autoSpawnEnabled = enabled
end
-- sets a callback to execute instead of 'native' spawning when trying to auto-spawn
function setAutoSpawnCallback(cb)
autoSpawnCallback = cb
autoSpawnEnabled = true
end
-- function as existing in original R* scripts
local function freezePlayer(id, freeze)
local player = id
SetPlayerControl(player, not freeze, false)
local ped = GetPlayerPed(player)
if not freeze then
if not IsEntityVisible(ped) then
SetEntityVisible(ped, true)
end
if not IsPedInAnyVehicle(ped) then
SetEntityCollision(ped, true)
end
FreezeEntityPosition(ped, false)
--SetCharNeverTargetted(ped, false)
SetPlayerInvincible(player, false)
else
if IsEntityVisible(ped) then
SetEntityVisible(ped, false)
end
SetEntityCollision(ped, false)
FreezeEntityPosition(ped, true)
--SetCharNeverTargetted(ped, true)
SetPlayerInvincible(player, true)
--RemovePtfxFromPed(ped)
if not IsPedFatallyInjured(ped) then
ClearPedTasksImmediately(ped)
end
end
end
function loadScene(x, y, z)
if not NewLoadSceneStart then
return
end
NewLoadSceneStart(x, y, z, 0.0, 0.0, 0.0, 20.0, 0)
while IsNewLoadSceneActive() do
networkTimer = GetNetworkTimer()
NetworkUpdateLoadScene()
end
end
-- to prevent trying to spawn multiple times
local spawnLock = false
-- spawns the current player at a certain spawn point index (or a random one, for that matter)
function spawnPlayer(spawnIdx, cb)
if spawnLock then
return
end
spawnLock = true
Citizen.CreateThread(function()
-- if the spawn isn't set, select a random one
if not spawnIdx then
spawnIdx = GetRandomIntInRange(1, #spawnPoints + 1)
end
-- get the spawn from the array
local spawn
if type(spawnIdx) == 'table' then
spawn = spawnIdx
-- prevent errors when passing spawn table
spawn.x = spawn.x + 0.00
spawn.y = spawn.y + 0.00
spawn.z = spawn.z + 0.00
spawn.heading = spawn.heading and (spawn.heading + 0.00) or 0
else
spawn = spawnPoints[spawnIdx]
end
if not spawn.skipFade then
DoScreenFadeOut(500)
while not IsScreenFadedOut() do
Citizen.Wait(0)
end
end
-- validate the index
if not spawn then
Citizen.Trace("tried to spawn at an invalid spawn index\n")
spawnLock = false
return
end
-- freeze the local player
freezePlayer(PlayerId(), true)
-- if the spawn has a model set
if spawn.model then
RequestModel(spawn.model)
-- load the model for this spawn
while not HasModelLoaded(spawn.model) do
RequestModel(spawn.model)
Wait(0)
end
-- change the player model
SetPlayerModel(PlayerId(), spawn.model)
-- release the player model
SetModelAsNoLongerNeeded(spawn.model)
-- RDR3 player model bits
if N_0x283978a15512b2fe then
N_0x283978a15512b2fe(PlayerPedId(), true)
end
end
-- preload collisions for the spawnpoint
RequestCollisionAtCoord(spawn.x, spawn.y, spawn.z)
-- spawn the player
local ped = PlayerPedId()
-- V requires setting coords as well
SetEntityCoordsNoOffset(ped, spawn.x, spawn.y, spawn.z, false, false, false, true)
NetworkResurrectLocalPlayer(spawn.x, spawn.y, spawn.z, spawn.heading, true, true, false)
-- gamelogic-style cleanup stuff
ClearPedTasksImmediately(ped)
--SetEntityHealth(ped, 300) -- TODO: allow configuration of this?
RemoveAllPedWeapons(ped) -- TODO: make configurable (V behavior?)
ClearPlayerWantedLevel(PlayerId())
-- why is this even a flag?
--SetCharWillFlyThroughWindscreen(ped, false)
-- set primary camera heading
--SetGameCamHeading(spawn.heading)
--CamRestoreJumpcut(GetGameCam())
-- load the scene; streaming expects us to do it
--ForceLoadingScreen(true)
--loadScene(spawn.x, spawn.y, spawn.z)
--ForceLoadingScreen(false)
local time = GetGameTimer()
while (not HasCollisionLoadedAroundEntity(ped) and (GetGameTimer() - time) < 5000) do
Citizen.Wait(0)
end
ShutdownLoadingScreen()
if IsScreenFadedOut() then
DoScreenFadeIn(500)
while not IsScreenFadedIn() do
Citizen.Wait(0)
end
end
-- and unfreeze the player
freezePlayer(PlayerId(), false)
TriggerEvent('playerSpawned', spawn)
if cb then
cb(spawn)
end
spawnLock = false
end)
end
-- automatic spawning monitor thread, too
local respawnForced
local diedAt
Citizen.CreateThread(function()
-- main loop thing
while true do
Citizen.Wait(50)
local playerPed = PlayerPedId()
if playerPed and playerPed ~= -1 then
-- check if we want to autospawn
if autoSpawnEnabled then
if NetworkIsPlayerActive(PlayerId()) then
if (diedAt and (math.abs(GetTimeDifference(GetGameTimer(), diedAt)) > 2000)) or respawnForced then
if autoSpawnCallback then
autoSpawnCallback()
else
spawnPlayer()
end
respawnForced = false
end
end
end
if IsEntityDead(playerPed) then
if not diedAt then
diedAt = GetGameTimer()
end
else
diedAt = nil
end
end
end
end)
function forceRespawn()
spawnLock = false
respawnForced = true
end
exports('spawnPlayer', spawnPlayer)
exports('addSpawnPoint', addSpawnPoint)
exports('removeSpawnPoint', removeSpawnPoint)
exports('loadSpawns', loadSpawns)
exports('setAutoSpawn', setAutoSpawn)
exports('setAutoSpawnCallback', setAutoSpawnCallback)
exports('forceRespawn', forceRespawn)

View File

@@ -0,0 +1,2 @@
.yarn.installed
node_modules/

View File

@@ -0,0 +1,13 @@
-- 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 'Builds resources with webpack. To learn more: https://webpack.js.org'
repository 'https://github.com/citizenfx/cfx-server-data'
dependency 'yarn'
server_script 'webpack_builder.js'
fx_version 'adamant'
game 'common'

View File

@@ -0,0 +1,16 @@
{
"name": "webpack-builder",
"version": "1.0.1",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"async": "^3.1.0",
"webpack": "^4.41.2",
"worker-farm": "^1.7.0"
}
}

View File

@@ -0,0 +1,173 @@
const fs = require('fs');
const path = require('path');
const workerFarm = require('worker-farm');
const async = require('async');
let buildingInProgress = false;
let currentBuildingModule = '';
// some modules will not like the custom stack trace logic
const ops = Error.prepareStackTrace;
Error.prepareStackTrace = undefined;
const webpackBuildTask = {
shouldBuild(resourceName) {
const numMetaData = GetNumResourceMetadata(resourceName, 'webpack_config');
if (numMetaData > 0) {
for (let i = 0; i < numMetaData; i++) {
const configName = GetResourceMetadata(resourceName, 'webpack_config');
if (shouldBuild(configName)) {
return true;
}
}
}
return false;
function loadCache(config) {
const cachePath = `cache/${resourceName}/${config.replace(/\//g, '_')}.json`;
try {
return JSON.parse(fs.readFileSync(cachePath, {encoding: 'utf8'}));
} catch {
return null;
}
}
function shouldBuild(config) {
const cache = loadCache(config);
if (!cache) {
return true;
}
for (const file of cache) {
const stats = getStat(file.name);
if (!stats ||
stats.mtime !== file.stats.mtime ||
stats.size !== file.stats.size ||
stats.inode !== file.stats.inode) {
return true;
}
}
return false;
}
function getStat(path) {
try {
const stat = fs.statSync(path);
return stat ? {
mtime: stat.mtimeMs,
size: stat.size,
inode: stat.ino,
} : null;
} catch {
return null;
}
}
},
build(resourceName, cb) {
let buildWebpack = async () => {
let error = null;
const configs = [];
const promises = [];
const numMetaData = GetNumResourceMetadata(resourceName, 'webpack_config');
for (let i = 0; i < numMetaData; i++) {
configs.push(GetResourceMetadata(resourceName, 'webpack_config', i));
}
for (const configName of configs) {
const configPath = GetResourcePath(resourceName) + '/' + configName;
const cachePath = `cache/${resourceName}/${configName.replace(/\//g, '_')}.json`;
try {
fs.mkdirSync(path.dirname(cachePath));
} catch {
}
const config = require(configPath);
const workers = workerFarm(require.resolve('./webpack_runner'));
if (config) {
const resourcePath = path.resolve(GetResourcePath(resourceName));
while (buildingInProgress) {
console.log(`webpack is busy: we are waiting to compile ${resourceName} (${configName})`);
await sleep(3000);
}
console.log(`${resourceName}: started building ${configName}`);
buildingInProgress = true;
currentBuildingModule = resourceName;
promises.push(new Promise((resolve, reject) => {
workers({
configPath,
resourcePath,
cachePath
}, (err, outp) => {
workerFarm.end(workers);
if (err) {
console.error(err.stack || err);
if (err.details) {
console.error(err.details);
}
buildingInProgress = false;
currentBuildingModule = '';
currentBuildingScript = '';
reject("worker farm webpack errored out");
return;
}
if (outp.errors) {
for (const error of outp.errors) {
console.log(error);
}
buildingInProgress = false;
currentBuildingModule = '';
currentBuildingScript = '';
reject("webpack got an error");
return;
}
console.log(`${resourceName}: built ${configName}`);
buildingInProgress = false;
resolve();
});
}));
}
}
try {
await Promise.all(promises);
} catch (e) {
error = e.toString();
}
buildingInProgress = false;
currentBuildingModule = '';
if (error) {
cb(false, error);
} else cb(true);
};
buildWebpack().then();
}
};
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
RegisterResourceBuildTaskFactory('z_webpack', () => webpackBuildTask);

View File

@@ -0,0 +1,75 @@
const webpack = require('webpack');
const path = require('path');
const fs = require('fs');
function getStat(path) {
try {
const stat = fs.statSync(path);
return stat ? {
mtime: stat.mtimeMs,
size: stat.size,
inode: stat.ino,
} : null;
} catch {
return null;
}
}
class SaveStatePlugin {
constructor(inp) {
this.cache = [];
this.cachePath = inp.cachePath;
}
apply(compiler) {
compiler.hooks.afterCompile.tap('SaveStatePlugin', (compilation) => {
for (const file of compilation.fileDependencies) {
this.cache.push({
name: file,
stats: getStat(file)
});
}
});
compiler.hooks.done.tap('SaveStatePlugin', (stats) => {
if (stats.hasErrors()) {
return;
}
fs.writeFile(this.cachePath, JSON.stringify(this.cache), () => {
});
});
}
}
module.exports = (inp, callback) => {
const config = require(inp.configPath);
config.context = inp.resourcePath;
if (config.output && config.output.path) {
config.output.path = path.resolve(inp.resourcePath, config.output.path);
}
if (!config.plugins) {
config.plugins = [];
}
config.plugins.push(new SaveStatePlugin(inp));
webpack(config, (err, stats) => {
if (err) {
callback(err);
return;
}
if (stats.hasErrors()) {
callback(null, stats.toJson());
return;
}
callback(null, {});
});
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
-- 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 'Builds resources with yarn. To learn more: https://classic.yarnpkg.com'
repository 'https://github.com/citizenfx/cfx-server-data'
fx_version 'adamant'
game 'common'
server_script 'yarn_builder.js'

View File

@@ -0,0 +1,81 @@
const path = require('path');
const fs = require('fs');
const child_process = require('child_process');
let buildingInProgress = false;
let currentBuildingModule = '';
const initCwd = process.cwd();
const trimOutput = (data) => {
return `[yarn]\t` + data.toString().replace(/\s+$/, '');
}
const yarnBuildTask = {
shouldBuild(resourceName) {
try {
const resourcePath = GetResourcePath(resourceName);
const packageJson = path.resolve(resourcePath, 'package.json');
const yarnLock = path.resolve(resourcePath, '.yarn.installed');
const packageStat = fs.statSync(packageJson);
try {
const yarnStat = fs.statSync(yarnLock);
if (packageStat.mtimeMs > yarnStat.mtimeMs) {
return true;
}
} catch (e) {
// no yarn.installed, but package.json - install time!
return true;
}
} catch (e) {
}
return false;
},
build(resourceName, cb) {
(async () => {
while (buildingInProgress && currentBuildingModule !== resourceName) {
console.log(`yarn is currently busy: we are waiting to compile ${resourceName}`);
await sleep(3000);
}
buildingInProgress = true;
currentBuildingModule = resourceName;
const proc = child_process.fork(
require.resolve('./yarn_cli.js'),
['install', '--ignore-scripts', '--cache-folder', path.join(initCwd, 'cache', 'yarn-cache'), '--mutex', 'file:' + path.join(initCwd, 'cache', 'yarn-mutex')],
{
cwd: path.resolve(GetResourcePath(resourceName)),
stdio: 'pipe',
});
proc.stdout.on('data', (data) => console.log(trimOutput(data)));
proc.stderr.on('data', (data) => console.error(trimOutput(data)));
proc.on('exit', (code, signal) => {
setImmediate(() => {
if (code != 0 || signal) {
buildingInProgress = false;
currentBuildingModule = '';
cb(false, 'yarn failed!');
return;
}
const resourcePath = GetResourcePath(resourceName);
const yarnLock = path.resolve(resourcePath, '.yarn.installed');
fs.writeFileSync(yarnLock, '');
buildingInProgress = false;
currentBuildingModule = '';
cb(true);
});
});
})();
}
};
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
RegisterResourceBuildTaskFactory('yarn', () => yarnBuildTask);

Some files were not shown because too many files have changed in this diff Show More