mirror of
https://github.com/citizenfx/cfx-server-data.git
synced 2026-09-21 02:39:29 +02:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32d98e7524 | ||
|
|
94471a3f63 | ||
|
|
f798007b6b | ||
|
|
bfc8ccb8e9 |
@@ -2,3 +2,7 @@
|
|||||||
/cache/
|
/cache/
|
||||||
/resources/\[local\]/
|
/resources/\[local\]/
|
||||||
/db/
|
/db/
|
||||||
|
/logs/
|
||||||
|
/local-database/
|
||||||
|
.console-history
|
||||||
|
.id
|
||||||
@@ -2,7 +2,7 @@ AddEventHandler('gameEventTriggered', function(eventName, args)
|
|||||||
if eventName == 'CEventNetworkEntityDamage' then
|
if eventName == 'CEventNetworkEntityDamage' then
|
||||||
local victim = args[1]
|
local victim = args[1]
|
||||||
local culprit = args[2]
|
local culprit = args[2]
|
||||||
local isDead = args[4] == 1
|
local isDead = args[6] == 1
|
||||||
|
|
||||||
if isDead then
|
if isDead then
|
||||||
local origCoords = GetEntityCoords(victim)
|
local origCoords = GetEntityCoords(victim)
|
||||||
|
|||||||
+2
-2
@@ -3,13 +3,13 @@
|
|||||||
|
|
||||||
version '1.0.0'
|
version '1.0.0'
|
||||||
author 'Cfx.re <[email protected]>'
|
author 'Cfx.re <[email protected]>'
|
||||||
description 'A GTA Online-styled theme for the chat resource.'
|
description 'Example theme for the chat resource.'
|
||||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
repository 'https://github.com/citizenfx/cfx-server-data'
|
||||||
|
|
||||||
file 'style.css'
|
file 'style.css'
|
||||||
file 'shadow.js'
|
file 'shadow.js'
|
||||||
|
|
||||||
chat_theme 'gtao' {
|
chat_theme 'example' {
|
||||||
styleSheet = 'style.css',
|
styleSheet = 'style.css',
|
||||||
script = 'shadow.js',
|
script = 'shadow.js',
|
||||||
msgTemplates = {
|
msgTemplates = {
|
||||||
+3
-16
@@ -19,20 +19,9 @@
|
|||||||
user-select: none;
|
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 {
|
.msg {
|
||||||
font-family: Font2, sans-serif;
|
font-family: sans-serif;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
|
||||||
font-size: calc(1.8vh); /* 13px in 720p, calc'd by width */
|
font-size: calc(1.8vh); /* 13px in 720p, calc'd by width */
|
||||||
filter: url(#svgDropShadowFilter);
|
filter: url(#svgDropShadowFilter);
|
||||||
|
|
||||||
@@ -47,15 +36,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.msg > span > span > b {
|
.msg > span > span > b {
|
||||||
font-family: Font2_cond, sans-serif;
|
font-family: sans-serif;
|
||||||
font-weight: normal;
|
|
||||||
|
|
||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
padding-right: 11px;
|
padding-right: 11px;
|
||||||
|
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|
||||||
font-size: calc(2.7vh);
|
font-size: calc(1.8vh);
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg > span > span > span {
|
.msg > span > span > span {
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
node_modules/
|
|
||||||
.yarn.installed
|
|
||||||
yarn-error.log
|
|
||||||
dist/
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# Chat
|
|
||||||
@@ -1,308 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
-- 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 <[email protected]>'
|
|
||||||
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'
|
|
||||||
@@ -1,467 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
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, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="msg" :class="{ multiline }">
|
|
||||||
<span v-html="textEscaped"></span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" src="./Message.ts"></script>
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
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: {},
|
|
||||||
});
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
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%',
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
-4
@@ -1,4 +0,0 @@
|
|||||||
declare module '*.vue' {
|
|
||||||
import Vue from 'vue'
|
|
||||||
export default Vue
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import Vue from 'vue';
|
|
||||||
import App from './App.vue';
|
|
||||||
|
|
||||||
const instance = new Vue({
|
|
||||||
el: '#app',
|
|
||||||
render: h => h(App),
|
|
||||||
});
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "./",
|
|
||||||
"module": "es6",
|
|
||||||
"strict": true,
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"target": "es6",
|
|
||||||
"allowJs": true,
|
|
||||||
"lib": [
|
|
||||||
"es2017",
|
|
||||||
"dom"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"./**/*"
|
|
||||||
],
|
|
||||||
"exclude": []
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
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.
@@ -1,48 +0,0 @@
|
|||||||
/* 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;
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,293 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
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
@@ -216,70 +216,54 @@ AddEventHandler('onResourceStop', function(resource)
|
|||||||
unloadMap(resource)
|
unloadMap(resource)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
AddEventHandler('rconCommand', function(commandName, args)
|
RegisterCommand("map", function(source, args)
|
||||||
if commandName == 'map' then
|
if #args ~= 1 then
|
||||||
if #args ~= 1 then
|
print("usage: map [mapname]")
|
||||||
RconPrint("usage: map [mapname]\n")
|
return
|
||||||
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
|
||||||
|
|
||||||
|
if not maps[args[1]] then
|
||||||
|
print('no such map ' .. args[1] .. '\n')
|
||||||
|
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])
|
||||||
|
|
||||||
|
print('map ' .. args[1] .. "\n")
|
||||||
|
else
|
||||||
|
print('map ' .. args[1] .. ' does not support ' .. currentGameType .. "\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end, true)
|
||||||
|
|
||||||
|
RegisterCommand("gametype", function(source, args)
|
||||||
|
if #args ~= 1 then
|
||||||
|
print("usage: gametype [name]\n")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if not gametypes[args[1]] then
|
||||||
|
print('no such gametype ' .. args[1] .. "\n")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
changeGameType(args[1])
|
||||||
|
print('gametype ' .. args[1] .. "\n")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
function getCurrentGameType()
|
function getCurrentGameType()
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
.yarn.installed
|
|
||||||
node_modules/
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
-- 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 <[email protected]>'
|
|
||||||
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'
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
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
@@ -1,12 +0,0 @@
|
|||||||
-- 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 <[email protected]>'
|
|
||||||
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'
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
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);
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -33,8 +33,8 @@ Citizen.CreateThread(function()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local killerid = GetPlayerByEntityID(killer)
|
local killerid = NetworkGetPlayerIndexFromPed(killer)
|
||||||
if killer ~= ped and killerid ~= nil and NetworkIsPlayerActive(killerid) then killerid = GetPlayerServerId(killerid)
|
if killer ~= ped and killerid ~= -1 and NetworkIsPlayerActive(killerid) then killerid = GetPlayerServerId(killerid)
|
||||||
else killerid = -1
|
else killerid = -1
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -64,10 +64,3 @@ Citizen.CreateThread(function()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
function GetPlayerByEntityID(id)
|
|
||||||
for i=0,32 do
|
|
||||||
if(NetworkIsPlayerActive(i) and GetPlayerPed(i) == id) then return i end
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
Citizen.CreateThread(function()
|
|
||||||
while true do
|
|
||||||
Wait(0)
|
|
||||||
|
|
||||||
if NetworkIsSessionStarted() then
|
|
||||||
TriggerServerEvent('hardcap:playerActivated')
|
|
||||||
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
-- 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 <[email protected]>'
|
|
||||||
description 'Limits the number of players to the amount set by sv_maxclients in your server.cfg.'
|
|
||||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
|
||||||
|
|
||||||
client_script 'client.lua'
|
|
||||||
server_script 'server.lua'
|
|
||||||
|
|
||||||
fx_version 'adamant'
|
|
||||||
games { 'gta5', '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.'
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
local playerCount = 0
|
|
||||||
local list = {}
|
|
||||||
|
|
||||||
RegisterServerEvent('hardcap:playerActivated')
|
|
||||||
|
|
||||||
AddEventHandler('hardcap:playerActivated', function()
|
|
||||||
if not list[source] then
|
|
||||||
playerCount = playerCount + 1
|
|
||||||
list[source] = true
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
AddEventHandler('playerDropped', function()
|
|
||||||
if list[source] then
|
|
||||||
playerCount = playerCount - 1
|
|
||||||
list[source] = nil
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
AddEventHandler('playerConnecting', function(name, setReason)
|
|
||||||
local cv = GetConvarInt('sv_maxclients', 32)
|
|
||||||
|
|
||||||
print('Connecting: ' .. name .. '^7')
|
|
||||||
|
|
||||||
if playerCount >= cv then
|
|
||||||
print('Full. :(')
|
|
||||||
|
|
||||||
setReason('This server is full (past ' .. tostring(cv) .. ' players).')
|
|
||||||
CancelEvent()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
-- 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 <[email protected]>'
|
|
||||||
description 'Handles old-style server player management commands.'
|
|
||||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
|
||||||
|
|
||||||
client_script 'rconlog_client.lua'
|
|
||||||
server_script 'rconlog_server.lua'
|
|
||||||
|
|
||||||
fx_version 'adamant'
|
|
||||||
games { 'gta5', '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.'
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
RegisterNetEvent('rlUpdateNames')
|
|
||||||
|
|
||||||
AddEventHandler('rlUpdateNames', function()
|
|
||||||
local names = {}
|
|
||||||
|
|
||||||
for i = 0, 31 do
|
|
||||||
if NetworkIsPlayerActive(i) then
|
|
||||||
names[GetPlayerServerId(i)] = { id = i, name = GetPlayerName(i) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
TriggerServerEvent('rlUpdateNamesResult', names)
|
|
||||||
end)
|
|
||||||
|
|
||||||
Citizen.CreateThread(function()
|
|
||||||
while true do
|
|
||||||
Wait(0)
|
|
||||||
|
|
||||||
if NetworkIsSessionStarted() then
|
|
||||||
TriggerServerEvent('rlPlayerActivated')
|
|
||||||
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
RconLog({ msgType = 'serverStart', hostname = 'lovely', maxplayers = 32 })
|
|
||||||
|
|
||||||
RegisterServerEvent('rlPlayerActivated')
|
|
||||||
|
|
||||||
local names = {}
|
|
||||||
|
|
||||||
AddEventHandler('rlPlayerActivated', function()
|
|
||||||
RconLog({ msgType = 'playerActivated', netID = source, name = GetPlayerName(source), guid = GetPlayerIdentifiers(source)[1], ip = GetPlayerEP(source) })
|
|
||||||
|
|
||||||
names[source] = { name = GetPlayerName(source), id = source }
|
|
||||||
|
|
||||||
if GetHostId() then
|
|
||||||
TriggerClientEvent('rlUpdateNames', GetHostId())
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
RegisterServerEvent('rlUpdateNamesResult')
|
|
||||||
|
|
||||||
AddEventHandler('rlUpdateNamesResult', function(res)
|
|
||||||
if source ~= tonumber(GetHostId()) then
|
|
||||||
print('bad guy')
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
for id, data in pairs(res) do
|
|
||||||
if data then
|
|
||||||
if data.name then
|
|
||||||
if not names[id] then
|
|
||||||
names[id] = data
|
|
||||||
end
|
|
||||||
|
|
||||||
if names[id].name ~= data.name or names[id].id ~= data.id then
|
|
||||||
names[id] = data
|
|
||||||
|
|
||||||
RconLog({ msgType = 'playerRenamed', netID = id, name = data.name })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
names[id] = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
AddEventHandler('playerDropped', function()
|
|
||||||
RconLog({ msgType = 'playerDropped', netID = source, name = GetPlayerName(source) })
|
|
||||||
|
|
||||||
names[source] = nil
|
|
||||||
end)
|
|
||||||
|
|
||||||
AddEventHandler('chatMessage', function(netID, name, message)
|
|
||||||
RconLog({ msgType = 'chatMessage', netID = netID, name = name, message = message, guid = GetPlayerIdentifiers(netID)[1] })
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- NOTE: DO NOT USE THIS METHOD FOR HANDLING COMMANDS
|
|
||||||
-- This resource has not been updated to use newer methods such as RegisterCommand.
|
|
||||||
AddEventHandler('rconCommand', function(commandName, args)
|
|
||||||
if commandName == 'status' then
|
|
||||||
for netid, data in pairs(names) do
|
|
||||||
local guid = GetPlayerIdentifiers(netid)
|
|
||||||
|
|
||||||
if guid and guid[1] and data then
|
|
||||||
local ping = GetPlayerPing(netid)
|
|
||||||
|
|
||||||
RconPrint(netid .. ' ' .. guid[1] .. ' ' .. data.name .. ' ' .. GetPlayerEP(netid) .. ' ' .. ping .. "\n")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
CancelEvent()
|
|
||||||
elseif commandName:lower() == 'clientkick' then
|
|
||||||
local playerId = table.remove(args, 1)
|
|
||||||
local msg = table.concat(args, ' ')
|
|
||||||
|
|
||||||
DropPlayer(playerId, msg)
|
|
||||||
|
|
||||||
CancelEvent()
|
|
||||||
elseif commandName:lower() == 'tempbanclient' then
|
|
||||||
local playerId = table.remove(args, 1)
|
|
||||||
local msg = table.concat(args, ' ')
|
|
||||||
|
|
||||||
TempBanPlayer(playerId, msg)
|
|
||||||
|
|
||||||
CancelEvent()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
@@ -19,6 +19,7 @@ shared_script 'runcode.js'
|
|||||||
client_script 'runcode_ui.lua'
|
client_script 'runcode_ui.lua'
|
||||||
|
|
||||||
ui_page 'web/nui.html'
|
ui_page 'web/nui.html'
|
||||||
|
nui_callback_strict_mode 'false'
|
||||||
files {
|
files {
|
||||||
'web/nui.html'
|
'web/nui.html'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
node_modules/
|
|
||||||
.yarn.installed
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
-- 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 <[email protected]>'
|
|
||||||
description 'Handles Social Club conductor session API for RedM. Do not disable.'
|
|
||||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
|
||||||
|
|
||||||
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.'
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
'yarn'
|
|
||||||
}
|
|
||||||
|
|
||||||
server_script 'sm_server.js'
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"private": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@citizenfx/protobufjs": "6.8.8"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
syntax = "proto3";
|
|
||||||
package rline;
|
|
||||||
|
|
||||||
message RpcErrorData {
|
|
||||||
string ErrorCodeString = 1;
|
|
||||||
int32 ErrorCode = 2;
|
|
||||||
string DomainString = 3;
|
|
||||||
int32 DomainCode = 4;
|
|
||||||
bytes DataEx = 5;
|
|
||||||
};
|
|
||||||
|
|
||||||
message RpcError {
|
|
||||||
int32 ErrorCode = 1;
|
|
||||||
string ErrorMessage = 2;
|
|
||||||
RpcErrorData Data = 3;
|
|
||||||
};
|
|
||||||
|
|
||||||
message RpcHeader {
|
|
||||||
string RequestId = 1;
|
|
||||||
string MethodName = 2;
|
|
||||||
RpcError Error = 3;
|
|
||||||
string srcTid = 4;
|
|
||||||
};
|
|
||||||
|
|
||||||
message RpcMessage {
|
|
||||||
RpcHeader Header = 1;
|
|
||||||
bytes Content = 2;
|
|
||||||
};
|
|
||||||
|
|
||||||
message RpcResponseContainer {
|
|
||||||
bytes Content = 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
message RpcResponseMessage {
|
|
||||||
RpcHeader Header = 1;
|
|
||||||
RpcResponseContainer Container = 2;
|
|
||||||
};
|
|
||||||
|
|
||||||
message TokenStuff {
|
|
||||||
string tkn = 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
message InitSessionResponse {
|
|
||||||
bytes sesid = 1;
|
|
||||||
TokenStuff token = 2;
|
|
||||||
};
|
|
||||||
|
|
||||||
message MpGamerHandleDto {
|
|
||||||
string gh = 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
message MpPeerAddressDto {
|
|
||||||
string addr = 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
message InitPlayer2_Parameters {
|
|
||||||
MpGamerHandleDto gh = 1;
|
|
||||||
MpPeerAddressDto peerAddress = 2;
|
|
||||||
int32 discriminator = 3;
|
|
||||||
int32 seamlessType = 4;
|
|
||||||
uint32 connectionReason = 5;
|
|
||||||
};
|
|
||||||
|
|
||||||
message InitPlayerResult {
|
|
||||||
uint32 code = 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
message Restriction {
|
|
||||||
int32 u1 = 1;
|
|
||||||
int32 u2 = 2;
|
|
||||||
int32 u3 = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message GetRestrictionsData {
|
|
||||||
repeated Restriction restriction = 1;
|
|
||||||
repeated string unk2 = 2;
|
|
||||||
};
|
|
||||||
|
|
||||||
message GetRestrictionsResult {
|
|
||||||
GetRestrictionsData data = 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
message PlayerIdSto {
|
|
||||||
int32 acctId = 1;
|
|
||||||
int32 platId = 2;
|
|
||||||
};
|
|
||||||
|
|
||||||
message MpSessionRequestIdDto {
|
|
||||||
PlayerIdSto requestor = 1;
|
|
||||||
int32 index = 2;
|
|
||||||
int32 hash = 3;
|
|
||||||
};
|
|
||||||
|
|
||||||
message QueueForSession_Seamless_Parameters {
|
|
||||||
MpSessionRequestIdDto requestId = 1;
|
|
||||||
uint32 optionFlags = 2;
|
|
||||||
int32 x = 3;
|
|
||||||
int32 y = 4;
|
|
||||||
};
|
|
||||||
|
|
||||||
message QueueForSessionResult {
|
|
||||||
uint32 code = 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
message QueueEntered_Parameters {
|
|
||||||
uint32 queueGroup = 1;
|
|
||||||
MpSessionRequestIdDto requestId = 2;
|
|
||||||
uint32 optionFlags = 3;
|
|
||||||
};
|
|
||||||
|
|
||||||
message GuidDto {
|
|
||||||
fixed64 a = 1;
|
|
||||||
fixed64 b = 2;
|
|
||||||
};
|
|
||||||
|
|
||||||
message MpTransitionIdDto {
|
|
||||||
GuidDto value = 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
message MpSessionIdDto {
|
|
||||||
GuidDto value = 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
message SessionSubcommandEnterSession {
|
|
||||||
int32 index = 1;
|
|
||||||
int32 hindex = 2;
|
|
||||||
uint32 sessionFlags = 3;
|
|
||||||
uint32 mode = 4;
|
|
||||||
int32 size = 5;
|
|
||||||
int32 teamIndex = 6;
|
|
||||||
MpTransitionIdDto transitionId = 7;
|
|
||||||
uint32 sessionManagerType = 8;
|
|
||||||
int32 slotCount = 9;
|
|
||||||
};
|
|
||||||
|
|
||||||
message SessionSubcommandLeaveSession {
|
|
||||||
uint32 reason = 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
message SessionSubcommandAddPlayer {
|
|
||||||
PlayerIdSto id = 1;
|
|
||||||
MpGamerHandleDto gh = 2;
|
|
||||||
MpPeerAddressDto addr = 3;
|
|
||||||
int32 index = 4;
|
|
||||||
};
|
|
||||||
|
|
||||||
message SessionSubcommandRemovePlayer {
|
|
||||||
PlayerIdSto id = 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
message SessionSubcommandHostChanged {
|
|
||||||
int32 index = 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
message SessionCommand {
|
|
||||||
uint32 cmd = 1;
|
|
||||||
string cmdname = 2;
|
|
||||||
SessionSubcommandEnterSession EnterSession = 3;
|
|
||||||
SessionSubcommandLeaveSession LeaveSession = 4;
|
|
||||||
SessionSubcommandAddPlayer AddPlayer = 5;
|
|
||||||
SessionSubcommandRemovePlayer RemovePlayer = 6;
|
|
||||||
SessionSubcommandHostChanged HostChanged = 7;
|
|
||||||
};
|
|
||||||
|
|
||||||
message scmds_Parameters {
|
|
||||||
MpSessionIdDto sid = 1;
|
|
||||||
int32 ncmds = 2;
|
|
||||||
repeated SessionCommand cmds = 3;
|
|
||||||
};
|
|
||||||
|
|
||||||
message UriType {
|
|
||||||
string url = 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
message TransitionReady_PlayerQueue_Parameters {
|
|
||||||
UriType serverUri = 1;
|
|
||||||
uint32 serverSandbox = 2;
|
|
||||||
MpTransitionIdDto id = 3;
|
|
||||||
uint32 sessionType = 4;
|
|
||||||
MpSessionRequestIdDto requestId = 5;
|
|
||||||
MpSessionIdDto transferId = 6;
|
|
||||||
};
|
|
||||||
|
|
||||||
message TransitionToSession_Parameters {
|
|
||||||
MpTransitionIdDto id = 1;
|
|
||||||
float x = 2;
|
|
||||||
float y = 3;
|
|
||||||
};
|
|
||||||
|
|
||||||
message TransitionToSessionResult {
|
|
||||||
uint32 code = 1;
|
|
||||||
};
|
|
||||||
@@ -1,328 +0,0 @@
|
|||||||
const protobuf = require("@citizenfx/protobufjs");
|
|
||||||
|
|
||||||
const playerDatas = {};
|
|
||||||
let slotsUsed = 0;
|
|
||||||
|
|
||||||
function assignSlotId() {
|
|
||||||
for (let i = 0; i < 32; i++) {
|
|
||||||
if (!(slotsUsed & (1 << i))) {
|
|
||||||
slotsUsed |= (1 << i);
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
let hostIndex = -1;
|
|
||||||
const isOneSync = GetConvar("onesync", "off") !== "off";
|
|
||||||
|
|
||||||
protobuf.load(GetResourcePath(GetCurrentResourceName()) + "/rline.proto", function(err, root) {
|
|
||||||
if (err) {
|
|
||||||
console.log(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const RpcMessage = root.lookupType("rline.RpcMessage");
|
|
||||||
const RpcResponseMessage = root.lookupType("rline.RpcResponseMessage");
|
|
||||||
const InitSessionResponse = root.lookupType("rline.InitSessionResponse");
|
|
||||||
const InitPlayer2_Parameters = root.lookupType("rline.InitPlayer2_Parameters");
|
|
||||||
const InitPlayerResult = root.lookupType("rline.InitPlayerResult");
|
|
||||||
const GetRestrictionsResult = root.lookupType("rline.GetRestrictionsResult");
|
|
||||||
const QueueForSession_Seamless_Parameters = root.lookupType("rline.QueueForSession_Seamless_Parameters");
|
|
||||||
const QueueForSessionResult = root.lookupType("rline.QueueForSessionResult");
|
|
||||||
const QueueEntered_Parameters = root.lookupType("rline.QueueEntered_Parameters");
|
|
||||||
const TransitionReady_PlayerQueue_Parameters = root.lookupType("rline.TransitionReady_PlayerQueue_Parameters");
|
|
||||||
const TransitionToSession_Parameters = root.lookupType("rline.TransitionToSession_Parameters");
|
|
||||||
const TransitionToSessionResult = root.lookupType("rline.TransitionToSessionResult");
|
|
||||||
const scmds_Parameters = root.lookupType("rline.scmds_Parameters");
|
|
||||||
|
|
||||||
function toArrayBuffer(buf) {
|
|
||||||
var ab = new ArrayBuffer(buf.length);
|
|
||||||
var view = new Uint8Array(ab);
|
|
||||||
for (var i = 0; i < buf.length; ++i) {
|
|
||||||
view[i] = buf[i];
|
|
||||||
}
|
|
||||||
return ab;
|
|
||||||
}
|
|
||||||
|
|
||||||
function emitMsg(target, data) {
|
|
||||||
emitNet('__cfx_internal:pbRlScSession', target, toArrayBuffer(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
function emitSessionCmds(target, cmd, cmdname, msg) {
|
|
||||||
const stuff = {};
|
|
||||||
stuff[cmdname] = msg;
|
|
||||||
|
|
||||||
emitMsg(target, RpcMessage.encode({
|
|
||||||
Header: {
|
|
||||||
MethodName: 'scmds'
|
|
||||||
},
|
|
||||||
Content: scmds_Parameters.encode({
|
|
||||||
sid: {
|
|
||||||
value: {
|
|
||||||
a: 2,
|
|
||||||
b: 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ncmds: 1,
|
|
||||||
cmds: [
|
|
||||||
{
|
|
||||||
cmd,
|
|
||||||
cmdname,
|
|
||||||
...stuff
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}).finish()
|
|
||||||
}).finish());
|
|
||||||
}
|
|
||||||
|
|
||||||
function emitAddPlayer(target, msg) {
|
|
||||||
emitSessionCmds(target, 2, 'AddPlayer', msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
function emitRemovePlayer(target, msg) {
|
|
||||||
emitSessionCmds(target, 3, 'RemovePlayer', msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
function emitHostChanged(target, msg) {
|
|
||||||
emitSessionCmds(target, 5, 'HostChanged', msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
onNet('playerDropped', () => {
|
|
||||||
if (isOneSync) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const oData = playerDatas[source];
|
|
||||||
delete playerDatas[source];
|
|
||||||
|
|
||||||
if (oData && hostIndex === oData.slot) {
|
|
||||||
const pda = Object.entries(playerDatas);
|
|
||||||
|
|
||||||
if (pda.length > 0) {
|
|
||||||
hostIndex = pda[0][1].slot | 0; // TODO: actually use <=31 slot index *and* check for id
|
|
||||||
|
|
||||||
for (const [ id, data ] of Object.entries(playerDatas)) {
|
|
||||||
emitHostChanged(id, {
|
|
||||||
index: hostIndex
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
hostIndex = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!oData) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oData.slot > -1) {
|
|
||||||
slotsUsed &= ~(1 << oData.slot);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [ id, data ] of Object.entries(playerDatas)) {
|
|
||||||
emitRemovePlayer(id, {
|
|
||||||
id: oData.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
console.log(e.stack);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function makeResponse(type, data) {
|
|
||||||
return {
|
|
||||||
Header: {
|
|
||||||
},
|
|
||||||
Container: {
|
|
||||||
Content: type.encode(data).finish()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlers = {
|
|
||||||
async InitSession(source, data) {
|
|
||||||
return makeResponse(InitSessionResponse, {
|
|
||||||
sesid: Buffer.alloc(16),
|
|
||||||
/*token: {
|
|
||||||
tkn: 'ACSTOKEN token="meow",signature="meow"'
|
|
||||||
}*/
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async InitPlayer2(source, data) {
|
|
||||||
const req = InitPlayer2_Parameters.decode(data);
|
|
||||||
|
|
||||||
if (!isOneSync) {
|
|
||||||
playerDatas[source] = {
|
|
||||||
gh: req.gh,
|
|
||||||
peerAddress: req.peerAddress,
|
|
||||||
discriminator: req.discriminator,
|
|
||||||
slot: -1
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return makeResponse(InitPlayerResult, {
|
|
||||||
code: 0
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async GetRestrictions(source, data) {
|
|
||||||
return makeResponse(GetRestrictionsResult, {
|
|
||||||
data: {
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async ConfirmSessionEntered(source, data) {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
|
|
||||||
async TransitionToSession(source, data) {
|
|
||||||
const req = TransitionToSession_Parameters.decode(data);
|
|
||||||
|
|
||||||
return makeResponse(TransitionToSessionResult, {
|
|
||||||
code: 1 // in this message, 1 is success
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async QueueForSession_Seamless(source, data) {
|
|
||||||
const req = QueueForSession_Seamless_Parameters.decode(data);
|
|
||||||
|
|
||||||
if (!isOneSync) {
|
|
||||||
playerDatas[source].req = req.requestId;
|
|
||||||
playerDatas[source].id = req.requestId.requestor;
|
|
||||||
playerDatas[source].slot = assignSlotId();
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
emitMsg(source, RpcMessage.encode({
|
|
||||||
Header: {
|
|
||||||
MethodName: 'QueueEntered'
|
|
||||||
},
|
|
||||||
Content: QueueEntered_Parameters.encode({
|
|
||||||
queueGroup: 69,
|
|
||||||
requestId: req.requestId,
|
|
||||||
optionFlags: req.optionFlags
|
|
||||||
}).finish()
|
|
||||||
}).finish());
|
|
||||||
|
|
||||||
if (isOneSync) {
|
|
||||||
hostIndex = 16
|
|
||||||
} else if (hostIndex === -1) {
|
|
||||||
hostIndex = playerDatas[source].slot | 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
emitMsg(source, RpcMessage.encode({
|
|
||||||
Header: {
|
|
||||||
MethodName: 'TransitionReady_PlayerQueue'
|
|
||||||
},
|
|
||||||
Content: TransitionReady_PlayerQueue_Parameters.encode({
|
|
||||||
serverUri: {
|
|
||||||
url: ''
|
|
||||||
},
|
|
||||||
requestId: req.requestId,
|
|
||||||
id: {
|
|
||||||
value: {
|
|
||||||
a: 2,
|
|
||||||
b: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
serverSandbox: 0xD656C677,
|
|
||||||
sessionType: 3,
|
|
||||||
transferId: {
|
|
||||||
value: {
|
|
||||||
a: 2,
|
|
||||||
b: 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}).finish()
|
|
||||||
}).finish());
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
emitSessionCmds(source, 0, 'EnterSession', {
|
|
||||||
index: (isOneSync) ? 16 : playerDatas[source].slot | 0,
|
|
||||||
hindex: hostIndex,
|
|
||||||
sessionFlags: 0,
|
|
||||||
mode: 0,
|
|
||||||
size: (isOneSync) ? 0 : Object.entries(playerDatas).filter(a => a[1].id).length,
|
|
||||||
//size: 2,
|
|
||||||
//size: Object.entries(playerDatas).length,
|
|
||||||
teamIndex: 0,
|
|
||||||
transitionId: {
|
|
||||||
value: {
|
|
||||||
a: 2,
|
|
||||||
b: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sessionManagerType: 0,
|
|
||||||
slotCount: 32
|
|
||||||
});
|
|
||||||
}, 50);
|
|
||||||
|
|
||||||
if (!isOneSync) {
|
|
||||||
setTimeout(() => {
|
|
||||||
// tell player about everyone, and everyone about player
|
|
||||||
const meData = playerDatas[source];
|
|
||||||
|
|
||||||
const aboutMe = {
|
|
||||||
id: meData.id,
|
|
||||||
gh: meData.gh,
|
|
||||||
addr: meData.peerAddress,
|
|
||||||
index: playerDatas[source].slot | 0
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const [ id, data ] of Object.entries(playerDatas)) {
|
|
||||||
if (id == source || !data.id) continue;
|
|
||||||
|
|
||||||
emitAddPlayer(source, {
|
|
||||||
id: data.id,
|
|
||||||
gh: data.gh,
|
|
||||||
addr: data.peerAddress,
|
|
||||||
index: data.slot | 0
|
|
||||||
});
|
|
||||||
|
|
||||||
emitAddPlayer(id, aboutMe);
|
|
||||||
}
|
|
||||||
}, 150);
|
|
||||||
}
|
|
||||||
}, 250);
|
|
||||||
|
|
||||||
return makeResponse(QueueForSessionResult, {
|
|
||||||
code: 1
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
async function handleMessage(source, method, data) {
|
|
||||||
if (handlers[method]) {
|
|
||||||
return await handlers[method](source, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
onNet('__cfx_internal:pbRlScSession', async (data) => {
|
|
||||||
const s = source;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const message = RpcMessage.decode(new Uint8Array(data));
|
|
||||||
const response = await handleMessage(s, message.Header.MethodName, message.Content);
|
|
||||||
|
|
||||||
if (!response || !response.Header) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
response.Header.RequestId = message.Header.RequestId;
|
|
||||||
|
|
||||||
emitMsg(s, RpcResponseMessage.encode(response).finish());
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
console.log(e.stack);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
|
||||||
# yarn lockfile v1
|
|
||||||
|
|
||||||
|
|
||||||
"@citizenfx/[email protected]":
|
|
||||||
version "6.8.8"
|
|
||||||
resolved "https://registry.yarnpkg.com/@citizenfx/protobufjs/-/protobufjs-6.8.8.tgz#d12edcada06182f3785dea1d35eebf0ebe4fd2c3"
|
|
||||||
integrity sha512-RBJvHPWNwguEPxV+ALbCZBXAEQf2byP5KtYrYl36Mbb0+Ch7MxpOm+764S+YqYWj/A/iNSW3jXglfJ9hlxjBLA==
|
|
||||||
dependencies:
|
|
||||||
"@protobufjs/aspromise" "^1.1.2"
|
|
||||||
"@protobufjs/base64" "^1.1.2"
|
|
||||||
"@protobufjs/codegen" "^2.0.4"
|
|
||||||
"@protobufjs/eventemitter" "^1.1.0"
|
|
||||||
"@protobufjs/fetch" "^1.1.0"
|
|
||||||
"@protobufjs/float" "^1.0.2"
|
|
||||||
"@protobufjs/inquire" "^1.1.0"
|
|
||||||
"@protobufjs/path" "^1.1.2"
|
|
||||||
"@protobufjs/pool" "^1.1.0"
|
|
||||||
"@protobufjs/utf8" "^1.1.0"
|
|
||||||
"@types/long" "^4.0.0"
|
|
||||||
"@types/node" "^10.1.0"
|
|
||||||
long "^4.0.0"
|
|
||||||
|
|
||||||
"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
|
|
||||||
version "1.1.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
|
|
||||||
integrity sha1-m4sMxmPWaafY9vXQiToU00jzD78=
|
|
||||||
|
|
||||||
"@protobufjs/base64@^1.1.2":
|
|
||||||
version "1.1.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735"
|
|
||||||
integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==
|
|
||||||
|
|
||||||
"@protobufjs/codegen@^2.0.4":
|
|
||||||
version "2.0.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb"
|
|
||||||
integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==
|
|
||||||
|
|
||||||
"@protobufjs/eventemitter@^1.1.0":
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70"
|
|
||||||
integrity sha1-NVy8mLr61ZePntCV85diHx0Ga3A=
|
|
||||||
|
|
||||||
"@protobufjs/fetch@^1.1.0":
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45"
|
|
||||||
integrity sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=
|
|
||||||
dependencies:
|
|
||||||
"@protobufjs/aspromise" "^1.1.1"
|
|
||||||
"@protobufjs/inquire" "^1.1.0"
|
|
||||||
|
|
||||||
"@protobufjs/float@^1.0.2":
|
|
||||||
version "1.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1"
|
|
||||||
integrity sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=
|
|
||||||
|
|
||||||
"@protobufjs/inquire@^1.1.0":
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089"
|
|
||||||
integrity sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=
|
|
||||||
|
|
||||||
"@protobufjs/path@^1.1.2":
|
|
||||||
version "1.1.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d"
|
|
||||||
integrity sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=
|
|
||||||
|
|
||||||
"@protobufjs/pool@^1.1.0":
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54"
|
|
||||||
integrity sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=
|
|
||||||
|
|
||||||
"@protobufjs/utf8@^1.1.0":
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
|
|
||||||
integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=
|
|
||||||
|
|
||||||
"@types/long@^4.0.0":
|
|
||||||
version "4.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
|
|
||||||
integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==
|
|
||||||
|
|
||||||
"@types/node@^10.1.0":
|
|
||||||
version "10.17.58"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.58.tgz#10682f6016fd866725c36d22ce6bbbd029bf4545"
|
|
||||||
integrity sha512-Dn5RBxLohjdHFj17dVVw3rtrZAeXeWg+LQfvxDIW/fdPkSiuQk7h3frKMYtsQhtIW42wkErDcy9UMVxhGW4O7w==
|
|
||||||
|
|
||||||
long@^4.0.0:
|
|
||||||
version "4.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
|
|
||||||
integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
--This empty file causes the scheduler.lua to load clientside
|
|
||||||
--scheduler.lua when loaded inside the sessionmanager resource currently manages remote callbacks.
|
|
||||||
--Without this, callbacks will only work server->client and not client->server.
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
-- 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 <[email protected]>'
|
|
||||||
description 'Handles the "host lock" for non-OneSync servers. Do not disable.'
|
|
||||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
|
||||||
|
|
||||||
fx_version 'cerulean'
|
|
||||||
games { 'gta4', 'gta5' }
|
|
||||||
|
|
||||||
server_script 'server/host_lock.lua'
|
|
||||||
client_script 'client/empty.lua'
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
-- whitelist c2s events
|
|
||||||
RegisterServerEvent('hostingSession')
|
|
||||||
RegisterServerEvent('hostedSession')
|
|
||||||
|
|
||||||
-- event handler for pre-session 'acquire'
|
|
||||||
local currentHosting
|
|
||||||
local hostReleaseCallbacks = {}
|
|
||||||
|
|
||||||
-- TODO: add a timeout for the hosting lock to be held
|
|
||||||
-- TODO: add checks for 'fraudulent' conflict cases of hosting attempts (typically whenever the host can not be reached)
|
|
||||||
AddEventHandler('hostingSession', function()
|
|
||||||
-- if the lock is currently held, tell the client to await further instruction
|
|
||||||
if currentHosting then
|
|
||||||
TriggerClientEvent('sessionHostResult', source, 'wait')
|
|
||||||
|
|
||||||
-- register a callback for when the lock is freed
|
|
||||||
table.insert(hostReleaseCallbacks, function()
|
|
||||||
TriggerClientEvent('sessionHostResult', source, 'free')
|
|
||||||
end)
|
|
||||||
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- if the current host was last contacted less than a second ago
|
|
||||||
if GetHostId() then
|
|
||||||
if GetPlayerLastMsg(GetHostId()) < 1000 then
|
|
||||||
TriggerClientEvent('sessionHostResult', source, 'conflict')
|
|
||||||
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
hostReleaseCallbacks = {}
|
|
||||||
|
|
||||||
currentHosting = source
|
|
||||||
|
|
||||||
TriggerClientEvent('sessionHostResult', source, 'go')
|
|
||||||
|
|
||||||
-- set a timeout of 5 seconds
|
|
||||||
SetTimeout(5000, function()
|
|
||||||
if not currentHosting then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
currentHosting = nil
|
|
||||||
|
|
||||||
for _, cb in ipairs(hostReleaseCallbacks) do
|
|
||||||
cb()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
AddEventHandler('hostedSession', function()
|
|
||||||
-- check if the client is the original locker
|
|
||||||
if currentHosting ~= source then
|
|
||||||
-- TODO: drop client as they're clearly lying
|
|
||||||
print(currentHosting, '~=', source)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- free the host lock (call callbacks and remove the lock value)
|
|
||||||
for _, cb in ipairs(hostReleaseCallbacks) do
|
|
||||||
cb()
|
|
||||||
end
|
|
||||||
|
|
||||||
currentHosting = nil
|
|
||||||
end)
|
|
||||||
|
|
||||||
EnableEnhancedHostSupport(true)
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
-- 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 <[email protected]>'
|
|
||||||
description 'A compatibility resource to load basic-gamemode.'
|
|
||||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
|
||||||
|
|
||||||
-- compatibility wrapper
|
|
||||||
fx_version 'adamant'
|
|
||||||
game 'common'
|
|
||||||
|
|
||||||
dependency 'basic-gamemode'
|
|
||||||
Reference in New Issue
Block a user