@thigasdevelopment/luam 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +616 -0
  3. package/lua/async.lua +119 -0
  4. package/lua/class.lua +178 -0
  5. package/lua/development-logs-client.lua +13 -0
  6. package/lua/development-logs-server.lua +44 -0
  7. package/lua/dotenv.lua +160 -0
  8. package/lua/env.lua +18 -0
  9. package/lua/math.lua +24 -0
  10. package/lua/string.lua +82 -0
  11. package/lua/table.lua +63 -0
  12. package/lua/threads.lua +274 -0
  13. package/luam.mjs +9910 -0
  14. package/package.json +38 -0
  15. package/template/README.md +120 -0
  16. package/template/config.lua +14 -0
  17. package/template/env +11 -0
  18. package/template/framework/bootstrap-client.luam +9 -0
  19. package/template/framework/bootstrap-server.luam +9 -0
  20. package/template/framework/command.luam +54 -0
  21. package/template/framework/core.luam +58 -0
  22. package/template/framework/event.luam +39 -0
  23. package/template/framework/listener.luam +40 -0
  24. package/template/framework/loader.luam +73 -0
  25. package/template/framework/thread-pool.luam +27 -0
  26. package/template/gitignore +7 -0
  27. package/template/luam.json +11 -0
  28. package/template/src/client/commands/ping-command.luam +8 -0
  29. package/template/src/client/main.luam +5 -0
  30. package/template/src/server/commands/hello-command.luam +9 -0
  31. package/template/src/server/handlers/player-join-listener.luam +9 -0
  32. package/template/src/server/main.luam +11 -0
  33. package/template/src/shared/config.luam +5 -0
  34. package/template/src/shared/framework/command.luam +54 -0
  35. package/template/src/shared/framework/core.luam +58 -0
  36. package/template/src/shared/framework/event.luam +39 -0
  37. package/template/src/shared/framework/listener.luam +40 -0
  38. package/template/src/shared/framework/loader.luam +73 -0
  39. package/template/src/shared/framework/thread-pool.luam +27 -0
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@thigasdevelopment/luam",
3
+ "version": "0.1.1",
4
+ "description": "The Luam compiler for Multi Theft Auto resources.",
5
+ "keywords": [
6
+ "mta",
7
+ "mtasa",
8
+ "multi-theft-auto",
9
+ "lua",
10
+ "lua5.1",
11
+ "luau",
12
+ "compiler",
13
+ "transpiler",
14
+ "typed-lua"
15
+ ],
16
+ "homepage": "https://github.com/ThigasDevelopment/luam#readme",
17
+ "bugs": {
18
+ "url": "https://github.com/ThigasDevelopment/luam/issues"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/ThigasDevelopment/luam.git"
23
+ },
24
+ "license": "MIT",
25
+ "author": "Thigas",
26
+ "type": "module",
27
+ "bin": {
28
+ "luam": "luam.mjs"
29
+ },
30
+ "engines": {
31
+ "node": ">=20"
32
+ },
33
+ "files": [
34
+ "luam.mjs",
35
+ "lua",
36
+ "template"
37
+ ]
38
+ }
@@ -0,0 +1,120 @@
1
+ # luam-resource
2
+
3
+ A Luam resource scaffolded by `luam init`. Write `.luam` sources under `src`, run
4
+ `luam build`, and MTA gets a plain Lua 5.1 resource in `build/luam-resource`.
5
+
6
+ ## Commands
7
+
8
+ | Command | Behavior |
9
+ | ------- | -------- |
10
+ | `luam check` | Compiles and reports diagnostics. Writes nothing. |
11
+ | `luam build` | Writes the resource into `build/luam-resource`. |
12
+ | `luam ensure` | Builds, syncs into the MTA server, restarts, and watches sources. |
13
+
14
+ Set `serverPath` in `luam.json` before running `luam ensure`.
15
+
16
+ ## Layout
17
+
18
+ ```
19
+ .env Declares deployment keys and safe defaults
20
+ config.lua Structural settings the resource owns
21
+ assets/ Files copied verbatim and downloaded by clients
22
+ src/
23
+ shared/
24
+ config.luam Constants and helpers both sides use
25
+ framework/ Core, Event, Listener, Command, Loader, ThreadPool
26
+ server/
27
+ main.luam Creates the server Core and starts it
28
+ handlers/ One file per server listener
29
+ commands/ One file per server command
30
+ client/
31
+ main.luam Creates the client Core and starts it
32
+ commands/ One file per client command
33
+ ```
34
+
35
+ The generated resource mirrors this tree. `src/server/main.luam` becomes
36
+ `src/server/main.lua`, and runtime helpers land in `src/<environment>/lib`, so a
37
+ server-only helper is never downloaded by a client.
38
+
39
+ ## Configuration
40
+
41
+ Two files hold settings, and they have different owners.
42
+
43
+ `config.lua` belongs to the resource author. It is plain Lua, copied verbatim,
44
+ listed in `meta.xml` as a shared script, and readable by clients. Anything a
45
+ player may see belongs here.
46
+
47
+ `.env` belongs to the deployment. It declares the keys and safe defaults, and it
48
+ is the source of truth for their types: an unquoted number is a number, `true`
49
+ and `false` are booleans, and quoting forces a string. Override values locally in
50
+ `.env.local`, which is never committed.
51
+
52
+ The first `luam build` writes `build/<resource>/.env` from those keys, leaving
53
+ sensitive values blank, and never overwrites it again — that file belongs to the
54
+ server administrator.
55
+
56
+ Values reach Luam through `process.env`, which exists only on the server:
57
+
58
+ ```lua
59
+ outputServerLog(tostring(process.env.SERVER_NAME))
60
+ ```
61
+
62
+ A key that `.env` does not declare is a compile error, and `process` in a client
63
+ or shared file is a compile error too.
64
+
65
+ ## Framework
66
+
67
+ `Core` owns the resource lifecycle. `main.luam` creates it with the side it runs
68
+ on and calls `start()` once the resource has finished loading. `start()` scans
69
+ the class registry, instantiates every `Listener` and `Command` subclass with the
70
+ `Core` instance, and registers them.
71
+
72
+ ### Listeners
73
+
74
+ A listener declares the MTA event it handles and overrides `handle`. Nothing
75
+ registers it by hand — being a `Listener` subclass is enough.
76
+
77
+ ```lua
78
+ class PlayerQuitListener extends Listener {
79
+ event: string = 'onPlayerQuit'
80
+
81
+ handle(...): void {
82
+ outputDebugString(getPlayerName(source) .. ' left.')
83
+ }
84
+ }
85
+ ```
86
+
87
+ `element` defaults to `root`. Set it to narrow the handler to one element.
88
+
89
+ ### Commands
90
+
91
+ A command declares its name, optional aliases, and overrides `execute`. Every
92
+ name and alias is registered.
93
+
94
+ ```lua
95
+ class KickCommand extends Command {
96
+ name: string = 'kick'
97
+ aliases: string[] = { 'k' }
98
+ description: string = 'Kicks a player by name.'
99
+
100
+ execute(player: any, ...): void {
101
+ local args: any = { ... }
102
+
103
+ kickPlayer(getPlayerFromName(args[1]), player, 'kicked')
104
+ }
105
+ }
106
+ ```
107
+
108
+ On the server `execute` receives the calling player; on the client the first
109
+ parameter is `nil`.
110
+
111
+ ### Threads
112
+
113
+ `ThreadPool` wraps the `threads.lua` runtime helper, which is opt-in. Add it to
114
+ `luam.json` before using the class:
115
+
116
+ ```json
117
+ {
118
+ "helpers": ["threads"]
119
+ }
120
+ ```
@@ -0,0 +1,14 @@
1
+ Config = {
2
+ greeting = 'Welcome to the server.',
3
+
4
+ spawn = {
5
+ x = 1959.55,
6
+ y = -1714.46,
7
+ z = 10.0,
8
+ },
9
+
10
+ ping = {
11
+ limit = 250,
12
+ warnAt = 180,
13
+ },
14
+ }
package/template/env ADDED
@@ -0,0 +1,11 @@
1
+ # Deployment values. This file is versioned and declares the keys and safe
2
+ # defaults. Types come from this file: an unquoted number is a number, "true"
3
+ # and "false" are booleans, and quoting forces a string.
4
+ #
5
+ # Override values on your machine in ".env.local", which is never committed.
6
+ # The server administrator edits the generated "build/.env" instead.
7
+
8
+ # Server
9
+ SERVER_NAME="Luam Server"
10
+ MAX_PLAYERS=32
11
+ DEBUG=false
@@ -0,0 +1,9 @@
1
+ local core: Core = new Core('client')
2
+
3
+ addEventHandler('onClientResourceStart', resourceRoot, function()
4
+ core:start()
5
+ end)
6
+
7
+ addEventHandler('onClientResourceStop', resourceRoot, function()
8
+ core:stop()
9
+ end)
@@ -0,0 +1,9 @@
1
+ local core: Core = new Core('server')
2
+
3
+ addEventHandler('onResourceStart', resourceRoot, function()
4
+ core:start()
5
+ end)
6
+
7
+ addEventHandler('onResourceStop', resourceRoot, function()
8
+ core:stop()
9
+ end)
@@ -0,0 +1,54 @@
1
+ class Command {
2
+ core: any = nil
3
+ name: string = ''
4
+ aliases: string[] = {}
5
+ description: string = ''
6
+
7
+ constructor(core: any) {
8
+ self.core = core
9
+ }
10
+
11
+ execute(player: any, ...): void {
12
+ }
13
+
14
+ names(): string[] {
15
+ local found: string[] = {}
16
+
17
+ if self.name ~= '' then
18
+ table.insert(found, self.name)
19
+ end
20
+
21
+ for index = 1, #self.aliases do
22
+ table.insert(found, self.aliases[index])
23
+ end
24
+
25
+ return found
26
+ }
27
+
28
+ handler(): any {
29
+ local command: Command = self
30
+
31
+ if self.core:isServer() then
32
+ return function(player: any, invoked: string, ...)
33
+ command:execute(player, ...)
34
+ end
35
+ end
36
+
37
+ return function(invoked: string, ...)
38
+ command:execute(nil, ...)
39
+ end
40
+ }
41
+
42
+ register(): number {
43
+ local registered: number = 0
44
+ local found: string[] = self:names()
45
+
46
+ for index = 1, #found do
47
+ addCommandHandler(found[index], self:handler())
48
+
49
+ registered += 1
50
+ end
51
+
52
+ return registered
53
+ }
54
+ }
@@ -0,0 +1,58 @@
1
+ class Core {
2
+ side: string = 'shared'
3
+ listeners: any = nil
4
+ commands: any = nil
5
+ started: boolean = false
6
+
7
+ constructor(side: string) {
8
+ self.side = side
9
+ self.listeners = {}
10
+ self.commands = {}
11
+ self.started = false
12
+ }
13
+
14
+ isServer(): boolean {
15
+ return self.side == 'server'
16
+ }
17
+
18
+ isClient(): boolean {
19
+ return self.side == 'client'
20
+ }
21
+
22
+ log(message: string): void {
23
+ outputDebugString('[' .. self.side .. '] ' .. message)
24
+ }
25
+
26
+ start(): boolean {
27
+ if self.started then
28
+ return false
29
+ end
30
+
31
+ self.started = true
32
+
33
+ local loader: Loader = new Loader(self)
34
+
35
+ self.listeners = loader:loadListeners()
36
+ self.commands = loader:loadCommands()
37
+
38
+ self:log('loaded ' .. #self.listeners .. ' listener(s) and ' .. #self.commands .. ' command(s)')
39
+
40
+ return true
41
+ }
42
+
43
+ stop(): boolean {
44
+ if not self.started then
45
+ return false
46
+ end
47
+
48
+ for index = 1, #self.listeners do
49
+ self.listeners[index]:unregister()
50
+ end
51
+
52
+ self.listeners = {}
53
+ self.commands = {}
54
+ self.started = false
55
+
56
+ return true
57
+ }
58
+ }
@@ -0,0 +1,39 @@
1
+ class Event {
2
+ name: string = ''
3
+ element: any = nil
4
+ handler: any = nil
5
+ attached: boolean = false
6
+
7
+ constructor(name: string, element: any, handler: any) {
8
+ self.name = name
9
+ self.element = element
10
+ self.handler = handler
11
+ self.attached = false
12
+ }
13
+
14
+ attach(): boolean {
15
+ if self.attached then
16
+ return false
17
+ end
18
+
19
+ self.attached = addEventHandler(self.name, self.element, self.handler)
20
+
21
+ return self.attached
22
+ }
23
+
24
+ detach(): boolean {
25
+ if not self.attached then
26
+ return false
27
+ end
28
+
29
+ removeEventHandler(self.name, self.element, self.handler)
30
+
31
+ self.attached = false
32
+
33
+ return true
34
+ }
35
+
36
+ describe(): string {
37
+ return self.name
38
+ }
39
+ }
@@ -0,0 +1,40 @@
1
+ class Listener {
2
+ core: any = nil
3
+ event: string = ''
4
+ element: any = nil
5
+ binding: any = nil
6
+
7
+ constructor(core: any) {
8
+ self.core = core
9
+ self.binding = nil
10
+ }
11
+
12
+ target(): any {
13
+ if self.element == nil then
14
+ return root
15
+ end
16
+
17
+ return self.element
18
+ }
19
+
20
+ handle(...): void {
21
+ }
22
+
23
+ register(): boolean {
24
+ if self.event == '' then
25
+ return false
26
+ end
27
+
28
+ self.binding = new Event(self.event, self:target(), bind(self.handle, self))
29
+
30
+ return self.binding:attach()
31
+ }
32
+
33
+ unregister(): boolean {
34
+ if self.binding == nil then
35
+ return false
36
+ end
37
+
38
+ return self.binding:detach()
39
+ }
40
+ }
@@ -0,0 +1,73 @@
1
+ class Loader {
2
+ core: any = nil
3
+
4
+ constructor(core: any) {
5
+ self.core = core
6
+ }
7
+
8
+ inherits(definition: any, baseName: string): boolean {
9
+ local current: any = definition
10
+
11
+ while current ~= nil do
12
+ local parent: any = rawget(current, '__super')
13
+
14
+ if parent == nil then
15
+ return false
16
+ end
17
+
18
+ if rawget(parent, '__name') == baseName then
19
+ return true
20
+ end
21
+
22
+ current = parent
23
+ end
24
+
25
+ return false
26
+ }
27
+
28
+ subclasses(baseName: string): string[] {
29
+ local found: string[] = {}
30
+ local definitions: table = getClasses()
31
+
32
+ for name, definition in pairs(definitions) do
33
+ if self:inherits(definition, baseName) then
34
+ table.insert(found, name)
35
+ end
36
+ end
37
+
38
+ table.sort(found)
39
+
40
+ return found
41
+ }
42
+
43
+ instantiate(baseName: string): any {
44
+ local created: any = {}
45
+ local names: string[] = self:subclasses(baseName)
46
+
47
+ for index = 1, #names do
48
+ table.insert(created, new(names[index])(self.core))
49
+ end
50
+
51
+ return created
52
+ }
53
+
54
+ loadListeners(): any {
55
+ local listeners: any = self:instantiate('Listener')
56
+
57
+ for index = 1, #listeners do
58
+ listeners[index]:register()
59
+ end
60
+
61
+ return listeners
62
+ }
63
+
64
+ loadCommands(): any {
65
+ local commands: any = self:instantiate('Command')
66
+
67
+ for index = 1, #commands do
68
+ commands[index]:register()
69
+ end
70
+
71
+ return commands
72
+ }
73
+ }
@@ -0,0 +1,27 @@
1
+ class ThreadPool {
2
+ pool: any = nil
3
+
4
+ constructor(style: string, priority: string) {
5
+ self.pool = Threads.new(style, priority)
6
+ }
7
+
8
+ add(task: any): number {
9
+ return self.pool:add(task, {})
10
+ }
11
+
12
+ remove(id: number): boolean {
13
+ return self.pool:remove(id)
14
+ }
15
+
16
+ clear(): boolean {
17
+ return self.pool:clear()
18
+ }
19
+
20
+ priority(): string {
21
+ return self.pool:getPriority()
22
+ }
23
+
24
+ setPriority(priority: string): boolean {
25
+ return self.pool:setPriority(priority)
26
+ }
27
+ }
@@ -0,0 +1,7 @@
1
+ build/
2
+ node_modules/
3
+ .luam/
4
+
5
+ .env.local
6
+
7
+ *.log
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "luam-resource",
3
+ "version": "1.0.0",
4
+ "description": "A Luam resource scaffolded by luam init.",
5
+ "sourceDirs": ["src"],
6
+ "assetDirs": ["assets"],
7
+ "outDir": "build",
8
+ "transport": {
9
+ "kind": "none"
10
+ }
11
+ }
@@ -0,0 +1,8 @@
1
+ class PingCommand extends Command {
2
+ name: string = 'ping'
3
+ description: string = 'Writes a pong line into the debug console.'
4
+
5
+ execute(player: any, ...): void {
6
+ outputDebugString(formatPlayerLabel('pong'))
7
+ }
8
+ }
@@ -0,0 +1,5 @@
1
+ local core: Core = new Core('client')
2
+
3
+ addEventHandler('onClientResourceStart', resourceRoot, function()
4
+ core:start()
5
+ end)
@@ -0,0 +1,9 @@
1
+ class HelloCommand extends Command {
2
+ name: string = 'hello'
3
+ aliases: string[] = { 'hi', 'greet' }
4
+ description: string = 'Greets the calling player with the configured message.'
5
+
6
+ execute(player: any, ...): void {
7
+ outputChatBox(formatPlayerLabel(tostring(Config.greeting)), player)
8
+ }
9
+ }
@@ -0,0 +1,9 @@
1
+ class PlayerJoinListener extends Listener {
2
+ event: string = 'onPlayerJoin'
3
+
4
+ handle(...): void {
5
+ local player: any = source
6
+
7
+ outputChatBox(formatPlayerLabel(getPlayerName(player)) .. ' joined the server.', root)
8
+ }
9
+ }
@@ -0,0 +1,11 @@
1
+ local core: Core = new Core('server')
2
+
3
+ addEventHandler('onResourceStart', resourceRoot, function()
4
+ core:start()
5
+
6
+ outputServerLog(`[${process.env.SERVER_NAME}] started with room for ${process.env.MAX_PLAYERS} players.`)
7
+ end)
8
+
9
+ addEventHandler('onResourceStop', resourceRoot, function()
10
+ core:stop()
11
+ end)
@@ -0,0 +1,5 @@
1
+ RESOURCE_LABEL = getResourceName(getThisResource())
2
+
3
+ function formatPlayerLabel(name: string): string
4
+ return `[${RESOURCE_LABEL}] ${name}`
5
+ end
@@ -0,0 +1,54 @@
1
+ class Command {
2
+ core: any = nil
3
+ name: string = ''
4
+ aliases: string[] = {}
5
+ description: string = ''
6
+
7
+ constructor(core: any) {
8
+ self.core = core
9
+ }
10
+
11
+ execute(player: any, ...): void {
12
+ }
13
+
14
+ names(): string[] {
15
+ local found: string[] = {}
16
+
17
+ if self.name ~= '' then
18
+ table.insert(found, self.name)
19
+ end
20
+
21
+ for index = 1, #self.aliases do
22
+ table.insert(found, self.aliases[index])
23
+ end
24
+
25
+ return found
26
+ }
27
+
28
+ handler(): any {
29
+ local command: Command = self
30
+
31
+ if self.core:isServer() then
32
+ return function(player: any, invoked: string, ...)
33
+ command:execute(player, ...)
34
+ end
35
+ end
36
+
37
+ return function(invoked: string, ...)
38
+ command:execute(nil, ...)
39
+ end
40
+ }
41
+
42
+ register(): number {
43
+ local registered: number = 0
44
+ local found: string[] = self:names()
45
+
46
+ for index = 1, #found do
47
+ addCommandHandler(found[index], self:handler())
48
+
49
+ registered += 1
50
+ end
51
+
52
+ return registered
53
+ }
54
+ }
@@ -0,0 +1,58 @@
1
+ class Core {
2
+ side: string = 'shared'
3
+ listeners: any = nil
4
+ commands: any = nil
5
+ started: boolean = false
6
+
7
+ constructor(side: string) {
8
+ self.side = side
9
+ self.listeners = {}
10
+ self.commands = {}
11
+ self.started = false
12
+ }
13
+
14
+ isServer(): boolean {
15
+ return self.side == 'server'
16
+ }
17
+
18
+ isClient(): boolean {
19
+ return self.side == 'client'
20
+ }
21
+
22
+ log(message: string): void {
23
+ outputDebugString('[' .. self.side .. '] ' .. message)
24
+ }
25
+
26
+ start(): boolean {
27
+ if self.started then
28
+ return false
29
+ end
30
+
31
+ self.started = true
32
+
33
+ local loader: Loader = new Loader(self)
34
+
35
+ self.listeners = loader:loadListeners()
36
+ self.commands = loader:loadCommands()
37
+
38
+ self:log('loaded ' .. #self.listeners .. ' listener(s) and ' .. #self.commands .. ' command(s)')
39
+
40
+ return true
41
+ }
42
+
43
+ stop(): boolean {
44
+ if not self.started then
45
+ return false
46
+ end
47
+
48
+ for index = 1, #self.listeners do
49
+ self.listeners[index]:unregister()
50
+ end
51
+
52
+ self.listeners = {}
53
+ self.commands = {}
54
+ self.started = false
55
+
56
+ return true
57
+ }
58
+ }