mirror of
https://github.com/citizenfx/cfx-server-data.git
synced 2025-12-12 06:14:09 +01:00
reorganize resource directories
This commit is contained in:
@@ -1,10 +0,0 @@
|
||||
file 'style.css'
|
||||
file 'shadow.js'
|
||||
|
||||
chat_theme 'gtao' {
|
||||
styleSheet = 'style.css',
|
||||
script = 'shadow.js',
|
||||
msgTemplates = {
|
||||
default = '<b>{0}</b><span>{1}</span>'
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
(function() {
|
||||
var Filters = {}
|
||||
|
||||
var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
svg.setAttribute("style", "display:block;width:0px;height:0px");
|
||||
var defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
|
||||
|
||||
var blurFilter = document.createElementNS("http://www.w3.org/2000/svg", "filter");
|
||||
blurFilter.setAttribute("id", "svgBlurFilter");
|
||||
var feGaussianFilter = document.createElementNS("http://www.w3.org/2000/svg", "feGaussianBlur");
|
||||
feGaussianFilter.setAttribute("stdDeviation", "0 0");
|
||||
blurFilter.appendChild(feGaussianFilter);
|
||||
defs.appendChild(blurFilter);
|
||||
Filters._svgBlurFilter = feGaussianFilter;
|
||||
|
||||
// Drop Shadow Filter
|
||||
var dropShadowFilter = document.createElementNS("http://www.w3.org/2000/svg", "filter");
|
||||
dropShadowFilter.setAttribute("id", "svgDropShadowFilter");
|
||||
var feGaussianFilter = document.createElementNS("http://www.w3.org/2000/svg", "feGaussianBlur");
|
||||
feGaussianFilter.setAttribute("in", "SourceAlpha");
|
||||
feGaussianFilter.setAttribute("stdDeviation", "3");
|
||||
dropShadowFilter.appendChild(feGaussianFilter);
|
||||
Filters._svgDropshadowFilterBlur = feGaussianFilter;
|
||||
|
||||
var feOffset = document.createElementNS("http://www.w3.org/2000/svg", "feOffset");
|
||||
feOffset.setAttribute("dx", "0");
|
||||
feOffset.setAttribute("dy", "0");
|
||||
feOffset.setAttribute("result", "offsetblur");
|
||||
dropShadowFilter.appendChild(feOffset);
|
||||
Filters._svgDropshadowFilterOffset = feOffset;
|
||||
|
||||
var feFlood = document.createElementNS("http://www.w3.org/2000/svg", "feFlood");
|
||||
feFlood.setAttribute("flood-color", "rgba(0,0,0,1)");
|
||||
dropShadowFilter.appendChild(feFlood);
|
||||
Filters._svgDropshadowFilterFlood = feFlood;
|
||||
|
||||
var feComposite = document.createElementNS("http://www.w3.org/2000/svg", "feComposite");
|
||||
feComposite.setAttribute("in2", "offsetblur");
|
||||
feComposite.setAttribute("operator", "in");
|
||||
dropShadowFilter.appendChild(feComposite);
|
||||
var feComposite = document.createElementNS("http://www.w3.org/2000/svg", "feComposite");
|
||||
feComposite.setAttribute("in2", "SourceAlpha");
|
||||
feComposite.setAttribute("operator", "out");
|
||||
feComposite.setAttribute("result", "outer");
|
||||
dropShadowFilter.appendChild(feComposite);
|
||||
|
||||
var feMerge = document.createElementNS("http://www.w3.org/2000/svg", "feMerge");
|
||||
var feMergeNode = document.createElementNS("http://www.w3.org/2000/svg", "feMergeNode");
|
||||
feMerge.appendChild(feMergeNode);
|
||||
var feMergeNode = document.createElementNS("http://www.w3.org/2000/svg", "feMergeNode");
|
||||
feMerge.appendChild(feMergeNode);
|
||||
Filters._svgDropshadowMergeNode = feMergeNode;
|
||||
dropShadowFilter.appendChild(feMerge);
|
||||
defs.appendChild(dropShadowFilter);
|
||||
svg.appendChild(defs);
|
||||
document.documentElement.appendChild(svg);
|
||||
|
||||
const blurScale = 1;
|
||||
const scale = (document.body.clientWidth / 1280);
|
||||
|
||||
Filters._svgDropshadowFilterBlur.setAttribute("stdDeviation",
|
||||
1 * blurScale + " " +
|
||||
1 * blurScale
|
||||
);
|
||||
Filters._svgDropshadowFilterOffset.setAttribute("dx",
|
||||
String(Math.cos(45 * Math.PI / 180) * 1 * scale));
|
||||
Filters._svgDropshadowFilterOffset.setAttribute("dy",
|
||||
String(Math.sin(45 * Math.PI / 180) * 1 * scale));
|
||||
Filters._svgDropshadowFilterFlood.setAttribute("flood-color",
|
||||
'rgba(0, 0, 0, 1)');
|
||||
Filters._svgDropshadowMergeNode.setAttribute("in",
|
||||
"SourceGraphic");
|
||||
|
||||
})();
|
||||
@@ -1,116 +0,0 @@
|
||||
* {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.chat-window {
|
||||
--size: calc((((2.7vw / 1.77777) * 1.2)) * 6);
|
||||
|
||||
position: absolute;
|
||||
right: calc(1.56vw);
|
||||
top: calc(50% - (var(--size) / 2));
|
||||
height: var(--size) !important;
|
||||
|
||||
background: inherit !important;
|
||||
|
||||
text-align: right;
|
||||
|
||||
left: auto;
|
||||
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Font2';
|
||||
src: url(https://runtime.fivem.net/temp/ChaletLondonNineteenSixty.otf?a);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Font2_cond';
|
||||
src: url(https://runtime.fivem.net/temp/chaletcomprime-colognesixty-webfont.ttf?a);
|
||||
}
|
||||
|
||||
.msg {
|
||||
font-family: Font2, sans-serif;
|
||||
color: #fff;
|
||||
|
||||
font-size: calc(1.8vw / 1.77777); /* 13px in 720p, calc'd by width */
|
||||
filter: url(#svgDropShadowFilter);
|
||||
|
||||
line-height: calc((2.7vw / 1.77777) * 1.2);
|
||||
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.msg > span > span > b {
|
||||
font-family: Font2_cond, sans-serif;
|
||||
font-weight: normal;
|
||||
|
||||
vertical-align: baseline;
|
||||
padding-right: 11px;
|
||||
|
||||
line-height: 1;
|
||||
|
||||
font-size: calc(2.7vw / 1.77777); /* 13px in 720p, calc'd by width */
|
||||
}
|
||||
|
||||
.msg > span > span > span {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
.msg i:first-of-type {
|
||||
font-style: normal;
|
||||
color: #c0c0c0;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
position: absolute;
|
||||
right: calc(1.56vw);
|
||||
bottom: calc(1.56vw);
|
||||
|
||||
background: inherit !important;
|
||||
|
||||
text-align: right;
|
||||
|
||||
top: auto;
|
||||
left: auto;
|
||||
|
||||
height: auto;
|
||||
|
||||
font-family: Font2, sans-serif;
|
||||
}
|
||||
|
||||
.chat-input > div {
|
||||
background-color: rgba(0, 0, 0, .6);
|
||||
padding: calc(0.15625vw / 2);
|
||||
}
|
||||
|
||||
.chat-input .prefix {
|
||||
margin: 0;
|
||||
margin-left: 0.7%;
|
||||
margin-top: -0.1%;
|
||||
}
|
||||
|
||||
.chat-input > div + div {
|
||||
position: absolute;
|
||||
bottom: calc(1.65vh + 0.15625vw + 0.15625vw + 0.15625vw + (0.15625vw / 2));
|
||||
width: 99.6%;
|
||||
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.suggestions {
|
||||
border: calc(0.15625vw / 2) solid rgba(180, 180, 180, .6);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
textarea {
|
||||
background: transparent;
|
||||
border: calc(0.15625vw / 2) solid rgba(180, 180, 180, .6);
|
||||
padding: calc(0.15625vw / 2);
|
||||
padding-left: calc(3.5% + (0.15625vw / 2));
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
# Chat
|
||||
@@ -1,26 +0,0 @@
|
||||
description 'chat management stuff'
|
||||
|
||||
ui_page 'html/index.html'
|
||||
|
||||
client_script 'cl_chat.lua'
|
||||
server_script 'sv_chat.lua'
|
||||
|
||||
files {
|
||||
'html/index.html',
|
||||
'html/index.css',
|
||||
'html/config.default.js',
|
||||
'html/config.js',
|
||||
'html/App.js',
|
||||
'html/Message.js',
|
||||
'html/Suggestions.js',
|
||||
'html/vendor/vue.2.3.3.min.js',
|
||||
'html/vendor/flexboxgrid.6.3.1.min.css',
|
||||
'html/vendor/animate.3.5.2.min.css',
|
||||
'html/vendor/latofonts.css',
|
||||
'html/vendor/fonts/LatoRegular.woff2',
|
||||
'html/vendor/fonts/LatoRegular2.woff2',
|
||||
'html/vendor/fonts/LatoLight2.woff2',
|
||||
'html/vendor/fonts/LatoLight.woff2',
|
||||
'html/vendor/fonts/LatoBold.woff2',
|
||||
'html/vendor/fonts/LatoBold2.woff2',
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
local chatInputActive = false
|
||||
local chatInputActivating = false
|
||||
local chatHidden = true
|
||||
local chatLoaded = false
|
||||
|
||||
RegisterNetEvent('chatMessage')
|
||||
RegisterNetEvent('chat:addTemplate')
|
||||
RegisterNetEvent('chat:addMessage')
|
||||
RegisterNetEvent('chat:addSuggestion')
|
||||
RegisterNetEvent('chat:addSuggestions')
|
||||
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 }
|
||||
}
|
||||
})
|
||||
end)
|
||||
|
||||
AddEventHandler('chat:addMessage', function(message)
|
||||
SendNUIMessage({
|
||||
type = 'ON_MESSAGE',
|
||||
message = message
|
||||
})
|
||||
end)
|
||||
|
||||
AddEventHandler('chat:addSuggestion', function(name, help, params)
|
||||
SendNUIMessage({
|
||||
type = 'ON_SUGGESTION_ADD',
|
||||
suggestion = {
|
||||
name = name,
|
||||
help = help,
|
||||
params = params or nil
|
||||
}
|
||||
})
|
||||
end)
|
||||
|
||||
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: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)
|
||||
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)) 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)
|
||||
|
||||
Citizen.CreateThread(function()
|
||||
SetTextChatEnabled(false)
|
||||
SetNuiFocus(false)
|
||||
|
||||
while true do
|
||||
Wait(0)
|
||||
|
||||
if not chatInputActive then
|
||||
if IsControlPressed(0, 245) --[[ INPUT_MP_TEXT_CHAT_ALL ]] then
|
||||
chatInputActive = true
|
||||
chatInputActivating = true
|
||||
|
||||
SendNUIMessage({
|
||||
type = 'ON_OPEN'
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
if chatInputActivating then
|
||||
if not IsControlPressed(0, 245) then
|
||||
SetNuiFocus(true)
|
||||
|
||||
chatInputActivating = false
|
||||
end
|
||||
end
|
||||
|
||||
if chatLoaded then
|
||||
local shouldBeHidden = false
|
||||
|
||||
if IsScreenFadedOut() or IsPauseMenuActive() then
|
||||
shouldBeHidden = true
|
||||
end
|
||||
|
||||
if (shouldBeHidden and not chatHidden) or (not shouldBeHidden and chatHidden) then
|
||||
chatHidden = shouldBeHidden
|
||||
|
||||
SendNUIMessage({
|
||||
type = 'ON_SCREEN_STATE_CHANGE',
|
||||
shouldHide = shouldBeHidden
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
@@ -1,255 +0,0 @@
|
||||
window.APP = {
|
||||
template: '#app_template',
|
||||
name: 'app',
|
||||
data() {
|
||||
return {
|
||||
style: CONFIG.style,
|
||||
showInput: false,
|
||||
showWindow: false,
|
||||
shouldHide: true,
|
||||
backingSuggestions: [],
|
||||
removedSuggestions: [],
|
||||
templates: CONFIG.templates,
|
||||
message: '',
|
||||
messages: [],
|
||||
oldMessages: [],
|
||||
oldMessagesIndex: -1,
|
||||
tplBackups: [],
|
||||
msgTplBackups: []
|
||||
};
|
||||
},
|
||||
destroyed() {
|
||||
clearInterval(this.focusTimer);
|
||||
window.removeEventListener('message', this.listener);
|
||||
},
|
||||
mounted() {
|
||||
post('http://chat/loaded', JSON.stringify({}));
|
||||
this.listener = window.addEventListener('message', (event) => {
|
||||
const item = event.data || event.detail; //'detail' is for debuging via browsers
|
||||
if (this[item.type]) {
|
||||
this[item.type](item);
|
||||
}
|
||||
});
|
||||
},
|
||||
watch: {
|
||||
messages() {
|
||||
if (this.showWindowTimer) {
|
||||
clearTimeout(this.showWindowTimer);
|
||||
}
|
||||
this.showWindow = true;
|
||||
this.resetShowWindowTimer();
|
||||
|
||||
const messagesObj = this.$refs.messages;
|
||||
this.$nextTick(() => {
|
||||
messagesObj.scrollTop = messagesObj.scrollHeight;
|
||||
});
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
suggestions() {
|
||||
return this.backingSuggestions.filter((el) => this.removedSuggestions.indexOf(el.name) <= -1);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
ON_SCREEN_STATE_CHANGE({ shouldHide }) {
|
||||
this.shouldHide = shouldHide;
|
||||
},
|
||||
ON_OPEN() {
|
||||
this.showInput = true;
|
||||
this.showWindow = true;
|
||||
if (this.showWindowTimer) {
|
||||
clearTimeout(this.showWindowTimer);
|
||||
}
|
||||
this.focusTimer = setInterval(() => {
|
||||
if (this.$refs.input) {
|
||||
this.$refs.input.focus();
|
||||
} else {
|
||||
clearInterval(this.focusTimer);
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
ON_MESSAGE({ message }) {
|
||||
this.messages.push(message);
|
||||
},
|
||||
ON_CLEAR() {
|
||||
this.messages = [];
|
||||
this.oldMessages = [];
|
||||
this.oldMessagesIndex = -1;
|
||||
},
|
||||
ON_SUGGESTION_ADD({ suggestion }) {
|
||||
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 }) {
|
||||
if(this.removedSuggestions.indexOf(name) <= -1) {
|
||||
this.removedSuggestions.push(name);
|
||||
}
|
||||
},
|
||||
ON_TEMPLATE_ADD({ template }) {
|
||||
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 }) {
|
||||
this.removeThemes();
|
||||
|
||||
this.setThemes(themes);
|
||||
},
|
||||
removeThemes() {
|
||||
for (let i = 0; i < document.styleSheets.length; i++) {
|
||||
const styleSheet = document.styleSheets[i];
|
||||
const node = styleSheet.ownerNode;
|
||||
|
||||
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) {
|
||||
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) {
|
||||
this.messages.push({
|
||||
args: [msg],
|
||||
template: '^3<b>CHAT-WARN</b>: ^0{0}',
|
||||
});
|
||||
},
|
||||
clearShowWindowTimer() {
|
||||
clearTimeout(this.showWindowTimer);
|
||||
},
|
||||
resetShowWindowTimer() {
|
||||
this.clearShowWindowTimer();
|
||||
this.showWindowTimer = setTimeout(() => {
|
||||
if (!this.showInput) {
|
||||
this.showWindow = false;
|
||||
}
|
||||
}, CONFIG.fadeTimeout);
|
||||
},
|
||||
keyUp() {
|
||||
this.resize();
|
||||
},
|
||||
keyDown(e) {
|
||||
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;
|
||||
}
|
||||
},
|
||||
moveOldMessageIndex(up) {
|
||||
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;
|
||||
input.style.height = '5px';
|
||||
input.style.height = `${input.scrollHeight + 2}px`;
|
||||
},
|
||||
send(e) {
|
||||
if(this.message !== '') {
|
||||
post('http://chat/chatResult', JSON.stringify({
|
||||
message: this.message,
|
||||
}));
|
||||
this.oldMessages.unshift(this.message);
|
||||
this.oldMessagesIndex = -1;
|
||||
this.hideInput();
|
||||
} else {
|
||||
this.hideInput(true);
|
||||
}
|
||||
},
|
||||
hideInput(canceled = false) {
|
||||
if (canceled) {
|
||||
post('http://chat/chatResult', JSON.stringify({ canceled }));
|
||||
}
|
||||
this.message = '';
|
||||
this.showInput = false;
|
||||
clearInterval(this.focusTimer);
|
||||
this.resetShowWindowTimer();
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,86 +0,0 @@
|
||||
Vue.component('message', {
|
||||
template: '#message_template',
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
textEscaped() {
|
||||
let s = this.template ? this.template : this.templates[this.templateId];
|
||||
|
||||
if (this.template) {
|
||||
//We disable templateId since we are using a direct raw template
|
||||
this.templateId = -1;
|
||||
}
|
||||
|
||||
//This hack is required to preserve backwards compatability
|
||||
if (this.templateId == CONFIG.defaultTemplateId
|
||||
&& this.args.length == 1) {
|
||||
s = this.templates[CONFIG.defaultAltTemplateId] //Swap out default template :/
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
return this.colorize(s);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
colorizeOld(str) {
|
||||
return `<span style="color: rgb(${this.color[0]}, ${this.color[1]}, ${this.color[2]})">${str}</span>`
|
||||
},
|
||||
colorize(str) {
|
||||
let s = "<span>" + (str.replace(/\^([0-9])/g, (str, color) => `</span><span class="color-${color}">`)) + "</span>";
|
||||
|
||||
const styleDict = {
|
||||
'*': '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, '');
|
||||
},
|
||||
escape(unsafe) {
|
||||
return String(unsafe)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
},
|
||||
},
|
||||
props: {
|
||||
templates: {
|
||||
type: Object,
|
||||
},
|
||||
args: {
|
||||
type: Array,
|
||||
},
|
||||
template: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
templateId: {
|
||||
type: String,
|
||||
default: CONFIG.defaultTemplateId,
|
||||
},
|
||||
multiline: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
color: { //deprecated
|
||||
type: Array,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
Vue.component('suggestions', {
|
||||
template: '#suggestions_template',
|
||||
props: ['message', 'suggestions'],
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
currentSuggestions() {
|
||||
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
|
||||
p.disabled = this.message.match(regex) == null;
|
||||
});
|
||||
});
|
||||
return currentSuggestions;
|
||||
},
|
||||
},
|
||||
methods: {},
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
// DO NOT EDIT THIS FILE
|
||||
// Copy it to `config.js` and edit it
|
||||
window.CONFIG = {
|
||||
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: '38%',
|
||||
height: '22%',
|
||||
}
|
||||
};
|
||||
@@ -1,127 +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;}
|
||||
|
||||
* {
|
||||
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;
|
||||
}
|
||||
|
||||
.prefix {
|
||||
font-size: 1.8vh;
|
||||
position: absolute;
|
||||
margin-top: 0.5%;
|
||||
left: 0.208%;
|
||||
}
|
||||
|
||||
textarea {
|
||||
font-size: 1.65vh;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
padding: 1%;
|
||||
padding-left: 3.5%;
|
||||
color: white;
|
||||
background-color: rgba(44, 62, 80, 1.0);
|
||||
width: 100%;
|
||||
border-width: 0;
|
||||
height: 3.15%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
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 {
|
||||
display: none;
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title></title>
|
||||
<link href="vendor/latofonts.css" rel="stylesheet">
|
||||
<link href="vendor/flexboxgrid.6.3.1.min.css" rel="stylesheet"></link>
|
||||
<link href="vendor/animate.3.5.2.min.css" rel="stylesheet"></link>
|
||||
<link href="index.css" rel="stylesheet"></link>
|
||||
|
||||
<script src="vendor/vue.2.3.3.min.js" type="text/javascript"></script>
|
||||
<script src="config.default.js" type="text/javascript"></script>
|
||||
<script src="config.js" type="text/javascript"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<!-- App Template -->
|
||||
<script type="text/x-template" id="app_template">
|
||||
<div id="app">
|
||||
<div class="chat-window" :style="this.style" :class="{ 'fadeOut animated': !showWindow, 'hidden': shouldHide }">
|
||||
<div class="chat-messages" ref="messages">
|
||||
<message v-for="msg in messages"
|
||||
:templates="templates"
|
||||
:multiline="msg.multiline"
|
||||
:args="msg.args"
|
||||
:color="msg.color"
|
||||
:template="msg.template"
|
||||
:template-id="msg.templateId"
|
||||
:key="msg">
|
||||
</message>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-input" v-show="showInput">
|
||||
<div>
|
||||
<span class="prefix">➤</span>
|
||||
<textarea v-model="message"
|
||||
ref="input"
|
||||
type="text"
|
||||
autofocus
|
||||
spellcheck="false"
|
||||
@keyup.esc="hideInput"
|
||||
@keyup="keyUp"
|
||||
@keydown="keyDown"
|
||||
@keypress.enter.prevent="send">
|
||||
</textarea>
|
||||
</div>
|
||||
<suggestions :message="message" :suggestions="suggestions">
|
||||
</suggestions>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<!-- Message Template -->
|
||||
<script type="text/x-template" id="message_template">
|
||||
<div class="msg" :class="{ multiline }">
|
||||
<span v-html="textEscaped"></span>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<!-- Suggestions Template -->
|
||||
<script type="text/x-template" id="suggestions_template">
|
||||
<div class="suggestions-wrap" v-show="currentSuggestions.length > 0">
|
||||
<ul class="suggestions">
|
||||
<li class="suggestion" v-for="s in currentSuggestions">
|
||||
<p>
|
||||
<span :class="{ 'disabled': s.disabled }">
|
||||
{{s.name}}
|
||||
</span>
|
||||
<span class="param"
|
||||
v-for="(p, index) in s.params"
|
||||
:class="{ 'disabled': p.disabled }">
|
||||
[{{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>
|
||||
</script>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script type="text/javascript" src="./Suggestions.js"></script>
|
||||
<script type="text/javascript" src="./Message.js"></script>
|
||||
<script type="text/javascript" src="./App.js"></script>
|
||||
|
||||
<!-- Main Entry -->
|
||||
<script type="text/javascript">
|
||||
window.post = (url, data) => {
|
||||
var request = new XMLHttpRequest();
|
||||
request.open('POST', url, true);
|
||||
request.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
request.send(data);
|
||||
}
|
||||
|
||||
const instance = new Vue({
|
||||
el: '#app',
|
||||
render: h => h(APP),
|
||||
});
|
||||
|
||||
window.emulate = (type, detail = {}) => {
|
||||
detail.type = type;
|
||||
window.dispatchEvent(new CustomEvent('message', {
|
||||
detail,
|
||||
}));
|
||||
};
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
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;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,79 +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')
|
||||
|
||||
AddEventHandler('_chat:messageEntered', function(author, color, message)
|
||||
if not message or not author then
|
||||
return
|
||||
end
|
||||
|
||||
TriggerEvent('chatMessage', source, author, message)
|
||||
|
||||
if not WasEventCanceled() then
|
||||
TriggerClientEvent('chatMessage', -1, author, { 255, 255, 255 }, message)
|
||||
end
|
||||
|
||||
print(author .. '^7: ' .. message .. '^7')
|
||||
end)
|
||||
|
||||
AddEventHandler('__cfx_internal:commandFallback', function(command)
|
||||
local name = GetPlayerName(source)
|
||||
|
||||
TriggerEvent('chatMessage', source, name, '/' .. command)
|
||||
|
||||
if not WasEventCanceled() then
|
||||
TriggerClientEvent('chatMessage', -1, name, { 255, 255, 255 }, '/' .. command)
|
||||
end
|
||||
|
||||
CancelEvent()
|
||||
end)
|
||||
|
||||
-- player join messages
|
||||
AddEventHandler('chat:init', function()
|
||||
TriggerClientEvent('chatMessage', -1, '', { 255, 255, 255 }, '^2* ' .. GetPlayerName(source) .. ' joined.')
|
||||
end)
|
||||
|
||||
AddEventHandler('playerDropped', function(reason)
|
||||
TriggerClientEvent('chatMessage', -1, '', { 255, 255, 255 }, '^2* ' .. GetPlayerName(source) ..' left (' .. reason .. ')')
|
||||
end)
|
||||
|
||||
RegisterCommand('say', function(source, args, rawCommand)
|
||||
TriggerClientEvent('chatMessage', -1, (source == 0) and 'console' or GetPlayerName(source), { 255, 255, 255 }, rawCommand:sub(5))
|
||||
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()
|
||||
refreshCommands(source)
|
||||
end)
|
||||
|
||||
AddEventHandler('onServerResourceStart', function(resName)
|
||||
Wait(500)
|
||||
|
||||
for _, player in ipairs(GetPlayers()) do
|
||||
refreshCommands(player)
|
||||
end
|
||||
end)
|
||||
1
resources/[system]/runcode/.gitignore
vendored
Normal file
1
resources/[system]/runcode/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
data.json
|
||||
17
resources/[system]/runcode/__resource.lua
Normal file
17
resources/[system]/runcode/__resource.lua
Normal file
@@ -0,0 +1,17 @@
|
||||
client_script 'runcode_cl.lua'
|
||||
server_script 'runcode_sv.lua'
|
||||
server_script 'runcode_web.lua'
|
||||
|
||||
shared_script 'runcode_shared.lua'
|
||||
|
||||
shared_script 'runcode.js'
|
||||
|
||||
resource_manifest_version '44febabe-d386-4d18-afbe-5e627f4af937'
|
||||
|
||||
client_script 'runcode_ui.lua'
|
||||
|
||||
ui_page 'web/nui.html'
|
||||
|
||||
files {
|
||||
'web/nui.html'
|
||||
}
|
||||
11
resources/[system]/runcode/runcode.js
Normal file
11
resources/[system]/runcode/runcode.js
Normal file
@@ -0,0 +1,11 @@
|
||||
exports('runJS', (snippet) => {
|
||||
if (IsDuplicityVersion() && GetInvokingResource() !== GetCurrentResourceName()) {
|
||||
return [ 'Invalid caller.', false ];
|
||||
}
|
||||
|
||||
try {
|
||||
return [ new Function(snippet)(), false ];
|
||||
} catch (e) {
|
||||
return [ false, e.toString() ];
|
||||
}
|
||||
});
|
||||
15
resources/[system]/runcode/runcode_cl.lua
Normal file
15
resources/[system]/runcode/runcode_cl.lua
Normal file
@@ -0,0 +1,15 @@
|
||||
RegisterNetEvent('runcode:gotSnippet')
|
||||
|
||||
AddEventHandler('runcode:gotSnippet', function(id, lang, code)
|
||||
local res, err = RunCode(lang, code)
|
||||
|
||||
if not err then
|
||||
if type(res) == 'vector3' then
|
||||
res = json.encode({ table.unpack(res) })
|
||||
elseif type(res) == 'table' then
|
||||
res = json.encode(res)
|
||||
end
|
||||
end
|
||||
|
||||
TriggerServerEvent('runcode:gotResult', id, res, err)
|
||||
end)
|
||||
32
resources/[system]/runcode/runcode_shared.lua
Normal file
32
resources/[system]/runcode/runcode_shared.lua
Normal file
@@ -0,0 +1,32 @@
|
||||
local runners = {}
|
||||
|
||||
function runners.lua(arg)
|
||||
local code, err = load('return ' .. arg, '@runcode')
|
||||
|
||||
-- if failed, try without return
|
||||
if err then
|
||||
code, err = load(arg, '@runcode')
|
||||
end
|
||||
|
||||
if err then
|
||||
print(err)
|
||||
return nil, err
|
||||
end
|
||||
|
||||
local status, result = pcall(code)
|
||||
print(result)
|
||||
|
||||
if status then
|
||||
return result
|
||||
end
|
||||
|
||||
return nil, result
|
||||
end
|
||||
|
||||
function runners.js(arg)
|
||||
return table.unpack(exports[GetCurrentResourceName()]:runJS(arg))
|
||||
end
|
||||
|
||||
function RunCode(lang, str)
|
||||
return runners[lang](str)
|
||||
end
|
||||
42
resources/[system]/runcode/runcode_sv.lua
Normal file
42
resources/[system]/runcode/runcode_sv.lua
Normal file
@@ -0,0 +1,42 @@
|
||||
function GetPrivs(source)
|
||||
return {
|
||||
canServer = IsPlayerAceAllowed(source, 'command.run'),
|
||||
canClient = IsPlayerAceAllowed(source, 'command.crun'),
|
||||
canSelf = IsPlayerAceAllowed(source, 'runcode.self'),
|
||||
}
|
||||
end
|
||||
|
||||
RegisterCommand('run', function(source, args, rawCommand)
|
||||
local res, err = RunCode('lua', rawCommand:sub(4))
|
||||
end, true)
|
||||
|
||||
RegisterCommand('crun', function(source, args, rawCommand)
|
||||
if not source then
|
||||
return
|
||||
end
|
||||
|
||||
TriggerClientEvent('runcode:gotSnippet', source, -1, 'lua', rawCommand:sub(5))
|
||||
end, true)
|
||||
|
||||
RegisterCommand('runcode', function(source, args, rawCommand)
|
||||
if not source then
|
||||
return
|
||||
end
|
||||
|
||||
local df = LoadResourceFile(GetCurrentResourceName(), 'data.json')
|
||||
local saveData = {}
|
||||
|
||||
if df then
|
||||
saveData = json.decode(df)
|
||||
end
|
||||
|
||||
local p = GetPrivs(source)
|
||||
|
||||
if not p.canServer and not p.canClient and not p.canSelf then
|
||||
return
|
||||
end
|
||||
|
||||
p.saveData = saveData
|
||||
|
||||
TriggerClientEvent('runcode:openUi', source, p)
|
||||
end, true)
|
||||
66
resources/[system]/runcode/runcode_ui.lua
Normal file
66
resources/[system]/runcode/runcode_ui.lua
Normal file
@@ -0,0 +1,66 @@
|
||||
local openData
|
||||
|
||||
RegisterNetEvent('runcode:openUi')
|
||||
|
||||
AddEventHandler('runcode:openUi', function(options)
|
||||
openData = {
|
||||
type = 'open',
|
||||
options = options,
|
||||
url = 'http://' .. GetCurrentServerEndpoint() .. '/' .. GetCurrentResourceName() .. '/',
|
||||
res = GetCurrentResourceName()
|
||||
}
|
||||
|
||||
SendNuiMessage(json.encode(openData))
|
||||
end)
|
||||
|
||||
RegisterNUICallback('getOpenData', function(args, cb)
|
||||
cb(openData)
|
||||
end)
|
||||
|
||||
RegisterNUICallback('doOk', function(args, cb)
|
||||
SendNuiMessage(json.encode({
|
||||
type = 'ok'
|
||||
}))
|
||||
|
||||
SetNuiFocus(true, true)
|
||||
|
||||
cb('ok')
|
||||
end)
|
||||
|
||||
RegisterNUICallback('doClose', function(args, cb)
|
||||
SendNuiMessage(json.encode({
|
||||
type = 'close'
|
||||
}))
|
||||
|
||||
SetNuiFocus(false, false)
|
||||
|
||||
cb('ok')
|
||||
end)
|
||||
|
||||
local rcCbs = {}
|
||||
local id = 1
|
||||
|
||||
RegisterNUICallback('runCodeInBand', function(args, cb)
|
||||
id = id + 1
|
||||
|
||||
rcCbs[id] = cb
|
||||
|
||||
TriggerServerEvent('runcode:runInBand', id, args)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('runcode:inBandResult')
|
||||
|
||||
AddEventHandler('runcode:inBandResult', function(id, result)
|
||||
if rcCbs[id] then
|
||||
local cb = rcCbs[id]
|
||||
rcCbs[id] = nil
|
||||
|
||||
cb(result)
|
||||
end
|
||||
end)
|
||||
|
||||
AddEventHandler('onResourceStop', function(resourceName)
|
||||
if resourceName == GetCurrentResourceName() then
|
||||
SetNuiFocus(false, false)
|
||||
end
|
||||
end)
|
||||
192
resources/[system]/runcode/runcode_web.lua
Normal file
192
resources/[system]/runcode/runcode_web.lua
Normal file
@@ -0,0 +1,192 @@
|
||||
local cachedFiles = {}
|
||||
|
||||
local function sendFile(res, fileName)
|
||||
if cachedFiles[fileName] then
|
||||
res.send(cachedFiles[fileName])
|
||||
return
|
||||
end
|
||||
|
||||
local fileData = LoadResourceFile(GetCurrentResourceName(), 'web/' .. fileName)
|
||||
|
||||
if not fileData then
|
||||
res.writeHead(404)
|
||||
res.send('Not found.')
|
||||
return
|
||||
end
|
||||
|
||||
cachedFiles[fileName] = fileData
|
||||
res.send(fileData)
|
||||
end
|
||||
|
||||
local codeId = 1
|
||||
local codes = {}
|
||||
|
||||
local attempts = 0
|
||||
local lastAttempt
|
||||
|
||||
local function handleRunCode(data, res)
|
||||
if not data.lang then
|
||||
data.lang = 'lua'
|
||||
end
|
||||
|
||||
if not data.client or data.client == '' then
|
||||
CreateThread(function()
|
||||
local result, err = RunCode(data.lang, data.code)
|
||||
|
||||
res.send(json.encode({
|
||||
result = result,
|
||||
error = err
|
||||
}))
|
||||
end)
|
||||
else
|
||||
codes[codeId] = {
|
||||
timeout = GetGameTimer() + 1000,
|
||||
res = res
|
||||
}
|
||||
|
||||
TriggerClientEvent('runcode:gotSnippet', tonumber(data.client), codeId, data.lang, data.code)
|
||||
|
||||
codeId = codeId + 1
|
||||
end
|
||||
end
|
||||
|
||||
RegisterNetEvent('runcode:runInBand')
|
||||
|
||||
AddEventHandler('runcode:runInBand', function(id, data)
|
||||
local s = source
|
||||
local privs = GetPrivs(s)
|
||||
|
||||
local res = {
|
||||
send = function(str)
|
||||
TriggerClientEvent('runcode:inBandResult', s, id, str)
|
||||
end
|
||||
}
|
||||
|
||||
if (not data.client or data.client == '') and not privs.canServer then
|
||||
res.send(json.encode({ error = 'Insufficient permissions.'}))
|
||||
return
|
||||
end
|
||||
|
||||
if (data.client and data.client ~= '') and not privs.canClient then
|
||||
if privs.canSelf then
|
||||
data.client = s
|
||||
else
|
||||
res.send(json.encode({ error = 'Insufficient permissions.'}))
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
SaveResourceFile(GetCurrentResourceName(), 'data.json', json.encode({
|
||||
lastSnippet = data.code,
|
||||
lastLang = data.lang or 'lua'
|
||||
}), -1)
|
||||
|
||||
handleRunCode(data, res)
|
||||
end)
|
||||
|
||||
local function handlePost(req, res)
|
||||
req.setDataHandler(function(body)
|
||||
local data = json.decode(body)
|
||||
|
||||
if not data or not data.password or not data.code then
|
||||
res.send(json.encode({ error = 'Bad request.'}))
|
||||
return
|
||||
end
|
||||
|
||||
if GetConvar('rcon_password', '') == '' then
|
||||
res.send(json.encode({ error = 'The server has an empty rcon_password.'}))
|
||||
return
|
||||
end
|
||||
|
||||
if attempts > 5 or data.password ~= GetConvar('rcon_password', '') then
|
||||
attempts = attempts + 1
|
||||
lastAttempt = GetGameTimer()
|
||||
|
||||
res.send(json.encode({ error = 'Bad password.'}))
|
||||
return
|
||||
end
|
||||
|
||||
handleRunCode(data, res)
|
||||
end)
|
||||
end
|
||||
|
||||
CreateThread(function()
|
||||
while true do
|
||||
Wait(1000)
|
||||
|
||||
if attempts > 0 and (GetGameTimer() - lastAttempt) > 5000 then
|
||||
attempts = 0
|
||||
lastAttempt = 0
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
local function returnCode(id, res, err)
|
||||
if not codes[id] then
|
||||
return
|
||||
end
|
||||
|
||||
local code = codes[id]
|
||||
codes[id] = nil
|
||||
|
||||
local gotFrom
|
||||
|
||||
if source then
|
||||
gotFrom = GetPlayerName(source) .. ' [' .. tostring(source) .. ']'
|
||||
end
|
||||
|
||||
code.res.send(json.encode({
|
||||
result = res,
|
||||
error = err,
|
||||
from = gotFrom
|
||||
}))
|
||||
end
|
||||
|
||||
CreateThread(function()
|
||||
while true do
|
||||
Wait(100)
|
||||
|
||||
for k, v in ipairs(codes) do
|
||||
if GetGameTimer() > v.timeout then
|
||||
source = nil
|
||||
returnCode(k, '', 'Timed out waiting on the target client.')
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterNetEvent('runcode:gotResult')
|
||||
AddEventHandler('runcode:gotResult', returnCode)
|
||||
|
||||
SetHttpHandler(function(req, res)
|
||||
local path = req.path
|
||||
|
||||
if req.method == 'POST' then
|
||||
return handlePost(req, res)
|
||||
end
|
||||
|
||||
-- client shortcuts
|
||||
if req.path == '/clients' then
|
||||
local clientList = {}
|
||||
|
||||
for _, id in ipairs(GetPlayers()) do
|
||||
table.insert(clientList, { GetPlayerName(id), id })
|
||||
end
|
||||
|
||||
res.send(json.encode({
|
||||
clients = clientList
|
||||
}))
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
-- should this be the index?
|
||||
if req.path == '/' then
|
||||
path = 'index.html'
|
||||
end
|
||||
|
||||
-- remove any '..' from the path
|
||||
path = path:gsub("%.%.", "")
|
||||
|
||||
return sendFile(res, path)
|
||||
end)
|
||||
486
resources/[system]/runcode/web/index.html
Normal file
486
resources/[system]/runcode/web/index.html
Normal file
@@ -0,0 +1,486 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>fivem runcode</title>
|
||||
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulmaswatch/0.7.2/cyborg/bulmaswatch.min.css">
|
||||
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
|
||||
|
||||
<style type="text/css">
|
||||
body {
|
||||
font-family: "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
z-index: inherit;
|
||||
}
|
||||
|
||||
html.in-nui {
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
|
||||
margin-top: 5vh;
|
||||
margin-left: 7.5vw;
|
||||
margin-right: 7.5vw;
|
||||
margin-bottom: 5vh;
|
||||
|
||||
height: calc(100% - 10vh);
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
html.in-nui body > div.bg {
|
||||
background-color: rgba(0, 0, 0, 1);
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
z-index: -999;
|
||||
|
||||
box-shadow: 0 22px 70px 4px rgba(0, 0, 0, 0.56);
|
||||
}
|
||||
|
||||
span.nui-edition {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html.in-nui span.nui-edition {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
#close {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html.in-nui #close {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#result {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg">
|
||||
|
||||
</div>
|
||||
|
||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="/runcode">
|
||||
<strong>runcode</strong> <span class="nui-edition"> in-game</span>
|
||||
</a>
|
||||
|
||||
<a role="button" class="navbar-burger burger" aria-label="menu" aria-expanded="false" data-target="navbarMain">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="navbarMain" class="navbar-menu">
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item">
|
||||
<div class="field" id="cl-field">
|
||||
<div class="control has-icons-left">
|
||||
<div class="select">
|
||||
<select id="cl-select">
|
||||
</select>
|
||||
</div>
|
||||
<div class="icon is-small is-left">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="navbar-item">
|
||||
<div class="field has-addons" id="lang-toggle">
|
||||
<p class="control">
|
||||
<button class="button" id="lua-button">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-moon"></i>
|
||||
</span>
|
||||
<span>Lua</span>
|
||||
</button>
|
||||
</p>
|
||||
<p class="control">
|
||||
<button class="button" id="js-button">
|
||||
<span class="icon is-small">
|
||||
<i class="fab fa-js"></i>
|
||||
</span>
|
||||
<span>JS</span>
|
||||
</button>
|
||||
</p>
|
||||
<!-- TODO pending add-on resource that'll contain webpack'd compiler
|
||||
<p class="control">
|
||||
<button class="button" id="ts-button">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-code"></i>
|
||||
</span>
|
||||
<span>TS</span>
|
||||
</button>
|
||||
</p>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="navbar-item">
|
||||
<div class="field has-addons" id="cl-sv-toggle">
|
||||
<p class="control">
|
||||
<button class="button" id="cl-button">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-user-friends"></i>
|
||||
</span>
|
||||
<span>Client</span>
|
||||
</button>
|
||||
</p>
|
||||
<p class="control">
|
||||
<button class="button" id="sv-button">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-server"></i>
|
||||
</span>
|
||||
<span>Server</span>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="navbar-item" id="close">
|
||||
<button class="button is-danger">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div id="code-container" style="width:100%;height:60vh;border:1px solid grey"></div><br>
|
||||
<div class="field" id="passwordField">
|
||||
<p class="control has-icons-left">
|
||||
<input class="input" type="password" id="password" placeholder="RCon Password">
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fas fa-lock"></i>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<button class="button is-primary" id="run">Run</button>
|
||||
<div id="result">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!--
|
||||
to use a local deployment, uncomment; do note currently the server isn't optimized to serve >1MB files
|
||||
<script src="monaco-editor/vs/loader.js"></script>
|
||||
-->
|
||||
|
||||
<script src="https://unpkg.com/monaco-editor@0.18.1/min/vs/loader.js"></script>
|
||||
|
||||
<script>
|
||||
function fetchClients() {
|
||||
fetch('/runcode/clients').then(res => res.json()).then(res => {
|
||||
const el = document.querySelector('#cl-select');
|
||||
|
||||
const clients = res.clients;
|
||||
const realClients = [['All', '-1'], ...clients];
|
||||
|
||||
const createdClients = new Set([...el.querySelectorAll('option').entries()].map(([i, el]) => el.value));
|
||||
const existentClients = new Set(realClients.map(([ name, id ]) => id));
|
||||
|
||||
const toRemove = [...createdClients].filter(a => !existentClients.has(a));
|
||||
|
||||
for (const [name, id] of realClients) {
|
||||
const ex = el.querySelector(`option[value="${id}"]`);
|
||||
|
||||
if (!ex) {
|
||||
const l = document.createElement('option');
|
||||
l.setAttribute('value', id);
|
||||
l.appendChild(document.createTextNode(name));
|
||||
|
||||
el.appendChild(l);
|
||||
}
|
||||
}
|
||||
|
||||
for (const id of toRemove) {
|
||||
const l = el.querySelector(`option[value="${id}"]`);
|
||||
|
||||
if (l) {
|
||||
el.removeChild(l);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let useClient = false;
|
||||
let editServerCb = null;
|
||||
|
||||
[['#cl-button', true], ['#sv-button', false]].forEach(([ selector, isClient ]) => {
|
||||
const eh = () => {
|
||||
if (isClient) {
|
||||
document.querySelector('#cl-select').disabled = false;
|
||||
useClient = true;
|
||||
} else {
|
||||
document.querySelector('#cl-select').disabled = true;
|
||||
useClient = false;
|
||||
}
|
||||
|
||||
document.querySelectorAll('#cl-sv-toggle button').forEach(el => {
|
||||
el.classList.remove('is-selected', 'is-info');
|
||||
});
|
||||
|
||||
const tgt = document.querySelector(selector);
|
||||
|
||||
tgt.classList.add('is-selected', 'is-info');
|
||||
|
||||
if (editServerCb) {
|
||||
editServerCb();
|
||||
}
|
||||
};
|
||||
|
||||
// default to not-client
|
||||
if (!isClient) {
|
||||
eh();
|
||||
}
|
||||
|
||||
document.querySelector(selector).addEventListener('click', ev => {
|
||||
eh();
|
||||
|
||||
ev.preventDefault();
|
||||
});
|
||||
});
|
||||
|
||||
let lang = 'lua';
|
||||
let editLangCb = null;
|
||||
let initCb = null;
|
||||
|
||||
function getLangCode(lang) {
|
||||
switch (lang) {
|
||||
case 'js':
|
||||
return 'javascript';
|
||||
case 'ts':
|
||||
return 'typescript';
|
||||
}
|
||||
|
||||
return lang;
|
||||
}
|
||||
|
||||
[['#lua-button', 'lua'], ['#js-button', 'js']/*, ['#ts-button', 'ts']*/].forEach(([ selector, langOpt ]) => {
|
||||
const eh = () => {
|
||||
lang = langOpt;
|
||||
|
||||
document.querySelectorAll('#lang-toggle button').forEach(el => {
|
||||
el.classList.remove('is-selected', 'is-info');
|
||||
});
|
||||
|
||||
const tgt = document.querySelector(selector);
|
||||
|
||||
tgt.classList.add('is-selected', 'is-info');
|
||||
|
||||
if (editLangCb) {
|
||||
editLangCb();
|
||||
}
|
||||
};
|
||||
|
||||
// default to not-client
|
||||
if (langOpt === 'lua') {
|
||||
eh();
|
||||
}
|
||||
|
||||
document.querySelector(selector).addEventListener('click', ev => {
|
||||
eh();
|
||||
|
||||
ev.preventDefault();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
setInterval(() => fetchClients(), 1000);
|
||||
|
||||
const inNui = (!!window.invokeNative);
|
||||
let openData = {};
|
||||
|
||||
if (inNui) {
|
||||
document.querySelector('#passwordField').style.display = 'none';
|
||||
document.querySelector('html').classList.add('in-nui');
|
||||
|
||||
fetch(`http://${window.parent.GetParentResourceName()}/getOpenData`, {
|
||||
method: 'POST',
|
||||
body: '{}'
|
||||
}).then(a => a.json())
|
||||
.then(a => {
|
||||
openData = a;
|
||||
|
||||
if (!openData.options.canServer) {
|
||||
document.querySelector('#cl-sv-toggle').style.display = 'none';
|
||||
|
||||
const trigger = document.createEvent('HTMLEvents');
|
||||
trigger.initEvent('click', true, true);
|
||||
|
||||
document.querySelector('#cl-button').dispatchEvent(trigger);
|
||||
} else if (!openData.options.canClient && !openData.options.canSelf) {
|
||||
document.querySelector('#cl-sv-toggle').style.display = 'none';
|
||||
document.querySelector('#cl-field').style.display = 'none';
|
||||
|
||||
const trigger = document.createEvent('HTMLEvents');
|
||||
trigger.initEvent('click', true, true);
|
||||
|
||||
document.querySelector('#sv-button').dispatchEvent(trigger);
|
||||
}
|
||||
|
||||
if (!openData.options.canClient && openData.options.canSelf) {
|
||||
document.querySelector('#cl-field').style.display = 'none';
|
||||
}
|
||||
|
||||
if (openData.options.saveData) {
|
||||
const cb = () => {
|
||||
if (initCb) {
|
||||
initCb({
|
||||
lastLang: openData.options.saveData.lastLang,
|
||||
lastSnippet: openData.options.saveData.lastSnippet
|
||||
});
|
||||
} else {
|
||||
setTimeout(cb, 50);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(cb, 50);
|
||||
}
|
||||
|
||||
fetch(`https://${window.parent.GetParentResourceName()}/doOk`, {
|
||||
method: 'POST',
|
||||
body: '{}'
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelector('#close button').addEventListener('click', ev => {
|
||||
fetch(`https://${window.parent.GetParentResourceName()}/doClose`, {
|
||||
method: 'POST',
|
||||
body: '{}'
|
||||
});
|
||||
|
||||
ev.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
const defFiles = ['index.d.ts'];
|
||||
const defFilesServer = [...defFiles, 'natives_server.d.ts'];
|
||||
const defFilesClient = [...defFiles, 'natives_universal.d.ts'];
|
||||
|
||||
const prefix = 'https://unpkg.com/@citizenfx/{}/';
|
||||
const prefixClient = prefix.replace('{}', 'client');
|
||||
const prefixServer = prefix.replace('{}', 'server');
|
||||
|
||||
require.config({ paths: { 'vs': 'https://unpkg.com/monaco-editor@0.18.1/min/vs' }});
|
||||
require(['vs/editor/editor.main'], function() {
|
||||
const editor = monaco.editor.create(document.getElementById('code-container'), {
|
||||
value: 'return 42',
|
||||
language: 'lua'
|
||||
});
|
||||
|
||||
monaco.editor.setTheme('vs-dark');
|
||||
|
||||
let finalizers = [];
|
||||
|
||||
const updateScript = (client, lang) => {
|
||||
finalizers.forEach(a => a());
|
||||
finalizers = [];
|
||||
|
||||
if (lang === 'js' || lang === 'ts') {
|
||||
const defaults = (lang === 'js') ? monaco.languages.typescript.javascriptDefaults :
|
||||
monaco.languages.typescript.typescriptDefaults;
|
||||
|
||||
defaults.setCompilerOptions({
|
||||
noLib: true,
|
||||
allowNonTsExtensions: true
|
||||
});
|
||||
|
||||
for (const file of (client ? defFilesClient : defFilesServer)) {
|
||||
const prefix = (client ? prefixClient : prefixServer);
|
||||
|
||||
fetch(`${prefix}${file}`)
|
||||
.then(a => a.text())
|
||||
.then(a => {
|
||||
const l = defaults.addExtraLib(a, file);
|
||||
|
||||
finalizers.push(() => l.dispose());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editLangCb = () => {
|
||||
monaco.editor.setModelLanguage(editor.getModel(), getLangCode(lang));
|
||||
|
||||
updateScript(useClient, lang);
|
||||
};
|
||||
|
||||
editServerCb = () => {
|
||||
updateScript(useClient, lang);
|
||||
};
|
||||
|
||||
initCb = (data) => {
|
||||
if (data.lastLang) {
|
||||
const trigger = document.createEvent('HTMLEvents');
|
||||
trigger.initEvent('click', true, true);
|
||||
document.querySelector(`#${data.lastLang}-button`).dispatchEvent(trigger);
|
||||
}
|
||||
|
||||
if (data.lastSnippet) {
|
||||
editor.getModel().setValue(data.lastSnippet);
|
||||
}
|
||||
};
|
||||
|
||||
document.querySelector('#run').addEventListener('click', e => {
|
||||
const text = editor.getValue();
|
||||
|
||||
fetch((!inNui) ? '/runcode/' : `https://${openData.res}/runCodeInBand`, {
|
||||
method: 'post',
|
||||
body: JSON.stringify({
|
||||
password: document.querySelector('#password').value,
|
||||
client: (useClient) ? document.querySelector('#cl-select').value : '',
|
||||
code: text,
|
||||
lang: lang
|
||||
})
|
||||
}).then(res => res.json()).then(res => {
|
||||
if (inNui) {
|
||||
res = JSON.parse(res); // double packing for sad msgpack-to-json
|
||||
}
|
||||
|
||||
const resultElement = document.querySelector('#result');
|
||||
|
||||
if (res.error) {
|
||||
resultElement.classList.remove('notification', 'is-success');
|
||||
resultElement.classList.add('notification', 'is-danger');
|
||||
} else {
|
||||
resultElement.classList.remove('notification', 'is-danger');
|
||||
resultElement.classList.add('notification', 'is-success');
|
||||
}
|
||||
|
||||
resultElement.innerHTML = res.error || res.result;
|
||||
|
||||
if (res.from) {
|
||||
resultElement.innerHTML += ' (from ' + res.from + ')';
|
||||
}
|
||||
});
|
||||
|
||||
e.preventDefault();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
60
resources/[system]/runcode/web/nui.html
Normal file
60
resources/[system]/runcode/web/nui.html
Normal file
@@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<title>runcode nui</title>
|
||||
|
||||
<style type="text/css">
|
||||
html {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: transparent;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="holder">
|
||||
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
let openData = null;
|
||||
|
||||
window.addEventListener('message', ev => {
|
||||
switch (ev.data.type) {
|
||||
case 'open':
|
||||
const frame = document.createElement('iframe');
|
||||
|
||||
frame.name = 'rc';
|
||||
frame.allow = 'microphone *;';
|
||||
frame.src = ev.data.url;
|
||||
frame.style.visibility = 'hidden';
|
||||
|
||||
openData = ev.data;
|
||||
openData.frame = frame;
|
||||
|
||||
document.querySelector('#holder').appendChild(frame);
|
||||
break;
|
||||
case 'ok':
|
||||
openData.frame.style.visibility = 'visible';
|
||||
break;
|
||||
case 'close':
|
||||
document.querySelector('#holder').removeChild(openData.frame);
|
||||
|
||||
openData = null;
|
||||
break;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -1,10 +0,0 @@
|
||||
client_script 'spawnmanager.lua'
|
||||
|
||||
export 'getRandomSpawnPoint'
|
||||
export 'spawnPlayer'
|
||||
export 'addSpawnPoint'
|
||||
export 'removeSpawnPoint'
|
||||
export 'loadSpawns'
|
||||
export 'setAutoSpawn'
|
||||
export 'setAutoSpawnCallback'
|
||||
export 'forceRespawn'
|
||||
@@ -1,363 +0,0 @@
|
||||
-- in-memory spawnpoint array for this script execution instance
|
||||
local spawnPoints = {}
|
||||
|
||||
-- auto-spawn enabled flag
|
||||
local autoSpawnEnabled = false
|
||||
local autoSpawnCallback
|
||||
|
||||
-- support for mapmanager maps
|
||||
AddEventHandler('getMapDirectives', function(add)
|
||||
-- call the remote callback
|
||||
add('spawnpoint', function(state, model)
|
||||
-- return another callback to pass coordinates and so on (as such syntax would be [spawnpoint 'model' { options/coords }])
|
||||
return function(opts)
|
||||
local x, y, z, heading
|
||||
|
||||
local s, e = pcall(function()
|
||||
-- is this a map or an array?
|
||||
if opts.x then
|
||||
x = opts.x
|
||||
y = opts.y
|
||||
z = opts.z
|
||||
else
|
||||
x = opts[1]
|
||||
y = opts[2]
|
||||
z = opts[3]
|
||||
end
|
||||
|
||||
x = x + 0.0001
|
||||
y = y + 0.0001
|
||||
z = z + 0.0001
|
||||
|
||||
-- get a heading and force it to a float, or just default to null
|
||||
heading = opts.heading and (opts.heading + 0.01) or 0
|
||||
|
||||
-- add the spawnpoint
|
||||
addSpawnPoint({
|
||||
x = x, y = y, z = z,
|
||||
heading = heading,
|
||||
model = model
|
||||
})
|
||||
|
||||
-- recalculate the model for storage
|
||||
if not tonumber(model) then
|
||||
model = GetHashKey(model, _r)
|
||||
end
|
||||
|
||||
-- store the spawn data in the state so we can erase it later on
|
||||
state.add('xyz', { x, y, z })
|
||||
state.add('model', model)
|
||||
end)
|
||||
|
||||
if not s then
|
||||
Citizen.Trace(e .. "\n")
|
||||
end
|
||||
end
|
||||
-- delete callback follows on the next line
|
||||
end, function(state, arg)
|
||||
-- loop through all spawn points to find one with our state
|
||||
for i, sp in ipairs(spawnPoints) do
|
||||
-- if it matches...
|
||||
if sp.x == state.xyz[1] and sp.y == state.xyz[2] and sp.z == state.xyz[3] and sp.model == state.model then
|
||||
-- remove it.
|
||||
table.remove(spawnPoints, i)
|
||||
return
|
||||
end
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
|
||||
-- loads a set of spawn points from a JSON string
|
||||
function loadSpawns(spawnString)
|
||||
-- decode the JSON string
|
||||
local data = json.decode(spawnString)
|
||||
|
||||
-- do we have a 'spawns' field?
|
||||
if not data.spawns then
|
||||
error("no 'spawns' in JSON data")
|
||||
end
|
||||
|
||||
-- loop through the spawns
|
||||
for i, spawn in ipairs(data.spawns) do
|
||||
-- and add it to the list (validating as we go)
|
||||
addSpawnPoint(spawn)
|
||||
end
|
||||
end
|
||||
|
||||
local spawnNum = 1
|
||||
|
||||
function addSpawnPoint(spawn)
|
||||
-- validate the spawn (position)
|
||||
if not tonumber(spawn.x) or not tonumber(spawn.y) or not tonumber(spawn.z) then
|
||||
error("invalid spawn position")
|
||||
end
|
||||
|
||||
-- heading
|
||||
if not tonumber(spawn.heading) then
|
||||
error("invalid spawn heading")
|
||||
end
|
||||
|
||||
-- model (try integer first, if not, hash it)
|
||||
local model = spawn.model
|
||||
|
||||
if not tonumber(spawn.model) then
|
||||
model = GetHashKey(spawn.model)
|
||||
end
|
||||
|
||||
-- is the model actually a model?
|
||||
if not IsModelInCdimage(model) then
|
||||
error("invalid spawn model")
|
||||
end
|
||||
|
||||
-- is is even a ped?
|
||||
-- not in V?
|
||||
--[[if not IsThisModelAPed(model) then
|
||||
error("this model ain't a ped!")
|
||||
end]]
|
||||
|
||||
-- overwrite the model in case we hashed it
|
||||
spawn.model = model
|
||||
|
||||
-- add an index
|
||||
spawn.idx = spawnNum
|
||||
spawnNum = spawnNum + 1
|
||||
|
||||
-- all OK, add the spawn entry to the list
|
||||
table.insert(spawnPoints, spawn)
|
||||
|
||||
return spawn.idx
|
||||
end
|
||||
|
||||
-- removes a spawn point
|
||||
function removeSpawnPoint(spawn)
|
||||
for i = 1, #spawnPoints do
|
||||
if spawnPoints[i].idx == spawn then
|
||||
table.remove(spawnPoints, i)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- changes the auto-spawn flag
|
||||
function setAutoSpawn(enabled)
|
||||
autoSpawnEnabled = enabled
|
||||
end
|
||||
|
||||
-- sets a callback to execute instead of 'native' spawning when trying to auto-spawn
|
||||
function setAutoSpawnCallback(cb)
|
||||
autoSpawnCallback = cb
|
||||
autoSpawnEnabled = true
|
||||
end
|
||||
|
||||
-- function as existing in original R* scripts
|
||||
local function freezePlayer(id, freeze)
|
||||
local player = id
|
||||
SetPlayerControl(player, not freeze, false)
|
||||
|
||||
local ped = GetPlayerPed(player)
|
||||
|
||||
if not freeze then
|
||||
if not IsEntityVisible(ped) then
|
||||
SetEntityVisible(ped, true)
|
||||
end
|
||||
|
||||
if not IsPedInAnyVehicle(ped) then
|
||||
SetEntityCollision(ped, true)
|
||||
end
|
||||
|
||||
FreezeEntityPosition(ped, false)
|
||||
--SetCharNeverTargetted(ped, false)
|
||||
SetPlayerInvincible(player, false)
|
||||
else
|
||||
if IsEntityVisible(ped) then
|
||||
SetEntityVisible(ped, false)
|
||||
end
|
||||
|
||||
SetEntityCollision(ped, false)
|
||||
FreezeEntityPosition(ped, true)
|
||||
--SetCharNeverTargetted(ped, true)
|
||||
SetPlayerInvincible(player, true)
|
||||
--RemovePtfxFromPed(ped)
|
||||
|
||||
if not IsPedFatallyInjured(ped) then
|
||||
ClearPedTasksImmediately(ped)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function loadScene(x, y, z)
|
||||
NewLoadSceneStart(x, y, z, 0.0, 0.0, 0.0, 20.0, 0)
|
||||
|
||||
while IsNewLoadSceneActive() do
|
||||
networkTimer = GetNetworkTimer()
|
||||
|
||||
NetworkUpdateLoadScene()
|
||||
end
|
||||
end
|
||||
|
||||
-- to prevent trying to spawn multiple times
|
||||
local spawnLock = false
|
||||
|
||||
-- spawns the current player at a certain spawn point index (or a random one, for that matter)
|
||||
function spawnPlayer(spawnIdx, cb)
|
||||
if spawnLock then
|
||||
return
|
||||
end
|
||||
|
||||
spawnLock = true
|
||||
|
||||
Citizen.CreateThread(function()
|
||||
-- if the spawn isn't set, select a random one
|
||||
if not spawnIdx then
|
||||
spawnIdx = GetRandomIntInRange(1, #spawnPoints + 1)
|
||||
end
|
||||
|
||||
-- get the spawn from the array
|
||||
local spawn
|
||||
|
||||
if type(spawnIdx) == 'table' then
|
||||
spawn = spawnIdx
|
||||
else
|
||||
spawn = spawnPoints[spawnIdx]
|
||||
end
|
||||
|
||||
if not spawn.skipFade then
|
||||
DoScreenFadeOut(500)
|
||||
|
||||
while not IsScreenFadedOut() do
|
||||
Citizen.Wait(0)
|
||||
end
|
||||
end
|
||||
|
||||
-- validate the index
|
||||
if not spawn then
|
||||
Citizen.Trace("tried to spawn at an invalid spawn index\n")
|
||||
|
||||
spawnLock = false
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
-- freeze the local player
|
||||
freezePlayer(PlayerId(), true)
|
||||
|
||||
-- if the spawn has a model set
|
||||
if spawn.model then
|
||||
RequestModel(spawn.model)
|
||||
|
||||
-- load the model for this spawn
|
||||
while not HasModelLoaded(spawn.model) do
|
||||
RequestModel(spawn.model)
|
||||
|
||||
Wait(0)
|
||||
end
|
||||
|
||||
-- change the player model
|
||||
SetPlayerModel(PlayerId(), spawn.model)
|
||||
|
||||
-- release the player model
|
||||
SetModelAsNoLongerNeeded(spawn.model)
|
||||
end
|
||||
|
||||
-- preload collisions for the spawnpoint
|
||||
RequestCollisionAtCoord(spawn.x, spawn.y, spawn.z)
|
||||
|
||||
-- spawn the player
|
||||
--ResurrectNetworkPlayer(GetPlayerId(), spawn.x, spawn.y, spawn.z, spawn.heading)
|
||||
local ped = GetPlayerPed(-1)
|
||||
|
||||
-- V requires setting coords as well
|
||||
SetEntityCoordsNoOffset(ped, spawn.x, spawn.y, spawn.z, false, false, false, true)
|
||||
|
||||
NetworkResurrectLocalPlayer(spawn.x, spawn.y, spawn.z, spawn.heading, true, true, false)
|
||||
|
||||
-- gamelogic-style cleanup stuff
|
||||
ClearPedTasksImmediately(ped)
|
||||
--SetEntityHealth(ped, 300) -- TODO: allow configuration of this?
|
||||
RemoveAllPedWeapons(ped) -- TODO: make configurable (V behavior?)
|
||||
ClearPlayerWantedLevel(PlayerId())
|
||||
|
||||
-- why is this even a flag?
|
||||
--SetCharWillFlyThroughWindscreen(ped, false)
|
||||
|
||||
-- set primary camera heading
|
||||
--SetGameCamHeading(spawn.heading)
|
||||
--CamRestoreJumpcut(GetGameCam())
|
||||
|
||||
-- load the scene; streaming expects us to do it
|
||||
--ForceLoadingScreen(true)
|
||||
--loadScene(spawn.x, spawn.y, spawn.z)
|
||||
--ForceLoadingScreen(false)
|
||||
|
||||
local time = GetGameTimer()
|
||||
|
||||
while (not HasCollisionLoadedAroundEntity(ped) and (GetGameTimer() - time) < 5000) do
|
||||
Citizen.Wait(0)
|
||||
end
|
||||
|
||||
ShutdownLoadingScreen()
|
||||
|
||||
if IsScreenFadedOut() then
|
||||
DoScreenFadeIn(500)
|
||||
|
||||
while not IsScreenFadedIn() do
|
||||
Citizen.Wait(0)
|
||||
end
|
||||
end
|
||||
|
||||
-- and unfreeze the player
|
||||
freezePlayer(PlayerId(), false)
|
||||
|
||||
TriggerEvent('playerSpawned', spawn)
|
||||
|
||||
if cb then
|
||||
cb(spawn)
|
||||
end
|
||||
|
||||
spawnLock = false
|
||||
end)
|
||||
end
|
||||
|
||||
-- automatic spawning monitor thread, too
|
||||
local respawnForced
|
||||
local diedAt
|
||||
|
||||
Citizen.CreateThread(function()
|
||||
-- main loop thing
|
||||
while true do
|
||||
Citizen.Wait(50)
|
||||
|
||||
local playerPed = GetPlayerPed(-1)
|
||||
|
||||
if playerPed and playerPed ~= -1 then
|
||||
-- check if we want to autospawn
|
||||
if autoSpawnEnabled then
|
||||
if NetworkIsPlayerActive(PlayerId()) then
|
||||
if (diedAt and (GetTimeDifference(GetGameTimer(), diedAt) > 2000)) or respawnForced then
|
||||
if autoSpawnCallback then
|
||||
autoSpawnCallback()
|
||||
else
|
||||
spawnPlayer()
|
||||
end
|
||||
|
||||
respawnForced = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if IsEntityDead(playerPed) then
|
||||
if not diedAt then
|
||||
diedAt = GetGameTimer()
|
||||
end
|
||||
else
|
||||
diedAt = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
function forceRespawn()
|
||||
spawnLock = false
|
||||
respawnForced = true
|
||||
end
|
||||
Reference in New Issue
Block a user