feat: add non-aggression proposal and acceptance actions with constraints
This commit is contained in:
@@ -216,6 +216,11 @@ const ACTION_CONTEXT_BUILDERS: Record<string, ActionContextBuilder> = {
|
||||
startYear: resolveStartYear(options.world, options.scenarioMeta),
|
||||
};
|
||||
},
|
||||
che_불가침제의: (base, options) => ({
|
||||
...base,
|
||||
currentYear: options.world.currentYear,
|
||||
currentMonth: options.world.currentMonth,
|
||||
}),
|
||||
};
|
||||
|
||||
export const buildActionContext = (
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
import type { GeneralTriggerState } from '../../../domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '../../../constraints/types.js';
|
||||
import {
|
||||
beChief,
|
||||
destGeneralInDestNation,
|
||||
disallowDiplomacyBetweenStatus,
|
||||
existsDestGeneral,
|
||||
existsDestNation,
|
||||
notBeNeutral,
|
||||
occupiedCity,
|
||||
suppliedCity,
|
||||
} from '../../../constraints/presets.js';
|
||||
import { allow, unknownOrDeny } from '../../../constraints/helpers.js';
|
||||
import type { GeneralActionDefinition } from '../../definition.js';
|
||||
import type {
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
} from '../../engine.js';
|
||||
import { createDiplomacyPatchEffect, createLogEffect } from '../../engine.js';
|
||||
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
||||
|
||||
export interface NonAggressionAcceptArgs {
|
||||
destNationId: number;
|
||||
destGeneralId: number;
|
||||
year: number;
|
||||
month: number;
|
||||
}
|
||||
|
||||
export interface NonAggressionAcceptContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> extends GeneralActionResolveContext<TriggerState> {
|
||||
currentYear: number;
|
||||
currentMonth: number;
|
||||
}
|
||||
|
||||
const ACTION_NAME = '불가침 수락';
|
||||
const DIPLOMACY_NON_AGGRESSION = 7;
|
||||
|
||||
const parseNationId = (raw: unknown): number | null => {
|
||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||
return null;
|
||||
}
|
||||
return raw > 0 ? Math.floor(raw) : null;
|
||||
};
|
||||
|
||||
const parseGeneralId = (raw: unknown): number | null => {
|
||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||
return null;
|
||||
}
|
||||
return raw > 0 ? Math.floor(raw) : null;
|
||||
};
|
||||
|
||||
const parseYear = (raw: unknown): number | null => {
|
||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||
return null;
|
||||
}
|
||||
return raw >= 0 ? Math.floor(raw) : null;
|
||||
};
|
||||
|
||||
const parseMonth = (raw: unknown): number | null => {
|
||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||
return null;
|
||||
}
|
||||
const month = Math.floor(raw);
|
||||
return month >= 1 && month <= 12 ? month : null;
|
||||
};
|
||||
|
||||
const resolveMonthIndex = (year: number, month: number): number =>
|
||||
year * 12 + month - 1;
|
||||
|
||||
const requireFutureTerm = (): Constraint => ({
|
||||
name: 'RequireNonAggressionFutureTerm',
|
||||
requires: () => [
|
||||
{ kind: 'arg', key: 'year' },
|
||||
{ kind: 'arg', key: 'month' },
|
||||
{ kind: 'env', key: 'year' },
|
||||
{ kind: 'env', key: 'month' },
|
||||
],
|
||||
test: (ctx) => {
|
||||
const yearValue = typeof ctx.args.year === 'number' ? ctx.args.year : null;
|
||||
const monthValue =
|
||||
typeof ctx.args.month === 'number' ? ctx.args.month : null;
|
||||
const envYearValue = typeof ctx.env.year === 'number' ? ctx.env.year : null;
|
||||
const envMonthValue =
|
||||
typeof ctx.env.month === 'number' ? ctx.env.month : null;
|
||||
const missing = [];
|
||||
|
||||
if (yearValue === null) {
|
||||
missing.push({ kind: 'arg', key: 'year' } as const);
|
||||
}
|
||||
if (monthValue === null) {
|
||||
missing.push({ kind: 'arg', key: 'month' } as const);
|
||||
}
|
||||
if (envYearValue === null) {
|
||||
missing.push({ kind: 'env', key: 'year' } as const);
|
||||
}
|
||||
if (envMonthValue === null) {
|
||||
missing.push({ kind: 'env', key: 'month' } as const);
|
||||
}
|
||||
|
||||
if (
|
||||
missing.length > 0 ||
|
||||
yearValue === null ||
|
||||
monthValue === null ||
|
||||
envYearValue === null ||
|
||||
envMonthValue === null
|
||||
) {
|
||||
return unknownOrDeny(ctx, missing, '기한 정보가 없습니다.');
|
||||
}
|
||||
|
||||
const currentMonth = resolveMonthIndex(envYearValue, envMonthValue);
|
||||
const targetMonth = yearValue * 12 + monthValue;
|
||||
if (targetMonth <= currentMonth) {
|
||||
return {
|
||||
kind: 'deny',
|
||||
reason: '이미 기한이 지났습니다.',
|
||||
};
|
||||
}
|
||||
return allow();
|
||||
},
|
||||
});
|
||||
|
||||
const notSameDestGeneral = (): Constraint => ({
|
||||
name: 'NotSameDestGeneral',
|
||||
requires: () => [{ kind: 'arg', key: 'destGeneralId' }],
|
||||
test: (ctx) => {
|
||||
const destGeneralId = ctx.args.destGeneralId;
|
||||
if (typeof destGeneralId !== 'number') {
|
||||
return unknownOrDeny(
|
||||
ctx,
|
||||
[{ kind: 'arg', key: 'destGeneralId' }],
|
||||
'장수 정보가 없습니다.'
|
||||
);
|
||||
}
|
||||
if (destGeneralId === ctx.actorId) {
|
||||
return { kind: 'deny', reason: '대상이 올바르지 않습니다.' };
|
||||
}
|
||||
return allow();
|
||||
},
|
||||
});
|
||||
|
||||
// 불가침 수락은 메시지와 연결되는 즉시 국가 커맨드로 사용한다.
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionDefinition<
|
||||
TriggerState,
|
||||
NonAggressionAcceptArgs,
|
||||
NonAggressionAcceptContext<TriggerState>
|
||||
> {
|
||||
public readonly key = 'che_불가침수락';
|
||||
public readonly name = ACTION_NAME;
|
||||
|
||||
parseArgs(raw: unknown): NonAggressionAcceptArgs | null {
|
||||
const data = raw as {
|
||||
destNationId?: unknown;
|
||||
destGeneralId?: unknown;
|
||||
year?: unknown;
|
||||
month?: unknown;
|
||||
};
|
||||
const destNationId = parseNationId(data?.destNationId);
|
||||
const destGeneralId = parseGeneralId(data?.destGeneralId);
|
||||
const year = parseYear(data?.year);
|
||||
const month = parseMonth(data?.month);
|
||||
if (
|
||||
destNationId === null ||
|
||||
destGeneralId === null ||
|
||||
year === null ||
|
||||
month === null
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return { destNationId, destGeneralId, year, month };
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: NonAggressionAcceptArgs
|
||||
): Constraint[] {
|
||||
return [
|
||||
beChief(),
|
||||
notBeNeutral(),
|
||||
occupiedCity(),
|
||||
suppliedCity(),
|
||||
existsDestNation(),
|
||||
existsDestGeneral(),
|
||||
destGeneralInDestNation(),
|
||||
notSameDestGeneral(),
|
||||
requireFutureTerm(),
|
||||
disallowDiplomacyBetweenStatus({
|
||||
0: '아국과 이미 교전중입니다.',
|
||||
1: '아국과 이미 선포중입니다.',
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
resolve(
|
||||
context: NonAggressionAcceptContext<TriggerState>,
|
||||
args: NonAggressionAcceptArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
const nationId = context.nation?.id;
|
||||
if (nationId === undefined || nationId <= 0) {
|
||||
return {
|
||||
effects: [
|
||||
createLogEffect(
|
||||
`${ACTION_NAME}을 준비했지만 국가 정보가 없습니다.`,
|
||||
{
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
}
|
||||
),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const currentMonth = resolveMonthIndex(
|
||||
context.currentYear,
|
||||
context.currentMonth
|
||||
);
|
||||
const targetMonth = args.year * 12 + args.month;
|
||||
const term = Math.max(0, targetMonth - currentMonth);
|
||||
|
||||
return {
|
||||
effects: [
|
||||
createDiplomacyPatchEffect(nationId, args.destNationId, {
|
||||
state: DIPLOMACY_NON_AGGRESSION,
|
||||
term,
|
||||
}),
|
||||
createDiplomacyPatchEffect(args.destNationId, nationId, {
|
||||
state: DIPLOMACY_NON_AGGRESSION,
|
||||
term,
|
||||
}),
|
||||
createLogEffect(
|
||||
`${ACTION_NAME}을 실행했습니다. (국가 ${args.destNationId})`,
|
||||
{
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
}
|
||||
),
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
export {};
|
||||
export * from './che_불가침수락.js';
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import type { GeneralTriggerState } from '../../../domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '../../../constraints/types.js';
|
||||
import {
|
||||
beChief,
|
||||
differentDestNation,
|
||||
disallowDiplomacyBetweenStatus,
|
||||
existsDestNation,
|
||||
notBeNeutral,
|
||||
} from '../../../constraints/presets.js';
|
||||
import { allow, unknownOrDeny } from '../../../constraints/helpers.js';
|
||||
import type { GeneralActionDefinition } from '../../definition.js';
|
||||
import type {
|
||||
GeneralActionOutcome,
|
||||
GeneralActionResolveContext,
|
||||
} from '../../engine.js';
|
||||
import { createLogEffect } from '../../engine.js';
|
||||
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
|
||||
import type { TurnCommandEnv } from '../commandEnv.js';
|
||||
import type { NationTurnCommandSpec } from './index.js';
|
||||
|
||||
export interface NonAggressionProposalArgs {
|
||||
destNationId: number;
|
||||
year: number;
|
||||
month: number;
|
||||
}
|
||||
|
||||
const ACTION_NAME = '불가침 제의';
|
||||
const MIN_TERM_MONTHS = 6;
|
||||
|
||||
const parseNationId = (raw: unknown): number | null => {
|
||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||
return null;
|
||||
}
|
||||
return raw > 0 ? Math.floor(raw) : null;
|
||||
};
|
||||
|
||||
const parseYear = (raw: unknown): number | null => {
|
||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||
return null;
|
||||
}
|
||||
return raw >= 0 ? Math.floor(raw) : null;
|
||||
};
|
||||
|
||||
const parseMonth = (raw: unknown): number | null => {
|
||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
||||
return null;
|
||||
}
|
||||
const month = Math.floor(raw);
|
||||
return month >= 1 && month <= 12 ? month : null;
|
||||
};
|
||||
|
||||
const resolveMonthIndex = (year: number, month: number): number =>
|
||||
year * 12 + month - 1;
|
||||
|
||||
const requireMinimumTerm = (minMonths: number): Constraint => ({
|
||||
name: 'RequireNonAggressionMinimumTerm',
|
||||
requires: () => [
|
||||
{ kind: 'arg', key: 'year' },
|
||||
{ kind: 'arg', key: 'month' },
|
||||
{ kind: 'env', key: 'year' },
|
||||
{ kind: 'env', key: 'month' },
|
||||
],
|
||||
test: (ctx) => {
|
||||
const yearValue = typeof ctx.args.year === 'number' ? ctx.args.year : null;
|
||||
const monthValue = typeof ctx.args.month === 'number' ? ctx.args.month : null;
|
||||
const envYearValue = typeof ctx.env.year === 'number' ? ctx.env.year : null;
|
||||
const envMonthValue =
|
||||
typeof ctx.env.month === 'number' ? ctx.env.month : null;
|
||||
const missing = [];
|
||||
|
||||
if (yearValue === null) {
|
||||
missing.push({ kind: 'arg', key: 'year' } as const);
|
||||
}
|
||||
if (monthValue === null) {
|
||||
missing.push({ kind: 'arg', key: 'month' } as const);
|
||||
}
|
||||
if (envYearValue === null) {
|
||||
missing.push({ kind: 'env', key: 'year' } as const);
|
||||
}
|
||||
if (envMonthValue === null) {
|
||||
missing.push({ kind: 'env', key: 'month' } as const);
|
||||
}
|
||||
|
||||
if (
|
||||
missing.length > 0 ||
|
||||
yearValue === null ||
|
||||
monthValue === null ||
|
||||
envYearValue === null ||
|
||||
envMonthValue === null
|
||||
) {
|
||||
return unknownOrDeny(ctx, missing, '기한 정보가 없습니다.');
|
||||
}
|
||||
|
||||
const currentMonth = resolveMonthIndex(envYearValue, envMonthValue);
|
||||
const targetMonth = resolveMonthIndex(yearValue, monthValue);
|
||||
if (targetMonth < currentMonth + minMonths) {
|
||||
return {
|
||||
kind: 'deny',
|
||||
reason: `기한은 ${minMonths}개월 이상이어야 합니다.`,
|
||||
};
|
||||
}
|
||||
return allow();
|
||||
},
|
||||
});
|
||||
|
||||
// 불가침 제의를 처리하는 국가 커맨드.
|
||||
export class ActionDefinition<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||
> implements GeneralActionDefinition<TriggerState, NonAggressionProposalArgs> {
|
||||
public readonly key = 'che_불가침제의';
|
||||
public readonly name = ACTION_NAME;
|
||||
|
||||
parseArgs(raw: unknown): NonAggressionProposalArgs | null {
|
||||
const data = raw as {
|
||||
destNationId?: unknown;
|
||||
year?: unknown;
|
||||
month?: unknown;
|
||||
};
|
||||
const destNationId = parseNationId(data?.destNationId);
|
||||
const year = parseYear(data?.year);
|
||||
const month = parseMonth(data?.month);
|
||||
if (destNationId === null || year === null || month === null) {
|
||||
return null;
|
||||
}
|
||||
return { destNationId, year, month };
|
||||
}
|
||||
|
||||
buildConstraints(
|
||||
_ctx: ConstraintContext,
|
||||
_args: NonAggressionProposalArgs
|
||||
): Constraint[] {
|
||||
return [
|
||||
beChief(),
|
||||
notBeNeutral(),
|
||||
existsDestNation(),
|
||||
differentDestNation(),
|
||||
requireMinimumTerm(MIN_TERM_MONTHS),
|
||||
disallowDiplomacyBetweenStatus({
|
||||
0: '아국과 이미 교전중입니다.',
|
||||
1: '아국과 이미 선포중입니다.',
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
resolve(
|
||||
_context: GeneralActionResolveContext<TriggerState>,
|
||||
args: NonAggressionProposalArgs
|
||||
): GeneralActionOutcome<TriggerState> {
|
||||
return {
|
||||
effects: [
|
||||
createLogEffect(
|
||||
`${ACTION_NAME}을 준비했습니다. (국가 ${args.destNationId})`,
|
||||
{
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
}
|
||||
),
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const commandSpec: NationTurnCommandSpec = {
|
||||
key: 'che_불가침제의',
|
||||
category: '외교',
|
||||
reqArg: true,
|
||||
args: { destNationId: 0, year: 0, month: 0 },
|
||||
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import type * as NationRestModule from './휴식.js';
|
||||
import type * as AwardModule from './che_포상.js';
|
||||
import type * as AssignmentModule from './che_발령.js';
|
||||
import type * as DeclarationModule from './che_선전포고.js';
|
||||
import type * as NonAggressionProposalModule from './che_불가침제의.js';
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +12,8 @@ export type NationTurnCommandModule =
|
||||
| typeof NationRestModule
|
||||
| typeof AwardModule
|
||||
| typeof AssignmentModule
|
||||
| typeof DeclarationModule;
|
||||
| typeof DeclarationModule
|
||||
| typeof NonAggressionProposalModule;
|
||||
|
||||
export type NationTurnCommandImporter = () => Promise<NationTurnCommandModule>;
|
||||
|
||||
@@ -20,6 +22,7 @@ const defaultImporters = {
|
||||
che_포상: async () => import('./che_포상.js'),
|
||||
che_발령: async () => import('./che_발령.js'),
|
||||
che_선전포고: async () => import('./che_선전포고.js'),
|
||||
che_불가침제의: async () => import('./che_불가침제의.js'),
|
||||
} as const satisfies Record<string, NationTurnCommandImporter>;
|
||||
|
||||
export type NationTurnCommandKey = keyof typeof defaultImporters;
|
||||
@@ -95,3 +98,6 @@ export {
|
||||
export {
|
||||
ActionDefinition as DeclarationActionDefinition,
|
||||
} from './che_선전포고.js';
|
||||
export {
|
||||
ActionDefinition as NonAggressionProposalActionDefinition,
|
||||
} from './che_불가침제의.js';
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import type { General } from '../domain/entities.js';
|
||||
import { allow, readDestGeneral, resolveDestGeneralId, unknownOrDeny } from './helpers.js';
|
||||
import {
|
||||
allow,
|
||||
readDestGeneral,
|
||||
resolveDestGeneralId,
|
||||
resolveDestNationId,
|
||||
unknownOrDeny,
|
||||
} from './helpers.js';
|
||||
import type { Constraint, ConstraintContext, RequirementKey, StateView } from './types.js';
|
||||
|
||||
export const notBeNeutral = (): Constraint => ({
|
||||
@@ -172,6 +178,47 @@ export const existsDestGeneral = (): Constraint => ({
|
||||
},
|
||||
});
|
||||
|
||||
export const destGeneralInDestNation = (): Constraint => ({
|
||||
name: 'DestGeneralInDestNation',
|
||||
requires: (ctx) => {
|
||||
const reqs: RequirementKey[] = [];
|
||||
const destGeneralId = resolveDestGeneralId(ctx);
|
||||
if (destGeneralId !== undefined) {
|
||||
reqs.push({ kind: 'destGeneral', id: destGeneralId });
|
||||
}
|
||||
const destNationId = resolveDestNationId(ctx);
|
||||
if (destNationId !== undefined) {
|
||||
reqs.push({ kind: 'destNation', id: destNationId });
|
||||
}
|
||||
return reqs;
|
||||
},
|
||||
test: (ctx, view) => {
|
||||
const destGeneral = readDestGeneral(ctx, view);
|
||||
if (!destGeneral) {
|
||||
const destGeneralId = resolveDestGeneralId(ctx);
|
||||
if (destGeneralId === undefined) {
|
||||
return unknownOrDeny(ctx, [], '장수 정보가 없습니다.');
|
||||
}
|
||||
const req: RequirementKey = {
|
||||
kind: 'destGeneral',
|
||||
id: destGeneralId,
|
||||
};
|
||||
return unknownOrDeny(ctx, [req], '장수 정보가 없습니다.');
|
||||
}
|
||||
const destNationId = resolveDestNationId(ctx);
|
||||
if (destNationId === undefined) {
|
||||
return unknownOrDeny(ctx, [], '국가 정보가 없습니다.');
|
||||
}
|
||||
if (destGeneral.nationId !== destNationId) {
|
||||
return {
|
||||
kind: 'deny',
|
||||
reason: '제의 장수가 국가 소속이 아닙니다.',
|
||||
};
|
||||
}
|
||||
return allow();
|
||||
},
|
||||
});
|
||||
|
||||
export const friendlyDestGeneral = (): Constraint => ({
|
||||
name: 'FriendlyDestGeneral',
|
||||
requires: (ctx) => {
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import type { General, Nation } from '../domain/entities.js';
|
||||
import { allow, readMetaNumber, readNation, resolveDestNationId, unknownOrDeny } from './helpers.js';
|
||||
import {
|
||||
allow,
|
||||
readGeneral,
|
||||
readMetaNumber,
|
||||
readNation,
|
||||
resolveDestNationId,
|
||||
unknownOrDeny,
|
||||
} from './helpers.js';
|
||||
import type { Constraint, ConstraintContext, RequirementKey, StateView } from './types.js';
|
||||
|
||||
export const notWanderingNation = (): Constraint => ({
|
||||
@@ -166,3 +173,42 @@ export const existsDestNation = (): Constraint => ({
|
||||
return allow();
|
||||
},
|
||||
});
|
||||
|
||||
export const differentDestNation = (): Constraint => ({
|
||||
name: 'DifferentDestNation',
|
||||
requires: (ctx) => {
|
||||
const reqs: RequirementKey[] = [];
|
||||
if (ctx.nationId !== undefined) {
|
||||
reqs.push({ kind: 'nation', id: ctx.nationId });
|
||||
} else {
|
||||
reqs.push({ kind: 'general', id: ctx.actorId });
|
||||
}
|
||||
const destNationId = resolveDestNationId(ctx);
|
||||
if (destNationId !== undefined) {
|
||||
reqs.push({ kind: 'destNation', id: destNationId });
|
||||
}
|
||||
return reqs;
|
||||
},
|
||||
test: (ctx, view) => {
|
||||
const destNationId = resolveDestNationId(ctx);
|
||||
if (destNationId === undefined) {
|
||||
return unknownOrDeny(ctx, [], '국가 정보가 없습니다.');
|
||||
}
|
||||
let baseNationId = ctx.nationId;
|
||||
if (baseNationId === undefined) {
|
||||
const general = readGeneral(ctx, view);
|
||||
if (!general) {
|
||||
const req: RequirementKey = {
|
||||
kind: 'general',
|
||||
id: ctx.actorId,
|
||||
};
|
||||
return unknownOrDeny(ctx, [req], '국가 정보가 없습니다.');
|
||||
}
|
||||
baseNationId = general.nationId;
|
||||
}
|
||||
if (baseNationId === destNationId) {
|
||||
return { kind: 'deny', reason: '같은 국가입니다.' };
|
||||
}
|
||||
return allow();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"휴식",
|
||||
"che_포상",
|
||||
"che_발령",
|
||||
"che_선전포고"
|
||||
"che_선전포고",
|
||||
"che_불가침제의"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user