猫史档案馆


RemoteWrapper.js - 端与端简易通信包装器/支持信息交换与分组监听

用户:NomenNomen查看:3 回复:12 评论:3 创建时间:2023-12-24T03:54:04


这里是没有开场白的Nomen,最近阿吉又发新API了,看着很有意思,但是实际文档 和效果相当迷人,所以我决定将官方API包装起来并且自定义事件喵,同时还支持分组监听管理与双向信息交换

如果还没有搞懂新API,可以前往这个帖子进行简单的入门

shequ.codemao.cn/community/674361

这里是一份例子,使用了七莓大佬的UiWrapper.client.js

/**
在RemoteWrapper中,通信分为两种协议:data和communicate
data用于直接传输消息,而communicate则用于交换消息

communicate会话中,接收方必须回应,否则可能导致无限等待,在服务端RemoteServerWrapper提供了timer函数用于控制超时
如果想要直接向客户端发送消息,可以直接操作rmsInstance.client.send发送原始数据
*/

// 玩家进行抽卡的例子
// 当玩家点击抽卡按钮,服务端返回结果
// 使用七莓大佬的UiWrapper作为按钮进行演示

// CLIENT clientIndex.js
let rmc = RemoteClientWrapper();

pullButton.eventOnce("pointerdown", ()=>{
    rmc.communicate({
        action: "抽卡", // 发送的自定义的内容,可以是你想要的任何东西
    },(result)=>{
        CREATE_DIALOG(result.message); // 显示弹窗,展示服务端发送的结果,结果应该是 {message: "无锋剑x1"}
    });
});


// SERVER index.js
world.onPlayerJoin(({ entity }) => {
    let rms = new RemoteServerWrapper(entity); // 初始化管理器
    rms.onMessage("communicate", async (request, entity) => {
        if(request.action === "抽卡"){
            entity.yuanshi -= 160;
            let result = RANDOM_TRASH(); // 随机生成抽卡结果
            await entity.UPDATE_DATA();
            return {message: result}; // 直接作为函数返回即可,函数可以是异步函数
        }
    })
});


// 另一个例子,展示简易聊天框
// CLIENT clientIndex.js
let rmc = RemoteClientWrapper();
sendButton.eventOnce("pointerdown", ()=>{
    rmc.send(chatBox.findChildByName("textbox").text); // 假想的文字输入框
    rmc.onMessage("data", (message)=>{ // 收到来自服务器的新消息
        chatBox.createChildNode(UiText)
            .config("textContent", message); // 创建文本并且显示给玩家
    })
});


// SERVER index.js
let rms = new RemoteServerWrapper(); // 全局玩家聊天管理器
world.onPlayerJoin(({ entity }) => {
    rms.add(entity); // 将玩家加入管理器
    RemoteServerWrapper.listen(entity, "data", (arg)=>{ // 单独监听玩家,实际作用和onMessage差不多
        rms.send(arg, "data"); // 向所有收管理的玩家广播该玩家发送的消息
    });
});
world.onPlayerLeave(({entity})=>{
    rms.cancel(entity); // 当玩家退出的时候取消管理这名玩家以释放资源
})

以下为Remote.server.js,需要有效地放置在服务端

class RemoteGroups {
    constructor(target) {
        this.target = Array.isArray(target) ? target : [target];
        this.listeners = [];
    }
    send(message) {
        remoteChannel.sendClientEvent(this.target, message);
    }
    sendOne(entity, message) {
        if (this.target.includes(entity)) {
            remoteChannel.sendClientEvent([entity], message)
        }
    }
    each(c) {
        this.target.forEach(v => c(v, this));
    }
    add(entity) {
        this.target.push(entity);
    }
    cancel(entity) {
        this.target = this.target.filter(v => v !== entity);
    }
    stop() {
        this.listeners.forEach(v => v.cancel());
        this.target = [];
        this.listeners = [];
    }
    _register(f, hdls) {
        this.listeners.push(remoteChannel.onServerEvent((a) => {
            if (this.target.includes(a.entity) && typeof a.args?.id !== "number") hdls[a.args?.id]?.(a);
            if (this.target.includes(a.entity)) f(a);
        }));
    }
}

