First
This commit is contained in:
commit
052db244e6
22 changed files with 2535 additions and 0 deletions
476
src/NpcControl.ts
Normal file
476
src/NpcControl.ts
Normal file
|
|
@ -0,0 +1,476 @@
|
|||
import { GBufferReader, GBufferWriter } from './GBuffer';
|
||||
import { ProtocolGen } from './GProtocol';
|
||||
import { GSocket } from './GSocket';
|
||||
import { PacketTable } from './PacketTable';
|
||||
import { PromiseManager } from './PromiseManager';
|
||||
import { gtokenize, guntokenize } from './utils';
|
||||
import { NPC, NPCManager, NPCPropID } from './misc/npcs';
|
||||
import { NCIncomingPacket, NCOutgoingPacket } from './misc/packet';
|
||||
import { NCEvents, NCInterface, ServerlistConfig } from './types';
|
||||
|
||||
enum UriConstants {
|
||||
NpcPrefix = 'npcserver://npcs/',
|
||||
ScriptPrefix = 'npcserver://scripts/',
|
||||
WeaponPrefix = 'npcserver://weapons/',
|
||||
WeaponList = 'npcserver://weapons',
|
||||
LevelList = 'npcserver://levellist',
|
||||
}
|
||||
|
||||
enum ErrorMsg {
|
||||
NotFound = 'Resource not found',
|
||||
}
|
||||
|
||||
interface NpcControlConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
export class NPCControl implements NCInterface {
|
||||
private sock?: GSocket;
|
||||
private readonly packetTable: PacketTable;
|
||||
private readonly eventHandler: NCEvents;
|
||||
|
||||
private promiseMngr: PromiseManager = new PromiseManager();
|
||||
private npcMngr: NPCManager = new NPCManager();
|
||||
private classList: Set<string> = new Set<string>();
|
||||
|
||||
public get classes(): Set<string> {
|
||||
return this.classList;
|
||||
}
|
||||
|
||||
public get npcs(): NPC[] {
|
||||
return this.npcMngr.npcs;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly config: ServerlistConfig,
|
||||
ncConfig: NpcControlConfig,
|
||||
eventHandler: NCEvents,
|
||||
) {
|
||||
this.eventHandler = eventHandler;
|
||||
this.packetTable = this.initializeHandlers();
|
||||
this.connect(ncConfig.host, ncConfig.port);
|
||||
}
|
||||
|
||||
public connect(host: string, port: number): boolean {
|
||||
if (this.sock) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.sock = GSocket.connect(host, port, {
|
||||
connectCallback: () => this.onConnect(),
|
||||
disconnectCallback: () => this.onDisconnect(),
|
||||
packetTable: this.packetTable,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public disconnect(): void {
|
||||
this.sock?.disconnect();
|
||||
}
|
||||
|
||||
////////////////////
|
||||
|
||||
private onConnect(): void {
|
||||
console.log('[NC] Connected!');
|
||||
|
||||
if (!this.sock) {
|
||||
console.log('[NC] no sock?');
|
||||
return;
|
||||
}
|
||||
|
||||
// Send login packet
|
||||
let nb = GBufferWriter.create();
|
||||
nb.writeChars('NCL21075');
|
||||
nb.writeGString(this.config.account);
|
||||
nb.writeGString(this.config.password);
|
||||
this.sock.sendData(this.sock.sendPacket(3, nb.buffer));
|
||||
|
||||
this.sock.setProtocol(ProtocolGen.Gen3, 0);
|
||||
|
||||
// The only proof that you passed verification, is the fact that you are
|
||||
// still connected to the server.
|
||||
this.eventHandler.onNCConnected?.();
|
||||
}
|
||||
|
||||
private onDisconnect(): void {
|
||||
console.log('[NC] Disconnected!');
|
||||
|
||||
if (!this.sock) {
|
||||
console.log('[NC] no sock?');
|
||||
return;
|
||||
}
|
||||
|
||||
this.eventHandler.onNCDisconnected?.();
|
||||
}
|
||||
|
||||
////////////////////
|
||||
|
||||
requestLevelList(): Promise<string> {
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(NCOutgoingPacket.PLI_NC_LEVELLISTGET),
|
||||
);
|
||||
return this.promiseMngr.createPromise(UriConstants.LevelList);
|
||||
}
|
||||
|
||||
deleteWeapon(name: string): void {
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(
|
||||
NCOutgoingPacket.PLI_NC_WEAPONDELETE,
|
||||
Buffer.from(name),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
requestWeaponList(): Promise<Set<string>> {
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(NCOutgoingPacket.PLI_NC_WEAPONLISTGET),
|
||||
);
|
||||
return this.promiseMngr.createPromise(UriConstants.WeaponList);
|
||||
}
|
||||
|
||||
requestWeapon(name: string): Promise<[string, string]> {
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(
|
||||
NCOutgoingPacket.PLI_NC_WEAPONGET,
|
||||
Buffer.from(name),
|
||||
),
|
||||
);
|
||||
return this.promiseMngr.createPromise(UriConstants.WeaponPrefix + name);
|
||||
}
|
||||
|
||||
setWeaponScript(name: string, image: string, script: string): void {
|
||||
// Weapons are sent by replacing newlines with \xa7 character
|
||||
script = script.replace(/\n/g, '§');
|
||||
script = script.replace(/\r/g, '');
|
||||
|
||||
const writer = GBufferWriter.create(
|
||||
1 + name.length + 1 + image.length + script.length,
|
||||
);
|
||||
writer.writeGString(name);
|
||||
writer.writeGString(image);
|
||||
writer.writeChars(script);
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(
|
||||
NCOutgoingPacket.PLI_NC_WEAPONADD,
|
||||
writer.buffer,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
deleteNpc(name: string): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
requestNpcAttributes(name: string): Promise<string> {
|
||||
const npcObject = this.npcMngr.findNPC(name);
|
||||
if (npcObject) {
|
||||
const nb = GBufferWriter.create(3);
|
||||
nb.writeGUInt24(npcObject.id);
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(NCOutgoingPacket.PLI_NC_NPCGET, nb.buffer),
|
||||
);
|
||||
return this.promiseMngr.createPromise(
|
||||
UriConstants.NpcPrefix + name + '.attrs',
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.reject(ErrorMsg.NotFound);
|
||||
}
|
||||
|
||||
requestNpcFlags(name: string): Promise<string> {
|
||||
const npcObject = this.npcMngr.findNPC(name);
|
||||
if (npcObject) {
|
||||
const nb = GBufferWriter.create(3);
|
||||
nb.writeGUInt24(npcObject.id);
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(
|
||||
NCOutgoingPacket.PLI_NC_NPCFLAGSGET,
|
||||
nb.buffer,
|
||||
),
|
||||
);
|
||||
return this.promiseMngr.createPromise(
|
||||
UriConstants.NpcPrefix + name + '.flags',
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.reject(ErrorMsg.NotFound);
|
||||
}
|
||||
|
||||
requestNpcScript(name: string): Promise<string> {
|
||||
const npcObject = this.npcMngr.findNPC(name);
|
||||
if (npcObject) {
|
||||
const nb = GBufferWriter.create(3);
|
||||
nb.writeGUInt24(npcObject.id);
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(
|
||||
NCOutgoingPacket.PLI_NC_NPCSCRIPTGET,
|
||||
nb.buffer,
|
||||
),
|
||||
);
|
||||
return this.promiseMngr.createPromise(
|
||||
UriConstants.NpcPrefix + name + '.script',
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.reject(ErrorMsg.NotFound);
|
||||
}
|
||||
|
||||
setNpcFlags(name: string, script: string): void {
|
||||
const npcObject = this.npcMngr.findNPC(name);
|
||||
if (npcObject) {
|
||||
const nb = GBufferWriter.create(3);
|
||||
nb.writeGUInt24(npcObject.id);
|
||||
nb.writeChars(gtokenize(script));
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(
|
||||
NCOutgoingPacket.PLI_NC_NPCFLAGSSET,
|
||||
nb.buffer,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setNpcScript(name: string, script: string): void {
|
||||
const npcObject = this.npcMngr.findNPC(name);
|
||||
if (npcObject) {
|
||||
const nb = GBufferWriter.create(3);
|
||||
nb.writeGUInt24(npcObject.id);
|
||||
nb.writeChars(gtokenize(script));
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(
|
||||
NCOutgoingPacket.PLI_NC_NPCSCRIPTSET,
|
||||
nb.buffer,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
deleteClass(name: string): void {
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(
|
||||
NCOutgoingPacket.PLI_NC_CLASSDELETE,
|
||||
Buffer.from(name),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
requestClass(name: string): Promise<string> {
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(
|
||||
NCOutgoingPacket.PLI_NC_CLASSEDIT,
|
||||
Buffer.from(name),
|
||||
),
|
||||
);
|
||||
return this.promiseMngr.createPromise(UriConstants.ScriptPrefix + name);
|
||||
}
|
||||
|
||||
setClassScript(name: string, script: string): void {
|
||||
const nb = GBufferWriter.create();
|
||||
nb.writeGString(name);
|
||||
nb.writeChars(gtokenize(script));
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(NCOutgoingPacket.PLI_NC_CLASSADD, nb.buffer),
|
||||
);
|
||||
}
|
||||
|
||||
private initializeHandlers(): PacketTable {
|
||||
const packetTable = new PacketTable();
|
||||
|
||||
packetTable.setDefault((id: number, packet: Buffer): void => {
|
||||
if (id !== 42) {
|
||||
console.log(
|
||||
`[NC] Unhandled Packet (${id}): ${packet
|
||||
.toString()
|
||||
.replace(/\r/g, '')}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
packetTable.on(
|
||||
NCIncomingPacket.PLO_NEWWORLDTIME,
|
||||
(id: number, packet: Buffer): void => {
|
||||
const reader = GBufferReader.from(packet);
|
||||
const serverTime = reader.readGUInt32();
|
||||
},
|
||||
);
|
||||
|
||||
packetTable.on(
|
||||
NCIncomingPacket.PLO_RCCHAT,
|
||||
(id: number, packet: Buffer): void => {
|
||||
const msg = packet.toString();
|
||||
this.eventHandler.onNCChat?.(msg);
|
||||
},
|
||||
);
|
||||
|
||||
packetTable.on(
|
||||
NCIncomingPacket.PLO_NC_LEVELLIST,
|
||||
(id: number, packet: Buffer): void => {
|
||||
const levelList = guntokenize(packet.toString());
|
||||
this.promiseMngr.resolvePromise(
|
||||
UriConstants.LevelList,
|
||||
levelList,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
packetTable.on(
|
||||
NCIncomingPacket.PLO_NC_NPCATTRIBUTES,
|
||||
(id: number, packet: Buffer): void => {
|
||||
const text = guntokenize(packet.toString());
|
||||
const name = text
|
||||
.substring(
|
||||
'Variable dump from npc '.length + 1,
|
||||
text.indexOf('\n'),
|
||||
)
|
||||
.trimEnd();
|
||||
|
||||
this.promiseMngr.resolvePromise(
|
||||
UriConstants.NpcPrefix + name + '.attrs',
|
||||
text,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
packetTable.on(
|
||||
NCIncomingPacket.PLO_NC_NPCADD,
|
||||
(id: number, packet: Buffer): void => {
|
||||
const reader = GBufferReader.from(packet);
|
||||
|
||||
const npcId = reader.readGUInt24();
|
||||
const npcObj = this.npcMngr.getNpc(npcId);
|
||||
npcObj.setProps(reader);
|
||||
|
||||
this.eventHandler.onNpcAdded?.(
|
||||
npcObj.props[NPCPropID.NPCPROP_NAME] as string,
|
||||
);
|
||||
// console.log(`NPC Added: ${npcObj.props[NPCPropID.NPCPROP_NAME]}`);
|
||||
},
|
||||
);
|
||||
|
||||
packetTable.on(
|
||||
NCIncomingPacket.PLO_NC_NPCDELETE,
|
||||
(id: number, packet: Buffer): void => {
|
||||
const reader = GBufferReader.from(packet);
|
||||
|
||||
const npcId = reader.readGUInt24();
|
||||
// this.npcMngr.deleteNpc(npcId);
|
||||
|
||||
const npcObj = this.npcMngr.getNpc(npcId);
|
||||
const npcName = npcObj.props[NPCPropID.NPCPROP_NAME] as string;
|
||||
|
||||
if (this.npcMngr.deleteNpc(npcId)) {
|
||||
this.eventHandler.onNpcDeleted?.(npcName);
|
||||
console.log(`Delete npc ${npcName}`);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
packetTable.on(
|
||||
NCIncomingPacket.PLO_NC_NPCSCRIPT,
|
||||
(id: number, packet: Buffer): void => {
|
||||
const reader = GBufferReader.from(packet);
|
||||
const npcId = reader.readGUInt24();
|
||||
const text = guntokenize(reader.readChars(reader.bytesLeft));
|
||||
|
||||
const npcObj = this.npcMngr.getNpc(npcId);
|
||||
if (npcObj) {
|
||||
const npcName = npcObj.props[
|
||||
NPCPropID.NPCPROP_NAME
|
||||
] as string;
|
||||
this.promiseMngr.resolvePromise(
|
||||
UriConstants.NpcPrefix + npcName + '.script',
|
||||
text,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
packetTable.on(
|
||||
NCIncomingPacket.PLO_NC_NPCFLAGS,
|
||||
(id: number, packet: Buffer): void => {
|
||||
const reader = GBufferReader.from(packet);
|
||||
const npcId = reader.readGUInt24();
|
||||
const text = guntokenize(reader.readChars(reader.bytesLeft));
|
||||
|
||||
const npcObj = this.npcMngr.getNpc(npcId);
|
||||
if (npcObj) {
|
||||
const npcName = npcObj.props[
|
||||
NPCPropID.NPCPROP_NAME
|
||||
] as string;
|
||||
this.promiseMngr.resolvePromise(
|
||||
UriConstants.NpcPrefix + npcName + '.flags',
|
||||
text,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
packetTable.on(
|
||||
NCIncomingPacket.PLO_NC_CLASSGET,
|
||||
(id: number, packet: Buffer): void => {
|
||||
const reader = GBufferReader.from(packet);
|
||||
const name = reader.readGString();
|
||||
const script = guntokenize(reader.readChars(reader.bytesLeft));
|
||||
|
||||
this.promiseMngr.resolvePromise(
|
||||
UriConstants.ScriptPrefix + name,
|
||||
script,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
packetTable.on(
|
||||
NCIncomingPacket.PLO_NC_CLASSADD,
|
||||
(id: number, packet: Buffer): void => {
|
||||
const className = packet.toString();
|
||||
this.classList.add(className);
|
||||
},
|
||||
);
|
||||
|
||||
packetTable.on(
|
||||
NCIncomingPacket.PLO_NC_CLASSDELETE,
|
||||
(id: number, packet: Buffer): void => {
|
||||
const className = packet.toString();
|
||||
this.classList.delete(className);
|
||||
},
|
||||
);
|
||||
|
||||
packetTable.on(
|
||||
NCIncomingPacket.PLO_NC_WEAPONGET,
|
||||
(id: number, packet: Buffer): void => {
|
||||
const reader = GBufferReader.from(packet);
|
||||
const name = reader.readGString();
|
||||
const image = reader.readGString();
|
||||
|
||||
let script = reader.readChars(reader.bytesLeft);
|
||||
script = script.replace(/\xa7/g, '\n');
|
||||
|
||||
this.promiseMngr.resolvePromise<[string, string]>(
|
||||
UriConstants.WeaponPrefix + name,
|
||||
[image, script],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
packetTable.on(
|
||||
NCIncomingPacket.PLO_NC_WEAPONLISTGET,
|
||||
(id: number, packet: Buffer): void => {
|
||||
const weaponList: Set<string> = new Set();
|
||||
|
||||
const reader = GBufferReader.from(packet);
|
||||
while (reader.bytesLeft) {
|
||||
weaponList.add(reader.readGString());
|
||||
}
|
||||
|
||||
this.promiseMngr.resolvePromise(
|
||||
UriConstants.WeaponList,
|
||||
weaponList,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return packetTable;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue