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,18 +0,0 @@
description 'output lists for game content'
SetResourceInfo('uiPage', 'client/html/index.html')
client_script 'client/channelfeed.lua'
export 'printTo'
export 'addChannel'
export 'removeChannel'
files
{
'client/html/index.html',
'client/html/feed.js',
'client/html/feed.css',
'client/fonts/roboto-regular.ttf',
'client/fonts/roboto-condensed.ttf',
}

View File

@@ -1,40 +0,0 @@
local eventBuffer = {}
AddUIHandler('getNew', function(data, cb)
local localBuf = eventBuffer
eventBuffer = {}
cb(localBuf)
end)
function printTo(channel, data)
table.insert(eventBuffer, {
meta = 'print',
channel = channel,
data = data
})
PollUI()
end
function addChannel(id, options)
if not options.template then
return
end
options.id = id
table.insert(eventBuffer, {
meta = 'addChannel',
data = options
})
PollUI()
end
function removeChannel(id)
table.insert(eventBuffer, {
meta = 'removeChannel',
data = id
})
end

View File

@@ -1,118 +0,0 @@
(function() {
var getLock = 0;
var channels = {};
var zoomLevel = '100%';
$(function()
{
zoomLevel = Math.round(($(window).height() / 720) * 100) + '%'; // yay dynamic typing
});
function refetchData()
{
getLock = 0;
$.get('http://channelfeed/getNew', function(data)
{
if (getLock > 1)
{
setTimeout(refetchData, 50);
return;
}
getLock++;
data.forEach(function(item)
{
switch (item.meta)
{
case 'print':
var channel = item.channel;
if (!(channel in channels))
{
return;
}
channel = channels[channel];
var elem = $($.Mustache.render(item.channel, item.data, { method: channel.method })).appendTo(channel.$elem);
setTimeout(function()
{
elem.fadeOut(400, function()
{
elem.remove();
});
}, 7500);
break;
case 'addChannel':
var channel = item.data;
if (channel.id in channels)
{
return;
}
channel.$elem = $('<div></div>').attr('id', 'channel-' + channel.id).appendTo('#channels');
if (channel.styles !== undefined)
{
channel.$elem.css(channel.styles);
}
channel.$elem = $('<div></div>').css('zoom', zoomLevel).appendTo(channel.$elem);
if (channel.styleUrl !== undefined)
{
$('<link>').appendTo('head').attr({ type: 'text/css', rel: 'stylesheet' }).attr('href', channel.styleUrl);
}
$.Mustache.add(channel.id, channel.template);
channels[channel.id] = channel;
break;
case 'removeChannel':
var channelId = item.data;
if (channelId in channels)
{
channel.$elem.parent().remove();
delete channels[channelId];
}
break;
case 'clear':
var channel = item.channel;
if (!(channel in channels))
{
return;
}
channel = channels[channel];
channel.$elem.html();
break;
}
});
});
}
window.addEventListener('message', function(event)
{
if (event.data.type != 'poll')
{
return;
}
refetchData();
});
})();

View File

@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<script src="nui://game/ui/jquery.js" type="text/javascript"></script>
<script src="nui://game/ui/mustache.js" type="text/javascript"></script>
<script src="nui://game/ui/jquery.mustache.js" type="text/javascript"></script>
<script src="feed.js" type="text/javascript"></script>
</head>
<body>
<div id="channels">
</div>
</body>
</html>

View File

@@ -0,0 +1,10 @@
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

