feat: 合并星图和聊天页面为 MainPage,响应式 GridRow 布局
- 新建 MainPage.ets,用 GridRow 响应式布局组合星图+聊天 - Index.ets 改为 2 Tab(TrulyMEM + 设置) - GraphPage/ChatPage 接收 @Prop db 从父组件传入 - 恢复 README.md / README_EN.md 的特别鸣谢部分
This commit is contained in:
21
harmonyos/trulymem-core/src/main/ets/GraphDatabase.ets
Normal file
21
harmonyos/trulymem-core/src/main/ets/GraphDatabase.ets
Normal file
@ -0,0 +1,21 @@
|
||||
import relationalStore from '@ohos.data.relationalStore';
|
||||
import { Context } from '@ohos.abilityAccessCtrl';
|
||||
|
||||
const STORE_CONFIG: relationalStore.StoreConfig = {
|
||||
name: 'trulymem.db',
|
||||
securityLevel: relationalStore.SecurityLevel.S1
|
||||
};
|
||||
|
||||
export class GraphDatabase {
|
||||
private store?: relationalStore.RdbStore;
|
||||
private context?: Context;
|
||||
|
||||
async init(context: Context) {
|
||||
this.context = context;
|
||||
this.store = await relationalStore.getRdbStore(context, STORE_CONFIG);
|
||||
}
|
||||
|
||||
getStore(): relationalStore.RdbStore | undefined {
|
||||
return this.store;
|
||||
}
|
||||
}
|
||||
208
harmonyos/trulymem-core/src/main/ets/components/RconClient.ets
Normal file
208
harmonyos/trulymem-core/src/main/ets/components/RconClient.ets
Normal file
@ -0,0 +1,208 @@
|
||||
import { socket } from '@kit.NetworkKit';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import {RConResult,parseRCONBuffer,buildPacket,GetAuth} from '../components/tools';
|
||||
|
||||
type RConCallback = (result: RConResult) => void;
|
||||
|
||||
export class RConClient
|
||||
{
|
||||
public isActive: boolean; //是否可用标志位,随tcp状态更改
|
||||
private tcp: socket.TCPSocket; //socket连接
|
||||
private ipAddress: socket.NetAddress; //ip地址
|
||||
private passWord: string; //密码
|
||||
private isConnecting: boolean = false; // 标记连接是否正在进行中,防止并发连接
|
||||
private closeHandler: (() => void) | null = null; // 保存 close 事件处理器引用以便清理
|
||||
private authResolve: (() => void) | null = null; // 认证完成信号
|
||||
private nextPacketId: number = 1; // 递增的包序号,避免重复
|
||||
|
||||
constructor(ip:string,LPort:number,PassWord:string) {
|
||||
this.isActive = false;
|
||||
this.tcp = socket.constructTCPSocketInstance();
|
||||
this.ipAddress = {
|
||||
address: ip,
|
||||
port: LPort
|
||||
}
|
||||
this.passWord = PassWord;//初始化tcp连接相关配置
|
||||
}
|
||||
//连接服务器
|
||||
async setConnect(timeOut:number,callBack:RConCallback)//异步获取服务器连接
|
||||
{
|
||||
if (this.isConnecting) {
|
||||
// Already connecting, return an error via callback
|
||||
callBack(RConResult.error("ALREADY_CONNECTING", "A connection attempt is already in progress"));
|
||||
return;
|
||||
}
|
||||
this.isConnecting = true;
|
||||
let finished = false; // To ensure we set isConnecting to false only once
|
||||
const setFinished = () => {
|
||||
if (!finished) {
|
||||
finished = true;
|
||||
this.isConnecting = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 清理旧的事件监听器
|
||||
this.cleanupListeners();
|
||||
|
||||
let connectOptions: socket.TCPConnectOptions = {
|
||||
address: this.ipAddress,
|
||||
timeout: timeOut
|
||||
};
|
||||
|
||||
await this.tcp.connect(connectOptions).then(() => {
|
||||
console.info('Connection established');
|
||||
let result:RConResult = RConResult.result(undefined,"tcp_ok")
|
||||
// 注册 close 事件并保存引用
|
||||
this.closeHandler = () => {
|
||||
this.isActive = false;
|
||||
};
|
||||
this.tcp.on('close', this.closeHandler);
|
||||
//发送认证信息
|
||||
GetAuth(this.tcp,this.passWord,3,(result:RConResult)=>{
|
||||
setFinished();
|
||||
if(!result.success) {
|
||||
callBack(result);
|
||||
// 通知等待的 sendAfterAuth 认证失败
|
||||
this.notifyAuthComplete();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.isActive = true;
|
||||
callBack(result);
|
||||
// 通知等待的 sendAfterAuth 认证成功
|
||||
this.notifyAuthComplete();
|
||||
}
|
||||
|
||||
})
|
||||
}).catch((err: BusinessError) => {
|
||||
setFinished();
|
||||
let result = RConResult.error(JSON.stringify({ code: err.code, message: err.message }), "tcp_fail");
|
||||
callBack(result);
|
||||
this.notifyAuthComplete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知认证流程完成(供 sendAfterAuth 等待机制使用)
|
||||
*/
|
||||
private notifyAuthComplete(): void {
|
||||
if (this.authResolve) {
|
||||
this.authResolve();
|
||||
this.authResolve = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下一个递增包序号(跳过 -1,因为 -1 在 RCON 协议中表示认证失败)
|
||||
*/
|
||||
private getNextPacketId(): number {
|
||||
const id = this.nextPacketId;
|
||||
this.nextPacketId++;
|
||||
if (this.nextPacketId === -1 || this.nextPacketId > 2147483646) {
|
||||
this.nextPacketId = 1;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有事件监听器
|
||||
*/
|
||||
private cleanupListeners(): void {
|
||||
this.tcp.off("close");
|
||||
this.tcp.off("message");
|
||||
this.closeHandler = null;
|
||||
}
|
||||
|
||||
async sendMessage(message:string,type:number,callBack:RConCallback)
|
||||
{//发送一条指令
|
||||
if(!this.isActive)//错误重连机制
|
||||
{
|
||||
await this.setConnect(1000,(result:RConResult)=>{
|
||||
if (!result.success) {
|
||||
// Connection failed, call the outer callback with the error
|
||||
callBack(result);
|
||||
return;
|
||||
}
|
||||
// 重连后需要重新认证,不能直接发送命令
|
||||
// 等待认证完成后再发送消息
|
||||
this.sendAfterAuth(message, type, callBack);
|
||||
})
|
||||
}
|
||||
else {
|
||||
this.sendWithResponse(message, type, callBack);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息并等待响应(正常路径)
|
||||
*/
|
||||
private sendWithResponse(message: string, type: number, callBack: RConCallback): void {
|
||||
const packetId = this.getNextPacketId();
|
||||
const messageHandler = (info: socket.SocketMessageInfo) => {
|
||||
const buffer: ArrayBuffer = info.message;
|
||||
const result = parseRCONBuffer(buffer);
|
||||
callBack(result);
|
||||
this.tcp.off("message", messageHandler);
|
||||
};
|
||||
this.tcp.on("message", messageHandler);
|
||||
|
||||
let sendOption: socket.TCPSendOptions = { data: buildPacket(packetId, type, message) };
|
||||
this.tcp.send(sendOption).catch((err: BusinessError) => {
|
||||
let result = RConResult.error(JSON.stringify({ code: err.code, message: err.message }), "send_auth_fail");
|
||||
callBack(result);
|
||||
this.tcp.off("message", messageHandler);
|
||||
this.setDisconnect();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 重连后等待认证完成再发送消息
|
||||
* 使用 Promise 等待机制替代轮询
|
||||
*/
|
||||
private sendAfterAuth(message: string, type: number, callBack: RConCallback): void {
|
||||
// 创建认证等待 Promise(最多 5 秒)
|
||||
const authPromise = new Promise<boolean>((resolve) => {
|
||||
this.authResolve = () => resolve(this.isActive);
|
||||
// 超时兜底
|
||||
setTimeout(() => {
|
||||
if (this.authResolve) {
|
||||
this.authResolve = null;
|
||||
resolve(false);
|
||||
}
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
authPromise.then((authSuccess: boolean) => {
|
||||
if (authSuccess) {
|
||||
this.sendWithResponse(message, type, callBack);
|
||||
} else {
|
||||
let result = RConResult.error("AUTH_TIMEOUT", "Authentication timed out after reconnect");
|
||||
callBack(result);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async resetServer(ip:string,password:string,port:number,timeout:number,callback:RConCallback)
|
||||
{//重新设置服务器配置
|
||||
this.ipAddress = {
|
||||
address:ip,
|
||||
port:port
|
||||
}
|
||||
this.passWord = password;
|
||||
this.setDisconnect();//断开放弃使用的tcp连接
|
||||
await this.setConnect(timeout,callback);
|
||||
}
|
||||
setDisconnect()
|
||||
{
|
||||
// 清理所有事件监听器
|
||||
this.cleanupListeners();
|
||||
// 断链
|
||||
try {
|
||||
this.tcp.close();
|
||||
} catch {
|
||||
// Socket already closed
|
||||
}
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
}
|
||||
194
harmonyos/trulymem-core/src/main/ets/components/tools.ets
Normal file
194
harmonyos/trulymem-core/src/main/ets/components/tools.ets
Normal file
@ -0,0 +1,194 @@
|
||||
import { socket } from '@kit.NetworkKit';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { util } from '@kit.ArkTS';
|
||||
|
||||
const MAXLENGTH:number = 1447;
|
||||
|
||||
/**
|
||||
* 手动 UTF-8 编码(替代已弃用的 util.TextEncoder.encode/encodeInto)
|
||||
*/
|
||||
function utf8Encode(str: string): Uint8Array {
|
||||
const bytes: number[] = [];
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const code = str.charCodeAt(i);
|
||||
if (code <= 0x7F) {
|
||||
bytes.push(code);
|
||||
} else if (code <= 0x07FF) {
|
||||
bytes.push(0xC0 | (code >> 6));
|
||||
bytes.push(0x80 | (code & 0x3F));
|
||||
} else if (code <= 0xFFFF) {
|
||||
bytes.push(0xE0 | (code >> 12));
|
||||
bytes.push(0x80 | ((code >> 6) & 0x3F));
|
||||
bytes.push(0x80 | (code & 0x3F));
|
||||
} else {
|
||||
bytes.push(0xF0 | (code >> 18));
|
||||
bytes.push(0x80 | ((code >> 12) & 0x3F));
|
||||
bytes.push(0x80 | ((code >> 6) & 0x3F));
|
||||
bytes.push(0x80 | (code & 0x3F));
|
||||
}
|
||||
}
|
||||
return new Uint8Array(bytes);
|
||||
}
|
||||
|
||||
export class RConResult {
|
||||
success: boolean = false; // 操作是否成功
|
||||
errorCode?: string; // 错误码
|
||||
errorMessage?: string; // 错误描述
|
||||
rawData?: ArrayBuffer;
|
||||
resultText ?:string; // 附加数据
|
||||
type ?:number;
|
||||
id ?:number;
|
||||
static result(data?:ArrayBuffer,stringResult?:string,type?:number,id?:number):RConResult{
|
||||
const result = new RConResult();
|
||||
result.success = true;
|
||||
if(data !== undefined){result.rawData = data;}
|
||||
if(stringResult !== undefined){result.resultText = stringResult;}
|
||||
if(type !== undefined){result.type = type;}
|
||||
if(id !== undefined){result.id = id;}
|
||||
return result;
|
||||
}
|
||||
static error(code: string, message: string): RConResult {
|
||||
const result = new RConResult();
|
||||
result.success = false;
|
||||
result.errorCode = code;
|
||||
result.errorMessage = message;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export function buildPacket(packetId: number, type: number, payload: string): ArrayBuffer
|
||||
{
|
||||
// 使用自定义 UTF-8 编码器替代已弃用的 util.TextEncoder
|
||||
const encodedPayload: Uint8Array = utf8Encode(payload);
|
||||
const payloadWithTerminator: Uint8Array = new Uint8Array(encodedPayload.length + 1);
|
||||
payloadWithTerminator.set(encodedPayload);
|
||||
payloadWithTerminator[encodedPayload.length] = 0x00; // 添加空终止符
|
||||
|
||||
// 检查编码后的字节长度是否符合 RCON 协议限制(payload + null terminator + padding <= 1460)
|
||||
// 即 encodedPayload.length + 1 (null) + 1 (padding) <= 1460 - 4 (length) - 4 (id) - 4 (type)
|
||||
// 即 encodedPayload.length <= 1446
|
||||
if (encodedPayload.length > MAXLENGTH) {
|
||||
console.warn(`[RCON] Payload truncated from ${encodedPayload.length} to ${MAXLENGTH} bytes (RCON protocol limit)`);
|
||||
// 截断到最大字节长度
|
||||
const truncated = new Uint8Array(MAXLENGTH);
|
||||
truncated.set(encodedPayload.subarray(0, MAXLENGTH));
|
||||
payloadWithTerminator.set(truncated);
|
||||
}
|
||||
|
||||
// 计算各字段长度(单位:字节)
|
||||
const idFieldSize: number = 4;
|
||||
const typeFieldSize: number = 4;
|
||||
const payloadSize: number = payloadWithTerminator.byteLength;
|
||||
const paddingSize: number = 1;
|
||||
|
||||
// 总长度计算(不包含长度字段自身)
|
||||
const packetLength: number =
|
||||
idFieldSize +
|
||||
typeFieldSize +
|
||||
payloadSize +
|
||||
paddingSize;
|
||||
|
||||
// 创建二进制缓冲区
|
||||
const buffer: ArrayBuffer = new ArrayBuffer(
|
||||
packetLength + 4 // 包含长度字段自身
|
||||
);
|
||||
const view: DataView = new DataView(buffer);
|
||||
|
||||
// 写入字段(小端序)
|
||||
view.setInt32(0, packetLength, true); // 长度字段
|
||||
view.setInt32(4, packetId, true); // ID字段
|
||||
view.setInt32(8, type, true); // 类型字段
|
||||
|
||||
// 写入负载内容
|
||||
const payloadStartOffset: number = 12;
|
||||
new Uint8Array(buffer, payloadStartOffset).set(payloadWithTerminator);
|
||||
|
||||
// 添加填充字节
|
||||
const paddingOffset: number = payloadStartOffset + payloadSize;
|
||||
new Uint8Array(buffer, paddingOffset, paddingSize).fill(0x00);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
export function parseRCONBuffer(buffer: ArrayBuffer): RConResult
|
||||
{
|
||||
const view: DataView = new DataView(buffer);
|
||||
|
||||
// 基础头校验
|
||||
if (buffer.byteLength < 12) {
|
||||
return RConResult.error("INVALID_HEADER", "Packet length less than 12 bytes");
|
||||
}
|
||||
|
||||
// 小端序解析头字段
|
||||
const length: number = view.getInt32(0, true);
|
||||
const requestId: number = view.getInt32(4, true);
|
||||
const type: number = view.getInt32(8, true);
|
||||
|
||||
// 负载终止符定位
|
||||
let payloadEnd: number = 12;
|
||||
const maxPosition: number = Math.min(12 + length - 10, buffer.byteLength);
|
||||
while (payloadEnd < maxPosition) {
|
||||
if (view.getUint8(payloadEnd) === 0x00) break;
|
||||
payloadEnd++;
|
||||
}
|
||||
|
||||
// 提取有效负载
|
||||
const payloadBytes: Uint8Array = new Uint8Array(
|
||||
buffer.slice(12, payloadEnd)
|
||||
);
|
||||
// 解码 UTF-8 字符串
|
||||
const textDecoderOptions: util.TextDecoderOptions = {
|
||||
fatal: false,
|
||||
ignoreBOM: true
|
||||
};
|
||||
const decodeToStringOptions: util.DecodeToStringOptions = {
|
||||
stream: false
|
||||
};
|
||||
const textDecoder: util.TextDecoder = util.TextDecoder.create('utf-8', textDecoderOptions);
|
||||
const payloadString: string = textDecoder.decodeToString(payloadBytes, decodeToStringOptions);
|
||||
const result: RConResult = RConResult.result(buffer, payloadString, type, requestId);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function GetAuth(Connection:socket.TCPSocket,password:string,authRequestId:number,callBack :Function)
|
||||
{
|
||||
// 先注册监听器,再发送数据,防止快速响应丢失
|
||||
let messageHandlerRegistered = true;
|
||||
const messageHandler = (info: socket.SocketMessageInfo) => {
|
||||
const buffer:ArrayBuffer = info.message;
|
||||
const result = parseRCONBuffer(buffer);
|
||||
// 认证响应处理
|
||||
if (result.type === 2) {
|
||||
if (result?.id === authRequestId) {
|
||||
console.log("Authentication success");
|
||||
callBack(result);
|
||||
if (messageHandlerRegistered) {
|
||||
Connection.off('message', messageHandler);
|
||||
messageHandlerRegistered = false;
|
||||
}
|
||||
} else if (result?.id === -1) {
|
||||
console.error("Authentication failed");
|
||||
callBack(RConResult.error("Authentication failed","password wrong"));
|
||||
if (messageHandlerRegistered) {
|
||||
Connection.off('message', messageHandler);
|
||||
messageHandlerRegistered = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Connection.on('message', messageHandler);
|
||||
|
||||
// 修复:packetId 必须与 authRequestId 一致,服务器会回显此 ID
|
||||
let sendOption:socket.TCPSendOptions = {data : buildPacket(authRequestId, 3, password)};
|
||||
await Connection.send(sendOption).then(()=>{
|
||||
let result:RConResult = RConResult.result(undefined,"send_auth_ok")
|
||||
callBack(result);
|
||||
}).catch((err: BusinessError) => {
|
||||
let result = RConResult.error(JSON.stringify({ code: err.code, message: err.message }), "send_auth_fail");
|
||||
callBack(result);
|
||||
if (messageHandlerRegistered) {
|
||||
Connection.off('message', messageHandler);
|
||||
messageHandlerRegistered = false;
|
||||
}
|
||||
})
|
||||
}
|
||||
13
harmonyos/trulymem-core/src/main/module.json5
Normal file
13
harmonyos/trulymem-core/src/main/module.json5
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"module": {
|
||||
"name": "librcon",
|
||||
"type": "har",
|
||||
"requestPermissions": [
|
||||
],
|
||||
"deviceTypes": [
|
||||
"default",
|
||||
"tablet",
|
||||
"2in1"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
{
|
||||
"float": [
|
||||
{
|
||||
"name": "page_text_font_size",
|
||||
"value": "50fp"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
{
|
||||
"string": [
|
||||
{
|
||||
"name": "page_show",
|
||||
"value": "page from package"
|
||||
},
|
||||
{
|
||||
"name": "permmsion_for_tcp",
|
||||
"value": "to connect with rcon port opened in game servers"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user