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
@@ -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
View File
@@ -1 +0,0 @@
# Chat
-26
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',
}
-232
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)
-255
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();
},
},
};
-86
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,
},
},
});
@@ -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%',
}
};
-127
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;
}
-117
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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-48
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
-79
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
View File
@@ -0,0 +1 @@
data.json
+17
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'
}
+11
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() ];
}
});
+15
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)
@@ -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
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)
+66
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)
+192
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)
+486
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/[email protected]/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/[email protected]/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
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>
@@ -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