@@ -0,0 +1,74 @@
(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

@@ -0,0 +1,116 @@
* {
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

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

View File

@@ -0,0 +1,26 @@
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

@@ -0,0 +1,232 @@
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

@@ -0,0 +1,255 @@
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

@@ -0,0 +1,86 @@
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

@@ -0,0 +1,44 @@
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

@@ -0,0 +1,19 @@
// 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

@@ -0,0 +1,127 @@
.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

@@ -0,0 +1,117 @@
<!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.

View File

@@ -0,0 +1,48 @@
/* 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

@@ -0,0 +1,79 @@
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)

View File

@@ -1 +0,0 @@
server_script 'irc.lua'

View File

@@ -1,4 +0,0 @@
local reflection = clr.System.Reflection
local assembly = reflection.Assembly.LoadFrom('resources/[gameplay]/irc/ChatSharp.dll')
dofile('resources/[gameplay]/irc/irc_run.lua')

View File

@@ -1,91 +0,0 @@
local chatSharp = clr.ChatSharp
local client = chatSharp.IrcClient('irc.rizon.net', chatSharp.IrcUser('citimate', 'mateyate'), false)
-- temporary workaround for connections that never triggered playerActivated but triggered playerDropped
local activatedPlayers = {}
client.ConnectionComplete:add(function(s : object, e : System.EventArgs) : void
client:JoinChannel('#meow')
end)
-- why is 'received' even misspelled here?
client.ChannelMessageRecieved:add(function(s : object, e : ChatSharp.Events.PrivateMessageEventArgs) : void
local msg = e.PrivateMessage
TriggerClientEvent('chatMessage', -1, msg.User.Nick, { 0, 0x99, 255 }, msg.Message)
end)
AddEventHandler('playerActivated', function()
client:SendMessage('* ' .. GetPlayerName(source) .. '(' .. GetPlayerGuid(source) .. '@' .. GetPlayerEP(source) .. ') joined the server', '#fourdeltaone')
table.insert(activatedPlayers, GetPlayerGuid(source))
end)
AddEventHandler('playerDropped', function()
-- find out if this connection ever triggered playerActivated
for index,guid in pairs(activatedPlayers) do
if guid == playerGuid then
-- show player dropping connection in chat
client:SendMessage('* ' .. GetPlayerName(source) .. '(' .. GetPlayerGuid(source) .. '@' .. GetPlayerEP(source) .. ') left the server', '#fourdeltaone')
table.remove(activatedPlayers, index)
return
end
end
end)
AddEventHandler('chatMessage', function(source, name, message)
print('hey there ' .. name)
local displayMessage = gsub(message, '^%d', '')
-- ignore zero-length messages
if string.len(displayMessage) == 0 then
return
end
-- ignore chat messages that are actually commands
if string.sub(displayMessage, 1, 1) == "/" then
return
end
client:SendMessage('[' .. tostring(GetPlayerName(source)) .. ']: ' .. displayMessage, '#fourdeltaone')
end)
AddEventHandler('onPlayerKilled', function(playerId, attackerId, reason, position)
local player = GetPlayerByServerId(playerId)
local attacker = GetPlayerByServerId(attackerId)
local reasonString = 'killed'
if reason == 0 or reason == 56 or reason == 1 or reason == 2 then
reasonString = 'meleed'
elseif reason == 3 then
reasonString = 'knifed'
elseif reason == 4 or reason == 6 or reason == 18 or reason == 51 then
reasonString = 'bombed'
elseif reason == 5 or reason == 19 then
reasonString = 'burned'
elseif reason == 7 or reason == 9 then
reasonString = 'pistoled'
elseif reason == 10 or reason == 11 then
reasonString = 'shotgunned'
elseif reason == 12 or reason == 13 or reason == 52 then
reasonString = 'SMGd'
elseif reason == 14 or reason == 15 or reason == 20 then
reasonString = 'assaulted'
elseif reason == 16 or reason == 17 then
reasonString = 'sniped'
elseif reason == 49 or reason == 50 then
reasonString = 'ran over'
end
client:SendMessage('* ' .. attacker.name .. ' ' .. reasonString .. ' ' .. player.name, '#fourdeltaone')
end)
client:ConnectAsync()
AddEventHandler('onResourceStop', function(name)
if name == GetInvokingResource() then
client:Quit('Resource stopping.')
end
end)

View File

@@ -1,5 +0,0 @@
dependency 'obituary'
description 'death messages using the obituary resource'
client_script 'deathmessages.lua'

View File

@@ -1,42 +0,0 @@
AddEventHandler('onPlayerDied', function(playerId, reason, position)
local player = GetPlayerByServerId(playerId)
if player then
exports.obituary:printObituary('<b>%s</b> died.', player.name)
end
end)
AddEventHandler('onPlayerKilled', function(playerId, attackerId, reason, position)
local player = GetPlayerByServerId(playerId)
local attacker = GetPlayerByServerId(attackerId)
local reasonString = 'killed'
if reason == 0 or reason == 56 or reason == 1 or reason == 2 then
reasonString = 'meleed'
elseif reason == 3 then
reasonString = 'knifed'
elseif reason == 4 or reason == 6 or reason == 18 or reason == 51 then
reasonString = 'bombed'
elseif reason == 5 or reason == 19 then
reasonString = 'burned'
elseif reason == 7 or reason == 9 then
reasonString = 'pistoled'
elseif reason == 10 or reason == 11 then
reasonString = 'shotgunned'
elseif reason == 12 or reason == 13 or reason == 52 then
reasonString = 'SMGd'
elseif reason == 14 or reason == 15 or reason == 20 then
reasonString = 'assaulted'
elseif reason == 16 or reason == 17 then
reasonString = 'sniped'
elseif reason == 49 or reason == 50 then
reasonString = 'ran over'
end
echo("obituary-deaths: onPlayerKilled\n")
if player and attacker then
exports.obituary:printObituary('<b>%s</b> %s <b>%s</b>.', attacker.name, reasonString, player.name)
end
end)

View File

@@ -1,7 +0,0 @@
dependency 'channelfeed'
client_script 'obituary.lua'
export 'printObituary'
files 'obituary.css'

View File

@@ -1,46 +0,0 @@
@font-face {
font-family: 'Roboto Condensed';
src: url('nui://channelfeed/client/fonts/roboto-condensed.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Roboto';
src: url('nui://channelfeed/client/fonts/roboto-regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
#channel-obituary
{
position: absolute;
}
#channel-obituary > div
{
width: 100%;
font-family: 'Roboto';
}
#channel-obituary div.item
{
background-color: rgba(50, 50, 50, .6);
padding: 4px;
padding-left: 8px;
padding-right: 8px;
border-radius: 4px;
color: #eee;
margin-bottom: 3px;
font-size: 65%;
}
#channel-obituary div.item b
{
font-family: 'Roboto Condensed';
font-size: 125%;
font-weight: normal;
color: #09f;
}

View File

@@ -1,51 +0,0 @@
AddEventHandler('onClientResourceStart', function(name)
if name == GetCurrentResource() then
local x, y = GetHudPosition('HUD_RADAR')
local w, h = GetHudSize('HUD_RADAR')
x = x - 0.01
w = w + 0.02
if GetIsWidescreen() then
x = x / 1.333
w = w / 1.333
end
exports.channelfeed:addChannel('obituary', {
method = 'append',
styleUrl = 'nui://obituary/obituary.css',
styles = { -- temporary
left = tostring(x * 100) .. '%',
bottom = 'calc(' .. tostring((1 - y) * 100) .. '% + 10px)',
width = tostring(w * 100) .. '%'
},
template = '<div class="item">{{{text}}}</div>'
})
end
end)
function printObituary(format, ...)
local args = table.pack(...)
for i = 1, args.n do
if type(args[i]) == 'string' then
args[i] = args[i]:gsub('<', '&lt;')
end
end
echo("obituary: printObituary\n")
exports.channelfeed:printTo('obituary', {
text = string.format(format, table.unpack(args))
})
end
--[[AddEventHandler('chatMessage', function(name, color, message)
exports.channelfeed:printTo('obituary', {
text = message:gsub('<', '&lt;')
})
end)]]
AddEventHandler('onClientResourceStop', function()
-- todo: remove channel
end)