/**
 * @callback WhenGetMessage
 * @param {JSONValue|} arg 客户端发送的数据
 */
/**
 * @typedef {Object} RemoteServerEventToken
 * @property {Function} cancel 取消事件
 */
/**
 * @typedef {Object} RemoteServerEvent
 * @property {GameEntity} type 玩家实例
 * @property {WhenGetMessage} handler 回调函数,当事件触发时执行
 * @property {number} id 会话id
 */
class RemoteServerWrapper {
    static quitCode = {
        "1": "传入的实体/实体组不正确",
        "2": "非服务端"
    }
    /**@type {Map<GameEntity, RemoteServerEvent>} */
    static events = new Map();
    /**
     * 监听特定玩家触发的事件
     * @param {GameEntity} entity 要监听的玩家
     * @param {"data"|"communicate"} type 会话类型
     * @param {WhenGetMessage} f 回调函数,当事件触发时执行
     * @param {number?} 会话id,如果不指定则为通用触发器,指定后仅该id的会话会被捕获
     * @returns {RemoteServerEventToken} 调用返回值的cancel属性取消事件
     */
    static listen(entity, type = "data", f, id) {
        if (!this.events.get(entity)) this.events.set(entity, []);
        this.events.get(entity).push({ type, handler: f, id });
        return {
            cancel: () => {
                this.off(entity, f);
            }
        }
    }
    /**
     * 仅监听一次特定玩家触发的事件
     * @param {GameEntity} entity 要监听的玩家
     * @param {"data"|"communicate"} type 会话类型
     * @param {WhenGetMessage} f 回调函数,当事件触发时执行
     * @param {number?} id 会话id,如果不指定则为通用触发器,指定后仅该id的会话会被捕获
     * @returns {RemoteServerEventToken} 调用返回值的cancel属性取消事件
     */
    static once(entity, type = "data", f, id) {
        if (!this.events.get(entity)) this.events.set(entity, []);
        let handler = function (...args) {
            this.off(entity, f);
            return f(...args);
        };
        this.events.get(entity).push({
            type, handler, id
        });
        return {
            cancel: () => {
                this.off(entity, handler);
            }
        }
    }
    /**
     * 向特定客户端广播消息
     * @param {GameEntity} entity 要监听的玩家
     * @param {JSONValue} message 向客户端发送的可序列化数据
     * @param {"data"|"communicate"} type 会话类型
     * @param {number} id 会话id,可通过这个id追踪会话并且监听
     * @returns {number} 会话id
     */
    static send(entity, message, type = "data", id = Date.now()) {
        remoteChannel.sendClientEvent([entity], this.wrap(type, message, id));
        return id;
    }
    static emit(type, ctype, self, id, ...args) {
        if (!this.events.get(type)) this.events.set(type, []);
        this.events.get(type).forEach(v => {
            if (typeof v.handler === "function" && (!id || !v.id || (id && id === v.id)) && v.type === ctype) {
                if (ctype === "data") v.handler.apply(this, args)
                else if (ctype === "communicate") {
                    let res = v.handler.apply(this, args);
                    if (res.then) res.then(r => self.sendOne(type, r, "data", id))
                    else self.sendOne(type, res, "data", id)
                }
            }
        });
    }
    static off(entity, f) {
        if (this.events.get(entity)) this.events.set(entity, this.events.get(entity).filter(v => v.handler !== f));
    }
    static async timer(f, ms) {
        let done = false, rejected = false, timeout;
        try {
            let result = await Promise.race([(async () => { let res = await f(); clearTimeout(timeout); done = true; return res })(),
            new Promise((_r, reject) => {
                timeout = setTimeout(() => {
                    rejected = done = true;
                    reject("Timeout");
                }, ms);
            })]);
            if (result && done && !rejected) return result;
        } catch (err) {
            if (!done) throw err;
            if (rejected) return new Error("Timeout");
        }
    }
    constructor(target) {
        if ((Array.isArray(target) && target.every(v => v instanceof GameEntity)) || target instanceof GameEntity) this.client = new RemoteGroups(target)
        else throw this._quit("1");
        try { remoteChannel } catch { throw this._quit("2") };
        this._start();
    }
    /**
     * 监听受管理的玩家发送的消息
     * @param {"data"|"communicate"} type 会话类型
     * @param {WhenGetMessage} f 回调函数,当事件触发时执行
     */
    onMessage(type, f) {
        this.client.target.forEach(entity => {
            if (!this.constructor.events.get(entity)) this.constructor.events.set(entity, []);
            this.constructor.events.get(entity).push({ type, handler: f });
        });
    }
    /**
     * 向所有当前实例管理的客户端广播消息
     * @param {JSONValue} message 向客户端发送的可序列化数据
     * @param {"data"|"communicate"} type 会话类型
     * @param {number} id 会话id,可通过这个id追踪会话并且监听
     * @returns {number} 会话id
     */
    send(message, type = "data", id = Date.now()) {
        this.client.send(this.wrap(type, message, id));
        return id;
    }
    /**
     * 向特定客户端发送消息
     * @param {GameEntity} entity 指定发送的玩家
     * @param {JSONValue} message 向客户端发送的可序列化数据
     * @param {"data"|"communicate"} type 会话类型
     * @param {number} id 会话id,可通过这个id追踪会话并且监听
     * @returns {number} 会话id
     */
    sendOne(entity, message, type = "data", id = Date.now()) {
        this.client.sendOne(entity, this.wrap(type, message, id));
        return id;
    }
    /**
     * 添加管理特定实体
     * @param {GameEntity} entity 添加管理的实体
     */
    add(entity) {
        this.client.add(entity);
    }
    /**
     * 取消管理特定实体,并且清除共享事件管理器中的受管理单位事件
     * @param {GameEntity} entity 取消管理的实体
     */
    cancel(entity) {
        this.client.cancel(entity);
        this.constructor.events.delete(entity);
    }
    /**
     * 终止当前实例,并且清除共享事件管理器中的受管理单位事件  
     * 这仅应该在你认为你需要放弃该实例并且优化资源时才能调用,停止后尝试调用实例方法可能导致错误
     */
    stop() {
        this.client.target.forEach(v => this.constructor.events.delete(entity));
        this.client.stop();
    }
    /**
     * 向所有当前实例管理的客户端交换数据  
     * 返回的数组包含着所有结果
     * @param {JSONValue} data 向客户端发送的可序列化数据
     * @param {Function} rec 当收到某个客户端返回的数据时触发回调
     * @returns {Promise<JSONValue|Error>[]} 当所有客户端均响应,返回结果数组(即使出错)
     */
    async communicateEntities(data, rec) {
        let r = [];
        this.client.each((t) => {
            r.push(new Promise(resolve => {
                if (t && !t.destroyed) {
                    this.constructor.once(t, "data", (args) => { rec(args.data); resolve(args.data); }, this.send(data, "communicate"));
                }
            }))
        });
        return Promise.all(r);
    }
    /**
     * 向特定的实例交换数据
     * @param {GameEntity} entity 玩家实例
     * @param {JSONValue} data 向客户端发送的可序列化数据
     * @param {Function} rec 当收到某个客户端返回的数据时触发回调
     * @returns {Promise<JSONValue|Error>} 当客户端均响应,返回结果(即使出错)
     */
    async communicate(entity, data, rec) {
        if (!this.client.target.includes(entity)) return null;
        return new Promise(resolve => {
            if (entity && !entity.destroyed) {
                this.constructor.once(entity, "data", (args) => { rec(args.data); resolve(args.data); }, this.sendOne(entity, data, "communicate"))
            }
        });
    }
    wrap(type, data, id) {
        return {
            type,
            data,
            id
        }
    }
    _toData(data) {
        if (typeof data === "object" && data !== null) return data
        else return { data };
    }
    _start() {
        this.client._register(({ entity, args }) => {
            this.constructor.emit(entity, this._toData(args).type, this, this._toData(args).id, args, entity);
        },{});
    }
    _quit(c) {
        return new Error(`[Remote Server] ${this.constructor.quitCode[c]}`);
    }
}

