70 Commits
Author SHA1 Message Date
Hide_D 196150bedc dep update
- typescript 5.9 업데이트: Uint8Array 문제 해결
- jest -> vitest (이후엔 그냥 node.test ?)
- @trpc 준비
- node 24
2025-10-08 16:22:48 +09:00
Hide_D 74b4e955df wip ArgType 준비 2024-03-16 15:04:50 +00:00
Hide_D 1aee0f7881 fix: initWithArg 2024-03-15 17:57:03 +00:00
Hide_D 42e68f90ad misc 2024-03-15 17:54:56 +00:00
Hide_D 0ce8293b89 BaseCommand에 argSchema 지정 2024-03-15 17:54:24 +00:00
Hide_D 494ed3f3f1 Command 초안
- PHP 버전 거의 그대로 이식
2024-03-15 17:35:32 +00:00
Hide_D 78bcc95e2e refac: rsc -> rc 2024-03-15 16:13:27 +00:00
Hide_D 6a6bdba08b refac: boostrap 2024-03-14 17:24:14 +00:00
Hide_D 39e8eab6cb refac: bootstrap arg 정리 2024-03-14 16:58:51 +00:00
Hide_D b4b00e1983 refac: bootstrap()
- 각 클래스가 WeakRef로 상호 참조한다면, construct 시점엔 불가
- bootstrap() 함수로 이후 참조
2024-03-14 16:26:40 +00:00
Hide_D 3e4129c3f1 env.gameEngineManualStart 2024-03-14 14:50:08 +00:00
Hide_D 533400b1b9 LoggerEngine을 이어붙이기 2024-03-12 16:57:02 +00:00
Hide_D cdfaac773a GameLogger 2024-03-12 16:53:31 +00:00
Hide_D e7b751c3f1 IMessage 2024-03-12 16:18:36 +00:00
Hide_D dd9b236547 단어 재정의 2024-03-11 17:06:50 +00:00
Hide_D d40995e1d9 변수명 재정의 2024-03-11 17:05:00 +00:00
Hide_D 28b78e0872 용어 재정의 2024-03-11 16:53:44 +00:00
Hide_D 21a9fc54f3 삭제 2024-03-11 16:45:02 +00:00
Hide_D 74174ca343 fix: misc 2024-03-11 15:41:03 +00:00
Hide_D 1b2e062bb6 NationEntity 추가 정의 2024-03-11 15:36:58 +00:00
Hide_D e49408db2a OldGeneral, OldNation에 RecordVersion
-  1, 2에 따른 실제 구조체는 이후에 반영
2024-03-11 14:45:24 +00:00
Hide_D e6f6798f34 ReservedTurn 2024-03-11 14:28:13 +00:00
Hide_D 2266cad0f8 Entity 2024-03-11 14:21:31 +00:00
Hide_D c9c974d191 GeneralEntity 보완 2024-03-11 13:41:54 +00:00
Hide_D fb16369384 Persistant 2024-03-11 13:11:49 +00:00
Hide_D 5adf8db676 IUserRecord 2024-03-11 12:42:06 +00:00
Hide_D c99f97323d UserID, UserName type
- Owner가 아니고, User여야.
2024-03-11 12:38:37 +00:00
Hide_D cf3a208591 WorldHistory 2024-03-11 12:35:51 +00:00
Hide_D 1537e8a137 Survey Entity 2024-03-11 12:35:43 +00:00
Hide_D 9a9f41c784 ExternalEntity 2024-03-11 12:35:31 +00:00
Hide_D e236654ecf CityEntity 2024-03-11 12:20:50 +00:00
Hide_D f693406df6 feat: GeneralRank 2024-03-11 11:52:30 +00:00
Hide_D 708f14fdb6 docs: 예턴, 사령턴은 DB로부터 받아옴 2024-03-09 18:19:20 +00:00
Hide_D 918bb997b9 docs: NPC 명령 선택기 항목 제거, Server Action Queue 추가
- NPC 명령 선택은 기존처럼 동기 모드로 구현
  - 로직을 최대한 기존과 유사하게
- Server Action Queue 추가
  - 외부 이벤트 루프가 관리
2024-03-09 18:14:15 +00:00
Hide_D b449cf3a11 feat: Action 호출 타입 설계
- 호출 시점에선 강타입으로.
2024-03-09 17:45:14 +00:00
Hide_D fb68d9be22 refac: GameEngine 함수 분리 2024-03-09 16:38:20 +00:00
Hide_D 6b819bbce5 misc: 호출부 2024-03-09 16:32:57 +00:00
Hide_D d742633e6d refac: Signal.{notify,abort}를 동기함수로 변경
- async일 필요가 없음
2024-03-09 16:31:35 +00:00
Hide_D e0784a670c feat: GameEngineWorker 2024-03-09 16:28:46 +00:00
Hide_D c42a97bb5f feat: api/daemon/{start,stop,restart,status} 2024-03-09 16:16:12 +00:00
Hide_D ddfff44c12 feat: GameEngineController 2024-03-09 16:15:43 +00:00
Hide_D 554588fb42 fix: sessionKey 2024-03-09 15:15:54 +00:00
Hide_D 99238636e6 feat: ACL 관련 정리 2024-03-09 15:07:10 +00:00
Hide_D 5928655bc1 feat: Mongoose를 GameEngine으로 전달 2024-03-09 14:54:25 +00:00
Hide_D bf8ad0a78c feat: ReqACL 2024-03-09 14:48:08 +00:00
Hide_D d87c177d50 refac: wip GameEngine 2024-03-09 09:19:43 +00:00
Hide_D ff23cd27b8 feat: RSC TurntimeQueue 2024-03-09 09:19:28 +00:00
Hide_D 3d67f8bb55 feat: General.compareTurntime 2024-03-09 09:18:59 +00:00
Hide_D 6e7ca3f998 feat: insertItemAtArrayLike
- 정렬된 배열에 값 추가
- binary search, 뒤부터 순차 탐색 지원
2024-03-09 09:18:41 +00:00
Hide_D 473963b58b wip: gameEngine 2024-03-09 07:46:27 +00:00
Hide_D 31184c1cd4 feat: gameengine(wip) 2024-03-09 07:41:06 +00:00
Hide_D a430d45e2a refac: shorthand name. ResourceController > rsc 2024-03-09 07:40:55 +00:00
Hide_D a6833473de IResourceController 2024-03-09 07:40:24 +00:00
Hide_D 9aecd4dfb4 feat: entriesWithType 2024-03-09 07:39:48 +00:00
Hide_D c7c84b4fd3 dep: server -> game_logic 2024-03-09 07:39:30 +00:00
Hide_D 3ad136b803 dep: date-fns 2024-03-09 07:39:19 +00:00
Hide_D 3b460b5f04 feat: 재사용가능한 signal 2024-03-09 07:04:25 +00:00
Hide_D 66c8261438 refac: 값 정리 2024-03-09 05:58:17 +00:00
Hide_D 3ba5793755 refac: RSC 2024-03-09 05:26:13 +00:00
Hide_D c9afc111cc refac: 재정비 2024-03-09 05:21:50 +00:00
Hide_D f320ce58a2 refac: 경로 변경 2024-03-09 05:09:54 +00:00
Hide_D afb6fbb1b1 misc: wip 2024-03-09 05:02:55 +00:00
Hide_D e965ed9816 refac: ResouceController 자원 관리 방식 변경 2024-03-09 05:02:16 +00:00
Hide_D 94becec429 misc: 정리 2024-03-09 04:58:45 +00:00
Hide_D fda68b690d misc: 정리 2024-03-09 04:56:50 +00:00
Hide_D fa29486af6 feat: LazyEntityUpdater 2024-03-09 04:50:55 +00:00
Hide_D a38f72fa46 doc: 스레드 구분에 외부 이벤트 처리기 명시
- 원래도 API 처리기에서 루프 돌릴 생각이긴 했음
2024-03-09 04:12:21 +00:00
Hide_D a5f5fb00ba feat: portRPC
- port를 이용해서 worker_thread간 RPC 호출 가능
- 호출, 호출반환은 ArrayBuffer를 이용
- 호출당 100us
2024-03-09 04:06:21 +00:00
Hide_D 04e76ddd9d feat: ArrayBuffer - String converter 2024-03-09 03:36:24 +00:00
Hide_D 85b1cee1b9 feat: delay(0)를 즉시 실행 Promise로 변경
- Promise와 일반 코드의 순서는 정의되어 있지 않으나,
  Promise 간에서는 먼저 선언된 것이 먼저 실행되는 것을 활용
