reorganize resource directories

This commit is contained in:
astatine
2019-12-10 09:54:29 +01:00
parent 60da977c02
commit 51cc79eda2
77 changed files with 7 additions and 1518 deletions

View File

@@ -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>'
}
}

View File

@@ -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");
})();

View File

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

View File

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

View File

@@ -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',
}

View File

@@ -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)

View File

@@ -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();
},
},
};

View File

@@ -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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
},
},
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,
},
},
});

View File

@@ -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: {},
});

View File

@@ -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%',
}
};

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View File

@@ -0,0 +1 @@
data.json

View 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'
}

View 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() ];
}
});

View 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)

View 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

View 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)

View 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)

View 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)

View 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">&nbsp;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>

View 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>

View File

@@ -1,10 +0,0 @@
client_script 'spawnmanager.lua'
export 'getRandomSpawnPoint'
export 'spawnPlayer'
export 'addSpawnPoint'
export 'removeSpawnPoint'
export 'loadSpawns'
export 'setAutoSpawn'
export 'setAutoSpawnCallback'
export 'forceRespawn'

View File

@@ -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