以下为Remote.client.js,需要有效地放置在客户端

function RemoteClientWrapper() {
    let events = [], seed = 2024, random = () => (seed = (seed * 9301 + 49297) % 233280, parseInt(seed / 233280.0 * 1000000));
    function wrap(type, data, id) {
        return { type, data, id }
    }
    function send(data, id, type = "data") {
        console.log("sent")
        remoteChannel.sendServerEvent(wrap(type, data, id));
        return id;
    }
    function onMessage(type = "data", f) {
        events.push({ type, handler: f });
        return { cancel() { events = events.filter(v => v.handler !== f); } }
    }
    function communicate(data, f) {
        events.push({
            type: "communicate", handler: (arg) => {
                f(arg);
                events = events.filter(v => v.handler !== f);
            }, id: send(data, random(), "communicate")
        });
    }
    remoteChannel.events.on("client", arg => {
        if (arg.type === "communicate") {
            events.filter(v => v.type === arg.type).forEach(f => {
                let res = f.handler(arg.data);
                if (res.then) res.then(r => send(r, arg.id))
                else send(res, arg.id);
            });
        } else if (arg.type === "data") {
            events.filter(v => v.type === arg.type).forEach(f => f.handler(arg.data));
        }
    });
    return {
        send,
        onMessage,
        communicate
    }
}

