├── data ├── .gitignore ├── npc.zip ├── items.zip ├── monster.zip ├── set_items.zip ├── monster_drop.zip └── npc_monster_pos.zip ├── .gitignore ├── config ├── .gitignore └── default.yaml ├── up.sh ├── src ├── login_server │ ├── login_socket.ts │ ├── endpoints │ │ ├── index.ts │ │ ├── UNK_REQ.ts │ │ ├── CRYPTION.ts │ │ ├── VERSION_REQ.ts │ │ ├── NEWS.ts │ │ ├── DOWNLOADINFO_REQ.ts │ │ ├── SERVERLIST.ts │ │ ├── CHECK_OTP.ts │ │ └── LOGIN_REQ.ts │ ├── test │ │ └── server.test.ts │ ├── endpoint.ts │ └── server.ts ├── game_server │ ├── functions │ │ ├── isDead.ts │ │ ├── sendWeather.ts │ │ ├── sendWeightChange.ts │ │ ├── getLevelUp.ts │ │ ├── sendQuests.ts │ │ ├── sendTime.ts │ │ ├── sendLookChange.ts │ │ ├── generateItem.ts │ │ ├── sendZoneAbility.ts │ │ ├── sendNoahChange.ts │ │ ├── sendStackChange.ts │ │ ├── sendExperienceChange.ts │ │ ├── sendItemMove.ts │ │ ├── findSlotForItem.ts │ │ ├── sendStateChange.ts │ │ ├── buildNPCDetail.ts │ │ ├── sendBlink.ts │ │ ├── sendNotices.ts │ │ ├── sendTargetHP.ts │ │ ├── sendNoahEvent.ts │ │ ├── sendWarp.ts │ │ ├── sendItem.ts │ │ ├── sendLevelChange.ts │ │ ├── buildUserDetail.ts │ │ ├── getDamage.ts │ │ ├── sendChatMessage.ts │ │ └── sendMyInfo.ts │ ├── ai_system │ │ ├── uuid.ts │ │ ├── summon.ts │ │ └── start.ts │ ├── endpoints │ │ ├── QUEST.ts │ │ ├── RENTAL.ts │ │ ├── HACK_TOOL.ts │ │ ├── SPEEDHACK_CHECK.ts │ │ ├── SERVER_INDEX.ts │ │ ├── LOAD_GAME.ts │ │ ├── STATE_CHANGE.ts │ │ ├── ROTATE.ts │ │ ├── WAREHOUSE.ts │ │ ├── USER_DETAILED_REQUEST.ts │ │ ├── HELMET_STATUS_CHANGE.ts │ │ ├── VERSION_CHECK.ts │ │ ├── ZONE_CHANGE.ts │ │ ├── HOME.ts │ │ ├── DROP_OPEN.ts │ │ ├── NPC_DETAILED_REQUEST.ts │ │ ├── CHANGE_HAIR.ts │ │ ├── POINT_CHANGE.ts │ │ ├── GENIE.ts │ │ ├── SEL_NATION.ts │ │ ├── TARGET_HP.ts │ │ ├── SKILLDATA.ts │ │ ├── CHAT_TARGET.ts │ │ ├── MOVE.ts │ │ ├── USER_INFO.ts │ │ ├── GAME_START.ts │ │ ├── index.ts │ │ ├── NPC_EVENT.ts │ │ ├── ITEM_REMOVE.ts │ │ ├── CHAT.ts │ │ ├── LOGIN.ts │ │ ├── KNIGHT.ts │ │ ├── _INTERNAL_QUERY.ts │ │ ├── FRIEND.ts │ │ ├── ATTACK.ts │ │ ├── ALLCHAR_INFO_REQ.ts │ │ ├── SEL_CHAR.ts │ │ ├── NEW_CHAR.ts │ │ ├── DROP_TAKE.ts │ │ └── SHOPPING_MALL.ts │ ├── shared.ts │ ├── var │ │ ├── level_up.ts │ │ ├── item_slot.ts │ │ └── zone_codes.ts │ ├── events │ │ ├── onUserExit.ts │ │ ├── onServerTick.ts │ │ ├── onRegionUpdate.ts │ │ ├── onNPCTick.ts │ │ └── onNPCDead.ts │ ├── endpoint.ts │ ├── drop.ts │ ├── game_socket.ts │ └── server.ts ├── core │ ├── database │ │ ├── defaults │ │ │ ├── index.ts │ │ │ ├── server.ts │ │ │ ├── account.ts │ │ │ ├── setting.ts │ │ │ ├── set_item.ts │ │ │ └── item.ts │ │ ├── test │ │ │ └── database.test.ts │ │ ├── models │ │ │ ├── index.ts │ │ │ ├── news.ts │ │ │ ├── setting.ts │ │ │ ├── version.ts │ │ │ ├── warehouse.ts │ │ │ ├── server.ts │ │ │ ├── mail.ts │ │ │ ├── account.ts │ │ │ ├── set_item.ts │ │ │ ├── npc.ts │ │ │ └── character.ts │ │ ├── utils │ │ │ ├── model_loader.ts │ │ │ ├── default_checker.ts │ │ │ ├── connect.ts │ │ │ ├── csv_loader.ts │ │ │ └── csv_reader.ts │ │ └── index.ts │ ├── redis │ │ ├── index.ts │ │ └── cache.ts │ └── utils │ │ ├── test │ │ ├── deferred_promise.test.ts │ │ └── average_time.test.ts │ │ ├── average_time.ts │ │ ├── deferred_promise.ts │ │ ├── unique_queue.ts │ │ ├── general.ts │ │ ├── otp.ts │ │ ├── password_hash.ts │ │ └── unit.ts ├── client │ ├── index.ts │ ├── test │ │ ├── client_launcher.test.ts │ │ └── client_login.test.ts │ ├── launcher-client.ts │ └── login-client.ts ├── app.ts └── web │ └── server.ts ├── Dockerfile ├── docs ├── gameserver_59_pp_opcode.md ├── gameserver_72_hacktool_opcode.md └── c2gs_opcode_order.md ├── .travis.yml ├── tsconfig.json ├── .vscode └── launch.json ├── package.json └── docker-compose.yml /data/.gitignore: -------------------------------------------------------------------------------- 1 | *.csv -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | *.log -------------------------------------------------------------------------------- /config/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | 3 | !.gitignore 4 | !default.yaml 5 | !travis.yaml -------------------------------------------------------------------------------- /data/npc.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/co3moz/knightonline-js/HEAD/data/npc.zip -------------------------------------------------------------------------------- /up.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | git pull 4 | docker-compose build 5 | docker-compose up -d -------------------------------------------------------------------------------- /data/items.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/co3moz/knightonline-js/HEAD/data/items.zip -------------------------------------------------------------------------------- /data/monster.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/co3moz/knightonline-js/HEAD/data/monster.zip -------------------------------------------------------------------------------- /data/set_items.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/co3moz/knightonline-js/HEAD/data/set_items.zip -------------------------------------------------------------------------------- /data/monster_drop.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/co3moz/knightonline-js/HEAD/data/monster_drop.zip -------------------------------------------------------------------------------- /data/npc_monster_pos.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/co3moz/knightonline-js/HEAD/data/npc_monster_pos.zip -------------------------------------------------------------------------------- /src/login_server/login_socket.ts: -------------------------------------------------------------------------------- 1 | import type { IKOSocket } from "../core/server.js"; 2 | 3 | export interface ILoginSocket extends IKOSocket {} 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10.14.2 2 | WORKDIR /app 3 | COPY package*.json ./ 4 | RUN npm install --production 5 | COPY . . 6 | RUN npm run build 7 | EXPOSE 15100-15109 15001 8 | CMD [ "node", "--expose-gc" , "build/app"] -------------------------------------------------------------------------------- /src/game_server/functions/isDead.ts: -------------------------------------------------------------------------------- 1 | import type { IGameSocket } from "../game_socket.js"; 2 | 3 | export function isUserDead(socket: IGameSocket) { 4 | let c = socket.character; 5 | 6 | return c.hp == 0; 7 | } 8 | -------------------------------------------------------------------------------- /src/core/database/defaults/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./account.js"; 2 | export * from "./item.js"; 3 | export * from "./npc.js"; 4 | export * from "./server.js"; 5 | export * from "./set_item.js"; 6 | export * from "./setting.js"; 7 | -------------------------------------------------------------------------------- /docs/gameserver_59_pp_opcode.md: -------------------------------------------------------------------------------- 1 | Packet : 59040F2700001038383838373737373636363635353535 2 | 59 - OpCode 3 | 04 - SubOpCode 4 | 0F270000 - İlk kutu değeri (int) 5 | 10 38383838373737373636363635353535 - (ilk kutudan sonraki değerler string) -------------------------------------------------------------------------------- /src/core/database/test/database.test.ts: -------------------------------------------------------------------------------- 1 | import { Spec } from "nole"; 2 | import { Database } from "../index.js"; 3 | 4 | export class DatabaseTest { 5 | @Spec(60000) 6 | async Connect() { 7 | await Database(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/client/index.ts: -------------------------------------------------------------------------------- 1 | import { TestClient } from "./test-client.js"; 2 | 3 | (async function () { 4 | TestClient().catch((x) => { 5 | /* eslint-disable no-process-exit */ 6 | console.error(x.stack); 7 | process.exit(1); 8 | }); 9 | })(); 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | services: 5 | - mongodb 6 | - redis-server 7 | before_script: 8 | - sleep 15 9 | - mongo testdb --eval 'db.createUser({user:"travis",pwd:"test",roles:["readWrite"]});' 10 | addons: 11 | hostname: travis -------------------------------------------------------------------------------- /src/login_server/endpoints/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./CHECK_OTP.js"; 2 | export * from "./CRYPTION.js"; 3 | export * from "./DOWNLOADINFO_REQ.js"; 4 | export * from "./LOGIN_REQ.js"; 5 | export * from "./NEWS.js"; 6 | export * from "./SERVERLIST.js"; 7 | export * from "./UNK_REQ.js"; 8 | export * from "./VERSION_REQ.js"; 9 | -------------------------------------------------------------------------------- /src/game_server/functions/sendWeather.ts: -------------------------------------------------------------------------------- 1 | import type { IGameSocket } from "../game_socket.js"; 2 | 3 | export function SendWeather(socket: IGameSocket): void { 4 | // TODO: Handle weather system 5 | socket.send([ 6 | 0x14, // WEATHER 7 | 1, // rain 8 | 0, // amount 0-100 9 | 0, 10 | ]); 11 | } 12 | -------------------------------------------------------------------------------- /src/game_server/functions/sendWeightChange.ts: -------------------------------------------------------------------------------- 1 | import type { IGameSocket } from "../game_socket.js"; 2 | import { int } from "../../core/utils/unit.js"; 3 | 4 | export function SendWeightChange(socket: IGameSocket): void { 5 | socket.send([ 6 | 0x54, // WEIGHT_CHANGE 7 | ...int(socket.variables.itemWeight), 8 | ]); 9 | } 10 | -------------------------------------------------------------------------------- /src/game_server/ai_system/uuid.ts: -------------------------------------------------------------------------------- 1 | import { UniqueQueue } from "../../core/utils/unique_queue.js"; 2 | import type { INPCInstance } from "./declare.js"; 3 | 4 | export const NPCUUID = UniqueQueue.from(30000, 10000); 5 | 6 | export const NPCMap: INPCMap = {}; 7 | 8 | export interface INPCMap { 9 | [uuid: number]: INPCInstance; 10 | } 11 | -------------------------------------------------------------------------------- /src/game_server/endpoints/QUEST.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue } from "../../core/utils/unit.js"; 4 | 5 | export const QUEST: IGameEndpoint = async function ( 6 | socket: IGameSocket, 7 | body: Queue, 8 | opcode: number 9 | ) { 10 | // TODO: solve this 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_server/endpoints/RENTAL.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue } from "../../core/utils/unit.js"; 4 | 5 | export const RENTAL: IGameEndpoint = async function ( 6 | socket: IGameSocket, 7 | body: Queue, 8 | opcode: number 9 | ) { 10 | // TODO: solve this 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_server/endpoints/HACK_TOOL.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue } from "../../core/utils/unit.js"; 4 | 5 | export const HACK_TOOL: IGameEndpoint = async function ( 6 | socket: IGameSocket, 7 | body: Queue, 8 | opcode: number 9 | ) { 10 | // TODO: solve this 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_server/functions/getLevelUp.ts: -------------------------------------------------------------------------------- 1 | import { rawLevelUp } from "../var/level_up.js"; 2 | 3 | export const INVALID_EXP = 10000000000; 4 | 5 | export function GetLevelUp(level: number, rebirth: number = 0): number { 6 | if (level < 0 && level > 83) return INVALID_EXP; 7 | if (rebirth < 0 && rebirth > 10) return INVALID_EXP; 8 | 9 | return rawLevelUp[level + rebirth]; 10 | } 11 | -------------------------------------------------------------------------------- /src/login_server/endpoints/UNK_REQ.ts: -------------------------------------------------------------------------------- 1 | import { Queue } from "../../core/utils/unit.js"; 2 | import type { ILoginSocket } from "../login_socket.js"; 3 | import type { ILoginEndpoint } from "../endpoint.js"; 4 | 5 | export const UNK_REQ: ILoginEndpoint = async function ( 6 | socket: ILoginSocket, 7 | body: Queue, 8 | opcode: number 9 | ) { 10 | socket.send([0xfd, 0, 0]); 11 | }; 12 | -------------------------------------------------------------------------------- /src/game_server/endpoints/SPEEDHACK_CHECK.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue } from "../../core/utils/unit.js"; 4 | 5 | export const SPEEDHACK_CHECK: IGameEndpoint = async function ( 6 | socket: IGameSocket, 7 | body: Queue, 8 | opcode: number 9 | ) { 10 | // TODO: solve this 11 | }; 12 | -------------------------------------------------------------------------------- /src/core/database/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./account.js"; 2 | export * from "./character.js"; 3 | export * from "./item.js"; 4 | export * from "./mail.js"; 5 | export * from "./news.js"; 6 | export * from "./npc.js"; 7 | export * from "./server.js"; 8 | export * from "./set_item.js"; 9 | export * from "./setting.js"; 10 | export * from "./version.js"; 11 | export * from "./warehouse.js"; 12 | -------------------------------------------------------------------------------- /src/core/redis/index.ts: -------------------------------------------------------------------------------- 1 | import redis from "redis"; 2 | import config from "config"; 3 | 4 | export const redisClient = redis.createClient(config.get("redis")); 5 | 6 | let redisConnection: Promise | undefined = undefined; 7 | 8 | export async function RedisConnect() { 9 | if (!redisConnection) { 10 | redisConnection = redisClient.connect(); 11 | } 12 | await redisConnection; 13 | } 14 | -------------------------------------------------------------------------------- /src/game_server/endpoints/SERVER_INDEX.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue, short } from "../../core/utils/unit.js"; 4 | 5 | export const SERVER_INDEX: IGameEndpoint = async function ( 6 | socket: IGameSocket, 7 | body: Queue, 8 | opcode: number 9 | ) { 10 | socket.send([opcode, 1, 0, ...short(1)]); 11 | }; 12 | -------------------------------------------------------------------------------- /src/core/database/models/news.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, Document } from "mongoose"; 2 | 3 | export interface INews extends Document { 4 | title: string; 5 | message: string; 6 | } 7 | 8 | export const NewsSchema = new Schema( 9 | { 10 | title: { type: String, maxlength: 30 }, 11 | message: { type: String, maxlength: 200 }, 12 | }, 13 | { timestamps: false } 14 | ); 15 | 16 | export const News = model("News", NewsSchema, "news"); 17 | -------------------------------------------------------------------------------- /src/game_server/endpoints/LOAD_GAME.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue } from "../../core/utils/unit.js"; 4 | 5 | export const LOAD_GAME: IGameEndpoint = async function ( 6 | socket: IGameSocket, 7 | body: Queue, 8 | opcode: number 9 | ) { 10 | socket.send([ 11 | opcode, 12 | 1, 13 | 0, 14 | 0, 15 | 0, 16 | 0, //unit.int(0) 17 | ]); 18 | }; 19 | -------------------------------------------------------------------------------- /src/core/database/models/setting.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, Document, SchemaTypes } from "mongoose"; 2 | 3 | export interface ISetting extends Document { 4 | key: string; 5 | value: any; 6 | type: string; 7 | } 8 | 9 | export const SettingSchema = new Schema( 10 | { 11 | key: { type: String }, 12 | value: { type: SchemaTypes.Mixed }, 13 | type: { type: String }, 14 | }, 15 | { timestamps: true } 16 | ); 17 | 18 | export const Setting = model("Setting", SettingSchema, "settings"); 19 | -------------------------------------------------------------------------------- /src/login_server/test/server.test.ts: -------------------------------------------------------------------------------- 1 | import { Spec, Hook, Dependency } from "nole"; 2 | import { LoginServer } from "../server.js"; 3 | import type { IKOServer } from "../../core/server.js"; 4 | import { DatabaseTest } from "../../core/database/test/database.test.js"; 5 | 6 | export class LoginServerTest { 7 | @Dependency(DatabaseTest) 8 | databaseTest: DatabaseTest; 9 | 10 | servers: IKOServer[]; 11 | 12 | @Spec(250) 13 | async StartServer() { 14 | this.servers = await LoginServer(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/core/database/models/version.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, Document } from "mongoose"; 2 | 3 | export interface IVersion extends Document { 4 | version: number; 5 | fileName: string; 6 | } 7 | 8 | export const VersionSchema = new Schema( 9 | { 10 | version: { type: Number, required: true, min: 0, max: 10000 }, 11 | fileName: { type: String, required: true, maxlength: 1000 }, 12 | }, 13 | { timestamps: false } 14 | ); 15 | 16 | export const Version = model("Version", VersionSchema, "version"); 17 | -------------------------------------------------------------------------------- /src/game_server/functions/sendQuests.ts: -------------------------------------------------------------------------------- 1 | import type { IGameSocket } from "../game_socket.js"; 2 | import { short } from "../../core/utils/unit.js"; 3 | 4 | export function SendQuests(socket: IGameSocket) { 5 | let quests = socket.character.quests; 6 | 7 | let result: number[] = []; 8 | 9 | for (let quest of quests) { 10 | result.push(...short(quest.id), quest.state); 11 | } 12 | 13 | socket.send([ 14 | 0x64, // QUEST 15 | 1, 16 | ...short(quests.length), 17 | ...result, 18 | ]); 19 | } 20 | -------------------------------------------------------------------------------- /src/core/database/utils/model_loader.ts: -------------------------------------------------------------------------------- 1 | import { glob } from "glob"; 2 | import path from "path"; 3 | import { fileURLToPath } from "url"; 4 | 5 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 6 | 7 | export async function ModelLoader() { 8 | try { 9 | const files = await glob(path.resolve(__dirname, "../models/**/*.{ts,js}")); 10 | 11 | for (let file of files) { 12 | await import(file); 13 | } 14 | } catch (e) { 15 | console.error("model loader failed!"); 16 | throw e; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/game_server/ai_system/summon.ts: -------------------------------------------------------------------------------- 1 | import type { INpc, ISpawn } from "../../core/database/models/index.js"; 2 | import type { INPCInstance } from "./declare.js"; 3 | import { NPCUUID, NPCMap } from "./uuid.js"; 4 | 5 | export function SummonNPC(npc: INpc, spawn: ISpawn) { 6 | let npcObj: INPCInstance = { 7 | npc, 8 | spawn, 9 | 10 | status: "init", 11 | uuid: NPCUUID.reserve(), 12 | timestamp: Date.now(), 13 | wait: 0, 14 | }; 15 | 16 | NPCMap[npcObj.uuid] = npcObj; 17 | 18 | return npcObj; 19 | } 20 | -------------------------------------------------------------------------------- /src/game_server/functions/sendTime.ts: -------------------------------------------------------------------------------- 1 | import type { IGameSocket } from "../game_socket.js"; 2 | import { short } from "../../core/utils/unit.js"; 3 | 4 | export function SendTime(socket: IGameSocket): void { 5 | let now = new Date(); 6 | 7 | // 0x13, then year, month, day, hours, mins as short 8 | socket.send([ 9 | 0x13, 10 | ...short(now.getFullYear()), 11 | now.getMonth() + 1, 12 | 0, 13 | now.getDate(), 14 | 0, 15 | now.getHours(), 16 | 0, 17 | now.getMinutes(), 18 | 0, 19 | ]); 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleDetection": "auto", 4 | "target": "es2022", 5 | "module": "node16", 6 | "moduleResolution": "node16", 7 | "allowJs": true, 8 | "esModuleInterop": false, 9 | "isolatedModules": true, 10 | "verbatimModuleSyntax": true, 11 | "outDir": "build", 12 | "experimentalDecorators": true, 13 | "allowSyntheticDefaultImports": true, 14 | "skipLibCheck": true 15 | }, 16 | "include": ["src/**/*.ts"], 17 | "exclude": ["node_modules", "build"] 18 | } 19 | -------------------------------------------------------------------------------- /src/login_server/endpoints/CRYPTION.ts: -------------------------------------------------------------------------------- 1 | import { Queue } from "../../core/utils/unit.js"; 2 | import type { ILoginSocket } from "../login_socket.js"; 3 | import type { ILoginEndpoint } from "../endpoint.js"; 4 | import { Crypt } from "../../core/utils/crypt.js"; 5 | 6 | export const CRYPTION: ILoginEndpoint = async function ( 7 | socket: ILoginSocket, 8 | body: Queue, 9 | opcode: number 10 | ) { 11 | if (!socket.cryption) { 12 | socket.cryption = Crypt.createInstance(); 13 | } 14 | 15 | socket.send([opcode, ...socket.cryption.publicKey()]); 16 | }; 17 | -------------------------------------------------------------------------------- /src/game_server/endpoints/STATE_CHANGE.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue } from "../../core/utils/unit.js"; 4 | import { SendStateChange } from "../functions/sendStateChange.js"; 5 | 6 | export const STATE_CHANGE: IGameEndpoint = async function ( 7 | socket: IGameSocket, 8 | body: Queue, 9 | opcode: number 10 | ) { 11 | let subOpcode = body.byte(); 12 | let value = body.int(); 13 | 14 | if (subOpcode == 1) { 15 | SendStateChange(socket, subOpcode, value); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/game_server/functions/sendLookChange.ts: -------------------------------------------------------------------------------- 1 | import type { IGameSocket } from "../game_socket.js"; 2 | import { short, int } from "../../core/utils/unit.js"; 3 | import { RegionSend } from "../region.js"; 4 | import type { ICharacterItem } from "../../core/database/models/index.js"; 5 | 6 | export function SendLookChange( 7 | socket: IGameSocket, 8 | pos: number, 9 | item: ICharacterItem 10 | ) { 11 | RegionSend(socket, [ 12 | 0x2d, // look change 13 | ...short(socket.session), 14 | pos, 15 | ...int(item ? item.id : 0), 16 | ...short(item ? item.durability : 0), 17 | ]); 18 | } 19 | -------------------------------------------------------------------------------- /src/core/database/utils/default_checker.ts: -------------------------------------------------------------------------------- 1 | import { glob } from "glob"; 2 | import path from "path"; 3 | import { fileURLToPath } from "url"; 4 | 5 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 6 | 7 | export async function DefaultChecker() { 8 | try { 9 | const files = await glob( 10 | path.resolve(__dirname, "../defaults/**/*.{ts,js}") 11 | ); 12 | 13 | for (let file of files) { 14 | let lib = await (import(file)); 15 | await lib(); 16 | } 17 | } catch (e) { 18 | console.error("default checker failed!"); 19 | throw e; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/core/utils/test/deferred_promise.test.ts: -------------------------------------------------------------------------------- 1 | import { Spec } from "nole"; 2 | import { assert } from "chai"; 3 | import { CreateDeferredPromise } from "../deferred_promise.js"; 4 | 5 | export class DeferredPromiseTest { 6 | @Spec(10) 7 | async BasicCase() { 8 | let promise = CreateDeferredPromise(); 9 | 10 | promise.resolve(1); 11 | 12 | assert.equal(await promise, 1); 13 | } 14 | 15 | @Spec(10) 16 | async ErrorCase() { 17 | let promise = CreateDeferredPromise(); 18 | promise.reject(1); 19 | 20 | try { 21 | await promise; 22 | } catch (e) {} 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/game_server/endpoints/ROTATE.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue, short } from "../../core/utils/unit.js"; 4 | import { RegionSend } from "../region.js"; 5 | 6 | export const ROTATE: IGameEndpoint = async function ( 7 | socket: IGameSocket, 8 | body: Queue, 9 | opcode: number 10 | ) { 11 | let c = socket.character; 12 | 13 | if (!c) return; 14 | 15 | let direction = body.short(); 16 | c.direction = direction; 17 | 18 | RegionSend(socket, [opcode, ...short(socket.session), ...short(direction)]); 19 | }; 20 | -------------------------------------------------------------------------------- /src/game_server/endpoints/WAREHOUSE.ts: -------------------------------------------------------------------------------- 1 | import { Queue } from "../../core/utils/unit.js"; 2 | import type { IGameEndpoint } from "../endpoint.js"; 3 | import type { IGameSocket } from "../game_socket.js"; 4 | 5 | export const WAREHOUSE: IGameEndpoint = async function ( 6 | socket: IGameSocket, 7 | body: Queue, 8 | opcode: number 9 | ) { 10 | let type = body.byte(); 11 | 12 | if (type == WarehouseType.Open) { 13 | // TODO: handle me 14 | } 15 | }; 16 | 17 | export enum WarehouseType { 18 | Open = 1, 19 | Input = 2, 20 | Output = 3, 21 | Move = 4, 22 | InventoryMove = 5, 23 | 24 | Request = 10, 25 | } 26 | -------------------------------------------------------------------------------- /src/game_server/shared.ts: -------------------------------------------------------------------------------- 1 | import { SetItem, type ISetItem } from "../core/database/models/index.js"; 2 | import type { IGameSocket } from "./game_socket.js"; 3 | 4 | export const UserMap: { [name: string]: IGameSocket } = {}; 5 | export const CharacterMap: { [name: string]: IGameSocket } = {}; 6 | export let SetItems: { [itemId: string]: ISetItem } = {}; 7 | 8 | export async function LoadSetItems() { 9 | let obj = {}; 10 | let setItems = await SetItem.find({}).lean().select(["-id", "-_id"]).exec(); 11 | 12 | for (let setItem of setItems) { 13 | obj[setItem.id] = setItem; 14 | } 15 | 16 | SetItems = obj; 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "runtimeExecutable": "tsx", 11 | "name": "Launch", 12 | "cwd": "${workspaceRoot}", 13 | "args": ["./src/app.ts"], 14 | "sourceMaps": true, 15 | "outputCapture": "std", 16 | "env": { 17 | "PRETTY": "1" 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/game_server/functions/generateItem.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ICharacterItem, 3 | IItem, 4 | } from "../../core/database/models/index.js"; 5 | 6 | export function GenerateItem( 7 | detail: IItem, 8 | amount: number = 1, 9 | flag: number = 0 10 | ): ICharacterItem { 11 | return { 12 | id: detail.id, 13 | durability: detail.durability, 14 | amount, 15 | serial: GenerateItemSerial(), 16 | flag, 17 | }; 18 | } 19 | 20 | let serial = 0; 21 | export function GenerateItemSerial() { 22 | return ( 23 | Date.now().toString(16) + 24 | (++serial & 0xffffff).toString(16).padStart(6, "0") 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/login_server/endpoints/VERSION_REQ.ts: -------------------------------------------------------------------------------- 1 | import config from "config"; 2 | import { short, Queue } from "../../core/utils/unit.js"; 3 | import type { ILoginSocket } from "../login_socket.js"; 4 | import type { ILoginEndpoint } from "../endpoint.js"; 5 | 6 | let versions: any[] = config.get("loginServer.versions"); 7 | let { version: serverVersion } = versions[versions.length - 1]; 8 | 9 | export const VERSION_REQ: ILoginEndpoint = async function ( 10 | socket: ILoginSocket, 11 | body: Queue, 12 | opcode: number 13 | ) { 14 | let clientVersion = body.short(); 15 | 16 | socket.send([opcode, ...short(serverVersion)]); 17 | }; 18 | -------------------------------------------------------------------------------- /src/core/database/utils/connect.ts: -------------------------------------------------------------------------------- 1 | import config from "config"; 2 | import mongoose from "mongoose"; 3 | 4 | export async function ConnectToDatabase(): Promise { 5 | console.log("[DB] Connecting to database..."); 6 | 7 | let connectionUri: string = config.get("database.uri"); 8 | let connectionOptions = Object.assign({}, config.get("database.options")); 9 | 10 | try { 11 | await mongoose.connect(connectionUri, connectionOptions); 12 | return mongoose.connection; 13 | } catch (e) { 14 | console.log("[DB] Connecting to database failed!"); 15 | console.error(e); 16 | process.exit(1); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/core/utils/test/average_time.test.ts: -------------------------------------------------------------------------------- 1 | import { Spec } from "nole"; 2 | import { AverageTime } from "../average_time.js"; 3 | import { assert } from "chai"; 4 | 5 | export class AverageTimeTest { 6 | averageTime: AverageTime; 7 | 8 | @Spec() 9 | CreateInstance() { 10 | this.averageTime = AverageTime.instance(10); 11 | } 12 | 13 | @Spec() 14 | PushSomeData() { 15 | for (let i = 0; i <= 10; i++) { 16 | this.averageTime.push(i); 17 | } 18 | } 19 | 20 | @Spec() 21 | ExpectValueToBeValid() { 22 | assert.equal(this.averageTime.avg(), 5.5); 23 | assert.equal(this.averageTime.values().length, 10); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/login_server/endpoint.ts: -------------------------------------------------------------------------------- 1 | import { Queue } from "../core/utils/unit.js"; 2 | import type { ILoginSocket } from "./login_socket.js"; 3 | import * as Endpoints from './endpoints/index.js' 4 | 5 | export enum LoginEndpointCodes { 6 | VERSION_REQ = 0x01, 7 | DOWNLOADINFO_REQ = 0x02, 8 | CRYPTION = 0xF2, 9 | LOGIN_REQ = 0xF3, 10 | SERVERLIST = 0xF5, 11 | NEWS = 0xF6, 12 | CHECK_OTP = 0xFA, 13 | UNK_REQ = 0xFD 14 | } 15 | 16 | export function LoginEndpoint(name: string): ILoginEndpoint { 17 | return Endpoints[name]; 18 | } 19 | 20 | export interface ILoginEndpoint { 21 | (socket: ILoginSocket, body: Queue, opcode: number): Promise 22 | } -------------------------------------------------------------------------------- /src/game_server/functions/sendZoneAbility.ts: -------------------------------------------------------------------------------- 1 | import type { IGameSocket } from "../game_socket.js"; 2 | import { short } from "../../core/utils/unit.js"; 3 | import { ZoneRules, ZoneFlags } from "../var/zone_rules.js"; 4 | 5 | export function SendZoneAbility(socket: IGameSocket): void { 6 | let zoneRule = ZoneRules[socket.character.zone]; 7 | if (!zoneRule) return; 8 | 9 | socket.send([ 10 | 0x5e, 11 | 1, // ZONEABILITY 12 | +!!(zoneRule.flag & ZoneFlags.TRADE_OTHER_NATION), 13 | zoneRule.type, 14 | +!!(zoneRule.flag & ZoneFlags.TALK_OTHER_NATION), 15 | ...short(zoneRule.tariff), //TODO: dynamic tariff's for later 16 | ]); 17 | } 18 | -------------------------------------------------------------------------------- /src/game_server/endpoints/USER_DETAILED_REQUEST.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue } from "../../core/utils/unit.js"; 4 | import { SendRegionUserInDetailMultiple } from "../functions/sendRegionInOut.js"; 5 | 6 | export const USER_DETAILED_REQUEST: IGameEndpoint = async function ( 7 | socket: IGameSocket, 8 | body: Queue, 9 | opcode: number 10 | ) { 11 | let sessioncount = body.short(); 12 | let sessions = []; 13 | 14 | for (let i = 0; i < sessioncount; i++) { 15 | sessions.push(body.short()); 16 | } 17 | 18 | SendRegionUserInDetailMultiple(socket, sessions); 19 | }; 20 | -------------------------------------------------------------------------------- /src/core/database/defaults/server.ts: -------------------------------------------------------------------------------- 1 | import { Server } from "../models/index.js"; 2 | 3 | export async function ServerDefaults() { 4 | let testServer = { 5 | ip: "127.0.0.1", 6 | lanip: "127.0.0.1", 7 | name: "TEST|SERVER 1", 8 | karusKing: "KARUS KING", 9 | karusNotice: "KARUS NOTICE", 10 | elmoradKing: "ELMORAD KING", 11 | elmoradNotice: "ELMORAD NOTICE", 12 | }; 13 | 14 | let data = await Server.findOne({ 15 | name: testServer.name, 16 | }).exec(); 17 | 18 | if (data) { 19 | return; 20 | } 21 | 22 | let server = new Server(testServer); 23 | 24 | await server.save(); 25 | 26 | console.log("[DB] A test server has been defined!"); 27 | } 28 | -------------------------------------------------------------------------------- /src/game_server/endpoints/HELMET_STATUS_CHANGE.ts: -------------------------------------------------------------------------------- 1 | import { Queue, short } from "../../core/utils/unit.js"; 2 | import type { IGameEndpoint } from "../endpoint.js"; 3 | import type { IGameSocket } from "../game_socket.js"; 4 | import { RegionSend } from "../region.js"; 5 | 6 | export const HELMET_STATUS_CHANGE: IGameEndpoint = async function ( 7 | socket: IGameSocket, 8 | body: Queue, 9 | opcode: number 10 | ) { 11 | if (socket.character.hp == 0) { 12 | return; 13 | } 14 | 15 | let v = socket.variables; 16 | v.isHelmetHiding = body.byte(); 17 | v.isCospreHiding = body.byte(); 18 | 19 | RegionSend(socket, [ 20 | opcode, 21 | v.isHelmetHiding, 22 | v.isCospreHiding, 23 | ...short(socket.session), 24 | ]); 25 | }; 26 | -------------------------------------------------------------------------------- /src/login_server/endpoints/NEWS.ts: -------------------------------------------------------------------------------- 1 | import { Queue, string } from "../../core/utils/unit.js"; 2 | import type { ILoginSocket } from "../login_socket.js"; 3 | import { News } from "../../core/database/models/index.js"; 4 | import { RedisCaching } from "../../core/redis/cache.js"; 5 | import type { ILoginEndpoint } from "../endpoint.js"; 6 | 7 | export const NEWS: ILoginEndpoint = async function ( 8 | socket: ILoginSocket, 9 | body: Queue, 10 | opcode: number 11 | ) { 12 | let news = await RedisCaching("news", NewsCache); 13 | 14 | socket.send([opcode, ...string("Login Notice"), ...news]); 15 | }; 16 | 17 | async function NewsCache() { 18 | let news = await News.find().exec(); 19 | 20 | return string(news.map((x) => x.title + "#" + x.message + "#").join("")); 21 | } 22 | -------------------------------------------------------------------------------- /src/game_server/endpoints/VERSION_CHECK.ts: -------------------------------------------------------------------------------- 1 | import config from "config"; 2 | import type { IGameEndpoint } from "../endpoint.js"; 3 | import type { IGameSocket } from "../game_socket.js"; 4 | import { Queue, short } from "../../core/utils/unit.js"; 5 | import { Crypt } from "../../core/utils/crypt.js"; 6 | 7 | const clientExeVersion = config.get("gameServer.clientExeVersion"); 8 | 9 | export const VERSION_CHECK: IGameEndpoint = async function ( 10 | socket: IGameSocket, 11 | body: Queue, 12 | opcode: number 13 | ) { 14 | if (!socket.cryption) { 15 | socket.cryption = Crypt.createInstance(); 16 | } 17 | 18 | socket.send([ 19 | opcode, 20 | 0, 21 | ...short(clientExeVersion), // tell client the expected exe version 22 | ...socket.cryption.publicKey(), 23 | 0, // 0: ok, 1: ng 24 | ]); 25 | }; 26 | -------------------------------------------------------------------------------- /src/core/database/models/warehouse.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, SchemaTypes } from "mongoose"; 2 | import type { ICharacterItem } from "./character.js"; 3 | 4 | export interface IWarehouse { 5 | money: number; 6 | items: ICharacterItem[]; 7 | } 8 | 9 | export const WarehouseSchema = new Schema( 10 | { 11 | money: { type: Number, default: 0 }, 12 | items: [ 13 | { 14 | id: { type: Number }, 15 | durability: { type: Number }, 16 | amount: { type: Number }, 17 | serial: { type: String }, 18 | expire: { type: Number }, 19 | flag: { type: Number }, 20 | detail: SchemaTypes.Mixed, 21 | }, 22 | ], 23 | }, 24 | { timestamps: true } 25 | ); 26 | 27 | export const Warehouse = model( 28 | "Warehouse", 29 | WarehouseSchema, 30 | "warehouses" 31 | ); 32 | -------------------------------------------------------------------------------- /src/game_server/functions/sendNoahChange.ts: -------------------------------------------------------------------------------- 1 | import type { IGameSocket } from "../game_socket.js"; 2 | import { int } from "../../core/utils/unit.js"; 3 | 4 | const MAX_NOAH = 2100000000; 5 | export function SendNoahChange( 6 | socket: IGameSocket, 7 | noah: number, 8 | shouldISendPacket = true 9 | ) { 10 | let c = socket.character; 11 | if (noah < 0) { 12 | if (c.money + noah < 0) { 13 | return false; 14 | } 15 | } 16 | 17 | c.money = Math.min(MAX_NOAH, c.money + noah); 18 | 19 | if (shouldISendPacket) { 20 | socket.send([ 21 | 0x4a, // GOLD_CHANGE 22 | noah < 0 ? GoldGainType.Loss : GoldGainType.Gain, 23 | ...int(Math.abs(noah)), 24 | ...int(c.money), 25 | ]); 26 | } 27 | 28 | return true; 29 | } 30 | 31 | export enum GoldGainType { 32 | Gain = 1, 33 | Loss = 2, 34 | Event = 5, 35 | } 36 | -------------------------------------------------------------------------------- /src/core/database/defaults/account.ts: -------------------------------------------------------------------------------- 1 | import { Account } from "../models/index.js"; 2 | 3 | export async function AccountDefaults() { 4 | let testUsers = [ 5 | { 6 | account: "test", 7 | password: "1", 8 | }, 9 | { 10 | account: "test2", 11 | password: "1", 12 | }, 13 | { 14 | account: "banned", 15 | password: "1", 16 | banned: true, 17 | bannedMessage: "banli hesap", 18 | }, 19 | { 20 | account: "otp", 21 | password: "1", 22 | otp: true, 23 | otpSecret: "AAAAAAAAAAAAAAAA", 24 | }, 25 | ]; 26 | 27 | for (let user of testUsers) { 28 | let data = await Account.findOne({ 29 | account: user.account, 30 | }).exec(); 31 | 32 | if (data) { 33 | continue; 34 | } 35 | 36 | let account = new Account(user); 37 | 38 | await account.save(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/game_server/functions/sendStackChange.ts: -------------------------------------------------------------------------------- 1 | import type { IGameSocket } from "../game_socket.js"; 2 | import { int, short } from "../../core/utils/unit.js"; 3 | import { CalculateUserAbilities } from "./sendAbility.js"; 4 | import { SendWeightChange } from "./sendWeightChange.js"; 5 | 6 | export function SendStackChange( 7 | socket: IGameSocket, 8 | itemId: number, 9 | count: number, 10 | durability: number, 11 | pos: number, 12 | newItem: boolean, 13 | expirationTime: number 14 | ) { 15 | socket.send([ 16 | 0x3d, // ITEM COUNT CHANGE 17 | ...short(1), 18 | 1, 19 | pos, 20 | ...int(itemId), 21 | ...int(count), 22 | newItem ? 100 : 0, 23 | ...short(durability), 24 | ...int(0), 25 | ...int(expirationTime), 26 | 0, 27 | 0, 28 | ]); 29 | 30 | CalculateUserAbilities(socket); 31 | SendWeightChange(socket); 32 | 33 | return true; 34 | } 35 | -------------------------------------------------------------------------------- /src/game_server/endpoints/ZONE_CHANGE.ts: -------------------------------------------------------------------------------- 1 | import { Queue } from "../../core/utils/unit.js"; 2 | import type { IGameEndpoint } from "../endpoint.js"; 3 | import type { IGameSocket } from "../game_socket.js"; 4 | import { SendRegionUserInMultiple } from "../functions/sendRegionInOut.js"; 5 | import { SendBlinkStart } from "../functions/sendBlink.js"; 6 | 7 | export const ZONE_CHANGE: IGameEndpoint = async function ( 8 | socket: IGameSocket, 9 | body: Queue, 10 | opcode: number 11 | ) { 12 | let subOpcode = body.byte(); 13 | 14 | if (subOpcode == ZoneChangeEnum.LOADING) { 15 | socket.send([opcode, ZoneChangeEnum.LOADED]); 16 | } else if (subOpcode == ZoneChangeEnum.LOADED) { 17 | SendRegionUserInMultiple(socket, true); 18 | SendBlinkStart(socket); 19 | } 20 | }; 21 | 22 | export enum ZoneChangeEnum { 23 | LOADING = 1, 24 | LOADED = 2, 25 | TELEPORT = 3, 26 | MILITARY = 4, 27 | } 28 | -------------------------------------------------------------------------------- /src/game_server/endpoints/HOME.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue } from "../../core/utils/unit.js"; 4 | import { ZoneCode } from "../var/zone_codes.js"; 5 | import { SendWarp } from "../functions/sendWarp.js"; 6 | 7 | export const HOME: IGameEndpoint = async function ( 8 | socket: IGameSocket, 9 | body: Queue, 10 | opcode: number 11 | ) { 12 | let c = socket.character; 13 | let v = socket.variables; 14 | 15 | if ( 16 | c.zone == ZoneCode.ZONE_CHAOS_DUNGEON || 17 | c.zone == ZoneCode.ZONE_JURAD_MOUNTAIN || 18 | c.zone == ZoneCode.ZONE_BORDER_DEFENSE_WAR 19 | ) { 20 | return; 21 | } 22 | 23 | let now = Date.now(); 24 | if (v.lastHome) { 25 | if (v.lastHome > now) return; 26 | } 27 | 28 | v.lastHome = now + 1000; 29 | 30 | // TODO: HOME add health controls etc.. 31 | 32 | SendWarp(socket, c.zone); 33 | }; 34 | -------------------------------------------------------------------------------- /src/client/test/client_launcher.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import { Spec, Dependency } from "nole"; 3 | import { LoginServerTest } from "../../login_server/test/server.test.js"; 4 | import { ConnectKOServerForLauncher } from "../launcher-client.js"; 5 | 6 | export class ClientLauncherTest { 7 | @Dependency(LoginServerTest) 8 | loginServerTest: LoginServerTest; 9 | 10 | data; 11 | 12 | @Spec(20) 13 | async Connect() { 14 | let ports = this.loginServerTest.servers[0].params.ports; 15 | let randomPort = ports[(Math.random() * ports.length) | 0]; 16 | this.data = await ConnectKOServerForLauncher("127.0.0.1", randomPort); 17 | } 18 | 19 | @Spec() 20 | Validate() { 21 | let data = this.data; 22 | assert.isObject(data); 23 | assert.equal(data.ftpAddress, "localhost"); 24 | assert.equal(data.ftpRoot, "/"); 25 | assert.isArray(data.ftpFiles); 26 | assert.equal(data.ftpFiles.length, 1); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/login_server/endpoints/DOWNLOADINFO_REQ.ts: -------------------------------------------------------------------------------- 1 | import config from "config"; 2 | import { Queue, string, configString, short } from "../../core/utils/unit.js"; 3 | import type { ILoginSocket } from "../login_socket.js"; 4 | import type { ILoginEndpoint } from "../endpoint.js"; 5 | 6 | let versions: any[] = config.get("loginServer.versions"); 7 | 8 | export const DOWNLOADINFO_REQ: ILoginEndpoint = async function ( 9 | socket: ILoginSocket, 10 | body: Queue, 11 | opcode: number 12 | ) { 13 | let result = []; 14 | let totalFile = 0; 15 | let clientVersion = body.short(); 16 | 17 | for (let version of versions) { 18 | if (version.version > clientVersion) { 19 | totalFile++; 20 | result.push(...string(version.fileName)); 21 | } 22 | } 23 | 24 | socket.send([ 25 | opcode, 26 | ...configString("loginServer.ftp.host"), 27 | ...configString("loginServer.ftp.dir"), 28 | ...short(totalFile), 29 | ...result, 30 | ]); 31 | }; 32 | -------------------------------------------------------------------------------- /src/game_server/endpoints/DROP_OPEN.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue, int, short } from "../../core/utils/unit.js"; 4 | import { GetDrop } from "../drop.js"; 5 | 6 | export const DROP_OPEN: IGameEndpoint = async function ( 7 | socket: IGameSocket, 8 | body: Queue, 9 | opcode: number 10 | ) { 11 | let dropIndex = body.int(); 12 | 13 | let drop = GetDrop(dropIndex); 14 | if (!drop) return; 15 | 16 | let session = socket.session; 17 | if (!drop.owners || drop.owners.findIndex((owner) => owner == session) == -1) 18 | return; 19 | 20 | let result = [opcode, ...int(dropIndex), 1]; 21 | 22 | for (let i = 0; i < 6; i++) { 23 | let item = drop.dropped[i]; 24 | if (item) { 25 | result.push(...int(item.item), ...short(item.amount)); 26 | } else { 27 | result.push(0, 0, 0, 0, 0, 0); 28 | } 29 | } 30 | 31 | socket.send(result); 32 | }; 33 | -------------------------------------------------------------------------------- /src/client/test/client_login.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import { Spec, Dependency } from "nole"; 3 | import { LoginServerTest } from "../../login_server/test/server.test.js"; 4 | import { ConnectLoginClient } from "../login-client.js"; 5 | 6 | export class ClientLoginTest { 7 | @Dependency(LoginServerTest) 8 | loginServerTest: LoginServerTest; 9 | 10 | data; 11 | 12 | @Spec(80) 13 | async Connect() { 14 | let ports = this.loginServerTest.servers[0].params.ports; 15 | let randomPort = ports[(Math.random() * ports.length) | 0]; 16 | this.data = await ConnectLoginClient("127.0.0.1", randomPort, "test", "1"); 17 | } 18 | 19 | @Spec() 20 | Validate() { 21 | let data = this.data; 22 | assert.isString(data.sessionCode); 23 | assert.isArray(data.servers); 24 | assert.isObject(data.news); 25 | assert.isString(data.news.header); 26 | assert.isString(data.news.message); 27 | assert.isNumber(data.premiumHours); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/game_server/var/level_up.ts: -------------------------------------------------------------------------------- 1 | export const rawLevelUp = [ 2 | 50, 100, 190, 342, 581, 929, 1393, 1950, 2535, 5070, 6084, 7300, 8760, 10512, 3 | 12614, 15136, 18163, 21795, 26154, 52308, 60154, 69177, 79553, 91485, 105207, 4 | 120988, 139136, 160006, 184006, 368012, 404813, 445294, 489823, 538805, 5 | 808207, 889027, 977929, 1075721, 1183293, 2366586, 2603244, 2863568, 3149924, 6 | 3464916, 5197374, 5717111, 6288822, 6917704, 7609474, 15218948, 16740842, 7 | 18414926, 20256418, 22282059, 33423088, 36765396, 40441935, 44486128, 8 | 48934740, 73402110, 132123798, 145336177, 159869794, 175856773, 193442450, 9 | 212786695, 234065364, 257471900, 283219090, 311540999, 373849198, 453852927, 10 | 550977453, 668886628, 812028367, 985802438, 1196764160, 1452871690, 11 | 1763786232, 2141236485, 4317589248, 6130976733, 8705986960, 9576585858, 12 | 10534244222, 11587668644, 12746435508, 14021079059, 15423186965, 16965505661, 13 | 18662056227, 20528261850, 22581088035, 14 | ]; 15 | -------------------------------------------------------------------------------- /config/default.yaml: -------------------------------------------------------------------------------- 1 | database: 2 | uri: mongodb://127.0.0.1:27017/knight-online 3 | options: 4 | 5 | redis: 6 | host: '127.0.0.1' 7 | port: 6379 8 | 9 | web: 10 | host: '127.0.0.1' 11 | port: 15000 12 | 13 | defaults: 14 | account: true 15 | item: true 16 | npc: true 17 | server: true 18 | set_item: true 19 | setting: true 20 | 21 | loginServer: 22 | ip: 127.0.0.1 23 | ports: 24 | - 15100 25 | - 15101 26 | - 15102 27 | - 15103 28 | - 15104 29 | - 15105 30 | - 15106 31 | - 15107 32 | - 15108 33 | - 15109 34 | ftp: 35 | host: localhost 36 | dir: / 37 | otp: 'knightonline' 38 | maxOTPTry: 5 39 | versions: 40 | - version: 2228 41 | fileName: none 42 | 43 | gameServer: 44 | ip: 127.0.0.1 45 | ports: 46 | - 15001 47 | clientExeVersion: 2228 48 | 49 | testClient: 50 | ip: '127.0.0.1' 51 | port: 15100 52 | version: 2227 53 | user: 'test' 54 | password: '1' 55 | server: 'TEST|SERVER 1' -------------------------------------------------------------------------------- /src/game_server/endpoints/NPC_DETAILED_REQUEST.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue, short } from "../../core/utils/unit.js"; 4 | import { RNPCMap } from "../region.js"; 5 | import { BuildNPCDetail } from "../functions/buildNPCDetail.js"; 6 | 7 | export const NPC_DETAILED_REQUEST: IGameEndpoint = async function ( 8 | socket: IGameSocket, 9 | body: Queue, 10 | opcode: number 11 | ) { 12 | let length = body.short(); 13 | if (length > 1000) length = 1000; 14 | 15 | let result = [opcode, 0, 0]; 16 | let count = 0; 17 | 18 | for (let i = 0; i < length; i++) { 19 | let uuid = body.short(); 20 | let npc = RNPCMap[uuid]; 21 | if (npc) { 22 | count++; 23 | result.push(...short(uuid)); 24 | result.push(...BuildNPCDetail(npc.npc)); 25 | } 26 | } 27 | 28 | result[1] = count & 0xff; 29 | result[2] = count >>> 8; 30 | 31 | socket.send(result); 32 | }; 33 | -------------------------------------------------------------------------------- /src/game_server/functions/sendExperienceChange.ts: -------------------------------------------------------------------------------- 1 | import type { IGameSocket } from "../game_socket.js"; 2 | import { GetLevelUp } from "./getLevelUp.js"; 3 | import { SendLevelChange } from "./sendLevelChange.js"; 4 | import { long } from "../../core/utils/unit.js"; 5 | 6 | export function SendExperienceChange(socket: IGameSocket, experience: number) { 7 | if (experience <= 0) return; // TODO: handle this case later 8 | 9 | let c = socket.character; 10 | 11 | // TODO: Handle premium exp percentage 12 | 13 | c.exp += experience; 14 | 15 | let maxExp = GetLevelUp(c.level); 16 | 17 | if (c.exp > maxExp) { 18 | if (c.level < 83) { 19 | c.exp -= maxExp; 20 | return SendLevelChange(socket, c.level + 1); 21 | } else { 22 | c.exp = maxExp; 23 | } 24 | } 25 | 26 | // Yeah, user might not gain any exp because of maxExp. But we will report no matter what.. 27 | socket.send([ 28 | 0x1a, // EXP_CHANGE OPCODE 29 | 0, 30 | ...long(c.exp), 31 | ]); 32 | } 33 | -------------------------------------------------------------------------------- /src/core/database/models/server.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, Document } from "mongoose"; 2 | 3 | export interface IServer extends Document { 4 | ip: string; 5 | lanip: string; 6 | name: string; 7 | karusKing: string; 8 | karusNotice: string; 9 | elmoradKing: string; 10 | elmoradNotice: string; 11 | onlineCount: number; 12 | userFreeLimit: number; 13 | userPremiumLimit: number; 14 | } 15 | 16 | export const ServerSchema = new Schema( 17 | { 18 | ip: { type: String }, 19 | lanip: { type: String }, 20 | name: { type: String }, 21 | karusKing: { type: String }, 22 | karusNotice: { type: String }, 23 | elmoradKing: { type: String }, 24 | elmoradNotice: { type: String }, 25 | 26 | onlineCount: { type: Number, default: 0 }, 27 | userFreeLimit: { type: Number, default: 3000 }, 28 | userPremiumLimit: { type: Number, default: 3000 }, 29 | }, 30 | { timestamps: false } 31 | ); 32 | 33 | export const Server = model("Server", ServerSchema, "server"); 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ko-js", 3 | "version": "2.0.0", 4 | "type": "module", 5 | "private": true, 6 | "main": "src/index.js", 7 | "scripts": { 8 | "build": "tsc", 9 | "start": "node build/app", 10 | "dev": "tsx src/app.ts", 11 | "test": "nole ./src/**/*.test.ts" 12 | }, 13 | "author": "co3moz", 14 | "license": "MIT", 15 | "dependencies": { 16 | "chai": "^5.2.0", 17 | "config": "^4.0.0", 18 | "crc-32": "^1.2.2", 19 | "csv-streamify": "^4.0.0", 20 | "extract-zip": "^2.0.1", 21 | "fastify": "^5.4.0", 22 | "glob": "^11.0.3", 23 | "js-yaml": "^4.1.0", 24 | "long": "^5.3.2", 25 | "lzfjs": "^1.0.1", 26 | "mongoose": "^8.15.2", 27 | "nole": "^2.2.2", 28 | "redis": "^5.5.6", 29 | "typescript": "^5.8.3" 30 | }, 31 | "engines": { 32 | "node": ">= 20" 33 | }, 34 | "devDependencies": { 35 | "@types/chai": "^5.2.2", 36 | "@types/config": "3.3.5", 37 | "@types/node": "^22.15.31", 38 | "tsx": "^4.20.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /docs/gameserver_72_hacktool_opcode.md: -------------------------------------------------------------------------------- 1 | ```cpp 2 | void CUser::HackTool(Packet & pkt) 3 | { 4 | // Geçici -> Veritabanına bağlanması gerekiyor. 5 | // -> CENGLYY. 6 | Packet result(WIZ_HACKTOOL, uint8(0x01)); 7 | result << uint16(16) // kaç adet sayı 8 | << result << (std::string)"cheatengine-x86_64.exe" 9 | << result << (std::string)"Cheat Engine.exe" 10 | << result << (std::string)"Cheat Engine" 11 | << result << (std::string)"Window Hide Tool.exe" 12 | << result << (std::string)"CapsLock.exe" 13 | << result << (std::string)"winrar4.exe" 14 | << result << (std::string)"winrar33.exe" 15 | << result << (std::string)"Mozilla.exe" 16 | << result << (std::string)"pedalv9.0.exe" 17 | << result << (std::string)"Smart Utility Makro.exe" 18 | << result << (std::string)"kuduzinek.exe" 19 | << result << (std::string)"synapse.exe" 20 | << result << (std::string)"PCHunter64.exe" 21 | << result << (std::string)"PCHunter32.exe" 22 | << result << (std::string)"Kuduzsinek.exe" 23 | << result << (std::string)"CapsLock_1_.exe"; 24 | Send(&result); 25 | } 26 | ``` -------------------------------------------------------------------------------- /src/game_server/functions/sendItemMove.ts: -------------------------------------------------------------------------------- 1 | import type { IGameSocket } from "../game_socket.js"; 2 | import { short, int } from "../../core/utils/unit.js"; 3 | 4 | export function SendItemMove(socket: IGameSocket, command: number) { 5 | let v = socket.variables; 6 | 7 | if (!command) { 8 | socket.send([0x1f, 1]); 9 | } else { 10 | socket.send([ 11 | 0x1f, 12 | 1, 13 | command, 14 | ...short(v.totalHit), 15 | ...short(v.totalAc), 16 | ...int(v.maxWeight), 17 | 1, 18 | ...short(v.maxHp), 19 | ...short(v.maxMp), 20 | ...short(v.statBonus[0] + v.statBuffBonus[0]), 21 | ...short(v.statBonus[1] + v.statBuffBonus[1]), 22 | ...short(v.statBonus[2] + v.statBuffBonus[2]), 23 | ...short(v.statBonus[3] + v.statBuffBonus[3]), 24 | ...short(v.statBonus[4] + v.statBuffBonus[4]), 25 | ...short(v.fireR), 26 | ...short(v.coldR), 27 | ...short(v.lightningR), 28 | ...short(v.magicR), 29 | ...short(v.curseR), 30 | ...short(v.poisonR), 31 | ]); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/game_server/events/onUserExit.ts: -------------------------------------------------------------------------------- 1 | import type { IGameSocket } from "../game_socket.js"; 2 | import { RSessionMap } from "../region.js"; 3 | import { SendRegionUserOut } from "../functions/sendRegionInOut.js"; 4 | 5 | export function OnUserExit(socket: IGameSocket) { 6 | let leaveSocket = socket.session; 7 | let visiblePlayers = socket.visiblePlayers; 8 | 9 | if (visiblePlayers) { 10 | for (let session in visiblePlayers) { 11 | let userSocket = RSessionMap[session]; 12 | 13 | if (userSocket) { 14 | SendRegionUserOut(userSocket, leaveSocket, true); 15 | } 16 | 17 | delete visiblePlayers[session]; 18 | } 19 | } 20 | 21 | if (socket.variables && socket.variables.expiryBlink) { 22 | clearTimeout(socket.variables.expiryBlink); 23 | } 24 | 25 | let actions: Promise[] = []; 26 | 27 | if (socket.user) actions.push(socket.user.save()); 28 | if (socket.character) actions.push(socket.character.save()); 29 | if (socket.warehouse) actions.push(socket.warehouse.save()); 30 | 31 | return Promise.all(actions); 32 | } 33 | -------------------------------------------------------------------------------- /src/game_server/endpoints/CHANGE_HAIR.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue } from "../../core/utils/unit.js"; 4 | import { Character } from "../../core/database/models/index.js"; 5 | 6 | export const CHANGE_HAIR: IGameEndpoint = async function ( 7 | socket: IGameSocket, 8 | body: Queue, 9 | opcode: number 10 | ) { 11 | body.skip(1); // no idea why 12 | let charName = body.byte_string(); 13 | let face = body.byte(); 14 | let hair = body.uint(); 15 | 16 | if (!socket.user || !socket.user.characters.find((x) => x == charName)) { 17 | return socket.send([opcode, 0]); 18 | } 19 | 20 | var result = 1; 21 | try { 22 | let character = await Character.findOne({ name: charName }).exec(); 23 | character.hair = hair; 24 | character.face = face; 25 | await character.save(); 26 | } catch (e) { 27 | console.error("error ocurred on change hair"); 28 | console.error(e.stack); 29 | result = 0; 30 | } 31 | 32 | socket.send([opcode, result]); 33 | }; 34 | -------------------------------------------------------------------------------- /src/core/database/index.ts: -------------------------------------------------------------------------------- 1 | import config from "config"; 2 | import mongoose from "mongoose"; 3 | 4 | import { ConnectToDatabase } from "./utils/connect.js"; 5 | import { 6 | SettingDefaults, 7 | ServerDefaults, 8 | SetItemDefaults, 9 | ItemDefaults, 10 | NpcDefaults, 11 | AccountDefaults, 12 | } from "./defaults/index.js"; 13 | 14 | let connection: mongoose.Connection = null; 15 | 16 | export async function Database(): Promise { 17 | if (connection) { 18 | return connection; 19 | } 20 | 21 | connection = await ConnectToDatabase(); 22 | 23 | let defaults: any = config.get("defaults"); 24 | 25 | if (defaults.setting) await SettingDefaults(); 26 | if (defaults.server) await ServerDefaults(); 27 | if (defaults.set_item) await SetItemDefaults(); 28 | if (defaults.item) await ItemDefaults(); 29 | if (defaults.npc) await NpcDefaults(); 30 | if (defaults.account) await AccountDefaults(); 31 | 32 | return connection; 33 | } 34 | 35 | export function DisconnectFromDatabase(): Promise { 36 | return mongoose.disconnect(); 37 | } 38 | -------------------------------------------------------------------------------- /src/game_server/endpoints/POINT_CHANGE.ts: -------------------------------------------------------------------------------- 1 | import { Queue, short } from "../../core/utils/unit.js"; 2 | import type { IGameEndpoint } from "../endpoint.js"; 3 | import type { IGameSocket } from "../game_socket.js"; 4 | import { SendAbility } from "../functions/sendAbility.js"; 5 | 6 | export const POINT_CHANGE: IGameEndpoint = async function ( 7 | socket: IGameSocket, 8 | body: Queue, 9 | opcode: number 10 | ) { 11 | let type = body.byte(); 12 | 13 | let stat = StatType[type - 1]; 14 | 15 | let c = socket.character; 16 | let v = socket.variables; 17 | if (!stat || !c.statRemaining || c[stat] == 255) return; 18 | 19 | c[stat] += 1; 20 | c.statRemaining--; 21 | c.markModified(stat); 22 | 23 | SendAbility(socket); 24 | 25 | socket.send([ 26 | opcode, 27 | type, 28 | ...short(c[stat]), 29 | ...short(v.maxHp), 30 | ...short(v.maxMp), 31 | ...short(v.totalHit), 32 | ...short(v.maxWeight), 33 | ]); 34 | }; 35 | 36 | enum StatType { 37 | statStr = 0, 38 | statHp = 1, 39 | statDex = 2, 40 | statMp = 3, 41 | statInt = 4, 42 | } 43 | -------------------------------------------------------------------------------- /src/core/database/defaults/setting.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from "../models/index.js"; 2 | 3 | export async function SettingDefaults() { 4 | let defaultSettings = { 5 | test: ["bool", true, "DELETED"], 6 | }; 7 | 8 | for (let [key, value] of Object.entries(defaultSettings)) { 9 | let data; 10 | 11 | if ( 12 | !(data = await Setting.findOne({ 13 | key, 14 | }).exec()) 15 | ) { 16 | if (value[2] == "DELETED") continue; 17 | 18 | let setting = new Setting({ 19 | key, 20 | value: value[1], 21 | type: value[0], 22 | }); 23 | 24 | await setting.save(); 25 | 26 | console.log( 27 | "[DB] A setting has been defined! | (%s) %s =", 28 | value[0], 29 | key, 30 | value[1] 31 | ); 32 | } else { 33 | if (value[2] == "DELETED" && data) { 34 | await data.remove(); 35 | 36 | console.log( 37 | "[DB] A setting has been removed! | (%s) %s = NULL", 38 | value[0], 39 | key 40 | ); 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/core/database/models/mail.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, Document } from "mongoose"; 2 | 3 | export interface IMail extends Document { 4 | character: string; 5 | marker: number; 6 | sender: string; 7 | subject: string; 8 | message: string; 9 | type: number; 10 | item: number; 11 | count: number; 12 | durability: number; 13 | serial: string; 14 | money: number; 15 | deleted: boolean; 16 | status: number; 17 | 18 | createdAt: Date; 19 | updatedAt: Date; 20 | } 21 | 22 | export const MailSchema = new Schema( 23 | { 24 | character: { type: String, index: true }, 25 | marker: { type: Number }, 26 | sender: { type: String }, 27 | subject: { type: String }, 28 | message: { type: String }, 29 | type: { type: Number }, 30 | item: { type: Number }, 31 | count: { type: Number }, 32 | durability: { type: Number }, 33 | serial: { type: String }, 34 | money: { type: Number }, 35 | status: { type: Number }, 36 | deleted: { type: Boolean }, 37 | }, 38 | { timestamps: true } 39 | ); 40 | 41 | export const Mail = model("Mail", MailSchema, "mails"); 42 | -------------------------------------------------------------------------------- /src/game_server/functions/findSlotForItem.ts: -------------------------------------------------------------------------------- 1 | import type { IGameSocket } from "../game_socket.js"; 2 | import type { IItem } from "../../core/database/models/index.js"; 3 | import { ItemSlot } from "../var/item_slot.js"; 4 | 5 | const INVENTORY_START = ItemSlot.INVENTORY_START; 6 | const INVENTORY_END = ItemSlot.INVENTORY_END; 7 | 8 | export function FindSlotForItem( 9 | socket: IGameSocket, 10 | itemDetail: IItem, 11 | count = 1 12 | ) { 13 | if (!itemDetail) return -1; 14 | 15 | let c = socket.character; 16 | let items = c.items; 17 | if (itemDetail.countable) { 18 | // can item stack together like pots, buses, or quest stuff.. 19 | let emptySlot = -1; 20 | 21 | for (let i = INVENTORY_START; i < INVENTORY_END; i++) { 22 | let at = items[i]; 23 | if (!at && emptySlot < 0) emptySlot = i; 24 | else if (at && at.id == itemDetail.id && at.amount + count <= 9999) 25 | return i; 26 | } 27 | 28 | return emptySlot; 29 | } else { 30 | for (let i = INVENTORY_START; i < INVENTORY_END; i++) { 31 | if (!items[i]) return i; 32 | } 33 | 34 | return -1; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/game_server/functions/sendStateChange.ts: -------------------------------------------------------------------------------- 1 | import type { IGameSocket } from "../game_socket.js"; 2 | import { RegionSend } from "../region.js"; 3 | import { short, int } from "../../core/utils/unit.js"; 4 | 5 | export function SendStateChange( 6 | socket: IGameSocket, 7 | type: number, 8 | value: number 9 | ): void { 10 | let c = socket.character; 11 | let v = socket.variables; 12 | 13 | if (type == 1) { 14 | v.hptype = value; 15 | } else if (type == 3) { 16 | // v.old_abnormalType = v.abnormalType; 17 | 18 | // if (c.gm) { 19 | // sendStateChange(socket, 5, 1); 20 | // } 21 | 22 | v.abnormalType = value; 23 | } else if (type == 5) { 24 | v.abnormalType = value; 25 | } 26 | 27 | RegionSend(socket, [ 28 | 0x29, // STATE_CHANGE 29 | ...short(socket.session), 30 | type, 31 | ...int(value), 32 | ]); 33 | } 34 | 35 | export enum UserStates { 36 | STANDING = 1, 37 | SITDOWN = 2, 38 | DEAD = 3, 39 | BLINKING = 4, 40 | } 41 | 42 | export enum AbnormalStates { 43 | INVISIBLE = 0, 44 | NORMAL = 1, 45 | GIANT = 2, 46 | DWARF = 3, 47 | BLINKING = 4, 48 | GIANT_TARGET = 6, 49 | } 50 | -------------------------------------------------------------------------------- /src/client/launcher-client.ts: -------------------------------------------------------------------------------- 1 | import { KOClientFactory, type IKOClientSocket } from "../core/client.js"; 2 | import { short, Queue } from "../core/utils/unit.js"; 3 | 4 | export async function ConnectKOServerForLauncher( 5 | ip: string, 6 | port: number, 7 | version: number = 1299 8 | ) { 9 | let connection: IKOClientSocket; 10 | let data: Queue; 11 | 12 | try { 13 | connection = await KOClientFactory({ ip, port, name: "launcher-client" }); 14 | 15 | data = await connection.sendAndWait([0x01, ...short(version)], 0x01); 16 | 17 | let latestVersion = data.short(); 18 | 19 | data = await connection.sendAndWait([0x02, ...short(version)], 0x02); 20 | 21 | let ftpAddress = data.string(); 22 | let ftpRoot = data.string(); 23 | let totalFiles = data.short(); 24 | let files: string[] = []; 25 | 26 | for (var i = 0; i < totalFiles; i++) { 27 | files.push(data.string()); 28 | } 29 | 30 | return { 31 | latestVersion, 32 | ftpAddress, 33 | ftpRoot, 34 | ftpFiles: files, 35 | }; 36 | } finally { 37 | if (connection) { 38 | connection.terminate(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/core/utils/average_time.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Average time calculator 3 | */ 4 | export class AverageTime { 5 | private array: number[]; 6 | private size: number; 7 | private total: number; 8 | 9 | private constructor(size) { 10 | this.array = []; 11 | this.size = size; 12 | this.total = 0; 13 | } 14 | 15 | /** 16 | * Creates an instance for calculations 17 | * @param size How much data will be stored 18 | */ 19 | public static instance(size: number) { 20 | return new AverageTime(size); 21 | } 22 | 23 | /** 24 | * Pushes new data to instance 25 | * @param time Time information in milliseconds 26 | */ 27 | public push(time: number) { 28 | if (this.array.length >= this.size) { 29 | let item = this.array.pop(); 30 | this.total -= item; 31 | } 32 | 33 | this.array.unshift(time); 34 | this.total += time; 35 | } 36 | 37 | /** 38 | * Calculates average time. 39 | */ 40 | public avg() { 41 | return (((this.total / this.array.length) * 100) | 0) / 100; 42 | } 43 | 44 | /** 45 | * Fetches the stack 46 | */ 47 | public values() { 48 | return this.array; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/game_server/endpoints/GENIE.ts: -------------------------------------------------------------------------------- 1 | import { Queue, int, short } from "../../core/utils/unit.js"; 2 | import type { IGameEndpoint } from "../endpoint.js"; 3 | import type { IGameSocket } from "../game_socket.js"; 4 | 5 | export const GENIE: IGameEndpoint = async function ( 6 | socket: IGameSocket, 7 | body: Queue, 8 | opcode: number 9 | ) { 10 | let subOpcode = body.byte(); 11 | 12 | if (subOpcode == 1) { 13 | subOpcode = body.byte(); 14 | 15 | let c = socket.character; 16 | if (subOpcode == 1) { 17 | // spiringPotion 18 | } else if (subOpcode == 2) { 19 | // load options 20 | let g = c.genieSettings || []; 21 | 22 | let result = [opcode, 1, 2]; 23 | for (let i = 0; i < 100; i++) { 24 | result.push(g[i] || 0); 25 | } 26 | 27 | socket.send(result); 28 | } else if (subOpcode == 3) { 29 | // save options 30 | c.genieSettings = body.sub(100); 31 | c.markModified("genieSettings"); 32 | } else if (subOpcode == 4) { 33 | // start 34 | } else if (subOpcode == 5) { 35 | // stop 36 | } 37 | } else if (subOpcode == 2) { 38 | // TODO: HANDLE ME 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/game_server/functions/buildNPCDetail.ts: -------------------------------------------------------------------------------- 1 | import { short, int } from "../../core/utils/unit.js"; 2 | import type { INPCInstance } from "../ai_system/declare.js"; 3 | 4 | export function BuildNPCDetail(npc: INPCInstance) { 5 | // if (npc.cache) { 6 | 7 | // return npc.cache; 8 | // } 9 | 10 | let model = npc.npc; 11 | const result: number[] = []; 12 | 13 | result.push(...short(model.id)); 14 | result.push(model.isMonster ? 1 : 2); 15 | result.push(...short(model.pid)); 16 | result.push(...int(model.sellingGroup)); 17 | result.push(model.type || 0); 18 | result.push(0, 0, 0, 0); 19 | result.push(...short(model.size)); 20 | result.push(...int(model.weapon1)); 21 | result.push(...int(model.weapon2)); 22 | result.push((model.isMonster ? 0 : model.group) || 0); 23 | result.push(model.level || 1); 24 | result.push(...short(npc.x * 10)); 25 | result.push(...short(npc.z * 10)); 26 | result.push(0, 0); 27 | result.push(0, 0, 0, 0); // FIXME: isGameOpen thing 28 | result.push(0); // FIXME: special npc 29 | result.push(0, 0, 0, 0); 30 | result.push(...short(npc.direction)); 31 | 32 | // npc.cache = result; 33 | return result; 34 | } 35 | -------------------------------------------------------------------------------- /src/game_server/endpoints/SEL_NATION.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue } from "../../core/utils/unit.js"; 4 | import { Warehouse } from "../../core/database/models/index.js"; 5 | 6 | export const SEL_NATION: IGameEndpoint = async function ( 7 | socket: IGameSocket, 8 | body: Queue, 9 | opcode: number 10 | ) { 11 | let nation = body.byte(); 12 | 13 | if (!(nation == 1 || nation == 2)) { 14 | // invalid 15 | return socket.send([opcode, 0]); 16 | } 17 | 18 | var result = 1; 19 | try { 20 | if (!socket.user.warehouse) { 21 | let warehouse = new Warehouse({ money: 0 }); 22 | await warehouse.save(); 23 | socket.user.warehouse = warehouse._id.toString(); 24 | } 25 | 26 | if (socket.user.characters.length > 0) { 27 | throw 1; // cant change your nation, if you have a character 28 | } 29 | 30 | socket.user.nation = nation; 31 | await socket.user.save(); 32 | 33 | result = nation; 34 | } catch (e) { 35 | // if anything goes wrong 36 | result = 0; 37 | } 38 | 39 | socket.send([opcode, result]); 40 | }; 41 | -------------------------------------------------------------------------------- /src/game_server/functions/sendBlink.ts: -------------------------------------------------------------------------------- 1 | import type { IGameSocket } from "../game_socket.js"; 2 | import { ZoneCode } from "../var/zone_codes.js"; 3 | import { ZoneRules, ZoneFlags } from "../var/zone_rules.js"; 4 | import { SendStateChange, AbnormalStates } from "./sendStateChange.js"; 5 | 6 | export function SendBlinkStart(socket: IGameSocket) { 7 | let v = socket.variables; 8 | let c = socket.character; 9 | 10 | let zone = c.zone; 11 | 12 | if ( 13 | zone == ZoneCode.ZONE_ARDREAM || 14 | zone == ZoneCode.ZONE_NEW_RONARK_EVENT || 15 | zone == ZoneCode.ZONE_RONARK_LAND || 16 | zone == ZoneCode.ZONE_UNDER_THE_CASTLE || 17 | zone == ZoneCode.ZONE_JURAD_MOUNTAIN || 18 | zone == ZoneCode.ZONE_CHAOS_DUNGEON || 19 | zone == ZoneCode.ZONE_BORDER_DEFENSE_WAR 20 | ) { 21 | return; 22 | } 23 | 24 | if (ZoneRules[zone].flags & ZoneFlags.WAR_ZONE) return; 25 | 26 | if (v.expiryBlink) { 27 | clearTimeout(v.expiryBlink); 28 | } 29 | 30 | v.expiryBlink = setTimeout(() => { 31 | SendStateChange(socket, 3, AbnormalStates.NORMAL); 32 | 33 | delete v.expiryBlink; 34 | }, 10000); 35 | 36 | SendStateChange(socket, 3, AbnormalStates.BLINKING); 37 | } 38 | -------------------------------------------------------------------------------- /src/core/utils/deferred_promise.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates Deferred Promise 3 | */ 4 | export function CreateDeferredPromise(timeout?: number): IDeferredPromise { 5 | let resolve: (value: any) => void; 6 | let reject: (reason?: any) => void; 7 | 8 | let promise: IDeferredPromise = new Promise((a, b) => { 9 | resolve = a; 10 | reject = b; 11 | }); 12 | 13 | if (timeout) { 14 | let idx = setTimeout(TimeoutRejection, timeout, reject); 15 | 16 | promise.resolve = function () { 17 | if (idx) { 18 | clearTimeout(idx); 19 | idx = null; 20 | } 21 | resolve.apply(null, arguments); 22 | }; 23 | 24 | promise.reject = function () { 25 | if (idx) { 26 | clearTimeout(idx); 27 | idx = null; 28 | } 29 | reject.apply(null, arguments); 30 | }; 31 | } else { 32 | promise.resolve = resolve; 33 | promise.reject = reject; 34 | } 35 | 36 | return promise; 37 | } 38 | 39 | export interface IDeferredPromise extends Promise { 40 | resolve?: (data?: any) => void; 41 | reject?: (error: Error) => void; 42 | } 43 | 44 | function TimeoutRejection(reject) { 45 | reject(new Error("Timeout occurred.")); 46 | } 47 | -------------------------------------------------------------------------------- /src/game_server/endpoints/TARGET_HP.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue } from "../../core/utils/unit.js"; 4 | import { SendMessageToPlayer } from "../functions/sendChatMessage.js"; 5 | import { RNPCMap } from "../region.js"; 6 | import { SendTargetHP } from "../functions/sendTargetHP.js"; 7 | 8 | export const TARGET_HP: IGameEndpoint = async function ( 9 | socket: IGameSocket, 10 | body: Queue, 11 | opcode: number 12 | ) { 13 | let target = body.short(); 14 | let echo = body.byte(); 15 | 16 | socket.target = target; 17 | 18 | if (target >= 10000) { 19 | socket.targetType = "npc"; 20 | 21 | if (socket.character.gm) { 22 | let regionNpc = RNPCMap[socket.target]; 23 | if (regionNpc) { 24 | SendMessageToPlayer( 25 | socket, 26 | 1, 27 | "[TARGET]", 28 | `id: ${regionNpc.npc.uuid}, name: ${regionNpc.npc.npc.name}, hp: ${regionNpc.npc.hp}`, 29 | undefined, 30 | -1 31 | ); 32 | } 33 | } 34 | } else { 35 | socket.targetType = "user"; 36 | } 37 | 38 | SendTargetHP(socket, echo, 0); 39 | }; 40 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | ko-js: 4 | build: . 5 | hostname: "kojs_sandbox" 6 | ports: 7 | - "15100-15109:15100-15109" 8 | - "15001:15001" 9 | networks: 10 | outaccess: 11 | application: 12 | ipv4_address: 10.0.0.50 13 | restart: always 14 | mongodb: 15 | image: mongo:latest 16 | environment: 17 | - MONGO_DATA_DIR=/data/db 18 | - MONGO_LOG_DIR=/dev/null 19 | volumes: 20 | - /container/ko-js/mongo:/data/db 21 | networks: 22 | application: 23 | ipv4_address: 10.0.0.51 24 | command: mongod --smallfiles --logpath=/dev/null # --quiet 25 | redis: 26 | image: "redis:alpine" 27 | networks: 28 | application: 29 | ipv4_address: 10.0.0.52 30 | adminmongo: 31 | image: mrvautin/adminmongo 32 | environment: 33 | HOST: '0.0.0.0' 34 | CONN_NAME: '10.0.0.51' 35 | DB_HOST: 'knight-online' 36 | ports: 37 | - '1234:1234' 38 | networks: 39 | outaccess: 40 | application: 41 | ipv4_address: 10.0.0.53 42 | networks: 43 | outaccess: 44 | external: 45 | name: outaccess 46 | application: 47 | external: 48 | name: application -------------------------------------------------------------------------------- /src/core/utils/unique_queue.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Stores number stack for uuids, sessions etc.. 3 | */ 4 | export class UniqueQueue { 5 | private array: number[]; 6 | private size: number; 7 | 8 | private constructor(size: number, min: number) { 9 | let data = Array(size); 10 | let i = size; 11 | 12 | while (i--) data[i] = size - i + min; 13 | this.array = data; 14 | this.size = size; 15 | } 16 | 17 | /** 18 | * Creates stack 19 | * @param size how much item hold 20 | * @param min numbers starts with 21 | */ 22 | public static from(size: number, min: number = 0) { 23 | return new UniqueQueue(size, min); 24 | } 25 | 26 | /** 27 | * Puts the number back to stack 28 | * @param i number 29 | */ 30 | free(i: number) { 31 | return this.array.push(i); 32 | } 33 | 34 | /** 35 | * Gets new number from stack. It might be undefined 36 | */ 37 | reserve() { 38 | return this.array.pop(); 39 | } 40 | 41 | /** 42 | * How much number is available. 43 | */ 44 | freeSize() { 45 | return this.array.length; 46 | } 47 | 48 | /** 49 | * How much number is used. 50 | */ 51 | reservedSize() { 52 | return this.size - this.freeSize(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/game_server/endpoints/SKILLDATA.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue, short, int } from "../../core/utils/unit.js"; 4 | 5 | export const SKILLDATA: IGameEndpoint = async function ( 6 | socket: IGameSocket, 7 | body: Queue, 8 | opcode: number 9 | ) { 10 | let subOpcode = body.byte(); 11 | let c = socket.character; 12 | 13 | if (subOpcode == 2) { 14 | // load 15 | if (c.skillBar.length == 0) { 16 | return socket.send([ 17 | opcode, 18 | 2, // load 19 | 0, 20 | 0, // 0 21 | ]); 22 | } 23 | 24 | return socket.send([ 25 | opcode, 26 | 2, // load 27 | ...short(c.skillBar.length), 28 | ...[].concat(...c.skillBar.map((x) => int(x))), 29 | ]); 30 | } else if (subOpcode == 1) { 31 | // save 32 | let count = body.short(); 33 | if (count < 0 || count > 64) { 34 | return; 35 | } 36 | 37 | let skills = []; 38 | 39 | for (let i = 0; i < count; i++) { 40 | skills.push(body.int()); 41 | } 42 | 43 | c.skillBar = skills; 44 | c.markModified("skillBar"); 45 | 46 | // no need to response 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /src/game_server/endpoints/CHAT_TARGET.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue, string } from "../../core/utils/unit.js"; 4 | import { GM_COMMANDS_HEADER } from "../functions/GMController.js"; 5 | import { RUserMap } from "../region.js"; 6 | 7 | export const CHAT_TARGET: IGameEndpoint = async function ( 8 | socket: IGameSocket, 9 | body: Queue, 10 | opcode: number 11 | ) { 12 | let type = body.byte(); 13 | 14 | if (type == 1) { 15 | let user = body.string(); 16 | if (user == GM_COMMANDS_HEADER && socket.character.gm) { 17 | // allow gm chat with server 18 | socket.variables.chatTo = user; 19 | return socket.send([opcode, type, 1, 0, ...string(user)]); 20 | } 21 | 22 | let userSocket = RUserMap[user]; 23 | if (!userSocket) { 24 | socket.variables.chatTo = null; 25 | return socket.send([opcode, type, 0, 0]); 26 | } 27 | 28 | // TODO: check blocking 29 | 30 | socket.variables.chatTo = user; 31 | 32 | return socket.send([opcode, type, 1, 0, ...string(user)]); 33 | } 34 | 35 | console.error( 36 | "CHAT_TARGET type#" + type + " data#" + body.array().join(", ") 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/game_server/functions/sendNotices.ts: -------------------------------------------------------------------------------- 1 | import type { IGameSocket } from "../game_socket.js"; 2 | import { string, byte_string, short } from "../../core/utils/unit.js"; 3 | 4 | const notices = [["KO-JS", "Welcome to Knight Online Javascript Server"]]; 5 | 6 | export function SendNotices(socket: IGameSocket) { 7 | let u = socket.user; 8 | let c = socket.character; 9 | 10 | let nation = u.nation; 11 | 12 | socket.send([ 13 | 0x2e, 14 | 2, 15 | notices.length, 16 | ...[].concat( 17 | ...notices.map((notice) => [...string(notice[0]), ...string(notice[1])]) 18 | ), 19 | ]); 20 | 21 | socket.send([ 22 | 0x2e, 23 | 1, 24 | notices.length, 25 | ...[].concat( 26 | ...notices.map((notice) => [...byte_string(notice[1] + " " + notice[1])]) 27 | ), 28 | ]); 29 | 30 | socket.send([ 31 | 0x10, // CHAT 32 | 5, 33 | nation, 34 | ...short(socket.session & 0xffff), 35 | 0, 36 | ...string(`[SERVER] Server Time: ${new Date().toLocaleString("en-GB")}`), 37 | ]); 38 | 39 | socket.send([ 40 | 0x10, // CHAT 41 | 5, 42 | nation, 43 | ...short(socket.session & 0xffff), 44 | 0, 45 | ...string(`[SERVER] Welcome ${c.name}, ko-js is really working :)`), 46 | ]); 47 | } 48 | -------------------------------------------------------------------------------- /src/core/redis/cache.ts: -------------------------------------------------------------------------------- 1 | import { redisClient } from "./index.js"; 2 | 3 | export async function RedisCaching( 4 | name: string, 5 | action: () => Promise, 6 | cacheTime: number = 60 7 | ) { 8 | let data = await redisClient.get("cache-" + name); 9 | 10 | if (data) { 11 | return JSON.parse(data as string); 12 | } 13 | 14 | if (await waitLock(name)) { 15 | let data = await redisClient.get("cache-" + name); 16 | 17 | if (data) { 18 | return JSON.parse(data as string); 19 | } 20 | } 21 | 22 | lock(name); 23 | 24 | try { 25 | data = await action(); 26 | await redisClient.setEx("cache-" + name, cacheTime, JSON.stringify(data)); 27 | } finally { 28 | unlock(name); 29 | } 30 | 31 | return data; 32 | } 33 | 34 | const _locks = {}; 35 | const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); 36 | async function waitLock(name, timeout = 120) { 37 | if (!_locks[name]) { 38 | return false; 39 | } 40 | for (;;) { 41 | await delay(250); 42 | if (!_locks[name]) { 43 | return true; 44 | } 45 | 46 | if (timeout-- <= 0) { 47 | return false; 48 | } 49 | } 50 | } 51 | 52 | function lock(name) { 53 | _locks[name] = true; 54 | } 55 | 56 | function unlock(name) { 57 | _locks[name] = false; 58 | } 59 | -------------------------------------------------------------------------------- /src/game_server/endpoints/MOVE.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue, short } from "../../core/utils/unit.js"; 4 | import { RegionUpdate, RegionSend } from "../region.js"; 5 | 6 | export const MOVE: IGameEndpoint = async function ( 7 | socket: IGameSocket, 8 | body: Queue, 9 | opcode: number 10 | ) { 11 | let c = socket.character; 12 | 13 | let willX = body.short(); 14 | let willZ = body.short(); 15 | let willY = body.short(); 16 | let speed = body.short(); 17 | let echo = body.byte(); 18 | let newX = body.short(); 19 | let newZ = body.short(); 20 | let newY = body.short(); 21 | 22 | let realX = newX / 10; 23 | let realZ = newZ / 10; 24 | let realY = newY / 10; 25 | 26 | let rwillX = willX / 10; 27 | let rwillZ = willZ / 10; 28 | let rwillY = willY / 10; 29 | 30 | c.x = realX; 31 | c.z = realZ; 32 | c.y = realY; 33 | 34 | // TODO: do this right way :) 35 | 36 | RegionUpdate(socket); 37 | 38 | RegionSend(socket, [ 39 | opcode, 40 | ...short(socket.session), 41 | ...short(willX), 42 | ...short(willZ), 43 | ...short(willY), 44 | ...short(speed), 45 | echo, 46 | ...short(newX), 47 | ...short(newZ), 48 | ...short(newY), 49 | ]); 50 | }; 51 | -------------------------------------------------------------------------------- /src/login_server/endpoints/SERVERLIST.ts: -------------------------------------------------------------------------------- 1 | import { Queue, string, short } from "../../core/utils/unit.js"; 2 | import type { ILoginSocket } from "../login_socket.js"; 3 | import { Server } from "../../core/database/models/index.js"; 4 | import { RedisCaching } from "../../core/redis/cache.js"; 5 | import type { ILoginEndpoint } from "../endpoint.js"; 6 | 7 | export const SERVERLIST: ILoginEndpoint = async function ( 8 | socket: ILoginSocket, 9 | body: Queue, 10 | opcode: number 11 | ) { 12 | let echo = body.short(); 13 | 14 | let servers = await RedisCaching("servers", ServersCache); 15 | 16 | socket.send([opcode, ...short(echo), ...servers]); 17 | }; 18 | 19 | async function ServersCache() { 20 | let servers = await Server.find().lean().exec(); 21 | 22 | return [servers.length].concat( 23 | ...servers.map((server) => [ 24 | ...string(server.ip), 25 | ...string(server.lanip), 26 | ...string(server.name), 27 | ...short(server.onlineCount), 28 | ...short(1 /** server id */), 29 | ...short(1 /** group id */), 30 | ...short(server.userPremiumLimit), 31 | ...short(server.userFreeLimit), 32 | 0, 33 | ...string(server.karusKing), 34 | ...string(server.karusNotice), 35 | ...string(server.elmoradKing), 36 | ...string(server.elmoradNotice), 37 | ]) 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/game_server/endpoints/USER_INFO.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue, byte_string, short } from "../../core/utils/unit.js"; 4 | import { RegionZoneQuery } from "../region.js"; 5 | 6 | export const USER_INFO: IGameEndpoint = async function ( 7 | socket: IGameSocket, 8 | body: Queue, 9 | opcode: number 10 | ) { 11 | let c = socket.character; 12 | let subOpcode = body.byte(); 13 | 14 | if (subOpcode == 0x01) { 15 | // send all user data in same zone 16 | let result = [opcode, subOpcode, 1, c.zone, 0, 0, 0]; // last 0, 0 is count 17 | let userCount = 0; 18 | for (let userSocket of RegionZoneQuery(socket)) { 19 | // request all users in the zone 20 | userCount++; 21 | 22 | let uc = userSocket.character; 23 | result.push(...byte_string(uc.name)); 24 | result.push(userSocket.user.nation); 25 | result.push(1, 0); 26 | result.push(...short(uc.x * 10)); 27 | result.push(...short(uc.z * 10)); 28 | result.push(0, 0, 0, 0, 0, 0); // TODO: clan info 29 | result.push(4, 0); 30 | } 31 | 32 | result[5] = userCount % 0xff >>> 0; 33 | result[6] = userCount >>> 8; 34 | 35 | socket.send(result); 36 | } else { 37 | console.error("HANDLE THIS REQUEST USER_INFO " + subOpcode); 38 | // TODO: do this 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/game_server/functions/sendTargetHP.ts: -------------------------------------------------------------------------------- 1 | import type { IGameSocket } from "../game_socket.js"; 2 | import { RSessionMap, RNPCMap } from "../region.js"; 3 | import { short, int } from "../../core/utils/unit.js"; 4 | 5 | export function SendTargetHP( 6 | socket: IGameSocket, 7 | echo: number, 8 | damage: number 9 | ): void { 10 | if (socket.targetType == "user") { 11 | let targetSocket = RSessionMap[socket.target]; 12 | 13 | if (targetSocket && targetSocket.variables) { 14 | let c = targetSocket.character; 15 | let v = targetSocket.variables; 16 | 17 | socket.send([ 18 | 0x22, // TARGET_HP 19 | ...short(socket.target), 20 | echo, 21 | ...int(v.maxHp || 0), 22 | ...int(c.hp), 23 | ...short(damage || 0), 24 | ]); 25 | } else { 26 | socket.target = 0; 27 | socket.targetType = "notarget"; 28 | } 29 | } else if (socket.targetType == "npc") { 30 | let regionNPC = RNPCMap[socket.target]; 31 | 32 | if (regionNPC) { 33 | let npc = regionNPC.npc; 34 | 35 | socket.send([ 36 | 0x22, // TARGET_HP 37 | ...short(socket.target), 38 | echo, 39 | ...int(npc.maxHp || 0), 40 | ...int(npc.hp), 41 | ...short(damage || 0), 42 | ]); 43 | } else { 44 | socket.target = 0; 45 | socket.targetType = "notarget"; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/game_server/var/item_slot.ts: -------------------------------------------------------------------------------- 1 | export enum ItemSlot { 2 | RIGHTEAR = 0, // 0= sol küpe 3 | HEAD = 1, // 1= kafalık 4 | LEFTEAR = 2, // 2= sağ küpe 5 | NECK = 3, // 3= kolye 6 | BREAST = 4, // 4= göğüslük 7 | SHOULDER = 5, // 5= omuzluk 8 | RIGHTHAND = 6, // 6= sol el 9 | WAIST = 7, // 7= kemer 10 | LEFTHAND = 8, // 8= sağ kol 11 | RIGHTRING = 9, // 9= sol yüzük 12 | LEG = 10, // 10= pantolon 13 | LEFTRING = 11, // 11= sağ yüzük 14 | GLOVE = 12, // 12= kolluklar 15 | FOOT = 13, // 13= ayaklıklar 16 | RESERVED = 14, // 14= çöp kutusu 17 | 18 | PET = 20, 19 | CWING = 42, 20 | CHELMET = 43, 21 | CLEFT = 44, 22 | CRIGHT = 45, 23 | CTOP = 46, 24 | BAG1 = 47, 25 | BAG2 = 48, 26 | FAIRY = 49, 27 | 28 | ItemSlot1HEitherHand = 0, 29 | ItemSlot1HRightHand = 1, 30 | ItemSlot1HLeftHand = 2, 31 | ItemSlot2HRightHand = 3, 32 | ItemSlot2HLeftHand = 4, 33 | ItemSlotPauldron = 5, 34 | ItemSlotPads = 6, 35 | ItemSlotHelmet = 7, 36 | ItemSlotGloves = 8, 37 | ItemSlotBoots = 9, 38 | ItemSlotEarring = 10, 39 | ItemSlotNecklace = 11, 40 | ItemSlotRing = 12, 41 | ItemSlotShoulder = 13, 42 | ItemSlotBelt = 14, 43 | ItemSlotBag = 25, 44 | ItemSlotCospreGloves = 100, 45 | ItemSlotCosprePauldron = 105, 46 | ItemSlotCospreHelmet = 107, 47 | ItemSlotCospreWings = 110, 48 | ItemSlotCospreFairy = 111, 49 | 50 | INVENTORY_START = 14, 51 | INVENTORY_END = 14 + 28, 52 | } 53 | -------------------------------------------------------------------------------- /src/game_server/functions/sendNoahEvent.ts: -------------------------------------------------------------------------------- 1 | import type { IGameSocket } from "../game_socket.js"; 2 | import { RegionSend } from "../region.js"; 3 | import { short } from "../../core/utils/unit.js"; 4 | import { SendNoahChange, GoldGainType } from "./sendNoahChange.js"; 5 | 6 | export function SendNoahEvent(socket: IGameSocket, noah: number) { 7 | if (noah <= 0) return false; // this is event, not the punishment 8 | 9 | let multiplier = NoahEventChance(); 10 | 11 | if (!multiplier) return false; 12 | 13 | RegionSend(socket, [ 14 | 0x4a, // GOLD_CHANGE 15 | GoldGainType.Event, // Event 16 | ...short(740), 17 | 0, 18 | 0, 19 | 0, 20 | 0, 21 | 0, 22 | 0, 23 | ...short(multiplier), 24 | ...short(socket.session), 25 | ]); 26 | 27 | SendNoahChange(socket, multiplier * noah); 28 | 29 | return true; 30 | } 31 | 32 | /** 33 | * Calculates chance of noah event. 34 | * 35 | * 0 0.2 => 1000 36 | * 0.2 0.6 => 500 37 | * 0.6 1.4 => 100 38 | * 1.4 3.0 => 50 39 | * 3.0 6.2 => 10 40 | * 6.2 12.6 => 2 41 | * 12.6 100.0 => 0 42 | */ 43 | export function NoahEventChance() { 44 | let chance = Math.random() * 100; 45 | 46 | if (chance > 12.6) return 0; // no luck 47 | if (chance > 6.2) return 2; 48 | if (chance > 3.0) return 10; 49 | if (chance > 1.4) return 50; 50 | if (chance > 0.6) return 100; 51 | if (chance > 0.2) return 500; 52 | return 1000; 53 | } 54 | -------------------------------------------------------------------------------- /src/game_server/endpoints/GAME_START.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue, short } from "../../core/utils/unit.js"; 4 | import { RegionUpdate } from "../region.js"; 5 | import { SendQuests } from "../functions/sendQuests.js"; 6 | import { SendNotices } from "../functions/sendNotices.js"; 7 | import { SendTime } from "../functions/sendTime.js"; 8 | import { SendWeather } from "../functions/sendWeather.js"; 9 | import { SendMyInfo } from "../functions/sendMyInfo.js"; 10 | import { SendZoneAbility } from "../functions/sendZoneAbility.js"; 11 | import { SendRegionUserInMultiple } from "../functions/sendRegionInOut.js"; 12 | import { SendBlinkStart } from "../functions/sendBlink.js"; 13 | 14 | export const GAME_START: IGameEndpoint = async function ( 15 | socket: IGameSocket, 16 | body: Queue, 17 | opcode: number 18 | ) { 19 | let subOpCode = body.byte(); 20 | 21 | if (socket.ingame) { 22 | return; 23 | } 24 | 25 | if (subOpCode == 1) { 26 | SendQuests(socket); 27 | SendNotices(socket); 28 | SendTime(socket); 29 | SendWeather(socket); 30 | SendMyInfo(socket); 31 | SendZoneAbility(socket); 32 | 33 | socket.send([opcode]); 34 | } else if (subOpCode == 2) { 35 | RegionUpdate(socket); // put user in region 36 | socket.ingame = true; 37 | 38 | SendRegionUserInMultiple(socket); 39 | SendBlinkStart(socket); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/game_server/endpoints/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./VERSION_CHECK.js"; 2 | export * from "./LOGIN.js"; 3 | export * from "./LOAD_GAME.js"; 4 | export * from "./SEL_NATION.js"; 5 | export * from "./ALLCHAR_INFO_REQ.js"; 6 | export * from "./NEW_CHAR.js"; 7 | export * from "./SEL_CHAR.js"; 8 | export * from "./CHANGE_HAIR.js"; 9 | export * from "./SHOPPING_MALL.js"; 10 | export * from "./RENTAL.js"; 11 | export * from "./SPEEDHACK_CHECK.js"; 12 | export * from "./HACK_TOOL.js"; 13 | export * from "./SERVER_INDEX.js"; 14 | export * from "./GAME_START.js"; 15 | export * from "./KNIGHT.js"; 16 | export * from "./QUEST.js"; 17 | export * from "./FRIEND.js"; 18 | export * from "./SKILLDATA.js"; 19 | export * from "./CHAT.js"; 20 | export * from "./HOME.js"; 21 | export * from "./MOVE.js"; 22 | export * from "./ROTATE.js"; 23 | export * from "./USER_INFO.js"; 24 | export * from "./CHAT_TARGET.js"; 25 | export * from "./USER_DETAILED_REQUEST.js"; 26 | export * from "./ZONE_CHANGE.js"; 27 | export * from "./TARGET_HP.js"; 28 | export * from "./STATE_CHANGE.js"; 29 | export * from "./NPC_DETAILED_REQUEST.js"; 30 | export * from "./ATTACK.js"; 31 | export * from "./DROP_OPEN.js"; 32 | export * from "./DROP_TAKE.js"; 33 | export * from "./HELMET_STATUS_CHANGE.js"; 34 | export * from "./GENIE.js"; 35 | export * from "./ITEM_REMOVE.js"; 36 | export * from "./ITEM_MOVE.js"; 37 | export * from "./POINT_CHANGE.js"; 38 | export * from "./NPC_EVENT.js"; 39 | export * from "./ITEM_TRADE.js"; 40 | -------------------------------------------------------------------------------- /src/login_server/server.ts: -------------------------------------------------------------------------------- 1 | import config from "config"; 2 | import { Database } from "../core/database/index.js"; 3 | import { KOServerFactory, type IKOServer } from "../core/server.js"; 4 | import { Queue } from "../core/utils/unit.js"; 5 | import { LoginEndpointCodes, LoginEndpoint } from "./endpoint.js"; 6 | import type { ILoginSocket } from "./login_socket.js"; 7 | import { RedisConnect } from "../core/redis/index.js"; 8 | 9 | let loginServerCache: IKOServer[] = null; 10 | 11 | export async function LoginServer() { 12 | if (loginServerCache) return loginServerCache; 13 | 14 | console.log("[SERVER] Login server is going to start..."); 15 | await Database(); 16 | await RedisConnect(); 17 | 18 | let versions: any[] = config.get("loginServer.versions"); 19 | let { version: serverVersion } = versions[versions.length - 1]; 20 | 21 | console.log("[SERVER] Looks like latest server version is " + serverVersion); 22 | 23 | return (loginServerCache = await KOServerFactory({ 24 | ip: config.get("loginServer.ip"), 25 | ports: config.get("loginServer.ports"), 26 | timeout: 5000, 27 | 28 | onData: async (socket: ILoginSocket, data: Buffer) => { 29 | let body = Queue.from(data); 30 | let opcode = body.byte(); 31 | if (!LoginEndpointCodes[opcode]) return; 32 | 33 | let endpoint = LoginEndpoint(LoginEndpointCodes[opcode]); 34 | if (!endpoint) return; 35 | 36 | await endpoint(socket, body, opcode); 37 | }, 38 | })); 39 | } 40 | -------------------------------------------------------------------------------- /src/game_server/endpoint.ts: -------------------------------------------------------------------------------- 1 | import { Queue } from "../core/utils/unit.js"; 2 | import type { IGameSocket } from "./game_socket.js"; 3 | import * as Endpoints from "./endpoints/index.js"; 4 | 5 | export enum GameEndpointCodes { 6 | VERSION_CHECK = 0x2b, 7 | LOGIN = 0x1, 8 | LOAD_GAME = 0x9f, 9 | SEL_NATION = 0x05, 10 | ALLCHAR_INFO_REQ = 0x0c, 11 | NEW_CHAR = 0x02, 12 | SEL_CHAR = 0x04, 13 | CHANGE_HAIR = 0x89, // client calls if hair = 0 14 | SHOPPING_MALL = 0x6a, 15 | RENTAL = 0x73, // investigate this 16 | SPEEDHACK_CHECK = 0x41, 17 | HACK_TOOL = 0x72, 18 | SERVER_INDEX = 0x6b, 19 | GAME_START = 0x0d, 20 | KNIGHT = 0x3c, 21 | QUEST = 0x64, 22 | FRIEND = 0x49, 23 | SKILLDATA = 0x79, 24 | CHAT = 0x10, 25 | HOME = 0x48, 26 | MOVE = 0x06, 27 | ROTATE = 0x09, 28 | USER_INFO = 0x98, 29 | CHAT_TARGET = 0x35, 30 | USER_DETAILED_REQUEST = 0x16, 31 | ZONE_CHANGE = 0x27, 32 | TARGET_HP = 0x22, 33 | STATE_CHANGE = 0x29, 34 | NPC_DETAILED_REQUEST = 0x1d, 35 | ATTACK = 0x08, 36 | DROP_OPEN = 0x24, 37 | DROP_TAKE = 0x26, 38 | HELMET_STATUS_CHANGE = 0x87, 39 | GENIE = 0x97, 40 | ITEM_REMOVE = 0x3f, 41 | ITEM_MOVE = 0x1f, 42 | POINT_CHANGE = 0x28, 43 | NPC_EVENT = 0x20, 44 | ITEM_TRADE = 0x21, 45 | WAREHOUSE = 0x45, 46 | } 47 | 48 | export function GameEndpoint(name: string): IGameEndpoint { 49 | return Endpoints[name]; 50 | } 51 | 52 | export interface IGameEndpoint { 53 | (socket: IGameSocket, body: Queue, opcode: number): Promise; 54 | } 55 | -------------------------------------------------------------------------------- /src/game_server/drop.ts: -------------------------------------------------------------------------------- 1 | import { WaitNextTick } from "../core/utils/general.js"; 2 | 3 | const drops: IDropDictionary = {}; 4 | let dropIndex = 0; 5 | 6 | export function CreateDrop(owners: number[], dropped) { 7 | let index = ++dropIndex; 8 | 9 | drops[index] = { 10 | timestamp: Date.now(), 11 | owners, 12 | dropped, 13 | }; 14 | 15 | return index; 16 | } 17 | 18 | export function GetDrop(dropIndex: number) { 19 | return drops[dropIndex]; 20 | } 21 | 22 | export function RemoveDrop(dropIndex: number) { 23 | if (drops[dropIndex]) { 24 | delete drops[dropIndex]; 25 | } 26 | } 27 | 28 | export async function ClearDropsTick() { 29 | let timeout = Date.now() - 10 * 60 * 1000; // 10 mins 30 | let ioSafe = 0; 31 | let cleared = 0; 32 | for (let did in drops) { 33 | if (++ioSafe > 50) { 34 | ioSafe = 0; 35 | await WaitNextTick(); 36 | } 37 | 38 | let drop = drops[did]; 39 | if (drop.timestamp < timeout) { 40 | cleared++; 41 | delete drops[did]; 42 | } 43 | } 44 | 45 | if (cleared) { 46 | console.log("[TICK] %d drops cleared!", cleared); 47 | } 48 | } 49 | 50 | export interface IDrop { 51 | owners: number[]; // sessions that can take 52 | timestamp: number; // timestamp that item(s) dropped 53 | dropped: IDropItem[]; 54 | } 55 | 56 | export interface IDropItem { 57 | item: number; // item id 58 | amount: number; // item count 59 | } 60 | 61 | export interface IDropDictionary { 62 | [dropIndex: number]: IDrop; 63 | } 64 | -------------------------------------------------------------------------------- /src/core/database/defaults/set_item.ts: -------------------------------------------------------------------------------- 1 | import { CSVLoader } from "../utils/csv_loader.js"; 2 | import { SetItem } from "../models/index.js"; 3 | 4 | export async function SetItemDefaults() { 5 | await CSVLoader("set_items", SetItemTransferObjects, 1066, SetItem); 6 | } 7 | 8 | const SetItemTransferObjects = { 9 | SetIndex: "id", 10 | SetName: "name", 11 | ACBonus: "acBonus", 12 | HPBonus: "hpBonus", 13 | MPBonus: "mpBonus", 14 | StrengthBonus: "statstrBonus", 15 | StaminaBonus: "stathpBonus", 16 | DexterityBonus: "statdexBonus", 17 | IntelBonus: "statintBonus", 18 | CharismaBonus: "statmpBonus", 19 | FlameResistance: "flameResistance", 20 | GlacierResistance: "glacierResistance", 21 | LightningResistance: "lightningResistance", 22 | PoisonResistance: "poisonResistance", 23 | MagicResistance: "magicResistance", 24 | CurseResistance: "curseResistance", 25 | XPBonusPercent: "XPBonusPercent", 26 | CoinBonusPercent: "coinBonusPercent", 27 | APBonusPercent: "APBonusPercent", 28 | APBonusClassType: "APBonusClassType", 29 | APBonusClassPercent: "APBonusClassPercent", 30 | ACBonusClassType: "ACBonusClassType", 31 | ACBonusClassPercent: "ACBonusClassPercent", 32 | MaxWeightBonus: "maxWeightBonus", 33 | NPBonus: "NPBonus", 34 | Unk10: "unk10", 35 | Unk11: "unk11", 36 | Unk12: "unk12", 37 | Unk13: "unk13", 38 | Unk14: "unk14", 39 | Unk15: "unk15", 40 | Unk16: "unk16", 41 | Unk17: "unk17", 42 | Unk18: "unk18", 43 | Unk19: "unk19", 44 | Unk20: "unk20", 45 | Unk21: "unk21", 46 | }; 47 | -------------------------------------------------------------------------------- /src/game_server/events/onServerTick.ts: -------------------------------------------------------------------------------- 1 | import { OnNPCTick } from "./onNPCTick.js"; 2 | import { ClearDropsTick } from "../drop.js"; 3 | import { TimeDifference, GarbageCollect } from "../../core/utils/general.js"; 4 | import { AverageTime } from "../../core/utils/average_time.js"; 5 | 6 | export let tick = 0; 7 | 8 | export const avgNPC = AverageTime.instance(50); 9 | export const avgGC = AverageTime.instance(50); 10 | export const avgDrop = AverageTime.instance(50); 11 | 12 | export function OnServerTick() { 13 | ++tick; 14 | 15 | let time = TimeDifference.begin(); 16 | 17 | let gcDiff = 0; 18 | if (tick % 960 == 480) { 19 | // every 4 minute 20 | GarbageCollect(); 21 | gcDiff = time.end(); 22 | avgGC.push(gcDiff); 23 | 24 | if (gcDiff > 15) 25 | console.log( 26 | "[BUSY] Garbage collect took %dms (average %dms)", 27 | gcDiff, 28 | avgGC.avg() 29 | ); 30 | } 31 | 32 | OnNPCTick().then(() => { 33 | let diff = time.end() - gcDiff; 34 | avgNPC.push(diff); 35 | 36 | if (diff > 30) 37 | console.log( 38 | "[BUSY] NPC tick took %dms (average %dms)", 39 | diff, 40 | avgNPC.avg() 41 | ); 42 | }); 43 | 44 | if (tick % 480 == 240) { 45 | // every 2 minute 46 | ClearDropsTick().then(() => { 47 | let diff = time.end() - gcDiff; 48 | avgDrop.push(diff); 49 | 50 | if (diff > 10) 51 | console.log( 52 | "[BUSY] Drops clear tick took %dms (average %dms)", 53 | diff, 54 | avgDrop.avg() 55 | ); 56 | }); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/core/database/models/account.ts: -------------------------------------------------------------------------------- 1 | import { PasswordHash } from "../../utils/password_hash.js"; 2 | import { Schema, model, Document } from "mongoose"; 3 | 4 | export interface IAccount extends Document { 5 | account: string; 6 | password: string; 7 | session: string; 8 | banned: boolean; 9 | bannedMessage: string; 10 | premium: boolean; 11 | premiumEndsAt: Date; 12 | otp: boolean; 13 | otpSecret: string; 14 | otpLastFail: Date; 15 | otpTryCount: number; 16 | nation: number; 17 | characters: string[]; 18 | warehouse: string; 19 | } 20 | 21 | export const AccountSchema = new Schema( 22 | { 23 | account: { type: String, maxlength: 30 }, 24 | password: { type: String, maxlength: 100 }, 25 | session: { type: String }, 26 | 27 | banned: { type: Boolean, default: false }, 28 | bannedMessage: { type: String, maxlength: 50 }, 29 | 30 | premium: { type: Boolean }, 31 | premiumEndsAt: { type: Date }, 32 | 33 | otp: { type: Boolean, default: false }, 34 | otpSecret: { type: String }, 35 | otpLastFail: { type: Date }, 36 | otpTryCount: { type: Number, default: 0 }, 37 | 38 | nation: { type: Number }, 39 | characters: [{ type: String }], 40 | 41 | warehouse: { type: String }, 42 | }, 43 | { timestamps: true } 44 | ); 45 | 46 | AccountSchema.pre("save", function (next) { 47 | let user = this; 48 | 49 | if (user.isModified("password")) { 50 | user.password = PasswordHash(user.password); 51 | } 52 | 53 | next(); 54 | }); 55 | 56 | export const Account = model("Account", AccountSchema, "accounts"); 57 | -------------------------------------------------------------------------------- /src/game_server/ai_system/start.ts: -------------------------------------------------------------------------------- 1 | import { Npc } from "../../core/database/models/index.js"; 2 | import { NPCUUID, NPCMap } from "./uuid.js"; 3 | import type { INPCInstance } from "./declare.js"; 4 | 5 | let loaded = false; 6 | 7 | export async function AISystemStart() { 8 | if (loaded) return; 9 | loaded = true; 10 | 11 | console.log("[NPC] loading.."); 12 | let rawNpcs = await Npc.find({}).lean().select(["-_id"]).exec(); 13 | 14 | let npcCount = 0; 15 | let monsterCount = 0; 16 | let skipped = 0; 17 | 18 | let now = Date.now(); 19 | for (let npc of rawNpcs) { 20 | if (!npc.spawn.length) { 21 | skipped++; 22 | continue; 23 | } 24 | 25 | for (let spawn of npc.spawn) { 26 | if (spawn.zone == 21) { 27 | // rn do only moradon 28 | let amount = spawn.amount || 1; 29 | 30 | for (let i = 0; i < amount; i++) { 31 | npcCount++; 32 | if (npc.isMonster) monsterCount++; 33 | 34 | let npcObj: INPCInstance = { 35 | npc, 36 | spawn, 37 | 38 | uuid: NPCUUID.reserve(), 39 | status: "init", 40 | timestamp: now, 41 | wait: 0, 42 | }; 43 | 44 | NPCMap[npcObj.uuid] = npcObj; 45 | } 46 | } else { 47 | let amount = spawn.amount || 1; 48 | 49 | for (let i = 0; i < amount; i++) { 50 | skipped++; 51 | } 52 | } 53 | } 54 | } 55 | 56 | console.log( 57 | "[NPC] total: %d, mobs: %d, skipped: %d", 58 | npcCount, 59 | monsterCount, 60 | skipped 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /src/core/utils/general.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import { fileURLToPath } from "url"; 4 | 5 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 6 | 7 | /** 8 | * Calculates the time difference in nanoseconds 9 | */ 10 | export class TimeDifference { 11 | private startAt: [number, number]; 12 | 13 | private constructor() { 14 | this.startAt = process.hrtime(); 15 | } 16 | 17 | /** 18 | * Creates the instance at the begining 19 | */ 20 | public static begin() { 21 | return new TimeDifference(); 22 | } 23 | 24 | /** 25 | * Finishes the time, returns data in milliseconds 26 | */ 27 | end(): number { 28 | let diff = process.hrtime(this.startAt); 29 | 30 | return diff[0] * 1000 + diff[1] / 1e6; 31 | } 32 | } 33 | 34 | /** 35 | * This will be handy function for IO safety. 36 | */ 37 | export function WaitNextTick(): Promise { 38 | return new Promise((resolve) => setImmediate(resolve)); 39 | } 40 | 41 | /** 42 | * Triggers garbage collection. (THIS WILL LOCK THE EVENT LOOP) 43 | */ 44 | export function GarbageCollect() { 45 | if (global.gc) { 46 | global.gc(); 47 | } 48 | } 49 | 50 | /** 51 | * Loads the package.json file and returns as json object. 52 | */ 53 | export function GetPackageJSON(): Promise { 54 | return new Promise((resolve, reject) => { 55 | if (_packageJSON) return resolve(_packageJSON); 56 | 57 | fs.readFile( 58 | path.resolve(__dirname, "../../../package.json"), 59 | (err, data) => { 60 | if (err) { 61 | reject(err); 62 | } else { 63 | try { 64 | resolve((_packageJSON = JSON.parse(data.toString()))); 65 | } catch (e) { 66 | reject(e); 67 | } 68 | } 69 | } 70 | ); 71 | }); 72 | } 73 | 74 | let _packageJSON = null; 75 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import { LoginServer } from "./login_server/server.js"; 2 | import { GameServer } from "./game_server/server.js"; 3 | import { OnServerTick } from "./game_server/events/onServerTick.js"; 4 | import type { IKOServer } from "./core/server.js"; 5 | import { Database, DisconnectFromDatabase } from "./core/database/index.js"; 6 | import { GarbageCollect } from "./core/utils/general.js"; 7 | import { WebServer } from "./web/server.js"; 8 | import { RedisConnect } from "./core/redis/index.js"; 9 | 10 | async function main() { 11 | console.log("[MAIN] %s v%s", "ko-js", "1.0.0"); 12 | await Database(); 13 | await RedisConnect(); 14 | 15 | console.log("[MAIN] Servers are queued"); 16 | 17 | const servers: IKOServer[] = []; 18 | servers.push(...(await LoginServer())); 19 | servers.push(await GameServer()); 20 | await WebServer(); 21 | 22 | const tick = setInterval(OnServerTick, 250); 23 | 24 | GarbageCollect(); 25 | 26 | let bind = CloseSignal.bind(null, "SIGINT", servers, tick); 27 | process.on("SIGINT", bind); 28 | process.on("SIGTERM", bind); 29 | } 30 | 31 | async function CloseSignal( 32 | signal: string, 33 | servers: IKOServer[], 34 | tick: NodeJS.Timeout 35 | ) { 36 | if (stopping) return; 37 | stopping = true; 38 | console.log(`[MAIN] Closing signal came... (${signal})`); 39 | 40 | clearInterval(tick); // stop server ticking.. 41 | 42 | for (let server of servers) { 43 | await server.stop(); 44 | } 45 | 46 | await DisconnectFromDatabase(); 47 | 48 | process.exit(0); // safely exited 49 | } 50 | 51 | let stopping = false; 52 | 53 | main().catch((err) => { 54 | const crashText = " APP CRASHED "; 55 | console.error("-".repeat(15) + crashText + "-".repeat(15)); 56 | console.error("Date: %s", new Date()); 57 | console.error(err.stack); 58 | console.error("-".repeat(30 + crashText.length)); 59 | process.exit(1); 60 | }); 61 | -------------------------------------------------------------------------------- /src/game_server/endpoints/NPC_EVENT.ts: -------------------------------------------------------------------------------- 1 | import { Queue, short, int } from "../../core/utils/unit.js"; 2 | import type { IGameEndpoint } from "../endpoint.js"; 3 | import type { IGameSocket } from "../game_socket.js"; 4 | import { type INPCInstance, NPCType } from "../ai_system/declare.js"; 5 | import { NPCMap } from "../ai_system/uuid.js"; 6 | 7 | export const NPC_EVENT: IGameEndpoint = async function ( 8 | socket: IGameSocket, 9 | body: Queue, 10 | opcode: number 11 | ) { 12 | let unk = body.byte(); 13 | let npcID = body.short(); 14 | let questID = body.int(); 15 | 16 | let npc: INPCInstance = NPCMap[npcID]; 17 | 18 | if (!npc) return; // TODO: make more controls 19 | 20 | let npcType: NPCType = npc.npc.type; 21 | switch (npcType) { 22 | case NPCType.NPC_MERCHANT: 23 | case NPCType.NPC_TINKER: 24 | socket.send([ 25 | NPCType.NPC_MERCHANT ? 0x25 : 0x3a, 26 | ...int(npc.npc.sellingGroup | 0), 27 | ]); 28 | break; 29 | case NPCType.NPC_MARK: 30 | case NPCType.NPC_RENTAL: 31 | case NPCType.NPC_ELECTION: 32 | case NPCType.NPC_TREASURY: 33 | case NPCType.NPC_SIEGE: 34 | case NPCType.NPC_SIEGE_1: 35 | case NPCType.NPC_VICTORY_GATE: 36 | case NPCType.NPC_BORDER_MONUMENT: 37 | case NPCType.NPC_CLAN: 38 | // TODO: HANDLE 39 | break; 40 | case NPCType.NPC_CAPTAIN: 41 | socket.send([0x34, 0x01]); 42 | break; 43 | case NPCType.NPC_WAREHOUSE: 44 | socket.send([0x45, 0x10]); 45 | break; 46 | case NPCType.NPC_CHAOTIC_GENERATOR: 47 | case NPCType.NPC_CHAOTIC_GENERATOR2: 48 | socket.send([0x5b, 4, ...short(npc.uuid)]); 49 | break; 50 | case NPCType.NPC_KJWAR: 51 | socket.send([0x85, 1, 7]); 52 | break; 53 | default: 54 | console.log( 55 | "[NPC_EVENT] Handle this request. npc:" + npcID + " quest:" + questID 56 | ); 57 | break; 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /src/game_server/var/zone_codes.ts: -------------------------------------------------------------------------------- 1 | export const enum ZoneCode { 2 | ZONE_KARUS = 1, 3 | ZONE_ELMORAD = 2, 4 | ZONE_KARUS2 = 5, 5 | ZONE_KARUS3 = 6, 6 | ZONE_ELMORAD2 = 7, 7 | ZONE_ELMORAD3 = 8, 8 | ZONE_KARUS_ESLANT = 11, 9 | ZONE_ELMORAD_ESLANT = 12, 10 | ZONE_KARUS_ESLANT2 = 13, 11 | ZONE_KARUS_ESLANT3 = 14, 12 | ZONE_ELMORAD_ESLANT2 = 15, 13 | ZONE_ELMORAD_ESLANT3 = 16, 14 | ZONE_MORADON = 21, 15 | ZONE_MORADON2 = 22, 16 | ZONE_MORADON3 = 23, 17 | ZONE_MORADON4 = 24, 18 | ZONE_MORADON5 = 25, 19 | ZONE_DELOS = 30, 20 | ZONE_BIFROST = 31, 21 | ZONE_DESPERATION_ABYSS = 32, 22 | ZONE_HELL_ABYSS = 33, 23 | ZONE_DRAGON_CAVE = 34, 24 | ZONE_ARENA = 48, 25 | ZONE_ORC_ARENA = 51, 26 | ZONE_BLOOD_DON_ARENA = 52, 27 | ZONE_GOBLIN_ARENA = 53, 28 | ZONE_CAITHAROS_ARENA = 54, 29 | ZONE_FORGOTTEN_TEMPLE = 55, 30 | ZONE_LOST_TEMPLE = 56, 31 | 32 | ZONE_BATTLE = 61, // Napies Gorge 33 | ZONE_BATTLE2 = 62, // Alseids Prairie 34 | ZONE_BATTLE3 = 63, // Nieds Triangle 35 | ZONE_BATTLE4 = 64, // Nereid's Island 36 | ZONE_BATTLE5 = 65, // Zipang 37 | ZONE_BATTLE6 = 66, // Oreads 38 | 39 | ZONE_NAPIES_GORDE = 61, 40 | ZONE_ALSEIDS_PRAIRIE = 62, 41 | ZONE_NIEDS_TRIANGLE = 63, 42 | ZONE_NEREIDSISLAND = 64, 43 | ZONE_ZIPANG = 65, 44 | ZONE_OREADS = 66, 45 | 46 | ZONE_SNOW_BATTLE = 69, 47 | ZONE_RONARK_LAND = 71, 48 | ZONE_ARDREAM = 72, 49 | ZONE_RONARK_LAND_BASE = 73, 50 | ZONE_KROWAZ_DOMINION = 75, 51 | ZONE_CLAN_WAR = 77, 52 | ZONE_NEW_RONARK_EVENT = 78, 53 | 54 | ZONE_MONSTER_SQUAD1 = 81, 55 | ZONE_MONSTER_SQUAD2 = 82, 56 | ZONE_MONSTER_SQUAD3 = 83, 57 | 58 | ZONE_BORDER_DEFENSE_WAR = 84, 59 | ZONE_CHAOS_DUNGEON = 85, 60 | ZONE_UNDER_THE_CASTLE = 86, 61 | ZONE_JURAD_MOUNTAIN = 87, 62 | ZONE_PRISON = 92, 63 | ZONE_ISILOON_ARENA = 93, 64 | ZONE_FELANKOR_ARENA = 94, 65 | ZONE_WINNER_CASTLE = 97, 66 | ZONE_WINNER_CASTLE2 = 98, 67 | ZONE_OLD_MORADON = 91, 68 | } 69 | -------------------------------------------------------------------------------- /src/game_server/endpoints/ITEM_REMOVE.ts: -------------------------------------------------------------------------------- 1 | import { Queue } from "../../core/utils/unit.js"; 2 | import type { IGameEndpoint } from "../endpoint.js"; 3 | import type { IGameSocket } from "../game_socket.js"; 4 | import { SendAbility } from "../functions/sendAbility.js"; 5 | import { SendWeightChange } from "../functions/sendWeightChange.js"; 6 | 7 | export const ITEM_REMOVE: IGameEndpoint = async function ( 8 | socket: IGameSocket, 9 | body: Queue, 10 | opcode: number 11 | ) { 12 | let type = body.byte(); 13 | let pos = body.byte(); 14 | let itemID = body.int(); 15 | 16 | try { 17 | if (pos < 0) throw 1; 18 | 19 | if (type == 0 || type == 2) { 20 | // item in inventory 21 | if (pos >= 28) throw 1; // inventory cannot have more than 28 items (pos starts with 0) 22 | 23 | pos += 14; // we store inventory 14-42 area in database 24 | } else if (type == 1) { 25 | // item in equiped area 26 | if (pos >= 14) throw 1; // you cant wear more than 14 items 27 | } 28 | 29 | let c = socket.character; 30 | let item = c.items[pos]; 31 | 32 | if (!item) throw 1; // item doesnt exist 33 | if (item.id != itemID) throw 1; // item doesnt match with requested 34 | 35 | // TODO: more checks 36 | 37 | c.items[pos] = null; // good bye item 38 | 39 | if (!c.removedItems) { 40 | c.removedItems = []; 41 | } 42 | 43 | c.removedItems.unshift({ 44 | id: item.id, 45 | amount: item.amount, 46 | durability: item.durability, 47 | serial: item.serial, 48 | removedAt: new Date(), 49 | }); 50 | 51 | if (c.removedItems.length > 20) { 52 | c.removedItems.pop(); // remove old deleted ones 53 | } 54 | 55 | c.markModified("items"); 56 | c.markModified("removedItems"); 57 | 58 | SendAbility(socket, true); 59 | SendWeightChange(socket); 60 | 61 | socket.send([opcode, 1]); 62 | } catch (e) { 63 | socket.send([opcode, 0]); 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /src/game_server/functions/sendWarp.ts: -------------------------------------------------------------------------------- 1 | import type { IGameSocket } from "../game_socket.js"; 2 | import { ZoneCode } from "../var/zone_codes.js"; 3 | import { ZoneStartPosition } from "../var/zone_start_position.js"; 4 | import { short } from "../../core/utils/unit.js"; 5 | import { RegionUpdate } from "../region.js"; 6 | import { SendRegionUserOutForMe } from "./sendRegionInOut.js"; 7 | 8 | export function SendWarp(socket: IGameSocket, zone: ZoneCode): boolean { 9 | let pos = FindStartPositionOfZoneForUser(socket, zone); 10 | 11 | if (!pos) { 12 | return false; 13 | } 14 | 15 | socket.character.x = pos.x; 16 | socket.character.z = pos.z; 17 | socket.character.y = 0; 18 | 19 | if (socket.character.zone == zone) { 20 | socket.send([ 21 | 0x1e, // WARP 22 | ...short(pos.x * 10), 23 | ...short(pos.z * 10), 24 | ]); 25 | 26 | RegionUpdate(socket); 27 | } else { 28 | socket.send([ 29 | 0x27, // ZONE_CHANGE 30 | 3, // ZONE_CHANGE_TELEPORT 31 | ...short(zone), 32 | ...short(pos.x * 10), 33 | ...short(pos.z * 10), 34 | 0, 35 | 0, 36 | 0, 37 | ]); 38 | 39 | socket.character.zone = zone; 40 | 41 | RegionUpdate(socket, true); 42 | 43 | SendRegionUserOutForMe(socket); 44 | } 45 | 46 | return true; 47 | } 48 | 49 | export function FindStartPositionOfZoneForUser( 50 | socket: IGameSocket, 51 | zone: ZoneCode 52 | ) { 53 | let u = socket.user; 54 | 55 | let startPosition = ZoneStartPosition[zone]; 56 | let x = 0; 57 | let z = 0; 58 | 59 | if (!startPosition) { 60 | return; 61 | } 62 | 63 | if (u.nation == 1) { 64 | x = startPosition["karus"][0]; 65 | z = startPosition["karus"][1]; 66 | } else { 67 | x = startPosition["elmorad"][0]; 68 | z = startPosition["elmorad"][1]; 69 | } 70 | 71 | x += (Math.random() - 0.5) * startPosition["range"][0]; 72 | z += (Math.random() - 0.5) * startPosition["range"][1]; 73 | 74 | return { x, z }; 75 | } 76 | -------------------------------------------------------------------------------- /src/core/utils/otp.ts: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | import config from "config"; 3 | 4 | export function GenerateOTP(secret: string): string { 5 | let timestamp = Math.floor(Math.round(Date.now() / 1000) / 30).toString(16); 6 | let buffer = Buffer.from(("0".repeat(16) + timestamp).slice(-16), "hex"); 7 | let key = Buffer.from(base32tohex(secret), "hex"); 8 | let hmac = crypto.createHmac("sha1", key); 9 | hmac.setEncoding("hex"); 10 | hmac.update(buffer); 11 | hmac.end(); 12 | 13 | let hmacString: string = hmac.read(); 14 | let lastCharValue = parseInt(hmacString.slice(-1), 16); 15 | let fullValue = parseInt(hmacString.substr(lastCharValue * 2, 8), 16); 16 | let last6Digits = ((fullValue & 2147483647) + "").slice(-6); 17 | return last6Digits; 18 | } 19 | 20 | export const randomSecret = () => randomBase32(16); 21 | export const generateUrl = (userCredentials: string, secretKey: string) => 22 | "https://chart.googleapis.com/chart?chs=300x300&chld=M|0&cht=qr&chl=otpauth://totp/" + 23 | userCredentials + 24 | "%40" + 25 | domain + 26 | "%3Fsecret=" + 27 | secretKey; 28 | 29 | const domain = config.get("loginServer.otp"); 30 | 31 | const base32chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; 32 | 33 | function base32tohex(base32: string) { 34 | let bits = ""; 35 | let hex = ""; 36 | 37 | for (let i = 0; i < base32.length; i++) { 38 | let val = base32chars.indexOf(base32.charAt(i).toUpperCase()); 39 | bits += (Array(5).fill(0).join("") + val.toString(2)).slice(-5); 40 | } 41 | 42 | for (let i = 0; i < bits.length - 3; i += 4) { 43 | let chunk = bits.substr(i, 4); 44 | hex = hex + parseInt(chunk, 2).toString(16); 45 | } 46 | 47 | return hex; 48 | } 49 | 50 | function randomBase32(length: number) { 51 | length += length % 2; // always could divide by 2 52 | 53 | let secret = []; 54 | 55 | for (let i = 0; i < length; i++) { 56 | secret.push(base32chars.charAt((Math.random() * base32chars.length) | 0)); 57 | } 58 | 59 | return secret.join(""); 60 | } 61 | -------------------------------------------------------------------------------- /src/game_server/endpoints/CHAT.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue } from "../../core/utils/unit.js"; 4 | import { 5 | SendUsageMessageForGM, 6 | GM_COMMANDS_HEADER, 7 | GM_COMMANDS, 8 | } from "../functions/GMController.js"; 9 | import { 10 | ChatMessageType, 11 | SendPlayerMessageToRegion, 12 | SendMessageToPlayerFromPlayer, 13 | } from "../functions/sendChatMessage.js"; 14 | import { RUserMap } from "../region.js"; 15 | 16 | export const CHAT: IGameEndpoint = async function ( 17 | socket: IGameSocket, 18 | body: Queue, 19 | opcode: number 20 | ) { 21 | let type = body.byte(); 22 | let message = body.string(); 23 | 24 | if (socket.character.gm && type == 1 && message == "+") { 25 | return SendUsageMessageForGM(socket, "hello master, type help :)"); 26 | } 27 | 28 | if ( 29 | (type == 2 && 30 | socket.character.gm && 31 | socket.variables.chatTo == GM_COMMANDS_HEADER) || 32 | (type == 1 && socket.character.gm && message[0] == "+") 33 | ) { 34 | let args = (type == 1 ? message.substring(1) : message).split(" "); 35 | let command = args.shift(); 36 | 37 | if (!GM_COMMANDS[command]) { 38 | return SendUsageMessageForGM( 39 | socket, 40 | `ERROR: Invalid command "${command}"` 41 | ); 42 | } 43 | 44 | return GM_COMMANDS[command](args, socket, opcode); 45 | } 46 | 47 | if (type == ChatMessageType.GENERAL) { 48 | if (message.length > 128) { 49 | message = message.substring(0, 128); 50 | } 51 | 52 | return SendPlayerMessageToRegion(socket, message); 53 | } 54 | 55 | if (type == ChatMessageType.PRIVATE) { 56 | if (!socket.variables.chatTo) { 57 | return; 58 | } 59 | 60 | let userRegionContainer = RUserMap[socket.variables.chatTo]; 61 | 62 | if (!userRegionContainer) return; 63 | 64 | SendMessageToPlayerFromPlayer( 65 | userRegionContainer.socket, 66 | socket, 67 | ChatMessageType.PRIVATE, 68 | message 69 | ); 70 | } 71 | }; 72 | -------------------------------------------------------------------------------- /src/core/database/defaults/item.ts: -------------------------------------------------------------------------------- 1 | import { CSVLoader } from "../utils/csv_loader.js"; 2 | import { Item } from "../models/index.js"; 3 | 4 | export async function ItemDefaults() { 5 | await CSVLoader("items", ItemTransferObject, 443267, Item); 6 | } 7 | 8 | const ItemTransferObject = { 9 | Num: "id", 10 | strName: "name", 11 | Extension: "extension", 12 | isUnique: "isUnique", 13 | IconID: "iconID", 14 | Kind: "kind", 15 | Slot: "slot", 16 | Race: "race", 17 | Class: "klass", 18 | Damage: "damage", 19 | Delay: "delay", 20 | Range: "range", 21 | Weight: "weight", 22 | Duration: "durability", 23 | BuyPrice: "buyPrice", 24 | SellPrice: "sellPrice", 25 | Ac: "defenceAbility", 26 | Countable: "countable", 27 | Effect1: "effect1", 28 | Effect2: "effect2", 29 | ReqLevel: "reqLevel", 30 | ReqLevelMax: "reqLevelMax", 31 | ReqRank: "reqRank", 32 | ReqTitle: "reqTitle", 33 | ReqStr: "reqStr", 34 | ReqSta: "reqHp", 35 | ReqDex: "reqDex", 36 | ReqIntel: "reqInt", 37 | ReqCha: "reqMp", 38 | SellingGroup: "sellingGroup", 39 | ItemType: "itemType", 40 | Hitrate: "hitRate", 41 | Evasionrate: "evaRate", 42 | DaggerAc: "daggerDefenceAbility", 43 | JamadarAC: "jamadarDefenceAbility", 44 | SwordAc: "swordDefenceAbility", 45 | MaceAc: "maceDefenceAbility", 46 | AxeAc: "axeDefenceAbility", 47 | SpearAc: "spearDefenceAbility", 48 | BowAc: "bowDefenceAbility", 49 | FireDamage: "fireDamage", 50 | IceDamage: "iceDamage", 51 | LightningDamage: "lightningDamage", 52 | PoisonDamage: "poisonDamage", 53 | HPDrain: "hpDrain", 54 | MPDamage: "mpDamage", 55 | MPDrain: "mpDrain", 56 | MirrorDamage: "mirrorDamage", 57 | Droprate: "dropRate", 58 | StrB: "strB", 59 | StaB: "hpB", 60 | DexB: "dexB", 61 | IntelB: "intB", 62 | ChaB: "mpB", 63 | MaxHpB: "maxhpB", 64 | MaxMpB: "maxmpB", 65 | FireR: "fireR", 66 | ColdR: "coldR", 67 | LightningR: "lightningR", 68 | MagicR: "magicR", 69 | PoisonR: "poisonR", 70 | CurseR: "curseR", 71 | ItemClass: "itemClass", 72 | ItemExt: "itemExt", 73 | UpgradeNotice: "upgradeNotice", 74 | NPbuyPrice: "npBuyPrice", 75 | Bound: "bound", 76 | }; 77 | -------------------------------------------------------------------------------- /src/game_server/functions/sendItem.ts: -------------------------------------------------------------------------------- 1 | import type { IGameSocket } from "../game_socket.js"; 2 | import { byte_string, int, short } from "../../core/utils/unit.js"; 3 | import { FindSlotForItem } from "./findSlotForItem.js"; 4 | import { GetItemDetail } from "../../core/database/models/item.js"; 5 | import { GenerateItem } from "./generateItem.js"; 6 | import { SendWeightChange } from "./sendWeightChange.js"; 7 | import { AllSend } from "../region.js"; 8 | import { CalculateUserAbilities } from "./sendAbility.js"; 9 | import { SendStackChange } from "./sendStackChange.js"; 10 | import { EQUIP_MAX } from "../endpoints/ITEM_MOVE.js"; 11 | 12 | export function SendItem( 13 | socket: IGameSocket, 14 | itemId: number, 15 | amount: number, 16 | sendPacket = true 17 | ) { 18 | const c = socket.character; 19 | let v = socket.variables; 20 | let { itemWeight } = v; 21 | const itemDetail = GetItemDetail(itemId); 22 | 23 | if (!itemDetail) { 24 | return "unknown-item"; 25 | } 26 | 27 | const slot = FindSlotForItem(socket, itemDetail, amount); 28 | 29 | if (slot < 0) { 30 | return "no-space"; 31 | } 32 | 33 | if (c.items[slot]) { 34 | c.items[slot].amount = Math.min(9999, c.items[slot].amount + amount); 35 | } else { 36 | c.items[slot] = GenerateItem(itemDetail, amount); 37 | } 38 | 39 | if (itemDetail.kind === 255) { 40 | c.items[slot].amount = itemDetail.durability ?? 0; 41 | } 42 | 43 | let newTotalWeight = itemWeight + (itemDetail.weight | 0) * amount; 44 | socket.variables.itemWeight = newTotalWeight; 45 | 46 | if (itemDetail.itemType == 4 && itemDetail.id != 900144023) { 47 | AllSend([ 48 | 0x7d, // LOGOS SHOUT, 49 | 2, 50 | 4, 51 | ...byte_string(c.name), 52 | ...int(itemDetail.id), 53 | ]); 54 | } 55 | 56 | if (sendPacket) { 57 | SendStackChange( 58 | socket, 59 | itemId, 60 | c.items[slot].amount, 61 | c.items[slot].durability, 62 | slot - EQUIP_MAX, 63 | true, 64 | 0 65 | ); 66 | } else { 67 | CalculateUserAbilities(socket); 68 | 69 | SendWeightChange(socket); 70 | } 71 | 72 | return "ok"; 73 | } 74 | -------------------------------------------------------------------------------- /src/core/utils/password_hash.ts: -------------------------------------------------------------------------------- 1 | import Long from "long"; 2 | 3 | const encodingArray = [ 4 | 0x1a, 0x1f, 0x11, 0x0a, 0x1e, 0x10, 0x18, 0x02, 0x1d, 0x08, 0x14, 0x0f, 0x1c, 5 | 0x0b, 0x0d, 0x04, 0x13, 0x17, 0x00, 0x0c, 0x0e, 0x1b, 0x06, 0x12, 0x15, 0x03, 6 | 0x09, 0x07, 0x16, 0x01, 0x19, 0x05, 0x12, 0x1d, 0x07, 0x19, 0x0f, 0x1f, 0x16, 7 | 0x1b, 0x09, 0x1a, 0x03, 0x0d, 0x13, 0x0e, 0x14, 0x0b, 0x05, 0x02, 0x17, 0x10, 8 | 0x0a, 0x18, 0x1c, 0x11, 0x06, 0x1e, 0x00, 0x15, 0x0c, 0x08, 0x04, 0x01, 9 | ]; 10 | 11 | const alphabetArray = [ 12 | 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x41, 0x42, 0x43, 13 | 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 14 | 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 15 | ]; 16 | 17 | export function PasswordHash( 18 | password: string, 19 | encoding = "utf8" as BufferEncoding 20 | ): string { 21 | var buf = Array.from(Buffer.from(password, encoding)); 22 | while (buf.length % 4 != 0) buf.push(0); 23 | 24 | var outBuf: number[] = []; 25 | var counter = 0, 26 | tmp = 0, 27 | inputKey = 0, 28 | outHash = 0; 29 | 30 | for (var i = 0; i < buf.length; i += 4) { 31 | let encoded = 32 | (buf[i] + (buf[i + 1] << 8) + (buf[i + 2] << 16) + (buf[i + 3] << 24)) >>> 33 | 0; 34 | tmp = (encoded + 0x03e8) >>> 0; 35 | inputKey = tmp; 36 | counter = 0; 37 | outHash = 0; 38 | 39 | do { 40 | tmp = inputKey; 41 | 42 | inputKey = inputKey >>> 1; 43 | if (tmp % 2 != 0) { 44 | tmp = encodingArray[(counter / 4) >>> 0]; 45 | outHash = (outHash + ((1 << (tmp | 0)) >>> 0)) >>> 0; 46 | } 47 | counter = (counter + 4) | 0; 48 | } while (inputKey > 0); 49 | 50 | var long = new Long(outHash); 51 | for (var j = 0; j < 7; j++) { 52 | var upper = long.mul(954437177).shiftRight(35); 53 | var anotherTmp = upper.mul(8).add(upper).shiftLeft(2).low >>> 0; 54 | var difference = long.sub(anotherTmp.toString()).low >>> 0; 55 | outBuf.push(alphabetArray[difference]); 56 | long = upper; 57 | } 58 | } 59 | 60 | return Buffer.from(outBuf).toString("utf8"); 61 | } 62 | -------------------------------------------------------------------------------- /src/login_server/endpoints/CHECK_OTP.ts: -------------------------------------------------------------------------------- 1 | import { Queue, byte_string } from "../../core/utils/unit.js"; 2 | import { GenerateOTP } from "../../core/utils/otp.js"; 3 | import type { ILoginSocket } from "../login_socket.js"; 4 | import type { ILoginEndpoint } from "../endpoint.js"; 5 | import { Account } from "../../core/database/models/index.js"; 6 | 7 | export const CHECK_OTP: ILoginEndpoint = async function ( 8 | socket: ILoginSocket, 9 | body: Queue, 10 | opcode: number 11 | ) { 12 | let accountName = body.string(); 13 | let password = body.string(); 14 | let otpCode = body.string(); 15 | let ok; 16 | 17 | if (accountName.length > 20 || password.length > 28) { 18 | ok = 0; 19 | } else { 20 | try { 21 | let account = await Account.findOne({ 22 | account: accountName, 23 | }).exec(); 24 | 25 | if (!account) { 26 | ok = 0; 27 | } else { 28 | let timeCheck = 29 | account.otpLastFail && 30 | account.otpLastFail > new Date(Date.now() - 1000 * 60 * 30); // 30mins of otp ban 31 | if (timeCheck) { 32 | if (account.otpTryCount > 5) { 33 | ok = 0; 34 | } 35 | } 36 | if (ok == undefined && account.password == password) { 37 | if (account.banned) { 38 | ok = 0; 39 | } else { 40 | if (account.otp) { 41 | if (GenerateOTP(account.otpSecret) != otpCode) { 42 | ok = 2; // nonvalid 43 | 44 | if (timeCheck) { 45 | account.otpTryCount = 1; 46 | } else { 47 | account.otpTryCount = (account.otpTryCount | 0) + 1; 48 | } 49 | 50 | account.otpLastFail = new Date(); 51 | await account.save(); 52 | } else { 53 | ok = 1; // valid otp 54 | } 55 | } else { 56 | ok = 0; 57 | } 58 | } 59 | } else { 60 | ok = 0; 61 | } 62 | } 63 | } catch (e) { 64 | ok = 0; 65 | } 66 | } 67 | 68 | socket.send([opcode, ok, 0, ...byte_string(otpCode)]); 69 | }; 70 | -------------------------------------------------------------------------------- /src/game_server/events/onRegionUpdate.ts: -------------------------------------------------------------------------------- 1 | import type { IGameSocket } from "../game_socket.js"; 2 | import { 3 | SendRegionUserOut, 4 | SendRegionNpcOut, 5 | SendRegionUserIn, 6 | SendRegionNpcIn, 7 | RegionInCase, 8 | } from "../functions/sendRegionInOut.js"; 9 | import { BuildUserDetail } from "../functions/buildUserDetail.js"; 10 | import { 11 | RSessionMap, 12 | RegionQuery, 13 | RegionQueryNPC, 14 | RNPCMap, 15 | type IRegionNPC, 16 | } from "../region.js"; 17 | 18 | export function OnRegionUpdate(socket: IGameSocket) { 19 | if (!socket.ingame) return; 20 | let oldSessions: number[] = Object.keys(socket.visiblePlayers); 21 | let oldNPCSessions: number[] = Object.keys(socket.visibleNPCs); 22 | let newSessions: number[] = []; 23 | let newNPCSessions: number[] = []; 24 | 25 | for (let userSocket of RegionQuery(socket)) { 26 | newSessions.push(userSocket.session); 27 | } 28 | 29 | for (let npc of RegionQueryNPC(socket)) { 30 | newNPCSessions.push(npc.uuid); 31 | } 32 | 33 | for (let oldSession of oldSessions) { 34 | if (!newSessions.find((x) => x == oldSession)) { 35 | SendRegionUserOut(socket, oldSession); 36 | } 37 | } 38 | 39 | for (let oldNpcSession of oldNPCSessions) { 40 | if (!newNPCSessions.find((x) => x == oldNpcSession)) { 41 | SendRegionNpcOut(socket, oldNpcSession); 42 | } 43 | } 44 | 45 | let cache; 46 | 47 | for (let newSession of newSessions) { 48 | if (!oldSessions.find((x) => x == newSession)) { 49 | let userSocket = RSessionMap[newSession]; 50 | 51 | if (userSocket) { 52 | if (!cache) { 53 | cache = BuildUserDetail(socket); 54 | } 55 | 56 | SendRegionUserIn( 57 | userSocket, 58 | socket.session, 59 | RegionInCase.NORMAL, 60 | false, 61 | cache 62 | ); 63 | } 64 | } 65 | } 66 | 67 | for (let newNPCSession of newNPCSessions) { 68 | if (!oldNPCSessions.find((x) => x == newNPCSession)) { 69 | let npc: IRegionNPC = RNPCMap[newNPCSession]; 70 | 71 | if (npc) { 72 | SendRegionNpcIn(socket, npc.npc); 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/game_server/functions/sendLevelChange.ts: -------------------------------------------------------------------------------- 1 | import type { IGameSocket } from "../game_socket.js"; 2 | import { SendAbility } from "./sendAbility.js"; 3 | import { RegionSend } from "../region.js"; 4 | import { short, long, int } from "../../core/utils/unit.js"; 5 | import { GetLevelUp } from "./getLevelUp.js"; 6 | 7 | export function SendLevelChange(socket: IGameSocket, newLevel: number) { 8 | if (newLevel < 1 || newLevel > 83) return; 9 | 10 | let c = socket.character; 11 | let currentLevel = c.level; 12 | if (currentLevel == newLevel) return; 13 | 14 | let statTotal = 300 + (newLevel - 1) * 3; // each level gives 3 stat point 15 | let skillTotal = (newLevel - 9) * 2; 16 | 17 | if (newLevel > 60) { 18 | statTotal += 2 * (newLevel - 60); // after 60 level each level gives 5 so we increment "+2" 19 | } 20 | 21 | if (newLevel > currentLevel) { 22 | let { statStr, statHp, statDex, statMp, statInt, statRemaining } = c; 23 | let userStatTotal = 24 | statStr + statHp + statDex + statMp + statInt + statRemaining; 25 | 26 | let { 27 | skillPointCat1, 28 | skillPointCat2, 29 | skillPointCat3, 30 | skillPointMaster, 31 | skillPointFree, 32 | } = c; 33 | let userSkillTotal = 34 | skillPointCat1 + 35 | skillPointCat2 + 36 | skillPointCat3 + 37 | skillPointMaster + 38 | skillPointFree; 39 | 40 | let statGain = Math.max(0, statTotal - userStatTotal); 41 | let skillGain = Math.max(0, skillTotal - userSkillTotal); 42 | 43 | c.statRemaining += statGain; 44 | c.skillPointFree += skillGain; 45 | } 46 | 47 | c.level = newLevel; 48 | 49 | SendAbility(socket, false); 50 | 51 | let v = socket.variables; 52 | 53 | c.hp = v.maxHp; // level up so give hp 54 | c.mp = v.maxMp; // level up so give mp 55 | 56 | RegionSend(socket, [ 57 | 0x1b, // Level change 58 | ...short(socket.session), 59 | newLevel, 60 | ...short(c.statRemaining), 61 | c.skillPointFree, 62 | ...long(GetLevelUp(newLevel)), 63 | ...long(c.exp), 64 | ...short(v.maxHp || 0), 65 | ...short(c.hp), 66 | ...short(v.maxMp || 0), 67 | ...short(c.mp), 68 | ...int(v.maxWeight), 69 | ...int(v.itemWeight), 70 | ]); 71 | } 72 | -------------------------------------------------------------------------------- /docs/c2gs_opcode_order.md: -------------------------------------------------------------------------------- 1 | ```js 2 | "FORMAT:" AA 55 LEN[0] LEN[1] {DATA} 55 AA 3 | 4 | 2B FF FF // request client exe version and encryption 5 | 01 04 00 74 65 73 74 07 00 47 57 56 55 56 49 30 00 00 00 01 00 01 00 00 00 // user/pass (string 04 00 74 65 73 74 account name, string 07 00 47 57 56 55 56 49 30 hashed password, we dont know the rest bytes) 6 | 9F 01 // load game (request queue to login) 7 | 0C 01 // character list request 8 | 04 04 00 74 65 73 74 04 00 74 65 73 74 01 15 // pick user / character name 9 | 6A 02 // check the items that waiting to give, if there is an item to give, give it 10 | 73 02 03 02 // rental request really don't know what it is, what it does, probably check item rental time thing 11 | 41 00 AA 51 04 42 00 // speed hack thing 12 | 72 06 29 FA CE 56 02 00 00 00 // hack tool request, we clearly don't know what it is 13 | 6B // server index request 14 | 0D 01 04 74 65 73 74 // game start with (01) and character name as byte string (04 74 65 73 74) 15 | 3C 41 // request top 10 knights 16 | 64 03 89 13 00 00 // quest thing 17 | 49 01 // friend list request | note response is (49 02 ...data) 18 | 87 00 00 // helmet visibility data (first 00 is helmet second 00 is cospre visibility) 0 means don't hide 19 | 3C 22 // request knight ally list 20 | 79 02 // request skill data 21 | 6A 05 01 // store process no need to handle 22 | 6A 06 01 // request unread letter count 23 | 0D 02 04 74 65 73 74 // game start with (02) now we did login (really :D, data will flow to us) 24 | 98 01 // user info request (this will send massive data) 25 | 26 | 09 00 00 // send chracter direction (00 00 short direction data) 27 | 09 05 00 // another sample of direction 28 | 06 EA 1F FE 10 2F 00 00 00 00 EA 1F FE 10 2F 00 // send chracter position (EA 1F) tx (FE 10) tz (2F 00) ty (00 00) speed (00) echo (EA 1F) nx (FE 10) nz (2F 00) ny (00 00) {t=target, n=now} 29 | 06 F0 1F 41 11 2F 00 2D 00 01 EA 1F FE 10 2F 00 // all of them are position send, more samples = better 30 | 06 E9 1F F6 10 2F 00 F1 FF 01 EB 1F 0C 11 2F 00 31 | 06 EA 1F 0A 11 2F 00 00 00 00 EA 1F 0A 11 2F 00 32 | 33 | 79 01 00 00 // skill data save request 34 | 97 01 03 00 00 23 00 00 00 1E 0A 1E 00 1E 00 50 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0A 00 0A 00 00 00 00 00 00 00 // genie save options request 35 | ``` -------------------------------------------------------------------------------- /src/game_server/endpoints/LOGIN.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue, string } from "../../core/utils/unit.js"; 4 | import { Account } from "../../core/database/models/index.js"; 5 | import { UserMap } from "../shared.js"; 6 | 7 | export const LOGIN: IGameEndpoint = async function ( 8 | socket: IGameSocket, 9 | body: Queue, 10 | opcode: number 11 | ) { 12 | let sessionCode = body.string(); 13 | let password = body.string(); 14 | 15 | if (socket.user) { 16 | return; // don't allow this request if user already login 17 | } 18 | 19 | if (sessionCode.length != 30 || password.length > 28) { 20 | console.log( 21 | "[GAME] Invalid account (%s) access from %s", 22 | sessionCode, 23 | socket.remoteAddress 24 | ); 25 | socket.terminate(); 26 | return; 27 | } 28 | 29 | let user = await Account.findOne({ 30 | _id: sessionCode.substring(6, 30), 31 | }).exec(); 32 | 33 | if (!user || user.session != sessionCode || user.password != password) { 34 | console.log( 35 | "[GAME] Invalid account (%s) access from %s", 36 | sessionCode, 37 | socket.remoteAddress 38 | ); 39 | socket.terminate(); 40 | return; 41 | } 42 | 43 | let activeSocket = UserMap[user.account]; 44 | 45 | socket.setTimeout(10 * 60 * 1000); // 10 mins 46 | 47 | if (activeSocket && activeSocket != socket) { 48 | activeSocket.send([ 49 | 0x10, 50 | 7, 51 | activeSocket.user.nation, 52 | 0, 53 | 0, 54 | 0, 55 | ...string( 56 | "[SERVER] Hesabiniza " + 57 | socket.remoteAddress + 58 | " ip adresinden yeni bir baglanti yapildi!", 59 | "ascii" 60 | ), 61 | ]); 62 | 63 | await new Promise((r) => setTimeout(r, 100)); 64 | await activeSocket.terminate("another login request"); 65 | } 66 | 67 | socket.user = user; 68 | UserMap[user.account] = socket; 69 | 70 | console.log( 71 | "[GAME] Account connected (%s) from %s", 72 | user.account, 73 | socket.remoteAddress 74 | ); 75 | 76 | socket.send([ 77 | opcode, 78 | user.characters.length == 0 ? 0 : user.nation & 0xff, // if there is no character available, then allow user to change nation 79 | ]); 80 | }; 81 | -------------------------------------------------------------------------------- /src/game_server/functions/buildUserDetail.ts: -------------------------------------------------------------------------------- 1 | import type { IGameSocket } from "../game_socket.js"; 2 | import { byte_string, short, int } from "../../core/utils/unit.js"; 3 | import { ItemSlot } from "../var/item_slot.js"; 4 | 5 | const UserDetailSlots = [ 6 | ItemSlot.BREAST, 7 | ItemSlot.LEG, 8 | ItemSlot.HEAD, 9 | ItemSlot.GLOVE, 10 | ItemSlot.FOOT, 11 | ItemSlot.SHOULDER, 12 | ItemSlot.RIGHTHAND, 13 | ItemSlot.LEFTHAND, 14 | ItemSlot.CWING, 15 | ItemSlot.CHELMET, 16 | ItemSlot.CLEFT, 17 | ItemSlot.CRIGHT, 18 | ItemSlot.CTOP, 19 | ItemSlot.FAIRY, 20 | ItemSlot.FAIRY, 21 | ]; 22 | 23 | export function BuildUserDetail(socket: IGameSocket): number[] { 24 | const result: number[] = []; 25 | let uu = socket.user; 26 | let uc = socket.character; 27 | let uv = socket.variables; 28 | 29 | result.push(...byte_string(uc.name)); 30 | result.push(...short(uu.nation)); 31 | result.push(...short(-1)); // clan Id 32 | result.push(0); // fame 33 | result.push(0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0, 0, 0, 0, 0, 0); // clan_details.. 34 | result.push(uc.level); 35 | result.push(uc.race); 36 | result.push(...short(uc.klass)); 37 | result.push(...short(uc.x * 10)); 38 | result.push(...short(uc.z * 10)); 39 | result.push(...short(uc.y * 10)); 40 | result.push(uc.face); 41 | result.push(...int(uc.hair)); 42 | result.push(uv.hptype || 1); 43 | result.push(...int(uv.abnormalType || 1)); 44 | result.push(0); // need party 45 | result.push(uc.gm ? 0 : 1); 46 | result.push(0); // party leader? 47 | result.push(0); // invisibility state 48 | result.push(0); // teamcolor 49 | result.push(uv.isHelmetHiding || 0); // helmet hiding 50 | result.push(uv.isCospreHiding || 0); // cospre hiding 51 | result.push(...short(uc.direction)); 52 | result.push(0); // chicken? 53 | result.push(uc.rank); 54 | result.push(0, 0); 55 | result.push(0xff, 0xff); // np rank 56 | 57 | for (let slot of UserDetailSlots) { 58 | let item = uc.items[slot]; 59 | 60 | if (item) { 61 | result.push(...int(item.id), ...short(item.durability), item.flag); 62 | } else { 63 | result.push(0, 0, 0, 0, 0, 0, 0); 64 | } 65 | } 66 | 67 | result.push(uc.zone); 68 | result.push(0xff, 0xff, 0, 0, 0, 0, 0, 0, 0, 0 /* genie */); //? 69 | result.push(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1); //? 70 | 71 | return result; 72 | } 73 | -------------------------------------------------------------------------------- /src/game_server/endpoints/KNIGHT.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue } from "../../core/utils/unit.js"; 4 | 5 | export const KNIGHT: IGameEndpoint = async function ( 6 | socket: IGameSocket, 7 | body: Queue, 8 | opcode: number 9 | ) { 10 | let subOpcode = body.byte(); 11 | 12 | switch (subOpcode) { 13 | case KnightCodes.KNIGHTS_TOP10: 14 | // TODO: this is dummy, implement it later 15 | return socket.send([ 16 | opcode, 17 | KnightCodes.KNIGHTS_TOP10, 18 | 0, 19 | 0, 20 | ...[].concat( 21 | ...Array(10) 22 | .fill(0) 23 | .map((x, i) => [0xff, 0xff, 0, 0, 0xff, 0xff, i > 4 ? i - 5 : i, 0]) 24 | ), 25 | ]); 26 | default: 27 | //handle rest 28 | break; 29 | } 30 | }; 31 | 32 | export enum KnightCodes { 33 | KNIGHTS_CREATE = 0x01, // clan creation 34 | KNIGHTS_JOIN = 0x02, // joining a clan 35 | KNIGHTS_WITHDRAW = 0x03, // leaving a clan 36 | KNIGHTS_REMOVE = 0x04, // removing a clan member 37 | KNIGHTS_DESTROY = 0x05, // disbanding a clan 38 | KNIGHTS_ADMIT = 0x06, 39 | KNIGHTS_REJECT = 0x07, 40 | KNIGHTS_PUNISH = 0x08, 41 | KNIGHTS_CHIEF = 0x09, 42 | KNIGHTS_VICECHIEF = 0x0a, 43 | KNIGHTS_OFFICER = 0x0b, 44 | KNIGHTS_ALLLIST_REQ = 0x0c, 45 | KNIGHTS_MEMBER_REQ = 0x0d, 46 | KNIGHTS_CURRENT_REQ = 0x0e, 47 | KNIGHTS_STASH = 0x0f, 48 | KNIGHTS_MODIFY_FAME = 0x10, 49 | KNIGHTS_JOIN_REQ = 0x11, 50 | KNIGHTS_LIST_REQ = 0x12, 51 | 52 | KNIGHTS_WAR_ANSWER = 0x14, 53 | KNIGHTS_WAR_SURRENDER = 0x15, 54 | 55 | KNIGHTS_MARK_VERSION_REQ = 0x19, 56 | KNIGHTS_MARK_REGISTER = 0x1a, 57 | KNIGHTS_CAPE_NPC = 0x1b, 58 | KNIGHTS_ALLY_CREATE = 0x1c, 59 | KNIGHTS_ALLY_REQ = 0x1d, 60 | KNIGHTS_ALLY_INSERT = 0x1e, 61 | KNIGHTS_ALLY_REMOVE = 0x1f, 62 | KNIGHTS_ALLY_PUNISH = 0x20, 63 | KNIGHTS_ALLY_LIST = 0x22, 64 | 65 | KNIGHTS_MARK_REQ = 0x23, 66 | KNIGHTS_UPDATE = 0x24, 67 | KNIGHTS_MARK_REGION_REQ = 0x25, 68 | 69 | KNIGHTS_UPDATE_GRADE = 0x30, 70 | KNIGHTS_POINT_REQ = 0x3b, 71 | KNIGHTS_POINT_METHOD = 0x3c, 72 | KNIGHTS_DONATE_POINTS = 0x3d, 73 | KNIGHTS_HANDOVER_VICECHIEF_LIST = 0x3e, 74 | KNIGHTS_HANDOVER_REQ = 0x3f, 75 | 76 | KNIGHTS_DONATION_LIST = 0x40, 77 | KNIGHTS_TOP10 = 0x41, 78 | KNIGHTS_HANDOVER = 0x4f, 79 | } 80 | -------------------------------------------------------------------------------- /src/game_server/endpoints/_INTERNAL_QUERY.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue, short, string } from "../../core/utils/unit.js"; 4 | import crypto from "crypto"; 5 | import config from "config"; 6 | import { RUserMap } from "../region.js"; 7 | import { UserMap } from "../shared.js"; 8 | 9 | const internalCommunicationSecret = ( 10 | config.get("gameServer.internalCommunicationSecret") 11 | ); 12 | // TODO: DESTROY THIS FILE, IMPLEMENT BETTER WAY like IPC or redis subscription 13 | 14 | export const _INTERNAL_QUERY: IGameEndpoint = async function ( 15 | socket: IGameSocket, 16 | body: Queue, 17 | opcode: number 18 | ) { 19 | let hash = body.sub(20); 20 | 21 | if (hash.length != 20) { 22 | socket.terminate("internal_query_access!!!! invalid hash length"); 23 | return; 24 | } 25 | 26 | if ( 27 | Buffer.from(hash).toString("hex") != 28 | crypto 29 | .createHmac("sha1", internalCommunicationSecret) 30 | .update(Buffer.from(body.array())) 31 | .digest("hex") 32 | ) { 33 | socket.terminate("internal_query_access!!!! invalid hash"); 34 | return; 35 | } 36 | 37 | let subOpcode = body.byte(); 38 | let result = null; 39 | 40 | if (subOpcode == 1) { 41 | // get user count 42 | let users = Object.keys(RUserMap); 43 | result = [subOpcode, ...short(users.length)]; 44 | } else if (subOpcode == 2) { 45 | // check online 46 | let requestedAccount = body.string(); 47 | 48 | result = [subOpcode, +!!UserMap[requestedAccount]]; 49 | } else if (subOpcode == 3) { 50 | // terminate user 51 | let requestedAccount = body.string(); 52 | let remoteAddress = body.string(); 53 | let um = UserMap[requestedAccount]; 54 | result = [subOpcode, +!!um]; 55 | 56 | if (um) { 57 | um.send([ 58 | 0x10, 59 | 7, 60 | um.user.nation, 61 | 0, 62 | 0, 63 | 0, 64 | ...string( 65 | "[SERVER] Hesabiniza " + 66 | remoteAddress + 67 | " ip adresinden yeni bir baglanti yapildi!", 68 | "ascii" 69 | ), 70 | ]); 71 | 72 | await new Promise((r) => setTimeout(r, 100)); 73 | await um.terminate("another login request"); 74 | } 75 | } else { 76 | result = [0xff]; 77 | } 78 | 79 | socket.send([opcode, ...result]); 80 | }; 81 | -------------------------------------------------------------------------------- /src/core/database/utils/csv_loader.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "mongoose"; 2 | import csv from "csv-streamify"; 3 | import unzip from "extract-zip"; 4 | import fs from "fs"; 5 | import path from "path"; 6 | import { fileURLToPath } from "url"; 7 | 8 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 9 | 10 | export async function CSVLoader( 11 | file: string, 12 | transfer: object, 13 | expected: number, 14 | model: Model 15 | ): Promise { 16 | if (await model.findOne({}).exec()) { 17 | return false; 18 | } 19 | 20 | let dataPath = path.resolve(__dirname, "../../../../data/"); 21 | let zipPath = path.resolve(dataPath, file + ".zip"); 22 | let csvPath = path.resolve(dataPath, file + ".csv"); 23 | 24 | return await new Promise(async (resolve) => { 25 | await unzip(zipPath, { 26 | dir: dataPath, 27 | }); 28 | 29 | console.log("[CSV] " + file + ".zip unzipped"); 30 | 31 | let arr = []; 32 | const parser = csv({ columns: true, newline: "\r\n" }); 33 | parser.on("data", (data) => { 34 | var obj = {}; 35 | 36 | validObj(obj, data, transfer); 37 | 38 | arr.push(new model(obj)); 39 | }); 40 | 41 | fs.createReadStream(csvPath).pipe(parser); 42 | 43 | (async function () { 44 | let total = 0; 45 | while (arr.length == 0) await delay(500); 46 | 47 | while (arr.length) { 48 | parser.pause(); 49 | await model.insertMany(arr); 50 | total += arr.length; 51 | console.log( 52 | "[CSV] " + file + " patch sent %d status: %f %", 53 | total, 54 | (((total / expected) * 1000) | 0) / 10 55 | ); 56 | arr = []; 57 | parser.resume(); 58 | await delay(500); 59 | } 60 | 61 | console.log("[CSV] " + file + ".csv completed"); 62 | 63 | fs.unlink(csvPath, function () { 64 | console.log("[CSV] " + file + ".csv removed"); 65 | 66 | resolve(true); 67 | }); 68 | })(); 69 | }); 70 | } 71 | 72 | const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); 73 | 74 | var validAdd = (obj, data, field, newField) => { 75 | var val = data[field].trim(); 76 | 77 | if (!(val == "" || val == "0" || val == "NULL")) { 78 | obj[newField] = val; 79 | } 80 | }; 81 | 82 | function validObj(obj, data, cont) { 83 | for (var key in cont) { 84 | validAdd(obj, data, key, cont[key]); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/core/database/utils/csv_reader.ts: -------------------------------------------------------------------------------- 1 | import csv from "csv-streamify"; 2 | import unzip from "extract-zip"; 3 | import fs from "fs"; 4 | import path from "path"; 5 | import { fileURLToPath } from "url"; 6 | 7 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 8 | 9 | export async function CSVReader( 10 | file: string, 11 | transfer: object, 12 | expected: number 13 | ): Promise { 14 | let dataPath = path.resolve(__dirname, "../../../../data/"); 15 | let zipPath = path.resolve(dataPath, file + ".zip"); 16 | let csvPath = path.resolve(dataPath, file + ".csv"); 17 | 18 | return new Promise(async (resolve) => { 19 | await unzip(zipPath, { 20 | dir: dataPath, 21 | }); 22 | 23 | console.log("[CSV] " + file + ".zip unzipped for reading"); 24 | 25 | let arr = []; 26 | let patch = []; 27 | const parser = csv({ columns: true, newline: "\r\n" }); 28 | parser.on("data", (data) => { 29 | var obj = {}; 30 | 31 | validObj(obj, data, transfer); 32 | 33 | arr.push(obj); 34 | }); 35 | 36 | fs.createReadStream(csvPath).pipe(parser); 37 | 38 | (async function () { 39 | let total = 0; 40 | while (arr.length == 0) await delay(500); 41 | 42 | while (arr.length) { 43 | parser.pause(); 44 | 45 | popAndPushArray(arr, patch); 46 | 47 | total += arr.length; 48 | console.log( 49 | "[CSV] " + file + " patch read %d status: %f %", 50 | total, 51 | (((total / expected) * 1000) | 0) / 10 52 | ); 53 | arr = []; 54 | parser.resume(); 55 | await delay(500); 56 | } 57 | 58 | console.log("[CSV] " + file + ".csv read completed"); 59 | 60 | fs.unlink(csvPath, function () { 61 | console.log("[CSV] " + file + ".csv removed"); 62 | 63 | resolve(patch); 64 | }); 65 | })(); 66 | }); 67 | } 68 | 69 | const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); 70 | 71 | const popAndPushArray = (from: any[], to: any[]) => { 72 | while (from.length) { 73 | to.push(from.pop()); 74 | } 75 | }; 76 | 77 | var validAdd = (obj, data, field, newField) => { 78 | var val = data[field].trim(); 79 | 80 | if (!(val == "" || val == "0" || val == "NULL")) { 81 | obj[newField] = val; 82 | } 83 | }; 84 | 85 | function validObj(obj, data, cont) { 86 | for (var key in cont) { 87 | validAdd(obj, data, key, cont[key]); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/web/server.ts: -------------------------------------------------------------------------------- 1 | import config from "config"; 2 | import fastify from "fastify"; 3 | import { 4 | avgNPC, 5 | avgGC, 6 | avgDrop, 7 | tick, 8 | } from "../game_server/events/onServerTick.js"; 9 | import { GameServer } from "../game_server/server.js"; 10 | import { UserMap, CharacterMap } from "../game_server/shared.js"; 11 | import { NPCUUID } from "../game_server/ai_system/uuid.js"; 12 | import { Item, PrepareItems } from "../core/database/models/item.js"; 13 | import { SendItem } from "../game_server/functions/sendItem.js"; 14 | 15 | export async function WebServer() { 16 | const app = fastify({ 17 | logger: true, 18 | }); 19 | 20 | app.get("/stats", async (request, reply) => ({ 21 | connected: { 22 | npc: NPCUUID.reservedSize(), 23 | session: (await GameServer()).params.idPool.reservedSize(), 24 | logon: Object.keys(UserMap).length, 25 | ingame: Object.keys(CharacterMap).length, 26 | }, 27 | tick: { 28 | current: tick, 29 | gc: { 30 | avg: avgGC.avg(), 31 | values: avgGC.values().slice(0, 5), 32 | }, 33 | npc: { 34 | avg: avgNPC.avg(), 35 | values: avgNPC.values().slice(0, 5), 36 | }, 37 | drop: { 38 | avg: avgDrop.avg(), 39 | values: avgDrop.values().slice(0, 5), 40 | }, 41 | }, 42 | })); 43 | 44 | app.get("/users", async (request, reply) => Object.keys(CharacterMap)); 45 | 46 | app.get<{ 47 | Params: { 48 | id: string; 49 | }; 50 | }>("/users/:id", async (request, reply) => { 51 | let socket = CharacterMap[request.params.id]; 52 | 53 | if (socket) { 54 | return socket.character.toJSON(); 55 | } else { 56 | reply.code(404); 57 | return null; 58 | } 59 | }); 60 | 61 | app.get<{ 62 | Params: { 63 | id: string; 64 | itemId: number; 65 | }; 66 | }>("/users/:id/give-item/:itemId", async (request, reply) => { 67 | let socket = CharacterMap[request.params.id]; 68 | 69 | if (socket) { 70 | await PrepareItems([+request.params.itemId]); 71 | const result = SendItem(socket, request.params.itemId, 1); 72 | 73 | return result; 74 | } else { 75 | reply.code(404); 76 | return null; 77 | } 78 | }); 79 | 80 | console.log( 81 | "[WEB] Server started! (%s)", 82 | await app.listen({ 83 | host: config.get("web.host"), 84 | port: config.get("web.port"), 85 | }) 86 | ); 87 | 88 | return app; 89 | } 90 | -------------------------------------------------------------------------------- /src/game_server/functions/getDamage.ts: -------------------------------------------------------------------------------- 1 | import type { IGameSocket } from "../game_socket.js"; 2 | import type { INPCInstance } from "../ai_system/declare.js"; 3 | 4 | export function GetDamageNPC( 5 | socket: IGameSocket, 6 | npc: INPCInstance, 7 | skill?, 8 | preview? 9 | ) { 10 | if (!npc || npc.hp == 0) return -1; 11 | let model = npc.npc; 12 | 13 | let ac = model.ac || 0; 14 | let v = socket.variables; 15 | let ap = v.totalHit || 0; 16 | 17 | if (socket.character.gm && socket.variables.saitama) { 18 | return 30000; 19 | } 20 | 21 | if (skill) { 22 | // TODO: do here 23 | } else { 24 | if (GetHitRate(v.totalHitRate / (model.evadeRate || 1)) == "fail") { 25 | return 0; // lol unlucky shit 26 | } 27 | 28 | let damage = (ap * 200) / (ac + 240); 29 | damage = 0.85 * damage + 0.3 * damage * Math.random(); 30 | 31 | // TODO: Magic damage thing 32 | return Math.max(0, Math.min(32000, damage | 0)); 33 | } 34 | } 35 | 36 | export function GetHitRate(rate: number) { 37 | let rand = Math.random(); 38 | 39 | if (rate >= 5) { 40 | if (rand < 0.35) return "great"; 41 | if (rand < 0.75) return "success"; 42 | if (rand < 0.98) return "normal"; 43 | } else if (rate >= 3) { 44 | if (rand < 0.25) return "great"; 45 | if (rand < 0.6) return "success"; 46 | if (rand < 0.96) return "normal"; 47 | } else if (rate >= 2) { 48 | if (rand < 0.2) return "great"; 49 | if (rand < 0.5) return "success"; 50 | if (rand < 0.94) return "normal"; 51 | } else if (rate >= 1.25) { 52 | if (rand < 0.15) return "great"; 53 | if (rand < 0.4) return "success"; 54 | if (rand < 0.92) return "normal"; 55 | } else if (rate >= 0.8) { 56 | if (rand < 0.1) return "great"; 57 | if (rand < 0.3) return "success"; 58 | if (rand < 0.9) return "normal"; 59 | } else if (rate >= 0.5) { 60 | if (rand < 0.08) return "great"; 61 | if (rand < 0.25) return "success"; 62 | if (rand < 0.8) return "normal"; 63 | } else if (rate >= 0.33) { 64 | if (rand < 0.06) return "great"; 65 | if (rand < 0.2) return "success"; 66 | if (rand < 0.7) return "normal"; 67 | } else if (rate >= 0.2) { 68 | if (rand < 0.04) return "great"; 69 | if (rand < 0.15) return "success"; 70 | if (rand < 0.6) return "normal"; 71 | } else { 72 | if (rand < 0.02) return "great"; 73 | if (rand < 0.1) return "success"; 74 | if (rand < 0.5) return "normal"; 75 | } 76 | 77 | return "fail"; 78 | } 79 | -------------------------------------------------------------------------------- /src/game_server/functions/sendChatMessage.ts: -------------------------------------------------------------------------------- 1 | import type { IGameSocket } from "../game_socket.js"; 2 | import { short, byte_string, string } from "../../core/utils/unit.js"; 3 | import { RegionSend } from "../region.js"; 4 | 5 | export function SendMessageToPlayer( 6 | socket: IGameSocket, 7 | type: ChatMessageType, 8 | sender: string, 9 | message: string, 10 | nation?: number, 11 | session?: number 12 | ) { 13 | if (!nation) nation = socket.user.nation; 14 | if (!session) session = socket.session; 15 | 16 | socket.send([ 17 | 0x10, 18 | type, 19 | nation, 20 | ...short(session), 21 | ...byte_string(sender), 22 | ...string(message, "ascii"), 23 | ]); 24 | } 25 | 26 | export function SendMessageToPlayerFromPlayer( 27 | socket: IGameSocket, 28 | fromSocket: IGameSocket, 29 | type: ChatMessageType, 30 | message: string 31 | ) { 32 | socket.send([ 33 | 0x10, 34 | type, 35 | fromSocket.user.nation, 36 | ...short(fromSocket.session), 37 | ...byte_string(fromSocket.character.name), 38 | ...string(message, "ascii"), 39 | ]); 40 | } 41 | 42 | export function SendPlayerMessageToRegion( 43 | socket: IGameSocket, 44 | message: string 45 | ) { 46 | let nation = socket.user.nation || 0; 47 | let type = socket.character.gm ? ChatMessageType.GM : ChatMessageType.GENERAL; 48 | 49 | RegionSend(socket, [ 50 | 0x10, 51 | type, 52 | nation, 53 | ...short(socket.session & 0xffff), 54 | ...byte_string(socket.character.name), 55 | ...string(message, "ascii"), 56 | ]); 57 | } 58 | 59 | export enum ChatMessageType { 60 | GENERAL = 1, 61 | PRIVATE = 2, 62 | PARTY = 3, 63 | FORCE = 4, 64 | SHOUT = 5, 65 | KNIGHTS = 6, 66 | PUBLIC = 7, 67 | WAR_SYSTEM = 8, 68 | PERMANENT = 9, 69 | END_PERMANENT = 10, 70 | MONUMENT_NOTICE = 11, 71 | GM = 12, 72 | COMMAND = 13, 73 | MERCHANT = 14, 74 | ALLIANCE = 15, 75 | ANNOUNCEMENT = 17, 76 | SEEKING_PARTY = 19, 77 | GM_INFO = 21, // info window : "Level: 0, UserCount:16649" (NOTE: Think this is the missing overhead info (probably in form of a command (with args)) 78 | COMMAND_PM = 22, // Commander Chat PM?? 79 | CLAN_NOTICE = 24, 80 | KROWAZ_NOTICE = 25, 81 | DEATH_NOTICE = 26, 82 | CHAOS_STONE_ENEMY_NOTICE = 27, // The enemy has destroyed the Chaos stone something (Red text, middle of screen) 83 | CHAOS_STONE_NOTICE = 28, 84 | ANNOUNCEMENT_WHITE = 29, // what's it used for? 85 | CHATROM = 33, 86 | NOAH_KNIGHTS = 34, 87 | } 88 | -------------------------------------------------------------------------------- /src/game_server/endpoints/FRIEND.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue, short, string, byte_string } from "../../core/utils/unit.js"; 4 | import { CharacterMap } from "../shared.js"; 5 | 6 | export const FRIEND: IGameEndpoint = async function ( 7 | socket: IGameSocket, 8 | body: Queue, 9 | opcode: number 10 | ) { 11 | let subOpcode = body.byte(); 12 | let c = socket.character; 13 | let map = CharacterMap; 14 | 15 | switch (subOpcode) { 16 | case 1: // REQUEST FRIEND LIST 17 | return socket.send([ 18 | opcode, 19 | 2, 20 | ...short(c.friends.length), 21 | ...[].concat( 22 | ...c.friends.map((friend) => [ 23 | ...string(friend), 24 | ...short(map[friend] ? map[friend].session : -1), 25 | map[friend] ? (map[friend].variables.inParty ? 3 : 1) : 0, 26 | ]) 27 | ), 28 | ]); 29 | 30 | case 3: { 31 | // ADD FRIEND 32 | let newFriendName = body.string(); 33 | let socket = map[newFriendName]; 34 | if ( 35 | !socket || 36 | !socket.character || 37 | socket.character.name != newFriendName 38 | ) { 39 | return socket.send([ 40 | opcode, 41 | 3, 42 | 1, 43 | ...byte_string(newFriendName), 44 | 0xff, 45 | 0xff, 46 | 0, 47 | ]); 48 | } 49 | 50 | c.friends.push(newFriendName); 51 | c.markModified("friends"); 52 | 53 | return socket.send([ 54 | opcode, 55 | 3, 56 | 0, 57 | ...byte_string(newFriendName), 58 | ...short(socket.session), 59 | socket.variables.inParty ? 3 : 1, 60 | ]); 61 | } 62 | case 4: { 63 | // REMOVE FRIEND 64 | let friendName = body.string(); 65 | let index = c.friends.findIndex((friend) => friend == friendName); 66 | if (index == -1) { 67 | return socket.send([ 68 | opcode, 69 | 4, 70 | 1, 71 | ...byte_string(friendName), 72 | 0xff, 73 | 0xff, 74 | 0, 75 | ]); 76 | } 77 | 78 | c.friends.splice(index, 1); 79 | c.markModified("friends"); 80 | 81 | return socket.send([ 82 | opcode, 83 | 4, 84 | 0, 85 | ...byte_string(friendName), 86 | 0xff, 87 | 0xff, 88 | 0, 89 | ]); 90 | } 91 | } 92 | }; 93 | -------------------------------------------------------------------------------- /src/game_server/game_socket.ts: -------------------------------------------------------------------------------- 1 | import type { IKOSocket } from "../core/server.js"; 2 | import type { 3 | ICharacter, 4 | IAccount, 5 | IWarehouse, 6 | } from "../core/database/models/index.js"; 7 | import type { HydratedDocument } from "mongoose"; 8 | 9 | export interface IGameSocket extends IKOSocket { 10 | character: HydratedDocument; 11 | user: HydratedDocument; 12 | variables: IVariables; 13 | warehouse: HydratedDocument; 14 | ingame: boolean; 15 | 16 | visiblePlayers: IVisiblePlayers; 17 | visibleNPCs: IVisibleNPCs; 18 | 19 | target: number; // player's target 20 | targetType: "user" | "npc" | "notarget"; 21 | } 22 | 23 | export interface IVisiblePlayers { 24 | [session: number]: boolean; 25 | } 26 | 27 | export interface IVisibleNPCs { 28 | [uuid: number]: boolean; 29 | } 30 | 31 | export interface IVariables { 32 | expiryBlink: NodeJS.Timeout; // setTimeout id 33 | 34 | acAmount: number; 35 | totalHit: number; 36 | statBonus: number[]; 37 | statBuffBonus: number[]; 38 | APClassBonusAmount: number[]; 39 | ACClassBonusAmount: number[]; 40 | addWeaponDamage: number; 41 | weaponsDisabled: boolean; 42 | haveBow: boolean; 43 | maxWeightAmount: number; 44 | maxWeightBonus: number; 45 | maxWeight: number; 46 | APBonusAmount: number; 47 | totalAc: number; 48 | itemAc: number; 49 | acPercent: number; 50 | hitRateAmount: number; 51 | avoidRateAmount: number; 52 | totalHitRate: number; 53 | totalEvasionRate: number; 54 | itemHitRate: number; 55 | itemEvasionRate: number; 56 | resistanceBonus: number; 57 | itemMaxHp: number; 58 | itemMaxMp: number; 59 | itemWeight: number; 60 | fireR: number; 61 | coldR: number; 62 | lightningR: number; 63 | magicR: number; 64 | curseR: number; 65 | poisonR: number; 66 | daggerR: number; 67 | swordR: number; 68 | axeR: number; 69 | maceR: number; 70 | spearR: number; 71 | bowR: number; 72 | addArmourAc: number; 73 | pctArmourAc: number; 74 | itemExpGainAmount: number; 75 | itemNoahGainAmount: number; 76 | itemNPBonus: number; 77 | equipedItemBonus: { [itemId: number]: { [type: number]: number } }; 78 | maxMpAmount: number; 79 | maxHp: number; 80 | maxMp: number; 81 | maxHpAmount: number; 82 | hptype: number; 83 | abnormalType: number; 84 | inParty: boolean; 85 | 86 | isHelmetHiding: number; 87 | isCospreHiding: number; 88 | 89 | saitama: boolean; // gm feature to hit 30k always 90 | lastHome: number; // lastHomeTimestamp 91 | chatTo: string; // private message target user name 92 | } 93 | -------------------------------------------------------------------------------- /src/game_server/endpoints/ATTACK.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue, short } from "../../core/utils/unit.js"; 4 | import { ItemSlot } from "../var/item_slot.js"; 5 | import { RNPCMap, RegionSend } from "../region.js"; 6 | import { GetDamageNPC } from "../functions/getDamage.js"; 7 | import { SendTargetHP } from "../functions/sendTargetHP.js"; 8 | import { OnNPCDead } from "../events/onNPCDead.js"; 9 | 10 | export const ATTACK: IGameEndpoint = async function ( 11 | socket: IGameSocket, 12 | body: Queue, 13 | opcode: number 14 | ) { 15 | let type = body.byte(); 16 | let result = body.byte(); 17 | let tid = body.short(); 18 | let delayTime = body.short(); 19 | let distance = body.short(); 20 | 21 | let klass = socket.character.strKlass; 22 | let rightHand = socket.character.items[ItemSlot.RIGHTHAND]; 23 | 24 | if (rightHand && klass != "mage") { 25 | // FIXME: do controls 26 | } else if (delayTime < 100) return; 27 | 28 | // TODO: User cant attack more than 1 in 1 second.. 29 | 30 | try { 31 | if (tid >= 10000) { 32 | // npc 33 | let npcRegion = RNPCMap[tid]; 34 | 35 | if (npcRegion) { 36 | let npc = npcRegion.npc; 37 | 38 | let damage = GetDamageNPC(socket, npc); 39 | 40 | let oldHP = npc.hp; 41 | npc.hp = Math.max(0, npc.hp - damage); 42 | let realDamage = oldHP - npc.hp; 43 | 44 | if (realDamage > 0) { 45 | // TODO: Item wore etc.. 46 | } 47 | 48 | if (!npc.damagedBy) { 49 | npc.damagedBy = {}; 50 | } 51 | 52 | let session = socket.session; 53 | npc.damagedBy[session] = (npc.damagedBy[session] || 0) + realDamage; 54 | 55 | let status = 1; 56 | if (npc.hp == 0) { 57 | // dead lol 58 | status = 2; 59 | } 60 | 61 | SendTargetHP(socket, 0, -damage); 62 | RegionSend(socket, [ 63 | opcode, // ATTACK, 64 | type, 65 | status, // ok 66 | ...short(socket.session), 67 | ...short(tid), 68 | ]); 69 | 70 | if (status == 2) { 71 | OnNPCDead(npc); 72 | } 73 | return; 74 | } 75 | } 76 | 77 | if (tid < 10000) { 78 | // user 79 | } 80 | 81 | throw new Error("miss"); 82 | } catch (e) { 83 | RegionSend(socket, [ 84 | opcode, // ATTACK, 85 | type, 86 | 0, // failed 87 | ...short(socket.session), 88 | ...short(tid), 89 | ]); 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /src/core/database/models/set_item.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, Document } from "mongoose"; 2 | 3 | export interface ISetItem extends Document { 4 | id: number; 5 | name: string; 6 | acBonus: number; 7 | hpBonus: number; 8 | mpBonus: number; 9 | statstrBonus: number; 10 | stathpBonus: number; 11 | statdexBonus: number; 12 | statintBonus: number; 13 | statmpBonus: number; 14 | flameResistance: number; 15 | glacierResistance: number; 16 | lightningResistance: number; 17 | poisonResistance: number; 18 | magicResistance: number; 19 | curseResistance: number; 20 | XPBonusPercent: number; 21 | coinBonusPercent: number; 22 | APBonusPercent: number; 23 | APBonusClassType: number; 24 | APBonusClassPercent: number; 25 | ACBonusClassType: number; 26 | ACBonusClassPercent: number; 27 | maxWeightBonus: number; 28 | NPBonus: number; 29 | unk10: number; 30 | unk11: number; 31 | unk12: number; 32 | unk13: number; 33 | unk14: number; 34 | unk15: number; 35 | unk16: number; 36 | unk17: number; 37 | unk18: number; 38 | unk19: number; 39 | unk20: number; 40 | unk21: number; 41 | } 42 | 43 | export const SetItemSchema = new Schema( 44 | { 45 | id: { type: Number, index: true }, 46 | name: { type: String }, 47 | acBonus: { type: Number }, 48 | hpBonus: { type: Number }, 49 | mpBonus: { type: Number }, 50 | statstrBonus: { type: Number }, 51 | stathpBonus: { type: Number }, 52 | statdexBonus: { type: Number }, 53 | statintBonus: { type: Number }, 54 | statmpBonus: { type: Number }, 55 | flameResistance: { type: Number }, 56 | glacierResistance: { type: Number }, 57 | lightningResistance: { type: Number }, 58 | poisonResistance: { type: Number }, 59 | magicResistance: { type: Number }, 60 | curseResistance: { type: Number }, 61 | XPBonusPercent: { type: Number }, 62 | coinBonusPercent: { type: Number }, 63 | APBonusPercent: { type: Number }, 64 | APBonusClassType: { type: Number }, 65 | APBonusClassPercent: { type: Number }, 66 | ACBonusClassType: { type: Number }, 67 | ACBonusClassPercent: { type: Number }, 68 | maxWeightBonus: { type: Number }, 69 | NPBonus: { type: Number }, 70 | unk10: { type: Number }, 71 | unk11: { type: Number }, 72 | unk12: { type: Number }, 73 | unk13: { type: Number }, 74 | unk14: { type: Number }, 75 | unk15: { type: Number }, 76 | unk16: { type: Number }, 77 | unk17: { type: Number }, 78 | unk18: { type: Number }, 79 | unk19: { type: Number }, 80 | unk20: { type: Number }, 81 | unk21: { type: Number }, 82 | }, 83 | { timestamps: false } 84 | ); 85 | 86 | export const SetItem = model("SetItem", SetItemSchema, "set_items"); 87 | -------------------------------------------------------------------------------- /src/client/login-client.ts: -------------------------------------------------------------------------------- 1 | import { KOClientFactory, type IKOClientSocket } from "../core/client.js"; 2 | import { string, Queue } from "../core/utils/unit.js"; 3 | import { PasswordHash } from "../core/utils/password_hash.js"; 4 | import { AuthenticationCode } from "../login_server/endpoints/LOGIN_REQ.js"; 5 | 6 | export async function ConnectLoginClient( 7 | ip: string, 8 | port: number, 9 | user: string, 10 | password: string 11 | ) { 12 | let connection: IKOClientSocket; 13 | let data: Queue; 14 | 15 | try { 16 | connection = await KOClientFactory({ ip, port, name: "login-client" }); 17 | 18 | data = await connection.sendAndWait( 19 | [0xf3, ...string(user), ...string(PasswordHash(password))], 20 | 0xf3 21 | ); 22 | data.skip(2); 23 | 24 | let resultCode = data.byte(); 25 | let premiumHours = data.short(); 26 | let sessionCode = data.string(); 27 | 28 | if (resultCode == AuthenticationCode.NOT_FOUND) 29 | throw new Error("Account does not exist"); 30 | if (resultCode == AuthenticationCode.INVALID) 31 | throw new Error("Invalid credentials"); 32 | if (resultCode == AuthenticationCode.BANNED) 33 | throw new Error("Account banned"); 34 | if (resultCode == AuthenticationCode.IN_GAME) 35 | throw new Error("Account already in game"); 36 | if (resultCode == AuthenticationCode.OTP) 37 | throw new Error("OTP login is required"); 38 | if (resultCode == AuthenticationCode.OTP_BAN) 39 | throw new Error("OTP is locked for login."); 40 | if (resultCode != AuthenticationCode.SUCCESS) 41 | throw new Error("Unknown errror ocurred"); 42 | 43 | /** TODO: ENCRYPTION */ 44 | 45 | data = await connection.sendAndWait([0xf6], 0xf6); 46 | 47 | let newsHeader = data.string(); 48 | let newsData = data.string(); 49 | 50 | data = await connection.sendAndWait([0xf5, 1, 0], 0xf5); 51 | data.skip(2); // dummy 1, 0 52 | 53 | let serverCount = data.byte(); 54 | let servers = []; 55 | 56 | for (let i = 0; i < serverCount; i++) { 57 | servers.push({ 58 | ip: data.string(), 59 | lanip: data.string(), 60 | name: data.string(), 61 | onlineCount: data.short(), 62 | serverId: data.short(), 63 | groupId: data.short(), 64 | userPremiumLimit: data.short(), 65 | userFreeLimit: data.short(), 66 | unkwn: data.byte(), 67 | karusKing: data.string(), 68 | karusNotice: data.string(), 69 | elmoradKing: data.string(), 70 | elmoradNotice: data.string(), 71 | }); 72 | } 73 | 74 | return { 75 | servers, 76 | news: { 77 | header: newsHeader, 78 | message: newsData, 79 | }, 80 | premiumHours, 81 | sessionCode, 82 | }; 83 | } finally { 84 | if (connection) { 85 | connection.terminate(); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/game_server/endpoints/ALLCHAR_INFO_REQ.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue, int, short, string } from "../../core/utils/unit.js"; 4 | import { ItemSlot } from "../var/item_slot.js"; 5 | import { Character } from "../../core/database/models/index.js"; 6 | 7 | const CHARACTER_ITEM_SLOTS = [ 8 | ItemSlot.HEAD, 9 | ItemSlot.BREAST, 10 | ItemSlot.SHOULDER, 11 | ItemSlot.RIGHTHAND, 12 | ItemSlot.LEFTHAND, 13 | ItemSlot.LEG, 14 | ItemSlot.GLOVE, 15 | ItemSlot.FOOT, 16 | ]; 17 | 18 | export const ALLCHAR_INFO_REQ: IGameEndpoint = async function ( 19 | socket: IGameSocket, 20 | body: Queue, 21 | opcode: number 22 | ) { 23 | if (!socket.user.characters) { 24 | socket.user.characters = []; 25 | } 26 | 27 | var characters = []; 28 | 29 | for (var i = 0; i < 4; i++) { 30 | let characterName = socket.user.characters[i]; 31 | 32 | let data = { 33 | name: "", 34 | hair: 0, 35 | klass: 0, 36 | race: 0, 37 | level: 0, 38 | face: 0, 39 | zone: 0, 40 | items: null, 41 | }; 42 | 43 | if (characterName) { 44 | let character = await Character.findOne({ 45 | name: characterName, 46 | }) 47 | .select([ 48 | "name", 49 | "race", 50 | "klass", 51 | "hair", 52 | "level", 53 | "face", 54 | "zone", 55 | "items", 56 | ]) 57 | .exec(); 58 | 59 | if (character) { 60 | data.name = character.name; 61 | data.hair = character.hair; 62 | data.klass = character.klass; 63 | data.race = character.race; 64 | data.level = character.level; 65 | data.face = character.face; 66 | data.zone = character.zone; 67 | 68 | data.items = []; 69 | 70 | for (let slot of CHARACTER_ITEM_SLOTS) { 71 | let item = character.items[slot]; 72 | 73 | if (item) { 74 | data.items.push(...int(item.id), ...short(item.durability)); 75 | } else { 76 | data.items.push(0, 0, 0, 0, 0, 0); 77 | } 78 | } 79 | } 80 | } 81 | 82 | if (!data.items) { 83 | data.items = Array(6 * 8).fill(0); // (uint + short) * 8 item 84 | } 85 | 86 | characters.push(data); 87 | } 88 | 89 | socket.send([ 90 | opcode, 91 | 1, 92 | 1, 93 | ...[].concat( 94 | ...characters.map((character) => [ 95 | ...string(character.name), 96 | character.race, 97 | ...short(character.klass), 98 | character.level, 99 | character.face, 100 | ...int(character.hair), 101 | character.zone, 102 | ...character.items, 103 | ]) 104 | ), 105 | ]); 106 | }; 107 | -------------------------------------------------------------------------------- /src/game_server/server.ts: -------------------------------------------------------------------------------- 1 | import config from "config"; 2 | import { KOServerFactory, type IKOServer } from "../core/server.js"; 3 | import { Queue } from "../core/utils/unit.js"; 4 | import { GameEndpointCodes, GameEndpoint } from "./endpoint.js"; 5 | import type { IGameSocket } from "./game_socket.js"; 6 | import { UserMap, CharacterMap, LoadSetItems } from "./shared.js"; 7 | import { RegionRemove } from "./region.js"; 8 | import { AISystemStart } from "./ai_system/start.js"; 9 | import { OnUserExit } from "./events/onUserExit.js"; 10 | import { Database } from "../core/database/index.js"; 11 | import { RedisConnect } from "../core/redis/index.js"; 12 | 13 | let gameServerCache: IKOServer = null; 14 | export async function GameServer() { 15 | if (gameServerCache) return gameServerCache; 16 | 17 | console.log("[SERVER] Game server is going to start..."); 18 | await Database(); 19 | await RedisConnect(); 20 | 21 | await LoadSetItems(); // load set items to memory 22 | 23 | await AISystemStart(); 24 | 25 | return (gameServerCache = ( 26 | await KOServerFactory({ 27 | ip: config.get("gameServer.ip"), 28 | ports: config.get("gameServer.ports"), 29 | timeout: 5000, 30 | 31 | onConnect: (socket: IGameSocket) => {}, 32 | 33 | onDisconnect: async (socket: IGameSocket) => { 34 | try { 35 | await OnUserExit(socket); 36 | } catch (e) { 37 | console.error("[ERROR] Saving failed for user! " + socket.session); 38 | console.error(e); 39 | } 40 | 41 | RegionRemove(socket); 42 | 43 | if (socket.user) { 44 | if (UserMap[socket.user.account]) { 45 | delete UserMap[socket.user.account]; 46 | } 47 | 48 | if (socket.character) { 49 | console.log( 50 | "[GAME] Character disconnected (%s) from %s", 51 | socket.character.name, 52 | socket.remoteAddress 53 | ); 54 | } else { 55 | console.log( 56 | "[GAME] Account disconnected (%s) from %s", 57 | socket.user.account, 58 | socket.remoteAddress 59 | ); 60 | } 61 | } 62 | 63 | if (socket.character) { 64 | if (CharacterMap[socket.character.name]) { 65 | delete CharacterMap[socket.character.name]; 66 | } 67 | } 68 | }, 69 | 70 | onData: async (socket: IGameSocket, data: Buffer) => { 71 | let body = Queue.from(data); 72 | let opcode = body.byte(); 73 | let endpointCode = GameEndpointCodes[opcode]; 74 | if (!endpointCode) { 75 | return console.log( 76 | "[SERVER] Unknown opcode received! (0x" + 77 | (opcode ? opcode.toString(16).padStart(2, "0") : "00") + 78 | ") | " + 79 | body 80 | .array() 81 | .map((x) => (x < 16 ? "0" : "") + x.toString(16).toUpperCase()) 82 | .join(" ") 83 | ); 84 | } 85 | 86 | let endpoint = GameEndpoint(endpointCode); 87 | if (!endpoint) { 88 | return console.log( 89 | "[SERVER] Missing endpoint definition on endpoints/index.ts file! name: " + 90 | endpointCode 91 | ); 92 | } 93 | 94 | await endpoint(socket, body, opcode); 95 | }, 96 | }) 97 | )[0]); 98 | } 99 | -------------------------------------------------------------------------------- /src/game_server/endpoints/SEL_CHAR.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { 3 | IGameSocket, 4 | IVariables, 5 | IVisiblePlayers, 6 | } from "../game_socket.js"; 7 | import { Queue, short } from "../../core/utils/unit.js"; 8 | import { 9 | Character, 10 | Warehouse, 11 | PrepareItems, 12 | } from "../../core/database/models/index.js"; 13 | import { CharacterMap } from "../shared.js"; 14 | import { SendAbility } from "../functions/sendAbility.js"; 15 | 16 | export const SEL_CHAR: IGameEndpoint = async function ( 17 | socket: IGameSocket, 18 | body: Queue, 19 | opcode: number 20 | ) { 21 | let session = body.string(); 22 | let charName = body.string(); 23 | let init = body.byte(); 24 | 25 | let user = socket.user; 26 | if (!user || user.session != session) { 27 | return socket.terminate("illegal access to another account"); 28 | } 29 | 30 | if (user.banned) { 31 | return socket.terminate("banned account"); 32 | } 33 | 34 | if (!charName) return socket.send([opcode, 0]); 35 | 36 | let character = await Character.findOne({ 37 | name: charName, 38 | }).exec(); 39 | 40 | if (!character) return socket.send([opcode, 0]); 41 | 42 | if (!socket.user.characters.find((x) => x == charName)) { 43 | return socket.send([opcode, 0]); 44 | } 45 | 46 | let activeSocket = CharacterMap[charName]; 47 | if (activeSocket && activeSocket != socket) { 48 | CharacterMap[charName].terminate("another character select request"); 49 | delete CharacterMap[charName]; 50 | 51 | return socket.send([opcode, 0]); 52 | } 53 | 54 | socket.character = character; 55 | CharacterMap[charName] = socket; 56 | 57 | socket.warehouse = await Warehouse.findOne({ 58 | _id: socket.user.warehouse, 59 | }).exec(); 60 | 61 | /*if (user.nation == 'KARUS') { 62 | if (character.zone == 12) character.zone = 11; 63 | else if (character.zone == 28) character.zone = 18; 64 | 65 | await character.save(); 66 | } else if (user.nation == 'ELMORAD') { 67 | if (character.zone == 11) character.zone = 12; 68 | else if (character.zone == 18) character.zone = 28; 69 | 70 | await character.save(); 71 | }*/ 72 | 73 | /** TODO: IF WAR IS OVER MOVE CHAR TO CORRECT ZONE */ 74 | 75 | if (character.level > 83) { 76 | return socket.terminate("character level cannot be more than 83"); 77 | } 78 | 79 | /* let starterQuest = character.quests.find(quest => quest.id == questIds.STARTER_SEED_QUEST); 80 | if (!starterQuest) { 81 | starterQuest = { 82 | id: questIds.STARTER_SEED_QUEST, 83 | state: 1 84 | }; 85 | character.quest.push(starterQuest); 86 | await character.save() 87 | }*/ 88 | 89 | // load item details 90 | let items = character.items; 91 | let itemIds: number[] = []; 92 | 93 | for (let i = 0; i < items.length; i++) { 94 | let item = items[i]; 95 | if (!item) continue; 96 | itemIds.push(item.id); 97 | } 98 | 99 | await PrepareItems(itemIds); 100 | 101 | /* TODO: RENTAL*/ 102 | // TODO: clan stuff 103 | 104 | socket.variables = {}; 105 | socket.visiblePlayers = {}; 106 | socket.visibleNPCs = {}; 107 | 108 | SendAbility(socket); 109 | 110 | console.log( 111 | "[GAME] Character connected (%s) from %s", 112 | character.name, 113 | socket.remoteAddress 114 | ); 115 | 116 | socket.send([ 117 | opcode, 118 | 1, 119 | character.zone, 120 | ...short(character.x * 10), 121 | ...short(character.z * 10), 122 | ...short(character.y * 10), 123 | 1, 124 | ]); 125 | }; 126 | -------------------------------------------------------------------------------- /src/game_server/functions/sendMyInfo.ts: -------------------------------------------------------------------------------- 1 | import type { IGameSocket } from "../game_socket.js"; 2 | import { ItemSlot } from "../var/item_slot.js"; 3 | import { int, short, byte_string, long } from "../../core/utils/unit.js"; 4 | import { GetLevelUp } from "./getLevelUp.js"; 5 | 6 | export function SendMyInfo(socket: IGameSocket) { 7 | let u = socket.user; 8 | let c = socket.character; 9 | let v = socket.variables; 10 | 11 | let nation = u.nation; 12 | 13 | let items: number[] = []; 14 | 15 | for (let i = 0; i < 75; i++) { 16 | let item = c.items[i]; 17 | 18 | if (i == ItemSlot.BAG2) { 19 | item = c.items[ItemSlot.FAIRY]; 20 | } else if (i == ItemSlot.FAIRY) { 21 | item = c.items[ItemSlot.BAG2]; 22 | } 23 | 24 | if (item) { 25 | items.push( 26 | ...int(item.id), 27 | ...short(item.durability), 28 | ...short(item.amount), 29 | item.flag, 30 | 0, 31 | 0, // rental time, 32 | 0, 33 | 0, 34 | 0, 35 | 0, // TODO: seal serial data 36 | 0, 37 | 0, 38 | 0, 39 | 0 // unix expiration time 40 | ); 41 | } else { 42 | items.push(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); 43 | } 44 | } 45 | 46 | socket.send([ 47 | 0x0e, // MYINFO 48 | ...short(socket.session & 0xffff), 49 | ...byte_string(c.name), 50 | ...short(c.x * 10), 51 | ...short(c.z * 10), 52 | ...short(c.y * 10), 53 | nation, 54 | c.race, 55 | ...short(c.klass), 56 | c.face, 57 | ...int(c.hair), 58 | c.rank, 59 | c.title, 60 | 1, 61 | 1, 62 | c.level, 63 | ...short(c.statRemaining), 64 | ...long(GetLevelUp(c.level)), // maxExp 65 | ...long(c.exp), // exp 66 | ...int(c.loyalty), 67 | ...int(c.loyaltyMonthly), 68 | ...short(c.clan), 69 | c.fame, 70 | 0, 71 | 0, 72 | 0, 73 | 0, 74 | 0, 75 | 0, 76 | 0, 77 | 0, 78 | 0xff, 79 | 0xff, 80 | 0, 81 | 0, 82 | 0, 83 | 0, // TODO: clan details 84 | 2, 85 | 3, 86 | 4, 87 | 5, // unknown 88 | ...short(v.maxHp || 0), 89 | ...short(c.hp), 90 | ...short(v.maxMp || 0), 91 | ...short(c.mp), 92 | ...int(v.maxWeight), 93 | ...int(v.itemWeight), 94 | c.statStr, 95 | v.statBonus[0] + v.statBuffBonus[0], 96 | c.statHp, 97 | v.statBonus[1] + v.statBuffBonus[1], 98 | c.statDex, 99 | v.statBonus[2] + v.statBuffBonus[2], 100 | c.statInt, 101 | v.statBonus[3] + v.statBuffBonus[3], 102 | c.statMp, 103 | v.statBonus[4] + v.statBuffBonus[4], 104 | ...short(v.totalHit), 105 | ...short(v.totalAc), 106 | v.fireR, 107 | v.coldR, 108 | v.lightningR, 109 | v.magicR, 110 | v.curseR, 111 | v.poisonR, 112 | ...int(c.money), 113 | c.gm ? 0 : 1, // 0=GM 1=USER 114 | 0xff, 115 | 0xff, 116 | 0, 117 | 0, 118 | 0, 119 | 0, 120 | 0, 121 | 0, 122 | 0, 123 | 0, 124 | 0, // TODO: skill 125 | ...items, 126 | socket.user.premium ? 1 : 0, 127 | 0, // TODO: premium type 128 | 0, 129 | 0, // TODO: premium time, 130 | 0, // chicken flag 131 | 0, 132 | 0, 133 | 0, 134 | 0, // TODO: manner point 135 | 0, 136 | 0, 137 | 0, 138 | 0, 139 | 0, 140 | 0, // military camp 141 | 0, 142 | 0, // genie time 143 | c.rebirth, 144 | 0, 145 | 0, 146 | 0, 147 | 0, 148 | 0, // rebirth stats 149 | 0, 150 | 0, 151 | 0, 152 | 0, 153 | 0, 154 | 0, 155 | 0, 156 | 0, // unknown 157 | 0, 158 | 0, // cover title 159 | 0, 160 | 0, // skill title 161 | 0, 162 | 0, 163 | 0, 164 | 0, // return symbol // TODO: LATER 165 | 0, 166 | 0, 167 | ]); 168 | } 169 | -------------------------------------------------------------------------------- /src/game_server/events/onNPCTick.ts: -------------------------------------------------------------------------------- 1 | import type { INPCInstance } from "../ai_system/declare.js"; 2 | import { NPCMap } from "../ai_system/uuid.js"; 3 | import { 4 | RegionUpdateNPC, 5 | RegionQueryUsersByNpc, 6 | RegionSendByNpc, 7 | } from "../region.js"; 8 | import { SendRegionNpcIn } from "../functions/sendRegionInOut.js"; 9 | import { short } from "../../core/utils/unit.js"; 10 | import { WaitNextTick } from "../../core/utils/general.js"; 11 | 12 | let lock = 0; 13 | export async function OnNPCTick() { 14 | try { 15 | if (lock) return; // probably never happens but we should put 16 | lock = 1; 17 | 18 | let now = Date.now(); 19 | let ioSafe = 0; 20 | 21 | for (let uuid in NPCMap) { 22 | if (++ioSafe > 100) { 23 | ioSafe = 0; 24 | await WaitNextTick(); 25 | } 26 | 27 | let instance: INPCInstance = NPCMap[uuid]; 28 | 29 | let diff = now - instance.timestamp; 30 | 31 | if (instance.wait > diff) { 32 | continue; 33 | } 34 | 35 | instance.timestamp = now; 36 | 37 | let npc = instance.npc; 38 | let spawn = instance.spawn; 39 | if (instance.status == "init") { 40 | instance.status = "standing"; 41 | instance.zone = spawn.zone; 42 | 43 | instance.x = random(spawn.leftX, spawn.rightX); 44 | instance.z = random(spawn.topZ, spawn.bottomZ); 45 | 46 | instance.direction = spawn.direction; 47 | instance.hp = npc.hp; 48 | instance.mp = npc.mp; 49 | instance.maxMp = npc.mp; 50 | instance.maxHp = npc.hp; 51 | instance.initialized = true; 52 | delete instance.damagedBy; 53 | 54 | instance.agressive = !(npc.actType == 1 || npc.actType == 2); 55 | 56 | RegionUpdateNPC(instance); 57 | for (let socket of RegionQueryUsersByNpc(instance)) { 58 | SendRegionNpcIn(socket, instance); 59 | } 60 | 61 | instance.wait = npc.standtime; 62 | } else if (instance.status == "standing") { 63 | if (CanMove(instance)) { 64 | instance.tx = random(spawn.leftX, spawn.rightX); 65 | instance.tz = random(spawn.topZ, spawn.bottomZ); 66 | 67 | let distance = TargetPointDistance(instance); 68 | 69 | if (distance != 0) { 70 | instance.status = "moving"; 71 | instance.wait = npc.speed; 72 | } 73 | } 74 | } else if (instance.status == "moving") { 75 | let distance = TargetPointDistance(instance); 76 | if (distance < npc.speed1) { 77 | instance.x = instance.tx; 78 | instance.z = instance.tz; 79 | 80 | instance.wait = npc.standtime; 81 | instance.status = "standing"; 82 | } else { 83 | let ds = npc.speed1 / distance; 84 | let dx = (instance.tx - instance.x) * ds; 85 | let dz = (instance.tz - instance.z) * ds; 86 | 87 | instance.x += dx; 88 | instance.z += dz; 89 | } 90 | 91 | RegionUpdateNPC(instance); 92 | SendNPCMoveToRegion(instance); 93 | } else if (instance.status == "dead") { 94 | instance.status = "init"; 95 | instance.wait = 0; 96 | } 97 | } 98 | } finally { 99 | lock = 0; 100 | } 101 | } 102 | 103 | function CanMove(instance: INPCInstance) { 104 | let npc = instance.npc; 105 | if (!npc.actType) return false; 106 | if (npc.actType == 4) return false; 107 | 108 | return true; 109 | } 110 | 111 | function TargetPointDistance(i: INPCInstance) { 112 | let nx = i.tx - i.x; 113 | let nz = i.tz - i.z; 114 | return Math.sqrt(nx * nx + nz * nz); 115 | } 116 | 117 | function SendNPCMoveToRegion(instance: INPCInstance) { 118 | // speed is ms we convert it unit/sec 119 | RegionSendByNpc(instance, [ 120 | 0x0b, 121 | 1, 122 | ...short(instance.uuid), 123 | ...short(instance.x * 10), 124 | ...short(instance.z * 10), 125 | 0, 126 | 0, 127 | ...short(instance.npc.speed / 100), 128 | ]); 129 | } 130 | 131 | function random(min, max) { 132 | return Math.random() * (max - min) + min; 133 | } 134 | -------------------------------------------------------------------------------- /src/game_server/endpoints/NEW_CHAR.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue } from "../../core/utils/unit.js"; 4 | import { Character } from "../../core/database/models/index.js"; 5 | 6 | export const NEW_CHAR: IGameEndpoint = async function ( 7 | socket: IGameSocket, 8 | body: Queue, 9 | opcode: number 10 | ) { 11 | let index = body.byte(); 12 | let name = body.string(); 13 | let race = body.byte(); 14 | let klass = body.short(); 15 | let face = body.byte(); 16 | let hair = body.uint(); 17 | let str = body.byte(); 18 | let hp = body.byte(); 19 | let dex = body.byte(); 20 | let int = body.byte(); 21 | let mp = body.byte(); 22 | 23 | if (index > 3 || index < 0) { 24 | // invalid request 25 | return socket.send([ 26 | opcode, 27 | 1, // NO_MORE 28 | ]); 29 | } 30 | 31 | if (!/^[a-zA-Z0-9]{3,20}$/.test(name)) { 32 | return socket.send([ 33 | opcode, 34 | 5, // NEWCHAR_BAD_NAME 35 | ]); 36 | } 37 | 38 | if (!isKlassValid(klass) || str + hp + dex + int + mp > 300) { 39 | return socket.send([ 40 | opcode, 41 | 2, // NEWCHAR_INVALID_DETAILS 42 | ]); 43 | } 44 | 45 | if (socket.user.characters[index]) { 46 | // you have already created at this index dude 47 | return socket.send([ 48 | opcode, 49 | 2, // NEWCHAR_INVALID_DETAILS 50 | ]); 51 | } 52 | 53 | if (str < 50 || hp < 50 || dex < 50 || int < 50 || mp < 50) { 54 | return socket.send([ 55 | opcode, 56 | 11, // NEWCHAR_STAT_TOO_LOW 57 | ]); 58 | } 59 | 60 | if ( 61 | (socket.user.nation == 1 && race > 10) || 62 | (socket.user.nation == 2 && race < 10) 63 | ) { 64 | return socket.send([ 65 | opcode, 66 | 2, // NEWCHAR_INVALID_DETAILS 67 | ]); 68 | } 69 | 70 | let nameControl = await Character.findOne({ 71 | name, 72 | }).exec(); 73 | 74 | if (nameControl) { 75 | return socket.send([ 76 | opcode, 77 | 3, // NEWCHAR_EXISTS 78 | ]); 79 | } 80 | 81 | var error = false; 82 | try { 83 | let sklass = SimplifyKlass(klass); 84 | 85 | let character = new Character({ 86 | name, 87 | race, 88 | klass, 89 | strKlass: sklass, 90 | hair, 91 | level: 1, 92 | face, 93 | statStr: str, 94 | statHp: hp, 95 | statDex: dex, 96 | statMp: mp, 97 | statInt: int, 98 | money: 10, // 10 noah start 99 | items: Array(75).fill(null), 100 | }); 101 | 102 | await character.save(); 103 | 104 | if (!socket.user.characters) { 105 | socket.user.characters = []; 106 | } 107 | 108 | socket.user.characters[index] = character.name; 109 | socket.user.markModified("characters"); 110 | 111 | await socket.user.save(); 112 | } catch (e) { 113 | error = true; 114 | } 115 | 116 | if (error) { 117 | return socket.send([ 118 | opcode, 119 | 4, // NEWCHAR_DB_ERROR 120 | ]); 121 | } 122 | 123 | socket.send([ 124 | opcode, 125 | 0, // NEWCHAR_SUCCESS 126 | ]); 127 | }; 128 | 129 | function isKlassValid(klass: number) { 130 | if (klass >= 101 && klass <= 115) { 131 | return true; 132 | } 133 | 134 | if (klass >= 201 && klass <= 215) { 135 | return true; 136 | } 137 | 138 | return false; 139 | } 140 | 141 | export function SimplifyKlass(klass: number) { 142 | if (klass >= 100 && klass < 200) { 143 | klass -= 100; 144 | } else if (klass >= 200 && klass < 300) { 145 | klass -= 200; 146 | } else { 147 | return "unknown"; 148 | } 149 | 150 | if (klass == 1 || klass == 5 || klass == 6) { 151 | return "warrior"; 152 | } 153 | 154 | if (klass == 2 || klass == 7 || klass == 8) { 155 | return "rogue"; 156 | } 157 | 158 | if (klass == 3 || klass == 9 || klass == 10) { 159 | return "mage"; 160 | } 161 | 162 | if (klass == 4 || klass == 11 || klass == 12) { 163 | return "priest"; 164 | } 165 | 166 | if (klass == 5 || klass == 13 || klass == 14) { 167 | return "kurian"; 168 | } 169 | 170 | return "unknown"; 171 | } 172 | -------------------------------------------------------------------------------- /src/game_server/endpoints/DROP_TAKE.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue, int, short, byte_string } from "../../core/utils/unit.js"; 4 | import { GetDrop, RemoveDrop } from "../drop.js"; 5 | import { SendNoahChange } from "../functions/sendNoahChange.js"; 6 | import { SendNoahEvent } from "../functions/sendNoahEvent.js"; 7 | import { FindSlotForItem } from "../functions/findSlotForItem.js"; 8 | import { ItemSlot } from "../var/item_slot.js"; 9 | import { SendWeightChange } from "../functions/sendWeightChange.js"; 10 | import { AllSend } from "../region.js"; 11 | import { GenerateItem } from "../functions/generateItem.js"; 12 | import { GetItemDetail } from "../../core/database/models/index.js"; 13 | 14 | export const DROP_TAKE: IGameEndpoint = async function ( 15 | socket: IGameSocket, 16 | body: Queue, 17 | opcode: number 18 | ) { 19 | let dropIndex = body.int(); 20 | let item = body.int(); 21 | 22 | try { 23 | let drop = GetDrop(dropIndex); 24 | if (!drop) throw 1; 25 | 26 | let session = socket.session; 27 | if ( 28 | !drop.owners || 29 | drop.owners.findIndex((owner) => owner == session) == -1 30 | ) 31 | throw 1; 32 | 33 | // TODO: Check drop distance controls etc.. 34 | 35 | let idx = drop.dropped.findIndex((x) => x && x.item == item); 36 | if (idx == -1) throw 1; // probably already picked 37 | 38 | let dropItem = drop.dropped[idx]; 39 | drop.dropped.splice(idx, 1); // take the item from drop 40 | 41 | if (dropItem.item == 900000000) { 42 | // Money drop 43 | SendNoahChange(socket, dropItem.amount, false); // no need to send noah change, we will send with drop_take packet 44 | 45 | socket.send([ 46 | opcode, 47 | 1, // success 48 | ...int(dropIndex), 49 | 0xff, // item position in inventory (nowhere :D) 50 | ...int(dropItem.item), 51 | ...short(dropItem.amount), 52 | ...int(socket.character.money), // see, we sent the current money 53 | ]); 54 | 55 | SendNoahEvent(socket, dropItem.amount); // randomly showing 2x, 5x, 50x.. event 56 | } else { 57 | let c = socket.character; 58 | let v = socket.variables; 59 | let { maxWeight, itemWeight } = v; 60 | 61 | // this part of code has to be sync, we load items when npc dies. calculation all of this can work flawless 62 | let itemDetail = GetItemDetail(dropItem.item); 63 | 64 | let newTotalWeight = 65 | itemWeight + (itemDetail.weight | 0) * dropItem.amount; 66 | if (newTotalWeight > maxWeight) { 67 | return socket.send([ 68 | opcode, 69 | 6, // Weight limit reached.. 70 | ]); 71 | } 72 | 73 | let slot = FindSlotForItem(socket, itemDetail, dropItem.amount); 74 | if (slot < 0) throw 1; 75 | 76 | if (c.items[slot]) { 77 | c.items[slot].amount = Math.min( 78 | 9999, 79 | c.items[slot].amount + dropItem.amount 80 | ); 81 | } else { 82 | c.items[slot] = GenerateItem(itemDetail, dropItem.amount); 83 | } 84 | 85 | let outputItem = c.items[slot]; 86 | 87 | socket.send([ 88 | opcode, 89 | 1, // success 90 | ...int(dropIndex), 91 | slot - ItemSlot.INVENTORY_START, // item position in inventory (nowhere :D) 92 | ...int(outputItem.id), 93 | ...short(outputItem.amount), 94 | ...int(c.money), // see, we sent the current money 95 | ]); 96 | 97 | socket.variables.itemWeight = newTotalWeight; 98 | SendWeightChange(socket); 99 | 100 | if (itemDetail.itemType == 4 && itemDetail.id != 900144023) { 101 | AllSend([ 102 | 0x7d, // LOGOS SHOUT, 103 | 2, 104 | 4, 105 | ...byte_string(c.name), 106 | ...int(itemDetail.id), 107 | ]); 108 | } 109 | } 110 | 111 | if (drop.dropped.length == 0) { 112 | RemoveDrop(dropIndex); 113 | } 114 | } catch (e) { 115 | socket.send([ 116 | opcode, 117 | 0, // Loot Error 118 | ]); 119 | } 120 | }; 121 | -------------------------------------------------------------------------------- /src/core/database/models/npc.ts: -------------------------------------------------------------------------------- 1 | import { Schema, SchemaTypes, model, Document } from "mongoose"; 2 | 3 | export interface ISpawn { 4 | actType?: number; 5 | amount?: number; 6 | bottomZ: number; 7 | leftX: number; 8 | maxx?: number; 9 | maxz?: number; 10 | minx?: number; 11 | minz?: number; 12 | respawnTime?: number; 13 | rightX: number; 14 | specialType?: number; 15 | topZ: number; 16 | trap?: number; 17 | zone: number; 18 | direction: number; 19 | 20 | points?: { x: number; z: number }[]; 21 | } 22 | 23 | export interface IDrop { 24 | item: number; 25 | rate: number; 26 | } 27 | 28 | export interface INpc extends Document { 29 | id: number; 30 | name: string; 31 | pid: number; 32 | size: number; 33 | weapon1: number; 34 | weapon2: number; 35 | group: number; 36 | actType: number; 37 | type: number; 38 | family: number; 39 | rank: number; 40 | title: number; 41 | sellingGroup: number; 42 | level: number; 43 | exp: number; 44 | loyalty: number; 45 | hp: number; 46 | mp: number; 47 | attack: number; 48 | ac: number; 49 | hitRate: number; 50 | evadeRate: number; 51 | damage: number; 52 | attackDelay: number; 53 | speed1: number; 54 | speed2: number; 55 | standtime: number; 56 | magic1: number; 57 | magic2: number; 58 | magic3: number; 59 | fireR: number; 60 | coldR: number; 61 | lightningR: number; 62 | magicR: number; 63 | diseaseR: number; 64 | poisonR: number; 65 | bulk: number; 66 | attackRange: number; 67 | searchRange: number; 68 | tracingRange: number; 69 | money: number; 70 | directAttack: number; 71 | magicAttack: number; 72 | speed: number; 73 | moneyType: number; 74 | 75 | isMonster: boolean; 76 | spawn: ISpawn[]; 77 | drops: IDrop[]; 78 | } 79 | 80 | export const NpcSchema = new Schema( 81 | { 82 | id: { type: Number, index: true }, 83 | name: { type: String }, 84 | pid: { type: Number }, // pictureId 85 | size: { type: Number, default: 0 }, 86 | weapon1: { type: Number, default: 0 }, 87 | weapon2: { type: Number, default: 0 }, 88 | group: { type: Number, default: 0 }, 89 | actType: { type: Number, default: 0 }, 90 | type: { type: Number, default: 0 }, 91 | family: { type: Number, default: 0 }, 92 | rank: { type: Number, default: 0 }, 93 | title: { type: Number, default: 0 }, 94 | sellingGroup: { type: Number, default: 0 }, 95 | level: { type: Number, default: 0 }, 96 | exp: { type: Number, default: 0 }, 97 | loyalty: { type: Number, default: 0 }, 98 | hp: { type: Number, default: 0 }, 99 | mp: { type: Number, default: 0 }, 100 | attack: { type: Number, default: 0 }, 101 | ac: { type: Number, default: 0 }, 102 | hitRate: { type: Number, default: 0 }, 103 | evadeRate: { type: Number, default: 0 }, 104 | damage: { type: Number, default: 0 }, 105 | attackDelay: { type: Number, default: 0 }, 106 | speed1: { type: Number, default: 0 }, 107 | speed2: { type: Number, default: 0 }, 108 | standtime: { type: Number, default: 0 }, 109 | magic1: { type: Number, default: 0 }, 110 | magic2: { type: Number, default: 0 }, 111 | magic3: { type: Number, default: 0 }, 112 | fireR: { type: Number, default: 0 }, 113 | coldR: { type: Number, default: 0 }, 114 | lightningR: { type: Number, default: 0 }, 115 | magicR: { type: Number, default: 0 }, 116 | diseaseR: { type: Number, default: 0 }, 117 | poisonR: { type: Number, default: 0 }, 118 | bulk: { type: Number, default: 0 }, 119 | attackRange: { type: Number, default: 0 }, 120 | searchRange: { type: Number, default: 0 }, 121 | tracingRange: { type: Number, default: 0 }, 122 | money: { type: Number, default: 0 }, 123 | directAttack: { type: Number, default: 0 }, 124 | magicAttack: { type: Number, default: 0 }, 125 | speed: { type: Number, default: 0 }, 126 | moneyType: { type: Number, default: 0 }, 127 | 128 | isMonster: { type: Boolean }, 129 | spawn: [{ type: SchemaTypes.Mixed }], 130 | drops: [{ item: Number, rate: Number }], 131 | }, 132 | { timestamps: false } 133 | ); 134 | 135 | export const Npc = model("Npc", NpcSchema, "npcs"); 136 | -------------------------------------------------------------------------------- /src/game_server/endpoints/SHOPPING_MALL.ts: -------------------------------------------------------------------------------- 1 | import type { IGameEndpoint } from "../endpoint.js"; 2 | import type { IGameSocket } from "../game_socket.js"; 3 | import { Queue, int, short, byte_string } from "../../core/utils/unit.js"; 4 | import { Mail } from "../../core/database/models/index.js"; 5 | import { ItemSlot } from "../var/item_slot.js"; 6 | 7 | const INVENTORY_START = ItemSlot.INVENTORY_START; 8 | const INVENTORY_END = ItemSlot.INVENTORY_END; 9 | 10 | export const SHOPPING_MALL: IGameEndpoint = async function ( 11 | socket: IGameSocket, 12 | body: Queue, 13 | opcode: number 14 | ) { 15 | let subOpcode = body.byte(); 16 | if (!(subOpcode == 1 || subOpcode == 2 || subOpcode == 6)) return; // ignore 17 | 18 | if (subOpcode == 1) { 19 | // STORE_OPEN 20 | // TODO: do later 21 | return socket.send([ 22 | opcode, 23 | subOpcode, 24 | 1, // error code 25 | 1, // free slot 26 | ]); 27 | } 28 | 29 | if (subOpcode == 2) { 30 | // STORE_CLOSE 31 | // TODO: check this later 32 | // idea is check for items in database then send inventory data 33 | 34 | let data: number[] = []; 35 | let items = socket.character.items; 36 | for (var i = INVENTORY_START; i < INVENTORY_END; i++) { 37 | let item = items[i]; 38 | if (item) { 39 | data.push( 40 | ...int(item.id), 41 | ...short(item.durability), 42 | ...short(item.amount), 43 | item.flag, 44 | 0, 45 | 0 46 | ); 47 | } else { 48 | data.push(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); 49 | } 50 | } 51 | 52 | return socket.send([opcode, subOpcode, ...data]); 53 | } 54 | 55 | // STORE_LETTER 56 | let letterOperation = body.byte(); 57 | 58 | if (letterOperation == 1) { 59 | // LETTER_UNREAD 60 | return socket.send([ 61 | opcode, 62 | subOpcode, 63 | letterOperation, 64 | (await Mail.countDocuments({ 65 | character: socket.character.name, 66 | deleted: false, 67 | }).exec()) & 0xff, 68 | ]); 69 | } else if (letterOperation == 2 || letterOperation == 3) { 70 | // LETTER_LIST or HISTORY 71 | let mails = await Mail.find({ 72 | character: socket.character.name, 73 | status: 1, 74 | deleted: letterOperation == 3, 75 | }) 76 | .sort([["createdAt", -1]]) 77 | .limit(letterOperation == 3 ? 20 : 12) 78 | .exec(); 79 | 80 | return socket.send([ 81 | opcode, 82 | subOpcode, 83 | letterOperation, 84 | 1, 85 | mails.length, 86 | ...[].concat( 87 | ...mails.map((mail) => [ 88 | ...int(mail.marker), 89 | mail.status, 90 | ...byte_string(mail.subject), 91 | ...byte_string(mail.sender), 92 | ...int( 93 | mail.createdAt.getFullYear() * 10000 + 94 | (mail.createdAt.getMonth() + 1) * 100 + 95 | mail.createdAt.getDate() 96 | ), 97 | 7 - (Date.now() - mail.createdAt.getTime()) / 1000 / 3600 / 24, 98 | ]) 99 | ), 100 | ]); 101 | } else if (letterOperation == 5) { 102 | // LETTER_READ 103 | let marker = body.int(); 104 | 105 | let mail = await Mail.findOne({ 106 | character: socket.character.name, 107 | marker, 108 | }).exec(); 109 | 110 | if (!mail) { 111 | return socket.send([opcode, subOpcode, letterOperation, 0]); 112 | } 113 | 114 | mail.status = 2; 115 | await mail.save(); 116 | 117 | return socket.send([ 118 | opcode, 119 | subOpcode, 120 | letterOperation, 121 | 1, 122 | ...int(marker), 123 | ...byte_string(mail.message), 124 | ]); 125 | } else if (letterOperation == 6) { 126 | // SEND 127 | // TODO: need to do letter send 128 | } else if (letterOperation == 4) { 129 | // GET ITEM 130 | // TODO: need to do letter get item 131 | } else if (letterOperation == 7) { 132 | // DELETE 133 | let count = body.byte(); 134 | 135 | if (count > 5) { 136 | return socket.send([opcode, subOpcode, letterOperation, -3 & 0xff]); 137 | } 138 | } 139 | 140 | socket.send([ 141 | opcode, 142 | subOpcode, 143 | 0, 144 | 0, 145 | 0, 146 | 0, //unit.int(0) 147 | ]); 148 | }; 149 | -------------------------------------------------------------------------------- /src/game_server/events/onNPCDead.ts: -------------------------------------------------------------------------------- 1 | import type { INPCInstance } from "../ai_system/declare.js"; 2 | import { 3 | RegionRemoveNPC, 4 | RSessionMap, 5 | RegionQueryUsersByNpc, 6 | } from "../region.js"; 7 | import { short, int } from "../../core/utils/unit.js"; 8 | import { SendExperienceChange } from "../functions/sendExperienceChange.js"; 9 | import { ItemDropGroups } from "../var/item_drop_groups.js"; 10 | import { PrepareItems } from "../../core/database/models/index.js"; 11 | import { CreateDrop } from "../drop.js"; 12 | import type { IGameSocket } from "../game_socket.js"; 13 | import { NPCMap, NPCUUID } from "../ai_system/uuid.js"; 14 | 15 | const ARROW_MIN = 391010000; 16 | const ARROW_MAX = 392010000; 17 | 18 | export function OnNPCDead(npc: INPCInstance) { 19 | if (npc.status == "dead") return; 20 | 21 | npc.status = "dead"; 22 | 23 | console.log("[NPC] NPC died (%d)", npc.uuid); 24 | 25 | RegionRemoveNPC(npc); 26 | 27 | const npcDeadPacket = [ 28 | 0x11, // dead 29 | ...short(npc.uuid), 30 | ]; 31 | 32 | for (let s of RegionQueryUsersByNpc(npc)) { 33 | delete s.visibleNPCs[npc.uuid]; 34 | s.send(npcDeadPacket); 35 | } 36 | 37 | if (npc.spawn.respawnTime) { 38 | npc.wait = npc.spawn.respawnTime * 1000; 39 | } else { 40 | delete NPCMap[npc.uuid]; 41 | 42 | setTimeout(function () { 43 | NPCUUID.free(npc.uuid); 44 | }, 2000); 45 | } 46 | 47 | let exp = npc.npc.exp; 48 | let hp = npc.npc.hp; 49 | let greatestDamage: number = 0; 50 | let greatestSession: IGameSocket = null; 51 | for (let session in npc.damagedBy) { 52 | let userSocket = RSessionMap[session]; 53 | 54 | if (userSocket) { 55 | // still online 56 | let damage = npc.damagedBy[session]; 57 | if (greatestDamage < damage) { 58 | greatestDamage = damage; 59 | greatestSession = userSocket; 60 | } 61 | 62 | SendExperienceChange(userSocket, ((exp * damage) / hp) | 0); 63 | } 64 | } 65 | 66 | delete npc.damagedBy; 67 | 68 | if (greatestSession) { 69 | // you earned the drop :) 70 | let dropped = []; 71 | 72 | if (npc.npc.money) { 73 | let money = npc.npc.money * (Math.random() * 0.3 + 0.7); 74 | 75 | if (money > 0) { 76 | dropped.push({ 77 | item: 900000000, 78 | amount: Math.min(32000, money | 0), // amount is short, so we limit the max output 79 | }); 80 | } 81 | } 82 | 83 | if (npc.npc.drops && npc.npc.drops.length > 0) { 84 | for (let drop of npc.npc.drops) { 85 | let idx = drop.item; 86 | let luck = Math.random(); 87 | if (luck > drop.rate) { 88 | // unlucky 89 | continue; 90 | } 91 | 92 | if (idx > 100000000) { 93 | // regular item so we can give it directly 94 | let amount = 1; 95 | 96 | if (idx >= ARROW_MIN && idx <= ARROW_MAX) { 97 | amount = 20 + ((Math.random() * 30) | 0); 98 | } 99 | 100 | dropped.push({ 101 | item: idx, 102 | amount: amount, 103 | }); 104 | } else if (idx < 100) { 105 | // this range is for 106 | } else { 107 | // item has to be in a group, if it isnt than we dont care 108 | let pickItemInGroup: number[] = ItemDropGroups[idx]; 109 | if (pickItemInGroup) { 110 | let picked = 111 | pickItemInGroup[(Math.random() * pickItemInGroup.length) | 0]; 112 | 113 | dropped.push({ 114 | item: picked, 115 | amount: 1, 116 | }); 117 | } 118 | } 119 | } 120 | } 121 | 122 | if (dropped.length) { 123 | let itemIds = dropped.map((x) => x.item).filter((x) => x != 900000000); 124 | 125 | if (itemIds.length > 0) { 126 | PrepareItems(itemIds) 127 | .then(() => { 128 | let wrap = CreateDrop([greatestSession.session], dropped); 129 | 130 | greatestSession.send([ 131 | 0x23, // ITEM_DROP 132 | ...short(npc.uuid), 133 | ...int(wrap), // bundle id 134 | 2, 135 | ]); 136 | }) 137 | .catch(() => {}); 138 | } else { 139 | greatestSession.send([ 140 | 0x23, // ITEM_DROP 141 | ...short(npc.uuid), 142 | ...int(CreateDrop([greatestSession.session], dropped)), // bundle id 143 | 2, 144 | ]); 145 | } 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/core/database/models/character.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, Document } from "mongoose"; 2 | 3 | export interface ICharacterRemovedItem { 4 | id: number; 5 | durability: number; 6 | amount: number; 7 | serial: string; 8 | removedAt: Date; 9 | } 10 | 11 | export interface ICharacterItem { 12 | id: number; 13 | durability: number; 14 | amount: number; 15 | serial: string; 16 | expire?: number; 17 | flag: number; 18 | } 19 | 20 | export interface IQuestItem { 21 | id: number; 22 | state: number; 23 | } 24 | 25 | export interface ICharacter extends Document { 26 | name: string; 27 | race: number; 28 | klass: number; 29 | strKlass: "unknown" | "warrior" | "rogue" | "mage" | "priest" | "kurian"; 30 | hair: number; 31 | rank: number; 32 | title: number; 33 | level: number; 34 | rebirth: number; 35 | exp: number; 36 | loyalty: number; 37 | loyaltyMonthly: number; 38 | face: number; 39 | city: number; 40 | gm: boolean; 41 | 42 | clan: number; 43 | fame: number; 44 | 45 | hp: number; 46 | mp: number; 47 | sp: number; 48 | 49 | statStr: number; 50 | statHp: number; 51 | statDex: number; 52 | statMp: number; 53 | statInt: number; 54 | statRemaining: number; 55 | 56 | money: number; 57 | zone: number; 58 | 59 | x: number; 60 | y: number; 61 | z: number; 62 | direction: number; 63 | 64 | skills: Buffer; 65 | skillPointCat1: number; 66 | skillPointCat2: number; 67 | skillPointCat3: number; 68 | skillPointMaster: number; 69 | skillPointFree: number; 70 | 71 | items: ICharacterItem[]; 72 | removedItems: ICharacterRemovedItem[]; 73 | quests: IQuestItem[]; 74 | friends: string[]; 75 | skillBar: number[]; 76 | genieSettings: Buffer; 77 | } 78 | 79 | export const CharacterSchema = new Schema( 80 | { 81 | name: { type: String, index: true }, 82 | race: { type: Number }, 83 | klass: { type: Number }, 84 | strKlass: { type: String }, 85 | hair: { type: Number }, 86 | rank: { type: Number, default: 0 }, 87 | title: { type: Number, default: 0 }, 88 | level: { type: Number, default: 1 }, 89 | rebirth: { type: Number, default: 0 }, 90 | exp: { type: Number, default: 1 }, 91 | loyalty: { type: Number, default: 101 }, 92 | loyaltyMonthly: { type: Number, default: 0 }, 93 | face: { type: Number }, 94 | city: { type: Number, default: 0 }, 95 | gm: { type: Boolean, default: false }, // char based gm 96 | 97 | clan: { type: Number, default: 0 }, 98 | fame: { type: Number, default: 0 }, 99 | 100 | hp: { type: Number, default: 100 }, 101 | mp: { type: Number, default: 100 }, 102 | sp: { type: Number, default: 100 }, 103 | 104 | statStr: { type: Number }, 105 | statHp: { type: Number }, 106 | statDex: { type: Number }, 107 | statMp: { type: Number }, 108 | statInt: { type: Number }, 109 | statRemaining: { type: Number, default: 0 }, 110 | 111 | money: { type: Number, default: 0 }, 112 | zone: { type: Number, default: 21 }, 113 | 114 | x: { type: Number, default: 817 }, 115 | y: { type: Number, default: 0 }, 116 | z: { type: Number, default: 435 }, 117 | direction: { type: Number, default: 0 }, 118 | 119 | skills: { type: Buffer }, 120 | skillPointCat1: { type: Number, default: 0 }, 121 | skillPointCat2: { type: Number, default: 0 }, 122 | skillPointCat3: { type: Number, default: 0 }, 123 | skillPointMaster: { type: Number, default: 0 }, 124 | skillPointFree: { type: Number, default: 0 }, 125 | 126 | items: [ 127 | { 128 | id: { type: Number }, 129 | durability: { type: Number }, 130 | amount: { type: Number }, 131 | serial: { type: String }, 132 | expire: { type: Number }, 133 | flag: { type: Number }, 134 | }, 135 | ], 136 | removedItems: [ 137 | { 138 | id: { type: Number }, 139 | durability: { type: Number }, 140 | amount: { type: Number }, 141 | serial: { type: String }, 142 | removedAt: { type: Date }, 143 | }, 144 | ], 145 | quests: [ 146 | { 147 | id: { type: Number }, 148 | state: { type: Number }, 149 | }, 150 | ], 151 | 152 | magic: [ 153 | { 154 | id: { type: Number }, 155 | expiry: { type: Date }, 156 | }, 157 | ], 158 | 159 | friends: [String], 160 | skillBar: [Number], 161 | genieSettings: Buffer, 162 | }, 163 | { timestamps: true } 164 | ); 165 | 166 | export const Character = model( 167 | "Character", 168 | CharacterSchema, 169 | "characters" 170 | ); 171 | -------------------------------------------------------------------------------- /src/login_server/endpoints/LOGIN_REQ.ts: -------------------------------------------------------------------------------- 1 | import { Queue, string, configString, short } from "../../core/utils/unit.js"; 2 | import type { ILoginSocket } from "../login_socket.js"; 3 | import type { ILoginEndpoint } from "../endpoint.js"; 4 | import { Account, type IAccount } from "../../core/database/models/index.js"; 5 | import { GenerateOTP } from "../../core/utils/otp.js"; 6 | 7 | export const LOGIN_REQ: ILoginEndpoint = async function ( 8 | socket: ILoginSocket, 9 | body: Queue, 10 | opcode: number 11 | ) { 12 | let accountName = body.string(); 13 | let password = body.string(); 14 | body.skip(1); 15 | let otpCode = body.string(); 16 | 17 | var resultCode = 0; 18 | let account: IAccount; 19 | 20 | try { 21 | if (accountName.length > 20 || password.length > 28) { 22 | throw AuthenticationCode.INVALID; 23 | } 24 | 25 | account = await Account.findOne({ 26 | account: accountName, 27 | }).exec(); 28 | 29 | if (!account) { 30 | console.log( 31 | "[LOGIN] Invalid account (%s) access from %s", 32 | accountName, 33 | socket.remoteAddress 34 | ); 35 | throw AuthenticationCode.NOT_FOUND; 36 | } 37 | 38 | if (account.password != password) { 39 | console.log( 40 | "[LOGIN] Invalid account (%s) access from %s", 41 | account.account, 42 | socket.remoteAddress 43 | ); 44 | throw AuthenticationCode.INVALID; 45 | } 46 | 47 | if (account.banned) { 48 | throw AuthenticationCode.BANNED; 49 | } 50 | 51 | if (account.otp) { 52 | if ( 53 | account.otpLastFail && 54 | account.otpLastFail > new Date(Date.now() - 1000 * 60 * 30) && 55 | account.otpTryCount > 5 56 | ) { 57 | console.log( 58 | "[LOGIN] Invalid account (%s) access from %s (OTP BAN)", 59 | account.account, 60 | socket.remoteAddress 61 | ); 62 | throw AuthenticationCode.OTP_BAN; // special for otp ban 63 | } 64 | 65 | if (!otpCode) throw AuthenticationCode.OTP; 66 | 67 | if (GenerateOTP(account.otpSecret) != otpCode) { 68 | resultCode = AuthenticationCode.OTP; 69 | } else { 70 | resultCode = AuthenticationCode.SUCCESS; 71 | account.session = generateSession() + account._id; 72 | await account.save(); 73 | } 74 | } else { 75 | resultCode = AuthenticationCode.SUCCESS; 76 | account.session = generateSession() + account._id; 77 | await account.save(); 78 | } 79 | } catch (e) { 80 | if (e && typeof e == "number") { 81 | resultCode = e; 82 | } else { 83 | resultCode = AuthenticationCode.ERROR; 84 | } 85 | } 86 | 87 | if (resultCode == AuthenticationCode.SUCCESS) { 88 | let premiumHours = -1; 89 | 90 | if (account.premium) { 91 | premiumHours = 92 | (Date.now() - account.premiumEndsAt.getTime()) / 1000 / 3600; 93 | if (premiumHours < 0) { 94 | premiumHours = -1; 95 | } else { 96 | premiumHours = premiumHours >>> 0; 97 | } 98 | } 99 | 100 | socket.send([ 101 | opcode, 102 | 0, 103 | 0, 104 | 0x01, 105 | ...short(premiumHours), 106 | ...string(account.session), 107 | ]); 108 | 109 | console.log( 110 | "[LOGIN] Account connected (%s) from %s", 111 | account.account, 112 | socket.remoteAddress 113 | ); 114 | } else if (resultCode == AuthenticationCode.BANNED) { 115 | socket.send([ 116 | opcode, 117 | 0, 118 | 0, 119 | 0x04, 120 | 0xff, 121 | 0xff, 122 | ...string(accountName), 123 | ...string(account.bannedMessage), 124 | ]); 125 | } else if (resultCode == AuthenticationCode.OTP_BAN) { 126 | socket.send([ 127 | opcode, 128 | 0, 129 | 0, 130 | 0x04, 131 | 0xff, 132 | 0xff, 133 | ...string(accountName), 134 | ...string("OTP invalid ban. Account is locked for 30 mins."), 135 | ]); 136 | } else { 137 | socket.send([opcode, 0, 0, resultCode, 0xff, 0xff, ...string(accountName)]); 138 | } 139 | }; 140 | 141 | function generateSession(): string { 142 | return Array(3).fill(0).map(generateRandomHexString).join(""); 143 | } 144 | 145 | function generateRandomHexString(): string { 146 | return ((Math.random() * 0xff) | 0).toString(16).padStart(2, "0"); 147 | } 148 | 149 | export enum AuthenticationCode { 150 | SUCCESS = 0x01, 151 | NOT_FOUND = 0x02, 152 | INVALID = 0x03, 153 | BANNED = 0x04, 154 | IN_GAME = 0x05, 155 | ERROR = 0x06, 156 | AGREEMENT = 0x0f, 157 | OTP = 0x10, 158 | OTP_BAN = 0xdd, 159 | FAILED = 0xff, 160 | } 161 | -------------------------------------------------------------------------------- /src/core/utils/unit.ts: -------------------------------------------------------------------------------- 1 | import config from "config"; 2 | import longLib from "long"; 3 | 4 | export function readShort(data: number[] | Buffer, i: number): number { 5 | let sign = data[i + 1] & (1 << 7); 6 | let x = ((data[i + 1] & 0xff) << 8) | (data[i] & 0xff); 7 | if (sign) { 8 | return 0xffff0000 | x; 9 | } 10 | 11 | return x; 12 | } 13 | 14 | export function readInt(data: number[] | Buffer, i: number): number { 15 | return ( 16 | ((data[i] | 0) + 17 | (data[i + 1] << 8) + 18 | (data[i + 2] << 16) + 19 | ((data[i + 3] << 24) >>> 0)) >> 20 | 0 21 | ); 22 | } 23 | 24 | export function readUInt(data: number[] | Buffer, i: number): number { 25 | return ( 26 | ((data[i] | 0) + 27 | (data[i + 1] << 8) + 28 | (data[i + 2] << 16) + 29 | ((data[i + 3] << 24) >>> 0)) >>> 30 | 0 31 | ); 32 | } 33 | 34 | export function short(i: number): [number, number] { 35 | return [(i >>> 0) & 0xff, (i >>> 8) & 0xff]; 36 | } 37 | 38 | export function int(i: number): [number, number, number, number] { 39 | return [ 40 | (i >>> 0) & 0xff, 41 | (i >>> 8) & 0xff, 42 | (i >>> 16) & 0xff, 43 | (i >>> 24) & 0xff, 44 | ]; 45 | } 46 | 47 | export function long( 48 | i: number 49 | ): [number, number, number, number, number, number, number, number] { 50 | if (i > Number.MAX_SAFE_INTEGER) return [255, 255, 255, 255, 255, 255, 31, 0]; 51 | let l = i % 0x100000000 | 0; 52 | let h = (i / 0x100000000) | 0; 53 | return [ 54 | (l >>> 0) & 0xff, 55 | (l >>> 8) & 0xff, 56 | (l >>> 16) & 0xff, 57 | (l >>> 24) & 0xff, 58 | (h >>> 0) & 0xff, 59 | (h >>> 8) & 0xff, 60 | (h >>> 16) & 0xff, 61 | (h >>> 24) & 0xff, 62 | ]; 63 | } 64 | 65 | export function readStringArray( 66 | data: number[] | Buffer, 67 | i: number, 68 | len: number 69 | ): string[] { 70 | let str = []; 71 | 72 | for (; ; i++) { 73 | if (data[i] == undefined || str.length == len) break; 74 | 75 | str.push(String.fromCharCode(data[i])); 76 | } 77 | 78 | return str; 79 | } 80 | 81 | export function readString(data: number[], i: number, maxlen: number): string { 82 | return readStringArray(data, i, maxlen).join(""); 83 | } 84 | 85 | export function stringFromArray(i) { 86 | return [...short(i.length), ...i]; 87 | } 88 | 89 | export function string( 90 | str: string, 91 | encoding: "utf8" | "ascii" = "utf8" 92 | ): number[] { 93 | let array = Array.from(Buffer.from(str, encoding)); 94 | 95 | if (array.length > 65536) { 96 | array = array.slice(0, 65535); 97 | } 98 | 99 | return [...short(array.length), ...array]; 100 | } 101 | 102 | export function byte_string(str: string, encoding: "utf8" | "ascii" = "utf8") { 103 | let array = Array.from(Buffer.from(str, encoding)); 104 | 105 | if (array.length > 255) { 106 | array = array.slice(0, 255); 107 | } 108 | return [array.length, ...array]; 109 | } 110 | 111 | export function stringWithoutLength( 112 | str: string, 113 | encoding: "utf8" | "ascii" = "utf8" 114 | ) { 115 | return Array.from(Buffer.from(str, encoding)); 116 | } 117 | 118 | export function configString(name: string) { 119 | return string(config.get(name)); 120 | } 121 | 122 | export class Queue { 123 | private _: Buffer; // buffer 124 | private o: number; // offset 125 | private constructor(buf: Buffer) { 126 | this._ = buf; 127 | this.o = 0; 128 | } 129 | 130 | public static from(buffer: Buffer) { 131 | return new Queue(buffer); 132 | } 133 | 134 | byte(): number { 135 | return this._[this.o++] | 0; 136 | } 137 | 138 | short(): number { 139 | let data = readShort(this._, this.o); 140 | this.o += 2; 141 | return data; 142 | } 143 | 144 | int(): number { 145 | let data = readInt(this._, this.o); 146 | this.o += 4; 147 | return data; 148 | } 149 | 150 | uint(): number { 151 | let data = readUInt(this._, this.o); 152 | this.o += 4; 153 | return data; 154 | } 155 | 156 | skip(length: number): void { 157 | this.o += length; 158 | } 159 | 160 | sub(length: number): Buffer { 161 | this.o += length; 162 | 163 | return this._.slice(this.o - length, this.o); 164 | } 165 | 166 | string() { 167 | let len = this.short(); 168 | let data = readStringArray(this._, this.o, len); 169 | 170 | this.o += data.length; 171 | return data.join(""); 172 | } 173 | 174 | byte_string() { 175 | let len = this.byte(); 176 | let data = readStringArray(this._, this.o, len); 177 | 178 | this.o += data.length; 179 | return data.join(""); 180 | } 181 | 182 | long() { 183 | return longLib.fromBytesLE(this.sub(8)).toNumber(); 184 | } 185 | 186 | array(): number[] { 187 | return Array.from(this._.slice(this.o)); 188 | } 189 | } 190 | --------------------------------------------------------------------------------