Compare commits

...

20 Commits

Author SHA1 Message Date
memetrollsXD
038c4a44d5
Implement RCON (#72) 2022-10-07 15:40:32 +02:00
memetrollsXD
24c7f0e155
Implement kickall command 2022-09-24 11:20:53 +02:00
GanyusLeftHorn
20df17be83
Add scuffed battle. (#64) 2022-08-09 20:11:44 -07:00
theNekoMoe
97b383e8d6
Update proxy.py (#66) 2022-08-09 00:20:44 +02:00
memetrollsXD
794042d65f
Implement LeaveChallengeCsReq (#63)
* Implement LeaveChallengeCsReq

* CodeFactor LeaveChallengeCsReq

This commit resolves the "no-unsafe-finally" check
2022-08-07 18:57:46 +02:00
GanyusLeftHorn
22cd1f6ba1
(Only) Enter Forgotten Hall, Set Game Mode, Clean Up Map/Maze Excels (#56)
* Implement avatar levelup and promotion.

* Clean up maz and map excels, set the correct game mode based on plane type, make forgotten hall enterable.

* Remove non-working spawn command.
2022-08-07 16:16:40 +02:00
Dang Hoang Phuc
5e1209deba
Fix & implement some lineup stuff (#53)
* fix: add avatar for each lineup

* feat: switch lineup
2022-08-06 21:36:41 +02:00
Midrooms
27557a0bbd
Don't use vanity invite. (#60)
Discord often kills vanity invites for up to 8 hours at a time, even if it shows it's correct in settings. Requires manual update to fix, pretty stupid, so just use this permanent invite.
2022-08-06 12:59:48 +02:00
GanyusRightThigh
ac22e03f28
Update README.md (#58) 2022-08-06 11:51:08 +02:00
GanyusLeftHorn
9e7ae5bc3f
Implement Light Cone Enhancement (#52)
* Implement avatar levelup and promotion.

* Implement equipment enhancement features.
2022-08-05 17:28:34 +02:00
TheLostTree
c59789180b remove dynamic runtime generated root 2022-08-05 01:28:37 -07:00
memetrollsXD
91d842c1ec
Make targetting navigation 100% nullsafe 2022-08-05 07:59:55 +02:00
memetrollsXD
590236cbfa
Implement /ban command 2022-08-05 07:54:05 +02:00
memetrollsXD
3649681f9f
Refactor PlayerGetTokenCsReq to a try-catch syntax 2022-08-05 07:50:42 +02:00
memetrollsXD
1f2b9f5726
Merge branch 'main' of https://github.com/Crepe-Inc/CrepeSR 2022-08-05 07:40:30 +02:00
memetrollsXD
ec9bc3731c
Update dependencies and include typescript
Fixes Error: Cannot find module 'typescript' #31
2022-08-05 07:39:42 +02:00
memetrollsXD
4e0b5a3064
Implement some packets for character screen (#44)
* implement eidolon levelup

* implement changing weapons
2022-08-05 07:35:10 +02:00
GanyusLeftHorn
016faec86b
Revert "improve scene command (#42)" (#49)
This reverts commit 118df5656d.
2022-08-05 06:56:28 +02:00
memetrollsXD
212a03fb1b
implement changing weapons 2022-08-04 12:02:04 +02:00
memetrollsXD
38b5d51be2
implement eidolon levelup 2022-08-04 11:55:00 +02:00
47 changed files with 1202 additions and 245 deletions

View File

@ -1,3 +1,3 @@
# CrepeSR
[Discord](https://discord.gg/sCAC282C)
[Discord](https://discord.gg/KA4HqktWYG)

20
package-lock.json generated
View File

@ -11,7 +11,9 @@
"express": "^4.18.1",
"mongodb": "^4.8.0",
"node-kcp-token": "github:memetrollsxd/node-kcp",
"protobufjs": "^7.0.0"
"protobufjs": "^7.0.0",
"rcon-server": "^0.1.1",
"typescript": "^4.7.4"
},
"devDependencies": {
"ts-node-dev": "^2.0.0"
@ -1201,6 +1203,11 @@
"node": ">= 0.8"
}
},
"node_modules/rcon-server": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/rcon-server/-/rcon-server-0.1.1.tgz",
"integrity": "sha512-oJ9+s0yxGc7in6GBBBxFXKRY2OyIVxFsoAEHlo5iFXDCuK6O4gY4qJ9XndMBVmA+nABkyOrN6QAFzoE6wZiI7A=="
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@ -1571,8 +1578,6 @@
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
"dev": true,
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -2569,6 +2574,11 @@
"unpipe": "1.0.0"
}
},
"rcon-server": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/rcon-server/-/rcon-server-0.1.1.tgz",
"integrity": "sha512-oJ9+s0yxGc7in6GBBBxFXKRY2OyIVxFsoAEHlo5iFXDCuK6O4gY4qJ9XndMBVmA+nABkyOrN6QAFzoE6wZiI7A=="
},
"readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@ -2823,9 +2833,7 @@
"typescript": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
"dev": true,
"peer": true
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ=="
},
"unpipe": {
"version": "1.0.0",

View File

@ -12,6 +12,8 @@
"express": "^4.18.1",
"mongodb": "^4.8.0",
"node-kcp-token": "github:memetrollsxd/node-kcp",
"protobufjs": "^7.0.0"
"protobufjs": "^7.0.0",
"rcon-server": "^0.1.1",
"typescript": "^4.7.4"
}
}

View File

@ -18,7 +18,7 @@ def next_layer(nextlayer: layer.NextLayer):
f"{nextlayer.data_client()[:70]=}\n"
)
sni = nextlayer.context.client.sni
if nextlayer.context.client.tls and sni and (sni.endswith("yuanshen.com") or sni.endswith("mihoyo.com") or sni.endswith("hoyoverse.com") or sni.endswith("starrails.com")):
if nextlayer.context.client.tls and sni and (sni.endswith("yuanshen.com") or sni.endswith("mihoyo.com") or sni.endswith("hoyoverse.com") or sni.endswith("starrails.com") or sni.endswith("bhsr.com")):
ctx.log('sni:' + sni)
nextlayer.context.server.address = ("127.0.0.1", 443)
@ -33,4 +33,4 @@ def request(flow: http.HTTPFlow) -> None:
b"404 not found", # (optional) content
{"Content-Type": "text/html"} # (optional) headers
)
return
return

View File

@ -23,26 +23,18 @@ export default class Interface {
output: process.stdout
});
public static target: Session;
public static target?: Session;
private constructor() { }
public static readonly start = () => {
public static start() {
Interface.rl.question("", (_command) => {
if (!_command) {
Interface.start();
return;
}
const cmd = new Command(_command);
import(`./${alias[cmd.name] || cmd.name}`).then(async module => {
await module.default(cmd);
}).catch(err => {
if (err.code == "MODULE_NOT_FOUND") {
c.log(`Command ${cmd.name} not found.`);
return;
}
c.error(err);
});
Interface.handle(cmd);
Interface.start();
});
@ -51,4 +43,16 @@ export default class Interface {
process.exit(0);
});
}
public static handle(cmd: Command) {
import(`./${alias[cmd.name] || cmd.name}`).then(async module => {
await module.default(cmd);
}).catch(err => {
if (err.code == "MODULE_NOT_FOUND") {
c.log(`Command ${cmd.name} not found.`);
return;
}
c.error(err);
});
}
}

17
src/commands/ban.ts Normal file
View File

@ -0,0 +1,17 @@
import Logger from "../util/Logger";
import Interface, { Command } from "./Interface";
const c = new Logger("/ban", "blue");
export default async function handle(command: Command) {
if (!Interface.target) {
c.log("No target specified");
return;
}
const banStatus = Interface.target.player.db.banned;
Interface.target.player.db.banned = !banStatus;
Interface.target.player.save();
Interface.target.kick();
c.log(`${banStatus ? "Banned" : "Unbanned"} ${Interface.target.account.name}`);
}

68
src/commands/battle.ts Normal file
View File

@ -0,0 +1,68 @@
import { BattleAvatar, BattleBuff, HeroPath, Item, ItemList, SceneBattleInfo, SceneMonsterWave, StartCocoonStageScRsp } from "../data/proto/StarRail";
import Avatar from "../db/Avatar";
import Inventory from "../db/Inventory";
import Logger from "../util/Logger";
import Interface, { Command } from "./Interface";
const c = new Logger("/battle", "blue");
export default async function handle(command: Command) {
if (!Interface.target) {
c.log("No target specified");
return;
}
if (command.args.length == 0) {
c.log("Usage: /battle <monsterId>+");
return;
}
const player = Interface.target.player;
if (player.db.posData.planeID != 20101) {
c.log("For now, this is only confirmed to work on plane 20101, please teleport there first.");
}
const inventory = await player.getInventory();
const avatars = await Avatar.getAvatarsForLineup(player, player.db.lineup.lineups[player.db.lineup.curIndex]);
const monsters = command.args.map(arg => Number(arg));
Interface.target.send(StartCocoonStageScRsp, {
retcode: 0,
propEntityId: 0,
cocoonId: 1001,
wave: 0,
battleInfo: {
logicRandomSeed: 1,
stageId: 1022010,
monsterWaveList: [
{ monsterIdList: monsters, dropList: [{ itemList: [{ itemId: 102, num: 10 } as Item] } as ItemList] } as SceneMonsterWave
],
battleAvatarList: avatars.map((avatar, i) => {
const equipment = inventory.getEquipmentByUid(avatar.db.equipmentUniqueId);
const equipData = (equipment) ? [{
id: equipment.tid,
level: equipment.level,
promotion: equipment.promotion,
rank: equipment.rank
}] : [];
return {
avatarType: avatar.db.avatarType,
equipmentList: equipData,
hp: avatar.db.fightProps.hp,
id: avatar.db.baseAvatarId,
sp: avatar.db.fightProps.sp,
index: i,
level: avatar.db.level,
promotion: avatar.db.promotion,
rank: avatar.db.rank,
relicList: [],
skilltreeList: []
};
}),
buffList: new Array<BattleBuff>(),
battleId: 0,
heroPathList: new Array<HeroPath>(),
roundsLimit: 100
} as SceneBattleInfo
} as StartCocoonStageScRsp);
}

14
src/commands/kickall.ts Normal file
View File

@ -0,0 +1,14 @@
import SRServer from "../server/kcp/SRServer";
import Logger from "../util/Logger";
import { Command } from "./Interface";
const c = new Logger("/kickall", "blue");
export default async function handle(command: Command) {
const hard = command.args[0];
for (const [key, session] of SRServer.getInstance().sessions) {
session.kick(!!hard);
}
c.log(`Kicked all players. Hard kick: ${!!hard}`);
}

13
src/commands/pos.ts Normal file
View File

@ -0,0 +1,13 @@
import Logger from "../util/Logger";
import Interface, { Command } from "./Interface";
const c = new Logger("/pos", "blue");
export default async function handle(command: Command) {
if (!Interface.target) {
c.log("No target specified");
return;
}
const pos = Interface.target.player.db.posData.pos;
c.log(`Current position: x=${pos.x}, y=${pos.y}, z=${pos.z}.`);
}

View File

@ -3,6 +3,7 @@ import { ActorEntity } from "../game/entities/Actor";
import Interface, { Command } from "./Interface";
import { GetCurSceneInfoScRsp } from "../data/proto/StarRail";
import MazePlaneExcel from "../util/excel/MazePlaneExcel";
import MapEntryExcel from "../util/excel/MapEntryExcel";
const c = new Logger("/scene", "blue");
export default async function handle(command: Command) {
@ -11,57 +12,33 @@ export default async function handle(command: Command) {
return;
}
if(command.args.length == 0){
c.log("Usage: /scene <planeID|floorID>");
return;
}
const plane = MazePlaneExcel.fromFloorId(parseInt(command.args[0])) || MazePlaneExcel.fromPlaneId(parseInt(command.args[0])) //Get plane data
let floorId = 10001001 // Default floor data
if(plane!){
if(command.args[0].length === 5){//PLANE ID LOGIC
floorId = plane.StartFloorID;
}else if(command.args[0].length === 8){//FLOOR ID LOGIC
if(plane! && plane.FloorIDList.includes(parseInt(command.args[0]))){
floorId = parseInt(command.args[0]);
}else{
c.error("cannot find Scene data!");
return;
}
}else{
c.error("Invalid FloorID / PlaneID length!");
return;
}
}else{
c.error("cannot find Scene data!");
return;
}
const planeID = MazePlaneExcel.fromPlaneId(parseInt(command.args[0]));
const entryId = MapEntryExcel.fromFloorId(planeID.StartFloorID).ID;
const posData = Interface.target.player.db.posData;
const lineup = await Interface.target.player.getLineup();
const curAvatarEntity = new ActorEntity(Interface.target.player.scene, lineup.leaderSlot, posData.pos);
const lineup2 = await Interface.target.player.getLineup();
const curAvatarEntity = new ActorEntity(Interface.target.player.scene, lineup2.avatarList[0].id, posData.pos);
const planeId = parseInt(floorId.toString().slice(0,5));
if (!planeID) return c.log("Usage: /scene <planeID>");
// Update scene information on player.
Interface.target.player.db.posData.planeID = planeID!.PlaneID;
Interface.target.player.db.posData.floorID = planeID!.StartFloorID;
await Interface.target.player.save()
//ty for tamilpp25 scene
Interface.target.send(GetCurSceneInfoScRsp, {
retcode: 0,
scene: {
planeId: planeId,
floorId: floorId,
planeId: planeID.PlaneID,
floorId: planeID.StartFloorID,
entityList: [
curAvatarEntity
],
entityBuffList: [],
entryId: 10001,
entryId: entryId,
envBuffList: [],
gameModeType: 1,
gameModeType: MazePlaneExcel.getGameModeForPlaneType(planeID.PlaneType),
lightenSectionList: []
},
} as unknown as GetCurSceneInfoScRsp);
@ -69,5 +46,5 @@ export default async function handle(command: Command) {
Interface.target.sync();
c.log(`Scene set to floorId: ${floorId}`);
c.log(`Scene set to PlaneID: ${planeID.PlaneID}`);
}

View File

@ -15,26 +15,27 @@ export default async function handle(command: Command) {
SRServer.getInstance().sessions.forEach(client => {
possibleTargets.push({
id: `${client.ctx.address}:${client.ctx.port}`,
uid: client.player.uid,
id: `${client.ctx.address}:${client.ctx.port} (UID: ${client.account.uid})`,
uid: Number(client.account.uid),
session: client
});
});
if (!target) {
c.log("No target specified");
if (Interface.target) c.log(`Current target: ${Interface.target.account.name} (UID: ${Interface.target.account.uid})`);
c.log("Possible targets: ");
possibleTargets.forEach(x => c.trail(`${x.id} (UID: ${x.uid})`));
if(!possibleTargets[1] && possibleTargets[0]){
c.log(`Auto target the only session ${possibleTargets[0].uid}`);
if (!possibleTargets[1] && possibleTargets[0]) {
c.log(`Auto targetting the only session ${possibleTargets[0].uid}`);
Interface.target = possibleTargets[0].session;
}
}
return;
}
const autoTarget = findBestMatch(target, possibleTargets.map(x => x.id))?.bestMatch.target;
const autoTarget = findBestMatch(target, possibleTargets.map(x => x.id))?.bestMatch?.target;
Interface.target = possibleTargets.find(x => x.id === autoTarget)!.session;
Interface.target = possibleTargets.find(x => x.id === autoTarget)?.session;
c.log(`Target set to ${autoTarget}`);
c.log(`Target set to ${Interface.target ? Interface.target.account.name : "none"} (UID: ${Interface.target ? Interface.target.account.uid : "none"})`);
}

View File

@ -119,6 +119,15 @@ export default class Inventory {
return 0;
}
/**
* Fetch the equipment with the given unique ID from the player's inventory.
* @param uniqueId The unique ID of the equipment to fetch.
* @returns The `Equipment` with the given unique ID, or `undefined` if the player does not have that equipment.
*/
public getEquipmentByUid(uniqueId: number) {
return this.db.equipments.filter(e => e.uniqueId == uniqueId)?.[0];
}
/********************************************************************************
Add items to the inventory.
********************************************************************************/
@ -376,17 +385,28 @@ export default class Inventory {
/********************************************************************************
Player updating.
********************************************************************************/
private sendMaterialUpdate() {
/**
* Send `PlayerSyncScNotify` for materials.
*/
public sendMaterialUpdate() {
this.player.session.send(PlayerSyncScNotify, PlayerSyncScNotify.fromPartial({
materialList: this.getMaterialList()
}));
}
private sendEquipmentUpdate() {
/**
* Send `PlayerSyncScNotify` for equipments.
*/
public sendEquipmentUpdate() {
this.player.session.send(PlayerSyncScNotify, PlayerSyncScNotify.fromPartial({
equipmentList: this.getEquipmentList()
}));
}
private sendRelicUpdate() {
/**
* Send `PlayerSyncScNotify` for relics.
*/
public sendRelicUpdate() {
this.player.session.send(PlayerSyncScNotify, PlayerSyncScNotify.fromPartial({
relicList: this.getRelicsList()
}));

View File

@ -13,9 +13,11 @@ export class Scene {
public spawnEntity(entity: Entity, silent: boolean = false) {
this.entities.set(entity.entityId, entity);
if (!silent) {
this.player.session.send(SceneEntityUpdateScNotify, {
const dataObj : SceneEntityUpdateScNotify = {
entityList: [entity.getSceneEntityInfo()]
} as SceneEntityUpdateScNotify);
};
this.player.session.send(SceneEntityUpdateScNotify, dataObj);
}
}

View File

@ -10,15 +10,16 @@ export class ActorEntity extends Entity
super(scene, pos, rot);
}
public getSceneEntityInfo(): SceneEntityInfo {
const sceneEntityInfo = super.getSceneEntityInfo();
sceneEntityInfo.actor = {
avatarType: AvatarType.AVATAR_FORMAL_TYPE,
baseAvatarId: this.avatarId,
uid: this.scene.player.db._id,
mapLayer: 0 //?
public getSceneActorInfo(): SceneEntityInfo {
return {
...super.getSceneEntityInfo(),
actor: {
avatarType: AvatarType.AVATAR_FORMAL_TYPE,
baseAvatarId: this.avatarId,
uid: this.scene.player.db._id,
mapLayer: 0 //?
}
};
return sceneEntityInfo;
}
public getEntityType(): EntityType {

View File

@ -6,6 +6,7 @@
import Interface from "./commands/Interface";
import HttpServer from "./http/HttpServer";
import SRServer from "./server/kcp/SRServer";
import RCONServer from "./server/rcon/RCONServer";
import Banners from "./util/Banner";
import Logger from "./util/Logger";
import ProtoFactory from "./util/ProtoFactory"
@ -15,6 +16,7 @@ c.log(`Starting CrepeSR...`);
Banners.init();
ProtoFactory.init();
Interface.start();
Interface.start();
HttpServer.getInstance().start();
SRServer.getInstance().start();
SRServer.getInstance().start();
RCONServer.getInstance().start();

View File

@ -1,11 +1,15 @@
import Logger, { VerboseLevel } from "../../util/Logger";
import protobuf, { Root } from 'protobufjs';
import { resolve } from 'path';
import ProtoFactory from "../../util/ProtoFactory";
const c = new Logger("Packet")
export default class Packet {
public readonly cmdid: number;
public readonly data: Buffer;
private static root: Root = Packet.getRoot();
public body: {} = {};
public constructor(public readonly rawData: Buffer, public readonly protoName: string = "") {
@ -15,9 +19,10 @@ export default class Packet {
this.cmdid = this.rawData.readUInt16BE(4);
this.protoName = this.protoName || CmdID[this.cmdid];
if (this.protoName) {
if(this.protoName){
try {
const Message = Packet.root.lookupTypeOrEnum(this.protoName);
const Message = ProtoFactory.getType(this.protoName as PacketName);
this.body = Message.decode(this.data);
} catch (e) {
c.warn(`Failed to decode ${this.protoName}`);
@ -37,37 +42,6 @@ export default class Packet {
return str.startsWith("01234567") && str.endsWith("89abcdef");
}
public static encode(name: PacketName, body: {}, customCmdId?: number): Packet | null {
try {
const cmdid = CmdID[name];
const Message = Packet.root.lookupTypeOrEnum(name);
const data = Buffer.from(Message.encode(body).finish());
const packet = Buffer.allocUnsafe(16 + data.length);
packet.writeUInt32BE(0x1234567);
packet.writeUint16BE(customCmdId || cmdid, 4);
packet.writeUint16BE(0, 6);
packet.writeUint32BE(data.length, 8);
data.copy(packet, 12);
packet.writeUint32BE(0x89abcdef, 12 + data.length);
return new Packet(packet, name);
} catch (e) {
c.error(e as Error);
return null;
}
}
private static getRoot(): Root {
try {
// Combined proto file with all definitions
return protobuf.loadSync(resolve(__dirname, `../../data/proto/StarRail.proto`));
} catch (e) {
c.error("Failed to load proto root! Server will not be able to function properly. Please check your data/ folder.");
c.error(e as Error, false);
process.exit(1);
}
}
public static fromEncodedBuffer(data: Buffer, name: PacketName): Buffer {
const cmdid = CmdID[name];
@ -80,8 +54,6 @@ export default class Packet {
packet.writeUint32BE(0x89abcdef, 12 + data.length);
return packet;
}
}
export type PacketName = keyof typeof CmdID;

View File

@ -80,7 +80,6 @@ export default class Session {
public async sync() {
const avatars = await Avatar.loadAvatarsForPlayer(this.player);
//const avatars = await Avatar.fromUID(this.player.db._id);
const inventory = await this.player.getInventory();
this.send(PlayerSyncScNotify, PlayerSyncScNotify.fromPartial({
@ -92,17 +91,15 @@ export default class Session {
relicList: inventory.getRelicsList(),
basicInfo: this.player.db.basicInfo
}));
//this.player.save();
}
public async send<Class extends MessageType<any>, >(type: Class, data: UnWrapMessageType<Class>) {
public async send<Class extends MessageType<any>,>(type: Class, data: UnWrapMessageType<Class>) {
const typeName = ProtoFactory.getName(type);
const encodedBuffer = type.encode(type.fromPartial(data)).finish();
const packet = Packet.fromEncodedBuffer(Buffer.from(encodedBuffer), typeName);
this.c.verbL(data);
this.c.verbH(encodedBuffer);
if(!encodedBuffer) console.log("sad!")
if (!encodedBuffer) this.c.error("encodedBuffer is undefined");
if (Logger.VERBOSE_LEVEL >= VerboseLevel.WARNS) this.c.log(typeName);
//todo: might want to regen the ts-proto types with env = node
this.kcpobj.send(packet);
@ -112,6 +109,7 @@ export default class Session {
public kick(hard: boolean = true) {
SRServer.getInstance().sessions.delete(this.id);
this.kicked = true;
if (hard) this.send(PlayerKickOutScNotify, {
kickType: PlayerKickOutScNotify_KickType.KICK_BLACK,
blackInfo: {

View File

@ -65,9 +65,9 @@ export default async function handle(session: Session, packet: Packet) {
// Calculate the character's new EXP and any excess EXP.
let excessExp = 0;
if (avatar.db.level == levelCap && currentAvatarExp >= nextRequiredExp) {
avatar.db.exp = nextRequiredExp;
excessExp = currentAvatarExp - nextRequiredExp;
if (avatar.db.level == levelCap) {
avatar.db.exp = 0;
excessExp = currentAvatarExp;
}
else {
avatar.db.exp = currentAvatarExp;

View File

@ -16,7 +16,7 @@ export default async function handle(session: Session, packet: Packet) {
if (!success) {
session.send(DoGachaScRsp, {
retcode: 1
retcode: 1301
} as DoGachaScRsp);
}

View File

@ -0,0 +1,16 @@
import { DressAvatarCsReq, DressAvatarScRsp } from "../../data/proto/StarRail";
import Avatar from "../../db/Avatar";
import Packet from "../kcp/Packet";
import Session from "../kcp/Session";
export default async function handle(session: Session, packet: Packet) {
const body = packet.body as DressAvatarCsReq;
const avatar = await Avatar.loadAvatarForPlayer(session.player, body.baseAvatarId);
avatar.db.equipmentUniqueId = body.equipmentUniqueId;
await avatar.save();
session.send(DressAvatarScRsp, { retcode: 0 });
session.sync();
}

View File

@ -1,4 +1,5 @@
import { EnterMazeCsReq, EnterMazeScRsp } from "../../data/proto/StarRail";
import MapEntryExcel from "../../util/excel/MapEntryExcel";
import MazePlaneExcel from "../../util/excel/MazePlaneExcel";
import Packet from "../kcp/Packet";
import Session from "../kcp/Session";
@ -6,7 +7,8 @@ import Session from "../kcp/Session";
export default async function handle(session: Session, packet: Packet) {
const body = packet.body as EnterMazeCsReq;
const mazeEntry = MazePlaneExcel.getEntry(body.entryId);
const mazeEntry = MapEntryExcel.fromId(body.entryId);
const mazePlane = MazePlaneExcel.fromPlaneId(mazeEntry.PlaneID);
let curLineup = await session.player.getLineup();
curLineup.planeId = mazeEntry.PlaneID;
@ -23,7 +25,7 @@ export default async function handle(session: Session, packet: Packet) {
scene: {
planeId: mazeEntry.PlaneID,
floorId: mazeEntry.FloorID,
gameModeType: 1,
gameModeType: MazePlaneExcel.getGameModeForPlaneType(mazePlane.PlaneType),
}
},
id: mazeEntry.PlaneID,

View File

@ -0,0 +1,100 @@
import { ExpUpEquipmentCsReq, ExpUpEquipmentScRsp } from "../../data/proto/StarRail";
import { PayItemData } from "../../db/Inventory";
import EquipmentExcel from "../../util/excel/EquipmentExcel";
import EquipmentExpItemExcel from "../../util/excel/EquipmentExpItemExcel";
import EquipmentExpTypeExcel from "../../util/excel/EquipmentExpTypeExcel";
import EquipmentPromotionExcel from "../../util/excel/EquipmentPromotionExcel";
import Packet from "../kcp/Packet";
import Session from "../kcp/Session";
export default async function handle(session: Session, packet: Packet) {
const body = packet.body as ExpUpEquipmentCsReq;
const inventory = await session.player.getInventory();
// Get the target equipment.
const equipmentId = body.equipmentUniqueId;
const equipment = inventory.getEquipmentByUid(equipmentId);
const equipmentExcelData = EquipmentExcel.fromId(equipment.tid);
// Determine the next level cap based on the equipment's current promotion.
const levelCap = EquipmentPromotionExcel.fromId(`${equipment.tid}:${equipment.promotion}`).MaxLevel;
// Determine the EXP we get from the consumed items, and the coins it will cost.
let exp = 0;
let cost = 0;
const costMaterialList = [];
const costEquipmentList = []
for (const item of body.costData!.itemList) {
// Determine amount of EXP given by that item.
// If the consumed item is a PileItem, we can fetch the EXP it gives from the excels.
if (item.pileItem) {
const expItemData = EquipmentExpItemExcel.fromId(item.pileItem.itemId);
exp += expItemData.ExpProvide * item.pileItem.itemNum;
cost += expItemData.CoinCost;
costMaterialList.push({ id: item.pileItem.itemId, count: item.pileItem.itemNum } as PayItemData);
}
else if (item.equipmentUniqueId) {
const consumedEquipment = inventory.getEquipmentByUid(item.equipmentUniqueId);
const consumedEquipmentExcelData = EquipmentExcel.fromId(consumedEquipment.tid);
exp += consumedEquipmentExcelData.ExpProvide;
cost += consumedEquipmentExcelData.CoinCost;
costEquipmentList.push(item.equipmentUniqueId);
}
}
costMaterialList.push({ id: 2, count: cost } as PayItemData);
// Try consuming materials.
const success = await inventory.payItems(costMaterialList);
if (!success) {
// ToDo: Correct retcode.
session.send(ExpUpEquipmentScRsp, { retcode: 1, returnItemList: [] } as ExpUpEquipmentScRsp);
return;
}
for (const id of costEquipmentList) {
await inventory.removeEquipment(id);
}
// Cost has been paid - now level up.
let currentEquipmentExp = equipment.exp + exp;
let nextRequiredExp = EquipmentExpTypeExcel.fromId(`${equipmentExcelData.ExpType}:${equipment.level}`).Exp;
while (currentEquipmentExp >= nextRequiredExp && equipment.level < levelCap) {
// Increase level.
equipment.level++;
// Deduct EXP necessary for this level.
currentEquipmentExp -= nextRequiredExp;
// Determine EXP necessary for the next level.
nextRequiredExp = EquipmentExpTypeExcel.fromId(`${equipmentExcelData.ExpType}:${equipment.level}`).Exp;
}
// Calculate the equipment's new EXP and any excess EXP.
let excessExp = 0;
if (equipment.level == levelCap) {
equipment.exp = 0;
excessExp = currentEquipmentExp;
}
else {
equipment.exp = currentEquipmentExp;
}
// Save.
await inventory.save();
// ToDo: Handle return items.
// Done. Sync and send response.
await session.sync();
session.send(ExpUpEquipmentScRsp, {
retcode: 0,
returnItemList: []
} as ExpUpEquipmentScRsp);
}

View File

@ -1,28 +1,43 @@
import { GetCurSceneInfoScRsp, Vector } from "../../data/proto/StarRail";
import { GetCurSceneInfoScRsp, MotionInfo, SceneEntityInfo, SceneNpcMonsterInfo, StartCocoonStageCsReq, Vector } from "../../data/proto/StarRail";
import { ActorEntity } from "../../game/entities/Actor";
import { PropEntity } from "../../game/entities/Prop";
import MapEntryExcel from "../../util/excel/MapEntryExcel";
import MazePlaneExcel from "../../util/excel/MazePlaneExcel";
import Packet from "../kcp/Packet";
import Session from "../kcp/Session";
export default async function handle(session: Session, packet: Packet) {
// Get data.
const posData = session.player.db.posData;
const _lineup = session.player.db.lineup;
const lineup = _lineup.lineups[_lineup.curIndex];
const curAvatarEntity = new ActorEntity(session.player.scene, lineup.avatarList[0], posData.pos);
session.send(GetCurSceneInfoScRsp, {
const curAvatarEntity = new ActorEntity(session.player.scene, lineup.avatarList[lineup.leaderSlot], posData.pos);
const entryId = MapEntryExcel.fromFloorId(posData.floorID).ID;
const mazePlane = MazePlaneExcel.fromPlaneId(posData.planeID);
// Scene management.
session.player.scene.spawnEntity(curAvatarEntity, true);
session.player.scene.entryId = entryId;
// Build response.
const dataObj : GetCurSceneInfoScRsp = {
retcode: 0,
scene: {
planeId: posData.planeID,
floorId: posData.floorID,
entityList: [
curAvatarEntity
curAvatarEntity.getSceneEntityInfo(),
],
lightenSectionList: [],
leaderEntityId: curAvatarEntity.entityId,
entityBuffList: [],
entryId: 10001,
entryId: entryId,
envBuffList: [],
gameModeType: 1,
lightenSectionList: []
},
} as unknown as GetCurSceneInfoScRsp);
session.player.scene.spawnEntity(curAvatarEntity, true);
session.player.scene.entryId = 10001;
gameModeType: MazePlaneExcel.getGameModeForPlaneType(mazePlane.PlaneType),
}
};
// Send response.
session.send(GetCurSceneInfoScRsp, dataObj);
}

View File

@ -1,4 +1,5 @@
import { GetMazeMapInfoCsReq, GetMazeMapInfoScRsp } from "../../data/proto/StarRail";
import MapEntryExcel from "../../util/excel/MapEntryExcel";
import MappingInfoExcel from "../../util/excel/MappingInfoExcel";
import MazePlaneExcel from "../../util/excel/MazePlaneExcel";
import Packet from "../kcp/Packet";
@ -24,7 +25,7 @@ export default async function handle(session: Session, packet: Packet) {
dataObj.lightenSectionList.push(i)
}
dataObj.unlockTeleportList = MazePlaneExcel.getAllEntries().map(x => x.ID);
dataObj.unlockTeleportList = MapEntryExcel.all().map(x => x.ID);
session.send(GetMazeMapInfoScRsp, dataObj);
}

View File

@ -1,20 +1,26 @@
import { AvatarType, JoinLineupCsReq, JoinLineupScRsp, SyncLineupNotify, SyncLineupReason } from "../../data/proto/StarRail";
import Avatar from "../../db/Avatar";
import {
JoinLineupCsReq,
JoinLineupScRsp,
SyncLineupNotify,
SyncLineupReason
} from "../../data/proto/StarRail";
import Packet from "../kcp/Packet";
import Session from "../kcp/Session";
// JoinLineupCsReq { baseAvatarId: 1002, slot: 1 }
export default async function handle(session: Session, packet: Packet) {
const body = packet.body as JoinLineupCsReq;
session.send(JoinLineupScRsp, { retcode: 0 });
session.send(JoinLineupScRsp, {retcode: 0});
// Replace avatar in the player's lineup.
const slot = body.slot ?? 0;
session.player.db.lineup.lineups[session.player.db.lineup.curIndex].avatarList[slot] = body.baseAvatarId;
const index = body.index ?? 1;
session.player.db.lineup.lineups[index].avatarList[slot] = body.baseAvatarId;
await session.player.save();
session.send(SyncLineupNotify, {
lineup: await session.player.getLineup(),
lineup: await session.player.getLineup(index),
reasonList: [SyncLineupReason.SYNC_REASON_NONE]
} as SyncLineupNotify);
}
}

View File

@ -0,0 +1,70 @@
import { GetCurSceneInfoScRsp, LeaveChallengeScRsp } from "../../data/proto/StarRail";
import Packet from "../kcp/Packet";
import Session from "../kcp/Session";
import MapEntryExcel from "../../util/excel/MapEntryExcel";
import MazePlaneExcel from "../../util/excel/MazePlaneExcel";
import { ActorEntity } from "../../game/entities/Actor";
export default async function handle(session: Session, packet: Packet) {
const bfArray: number[] = [];
for (let i = 0; i < 500; i++) {
bfArray.push(i);
}
try {
// Challenge maze data doesn't get saved to DB.. We can abuse this and fall back to default
const posData = session.player.db.posData;
const entry = MapEntryExcel.fromFloorId(posData.floorID);
const maze = MazePlaneExcel.fromPlaneId(posData.planeID);
if (!entry || !maze) return session.send(LeaveChallengeScRsp, LeaveChallengeScRsp.fromPartial({ retcode: 2 }));
const dataObj = LeaveChallengeScRsp.fromPartial({
retcode: 0,
maze: {
floor: {
floorId: posData.floorID,
scene: {
entityList: [],
entryId: entry.ID,
floorId: entry.FloorID,
planeId: entry.PlaneID,
gameModeType: MazePlaneExcel.getGameModeForPlaneType(maze.PlaneType),
lightenSectionList: bfArray,
entityBuffList: [],
envBuffList: [],
}
},
id: posData.planeID,
mapEntryId: entry.ID
}
});
session.send(LeaveChallengeScRsp, dataObj);
} catch (e) {
session.c.error(e as Error);
session.send(LeaveChallengeScRsp, LeaveChallengeScRsp.fromPartial({ retcode: 2 }));
} finally {
// Force us back
const posData = session.player.db.posData;
const entry = MapEntryExcel.fromFloorId(posData.floorID);
const maze = MazePlaneExcel.fromPlaneId(posData.planeID);
if (entry && maze) {
const actor = new ActorEntity(session.player.scene, session.player.db.lineup.lineups[session.player.db.lineup.curIndex].avatarList[0], posData.pos).getSceneActorInfo();
session.send(GetCurSceneInfoScRsp, {
retcode: 0,
scene: {
planeId: posData.planeID,
floorId: posData.floorID,
lightenSectionList: bfArray,
gameModeType: MazePlaneExcel.getGameModeForPlaneType(maze.PlaneType),
entityBuffList: [],
envBuffList: [],
entryId: entry.ID,
entityList: [actor],
leaderEntityId: actor.entityId
}
});
}
}
}

View File

@ -0,0 +1,26 @@
import { LockEquipmentCsReq, LockEquipmentScRsp } from "../../data/proto/StarRail";
import Packet from "../kcp/Packet";
import Session from "../kcp/Session";
export default async function handle(session: Session, packet: Packet) {
const body = packet.body as LockEquipmentCsReq;
// Fetch the equipment in question.
const inventory = await session.player.getInventory();
const equipment = inventory.getEquipmentByUid(body.equipmentUniqueId);
if (!equipment) {
session.send(LockEquipmentScRsp, { retcode: 1 } as LockEquipmentScRsp);
}
// Toggle the equipment's lock.
equipment.isProtected = !equipment.isProtected;
await inventory.save();
// Done. Send and sync.
inventory.sendEquipmentUpdate();
session.send(LockEquipmentScRsp, {
retcode: 0,
equipmentUniqueId: body.equipmentUniqueId
} as LockEquipmentScRsp);
}

View File

@ -0,0 +1,34 @@
import { BattleEndStatus, EnterMazeCsReq, EnterMazeScRsp, Item, ItemList, PVEBattleResultCsReq, PVEBattleResultScRsp } from "../../data/proto/StarRail";
import MapEntryExcel from "../../util/excel/MapEntryExcel";
import MazePlaneExcel from "../../util/excel/MazePlaneExcel";
import Packet from "../kcp/Packet";
import Session from "../kcp/Session";
export default async function handle(session: Session, packet: Packet) {
const body = packet.body as PVEBattleResultCsReq;
// Add drops, for our little gambling addicts.
const inventory = await session.player.getInventory();
inventory.addItem(102, 10);
await inventory.save();
// Build response.
const dataObj : PVEBattleResultScRsp = {
retcode: 0,
stageId: body.stageId,
curFinishChallenge: 0,
dropData: { itemList: [{ itemId: 102, num: 10 } as Item] } as ItemList,
extraDropData: { itemList: [{ itemId: 102, num: 10 } as Item] } as ItemList,
avatarExpReward: 0,
binVer: "",
resVer: "",
battleId: body.battleId,
endStatus: body.endStatus,
checkIdentical: true,
eventId: 0,
mismatchTurnCount: 0
};
// Send response.
session.send(PVEBattleResultScRsp, dataObj);
}

View File

@ -18,33 +18,47 @@ const retWarn = (msg: string) => c.warn(msg);
export default async function handle(session: Session, packet: Packet) {
const body = packet.body as PlayerGetTokenCsReq;
const dataObj = {
retcode: 0,
secretKeySeed: 0,
} as PlayerGetTokenScRsp;
const account = await Account.fromToken(body.token || "");
if (!account) retWarn(`Account not found with token ${body.token}`);
try {
const account = await Account.fromToken(body.token || "");
if (!account) retWarn(`Account not found with token ${body.token}`);
const player = await Player.fromToken(session, account?.token || "");
if (!player) retWarn(`Player not found with accountToken ${account?.token}`);
if (!player || !account) {
dataObj.retcode = 6;
return;
}
session.account = account;
session.player = player;
const player = await Player.fromToken(session, account?.token || "");
if (!player) retWarn(`Player not found with accountToken ${account?.token}`);
if (!player || !account) {
dataObj.retcode = 6;
dataObj.msg = "Player not found";
return;
}
const isTokenValid = player.db.token === body.token;
const isBanned = player.db.banned;
if (isBanned) dataObj.retcode = 1013;
if (!isTokenValid) {
retWarn(`Token invalid (${session.ctx.address}:${session.ctx.port})`);
dataObj.retcode = 1005;
return;
session.account = account;
session.player = player;
dataObj.uid = player.db._id;
if (player.db.banned) {
dataObj.retcode = 1013;
dataObj.blackInfo = {
banType: 2,
beginTime: Math.floor(Date.now() / 1000),
endTime: Math.floor(Date.now() / 1000) + 86400,
limitLevel: 0,
}
}
if (player.db.token !== body.token) {
retWarn(`Token invalid (${session.ctx.address}:${session.ctx.port})`);
dataObj.retcode = 1005;
dataObj.msg = "Token invalid";
}
} catch (e) {
dataObj.retcode = 2;
c.error(e as Error);
} finally {
session.send(PlayerGetTokenScRsp, dataObj);
}
dataObj.uid = player.db._id;
session.send(PlayerGetTokenScRsp, dataObj);
}

View File

@ -1,10 +1,7 @@
import { PromoteAvatarCsReq, PromoteAvatarScRsp } from "../../data/proto/StarRail";
import Avatar from "../../db/Avatar";
import { PayItemData } from "../../db/Inventory";
import AvatarExcel from "../../util/excel/AvatarExcel";
import AvatarExpItemExcel from "../../util/excel/AvatarExpItemExcel";
import AvatarPromotionExcel from "../../util/excel/AvatarPromotionExcel";
import ExpTypeExcel from "../../util/excel/ExpTypeExcel";
import Packet from "../kcp/Packet";
import Session from "../kcp/Session";

View File

@ -0,0 +1,36 @@
import { PromoteEquipmentCsReq, PromoteEquipmentScRsp } from "../../data/proto/StarRail";
import { PayItemData } from "../../db/Inventory";
import EquipmentPromotionExcel from "../../util/excel/EquipmentPromotionExcel";
import Packet from "../kcp/Packet";
import Session from "../kcp/Session";
export default async function handle(session: Session, packet: Packet) {
const body = packet.body as PromoteEquipmentCsReq;
const inventory = await session.player.getInventory();
// Get the target avatar.
const equipmentId = body.equipmentUniqueId;
const equipment = inventory.getEquipmentByUid(equipmentId);
const promotionExcelData = EquipmentPromotionExcel.fromId(`${equipment.tid}:${equipment.promotion}`);
// Build list of consumed items. We take this from the excel, instead of the Req.
const costMaterialList = promotionExcelData.PromotionCostList.map(c => { return { id: c.ItemID, count: c.ItemNum } as PayItemData });
// Try consuming materials.
const success = await inventory.payItems(costMaterialList);
if (!success) {
// ToDo: Correct retcode.
session.send(PromoteEquipmentScRsp, { retcode: 1 } as PromoteEquipmentScRsp);
return;
}
await inventory.save();
// Promote the avatar and save.
equipment.promotion++;
await inventory.save();
// Done. Sync and send response.
await session.sync();
session.send(PromoteEquipmentScRsp, { retcode: 0 } as PromoteEquipmentScRsp);
}

View File

@ -0,0 +1,44 @@
import { RankUpAvatarCsReq, RankUpAvatarScRsp } from "../../data/proto/StarRail";
import Avatar from "../../db/Avatar";
import { PayItemData } from "../../db/Inventory";
import Logger from "../../util/Logger";
import Packet from "../kcp/Packet";
import Session from "../kcp/Session";
const c = new Logger("RankUpAvatarCsReq");
export default async function handle(session: Session, packet: Packet) {
const body = packet.body as RankUpAvatarCsReq;
const dataObj: RankUpAvatarScRsp = {
retcode: 0
};
try {
const inv = await session.player.getInventory();
if (!body.costData) return;
const list = body.costData.itemList;
const arr: Array<PayItemData> = [];
for (let i = 0; i < list.length; i++) {
const item = list[i];
if (!item.pileItem) continue;
arr.push({
count: item.pileItem.itemNum,
id: item.pileItem.itemId
});
}
if (await inv.payItems(arr)) {
const avatar = await Avatar.loadAvatarForPlayer(session.player, body.baseAvatarId);
avatar.db.rank = body.rank // gidra moment: ez hack
await avatar.save();
} else {
dataObj.retcode = 1301;
}
} catch (e) {
c.error(e as Error);
dataObj.retcode = 2;
} finally {
session.send(RankUpAvatarScRsp, dataObj);
session.sync();
}
}

View File

@ -0,0 +1,32 @@
import { RankUpEquipmentCsReq, RankUpEquipmentScRsp } from "../../data/proto/StarRail";
import Packet from "../kcp/Packet";
import Session from "../kcp/Session";
export default async function handle(session: Session, packet: Packet) {
const body = packet.body as RankUpEquipmentCsReq;
const inventory = await session.player.getInventory();
// Get equipment.
const equipment = inventory.getEquipmentByUid(body.equipmentUniqueId);
// Check if all sacrificed equipments exist.
for (const id of body.equipmentIdList) {
if (!inventory.getEquipmentByUid(id)) {
session.send(RankUpEquipmentScRsp, { retcode: 1 } as RankUpEquipmentScRsp);
return;
}
}
// Remove sacrificed equipments.
for (const id of body.equipmentIdList) {
await inventory.removeEquipment(id);
}
// Increase rank and save.
equipment.rank += body.equipmentIdList.length;
await inventory.save();
// Done. Send and sync.
await session.sync();
session.send(RankUpEquipmentScRsp, { retcode: 0 } as RankUpEquipmentScRsp);
}

View File

@ -5,6 +5,7 @@ import Session from "../kcp/Session";
export default async function handle(session: Session, packet: Packet) {
const body = packet.body as SceneEntityMoveCsReq;
if (session.player.scene.entryId !== body.entryId) {
return;
}

View File

@ -1,59 +1,76 @@
import { ChallengeStatus, ExtraLineupType, StartChallengeCsReq, StartChallengeScRsp } from "../../data/proto/StarRail";
import { ChallengeStatus, CurChallenge, ExtraLineupType, Maze, SceneActorInfo, SceneEntityInfo, SceneInfo, SceneNpcInfo, SceneNpcMonsterInfo, ScenePropInfo, StartChallengeCsReq, StartChallengeScRsp } from "../../data/proto/StarRail";
import { ActorEntity } from "../../game/entities/Actor";
import Packet from "../kcp/Packet";
import Session from "../kcp/Session";
// StartChallengeCsReq { challengeId: 101 }
export default async function handle(session: Session, packet: Packet) {
const body = packet.body as StartChallengeCsReq;
console.log(JSON.stringify(body, undefined, 4));
// TODO: This packet is just a base
const _lineup = session.player.db.lineup;
const lineup = _lineup.lineups[_lineup.curIndex];
const curAvatarEntity = new ActorEntity(session.player.scene, lineup.avatarList[lineup.leaderSlot], { x: 0, y: 0, z: 0 });
session.send(StartChallengeScRsp, {
retcode: 0,
curChallenge: {
challengeId: body.challengeId,
deadAvatarNum: 0,
extraLineupType: ExtraLineupType.LINEUP_CHALLENGE,
rounds: 1,
status: ChallengeStatus.CHALLENGE_DOING,
killMonsterList: [{
monsterId: 1001010,
killNum: 1,
}]
},
extraLineupType: ExtraLineupType.LINEUP_NONE,
killMonsterList: [],
deadAvatarNum: 0,
} as CurChallenge,
maze: {
// ? Data from MappingInfoExcelTable
id: 30101,
mapEntryId: 1000,
id: 30104,
mapEntryId: 3000401,
floor: {
floorId: 20121001,
scene: {
planeId: 20121,
entryId: 1000,
floorId: 20121001,
gameModeType: 1,
entityList: [{
entityId: 10010101,
npcMonster: {
monsterId: 8013010,
worldLevel: 1,
},
groupId: 11,
motion: {
pos: {
x: 0,
y: 100,
z: 0,
planeId: 30104,
entryId: 3000401,
floorId: 30104001,
lightenSectionList: [],
gameModeType: 4,
entityList: [
curAvatarEntity.getSceneEntityInfo(),
{
entityId: 10000,
motion: {
pos: {
x: 74719,
y: 2014,
z: -94205,
},
rot: {
x: 0,
y: 0,
z: 0
}
},
rot: {
x: 0,
y: 0,
z: 0
}
},
}]
}
groupId: 3,
instId: 1,
actor: {} as SceneActorInfo,
npc: {} as SceneNpcInfo,
prop: {} as ScenePropInfo,
npcMonster: {
monsterId: 1003020,
isGenMonster: false,
eventId: 0,
isSetWorldLevel: false,
worldLevel: 6
} as SceneNpcMonsterInfo
} as SceneEntityInfo,
],
leaderEntityId: curAvatarEntity.entityId,
entityBuffList: [],
envBuffList: [],
snar: ""
} as SceneInfo
}
}
} as Maze
} as StartChallengeScRsp);
}

View File

@ -0,0 +1,19 @@
import Session from "../kcp/Session";
import Packet from "../kcp/Packet";
import {SwitchLineupIndexCsReq, SwitchLineupIndexScRsp} from "../../data/proto/StarRail";
// SwitchLineupIndexCsReq { index: 0 }
export default async function handle(session: Session, packet: Packet) {
const body = packet.body as SwitchLineupIndexCsReq
const index = body.index ?? 0
session.send(SwitchLineupIndexScRsp, {
retcode: 0,
index: index
})
session.player.db.lineup.curIndex = index
await session.player.save()
// Todo: figure need to send SyncLineupNotify again ?
}

View File

@ -0,0 +1,54 @@
// @ts-ignore
import { RCONServer as RServer } from "rcon-server";
import { Writable } from "stream";
import Interface, { Command } from "../../commands/Interface";
import Config from "../../util/Config";
import Logger from "../../util/Logger";
const c = new Logger("RCON", "green");
export default class RCONServer {
private static _instance: RCONServer;
public RCON!: RServer;
private constructor() { }
public static getInstance(): RCONServer {
if (!this._instance) this._instance = new RCONServer();
return this._instance;
}
public start() {
if (!Config.RCON.RCON_ENABLED) return;
this.RCON = new RServer({
host: Config.HTTP.HTTP_HOST,
clientLimit: Config.RCON.RCON_CLIENT_LIMIT,
destroySocketOnLimitExceeded: false,
emitAdvancedEvents: false,
password: Config.RCON.RCON_PASSWORD,
port: Config.RCON.RCON_PORT
});
this.RCON.on("listening", () => {
c.log(`Listening on ${Config.RCON.RCON_PORT}`);
});
this.RCON.on("commandRequest", (cmd: {
size: number,
id: number,
type: number,
body: string
resolve: (value: string) => void,
}) => {
c.verbL(cmd);
Interface.handle(new Command(cmd.body));
cmd.resolve(`Command executed.`);
});
this.RCON.on("login", ({ pw, fail }: { pw: string, fail: boolean }) => {
c.debug(pw);
fail ? c.log(`Login failed`) : c.log(`Login successful`); // For some reason succ is flipped
});
this.RCON.connect();
}
}

263
src/server/rcon/rcon-server.d.ts vendored Normal file
View File

@ -0,0 +1,263 @@
/** Declaration file generated by dts-gen */
import { EventEmitter } from "events";
declare module "rcon-server" {
export class RCONServer extends EventEmitter {
constructor(obj: {
port: number = 3839,
host: string = "127.0.0.1",
password: string = "password",
clientLimit: number = 1,
destroySocketOnLimitExceeded: boolean = true,
emitAdvancedEvents: boolean = false
});
connect(...args: any[]): void;
getConnectedSockets(...args: any[]): void;
getServerSettings(...args: any[]): void;
getSocketServer(...args: any[]): void;
static captureRejectionSymbol: any;
static captureRejections: boolean;
static defaultMaxListeners: number;
static errorMonitor: any;
static getEventListeners(emitterOrTarget: any, type: any): any;
static init(opts: any): void;
static kMaxEventTargetListeners: any;
static kMaxEventTargetListenersWarned: any;
static listenerCount(emitter: any, type: any): any;
static on(emitter: any, event: any, options: any): any;
static once(emitter: any, name: any, options: any): any;
static setMaxListeners(n: any, eventTargets: any): void;
static usingDomains: boolean;
}
export namespace RCONServer {
class EventEmitter {
constructor(opts: any);
addListener(type: any, listener: any): any;
emit(type: any, args: any): any;
eventNames(): any;
getMaxListeners(): any;
listenerCount(type: any): any;
listeners(type: any): any;
off(type: any, listener: any): any;
on(type: any, listener: any): any;
once(type: any, listener: any): any;
prependListener(type: any, listener: any): any;
prependOnceListener(type: any, listener: any): any;
rawListeners(type: any): any;
removeAllListeners(type: any, ...args: any[]): any;
removeListener(type: any, listener: any): any;
setMaxListeners(n: any): any;
static EventEmitter: any;
static captureRejectionSymbol: any;
static captureRejections: boolean;
static defaultMaxListeners: number;
static errorMonitor: any;
static getEventListeners(emitterOrTarget: any, type: any): any;
static init(opts: any): void;
static kMaxEventTargetListeners: any;
static kMaxEventTargetListenersWarned: any;
static listenerCount(emitter: any, type: any): any;
static on(emitter: any, event: any, options: any): any;
static once(emitter: any, name: any, options: any): any;
static setMaxListeners(n: any, eventTargets: any): void;
static usingDomains: boolean;
}
class EventEmitterAsyncResource {
constructor(...args: any[]);
emit(...args: any[]): void;
emitDestroy(...args: any[]): void;
static EventEmitterAsyncResource: any;
static captureRejectionSymbol: any;
static captureRejections: boolean;
static defaultMaxListeners: number;
static errorMonitor: any;
static getEventListeners(emitterOrTarget: any, type: any): any;
static init(opts: any): void;
static kMaxEventTargetListeners: any;
static kMaxEventTargetListenersWarned: any;
static listenerCount(emitter: any, type: any): any;
static on(emitter: any, event: any, options: any): any;
static once(emitter: any, name: any, options: any): any;
static setMaxListeners(n: any, eventTargets: any): void;
static usingDomains: boolean;
}
namespace EventEmitter {
class EventEmitterAsyncResource {
constructor(...args: any[]);
emit(...args: any[]): void;
emitDestroy(...args: any[]): void;
static EventEmitter: any;
static EventEmitterAsyncResource: any;
static captureRejectionSymbol: any;
static captureRejections: boolean;
static defaultMaxListeners: number;
static errorMonitor: any;
static getEventListeners(emitterOrTarget: any, type: any): any;
static init(opts: any): void;
static kMaxEventTargetListeners: any;
static kMaxEventTargetListenersWarned: any;
static listenerCount(emitter: any, type: any): any;
static on(emitter: any, event: any, options: any): any;
static once(emitter: any, name: any, options: any): any;
static setMaxListeners(n: any, eventTargets: any): void;
static usingDomains: boolean;
}
}
namespace EventEmitterAsyncResource {
class EventEmitter {
constructor(opts: any);
addListener(type: any, listener: any): any;
emit(type: any, args: any): any;
eventNames(): any;
getMaxListeners(): any;
listenerCount(type: any): any;
listeners(type: any): any;
off(type: any, listener: any): any;
on(type: any, listener: any): any;
once(type: any, listener: any): any;
prependListener(type: any, listener: any): any;
prependOnceListener(type: any, listener: any): any;
rawListeners(type: any): any;
removeAllListeners(type: any, ...args: any[]): any;
removeListener(type: any, listener: any): any;
setMaxListeners(n: any): any;
static EventEmitter: any;
static EventEmitterAsyncResource: any;
static captureRejectionSymbol: any;
static captureRejections: boolean;
static defaultMaxListeners: number;
static errorMonitor: any;
static getEventListeners(emitterOrTarget: any, type: any): any;
static init(opts: any): void;
static kMaxEventTargetListeners: any;
static kMaxEventTargetListenersWarned: any;
static listenerCount(emitter: any, type: any): any;
static on(emitter: any, event: any, options: any): any;
static once(emitter: any, name: any, options: any): any;
static setMaxListeners(n: any, eventTargets: any): void;
static usingDomains: boolean;
}
}
}
}

View File

@ -28,6 +28,12 @@ const DEFAULT_CONFIG = {
MAINTENANCE: false,
MAINTENANCE_MSG: "Server is in maintenance mode."
},
RCON: {
RCON_ENABLED: false,
RCON_PASSWORD: "password",
RCON_PORT: 22103,
RCON_CLIENT_LIMIT: 1
},
AUTO_ACCOUNT: false
}
type DefaultConfig = typeof DEFAULT_CONFIG;
@ -82,6 +88,12 @@ export default class Config {
MAINTENANCE_MSG: string;
} = Config.config.GAMESERVER;
public static AUTO_ACCOUNT: boolean = Config.config.AUTO_ACCOUNT;
public static RCON: {
RCON_ENABLED: boolean;
RCON_PASSWORD: string;
RCON_PORT: number;
RCON_CLIENT_LIMIT: number;
} = Config.config.RCON;
private constructor() { }
}

View File

@ -8,7 +8,7 @@ const c = new Logger("ProtoFactory");
export class MessageType<T> {
"encode": (arg0: T) => protobufjs.Writer;
"fromPartial": (arg0: object) => T;
// "decode": (input: protobufjs.Reader | Uint8Array, length?: number)=> T;
"decode": (input: protobufjs.Reader | Uint8Array, length?: number)=> T;
// "fromJSON": (object: any)=>T;
// "toJSON": (message: T)=> unknown
//you can add more fields here from the generated types

View File

@ -0,0 +1,20 @@
import _EquipmentExcelTable from "../../data/excel/EquipmentExcelTable.json";
type EquipmentExcelTableEntry = typeof _EquipmentExcelTable[keyof typeof _EquipmentExcelTable]
const EquipmentExcelTable = _EquipmentExcelTable as { [key: string]: EquipmentExcelTableEntry };
export default class EquipmentExcel {
private constructor() {
}
public static all() : EquipmentExcelTableEntry[] {
return Object.values(EquipmentExcelTable);
}
public static fromId(id: number) : EquipmentExcelTableEntry {
return EquipmentExcelTable[id];
}
public static fromIds(ids: number[]): EquipmentExcelTableEntry[] {
return ids.map(id => EquipmentExcel.fromId(id));
}
}

View File

@ -0,0 +1,20 @@
import _EquipmentExpItemExcelTable from "../../data/excel/EquipmentExpItemExcelTable.json";
type EquipmentExpItemExcelTableEntry = typeof _EquipmentExpItemExcelTable[keyof typeof _EquipmentExpItemExcelTable]
const EquipmentExpItemExcelTable = _EquipmentExpItemExcelTable as { [key: string]: EquipmentExpItemExcelTableEntry };
export default class EquipmentExpItemExcel {
private constructor() {
}
public static all() : EquipmentExpItemExcelTableEntry[] {
return Object.values(EquipmentExpItemExcelTable);
}
public static fromId(id: number) : EquipmentExpItemExcelTableEntry {
return EquipmentExpItemExcelTable[id];
}
public static fromIds(ids: number[]): EquipmentExpItemExcelTableEntry[] {
return ids.map(id => EquipmentExpItemExcel.fromId(id));
}
}

View File

@ -0,0 +1,20 @@
import _EquipmentExpTypeExcelTable from "../../data/excel/EquipmentExpTypeExcelTable.json";
type EquipmentExpTypeExcelTableEntry = typeof _EquipmentExpTypeExcelTable[keyof typeof _EquipmentExpTypeExcelTable]
const EquipmentExpTypeExcelTable = _EquipmentExpTypeExcelTable as { [key: string]: EquipmentExpTypeExcelTableEntry };
export default class EquipmentExpTypeExcel {
private constructor() {
}
public static all() : EquipmentExpTypeExcelTableEntry[] {
return Object.values(EquipmentExpTypeExcelTable);
}
public static fromId(id: string) : EquipmentExpTypeExcelTableEntry {
return EquipmentExpTypeExcelTable[id];
}
public static fromIds(ids: string[]): EquipmentExpTypeExcelTableEntry[] {
return ids.map(id => EquipmentExpTypeExcel.fromId(id));
}
}

View File

@ -0,0 +1,20 @@
import _EquipmentPromotionExcelTable from "../../data/excel/EquipmentPromotionExcelTable.json";
type EquipmentPromotionExcelTableEntry = typeof _EquipmentPromotionExcelTable[keyof typeof _EquipmentPromotionExcelTable]
const EquipmentPromotionExcelTable = _EquipmentPromotionExcelTable as { [key: string]: EquipmentPromotionExcelTableEntry };
export default class EquipmentPromotionExcel {
private constructor() {
}
public static all() : EquipmentPromotionExcelTableEntry[] {
return Object.values(EquipmentPromotionExcelTable);
}
public static fromId(id: string) : EquipmentPromotionExcelTableEntry {
return EquipmentPromotionExcelTable[id];
}
public static fromIds(ids: string[]): EquipmentPromotionExcelTableEntry[] {
return ids.map(id => EquipmentPromotionExcel.fromId(id));
}
}

View File

@ -0,0 +1,24 @@
import _MapEntryExcelTable from "../../data/excel/MapEntryExcelTable.json";
type MapEntryExcelTableEntry = typeof _MapEntryExcelTable[keyof typeof _MapEntryExcelTable]
const MapEntryExcelTable = _MapEntryExcelTable as { [key: string]: MapEntryExcelTableEntry };
export default class MapEntryExcel {
private constructor() {
}
public static all() : MapEntryExcelTableEntry[] {
return Object.values(MapEntryExcelTable);
}
public static fromId(id: number) : MapEntryExcelTableEntry {
return MapEntryExcelTable[id];
}
public static fromIds(ids: number[]): MapEntryExcelTableEntry[] {
return ids.map(id => MapEntryExcel.fromId(id));
}
public static fromFloorId(id: number) : MapEntryExcelTableEntry {
return Object.values(MapEntryExcelTable).filter(e => e.FloorID == id)?.[0];
}
}

View File

@ -1,5 +1,5 @@
import _MapEntryExcelTable from "../../data/excel/MapEntryExcelTable.json";
import _MazePlaneExcelTable from "../../data/excel/MazePlaneExcelTable.json";
import MapEntryExcel from "./MapEntryExcel";
interface MazePlaneExcelTableEntry {
PlaneID: number;
@ -11,60 +11,35 @@ interface MazePlaneExcelTableEntry {
FloorIDList: number[];
}
interface TextMap {
hash: number;
}
type EntranceType = "Town" | "Mission" | "Explore";
interface MapEntryExcelTableEntry {
ID: number;
IsShowInMapMenu: boolean;
MapMenuSortID: number;
EntranceType: EntranceType | number; // Actually an enum. Town | Mission | Explore
EntranceGroupID: number;
Name: TextMap;
Desc: TextMap;
EntranceListIcon: string;
ImagePath: string;
MiniMapIconHintList: any[];
ShowReward: number;
PlaneID: number;
FloorID: number;
StartGroupID: number;
StartAnchorID: number;
TargetMission: number;
TargetMainMissionList: number[];
BeginMainMissionList: number[];
FinishMainMissionList: number[];
FinishQuestList: number[];
UnlockQuest: number;
}
const MazePlaneExcelTable = _MazePlaneExcelTable as { [key: string]: MazePlaneExcelTableEntry };
const MapEntryExcelTable = _MapEntryExcelTable as { [key: string]: MapEntryExcelTableEntry };
export default class MazePlaneExcel {
private constructor() { }
public static fromEntryId(entryId: number): MazePlaneExcelTableEntry {
const mapEntry = MapEntryExcelTable[entryId.toString()];
const mapEntry = MapEntryExcel.fromId(entryId);
return MazePlaneExcelTable[mapEntry.PlaneID.toString()];
}
public static fromPlaneId(planeId: number): MazePlaneExcelTableEntry {
return MazePlaneExcelTable[planeId.toString()];
}
public static fromFloorId(floorId: number): MazePlaneExcelTableEntry {
return MazePlaneExcelTable[floorId.toString().slice(0,5)];
}
public static getEntry(entryId: number): MapEntryExcelTableEntry {
return MapEntryExcelTable[entryId.toString()];
}
public static getGameModeForPlaneType(planeType: string): number {
switch (planeType) {
case "Town": return 1;
case "Maze": return 2;
case "Train": return 3;
case "Challenge": return 4;
case "RogueExplore": return 5;
case "RogueChallenge": return 6;
case "TownRoom": return 7;
case "Raid": return 8;
case "FarmRelic": return 9;
case "Client": return 10;
case "ChallengeActivity": return 11;
}
public static getAllEntries(): MapEntryExcelTableEntry[] {
return Object.values(MapEntryExcelTable);
return 0;
}
}

View File

@ -0,0 +1,20 @@
import _MonsterExcelTable from "../../data/excel/MonsterExcelTable.json";
type MonsterExcelTableEntry = typeof _MonsterExcelTable[keyof typeof _MonsterExcelTable]
const MonsterExcelTable = _MonsterExcelTable as { [key: string]: MonsterExcelTableEntry };
export default class MonsterExcel {
private constructor() {
}
public static all() : MonsterExcelTableEntry[] {
return Object.values(MonsterExcelTable);
}
public static fromId(id: number) : MonsterExcelTableEntry {
return MonsterExcelTable[id];
}
public static fromIds(ids: number[]): MonsterExcelTableEntry[] {
return ids.map(id => MonsterExcel.fromId(id));
}
}