这是Remote系列第一个版本,可能会存在大量漏洞与错误,但还是感谢你看完,我们下次再见


回复

上一页1 页 / 共 1下一页
Arafel晓星Arafel晓星

你起的跟我一样早诶!((((

点赞0


评论


NomenNomen

改一下实例代码

// 另一个例子,展示简易聊天框
// CLIENT clientIndex.js
let rmc = RemoteClientWrapper();
sendButton.eventOnce("pointerdown", ()=>{
    rmc.send(chatBox.findChildByName("textbox").text); // 假想的文字输入框
});
rmc.onMessage("data", (message)=>{ // 收到来自服务器的新消息
    chatBox.createChildNode(UiText)
        .config("textContent", message); // 创建文本并且显示给玩家
})

点赞0


评论


追梦ez追梦ez

板凳)(

点赞0


评论


七式草莓七式草莓

666,用了 UiWrapper 居然不给下载链接(推荐使用你的 github 链接,方便更新)!

另外请注意,UiText 不具备输入功能,你可以把简易聊天框改成选择选项然后提交。

点赞0


评论


七式草莓七式草莓

这里标一下UiWrapper1.1.0的下载链接:UiWrapper-1.1.0.7z:https://static.codemao.cn/coco/player/unstable/rJqP3QVvT.7z

点赞0


评论


NomenNomen

UiWrapper是一个由七式喵莓开发的GUI管理工具,包装并且简化了GUI操作,并且拥有着完整的JSDoc与文档教程 现已发布至Box3-Tools仓库,并且长期更新维护 github.com/helloyork/Box3-Tools/tree/main/src/GUI/UiWrapper/1.1.0

点赞0


评论


初夏晴雨初夏晴雨

%%%%%%,Nomen好厉害!

点赞1


评论


Star_Ink_sansStar_Ink_sans

ddd

点赞0


评论


神岛吉吉喵神岛吉吉喵

imgsrc="https://static.codemao.cn/emoji/codemao/%E7%BC%96%E7%A8%8B%E7%8C%AB_%E5%8E%89%E5%AE%B3%E4%BA%86.gif"alt="emotion_编程猫_厉害了"

点赞0


评论


NomenNomen

1.1.0版本更新编程猫社区-RemoteWrapper.js1.1.0-虚拟端口更新-编程学习交流(codemao.cn)

点赞0


评论


一个STUB用户_7175426一个STUB用户_7175426

七某的那玩意真的不算过度封装吗。。。

点赞0


评论


没尾巴的鲸鱼_没尾巴的鲸鱼_

怎么用玩家的摄像机角度计算出特定坐标在玩家屏幕的显示位置

点赞0


评论