2024-03-09 03:28:23 +00:00
121 changed files with 7770 additions and 6734 deletions
+1 -1
View File
@@ -1 +1 @@
20
24
+1 -1
View File
@@ -21,7 +21,7 @@
"@strpc/def": "workspace:^"
},
"devDependencies": {
"zod": "^3.22.4"
"zod": "^4.1.12"
},
"peerDependencies": {
"zod": "^3.22.2"
+10 -4
View File
@@ -1,5 +1,5 @@
import type {
DefAPINamespace,
DefAPINamespace,
} from "@strpc/def";
import { GET, POST } from '@strpc/def';
@@ -17,11 +17,17 @@ import type {
export type {
InferResponse,
InferError,
InferQuery,
InferResponse,
InferError,
InferQuery,
} from "@strpc/def";
export type * as def from './def/index.js';
export const structure = {
daemon: {
status: GET<ValidResponse & { isRunning: boolean, lastExecuted?: string }>(),
start: POST<ValidResponse>(),
stop: POST<ValidResponse>(),
restart: POST<ValidResponse>(),
}
} satisfies DefAPINamespace;
+21 -21
View File
@@ -21,31 +21,31 @@
"@sammo/util": "workspace:^",
"@strpc/client_ky": "workspace:^",
"@strpc/def": "workspace:^",
"bootstrap": "^5.3.3",
"bootstrap-vue-next": "^0.16.6",
"bson": "^6.4.0",
"date-fns": "^3.3.1",
"ky": "^1.2.2",
"@trpc/client": "^11.6.0",
"bootstrap": "^5.3.8",
"bootstrap-vue-next": "^0.40.6",
"bson": "^6.10.4",
"date-fns": "^4.1.0",
"ky": "^1.11.0",
"lodash-es": "^4.17.21",
"pinia": "^2.1.7",
"vue": "^3.4.21",
"vue-router": "^4.3.0"
"pinia": "^3.0.3",
"vue": "^3.5.22",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@tsconfig/node20": "^20.1.2",
"@tsconfig/node22": "^22.0.2",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.11.24",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.5.1",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.22.0",
"prettier": "^3.2.5",
"ts-node": "^10.9.2",
"tslib": "^2.6.2",
"vite": "^5.1.5",
"vue-tsc": "^2.0.5",
"zod": "^3.22.4"
"@types/node": "^24.7.0",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.6.0",
"@vue/tsconfig": "^0.8.1",
"eslint": "^9.37.0",
"eslint-plugin-vue": "^10.5.0",
"prettier": "^3.6.2",
"tslib": "^2.8.1",
"vite": "^7.1.9",
"vue-tsc": "^3.1.1",
"zod": "^4.1.12"
},
"peerDependencies": {
"zod": "^3.22.2"
-38
View File
@@ -1,38 +0,0 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
const jestConfig = {
preset: 'ts-jest/presets/default-esm',
extensionsToTreatAsEsm: ['.ts'],
testEnvironment: 'node',
rootDir: '.',
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
useESM: true,
tsconfig: 'tsconfig.jest.json',
},
],
},
moduleFileExtensions: [
'js',
'mjs',
'ts',
'mts',
'tsx',
'jsx',
],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
"^lodash-es$": "lodash",
"^lodash-es/(.*)$": "<rootDir>/node_modules/lodash/$1",
},
modulePathIgnorePatterns: [
'<rootDir>/dist/',
],
moduleDirectories: [
"node_modules",
".*/@sammo/util"
],
};
export default jestConfig
+10 -13
View File
@@ -16,29 +16,26 @@
},
"scripts": {
"build": "tsc --build",
"test": "jest"
"test": "vitest"
},
"author": "Hide_D <hided62@gmail.com>",
"type": "module",
"license": "MIT",
"dependencies": {
"@sammo/util": "workspace:^"
"@sammo/util": "workspace:^",
"vitest": "^3.2.4"
},
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/lodash": "^4.14.202",
"@types/lodash": "^4.17.20",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.11.24",
"@typescript-eslint/eslint-plugin": "^7.1.1",
"@typescript-eslint/parser": "^7.1.1",
"bson": "^6.4.0",
"@types/node": "^24.7.0",
"@typescript-eslint/eslint-plugin": "^8.46.0",
"@typescript-eslint/parser": "^8.46.0",
"bson": "^6.10.4",
"buffer": "^6.0.3",
"eslint": "^8.57.0",
"jest": "^29.7.0",
"eslint": "^9.37.0",
"lodash-es": "^4.17.21",
"ts-jest": "^29.1.2",
"tslib": "^2.6.2",
"typescript": "^5.3.3"
"tslib": "^2.8.1"
},
"peerDependencies": {
"bson": "^5.4.0",
+6 -5
View File
@@ -1,11 +1,12 @@
import { expect, test } from "vitest";
import { AES_GCM_Decrypt, AES_GCM_Encrypt } from "./AES.js";
type TestType = {
key: Buffer,
IV: Buffer,
PT: Buffer,
AAD?: Buffer,
CT: Buffer,
key: Buffer<ArrayBuffer>,
IV: Buffer<ArrayBuffer>,
PT: Buffer<ArrayBuffer>,
AAD?: Buffer<ArrayBuffer>,
CT: Buffer<ArrayBuffer>,
failed?: boolean,
};
-2
View File
@@ -1,5 +1,3 @@
import type { BufferSource } from "@sammo/util";
const subtle = globalThis.crypto.subtle;
+1
View File
@@ -1,3 +1,4 @@
import { expect, test } from "vitest";
import { ECDSA_sign_bson, ECDSA_verify_bson } from "./ECDSA.js";
import { genKey } from "./ECKey.js";
import { randomBytes } from "./utils.js";
+39 -16
View File
@@ -3,7 +3,6 @@ import { type Bsonifiable, bsonify, type Jsonifiable, jsonify } from "@sammo/uti
import { importSignKey, importVerifyKey } from "./ECKey.js";
import type { ECDSASignature, ECDSA_PKCS8_P384_SignKey, ECDSA_P384_VerifyKey, ECDSASignatureBSON, ECDSASignatureJSON, ECDSASignatureRaw } from "./RawTypes.js";
import { isBufferSource } from './types.js';
import { isArray, isDate, isObject } from "lodash-es";
// 간편하게 저장하기 위해서 그냥! 매번 복잡한 일을 하도록 하자
const subtle = globalThis.crypto.subtle;
@@ -13,35 +12,53 @@ const hashName: EcdsaParams = {
hash: 'SHA-512'
}
export async function ECDSA_sign(sign_key: ECDSA_PKCS8_P384_SignKey, msg: BufferSource): Promise<ECDSASignatureRaw> {
export async function ECDSA_sign(sign_key: ECDSA_PKCS8_P384_SignKey, msg: BufferSource | Uint8Array): Promise<ECDSASignatureRaw> {
const subtleSignKeyP = importSignKey(sign_key);
if (msg instanceof Uint8Array){
if(!(msg.buffer instanceof ArrayBuffer)){
const nmsg = new Uint8Array(new ArrayBuffer(msg.length));
nmsg.set(msg);
return Buffer.from(await subtle.sign(hashName, await subtleSignKeyP, nmsg));
}
//XXX: 멍청한 TS 5.9 버그. 신 버전 나오면 Uint8Array 해결할 것
return Buffer.from(await subtle.sign(hashName, await subtleSignKeyP, msg as Uint8Array<ArrayBuffer>));
}
return Buffer.from(await subtle.sign(hashName, await subtleSignKeyP, msg));
}
export async function ECDSA_sign_bson(sign_key: ECDSA_PKCS8_P384_SignKey, msg: Bsonifiable): Promise<ECDSASignatureBSON>
export async function ECDSA_sign_bson(sign_key: ECDSA_PKCS8_P384_SignKey, msg: BufferSource): Promise<ECDSASignatureRaw>
export async function ECDSA_sign_bson(sign_key: ECDSA_PKCS8_P384_SignKey, msg: Bsonifiable | BufferSource): Promise<ECDSASignatureRaw | ECDSASignatureBSON> {
export async function ECDSA_sign_bson(sign_key: ECDSA_PKCS8_P384_SignKey, msg: BufferSource | Uint8Array): Promise<ECDSASignatureRaw>
export async function ECDSA_sign_bson(sign_key: ECDSA_PKCS8_P384_SignKey, msg: Bsonifiable | BufferSource | Uint8Array): Promise<ECDSASignatureRaw | ECDSASignatureBSON> {
if (!isBufferSource(msg)) {
msg = BSON.serialize(bsonify(msg as Record<string, string>));
return await ECDSA_sign(sign_key, msg) as ECDSASignatureBSON;
const nmsg = BSON.serialize(bsonify(msg as Record<string, string>));
return await ECDSA_sign(sign_key, nmsg) as ECDSASignatureBSON;
}
return ECDSA_sign(sign_key, msg);
}
export async function ECDSA_sign_json(sign_key: ECDSA_PKCS8_P384_SignKey, msg: Bsonifiable): Promise<ECDSASignatureJSON>
export async function ECDSA_sign_json(sign_key: ECDSA_PKCS8_P384_SignKey, msg: BufferSource): Promise<ECDSASignatureRaw>
export async function ECDSA_sign_json(sign_key: ECDSA_PKCS8_P384_SignKey, msg: Jsonifiable | BufferSource): Promise<ECDSASignatureRaw | ECDSASignatureJSON> {
export async function ECDSA_sign_json(sign_key: ECDSA_PKCS8_P384_SignKey, msg: BufferSource | Uint8Array): Promise<ECDSASignatureRaw>
export async function ECDSA_sign_json(sign_key: ECDSA_PKCS8_P384_SignKey, msg: Jsonifiable | BufferSource | Uint8Array): Promise<ECDSASignatureRaw | ECDSASignatureJSON> {
if (!isBufferSource(msg)) {
msg = Buffer.from(JSON.stringify(jsonify(msg as Record<string, string>)), 'utf-8');
return await ECDSA_sign(sign_key, msg) as ECDSASignatureJSON;
const nmsg = Buffer.from(JSON.stringify(jsonify(msg as Record<string, string>)), 'utf-8') as Uint8Array<ArrayBuffer>;
return await ECDSA_sign(sign_key, nmsg) as ECDSASignatureJSON;
}
return ECDSA_sign(sign_key, msg);
}
export async function ECDSA_verify(verify_key: ECDSA_P384_VerifyKey, msg: BufferSource, sign: ECDSASignature): Promise<boolean> {
export async function ECDSA_verify(verify_key: ECDSA_P384_VerifyKey, msg: BufferSource | Uint8Array, sign: ECDSASignature): Promise<boolean> {
try {
//XXX: 멍청한 TS 5.9 버그. 신 버전 나오면 Uint8Array 해결할 것
const subtleVerifyKeyP = importVerifyKey(verify_key);
if (msg instanceof Uint8Array){
if(!(msg.buffer instanceof ArrayBuffer)){
const nmsg = new Uint8Array(new ArrayBuffer(msg.length));
nmsg.set(msg);
return await subtle.verify(hashName, await subtleVerifyKeyP, sign, nmsg);
}
return await subtle.verify(hashName, await subtleVerifyKeyP, sign, msg as Uint8Array<ArrayBuffer>);
}
return await subtle.verify(hashName, await subtleVerifyKeyP, sign, msg);
}
catch (e) {
@@ -50,10 +67,13 @@ export async function ECDSA_verify(verify_key: ECDSA_P384_VerifyKey, msg: Buffer
}
export async function ECDSA_verify_bson(verify_key: ECDSA_P384_VerifyKey, msg: Bsonifiable, sign: ECDSASignatureBSON): Promise<boolean>;
export async function ECDSA_verify_bson(verify_key: ECDSA_P384_VerifyKey, msg: BufferSource, sign: ECDSASignatureRaw): Promise<boolean>;
export async function ECDSA_verify_bson(verify_key: ECDSA_P384_VerifyKey, msg: Bsonifiable | BufferSource, sign: ECDSASignatureBSON | ECDSASignatureRaw): Promise<boolean> {
export async function ECDSA_verify_bson(verify_key: ECDSA_P384_VerifyKey, msg: BufferSource | Uint8Array, sign: ECDSASignatureRaw): Promise<boolean>;
export async function ECDSA_verify_bson(verify_key: ECDSA_P384_VerifyKey, msg: Bsonifiable | BufferSource | Uint8Array, sign: ECDSASignatureBSON | ECDSASignatureRaw): Promise<boolean> {
try {
if (msg instanceof Uint8Array){
return ECDSA_verify(verify_key, msg, sign);
}
if (!isBufferSource(msg)) {
msg = BSON.serialize(bsonify(msg as Record<string, string>));
}
@@ -65,13 +85,16 @@ export async function ECDSA_verify_bson(verify_key: ECDSA_P384_VerifyKey, msg: B
}
export async function ECDSA_verify_json(verify_key: ECDSA_P384_VerifyKey, msg: Bsonifiable, sign: ECDSASignatureJSON): Promise<boolean>;
export async function ECDSA_verify_json(verify_key: ECDSA_P384_VerifyKey, msg: BufferSource, sign: ECDSASignatureRaw): Promise<boolean>;
export async function ECDSA_verify_json(verify_key: ECDSA_P384_VerifyKey, msg: Jsonifiable | BufferSource, sign: ECDSASignatureJSON | ECDSASignatureRaw): Promise<boolean> {
export async function ECDSA_verify_json(verify_key: ECDSA_P384_VerifyKey, msg: BufferSource | Uint8Array, sign: ECDSASignatureRaw): Promise<boolean>;
export async function ECDSA_verify_json(verify_key: ECDSA_P384_VerifyKey, msg: Jsonifiable | BufferSource | Uint8Array, sign: ECDSASignatureJSON | ECDSASignatureRaw): Promise<boolean> {
try {
if (msg instanceof Uint8Array){
return ECDSA_verify(verify_key, msg, sign);
}
if (!isBufferSource(msg)) {
const x = JSON.stringify(jsonify(msg as Record<string, string>));
msg = Buffer.from(x, 'utf-8');
return ECDSA_verify(verify_key, Buffer.from(x, 'utf-8'), sign);
}
return ECDSA_verify(verify_key, msg, sign);
}
+1
View File
@@ -1,3 +1,4 @@
import { expect, test } from "vitest";
import { genKey, verifyKeyFromSignKey, pemFromVerifyKey, importVerifyKey, importSignKey, pemFromSignKey } from "./ECKey.js";
import { TypeLength } from "./TypeLength.js";
import { decodePEM } from "./utils.js";
+6 -5
View File
@@ -80,7 +80,7 @@ export class LiteHashDRBG implements RNG {
protected buffer!: ArrayBuffer;
protected bufferIdx!: number;
protected hq: DataView;
protected hq: DataView<ArrayBuffer>;
protected hqIdxPos: number;
protected ready: Promise<void>;
@@ -120,7 +120,7 @@ export class LiteHashDRBG implements RNG {
return maxInt;
}
public async nextBytes(bytes: number, baseBytes?: number): Promise<Uint8Array> {
public async nextBytes(bytes: number, baseBytes?: number): Promise<Uint8Array<ArrayBuffer>> {
bytes |= 0;
if (bytes <= 0) {
throw new Error(`${bytes} <= 0`);
@@ -128,7 +128,7 @@ export class LiteHashDRBG implements RNG {
const ticket = this.ready;
let waiter: Promise<Uint8Array | undefined> = Promise.resolve(undefined);
let waiter: Promise<Uint8Array<ArrayBuffer> | undefined> = Promise.resolve(undefined);
let nextBlockWait: (() => void) | null = (() => { throw 'something wrong'; });
@@ -194,10 +194,11 @@ export class LiteHashDRBG implements RNG {
if (nextBlockWait) {
nextBlockWait();
}
return nextBlock as Uint8Array;
if(!nextBlock){ throw new Error("Internal error: nextBlock is undefined"); }
return nextBlock;
}
public async nextBits(bits: number, baseBytes?: number): Promise<Uint8Array> {
public async nextBits(bits: number, baseBytes?: number): Promise<Uint8Array<ArrayBuffer>> {
await this.ready;
bits |= 0;
+2 -2
View File
@@ -5,8 +5,8 @@ export interface RNG {
*/
getMaxInt(): number;
nextBytes(bytes: number): Promise<Uint8Array>;
nextBits(bits: number): Promise<Uint8Array>;
nextBytes(bytes: number): Promise<Uint8Array<ArrayBuffer>>;
nextBits(bits: number): Promise<Uint8Array<ArrayBuffer>>;
nextInt(max?: number): Promise<number>;
nextFloat1(): Promise<number>;
+2 -2
View File
@@ -24,8 +24,8 @@ export class RandUtil {
}
public async nextBit(): Promise<boolean> {
const view = new DataView(await this.rng.nextBits(1) as ArrayBufferLike);
return view.getUint8(0) != 0;
const view = await this.rng.nextBits(1);
return view[0] != 0;
}
public async nextBool(prob = 0.5): Promise<boolean> {
+1
View File
@@ -1,3 +1,4 @@
import { expect, test } from "vitest";
import { sha256, sha512 } from "./SHA2.js";
test('sha2', async () => {
+30 -5
View File
@@ -1,19 +1,44 @@
import { isBufferSource } from "./types.js";
import { BSON } from "bson";
import { type Bsonifiable, bsonify } from "@sammo/util";
import { isArray, isDate, isObject } from "lodash-es";
const subtle = globalThis.crypto.subtle;
export async function sha256(msg: Bsonifiable | BufferSource): Promise<ArrayBuffer> {
export async function sha256(msg: Bsonifiable | BufferSource | Uint8Array): Promise<ArrayBuffer> {
if (msg instanceof Uint8Array){
if(!(msg.buffer instanceof ArrayBuffer)){
const nmsg = new Uint8Array(new ArrayBuffer(msg.length));
nmsg.set(msg);
return await subtle.digest('SHA-256', nmsg);
}
//XXX: 멍청한 TS 5.9 버그. 신 버전 나오면 Uint8Array 해결할 것
const nmsg = msg as Uint8Array<ArrayBuffer>;
return await subtle.digest('SHA-256', nmsg);
}
if (!isBufferSource(msg)) {
msg = BSON.serialize(bsonify(msg));
//XXX: 멍청한 TS 5.9 버그. 신 버전 나오면 Uint8Array 해결할 것
const nmsg = BSON.serialize(bsonify(msg)) as Uint8Array<ArrayBuffer>;
if (!(nmsg.buffer instanceof ArrayBuffer)){ throw new Error("Internal error: BSON.serialize did not return ArrayBuffer"); }
return await subtle.digest('SHA-256', nmsg);
}
return await subtle.digest('SHA-256', msg);
}
export async function sha512(msg: Bsonifiable | BufferSource): Promise<ArrayBuffer> {
export async function sha512(msg: Bsonifiable | BufferSource | Uint8Array): Promise<ArrayBuffer> {
if (msg instanceof Uint8Array){
if(!(msg.buffer instanceof ArrayBuffer)){
const nmsg = new Uint8Array(new ArrayBuffer(msg.length));
nmsg.set(msg);
return await subtle.digest('SHA-512', nmsg);
}
//XXX: 멍청한 TS 5.9 버그. 신 버전 나오면 Uint8Array 해결할 것
const nmsg = msg as Uint8Array<ArrayBuffer>;
return await subtle.digest('SHA-512', nmsg);
}
if (!isBufferSource(msg)) {
msg = BSON.serialize(bsonify(msg));
//XXX: 멍청한 TS 5.9 버그. 신 버전 나오면 Uint8Array 해결할 것
const nmsg = BSON.serialize(bsonify(msg)) as Uint8Array<ArrayBuffer>;
if (!(nmsg.buffer instanceof ArrayBuffer)){ throw new Error("Internal error: BSON.serialize did not return ArrayBuffer"); }
return await subtle.digest('SHA-512', nmsg);
}
return await subtle.digest('SHA-512', msg);
}
+2 -2
View File
@@ -35,7 +35,7 @@ export class PrivilegedGenViolation extends RuntimeError {
export type MergeableBuffer = MergeableBuffer[] | Buffer;
export function calcMergeableBuffer(item: MergeableBuffer): number {
if (item instanceof Buffer) {
if (item instanceof Uint8Array) {
return item.byteLength;
}
let bufferSize = 0;
@@ -57,7 +57,7 @@ export function mergeBuffer(...buffers: MergeableBuffer[]): Buffer {
}
const fillBuffer = function (item: MergeableBuffer, bufferIdx: number): number {
if (item instanceof Buffer) {
if (item instanceof Uint8Array) {
result.set(item, bufferIdx);
bufferIdx += item.byteLength;
return bufferIdx;
-3
View File
@@ -3,9 +3,6 @@ export function isBufferSource(obj: unknown): obj is BufferSource {
if (obj instanceof ArrayBuffer){
return true;
}
if('SharedArrayBuffer' in globalThis && obj instanceof SharedArrayBuffer){
return true;
}
if (ArrayBuffer.isView(obj)){
return true;
}
+6 -6
View File
@@ -41,12 +41,12 @@ type InferPEMBuffer<T extends PEMString<any, any>> = Exclude<undefined, T['_pem_
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function decodePEM<T extends PEMString<any, any>>(pem: T, pemType: InferPEMType<T>): InferPEMBuffer<T>[];
export function decodePEM(pem: string, pemType?: ValidPEMType): Buffer[];
export function decodePEM(pem: string, pemType?: ValidPEMType): Buffer[] {
export function decodePEM(pem: string, pemType?: ValidPEMType): Buffer<ArrayBuffer>[];
export function decodePEM(pem: string, pemType?: ValidPEMType): Buffer<ArrayBuffer>[] {
const tag = pemType ?? "[A-Z0-9 ]+";
const pattern = new RegExp(`-{5}BEGIN ${tag}-{5}([a-zA-Z0-9=+\\/\\n\\r]+)-{5}END ${tag}-{5}`, "g");
const res: Buffer[] = [];
const res: Buffer<ArrayBuffer>[] = [];
let matches: RegExpExecArray | null = null;
// eslint-disable-next-line no-cond-assign
while (matches = pattern.exec(pem)) {
@@ -63,11 +63,11 @@ export function decodePEM(pem: string, pemType?: ValidPEMType): Buffer[] {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function decodeSinglePEM<T extends PEMString<any, any>>(pem: T, pemType: InferPEMType<T>): InferPEMBuffer<T>;
export function decodeSinglePEM(pem: string, pemType?: ValidPEMType): Buffer;
export function decodeSinglePEM(pem: string, pemType?: ValidPEMType): Buffer {
export function decodeSinglePEM(pem: string, pemType?: ValidPEMType): Buffer<ArrayBuffer>;
export function decodeSinglePEM(pem: string, pemType?: ValidPEMType): Buffer<ArrayBuffer> {
const res = decodePEM(pem, pemType);
if (res.length != 1) {
throw new Error("invalid pem");
}
return res[0];
}
}
-6
View File
@@ -1,6 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"verbatimModuleSyntax": false,
}
}
+5 -3
View File
@@ -15,10 +15,12 @@
"@sammo/server_util": "workspace:^",
"@sammo/util": "workspace:^",
"@strpc/express": "workspace:^",
"dotenv": "^16.4.5",
"mongoose": "^8.2.1"
"date-fns": "^4.1.0",
"dotenv": "^17.2.3",
"mongoose": "^8.19.1",
"zod": "^4.1.12"
},
"devDependencies": {
"@types/node": "^20.11.24"
"@types/node": "^24.7.0"
}
}
@@ -0,0 +1,62 @@
import type { IResourceController } from "@/IResourceController.js";
import type { CityID, GeneralID, NationID, SquadID } from "@/defs.js";
import { NotYetImplemented } from "@sammo/util";
export type QueueType = 'server' | 'api';
//type QueueType = 'server' | 'npc' | 'api';
export function setDefenceTrained(invoker: QueueType, arg: { generalID: GeneralID, defenceTrained: number }, rc: IResourceController): void{
throw new NotYetImplemented();
}
export function kickSquadMember(invoker: QueueType, arg: { nationID: NationID, squadID: SquadID, memberID: GeneralID }, rc: IResourceController): void {
throw new NotYetImplemented();
}
export function blockAttackCommand(invoker: QueueType, arg: { nationID: NationID, block: boolean }, rc: IResourceController): void {
throw new NotYetImplemented();
}
export function blockJoinNation(invoker: QueueType, arg: { nationID: NationID, block: boolean }, rc: IResourceController): void {
throw new NotYetImplemented();
}
export function createGeneral(invoker: QueueType, arg: {
//유산 값등은 외부에서 처리하고 내부에는 순수 랜덤류만 입력
name: string,
ownerID: number,
ownerName: string,
leadership: number,
strength: number,
intel: number,
pic: string
character: string
turnTimeZone?: number,
warSpecial?: string,
city?: CityID,
bonusStat?: [number, number, number],
//penalty: Record<GeneralPenalty, number>,
}, rc: IResourceController):void{
throw new NotYetImplemented();
}
type ActionFunc<Arg extends Record<string, any>, Res> = (invoker: QueueType, arg: Arg, rc: IResourceController)=>Res;
export type ActionPackDef = {
[key: string]: ActionFunc<any, any>
}
export const ActionRequestList = {
setDefenceTrained,
kickSquadMember,
blockAttackCommand,
blockJoinNation,
createGeneral,
} as const satisfies ActionPackDef;
export type ActionRequestKey = keyof typeof ActionRequestList;
export type ActionMainArg<T extends ActionRequestKey> = Parameters<typeof ActionRequestList[T]>[1];
export type ActionRes<T extends ActionRequestKey> = ReturnType<typeof ActionRequestList[T]>;
+67 -3
View File
@@ -1,8 +1,72 @@
import type { CityID } from "./defs.js";
import type { ICityEntity } from "./Entity/CityEntity.js";
import { LazyEntityUpdater } from "./LazyEntityUpdater.js";
import type { IResourceController } from "./IResourceController.js";
import type { CityID, GeneralID, NationID } from "./defs.js";
import type { General } from "./General.js";
import { must } from "@sammo/util";
export class City extends LazyEntityUpdater<ICityEntity>{
protected readonly _indirectNames: (keyof ICityEntity)[] = ["id", "nationID"];
protected readonly _entityType = "City";
protected readonly _raw: ICityEntity;
protected _members = new Map<GeneralID, WeakRef<General>>;
protected _otherMembers = new Map<NationID, Map<GeneralID, WeakRef<General>>>;
export class City{
constructor(
public readonly id: CityID
raw: ICityEntity,
protected readonly rc: IResourceController,
bootstrapInfo: {
general: GeneralID[];
}
){
super();
this._raw = raw;
this._bootstrapInfo = bootstrapInfo;
}
private _bootstrapInfo?: ConstructorParameters<typeof City>[2];
public bootstrap(){
const rc = this.rc;
const bootstrapInfo = must(this._bootstrapInfo);
delete this._bootstrapInfo;
const nationID = this._raw.nationID;
for(const id of bootstrapInfo.general){
const general = must(rc.general(id));
this.notifyJoinGeneral(general);
}
}
get id(): CityID {
return this._raw.id;
}
notifyJoinGeneral(general: General){
const nationID = general.raw.nationID;
if(nationID === this._raw.nationID){
this._members.set(general.id, new WeakRef(general));
return;
}
let otherMembers = this._otherMembers.get(nationID);
if(!otherMembers){
otherMembers = new Map();
this._otherMembers.set(nationID, otherMembers);
}
otherMembers.set(general.id, new WeakRef(general));
}
notifyQuitGeneral(general: General){
const nationID = general.raw.nationID;
if(nationID === this._raw.nationID){
this._members.delete(general.id);
return;
}
const otherMembers = must(this._otherMembers.get(nationID));
otherMembers.delete(general.id);
}
}
+33
View File
@@ -0,0 +1,33 @@
import type { GeneralID } from "@/defs.js"
//TODO: 작성!!
export type DestGeneralArgType = {
destGeneralID: GeneralID;
}
export type DestCityArgType = {
destCityID: string;
}
export type DestNationArgType = {
destNationID: string;
}
export type ColorArgType = {
color: string;
}
export type TextArgType = {
text: string;
}
export type ArmyTypeArgType = {
armyType: number;
}
export type ArgType =
| DestGeneralArgType
| DestCityArgType
| DestNationArgType
;
+290
View File
@@ -0,0 +1,290 @@
import type { City } from "@/City.js";
import { testContraints, type IConstraint } from "@/Constraint/index.js";
import type { GameEnv } from "@/GameEnv.js";
import type { General } from "@/General.js";
import type { Nation } from "@/Nation.js";
import type { IntYearMonth } from "@/defs.js";
import type { RandUtil } from "@sammo/crypto";
import { InvalidArgument, must } from "@sammo/util";
import type { z } from "zod";
export const enum CommandType {
General = 0,
Nation = 1,
};
export const enum CompensateType {
Neutral = 0,
Positive = 1,
Negative = -1,
}
export abstract class BaseCommand<ArgType extends Record<string, any>>{
abstract readonly actionName: string;
abstract readonly className: string;
abstract readonly type: CommandType;
public abstract readonly compensateType: CompensateType;
readonly isLazyCalcReqTurn: boolean = false;
protected cachedPermissionToReserve = false;
protected cachedMinConditionMet = false;
protected cachedFullConditionMet = false;
protected reasonNotFullConditionMet: string | undefined = undefined;
protected reasonNotMinConditionMet: string | undefined = undefined;
protected reasonNotPermissionToReserve: string | undefined = undefined;
protected _destNation?: Nation;
protected _destCity?: City;
protected _destGeneral?: General;
public get destNation(): Readonly<Nation> | undefined {
return this._destNation;
}
public get destCity(): Readonly<City> | undefined {
return this._destCity;
}
public get destGeneral(): Readonly<General> | undefined {
return this._destGeneral;
}
protected abstract alternative: BaseCommand<any> | undefined;
protected abstract permissionConstraints: IConstraint<any>[] | undefined;
protected abstract minConditionConstraints: IConstraint<any>[] | undefined;
protected abstract fullConditionConstraints: IConstraint<any>[] | undefined;
protected abstract errorInvoker: string | undefined;
protected abstract readonly argSchema: {} extends ArgType ? undefined : z.ZodObject<ArgType>;
protected _arg: ArgType | undefined;
protected readonly reasonArgTestFailed?: string;
public get arg(): Readonly<ArgType> {
return must(this._arg);
}
public constructor(
public readonly general: General,
public readonly env: GameEnv,
arg: ArgType
) {
this.init();
this.reasonArgTestFailed = this.testArg(arg);
if(this.reasonArgTestFailed === undefined){
this.initWithArg();
}
}
private testArg(rawArg: ArgType): string | undefined {
if(this.argSchema === undefined){
if(Object.keys(rawArg).length === 0){
return undefined;
}
return "이 커맨드는 인자를 받지 않습니다";
}
const result = must(this.argSchema).safeParse(this.arg);
if(!result.success){
return result.error.errors[0].message;
}
this._arg = result.data as unknown as ArgType;
return undefined;
}
protected abstract init(): void;
protected initWithArg(): void {
if (Object.keys(this.arg).length == 0) {
return;
}
throw new InvalidArgument("initWithArg must be overridden if arg is not undefined");
}
public get commandBrief(): string {
return this.actionName;
}
//TODO: ExecuteKey의 타입
abstract getNextExecuteKey(): string;
abstract getNextAvailableTurn(): IntYearMonth | undefined;
abstract setNextAvailableTurn(turn?: IntYearMonth): void;
abstract getCost(): [gold: number, rice: number];
abstract getPreReqTurn(): number;
abstract getPostReqTurn(): number;
protected testPostReqTurn(): ["testPostReqTurn", string] | undefined {
if (this.getPostReqTurn() === 0) {
return undefined;
}
const nextAvailableTurn = this.getNextAvailableTurn();
if (nextAvailableTurn === undefined) {
return undefined;
}
const yearMonth = this.env.raw.yearMonth;
const remainTurn = nextAvailableTurn - yearMonth;
if (remainTurn <= 0) {
return undefined;
}
return ["testPostReqTurn", `${remainTurn}턴 더 기다려야 합니다`];
}
testPermissionToReserve(): string | undefined {
if (this.cachedPermissionToReserve) {
return this.reasonNotPermissionToReserve;
}
if (this.reasonNotPermissionToReserve) {
return this.reasonNotPermissionToReserve;
}
const testResult = testContraints(this.permissionConstraints ?? [], this);
if (testResult !== undefined) {
[this.errorInvoker, this.reasonNotPermissionToReserve] = testResult;
}
this.cachedPermissionToReserve = true;
return this.reasonNotPermissionToReserve;
}
public canDisplay(): boolean {
return true;
}
public testMinConditionMet(): string | undefined {
if (this.argSchema === undefined && this.minConditionConstraints === undefined) {
return this.testFullConditionMet();
}
if(this.minConditionConstraints === undefined){
throw new InvalidArgument("minConditionConstraints가 제대로 설정되지 않았습니다");
}
if (this.cachedMinConditionMet) {
return this.reasonNotMinConditionMet;
}
const testResult = testContraints(this.minConditionConstraints, this);
if (testResult !== undefined) {
[this.errorInvoker, this.reasonNotMinConditionMet] = testResult;
}
if (this.reasonNotMinConditionMet === undefined && !this.isLazyCalcReqTurn) {
const result = this.testPostReqTurn();
if (result !== undefined) {
[this.errorInvoker, this.reasonNotMinConditionMet] = result;
}
}
this.cachedMinConditionMet = true;
return this.reasonNotMinConditionMet;
}
public testFullConditionMet(): string | undefined {
if(this.reasonArgTestFailed !== undefined){
this.reasonNotFullConditionMet = this.reasonArgTestFailed;
this.cachedFullConditionMet = true;
return this.reasonNotFullConditionMet;
}
if (this.fullConditionConstraints === undefined) {
throw new InvalidArgument('fullConditionConstraints가 제대로 설정되지 않았습니다');
}
if (this.cachedFullConditionMet) {
return this.reasonNotFullConditionMet;
}
const testResult = testContraints(this.fullConditionConstraints, this);
if (testResult !== undefined) {
[this.errorInvoker, this.reasonNotFullConditionMet] = testResult;
}
if (this.reasonNotFullConditionMet === undefined) {
const result = this.testPostReqTurn();
if (result !== undefined) {
[this.errorInvoker, this.reasonNotFullConditionMet] = result;
}
}
this.cachedFullConditionMet = true;
return this.reasonNotFullConditionMet;
}
public get termString(): string {
const commandName = this.commandBrief;
const term = this.getNextAvailableTurn();
const termMax = this.getPreReqTurn() + 1;
return `${commandName} 수행중... (${term}/${termMax})`;
}
public addTermStack(): boolean {
if (this.getPreReqTurn() === 0) {
return true;
}
const lastTurn = this.general.raw.aux.lastTurn;
const nextTurn: Exclude<typeof lastTurn, undefined> = {
action: this.className,
arg: this.arg,
brief: this.commandBrief,
term: 1
};
if (lastTurn === undefined) {
this.general.update((raw) => {
raw.aux.lastTurn = nextTurn;
});
return false;
}
if (lastTurn.action !== this.className || lastTurn.arg !== this.arg) {
this.general.update((raw) => {
raw.aux.lastTurn = nextTurn;
});
return false;
}
if (lastTurn.term < this.getPreReqTurn()) {
nextTurn.term = lastTurn.term + 1;
this.general.update((raw) => {
raw.aux.lastTurn = nextTurn;
});
return false;
}
return true;
}
public get hasPermissionToReserve(): boolean {
return this.testPermissionToReserve() === undefined;
}
public get hasMinConditionMet(): boolean {
return this.testMinConditionMet() === undefined;
}
public get hasFullConditionMet(): boolean {
return this.testFullConditionMet() === undefined;
}
public get failString(): string {
const commandName = this.commandBrief;
const failReason = this.testFullConditionMet();
if (failReason === undefined) {
throw new Error('실행 가능한 커맨드에 대해 실패 이유를 수집');
}
return `${failReason} ${commandName} 실패.`;
}
public get alternativeCommand(): BaseCommand<any> {
return must(this.alternative);
}
abstract run(rng: RandUtil): boolean;
}
+50
View File
@@ -0,0 +1,50 @@
import type { City } from "@/City.js"
import type { GameEnv } from "@/GameEnv.js"
import type { General } from "@/General.js"
import type { Nation } from "@/Nation.js"
export const enum RequiredDestLevel {
None = 0,
Nation = 1,
City = 2,
General = 3,
}
export interface IConstraint<ArgType extends Record<string, unknown> = {}> {
readonly reason?: string;
readonly requiredDestLevel: RequiredDestLevel;
readonly arg: ArgType;
test(input: TestInput): undefined | [testName: string, err: string];
}
export const constraints: {
[key: string]: IConstraint
} = {
}
export interface TestInput {
general: Readonly<General>,
destNation?: Readonly<Nation>,
destCity?: Readonly<City>,
destGeneral?: Readonly<General>,
env: Readonly<GameEnv>,
}
export function testContraints(testList: IConstraint[], input: TestInput): undefined | [testName: string, err: string]{
for(const test of testList){
if(test.requiredDestLevel >= RequiredDestLevel.Nation && input.destNation === undefined){
return [test.constructor.name, "require destNation"];
}
else if(test.requiredDestLevel >= RequiredDestLevel.City && input.destCity === undefined){
return [test.constructor.name, "require destCity"];
}
else if(test.requiredDestLevel >= RequiredDestLevel.General && input.destGeneral === undefined){
return [test.constructor.name, "require destGeneral"];
}
const result = test.test(input);
if(result !== undefined){
return result;
}
}
return undefined;
}
@@ -0,0 +1,80 @@
import type { CityID, CityLevel, CityName, CityOfficerLevel, CityState, FrontStatus, NationID, FractionalRange, RegionID, IntMonth } from "../defs.js";
export interface ICityEntity{
id: CityID;
name: CityName;
nationID: NationID;
level: CityLevel;
//보급품 있음
isSupplied: boolean;
//도시 접경 상태
frontStatus: FrontStatus;
//인구
population: {
current: number;
max: number;
}
//농업
agriculture: {
current: number;
max: number;
}
//상업
commerce: {
current: number;
max: number;
}
//치안
security: {
current: number;
max: number;
}
//민도
loyalty: {
current: number;
max: 100;
}
//수비병
defence: {
current: number;
max: number;
}
//성벽
wall: {
current: number;
max: number;
}
//전투 사망자 수(혹은 패잔병)
battleDeath: number;
//상인 존재 및 거래율, base 1
traderStatus: {
exists: false;
} | {
exists: true;
rate: FractionalRange;
}
officerAssignability: Record<CityOfficerLevel, boolean>;
regionID: RegionID;
state: CityState;
//전투 잔여 개월
remainWarMonth: IntMonth;
//국가별 수비병 사살 수
cityDamageRatio: Record<NationID, number>;
}
+13
View File
@@ -0,0 +1,13 @@
import type { DiplomacyState, NationID } from "@/defs.js";
export interface Diplomacy {
_id: string;
lowerNationID: NationID;
higherNationID: NationID;
state: DiplomacyState;
lowerNationDeadCrew?: number;
higherNationDeadCrew?: number;
}
@@ -0,0 +1,29 @@
import type { GeneralID, GeneralName, NationID } from "@/defs.js";
export interface IDiplomaticLetter {
_id: string;
srcNationID: NationID;
destNationID: NationID;
prevID?: string;
state: 'proposed' | 'accepted' | 'rejected' | 'canceled' | 'replaced';
srcSigner: {
generalID: GeneralID;
generalName: GeneralName;
icon: string;
};
destSigner: {
generalID: GeneralID;
generalName: GeneralName;
icon: string;
};
publicText: string;
privateText: string;
createdAt: Date;
updatedAt: Date;
}
@@ -0,0 +1,41 @@
import type { GeneralID } from "@/defs.js";
//게임 외부적인 General Rank 정보
export interface IGeneralExternalRankEntity {
generalID: GeneralID;
//토너먼트 결과가 앞으로도 게임 내부 데이터로 반영되지 않을것인가?
//토너먼트 결과
tournamentResult: Record<'all'|'leadership'|'strength'|'intel', {
win: number,
lose: number,
draw: number,
//토너먼트 참가 횟수
cnt: number,
//포인트
point: number,
}>;
tournamentBet: {
//베팅 성공 횟수
betWin: number;
//베팅 금액
betGold: number;
//베팅 수익금
betProfit: number;
}
//유상 포인트 획득(지연)
inheritPointEarned: number;
//유상 포인트 소모(지연)
inheritPointSpent: number;
//유산 포인트 획득량(동적)
inheritPointEarnedDyn: number;
//유산 포인트 획득량(베팅 등 별도 명령)
inheritPointEarnedAction: number;
//유산 포인트 소모량(동적)
inheritPointSpentDyn: number;
}
+15
View File
@@ -0,0 +1,15 @@
import type { LogType } from "@/defs.js";
export interface IGeneralRecord {
_id: string;
generalID: string;
logType: LogType;
year: number;
month: number;
text: string;
}
@@ -0,0 +1,6 @@
import type { GeneralID, ReservedTurn } from "@/defs.js";
export interface IGeneralReservedTurn {
generalID: GeneralID;
turn: ReservedTurn[];
}
+7
View File
@@ -0,0 +1,7 @@
export interface IGlobalRecord {
_id: string;
type: 'history' | 'action';
year: number;
month: number;
text: string;
}
+37
View File
@@ -0,0 +1,37 @@
import type { GeneralID, GeneralName, MessageType, NationID, NationName } from "@/defs.js";
export interface MessageTarget {
mailbox: number;
generalID: GeneralID;
generalName: GeneralName;
nationID: NationID;
nationName: NationName;
icon: string;
color: string;
}
export interface IMessage{
_id: string;
mailbox: number;
state: 'unread' | 'read' | 'deleted';
type: MessageType;
src: MessageTarget;
dest: MessageTarget;
createdAt: Date;
text: string;
action?: {
validUntil?: Date;
used: boolean;
action: string;
arg: Record<string, unknown>;
}
}
+9
View File
@@ -0,0 +1,9 @@
import type { NationID } from "@/defs.js";
export interface INationRecord {
_id: string;
nationID: NationID;
year: number;
month: number;
text: string;
}
@@ -0,0 +1,10 @@
import type { ReservedTurn, StaffLevel } from "@/defs.js";
export interface INationReservedTurn {
_id: string;
nationID: string;
officerLevel: StaffLevel,
turn: ReservedTurn[];
}
+15
View File
@@ -0,0 +1,15 @@
export interface ISurvey {
_id: string;
title: string;
description: string;
//투표 시작 시간
startTime: Date;
//투표 종료 시간
endTime?: Date;
multipleOptions: number;
options: string[];
}
+15
View File
@@ -0,0 +1,15 @@
import type { GeneralID, GeneralName, NationID, NationName } from "@/defs.js";
export interface ISurveyComment {
_id: string;
surveyID: string;
generalID: GeneralID;
nationID: NationID;
generalName: GeneralName;
nationName: NationName;
comment: string;
createdAt: Date;
}
+14
View File
@@ -0,0 +1,14 @@
import type { GeneralID, NationID } from "@/defs.js";
export interface ISurveyResponse {
_id: string;
surveyID: string;
//투표자
generalID: GeneralID;
nationID: NationID;
//투표한 선택지
selectedOption: number[];
}
+14
View File
@@ -0,0 +1,14 @@
import type { NationID } from "@/defs.js";
//국제 정세
export interface IWorldHistory {
//MongoDB ID
_id: string;
//nationID -> year -> month
year: number;
month: number;
nationID?: NationID;
text: string;
}
@@ -0,0 +1,41 @@
import type { IntYear, IntYearMonth } from "@/defs.js";
export type UnitedGameState = 'onGame' | 'united' | 'onEvent' | 'endEvent';
export interface IGameEnvEntity {
startTime: Date;
lastExecuted: Date;
initYear: number;
initMonth: number;
gameStartYear: number;
gameStartMonth: number;
year: IntYear;
month: IntYearMonth;
yearMonth: IntYearMonth;
secondsPerTurn: number;
numberOfIdleTurnTillDeath: number;
isFictionMode: boolean;
unitedGameState: UnitedGameState;
blockGeneralCreate: boolean;
mapTheme: string;
prevWinnerName?: string;
scenarioID: number;
scenarioName: string;
gameSeason: number;
maxGeneralCnt: number;
maxNationCnt: number;
remainGeniusCnt: number;
autoRunUserPolicy: Record<string, unknown>;
//게임 외부는 다른 엔티티로 분리
}
@@ -1,16 +1,16 @@
import type { DiplomaticPermission, GeneralID, GeneralName, ItemID, ItemKeyType, NationID, NpcType, OfficerLevel, OwnerID, OwnerName, PersonalityType, SpecialityDomesticType, SpecialityWarType, TroopID } from "./defs.js";
import type { CityID, DiplomaticPermission, FractionalRange, GeneralID, GeneralName, IntMonth, IntYear, IntYearMonth, ItemID, ItemKeyType, NationID, NpcType, OfficerLevel, PersonalityType, ReservedTurn, SpecialityDomesticType, SpecialityWarType, SquadID, URILike, UserID, UserName } from "../defs.js";
export interface IGeneralEntity {
id: GeneralID;
owner?: OwnerID;
ownerID?: UserID;
npcType: NpcType;
affinity?: number;
picture?: string;
icon?: URILike;
name: GeneralName;
ownerName?: OwnerName;
ownerName?: UserName;
nationID: NationID;
cityID: number;
troopID?: TroopID;
squadID?: SquadID;
leadership: number;
leadershipExp: number;
@@ -19,7 +19,7 @@ export interface IGeneralEntity {
intel: number;
intelExp: number;
injury: number;
injury: FractionalRange;
experience: number;
dedication: number;
@@ -27,9 +27,11 @@ export interface IGeneralEntity {
experienceLevel: number;
dedicationLevel: number;
dex: number[];
armyClassExperience: number[];
officerLevel: OfficerLevel;
officerCityID?: CityID;
diplomaticPermission: DiplomaticPermission;
gold: number;
@@ -40,14 +42,14 @@ export interface IGeneralEntity {
option: Record<string, unknown>,
}>;
turntime: Date;
turnTime: Date;
killturn: number;
idleTurn: IntMonth;
age: number;
startAge: number;
age: IntYear;
startAge: IntYear;
nationBelong: number;
nationJoinedYearMonth: IntYearMonth;
betray: number;
@@ -55,11 +57,22 @@ export interface IGeneralEntity {
specialityDomestic: SpecialityDomesticType;
specialityWar: SpecialityWarType;
defenceTrain: number;
defenceTrained: number;
armyType: number;
armyCount: number;
trained: number;
morale: number;
npcSpec?: {
msg?: string;
bornyear: number;
deadyear: number;
}
aux: {
lastBattleDate?: Date;
lastBattleYearMonth?: IntYearMonth;
lastTurn?: ReservedTurn & { term: number };
}
}
@@ -0,0 +1,28 @@
import type { GeneralID } from "@/defs.js";
export interface IGeneralRankEntity {
generalID: GeneralID;
//계략 성공 횟수
sabotageCnt: number;
//전투 횟수
battleCnt: number;
//전투 승리 횟수
battleWinCnt: number;
//전투 패배 횟수
battleLoseCnt: number;
//누적 병력 사망 수
allDeathCnt: number;
//누적 병력 사살 수
allKillCnt: number;
//누적 대인 병력 사망 수
armyDeathCnt: number;
//누적 대인 병력 사살 수
armyKillCnt: number;
//점령 수
occupyCnt: number;
}
@@ -0,0 +1,64 @@
import type { CityID, ColorType, IntYearMonth, NationCmdType, NationID, NationLevel, NationName, NationType, FractionalRange, IntMonth, ReservedTurn, StaffLevel, StrategicCmdType } from "../defs.js";
export interface INationEntity{
id: NationID;
name: NationName;
type: NationType;
capitalID: CityID;
color: ColorType;
level: NationLevel;
lastExecutedYearMonth: Record<
'capitalSet'
,number>;
//TODO: 이게 뭐더라
capset: IntYearMonth;
strategicCmdLimit: Record<StrategicCmdType, IntYearMonth>;
//세율
taxRate: FractionalRange;
//지급율
payRate: FractionalRange;
gold: number;
rice: number;
blockJoin: boolean;
blockWar: boolean;
//특수외교커맨드 제한
specialDiplomacyCmdLimit: IntYearMonth;
//기술력
technology: number;
//환산 국력
power: number;
//첩보 제한
spyLimit: Map<CityID, IntMonth>;
//커맨드별 옵션
commandOption: Map<NationCmdType, Map<string, unknown>>;
lastExecutedTurn: Map<StaffLevel, ReservedTurn>;
aux: {
availableWarSettingCnt?: number;
prevIncomeGold: number;
prevIncomeRice: number;
lastBattleDate?: Date;
lastBattleYearMonth?: IntYearMonth;
}
//TODO: 타입 정의
nationPolicy: Record<string, unknown>;
//TODO: 타입 정의
npcPolicy: Record<string, unknown>;
}
@@ -0,0 +1,31 @@
import type { GeneralID, GeneralName, NationName, ScenarioID, ServerID, UserID, UserName } from "@/defs.js"
import type { IGeneralRankEntity } from "../GeneralRankEntity.js";
import type { IGeneralExternalRankEntity } from "../External/GeneralExternalRankEntity.js";
export interface IHallOfFame {
_id: string
serverID: ServerID;
serverIdx: number;
season: number;
scenarioID: ScenarioID;
scenarioName: string;
generalID: GeneralID;
generalName: GeneralName;
ownerID: UserID;
ownerName: UserName;
icon: string;
nationName: NationName;
nationColor: string;
startTime: Date;
endTime: Date;
ingame: IGeneralRankEntity;
external: IGeneralExternalRankEntity;
}
@@ -0,0 +1,32 @@
import type { CityID, CityLevel, CityState, NationID, NpcType, OfficerLevel, RegionID, ServerID } from "@/defs.js";
export interface IHistory {
_id: string;
serverID: ServerID;
year: number;
month: number;
mapBase: string;
nations: Map<NationID, {
name: string;
color: string;
cities: Set<CityID>;
generals: {
name: string;
npcType: NpcType;
}[];
}>;
cities: Map<CityID, {
nationID: NationID;
name: string;
isSupplied: boolean;
level: CityLevel;
state: CityState;
regionID: RegionID;
remainWarMonth: number;
}>
globalHistory: string[];
globalAction: string[];
}
@@ -0,0 +1,30 @@
import type { CityLevel, GeneralID, ServerID, UserID } from "@/defs.js";
import type { IGeneralEntity } from "../GeneralEntity.js";
import type { IGeneralRankEntity } from "../GeneralRankEntity.js";
export interface OldGeneral {
_id: string;
serverID: ServerID;
generalID: GeneralID;
ownerID?: UserID;
recordVersion: 1 | 2;
lastYearMonth: number;
general: IGeneralEntity;
rank: IGeneralRankEntity;
squadName?: string;
cityBasic: {
name: string;
level: CityLevel;
}
nationBasic: {
name: string;
color: string;
};
createdAt: Date;
}
@@ -0,0 +1,36 @@
import type { GeneralID, GeneralName, ItemID, ItemKeyType, NationID, NpcType, OfficerLevel, PersonalityType, ServerID, SpecialityDomesticType, SpecialityWarType, URILike, UserID } from "@/defs.js";
import type { INationEntity } from "../NationEntity.js";
export interface IOldNation {
_id: string;
serverID: ServerID;
nationID: NationID;
recordVersion: 1 | 2;
nation: INationEntity;
lastYearMonth: number;
members: Record<number, {
generalID: GeneralID;
generalName: GeneralName;
ownerID?: UserID;
ownerName?: string;
npcType?: NpcType;
icon?: URILike;
officerLevel: OfficerLevel;
officerCityID?: number;
}>
history: {
year: number;
month: number;
text: string[];
};
createdAt: Date;
}
@@ -0,0 +1,18 @@
import type { ServerID, UserID } from "@/defs.js";
export interface IUserRecord {
_id: string;
userID: UserID;
serverID: ServerID;
logType: 'inheritPoint';
//게임 내 연도
year: number;
month: number;
text: string;
createdAt: Date;
}
@@ -0,0 +1,7 @@
import type { NationID, SquadID } from "../defs.js";
export interface ISquadEntity {
id: SquadID;
nationID: NationID;
name: string;
}
+25
View File
@@ -0,0 +1,25 @@
import type { ValuesOf } from '@sammo/util';
import type { ICityEntity } from './CityEntity.js';
import type { IGeneralEntity } from './GeneralEntity.js';
import type { INationEntity } from './NationEntity.js';
import type { ISquadEntity } from './SquadEntity.js';
import type { IGameEnvEntity } from './GameEnvEntity.js';
export type { IGeneralEntity } from './GeneralEntity.js';
export type { ICityEntity } from './CityEntity.js';
export type { ISquadEntity } from './SquadEntity.js';
export type { INationEntity } from './NationEntity.js';
export type ValidEntityList = {
'Nation': INationEntity;
'City': ICityEntity;
'Squad': ISquadEntity;
'General': IGeneralEntity;
'GameEnv': IGameEnvEntity;
};
export type ValidEntity = ValuesOf<ValidEntityList>;
export type ValidEntityType = keyof ValidEntityList;
export type EntityType<Entity> = Entity extends ValidEntityList[infer T extends keyof ValidEntityList] ? T : never;
+6
View File
@@ -0,0 +1,6 @@
export class LogicError extends Error {
constructor(message?: string) {
super(message);
this.name = 'LogicError';
}
}
+21 -1
View File
@@ -1,3 +1,23 @@
export class GameEnv{
import type { IGameEnvEntity } from "./Entity/GameEnvEntity.js";
import { LazyEntityUpdater } from "./LazyEntityUpdater.js";
import type { IResourceController } from "./IResourceController.js";
export class GameEnv extends LazyEntityUpdater<IGameEnvEntity>{
protected readonly _indirectNames: readonly (keyof IGameEnvEntity)[] = [
//사실상 모두.
];
protected readonly _entityType = "GameEnv";
protected readonly _raw: IGameEnvEntity;
constructor(
raw: IGameEnvEntity,
protected readonly rc: IResourceController
){
super();
this._raw = raw;
}
public bootstrap(){
//do nothing;
}
}
+190
View File
@@ -0,0 +1,190 @@
import type { IResourceController } from "./IResourceController.js";
import type { GeneralID, IntMonth, IntYear, LogType, NationID } from "./defs.js";
type LoggerStack = {
symbol: Symbol;
year: IntYear;
month: IntMonth;
global: {
action: string[];
history: string[];
}
nation: {
nationID: NationID;
history: string[];
}
general: {
[key in LogType]: string[];
} & {
generalID: GeneralID;
}
}
export class GameLoggerEngine {
private static instance: GameLoggerEngine;
private constructor(
private rc: IResourceController
) {
}
public static initInstance(rc: IResourceController): GameLoggerEngine{
if(GameLoggerEngine.instance){
throw new Error("GameLogger is already initialized");
}
GameLoggerEngine.instance = new GameLoggerEngine(rc);
return GameLoggerEngine.instance;
}
public static getInstance(): GameLoggerEngine {
if (!GameLoggerEngine.instance) {
throw new Error("GameLogger is not initialized");
}
return GameLoggerEngine.instance;
}
private logStack = new Map<Symbol, LoggerStack>();
public addStack(stack: LoggerStack) {
this.logStack.set(stack.symbol, stack);
return stack;
}
public getGameLogger(year: IntYear, month: IntMonth): GameLogger {
return new GameLogger(year, month, this);
}
public getNationLogger(year: IntYear, month: IntMonth, nationID: NationID): NationLogger {
return new NationLogger(year, month, nationID, this);
}
public getGeneralLogger(year: IntYear, month: IntMonth, nationID: NationID, generalID: GeneralID): GeneralLogger {
return new GeneralLogger(year, month, nationID, generalID, this);
}
flushAll() {
if(this.logStack.size === 0){
return;
}
//rc?
throw new Error("Not yet implemented");
this.logStack.clear();
}
}
export class GameLogger {
protected stack: LoggerStack;
public constructor(
year: IntYear,
month: IntMonth,
protected readonly engine: GameLoggerEngine,
) {
this.stack = {
symbol: Symbol(),
year,
month,
global: {
action: [],
history: []
},
nation: {
nationID: 0,
history: []
},
general: {
action: [],
battle: [],
battleBrief: [],
history: [],
generalID: 0
}
};
}
pushGlobalActionLog(text: string) {
this.stack.global.action.push(text);
}
pushGlobalHistoryLog(text: string) {
this.stack.global.history.push(text);
}
reset() {
const { year, month } = this.stack;
const nationID = this.stack.nation.nationID;
const generalID = this.stack.general.generalID;
this.stack = {
symbol: Symbol(),
year,
month,
global: {
action: [],
history: []
},
nation: {
nationID,
history: []
},
general: {
action: [],
battle: [],
battleBrief: [],
history: [],
generalID
}
};
}
commit() {
this.engine.addStack(this.stack);
this.reset();
}
}
export class NationLogger extends GameLogger {
public constructor(
year: IntYear,
month: IntMonth,
private readonly nationID: NationID,
engine: GameLoggerEngine
) {
super(year, month, engine);
this.stack.nation.nationID = nationID;
}
pushNationLog(text: string) {
this.stack.nation.history.push(text);
}
}
export class GeneralLogger extends NationLogger {
public constructor(
year: IntYear,
month: IntMonth,
nationID: NationID,
private readonly generalID: GeneralID,
engine: GameLoggerEngine
) {
super(year, month, nationID, engine);
this.stack.general.generalID = generalID;
}
pushGeneralActionLog(text: string) {
this.stack.general.action.push(text);
}
pushGeneralBattleLog(text: string) {
this.stack.general.battle.push(text);
}
pushGeneralBattleBriefLog(text: string) {
this.stack.general.battleBrief.push(text);
}
pushGeneralHistoryLog(text: string) {
this.stack.general.history.push(text);
}
}
+126 -96
View File
@@ -1,33 +1,47 @@
import { InvalidArgument } from "@sammo/util";
import { InvalidArgument, NotYetImplemented } from "@sammo/util";
import { City } from "./City.js";
import type { IGeneralEntity } from "./GeneralEntity.js";
import type { IResourceController } from "./ResourceController.js";
import { DiplomaticPermission, type CityID, type GeneralID, type GeneralName, type NationID, OfficerLevel, type TroopID } from "./defs.js";
import type { IGeneralEntity } from "./Entity/GeneralEntity.js";
import type { IResourceController } from "./IResourceController.js";
import { DiplomaticPermission, type CityID, type GeneralID, type GeneralName, type NationID, OfficerLevel, type SquadID, NpcType, StaffLevel, CityOfficerLevel } from "./defs.js";
import { must } from "@sammo/util";
import { Nation } from "./Nation.js";
import { Troop } from "./Troop.js";
import { Squad } from "./Squad.js";
import { LazyEntityUpdater } from "./LazyEntityUpdater.js";
import type { IAction, IActionKey } from "./IAction.js";
import { addSeconds } from "date-fns";
export class General {
private _raw: IGeneralEntity;
private _city: WeakRef<City>;
private _troop: WeakRef<Troop> | undefined;
private _nation: WeakRef<Nation>;
export class General extends LazyEntityUpdater<IGeneralEntity> {
protected readonly _indirectNames: (keyof IGeneralEntity)[] = [
"id", "nationID", "squadID", "nationID", "turnTime"
];
protected readonly _entityType = "General";
protected readonly _raw: IGeneralEntity;
public get raw(): Readonly<IGeneralEntity> {
return this._raw;
}
protected _city!: WeakRef<City>;
protected _squad!: WeakRef<Squad> | undefined;
protected _nation!: WeakRef<Nation>;
protected readonly iActionHandlers = new Map<IActionKey, IAction>;
constructor(
raw: IGeneralEntity,
private resourceController: IResourceController
protected readonly rc: IResourceController
){
super();
this._raw = raw;
this._city = new WeakRef(must(resourceController.getCity(raw.cityID)));
this._nation = new WeakRef(must(resourceController.getNation(raw.nationID)));
}
if(raw.troopID){
this._troop = new WeakRef(must(resourceController.getTroop(raw.troopID, raw.nationID)));
public bootstrap(){
const rc = this.rc;
const raw = this._raw;
this._city = new WeakRef(must(rc.city(raw.cityID)));
this._nation = new WeakRef(must(rc.nation(raw.nationID)));
if(raw.squadID){
this._squad = new WeakRef(must(rc.squad(raw.squadID, raw.nationID)));
}
this.reconfigureIActionHandlers();
}
public get id(): GeneralID {
@@ -38,112 +52,128 @@ export class General {
return this._raw.name;
}
public update(callback: (oldRaw: IGeneralEntity) => void, force = false) {
public get city(): City {
return must(this._city.deref());
}
const oldCityID = this._raw.cityID;
const oldTroopID = this._raw.troopID;
const oldNationID = this._raw.nationID;
public get squad(): Squad | undefined {
return this._squad?.deref();
}
callback(this._raw);
public get nation(): Nation {
return must(this._nation.deref());
}
if(!force){
if(oldCityID !== this._raw.cityID){
throw new Error("City change by update() is not allowed");
}
if(oldTroopID !== this._raw.troopID){
throw new Error("Troop change by update() is not allowed");
}
if(oldNationID !== this._raw.nationID){
throw new Error("Nation change by update() is not allowed");
}
public get isUser(): boolean {
return this._raw.npcType <= NpcType.user_borrowed_npc;
}
public get staffLevel(): StaffLevel | undefined {
if(this._raw.officerLevel < OfficerLevel.staffIntel3){
return undefined;
}
this.resourceController.setDirtyGeneral(this._raw);
return this._raw.officerLevel as number as StaffLevel;
}
public changeCity(city: CityID | City){
const cityObj: City = (()=>{
if(city instanceof City){
return city;
}
const cityObj = this.resourceController.getCity(city);
if(!cityObj){
throw new InvalidArgument(`City not found: ${city}`);
}
return cityObj;
})();
this._city = new WeakRef(cityObj);
public get cityOfficerLevel(): CityOfficerLevel | undefined {
if(this._raw.officerLevel >= OfficerLevel.staffIntel3){
return undefined;
}
if(this._raw.officerLevel < OfficerLevel.cityLeadership){
return undefined;
}
//TODO: ResourceController에게 알려야 하는가?
this.update((raw) => {
raw.cityID = cityObj.id;
}, true);
return this._raw.officerLevel as number as CityOfficerLevel;
}
public quitTroop(){
if(!this._troop){
public changeCity(city: City){
this.city.notifyQuitGeneral(this);
this._city = new WeakRef(city);
this.forceUpdate((raw) => {
raw.cityID = city.id;
});
city.notifyJoinGeneral(this);
this.reconfigureIActionHandlers();
}
public quitSquad(){
if(!this._squad){
return;
}
const troop = must(this._troop.deref());
if(troop.leader.id === this._raw.id){
troop.destructTroop(false);
const squad = must(this._squad.deref());
if(squad.leader.id === this._raw.id){
squad.destroySquad(false);
}
else{
troop.members.delete(this._raw.id);
squad.notifyQuitGeneral(this);
}
this._troop = undefined;
this.update((raw) => {
raw.troopID = undefined;
}, true);
this._squad = undefined;
this.forceUpdate((raw) => {
raw.squadID = undefined;
});
}
public changeTroop(troop: TroopID | Troop){
this.quitTroop();
public changeSquad(squad: Squad){
this.quitSquad();
const troopObj: Troop = (()=>{
if(troop instanceof Troop){
return troop;
}
const troopObj = this.resourceController.getTroop(troop, this._raw.nationID);
if(!troopObj){
throw new InvalidArgument(`Troop not found: ${troop}`);
}
return troopObj;
})();
troopObj.members.set(this._raw.id, new WeakRef(this));
this._troop = new WeakRef(troopObj);
this.update((raw) => {
raw.troopID = troopObj.id;
}, true);
this._squad = new WeakRef(squad);
this.forceUpdate((raw) => {
raw.squadID = squad.id;
});
squad.notifyJoinGeneral(this);
}
public changeNation(nation: NationID | Nation){
const nationObj: Nation = (()=>{
if(nation instanceof Nation){
return nation;
}
const nationObj = this.resourceController.getNation(nation);
if(!nationObj){
throw new InvalidArgument(`Nation not found: ${nation}`);
}
return nationObj;
})();
this._nation = new WeakRef(nationObj);
this.quitTroop();
public changeNation(nation: Nation){
this.quitSquad();
this.nation.notifyQuitGeneral(this);
this.update((raw) => {
raw.nationID = nationObj.id;
this._nation = new WeakRef(nation);
this.forceUpdate((raw) => {
raw.nationID = nation.id;
raw.diplomaticPermission = DiplomaticPermission.none;
if(nationObj.id == 0){
if(nation.id == 0){
raw.officerLevel = OfficerLevel.none;
}
else{
raw.officerLevel = OfficerLevel.normal;
}
raw.nationBelong = 0;
}, true);
raw.nationJoinedYearMonth = this.rc.gameEnv.raw.yearMonth;
});
nation.notifyJoinGeneral(this);
this.reconfigureIActionHandlers();
}
private reconfigureIActionHandlers(){
throw new NotYetImplemented();
}
public updateTurnTime(){
const secondsPerTurn = this.rc.gameEnv.raw.secondsPerTurn;
this.forceUpdate((raw) => {
raw.turnTime = addSeconds(raw.turnTime, secondsPerTurn);
});
}
public compareTurnTime(other: General): number {
if(this._raw.turnTime < other._raw.turnTime){
return -1;
}
if(this._raw.turnTime > other._raw.turnTime){
return 1;
}
if(this._raw.id < other._raw.id){
return -1;
}
if(this._raw.id > other._raw.id){
return 1;
}
return 0;
}
}
+20 -1
View File
@@ -1,4 +1,6 @@
import type { ValuesOf } from "@sammo/util";
import type * as IMethod from "./IActionMethod.js";
import type { ItemKeyType } from "./defs.js";
export type * as IMethod from "./IActionMethod.js";
export interface IActionMethod extends
@@ -20,4 +22,21 @@ export interface IAction extends Partial<IActionMethod> {
getName(): string;
getInfo(): string[] | string;
}
}
export const IActionOrder = [
'nationType',
'officerLevel',
'specialityDomestic',
'specialityWar',
'personlity',
'armyType',
'inheritBuff',
'scenarioEffect',
'horse',
'weapon',
'book',
'item',
];
export type IActionKey = ValuesOf<IAction>;
+2 -2
View File
@@ -3,14 +3,14 @@ import type { WarUnitTriggerCaller } from "./TriggerCaller/WarUnitTriggerCaller.
import type { GeneralTriggerCaller } from "./TriggerCaller/GeneralTriggerCaller.js";
import type { General } from "./General.js";
import type { WarUnit } from "./WarUnit.js";
import type { ArbitraryActionType, CalcIncomeType, CalcStatName, CalcStatRangeName, CalcStrategicVarType, StrategicTurnType } from "./defs.js";
import type { ArbitraryActionType, CalcIncomeType, CalcStatName, CalcStatRangeName, CalcStrategicVarType, StrategicCmdType } from "./defs.js";
//TODO: turnType, varType이 string이면 안됨. enum에 가까워야함!
type onCalcDomesticFunc = (turnType: string, varType: string, value: number, aux?: unknown[]) => number;
type onCalcStatFunc = (general: General, statName: CalcStatName, value: number, aux?: unknown[]) => number;
type onCalcStatRangeFunc = (general: General, statName: CalcStatRangeName, value: [number, number], aux?: unknown[]) => [number, number];
type onCalcStrategicFunc = (turnType: StrategicTurnType, varType: CalcStrategicVarType, value: number) => number;
type onCalcStrategicFunc = (turnType: StrategicCmdType, varType: CalcStrategicVarType, value: number) => number;
type onCalcIncomeFunc = (type: CalcIncomeType, value: number) => number;
type getWarPowerMultiplierFunc = (unit: WarUnit) => [number, number];
@@ -0,0 +1,72 @@
import type { General } from "./General.js";
import type { City } from "./City.js";
import type { Nation } from "./Nation.js";
import type { CityID, GeneralID, NationID, ReservedTurn, SquadID } from "./defs.js";
import type { GameEnv } from "./GameEnv.js";
import type { IGeneralEntity } from "./Entity/GeneralEntity.js";
import type { Squad } from "./Squad.js";
import type { ISquadEntity } from "./Entity/SquadEntity.js";
import type { LazyEntityUpdater } from "./LazyEntityUpdater.js";
import type { ICityEntity } from "./Entity/CityEntity.js";
import type { INationEntity } from "./Entity/NationEntity.js";
import type { EntityType, ValidEntityType } from "./Entity/index.js";
export interface IResourceController {
nation(id: NationID): Nation | undefined;
city(id: CityID): City | undefined;
squad(id: SquadID, nation: NationID): Squad | undefined;
general(id: GeneralID): General | undefined;
peekGeneralTurnTimeQueue(): General | undefined;
popGeneralTurnTimeQueue(): General | undefined;
insertGeneralTurnTimeQueue(general: General, maybeTail: boolean): void;
readonly gameEnv: GameEnv;
readonly nationList: Map<NationID, Nation>;
readonly cityList: Map<CityID, City>;
readonly squadList: Map<SquadID, Squad>;
readonly generalList: Map<GeneralID, General>;
//----Insert, Update, Delete
reserveUpdate(entity: LazyEntityUpdater<any>): void;
reserveDelete(entity: LazyEntityUpdater<any>): void;
//TODO: create* 방식과 구현 방식 비교
reserveInsert(entity: LazyEntityUpdater<any>): void;
//TODO: reserveInsert와 구현 방식 비교
createGeneral(raw: IGeneralEntity): General;
createNation(raw: INationEntity): Nation;
createSquad(raw: ISquadEntity): Squad;
_getAllChanges(): Record<
ValidEntityType,
Map<number, [op: 'insert' | 'update' | 'delete', value: LazyEntityUpdater<any>]>
>;
//---- DB 접근 ---
//예턴은 호출마다 DB에서 조회
getReservedTurn(general: General): Promise<ReservedTurn>;
nationReservedTurn(general: General): Promise<ReservedTurn | undefined>;
resetAndFill(): Promise<void>;
saveAll(): Promise<void>;
}
let _resourceController: IResourceController | undefined = undefined;
export const setResourceController = (rc: IResourceController) => {
_resourceController = rc;
}
export const getResourceController = () => {
if (_resourceController === undefined) {
throw new Error("ResourceController not set");
}
return _resourceController;
}
@@ -0,0 +1,39 @@
import type { ValidEntity, EntityType } from "./Entity/index.js";
import type { IResourceController } from "./IResourceController.js";
export abstract class LazyEntityUpdater<Entity extends ValidEntity>{
protected abstract readonly _indirectNames: ReadonlyArray<keyof Entity>;
protected abstract readonly _entityType: EntityType<Entity>;
protected abstract readonly _raw: Entity;
protected abstract readonly rc: IResourceController;
public abstract bootstrap(): void;
public get raw(): Readonly<Entity> {
return this._raw;
}
public get entityType(): EntityType<Entity> {
return this._entityType;
}
protected forceUpdate(callback: (raw: Entity) => void) {
callback(this._raw);
this.rc.reserveUpdate(this);
}
public update(callback: (raw: Entity) => void) {
const oldRaw = { ...this._raw };
callback(this._raw);
//for구문이 빠른가, Proxy setter로 제어하는게 빠른가
for (const indirectName of this._indirectNames) {
if (oldRaw[indirectName] !== this._raw[indirectName]) {
throw new Error(`${String(indirectName)} change by update() is not allowed`);
}
}
this.rc.reserveUpdate(this);
}
}
@@ -0,0 +1,20 @@
import type { General } from "@/General.js";
import { NotYetImplemented } from "@sammo/util";
const battleMonthThreshold = 12;
export function categorizeGeneral(general: General): 'war' | 'domestic' {
const nationLastBattle = general.nation.raw.aux.lastBattleYearMonth;
const lastBattle = general.raw.aux.lastBattleYearMonth;
if(lastBattle !== undefined && nationLastBattle !== undefined){
if(lastBattle >= nationLastBattle - battleMonthThreshold){
return 'war';
}
}
throw new NotYetImplemented();
return 'domestic';
}
+158 -6
View File
@@ -1,9 +1,161 @@
import type { NationID, NationName, NationType } from "./defs.js";
import { LazyEntityUpdater } from "./LazyEntityUpdater.js";
import type { INationEntity } from "./Entity/NationEntity.js";
import type { IResourceController } from "./IResourceController.js";
import { StaffLevel, type CityID, type GeneralID, type NationID, type NationName, type SquadID } from "./defs.js";
import type { City } from "./City.js";
import { General } from "./General.js";
import { must } from "@sammo/util";
import type { Squad } from "./Squad.js";
import { LogicError } from "./Errors.js";
import { categorizeGeneral } from "./LogicFunc/categorizeGeneral.js";
export class Nation extends LazyEntityUpdater<INationEntity> {
protected readonly _indirectNames: (keyof INationEntity)[] = ["id"];
protected readonly _entityType = "Nation";
protected readonly _raw: INationEntity;
protected readonly categorizedNPCGeneralList = {
war: new Map<GeneralID, WeakRef<General>>(),
domestic: new Map<GeneralID, WeakRef<General>>()
};
protected readonly categorizedUserGeneralList = {
war: new Map<GeneralID, WeakRef<General>>(),
domestic: new Map<GeneralID, WeakRef<General>>()
};
protected readonly cityList = new Map<CityID, WeakRef<City>>;
protected readonly squadList = new Map<SquadID, WeakRef<Squad>>;
protected readonly generalList = new Map<GeneralID, WeakRef<General>>;
protected readonly npcGeneralList = new Map<GeneralID, WeakRef<General>>();
protected readonly userGeneralList = new Map<GeneralID, WeakRef<General>>();
protected readonly warNPCGeneralList = new Map<GeneralID, WeakRef<General>>();
protected readonly warUserGeneralList = new Map<GeneralID, WeakRef<General>>();
protected readonly domesticNPCGeneralList = new Map<GeneralID, WeakRef<General>>();
protected readonly domesticUserGeneralList = new Map<GeneralID, WeakRef<General>>();
protected readonly staffList = new Map<StaffLevel, WeakRef<General>>();
export class Nation {
constructor(
public readonly id: NationID,
public readonly name: NationName,
public readonly type: NationType,
) { }
raw: INationEntity,
protected readonly rc: IResourceController,
bootstrapInfo: {
general: GeneralID[];
squad: SquadID[];
city: CityID[];
},
) {
super();
this._raw = raw;
this._bootstrapInfo = bootstrapInfo;
}
private _bootstrapInfo?: ConstructorParameters<typeof Nation>[2];
bootstrap() {
const rc = this.rc;
const bootstrapInfo = must(this._bootstrapInfo);
delete this._bootstrapInfo;
const bootstrapCityID = bootstrapInfo.city;
for (const city of bootstrapCityID.map((id) => must(rc.cityList.get(id)))) {
this.notifyJoinCity(city);
}
const bootstrapSquadID = bootstrapInfo.squad;
for(const squad of bootstrapSquadID.map((id) => must(rc.squadList.get(id)))){
this.notifyAddSquad(squad);
}
const bootstrapGeneralID = bootstrapInfo.general;
for (const general of bootstrapGeneralID.map((id) => must(rc.generalList.get(id)))) {
this.notifyJoinGeneral(general);
}
}
public get id(): NationID {
return this._raw.id;
}
public get name(): NationName {
return this._raw.name;
}
public notifyAddSquad(squad: Squad) {
this.squadList.set(squad.id, new WeakRef(squad));
}
public notifyDestroySquad(squad: Squad) {
this.squadList.delete(squad.id);
}
public notifyJoinCity(city: City) {
this.cityList.set(city.id, new WeakRef(city));
}
public notifyQuitCity(city: City) {
this.cityList.delete(city.id);
}
public notifyQuitGeneral(general: General) {
const generalID = general.id;
this.generalList.delete(generalID);
if (general.isUser) {
this.userGeneralList.delete(generalID);
this.warUserGeneralList.delete(generalID);
this.domesticUserGeneralList.delete(generalID);
} else {
this.npcGeneralList.delete(generalID);
this.warNPCGeneralList.delete(generalID);
this.domesticNPCGeneralList.delete(generalID);
}
const staffLevel = general.staffLevel;
if (staffLevel !== undefined) {
this.staffList.delete(staffLevel);
if (staffLevel === StaffLevel.lord) {
throw new LogicError(`Lord is kicked: nationID=${this.id}`);
}
}
if (general.squad) {
general.quitSquad();
}
}
public notifyJoinGeneral(general: General) {
//국가에 장수가 들어오는 경우, class내 변수에 대한 처리.
const generalID = general.id;
this.generalList.set(generalID, new WeakRef(general));
if (general.isUser) {
this.userGeneralList.set(generalID, new WeakRef(general));
if (categorizeGeneral(general) === "war") {
this.warUserGeneralList.set(generalID, new WeakRef(general));
}
else {
this.domesticUserGeneralList.set(generalID, new WeakRef(general));
}
} else {
this.npcGeneralList.set(generalID, new WeakRef(general));
if (categorizeGeneral(general) === "war") {
this.warNPCGeneralList.set(generalID, new WeakRef(general));
}
else {
this.domesticNPCGeneralList.set(generalID, new WeakRef(general));
}
}
const staffLevel = general.staffLevel;
if (staffLevel !== undefined) {
this.staffList.set(staffLevel, new WeakRef(general));
}
}
}
@@ -1,64 +0,0 @@
import type { General } from "./General.js";
import type { City } from "./City.js";
import type { Nation } from "./Nation.js";
import type { CityID, GeneralID, NationID, TroopID } from "./defs.js";
import type { GameEnv } from "./GameEnv.js";
import type { IGeneralEntity } from "./GeneralEntity.js";
import type { Troop } from "./Troop.js";
import type { ITroopEntity } from "./TroopEntity.js";
export interface IResourceController{
addCity(city: City): boolean;
addNation(nation: Nation): boolean;
addGeneral(general: General): boolean;
getCity(id: CityID): City | undefined;
getNation(id: NationID): Nation | undefined;
getGeneral(id: GeneralID): General | undefined;
getGameEnv(): Promise<GameEnv>;
findAllNations(): Map<NationID, Nation>;
findAllCities(): Map<CityID, City>;
findAllGenerals(): Map<GeneralID, General>;
findGeneralByTroop(id: TroopID): Map<GeneralID, General>;
findCityByNation(id: NationID): Map<CityID, City>;
findGeneralByNation(id: NationID): Map<GeneralID, General>;
findGeneralByCity(id: CityID, nationID?: NationID): Map<GeneralID, General>;
getTroop(id: TroopID, nation: NationID): Troop | undefined;
_getAllChanges(): Map<unknown, unknown>;
setDirtyGeneral(rawGeneral: IGeneralEntity): void;
setDirtyCity(city: City): void;
setDirtyTroop(troop: Troop | TroopID): void;
setDirtyNation(nation: Nation): void;
createGeneral(raw: IGeneralEntity): General;
createTroop(raw: ITroopEntity): Troop;
//createNation
reserveDeleteTroop(id: TroopID): void;
reserveDeleteGeneral(id: GeneralID): void;
reserveDeleteNation(id: NationID): void;
resetAndFill(): Promise<void>;
saveAll(): Promise<void>;
}
let _resourceController: IResourceController | undefined = undefined;
export const setResourceController = (resourceController: IResourceController) => {
_resourceController = resourceController;
}
export const getResourceController = () => {
if (_resourceController === undefined) {
throw new Error("ResourceController not set");
}
return _resourceController;
}
+86
View File
@@ -0,0 +1,86 @@
import { must } from "@sammo/util";
import type { General } from "./General.js";
import type { IResourceController } from "./IResourceController.js";
import type { ISquadEntity } from "./Entity/SquadEntity.js";
import type { GeneralID } from "./defs.js";
import { LazyEntityUpdater } from "./LazyEntityUpdater.js";
export class Squad extends LazyEntityUpdater<ISquadEntity> {
protected readonly _indirectNames: (keyof ISquadEntity)[] = ["id"];
protected readonly _entityType = "Squad";
protected readonly _raw: ISquadEntity;
private _leader!: WeakRef<General>;
private _members!: Map<GeneralID, WeakRef<General>>;
constructor(
raw: ISquadEntity,
protected rc: IResourceController,
bootstrapInfo: {
general: GeneralID[]
}
) {
super();
this._raw = raw;
this._bootstrapInfo = bootstrapInfo;
}
private _bootstrapInfo?: ConstructorParameters<typeof Squad>[2];
public bootstrap() {
const rc = this.rc;
const bootstrapInfo = must(this._bootstrapInfo);
delete this._bootstrapInfo;
this._members = new Map(bootstrapInfo.general.map((id) => {
return [id, new WeakRef(must(rc.general(id)))];
}));
const leaderID = this._raw.id as GeneralID;
this._leader = must(this._members.get(leaderID));
this._members.delete(leaderID);
}
public get id() {
return this._raw.id;
}
public get leader() {
return must(this._leader.deref());
}
public get members(): Map<GeneralID, WeakRef<General>> {
return this._members;
}
public destroySquad(withLeader = true) {
this._members.forEach((member) => {
const general = must(member.deref());
general.quitSquad();
});
if (withLeader) {
this.leader.quitSquad();
}
this.leader.nation.notifyDestroySquad(this);
this.rc.reserveDelete(this);
}
public changeName(name: string) {
this.update((raw) => {
raw.name = name;
});
}
public notifyQuitGeneral(general: General) {
if(general.id === this._leader.deref()?.id){
throw new Error("Leader cannot quit");
}
this._members.delete(general.id);
}
public notifyJoinGeneral(general: General) {
this._members.set(general.id, new WeakRef(general));
}
}
-53
View File
@@ -1,53 +0,0 @@
import { must } from "@sammo/util";
import type { General } from "./General.js";
import type { IResourceController } from "./ResourceController.js";
import type { ITroopEntity } from "./TroopEntity.js";
import type { GeneralID } from "./defs.js";
export class Troop {
private _raw: ITroopEntity
private _leader: WeakRef<General>;
private _members: Map<GeneralID, WeakRef<General>>;
public get raw(): Readonly<ITroopEntity> {
return this._raw;
}
constructor(
raw: ITroopEntity,
private resourceController: IResourceController
){
this._raw = raw;
const members = resourceController.findGeneralByTroop(raw.id);
const leaderID: GeneralID = raw.id as GeneralID;
this._leader = new WeakRef(must(members.get(leaderID)));
members.delete(leaderID);
this._members = new Map([...members].map(([k,v])=>[k, new WeakRef(v)]));
}
public get id(){
return this._raw.id;
}
public get leader(){
return must(this._leader.deref());
}
public get members(): Map<GeneralID, WeakRef<General>> {
return this._members;
}
public destructTroop(withLeader = true){
this._members.forEach((member)=>{
const general = must(member.deref());
general.quitTroop();
});
if(withLeader){
this.leader.quitTroop();
}
this.resourceController.reserveDeleteTroop(this._raw.id);
}
}
-7
View File
@@ -1,7 +0,0 @@
import type { NationID, TroopID } from "./defs.js";
export interface ITroopEntity {
id: TroopID;
nationID: NationID;
name: string;
}
+167 -19
View File
@@ -1,6 +1,6 @@
//Type 정의
export type OwnerID = number & { zz_t?: "OwnerID" };
export type OwnerName = string & { zz_t?: "OwnerName" };
export type UserID = number & { zz_t?: "UserID" };
export type UserName = string & { zz_t?: "UserName" };
export type GeneralID = number & { zz_t?: "GeneralID" };
export type GeneralName = string & { zz_t?: "GeneralName" };
@@ -8,15 +8,23 @@ export type GeneralName = string & { zz_t?: "GeneralName" };
export type CityID = number & { zz_t?: "CityID" };
export type CityName = string & { zz_t?: "CityName" };
export type TroopID = number & { zz_t?: "TroopID" };
export type TroopName = string & { zz_t?: "TroopName" };
export type SquadID = number & { zz_t?: "SquadID" };
export type SquadName = string & { zz_t?: "SquadName" };
export type NationID = number & { zz_t?: "NationID" };
export type NationName = string & { zz_t?: "NationName" };
//지역 ID
export type RegionID = number & { zz_t?: "RegionID" };
//서버 ID
export type ServerID = number & { zz_t?: "ServerID" };
export type ScenarioID = number & { zz_t?: "ScenarioID" };
//TODO: 무언가의 keyof 로 바꿔야함
export type NationType = string & { zz_t?: "NationType" };
export type StrategicTurnType = string & { zz_t?: "StrategicTurnType" };
export type ItemKeyType = 'horse' | 'weapon' | 'book' | 'item';
export type ItemID = string & { zz_t?: "ItemID" };
@@ -31,24 +39,13 @@ export const enum NpcType {
npc_scenario = 2,
npc_generated = 3,
npc_strategic = 4,
npc_troop_leader = 5,
npc_squad_leader = 5,
npc_unselectable = 6,
npc_invader = 9,
}
export const enum OfficerLevel {
//재야
none = 0,
//일반
normal = 1,
//도시관직
cityLeadership = 2,
cityIntel = 3,
cityStrength = 4,
export const enum StaffLevel {
//지력관직
staffIntel3 = 5,
staffIntel2 = 7,
@@ -64,7 +61,45 @@ export const enum OfficerLevel {
//군주
lord = 12,
}
export const enum CityOfficerLevel {
cityLeadership = 2,
cityIntel = 3,
cityStrength = 4,
}
//XXX: const enum의 merge가 없어?
export const enum OfficerLevel {
//재야
none = 0,
//일반
normal = 1,
//도시관직
cityLeadership = CityOfficerLevel.cityLeadership,
cityIntel = CityOfficerLevel.cityIntel,
cityStrength = CityOfficerLevel.cityStrength,
//지력관직
staffIntel3 = StaffLevel.staffIntel3,
staffIntel2 = StaffLevel.staffIntel2,
staffIntel1 = StaffLevel.staffIntel1,
//무력관직
staffStrength3 = StaffLevel.staffStrength3,
staffStrength2 = StaffLevel.staffStrength2,
staffStrength1 = StaffLevel.staffStrength1,
//참모
staffChief = StaffLevel.staffChief,
//군주
lord = StaffLevel.lord,
}
export const enum DiplomaticPermission {
@@ -73,6 +108,87 @@ export const enum DiplomaticPermission {
ambassador = 2,
}
export const enum NationLevel {
= 0,
= 1,
= 2,
= 3,
= 4,
= 5,
= 6,
= 7,
}
export const enum CityLevel {
= 1,
= 2,
= 3,
= 4,
= 5,
= 6,
= 7,
= 8,
}
export const enum FrontStatus {
//출병가능한 도시 없음. 중립.
Neutral = 0,
//전쟁 중인 국가가 인접함
NearWar = 1,
//선포 중인 국가가 인접함
NearDeclare = 2,
//공백지 있음
NearEmpty = 3,
}
export type NationCmdType = string & { zz_t?: "NationCmdType" };
export type StrategicCmdType = string & { zz_t?: "StrategicCmdType" };
//string? const or? Enum?
export type ColorType = string & { zz_t?: "ColorType" };
//XXX: 기존처럼 값 하나만 유지할 것인가?
export const enum CityState {
//일반
normal = 0,
//풍년
goodHarvest = 1,
//호황
commercialBoom = 2,
//혹한
coldWave = 3,
//역병
plague = 4,
//지진
earthquake = 5,
//태풍
typhoon = 6,
//홍수
flood = 7,
//메뚜기 떼
locust = 8,
//도적
thief = 9,
}
//NOTE: PHP버전과 수치가 다름
export const enum DiplomacyState{
//중립
neutral = 0,
//선포중
declare = 1,
//전쟁중
war = 2,
//불가침
inviolable = 7,
}
/* 전용 stat이어서 확장하고자 하는경우 아래와 같이 확장 가능
```ts
@@ -123,4 +239,36 @@ export interface WarSkillNameBase {
회피불가: unknown;
회피: unknown;
}
export type WarSkillName = keyof WarSkillNameBase;
export type WarSkillName = keyof WarSkillNameBase;
export interface ReservedTurn {
action: string;
arg: Record<string, unknown>;
brief: string;
}
export type LogType = 'action' | 'battleBrief' | 'battle' | 'history';
export type MessageType = 'private' | 'national' | 'public' | 'diplomacy' | 'system';
export const enum ArmyClass {
//기병
cavalry = 0,
//보병
infantry = 1,
//궁병
archer = 2,
}
//특수 숫자 정의
//연 * 12 + 월
export type IntYearMonth = number & { zz_t?: "YearMonth" };
export type IntMonth = number & { zz_t?: "IntMonth" };
export type IntYear = number & { zz_t?: "IntYear" };
//1.0을 기준으로한 number 타입
export type FractionalRange = number & { zz_t?: "FractionalRange" };
//특수 문자열 정의
export type URILike = string & { zz_t?: "URILike" };
+4 -2
View File
@@ -1,5 +1,7 @@
export * from "./generalStats.js";
export * from "./LogicFunc/generalStats.js";
export * as defs from "./defs.js";
export * from "./IResourceController.js";
export * from "./ActionRequest/index.js";
export * from "./GameLogger.js";
// NOTE: game_logic은 client, server 모두에서 사용되는 라이브러리이다.
// DB에 접근한다면 DI여야하나?
+21 -24
View File
@@ -20,33 +20,30 @@
"@sammo/util": "workspace:^",
"@strpc/client_ky": "workspace:^",
"@strpc/def": "workspace:^",
"bootstrap": "^5.3.3",
"bootstrap-vue-next": "^0.16.6",
"bson": "^6.4.0",
"date-fns": "^3.3.1",
"ky": "^1.2.2",
"@trpc/client": "^11.6.0",
"bootstrap": "^5.3.8",
"bootstrap-vue-next": "^0.40.6",
"bson": "^6.10.4",
"date-fns": "^4.1.0",
"ky": "^1.11.0",
"lodash-es": "^4.17.21",
"pinia": "^2.1.7",
"vue": "^3.4.21",
"vue-router": "^4.3.0"
"pinia": "^3.0.3",
"vue": "^3.5.22",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@tsconfig/node20": "^20.1.2",
"@tsconfig/node20": "^20.1.6",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.11.24",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.5.1",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.22.0",
"prettier": "^3.2.5",
"ts-node": "^10.9.2",
"tslib": "^2.6.2",
"vite": "^5.1.5",
"vue-tsc": "^2.0.5",
"zod": "^3.22.4"
},
"peerDependencies": {
"zod": "^3.22.2"
"@types/node": "^24.7.0",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.6.0",
"@vue/tsconfig": "^0.8.1",
"eslint": "^9.37.0",
"eslint-plugin-vue": "^10.5.0",
"prettier": "^3.6.2",
"tslib": "^2.8.1",
"vite": "^7.1.9",
"vue-tsc": "^3.1.1",
"zod": "^4.1.12"
}
}
+13 -11
View File
@@ -20,19 +20,21 @@
"@sammo/server_util": "workspace:^",
"@sammo/util": "workspace:^",
"@strpc/express": "workspace:^",
"date-fns": "^3.3.1",
"dotenv": "^16.4.5",
"express": "^4.18.3",
"express-session": "^1.18.0",
"@trpc/client": "^11.6.0",
"@trpc/server": "^11.6.0",
"date-fns": "^4.1.0",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"express-session": "^1.18.2",
"lodash-es": "^4.17.21",
"mongoose": "^8.2.1",
"reflect-metadata": "^0.2.1",
"tslib": "^2.6.2",
"zod": "^3.22.4"
"mongoose": "^8.19.1",
"reflect-metadata": "^0.2.2",
"tslib": "^2.8.1",
"zod": "^4.1.12"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/express-session": "^1.18.0",
"@types/node": "^20.11.24"
"@types/express": "^5.0.3",
"@types/express-session": "^1.18.2",
"@types/node": "^24.7.0"
}
}
@@ -3,7 +3,7 @@ import { StartSession } from "@sammo/server_util";
import { POST, type APIReturnType } from "@strpc/express";
import { declProcDecorators } from "@strpc/express/proc_decorator";
import { z } from "zod";
import { loginCtxSessionKey, type GatewayLoginCtx } from "@/procDecorator/ReqGatewayLogin.js";
import { gatewayLoginCtxSessionKey, type GatewayLoginCtx } from "@/procDecorator/ReqGatewayLogin.js";
import { delay } from "@sammo/util";
type BaseAPI = typeof structure.Login.LoginByID;
@@ -52,7 +52,7 @@ export const LoginByID = POST<BaseAPI>(LoginByIDReq)(declProcDecorators(
loginDate: new Date(),
}
ctx.session.setItem(loginCtxSessionKey, loginCtx);
ctx.session.setItem(gatewayLoginCtxSessionKey, loginCtx);
return {
@@ -3,7 +3,7 @@ import { StartSession } from "@sammo/server_util";
import { POST, type APIReturnType } from "@strpc/express";
import { declProcDecorators } from "@strpc/express/proc_decorator";
import { z } from "zod";
import { loginCtxSessionKey, type GatewayLoginCtx } from "@/procDecorator/ReqGatewayLogin.js";
import { gatewayLoginCtxSessionKey, type GatewayLoginCtx } from "@/procDecorator/ReqGatewayLogin.js";
import { delay } from "@sammo/util";
type BaseAPI = typeof structure.Login.LoginByToken;
@@ -38,7 +38,7 @@ export const LoginByToken = POST<BaseAPI>(LoginByTokenReq)(declProcDecorators(
loginDate: new Date(),
}
ctx.session.setItem(loginCtxSessionKey, loginCtx);
ctx.session.setItem(gatewayLoginCtxSessionKey, loginCtx);
//throw new Error("Method not implemented.");
+1 -1
View File
@@ -1,5 +1,5 @@
import './dotenv.js';
import { connect, Mongoose } from "mongoose";
import { connect,type Mongoose } from "mongoose";
import { unwrap } from '@sammo/util';
const dbConfig = {
@@ -0,0 +1,37 @@
import type { SessionCtx } from "@sammo/server_util";
import type { ProcDecorator } from "@strpc/express/proc_decorator";
import type { GatewayLoginCtx } from "./ReqGatewayLogin.js";
import type { GatewayActionType } from "@/schema/User.js";
export function ReqGatewayACL<Q extends GatewayLoginCtx & SessionCtx>(...actions: GatewayActionType[]): ()=>ProcDecorator<Q, Q> {
return () => {
return (ctx) => {
if (actions.length === 0) {
return [{
result: true,
}, ctx];
}
if(!ctx.allowGatewayAction){
return [{
result: false,
type: 'Required ACL',
info: 'No Gateway ACL in session context.'
}, ctx];
}
for (const action of actions) {
if (!ctx.allowGatewayAction.has(action)) {
return [{
result: false,
type: 'Required ACL',
info: `NotEnoughPermission: ${action}`
}, ctx];
}
}
return [{
result: true,
}, ctx];
}
}
}
@@ -10,11 +10,11 @@ export type GatewayLoginCtx = {
allowGatewayAction: Set<GatewayActionType>;
loginDate: Date;
}
export const loginCtxSessionKey = 'loginCtx';
export const gatewayLoginCtxSessionKey = 'gatewayLoginCtx';
export function ReqGatewayLogin<Q extends SessionCtx>(): ProcDecorator<GatewayLoginCtx & Q, Q> {
return (ctx) => {
const loginCtx = ctx.session.getItem<GatewayLoginCtx>(loginCtxSessionKey);
const loginCtx = ctx.session.getItem<GatewayLoginCtx>(gatewayLoginCtxSessionKey);
if(!loginCtx){
return [{
result: false,
@@ -0,0 +1,38 @@
import type { SessionCtx } from "@sammo/server_util";
import type { ProcDecorator } from "@strpc/express/proc_decorator";
import type { ServerActionType } from "@sammo/gateway_server/exports"
import type { GatewayLoginCtx } from "./ReqGatewayLogin.js";
export function ReqServerACL<Q extends GatewayLoginCtx & SessionCtx>(serverName: string, ...actions: ServerActionType[]): ()=>ProcDecorator<Q, Q> {
return () => {
return (ctx) => {
if (actions.length === 0) {
return [{
result: true,
}, ctx];
}
const allowServerAction = ctx.allowServerAction.get(serverName);
if(!allowServerAction){
return [{
result: false,
type: 'Required ACL',
info: 'No Server ACL in session context.'
}, ctx];
}
for (const action of actions) {
if (!allowServerAction.has(action)) {
return [{
result: false,
type: 'Required ACL',
info: `NotEnoughPermission: ${action}`
}, ctx];
}
}
return [{
result: true,
}, ctx];
}
}
}
@@ -1,5 +1,5 @@
import { type UserIDType } from "./User.js";
import { Mongoose, Schema, model } from "mongoose";
import { type Mongoose, Schema, model } from "mongoose";
export interface ILoginToken {
userID: UserIDType;
+2 -2
View File
@@ -38,7 +38,7 @@ export interface IUser {
allowGatewayAction?: ServerActionType[];
penalty?: Map<string, Date>;
picture?: string;
icon?: string;
useImgSvr?: boolean;
regDate: Date;
@@ -75,7 +75,7 @@ export const User = new Schema<IUser>({
},
penalty: { type: Map, required: false, of: Date },
picture: { type: String, required: false },
icon: { type: String, required: false },
useImgSvr: { type: Boolean, required: false },
regDate: { type: Date, required: true },
+2 -2
View File
@@ -14,9 +14,9 @@
"@sammo/util": "workspace:^"
},
"devDependencies": {
"bson": "^6.4.0",
"bson": "^6.10.4",
"buffer": "^6.0.3",
"zod": "^3.22.4"
"zod": "^4.1.12"
},
"peerDependencies": {
"bson": "^5.4.0",
+3 -2
View File
@@ -5,8 +5,7 @@
"main": "dist/index.js",
"scripts": {
"build": "tsc --build",
"dev": "tsc --watch",
"worker_test": "tsx src/worker_test.ts"
"dev": "tsc --watch"
},
"author": "Hide_D <hided62@gmail.com>",
"type": "module",
@@ -18,6 +17,8 @@
"@sammo/server_util": "workspace:^",
"@sammo/util": "workspace:^",
"@strpc/express": "workspace:^",
"@trpc/client": "^11.6.0",
"@trpc/server": "^11.6.0",
"date-fns": "^3.3.1",
"denque": "^2.1.0",
"dotenv": "^16.4.5",
+285 -6
View File
@@ -1,30 +1,309 @@
import { Signal, delay, must, must_err } from "@sammo/util";
import Denque from "denque";
import { ResourceController } from "./ResourceController.js";
import { entriesWithType } from "@sammo/util/converter";
import db from "./connectDB.js";
import type { Mongoose } from "mongoose";
import type { ActionPack, GameEngineMsg } from "./GameEngineDefs.js";
import { ActionRequestList, GameLoggerEngine, type QueueType } from "@sammo/game_logic";
export interface ActionRequest {
name: keyof ActionPack;
args: Parameters<ActionPack[keyof ActionPack]>[1];
}
export interface ActionRequestContainer {
waiterSeq: number;
action: ActionRequest;
}
export type ActionSuccess = {
success: true;
value: ReturnType<ActionPack[keyof ActionPack]>;
}
export type ActionFailure = {
success: false;
reason: string;
}
export type ActionResult = ActionSuccess | ActionFailure;
export class GameEngine {
private static instance: GameEngine | null = null;
private npcActionQueue = new Denque<unknown>();
private apiActionQueue = new Denque<unknown>();
private actionSeq = 0;
private constructor() {
private queueWaiters = new Map<number, [resolve: (result: ActionSuccess) => void, reject: (reason: ActionFailure) => void]>();
private signal = new Signal(30000);
private actionQueue = {
server: new Denque<ActionRequestContainer>(),
//npc: new Denque<ActionRequestContainer>(),
api: new Denque<ActionRequestContainer>(),
} as const satisfies Record<QueueType, Denque<ActionRequestContainer>>;
private rc: ResourceController
private loggerEngine: GameLoggerEngine;
private constructor(
db: Mongoose,
private postStatus: (msg: GameEngineMsg) => void,
) {
this.rc = new ResourceController(db);
this.loggerEngine = GameLoggerEngine.initInstance(this.rc);
}
private continueRun = true;
private prevSaveWaiter: Promise<void> | null = null;
private async processGeneralTurn() {
const now = new Date();
let processedGeneralCount = 0;
while (true) {
//장수턴 부터 처리
const upcomingGeneral = this.rc.popGeneralTurnTimeQueue();
if (!upcomingGeneral) {
break;
}
const generalTurnTime = upcomingGeneral.raw.turnTime;
if (generalTurnTime > now) {
break;
}
//TODO: 턴 수행해야지!
upcomingGeneral.updateTurnTime();
this.rc.insertGeneralTurnTimeQueue(upcomingGeneral, true);
this.rc.gameEnv.update((env) => {
env.lastExecuted = generalTurnTime;
})
processedGeneralCount++;
}
if (processedGeneralCount > 0) {
await this.prevSaveWaiter;
const lastExecuted = this.rc.gameEnv.raw.lastExecuted;
this.loggerEngine.flushAll();
this.prevSaveWaiter = this.rc.saveAll().then(() => {
this.postStatus({
type: 'update',
lastExecuted: lastExecuted.toISOString()
});
});
}
}
private async processActions() {
//서버 이벤트 처리
for (const [invoker, queue] of entriesWithType(this.actionQueue)) {
let processEventCount = 0;
let responseQueue: (() => void)[] = [];
while (!queue.isEmpty()) {
const action = must(queue.shift());
const actionResult = this.processAction(invoker, action.action);
const [ticketResolve, ticketRejected] = must_err(
this.queueWaiters.get(action.waiterSeq),
Error, `GameEngine: waiterSeq(${action.waiterSeq}) not found`
);
this.queueWaiters.delete(action.waiterSeq);
if(actionResult.success){
responseQueue.push(() => {
ticketResolve(actionResult);
});
}
else{
responseQueue.push(() => {
ticketRejected(actionResult);
});
}
processEventCount++;
}
if (processEventCount > 0) {
await this.prevSaveWaiter;
this.prevSaveWaiter = null;
this.loggerEngine.flushAll();
await this.rc.saveAll();
for (const response of responseQueue) {
response();
}
}
}
}
private async gameLoop(): Promise<void> {
let generalWaitTimer: ReturnType<typeof setTimeout> | null = null;
while (this.continueRun) {
//가장 빠른 장수
do {
const now = new Date();
const upcomingGeneral = this.rc.peekGeneralTurnTimeQueue();
if (!upcomingGeneral) {
break;
}
const upcomingTurn = upcomingGeneral.raw.turnTime;
const waitGeneralTurnMs = upcomingTurn.getTime() - now.getTime();
if (waitGeneralTurnMs > 0) {
generalWaitTimer = setTimeout(() => {
this.signal.notify();
generalWaitTimer = null;
}, waitGeneralTurnMs);
}
else {
this.signal.notify();
}
} while (0);
for (const queue of Object.values(this.actionQueue)) {
if (!queue.isEmpty()) {
this.signal.notify();
break;
}
}
//이벤트 대기
await this.signal.wait();
if (generalWaitTimer) {
clearTimeout(generalWaitTimer);
}
if (!this.continueRun) {
break;
}
await this.processGeneralTurn();
await this.processActions();
}
}
private gameLoopPromise: Promise<void> | null = null;
public static async initInstance(updateHandler: (msg: GameEngineMsg) => void): Promise<GameEngine> {
if (GameEngine.instance) {
throw new Error('GameEngine is already initialized');
}
GameEngine.instance = new GameEngine(await db, updateHandler);
return GameEngine.instance;
}
public static getInstance() {
if (!GameEngine.instance) {
GameEngine.instance = new GameEngine();
throw new Error('GameEngine is not initialized');
}
return GameEngine.instance;
}
public async start() {
public start(): boolean {
if (this.gameLoopPromise) {
return false;
}
console.log('GameEngine started');
for (const queue of Object.values(this.actionQueue)) {
queue.clear();
}
this.continueRun = true;
this.gameLoopPromise = this.gameLoop();
console.info('GameEngine started');
return true;
}
public async stop() {
this.continueRun = false;
if (this.gameLoopPromise) {
this.signal.abort();
await this.gameLoopPromise;
this.gameLoopPromise = null;
}
console.log('GameEngine stopped');
}
private processAction(invoker: QueueType, action: ActionRequest): ActionResult {
const actionName = action.name;
const actionArgs = action.args;
try{
const result = ActionRequestList[actionName](invoker, actionArgs as any, this.rc);
return {
success: true,
value: result
};
}
catch(e){
const msg = e instanceof Error ? e.message : e;
return {
success: false,
reason: String(msg),
};
}
}
private async pushAction(queueName: QueueType, action: ActionRequest): Promise<ActionResult> {
const queue = this.actionQueue[queueName];
if (this.actionSeq >= Number.MAX_SAFE_INTEGER) {
this.actionSeq = 0;
}
const seq = this.actionSeq++;
if (this.queueWaiters.has(seq)) {
throw new Error(`GameEngine: seq(${seq}) is already used`);
}
let done = false;
const waiter = new Promise((resolve, reject) => {
this.queueWaiters.set(seq, [resolve, reject]);
done = true;
queue.push({
waiterSeq: seq,
action
});
});
await delay(0);
this.signal.notify();
if (!done) {
throw new Error(`GameEngine: pushAction(${queueName}) failed`);
}
return waiter as Promise<ActionResult>;
}
public async pushServerAction(action: ActionRequest): Promise<ActionResult> {
return this.pushAction('server', action);
}
/*
public async pushNPCAction(action: ActionRequest): Promise<ActionResult> {
return this.pushAction('npc', action);
}
*/
public async pushAPIAction(action: ActionRequest): Promise<ActionResult> {
return this.pushAction('api', action);
}
}
+113
View File
@@ -0,0 +1,113 @@
import { randomUUID } from "node:crypto";
import { fileURLToPath } from "node:url";
import { MessageChannel, Worker } from "node:worker_threads";
import * as nodePath from "node:path";
import { delay, must } from "@sammo/util";
import type { GameEngineMsg, GameEngineRPCDefs } from "./GameEngineDefs.js";
import { RPCClient } from "@sammo/server_util";
import type { ActionMainArg, ActionRes, ActionRequestKey, ActionRequestList } from "@sammo/game_logic";
const __filename = fileURLToPath(new URL(import.meta.url));
const __dirname = nodePath.dirname(__filename);
const workerPath = nodePath.resolve(__dirname, 'GameEngineWorker');
class GameEngineController {
private constructor() { }
private static _instance: GameEngineController;
static get instance(): GameEngineController {
if (!GameEngineController._instance) {
GameEngineController._instance = new GameEngineController();
}
return GameEngineController._instance;
}
private engineGUID?: string = undefined;
private _lastExecuted?: Date = undefined;
private stopWaiter: Promise<void> = Promise.resolve();
private stopWaiterResolve: () => void = () => { };
private rpcClient?: RPCClient<GameEngineRPCDefs> = undefined;
get isRunning(): boolean {
return this.engineGUID !== undefined;
}
get lastExecuted(): Date | undefined {
return this._lastExecuted;
}
async start(): Promise<void> {
if (this.engineGUID) {
throw new Error('GameEngine is already running');
}
const guid = randomUUID();
const { port1: rpcServer, port2: rpcClient } = new MessageChannel();
const worker = new Worker(workerPath);
this.stopWaiter = new Promise((resolve) => {
this.stopWaiterResolve = resolve;
});
worker.postMessage({
type: 'init',
port: rpcServer,
guid
}, [rpcServer]);
await delay(0);
this.engineGUID = guid;
worker.once('exit', () => {
this.engineGUID = undefined;
this._lastExecuted = undefined;
this.rpcClient = undefined;
this.stopWaiterResolve();
});
worker.on('message', (msg: GameEngineMsg) => {
if (msg.type === 'update') {
this._lastExecuted = new Date(msg.lastExecuted);
}
});
this.rpcClient = new RPCClient(rpcClient, 'GameEngineController')
}
async stop(): Promise<boolean> {
if (!this.isRunning) {
return false;
}
await must(this.rpcClient).callFunction('stop', undefined);
await this.stopWaiter;
return true;
}
async restart(): Promise<void> {
await this.stop();
await this.start();
}
async pushAPIAction<T extends ActionRequestKey>(action: T, arg: ActionMainArg<T>): Promise<ActionRes<T>> {
const result = await must(this.rpcClient).callFunction('pushAPIAction', { name: action, args: arg });
if (!result.success) {
throw new Error(result.reason);
}
return result.value as ReturnType<typeof ActionRequestList[T]>;
}
async pushServerAction<T extends ActionRequestKey>(action: T, arg: ActionMainArg<T>): Promise<ActionRes<T>> {
const result = await must(this.rpcClient).callFunction('pushServerAction', { name: action, args: arg });
if (!result.success) {
throw new Error(result.reason);
}
return result.value as ReturnType<typeof ActionRequestList[T]>;
}
}
export function getGameEngineController(): GameEngineController {
return GameEngineController.instance;
}
+35
View File
@@ -0,0 +1,35 @@
import type { RPCLists } from "@sammo/server_util";
import type { ActionRequest, ActionResult } from "./GameEngine.js";
import type { MessagePort } from "node:worker_threads";
import type { CityID, GeneralID, NationID, SquadID } from "@sammo/game_logic/src/defs.js";
import type { ActionPackDef } from "@sammo/game_logic";
import type { ActionRequestList, QueueType } from "@sammo/game_logic/src/ActionRequest/index.js";
//Remove Third argument. Use Infer
export type MapRscActionRequest<T extends ActionPackDef> = {
[P in keyof T]: (invoker: Parameters<T[P]>[1] & QueueType, arg: Parameters<T[P]>[1]) => ReturnType<T[P]>
}
export type ActionPack = MapRscActionRequest<typeof ActionRequestList>;
export type GameEngineRPCDefs = {
stop: () => Promise<void>,
pushAPIAction: (action: ActionRequest) => Promise<ActionResult>,
pushServerAction: (action: ActionRequest) => Promise<ActionResult>,
};
type WorkerUpdate = {
type: 'update',
lastExecuted: string,
}
export type GameEngineMsg = WorkerUpdate;
type WorkerInit = {
type: 'init',
port: MessagePort,
guid: string,
}
export type GameEngineWorkerMsg = WorkerInit;
+48
View File
@@ -0,0 +1,48 @@
import { parentPort as _parentPort } from "node:worker_threads";
import { type MessagePort } from "node:worker_threads";
import type { GameEngineMsg, GameEngineRPCDefs, GameEngineWorkerMsg } from "./GameEngineDefs.js";
import { GameEngine } from "./GameEngine.js";
import { RPCServer } from "@sammo/server_util";
if (!_parentPort) {
throw new Error('GameEngineWorker must be run as worker');
}
const parentPort = _parentPort;
function sendToParent(msg: GameEngineMsg) {
parentPort.postMessage(msg);
}
let engineDone: () => void = () => { throw new Error('GameEngineWorker: engineDone not set')};
let engine: GameEngine;
let rpcServer: RPCServer<GameEngineRPCDefs>;
async function startWorker(port: MessagePort, guid: string){
engine = await GameEngine.initInstance(sendToParent);
rpcServer = new RPCServer<GameEngineRPCDefs>(port, {
stop: async ()=>{
await engine.stop();
engineDone();
},
pushAPIAction: async (action)=>{
return await engine.pushAPIAction(action);
},
pushServerAction: async (action)=>{
return await engine.pushServerAction(action);
}
});
}
await new Promise<void>((resolve) => {
parentPort.on('message', (msg: GameEngineWorkerMsg) => {
switch (msg.type) {
case 'init':
engineDone = resolve;
startWorker(msg.port, msg.guid);
break;
default:
throw new Error(`GameEngineWorker: unknown message type ${msg.type}`);
}
});
})
+282
View File
@@ -0,0 +1,282 @@
import type { IResourceController } from "@sammo/game_logic";
import { City } from "@sammo/game_logic/src/City.js";
import type { CityID, GeneralID, NationID, SquadID } from "@sammo/game_logic/src/defs.js";
import type { IGeneralEntity } from "@sammo/game_logic/src/Entity/GeneralEntity.js";
import type { ICityEntity, ValidEntityType } from "@sammo/game_logic/src/Entity/index.js";
import type { INationEntity } from "@sammo/game_logic/src/Entity/NationEntity.js";
import type { ReservedTurn } from "@sammo/game_logic/src/defs.js";
import type { ISquadEntity } from "@sammo/game_logic/src/Entity/SquadEntity.js";
import { GameEnv } from "@sammo/game_logic/src/GameEnv.js";
import { General } from "@sammo/game_logic/src/General.js";
import type { LazyEntityUpdater } from "@sammo/game_logic/src/LazyEntityUpdater.js";
import { Nation } from "@sammo/game_logic/src/Nation.js";
import { Squad } from "@sammo/game_logic/src/Squad.js";
import { insertItemAtArrayLike, must, NotYetImplemented } from "@sammo/util";
import Denque from "denque";
import type { Mongoose } from "mongoose";
import type { IGameEnvEntity } from "@sammo/game_logic/src/Entity/GameEnvEntity.js";
type ChangeType = "insert" | "update" | "delete";
//TODO: Implement!
export class ResourceController implements IResourceController {
constructor(
private readonly db: Mongoose
) {
}
private _gameEnv: GameEnv = new GameEnv({} as IGameEnvEntity, this);
public get gameEnv(): GameEnv {
return this._gameEnv;
}
public readonly nationList: Map<NationID, Nation> = new Map();
public readonly cityList: Map<CityID, City> = new Map();
public readonly squadList: Map<SquadID, Squad> = new Map();
public readonly generalList: Map<GeneralID, General> = new Map();
//내부적으로 turnTime asc, id asc로 동작하는 PriorityQueue
private _generalByTurnTime = new Denque<General>();
private changes = {
Nation: new Map<number, [ChangeType, Nation]>(),
City: new Map<number, [ChangeType, City]>(),
Squad: new Map<number, [ChangeType, Squad]>(),
General: new Map<number, [ChangeType, General]>(),
GameEnv: new Map<number, [ChangeType, GameEnv]>(),
} as const satisfies Record<ValidEntityType, Map<number, [ChangeType, LazyEntityUpdater<any>]>>;
nation(id: NationID): Nation | undefined {
return this.nationList.get(id);
}
city(id: CityID): City | undefined {
return this.cityList.get(id);
}
squad(id: SquadID, nation: NationID): Squad | undefined {
const squad = this.squadList.get(id);
if (!squad) {
return undefined;
}
if (squad.raw.nationID !== nation) {
return undefined;
}
return squad;
}
general(id: GeneralID): General | undefined {
return this.generalList.get(id);
}
peekGeneralTurnTimeQueue(): General | undefined {
return this._generalByTurnTime.peekFront();
}
popGeneralTurnTimeQueue(): General | undefined {
return this._generalByTurnTime.shift();
}
insertGeneralTurnTimeQueue(general: General, maybeTail: boolean): void {
insertItemAtArrayLike(this._generalByTurnTime, general, (a, b) => a.compareTurnTime(b), maybeTail);
}
allNations(): Map<NationID, Nation> {
throw new NotYetImplemented();
}
allCities(): Map<CityID, City> {
throw new NotYetImplemented();
}
allCitiesByNation(): Map<NationID, Map<CityID, City>> {
throw new NotYetImplemented();
}
allSquads(): Map<NationID, Map<SquadID, Squad>> {
throw new NotYetImplemented();
}
allGenerals(): Map<GeneralID, General> {
throw new NotYetImplemented();
}
generalByNation(id: NationID): Map<GeneralID, General> {
throw new NotYetImplemented();
}
generalByCity(id: CityID, nationID?: NationID): Map<GeneralID, General> {
throw new NotYetImplemented();
}
generalBySquad(id: SquadID): Map<GeneralID, General> {
throw new NotYetImplemented();
}
cityByNation(id: NationID): Map<CityID, City> {
throw new NotYetImplemented();
}
squadByNation(id: NationID): Map<SquadID, Squad> {
throw new NotYetImplemented();
}
//----Insert, Update, Delete
reserveUpdate(entity: LazyEntityUpdater<any>): void {
throw new NotYetImplemented();
}
reserveDelete(entity: LazyEntityUpdater<any>): void {
throw new NotYetImplemented();
}
reserveInsert(entity: LazyEntityUpdater<any>): void {
throw new NotYetImplemented();
}
createGeneral(raw: IGeneralEntity): General {
throw new NotYetImplemented();
}
createNation(raw: INationEntity): Nation {
throw new NotYetImplemented();
}
createSquad(raw: ISquadEntity): Squad {
throw new NotYetImplemented();
}
_getAllChanges(): Record<ValidEntityType, Map<number, [op: ChangeType, value: LazyEntityUpdater<any>]>> {
return this.changes;
}
//---- DB 접근 ---
getReservedTurn(general: General): Promise<ReservedTurn> {
throw new NotYetImplemented();
}
nationReservedTurn(general: General): Promise<ReservedTurn | undefined> {
throw new NotYetImplemented();
}
resetAndFill(): Promise<void> {
this.nationList.clear();
this.cityList.clear();
this.squadList.clear();
this.generalList.clear();
//MongoDB로부터 데이터를 가져와서 채워넣는다.
const rawGameEnv = {} as IGameEnvEntity;
//TODO: Implement!
const rawNationList = new Map<NationID, INationEntity>();
const rawCityList = new Map<CityID, ICityEntity>();
const rawSquadList = new Map<SquadID, ISquadEntity>();
const rawGeneralList = new Map<GeneralID, IGeneralEntity>();
const generalIDByNation = new Map<NationID, GeneralID[]>();
const generalIDByCity = new Map<CityID, GeneralID[]>();
const generalIDListBySquad = new Map<SquadID, GeneralID[]>();
for (const rawSquad of rawSquadList.values()) {
generalIDListBySquad.set(rawSquad.id, []);
}
for (const rawGeneral of rawGeneralList.values()) {
if (rawGeneral.squadID) {
let list = generalIDListBySquad.get(rawGeneral.squadID);
if (!list) {
list = [];
generalIDListBySquad.set(rawGeneral.squadID, list);
}
list.push(rawGeneral.id);
}
{
let list = generalIDByNation.get(rawGeneral.nationID);
if (!list) {
list = [];
generalIDByNation.set(rawGeneral.nationID, list);
}
list.push(rawGeneral.id);
}
{
let list = generalIDByCity.get(rawGeneral.cityID);
if (!list) {
list = [];
generalIDByCity.set(rawGeneral.cityID, list);
}
list.push(rawGeneral.id);
}
}
const cityIDByNation = new Map<NationID, CityID[]>();
for (const rawCity of rawCityList.values()) {
const list = must(cityIDByNation.get(rawCity.nationID));
list.push(rawCity.id);
}
const squadIDByNation = new Map<NationID, SquadID[]>();
for (const rawSquad of rawSquadList.values()) {
const list = must(squadIDByNation.get(rawSquad.nationID));
list.push(rawSquad.id);
}
this._gameEnv = new GameEnv(rawGameEnv, this);
this._gameEnv.bootstrap();
for (const rawNation of rawNationList.values()) {
const nation = new Nation(rawNation, this,
{
general: must(generalIDByNation.get(rawNation.id)),
squad: squadIDByNation.get(rawNation.id) ?? [],
city: cityIDByNation.get(rawNation.id) ?? [],
}
);
this.nationList.set(nation.id, nation);
}
for (const rawCity of rawCityList.values()) {
const city = new City(rawCity, this, {
general: generalIDByCity.get(rawCity.id) ?? []
});
this.cityList.set(city.id, city);
}
for (const rawSquad of rawSquadList.values()) {
const squad = new Squad(rawSquad, this, {
general: generalIDListBySquad.get(rawSquad.id) ?? []
});
this.squadList.set(squad.id, squad);
}
for (const rawGeneral of rawGeneralList.values()) {
const general = new General(rawGeneral, this);
this.generalList.set(general.id, general);
}
//lazyInit()을 호출한다.
this.nationList.forEach(nation => nation.bootstrap());
this.cityList.forEach(city => city.bootstrap());
this.squadList.forEach(squad => squad.bootstrap());
//iAction때문에 general의 lazyInit이 가장 마지막
this.generalList.forEach(general => general.bootstrap());
//턴순서 정렬!
const generalList = Array.from(this.generalList.values());
generalList.sort((a, b) => a.compareTurnTime(b));
this._generalByTurnTime = new Denque(generalList);
throw new NotYetImplemented();
}
saveAll(): Promise<void> {
throw new NotYetImplemented();
}
}
+13
View File
@@ -0,0 +1,13 @@
import type { APINamespace, APINamespaceType } from '@strpc/express';
import { status } from './status.js';
import { start } from './start.js';
import { stop } from './stop.js';
import { restart } from './restart.js';
import type { structure } from "@sammo/api_def";
export const daemon = {
status,
start,
stop,
restart,
} satisfies APINamespaceType<typeof structure['daemon']>;
+24
View File
@@ -0,0 +1,24 @@
import { getGameEngineController } from "@/GameEngineController.js";
import { ReqACL } from "@/procDecorator/ReqACL.js";
import { ReqLogin } from "@/procDecorator/ReqLogin.js";
import type { structure } from "@sammo/api_def";
import { StartSession } from "@sammo/server_util";
import { type APIReturnType, POST } from "@strpc/express";
import { declProcDecorators } from "@strpc/express/proc_decorator";
type BaseAPI = typeof structure.daemon.restart;
type RType = APIReturnType<BaseAPI>;
export const restart = POST<BaseAPI>()(declProcDecorators(
StartSession,
ReqLogin,
ReqACL('StopAndResumeServer'),
))
(async (query, ctx): RType => {
const controller = getGameEngineController();
await controller.restart();
return {
result: true,
}
})
+31
View File
@@ -0,0 +1,31 @@
import { getGameEngineController } from "@/GameEngineController.js";
import { ReqACL } from "@/procDecorator/ReqACL.js";
import { ReqLogin } from "@/procDecorator/ReqLogin.js";
import type { structure } from "@sammo/api_def";
import { StartSession } from "@sammo/server_util";
import { type APIReturnType, POST } from "@strpc/express";
import { declProcDecorators } from "@strpc/express/proc_decorator";
type BaseAPI = typeof structure.daemon.start;
type RType = APIReturnType<BaseAPI>;
export const start = POST<BaseAPI>()(declProcDecorators(
StartSession,
ReqLogin,
ReqACL('StopAndResumeServer'),
))
(async (query, ctx): RType => {
const controller = getGameEngineController();
if(controller.isRunning) {
return {
result: false,
reason: 'Already running',
}
}
await controller.start();
return {
result: true,
}
})
+25
View File
@@ -0,0 +1,25 @@
import { getGameEngineController } from "@/GameEngineController.js";
import { ReqACL } from "@/procDecorator/ReqACL.js";
import { ReqLogin } from "@/procDecorator/ReqLogin.js";
import type { structure } from "@sammo/api_def";
import { StartSession } from "@sammo/server_util";
import { GET, type APIReturnType } from "@strpc/express";
import { declProcDecorators } from "@strpc/express/proc_decorator";
type BaseAPI = typeof structure.daemon.status;
type RType = APIReturnType<BaseAPI>;
export const status = GET<BaseAPI>()(declProcDecorators(
StartSession,
ReqLogin,
))
(async (query, ctx): RType => {
const controller = getGameEngineController();
const isRunning = controller.isRunning
const lastExecuted = controller.lastExecuted?.toISOString() ?? undefined;
return {
result: true,
isRunning,
lastExecuted,
}
})
+31
View File
@@ -0,0 +1,31 @@
import { getGameEngineController } from "@/GameEngineController.js";
import { ReqACL } from "@/procDecorator/ReqACL.js";
import { ReqLogin } from "@/procDecorator/ReqLogin.js";
import type { structure } from "@sammo/api_def";
import { StartSession } from "@sammo/server_util";
import { type APIReturnType, POST } from "@strpc/express";
import { declProcDecorators } from "@strpc/express/proc_decorator";
type BaseAPI = typeof structure.daemon.stop;
type RType = APIReturnType<BaseAPI>;
export const stop = POST<BaseAPI>()(declProcDecorators(
StartSession,
ReqLogin,
ReqACL('StopAndResumeServer'),
))
(async (query, ctx): RType => {
const controller = getGameEngineController();
if(!controller.isRunning) {
return {
result: false,
reason: 'Already stopped',
}
}
await controller.stop();
return {
result: true,
}
})
+2 -1
View File
@@ -1,5 +1,6 @@
import type { structure } from "@sammo/api_def";
import type { APINamespaceType } from "@strpc/express";
import { daemon } from "./daemon/index.js";
export const sammoAPI = {
daemon,
} satisfies APINamespaceType<typeof structure>;
+1 -1
View File
@@ -1,5 +1,5 @@
import './dotenv.js';
import { connect, Mongoose } from "mongoose";
import { connect, type Mongoose } from "mongoose";
import { unwrap } from '@sammo/util';
const dbConfig = {
+2
View File
@@ -27,6 +27,8 @@ declare namespace NodeJS {
API_ROOT_PATH?: string;
VITE_API_ROOT_PATH?: string;
GAME_ENGINE_MANUAL_START?: string; //숫자->Boolean
PRESHARED_SECURE_TOKEN_SECRET?: string; //길게
}
}
+11 -2
View File
@@ -7,6 +7,7 @@ import { buildAPISystem } from '@strpc/express/generator';
import { sammoAPI } from './api/index.js';
import { unwrap } from '@sammo/util';;
import { serverConfig } from './serverConfig.js';
import { getGameEngineController } from './GameEngineController.js';
connectDB.then(async (db) => {
// create express app
@@ -22,9 +23,17 @@ connectDB.then(async (db) => {
app.use('/api', buildAPISystem(sammoAPI));
// start express server
app.listen(serverConfig.port)
app.listen(serverConfig.port, "0.0.0.0", async ()=>{
console.log(`Express server has started on port ${serverConfig.port}`);
if(serverConfig.gameEngineManualStart){
console.log("Game Engine is not started automatically.");
return;
}
await getGameEngineController().start();
})
console.log(`Express server has started on port ${serverConfig.port}`)
}).catch(error => console.log(error));
+45
View File
@@ -0,0 +1,45 @@
import type { SessionCtx } from "@sammo/server_util";
import type { ProcDecorator } from "@strpc/express/proc_decorator";
import type { ServerActionType } from "@sammo/gateway_server/exports"
import { loginCtxSessionKey } from "./ReqLogin.js";
export type LoginCtx = {
userID: number;
userName: string;
userLevel: number;
allowServerAction?: Set<ServerActionType>;
loginDate: Date;
}
export function ReqACL<Q extends LoginCtx & SessionCtx>(...actions: ServerActionType[]): ()=>ProcDecorator<Q, Q> {
return () => {
return (ctx) => {
if (actions.length === 0) {
return [{
result: true,
}, ctx];
}
if(!ctx.allowServerAction){
return [{
result: false,
type: 'Required ACL',
info: 'No Server ACL in session context.'
}, ctx];
}
for (const action of actions) {
if (!ctx.allowServerAction.has(action)) {
return [{
result: false,
type: 'Required ACL',
info: `NotEnoughPermission: ${action}`
}, ctx];
}
}
return [{
result: true,
}, ctx];
}
}
}
+8 -6
View File
@@ -1,9 +1,11 @@
import 'dotenv/config';
import { unwrap } from '@sammo/util';
import { must } from '@sammo/util';
export const serverConfig = {
sessionSecret: unwrap(process.env.SESSION_SECRET),
port: Number(unwrap(process.env.SERVER_PORT)),
gatewayHost: unwrap(process.env.GATEWAY_HOST),
gatewayPort: Number(unwrap(process.env.GATEWAY_PORT)),
};
sessionSecret: must(process.env.SESSION_SECRET),
port: Number(must(process.env.SERVER_PORT)),
gatewayHost: must(process.env.GATEWAY_HOST),
gatewayPort: Number(must(process.env.GATEWAY_PORT)),
gameEngineManualStart: Boolean(Number(process.env.GAME_ENGINE_MANUAL_START??'0'))
} as const;
-49
View File
@@ -1,49 +0,0 @@
import { delay, must } from "@sammo/util";
import { parentPort as _parentPort } from "node:worker_threads";
import { type MessagePort } from "node:worker_threads";
import { generateMessage, PortRPCClient, type InitPort, type RandomMsg } from "./worker_util.js";
import type { RPCSampleList } from "./worker_common.js";
if (!_parentPort) {
throw new Error("parentPort is not defined");
}
const parentPort = must(_parentPort);
let run = true;
async function worker_a(port: MessagePort) {
const rpcClient = new PortRPCClient<RPCSampleList>(port);
console.log("Hello from worker_a.ts!");
let cnt = 0;
const now = Date.now();
while (run && cnt < 10000) {
const message = generateMessage();
await rpcClient.callFunction('do', message);
cnt++;
if(cnt % 100 === 0){
console.log(`a_cnt: ${cnt}`);
}
}
const elapsed = Date.now() - now;
console.log(`Elapsed: ${elapsed}, cnt: ${cnt}`);
parentPort.postMessage(elapsed);
}
await new Promise<void>((resolve) => {
parentPort.once('message', async (ports: InitPort) => {
await worker_a(ports.oppose);
resolve();
});
parentPort.once('close', () => {
run = false;
});
});
-40
View File
@@ -1,40 +0,0 @@
import { delay, must } from "@sammo/util";
import { parentPort as _parentPort } from "node:worker_threads";
import { type MessagePort } from "node:worker_threads";
import { PortRPCServer, type InitPort, type RandomMsg } from "./worker_util.js";
import { unknown } from "zod";
if (!_parentPort) {
throw new Error("parentPort is not defined");
}
const parentPort = must(_parentPort);
let run = true;
function doFunc(msg: RandomMsg): string {
return msg.key;
}
let rpcServer: PortRPCServer<{}>|null = null;
async function worker_b(port: MessagePort) {
console.log("Hello from worker_b.ts!");
rpcServer = new PortRPCServer(port, {
do: doFunc
});
parentPort.postMessage("ready");
}
await new Promise<void>((resolve) => {
parentPort.once('message', async (ports: InitPort) => {
await worker_b(ports.oppose);
resolve();
});
parentPort.once('close', () => {
run = false;
});
});
-5
View File
@@ -1,5 +0,0 @@
import type { RandomMsg } from "./worker_util.js";
export type RPCSampleList = {
do: (msg: RandomMsg) => string;
}
-32
View File
@@ -1,32 +0,0 @@
import { Worker, MessageChannel } from "node:worker_threads";
import * as nodePath from "node:path";
import { fileURLToPath } from "node:url";
console.log("Hello from worker_test.ts!");
const __filename = fileURLToPath(new URL(import.meta.url));
const __dirname = nodePath.dirname(__filename);
const worker_b_ts = nodePath.join(__dirname, "worker_b");
const worker_a_ts = nodePath.join(__dirname, "worker_a");
const worker_b = new Worker(worker_b_ts);
const worker_a = new Worker(worker_a_ts);
{
const betweenPort = new MessageChannel();
worker_b.postMessage({ oppose: betweenPort.port2 }, [betweenPort.port2]);
worker_b.once('message', ()=>{
worker_a.postMessage({ oppose: betweenPort.port1 }, [betweenPort.port1]);
});
}
await new Promise<void>((resolve) => {
worker_a.once('message', async (cnt: number) => {
await worker_b.terminate();
resolve();
});
});

Some files were not shown because too many files have changed in this diff Show More