// ==UserScript==
const gameConfig = {
type: "S-C-Link_server",
title: "S-C-Link 服务端",
doc: `EasyBox3Lib的附属库,用于通过服务端渲染客户端内容,需要安装EasyBox3Lib。EasyBox3Lib的安装见帮助链接`,
help: "https://qndm.github.io/EasyBox3Lib",
file: true,
isClient: false
}
// ==UserScript==
/**
* S-C-Link 库(服务端)
* 用于通过服务端渲染客户端内容的库
* 依赖EasyBox3Lib 0.1.6
* (为什么使用`Game`开头的API?因为GUI是新岛专属())
* @author qndm
* @module S-C-Link_server
* @version 0.0.3
* @license MIT
*/
/**
* 事件回调函数
* @callback EventCallBack
* @param {PackedNode} target 目标节点
* @param {GameEntity} entity 对应玩家
* @returns {void}
*/
/**
* 打包后的节点
* @typedef PackedNode
* @property {"renderMessage" | "removeMessage"} protocols 事件协议,打包节点所需的协议只有`"renderMessage"`和`"removeMessage"`
* @property {number} id 节点序号
* @property {?string} name 节点名称,只在`"renderMessage"`协议下使用
* @property {?object} data 节点自定义数量,只在`"renderMessage"`协议下使用
* @property {?NodeStyle} style 节点样式,只在`"renderMessage"`协议下使用
* @property {?string} content 节点内容,只在`"renderMessage"`协议下使用
* @property {?number} pointerEventBehavior 界面元素对指针事件的行为方式,只在`"renderMessage"`协议下使用
* @property {?string} autoResize 节点自动调整尺寸的方式,只在`"renderMessage"`协议下使用
* @property {?boolean} visible 节点的可见性,只在`"renderMessage"`协议下使用
* @property {?string} placeholder 节点占位文本内容,只在`"renderMessage"`协议下使用
* @property {?Coord2} size 节点大小,只在`"renderMessage"`协议下使用
* @property {?Coord2} position 节点位置,只在`"renderMessage"`协议下使用
* @property {?Vector2} anchor 节点锚点,只在`"renderMessage"`协议下使用
* @property {?number} parent 父节点id
* @property {?PackedNode[]} childern 子节点,只在`"renderMessage"`协议下使用
* @property {?NodeType} type 节点类型,只在`"renderMessage"`协议下使用
* @property {?PackedNodeEvent} events 打包后的节点事件
*/
/**
* 节点样式
* @typedef NodeStyle
* @property {string | number} textXAlignment 若节点为文本,则为节点的水平对齐方式
* @property {string | number} textYAlignment 若节点为文本,则为节点的垂直对齐方式
* @property {GameRGBColor} textColor 若节点为文本,则为文字颜色
* @property {GameRGBAColor} backgroundColor 节点背景颜色
* @property {GameRGBAColor} placeholderColor 若节点是输入框,表示占位文本背景颜色
* @property {number | null} zIndex 节点的层级,用于确定节点的渲染顺序。
* @property {number} textFontSize 若节点为文本,则为文字字号
* @property {number} imageOpacity 若节点是图片,表示图片不透明度,范围0~1
*/
/**
* 打包后的节点事件
* @typedef PackedNodeEvent
* @property {boolean} onPress 是否监听当节点被按下时,触发的事件
* @property {boolean} onRelease 是否监听当节点被松开时,触发的事件
* @property {boolean} onFocus 是否监听当节点为输入框,聚焦时触发的事件
* @property {boolean} onBlur 是否监听当节点为输入框,失去焦点时触发的事件
*/
/**
* 玩家节点事件
* @typedef EntityNodeEvent
* @property {ClientEvent[]} onPress 当节点被按下时,触发的事件
* @property {ClientEvent[]} onRelease 当节点被松开时,触发的事件
* @property {ClientEvent[]} onFocus 当节点为输入框,聚焦时触发的事件
* @property {ClientEvent[]} onBlur 当节点为输入框,失去焦点时触发的事件
*/
/**
* 玩家客户端事件
* @typedef {Map<number, EntityNodeEvent>} EntityClientEvent
*/
/**
* 客户端事件
* @typedef ClientEvent
* @property {EventCallBack} handler 事件监听回调函数
* @property {number} maxTimes 最大触发次数
* @property {number} times 当前已触发次数
*/
/**
* 当节点事件被触发时,发送到服务端的数据
* @typedef PackedClientEvent
* @property {"triggeredEvent"} protocols 事件协议,触发事件的协议为`"triggeredEvent"`
* @property {?PackedNode} node 节点
* @property {EventName} eventName 触发的事件类型
*/
/**
* 打包后的数据,用于发送给客户端
* 要求有`protocols`属性指定协议
* @typedef {PackedNode | PackedClientEvent} PackedData
*/
const EBL = global.EasyBox3Lib,
/**
* 配置文件
*/
CONFIG = require('./EasyBox3Lib.config.js'),
/**
* 建议EasyBox3Lib版本
* @type {number[]}
*/
EBL_VERSION = [0, 1, 6],
/**
* 当前版本
* @type {number[]}
*/
VERSION = [0, 0, 3];
// ----- S-C-Link_server Start -----
/**
* 节点类型
* @enum {string}
*/
const NodeType = {
BOX: 'box',
TEXT: 'text',
IMAGE: 'image',
INPUT: 'input',
ROOT: 'root'
}
/**
* 事件类型
* @enum {string}
*/
const EventName = {
onPress: "onPress",
onRelease: "onRelease",
onFocus: "onFocus",
onBlur: "onBlur",
onLockChange: "onLockChange",
onLockError: "onLockError",
}
/**
* 二维向量
* 大部分和三维向量相同
*/
class Vector2 {
x = 0;
y = 0;
/**
* 定义一个二维向量
* @param {number} x 二维向量`x`的值(水平方向)
* @param {number} y 二维向量`y`的值(竖直方向)
*/
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
set(x, y) {
this.x = x;
this.y = y;
return this;
}
clone() {
return new Vector2(this.x, this.y);
}
copy(v) {
this.x = v.x;
this.y = v.y;
return this;
}
add(v) {
return new Vector2(this.x + v.x, this.y / v.y);
}
sub(v) {
return new Vector2(this.x - v.x, this.y - v.y);
}
mul(v) {
return new Vector2(this.x * v.x, this.y * v.y);
}
div(v) {
return new Vector2(this.x / v.x, this.y / v.y);
}
addEq(v) {
this.x += v.x;
this.y += v.y;
return this;
}
subEq(v) {
this.x -= v.x;
this.y -= v.y;
return this;
}
mulEq(v) {
this.x *= v.x;
this.y *= v.y;
return this;
}
divEq(v) {
this.x /= v.x;
this.y /= v.y;
return this;
}
pow(n) {
return new Vector2(Math.pow(this.x, n), Math.pow(this.y, n));
}
distance(v) {
return Math.sqrt(Math.pow(v.x - this.x, 2) + Math.pow(v.y - this.y, 2));
}
mag() {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
min(v) {
return new Vector2(Math.min(this.x, v.x), Math.min(this.y, v.y));
}
max(v) {
return new Vector2(Math.max(this.x, v.x), Math.max(this.y, v.y));
}
/**
* 归一化函数
* @returns {Vector2}
*/
normalize() {
let max = Math.max(this.x, this.y);
return new Vector2(this.x / max, this.y / max);
}
scale(n) {
return new Vector2(this.x * n, this.y * n);
}
toString() {
return JSON.stringify(this);
}
towards(v) {
return new Vector2(v.x - this.x, v.y - this.y);
}
equals(v, tolerance = 0.0001) {
return Math.abs(v.x - this.x) <= tolerance && Math.abs(v.y - this.y) <= tolerance;
}
lerp(v) {
return this.add(v).scale(0.5);
}
}
/**
* 图像映射中区域的坐标
* 值为`offset`(绝对坐标)和`scale`(占父元素空间的百分比)之和
*/
class Coord2 {
offset = new Vector2(0, 0);
scale = new Vector2(0, 0);
constructor(offset = new Vector2(0, 0), scale = new Vector2(0, 0)) {
this.offset = offset;
this.scale = scale;
}
/**
* 设置`offset.x`的值
* @param {number} value
*/
set x(value) {
this.offset.x = value;
}
/**
* 设置`offset.y`的值
* @param {number} value
*/
set y(value) {
this.offset.y = value;
}
set(offset, scale) {
this.offset = offset;
this.scale = scale;
}
copy(c) {
this.offset = c.offset.clone();
this.scale = c.scale.clone();
}
clone() {
return new Coord2(this.offset.clone(), this.scale.clone());
}
}
/**
* 一个节点设计图
*/
class NodeBlueprint {
/**
* 节点的名称,可重复
* 对应`UiNode.name`
* @type {string}
*/
name = '';
/**
* 节点的类型
* @type {NodeType}
*/
type = NodeType.BOX;
/**
* 节点的锚点
* @type {Vector2}
*/
anchor = new Vector2();
/**
* 节点的位置
* @type {Coord2}
*/
position = new Coord2();
/**
* 节点的大小
* @type {Coord2}
*/
size = new Coord2();
/**
* 节点的子节点
* @type {NodeBlueprint[]}
*/
children = [];
/**
* 节点的样式
* @type {NodeStyle}
*/
style = {
/**
* 若节点为文本,则为节点的水平对齐方式
* `-1` 等同于 `Left`,表示左对齐
* `0` 等同于 `Center`,表示居中
* `1` 等同于 `Right`,表示右对齐
* 若不是文本,保留该属性,但不生效
* 若为其他的值,则以居中渲染
* @type {string | number}
*/
textXAlignment: 0,
/**
* 若节点为文本,则为节点的垂直对齐方式
* `-1` 等同于 `Top`,表示左对齐
* `0` 等同于 `Center`,表示居中
* `1` 等同于 `Bottom`,表示右对齐
* 若不是文本,保留该属性,但不生效
* 若为其他的值,则以居中渲染
* @type {string | number}
*/
textYAlignment: 0,
/**
* 若节点为文本,则为文字颜色
* 若不是文本,保留该属性,但不生效
* @type {GameRGBColor}
*/
textColor: new GameRGBColor(1, 1, 1),
/**
* 节点背景颜色
* @type {GameRGBAColor}
*/
backgroundColor: new GameRGBAColor(1, 1, 1, 1),
/**
* 若节点是输入框,表示占位文本背景颜色
* 若不是输入框,保留该属性,但不生效
* @type {GameRGBAColor}
*/
placeholderColor: new GameRGBAColor(1, 1, 1, 1),
/**
* 节点的层级,用于确定节点的渲染顺序。
* 若为`null`,则~~听天由命~~浏览器会自行处理
* @type {number}
*/
zIndex: 1,
/**
* 若节点为文本,则为文字字号
* 若不是文本,保留该属性,但不生效
* @type {number}
*/
textFontSize: 12,
/**
* 若节点是图片,表示图片不透明度
* 若不是输入框,保留该属性,但不生效
* @type {number}
*/
imageOpacity: 1
};
/**
* 节点的内容
* 若不是文本/图片/输入框,保留该属性,但不生效
* 对于文本,为文本内容`textContent`
* 对于图片,为图片路径`image`
* 对于输入框,为输入的文本内容`textContent`
* @type {string}
*/
content = '';
/**
* 若节点是输入框,表示占位文本内容
* 若不是输入框,保留该属性,但不生效
* @type {string}
*/
placeholder = '';
/**
* 节点的自定义数据
* 会传到客户端中
* @type {object}
*/
data = {};
/**
* 节点的可见性
*/
visible = true;
/**
* 节点的标签
* @type {string[]}
*/
tags = [];
/**
* 节点自动调整尺寸的方式
*/
autoResize = {
x: false,
y: false
}
/**
* 表示界面元素对指针事件的行为方式
*/
pointerEventBehavior = {
/**
* 自身是否响应
* @type {boolean}
*/
enable: true,
/**
* 自身后方的其他元素是否响应
* @type {boolean}
*/
passThrough: true
}
/**
* 节点的事件
* @type {EntityNodeEvent}
*/
_event = {
onPress: [],
onRelease: [],
onFocus: [],
onBlur: []
};
/**
* 定义一个节点
* @param {NodeType} type 节点类型
* @param {string} name 节点名称,可重复
*/
constructor(type, name) {
this.type = type;
this.name = name;
}
/**
* 复制自身
* @returns {NodeBlueprint}
*/
clone() {
var node = new NodeBlueprint(this.type, this.name);
Object.assign(node, EBL.copy(this));
return node;
}
/**
* 添加事件监听器
* 暂时没什么用,因为我还没做
* @param {EventName} eventName 事件类型
* @param {EventCallBack} handler 事件监听器
* @param {number} maxTimes 事件监听器最大监听次数,超过此次数,停止监听
*/
addEventHandler(eventName, handler, maxTimes = Infinity) {
if (!Object.keys(this._event).includes(eventName))
EBL.throwError("[S-C-LINK]", "未知事件", eventName);
this._event[eventName].push({ handler, maxTimes, times: 0 });
}
/**
* 打包节点,以发送到客户端
* 会往`entity.player`写入`_lastNodeIndex`、`_clientEvents`属性
* @param {GameEntity} entity 要发送的玩家
* @param {number} parentId 父节点id
* @param {?number} nodeId 节点id
* @returns {PackedNode}
*/
_pack(entity, parentId = 0, nodeId = null) {
var id,
/**@type {EntityClientEvent} */
_clientEvents;
if (entity.player._clientEvents)
_clientEvents = entity.player._clientEvents;
else
entity.player._clientEvents = _clientEvents = new Map();
if (nodeId !== null)
id = nodeId;
else if (entity.player._lastNodeIndex)
id = ++entity.player._lastNodeIndex;
else
entity.player._lastNodeIndex = id = 1;
_clientEvents.set(id, {
onPress: this._event.onPress.map(e => Object({ handler: e.handler, maxTimes: e.maxTimes, times: entity.player._clientEvents.get(id)?.onPress.times ?? 0 })),
onRelease: this._event.onRelease.map(e => Object({ handler: e.handler, maxTimes: e.maxTimes, times: entity.player._clientEvents.get(id)?.onRelease.times ?? 0 })),
onFocus: this._event.onFocus.map(e => Object({ handler: e.handler, maxTimes: e.maxTimes, times: entity.player._clientEvents.get(id)?.onFocus.times ?? 0 })),
onBlur: this._event.onBlur.map(e => Object({ handler: e.handler, maxTimes: e.maxTimes, times: entity.player._clientEvents.get(id)?.onBlur.times ?? 0 }))
});
return {
protocols: "renderMessage",
id: id,
name: this.name,
data: this.data,
style: this.style,
pointerEventBehavior: (Number(this.pointerEventBehavior.enable) << 1) | Number(!this.pointerEventBehavior.passThrough),
autoResize: (Number(this.autoResize.x) | Number(this.autoResize.y)) ? ((this.autoResize.x ? 'X' : '') + (this.autoResize.y ? 'Y' : '')) : 'NONE',
placeholder: this.placeholder,
size: this.size,
position: this.position,
anchor: this.anchor,
visible: this.visible,
parent: parentId,
content: this.content,
type: this.type,
events: {
onPress: this._event.onPress.filter(event => event.maxTimes > event.times).length > 0,
onRelease: this._event.onRelease.filter(event => event.maxTimes > event.times).length > 0,
onFocus: this._event.onFocus.filter(event => event.maxTimes > event.times).length > 0,
onBlur: this._event.onBlur.filter(event => event.maxTimes > event.times).length > 0
}
}
}
}
function createNode(type, name) {
return new NodeBlueprint(type, name);
}
/**
* 渲染节点
* @param {NodeBlueprint} node 要渲染的节点
* @param {GameEntity} entity 要进行渲染的玩家
* @param {number} parentId 父节点id,默认为`0`(根节点)
* @param {?number} id 节点id,可不填。若填写已经存在的id,则表示重新渲染已经渲染的节点。
* @returns {number} 打包后的节点id。此id是修改和删除该节点的唯一标识
*/
function renderNode(node, entity, parentId = 0, id = null) {
var packedNode = node._pack(entity, parentId, id);
sendClientEvent(packedNode, entity);
return packedNode.id;
}
/**
* 移除节点
* @param {number} id 要移除的节点序号。若填写无效数据,则在**客户端**抛出错误
* @param {GameEntity} entity 要进行移除节点的玩家
*/
function removeNode(id, entity) {
sendClientEvent({ protocols: "removeMessage", id: id }, entity);
}
/**
* 发送客户端事件
* @param {PackedData} data
* @param {...GameEntity} entities
*/
function sendClientEvent(data, ...entities) {
remoteChannel.sendClientEvent(entities, data);
}
EBL.regE('onClientLockPointer');
EBL.regE('onClientUnlockPointer');
remoteChannel.onServerEvent(({ entity, tick,/**@type {PackedData}*/args }) => {
switch (args.protocols) {
case "triggeredEvent":
switch (args.eventName) {
case EventName.onLockChange:
EBL.triE('onClientLockPointer', { entity, tick });
break;
case EventName.onLockError:
EBL.triE('onClientUnlockPointer', { entity, tick });
break;
default:
/**
* @type {EntityNodeEvent}
*/
let events = entity.player._clientEvents.get(args.node.id);
events[args.eventName].forEach(async event => {
if (event.times >= event.maxTimes)
return;
event.handler(args.node, entity);
++event.times;
});
break;
}
break;
}
});
/**
* 对指定玩家抛出错误
* @param {GameEntity} entity 要抛出错误的玩家
* @param {...string} message 错误消息
*/
function BSOD(entity, ...message){
sendClientEvent({protocols: "bsod", message}, entity);
throw message.join(' ');
}
// ----- S-C-Link_server End -----
const SCLink = {
Vector2,
Coord2,
EventName,
NodeType,
createNode,
renderNode,
removeNode,
BSOD
};
/**
* BehaviorLib的全局对象
* @global
*/
global.SCLink = SCLink;
console.log("S-C-Link_server", VERSION.join('.'));
module.exports = SCLink;
//客户端只要根据服务端的数据渲染节点就好了,而服务端就要考虑的事情就很多了
//服务端只要给客户端丢数据就好了,而客户端要考虑的事情就很多了
//remoteChannel就像一根可双向通行的大管道,里面啥都有