Compare commits
70
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
196150bedc | ||
|
|
74b4e955df | ||
|
|
1aee0f7881 | ||
|
|
42e68f90ad | ||
|
|
0ce8293b89 | ||
|
|
494ed3f3f1 | ||
|
|
78bcc95e2e | ||
|
|
6a6bdba08b | ||
|
|
39e8eab6cb | ||
|
|
b4b00e1983 | ||
|
|
3e4129c3f1 | ||
|
|
533400b1b9 | ||
|
|
cdfaac773a | ||
|
|
e7b751c3f1 | ||
|
|
dd9b236547 | ||
|
|
d40995e1d9 | ||
|
|
28b78e0872 | ||
|
|
21a9fc54f3 | ||
|
|
74174ca343 | ||
|
|
1b2e062bb6 | ||
|
|
e49408db2a | ||
|
|
e6f6798f34 | ||
|
|
2266cad0f8 | ||
|
|
c9c974d191 | ||
|
|
fb16369384 | ||
|
|
5adf8db676 | ||
|
|
c99f97323d | ||
|
|
cf3a208591 | ||
|
|
1537e8a137 | ||
|
|
9a9f41c784 | ||
|
|
e236654ecf | ||
|
|
f693406df6 | ||
|
|
708f14fdb6 | ||
|
|
918bb997b9 | ||
|
|
b449cf3a11 | ||
|
|
fb68d9be22 | ||
|
|
6b819bbce5 | ||
|
|
d742633e6d | ||
|
|
e0784a670c | ||
|
|
c42a97bb5f | ||
|
|
ddfff44c12 | ||
|
|
554588fb42 | ||
|
|
99238636e6 | ||
|
|
5928655bc1 | ||
|
|
bf8ad0a78c | ||
|
|
d87c177d50 | ||
|
|
ff23cd27b8 | ||
|
|
3d67f8bb55 | ||
|
|
6e7ca3f998 | ||
|
|
473963b58b | ||
|
|
31184c1cd4 | ||
|
|
a430d45e2a | ||
|
|
a6833473de | ||
|
|
9aecd4dfb4 | ||
|
|
c7c84b4fd3 | ||
|
|
3ad136b803 | ||
|
|
3b460b5f04 | ||
|
|
66c8261438 | ||
|
|
3ba5793755 | ||
|
|
c9afc111cc | ||
|
|
f320ce58a2 | ||
|
|
afb6fbb1b1 | ||
|
|
e965ed9816 | ||
|
|
94becec429 | ||
|
|
fda68b690d | ||
|
|
fa29486af6 | ||
|
|
a38f72fa46 | ||
|
|
a5f5fb00ba | ||
|
|
04e76ddd9d | ||
|
|
85b1cee1b9 |
@@ -21,7 +21,7 @@
|
||||
"@strpc/def": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"zod": "^3.22.4"
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.22.2"
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { BufferSource } from "@sammo/util";
|
||||
|
||||
const subtle = globalThis.crypto.subtle;
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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,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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { sha256, sha512 } from "./SHA2.js";
|
||||
|
||||
test('sha2', async () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"verbatimModuleSyntax": false,
|
||||
}
|
||||
}
|
||||
@@ -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]>;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface IGlobalRecord {
|
||||
_id: string;
|
||||
type: 'history' | 'action';
|
||||
year: number;
|
||||
month: number;
|
||||
text: string;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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
@@ -0,0 +1,15 @@
|
||||
export interface ISurvey {
|
||||
_id: string;
|
||||
|
||||
title: string;
|
||||
description: string;
|
||||
|
||||
//투표 시작 시간
|
||||
startTime: Date;
|
||||
//투표 종료 시간
|
||||
endTime?: Date;
|
||||
|
||||
multipleOptions: number;
|
||||
|
||||
options: string[];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { GeneralID, NationID } from "@/defs.js";
|
||||
|
||||
export interface ISurveyResponse {
|
||||
_id: string;
|
||||
surveyID: string;
|
||||
|
||||
//투표자
|
||||
generalID: GeneralID;
|
||||
|
||||
nationID: NationID;
|
||||
|
||||
//투표한 선택지
|
||||
selectedOption: number[];
|
||||
}
|
||||
@@ -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>;
|
||||
|
||||
//게임 외부는 다른 엔티티로 분리
|
||||
}
|
||||
+26
-13
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,6 @@
|
||||
export class LogicError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
this.name = 'LogicError';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { NationID, TroopID } from "./defs.js";
|
||||
|
||||
export interface ITroopEntity {
|
||||
id: TroopID;
|
||||
nationID: NationID;
|
||||
name: string;
|
||||
}
|
||||
+167
-19
@@ -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" };
|
||||
@@ -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여야하나?
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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}`);
|
||||
}
|
||||
});
|
||||
})
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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']>;
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
@@ -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,5 +1,5 @@
|
||||
import './dotenv.js';
|
||||
import { connect, Mongoose } from "mongoose";
|
||||
import { connect, type Mongoose } from "mongoose";
|
||||
import { unwrap } from '@sammo/util';
|
||||
|
||||
const dbConfig = {
|
||||
|
||||
Vendored
+2
@@ -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; //길게
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
import type { RandomMsg } from "./worker_util.js";
|
||||
|
||||
export type RPCSampleList = {
|
||||
do: (msg: RandomMsg) => string;
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user