fix: complete logical clock wall-time isolation

This commit is contained in:
2026-08-04 20:37:56 +09:00
committed by Hide_D
parent 5d9512ced7
commit fc6f84ce23
67 changed files with 689 additions and 231 deletions
+3 -1
View File
@@ -42,6 +42,7 @@
:maxTurn="maxChiefTurn"
:maxPushTurn="Math.floor(maxChiefTurn / 2)"
:date="date"
:clockMode="clockMode"
:officer="officer"
@raiseReload="reloadTable()"
/>
@@ -139,6 +140,7 @@ const tableObj = reactive<Omit<OptionalFull<ChiefResponse>, "result">>({
month: undefined,
turnTerm: undefined,
date: undefined,
clockMode: undefined,
troopList: undefined,
chiefList: undefined,
isChief: undefined,
@@ -149,7 +151,7 @@ const tableObj = reactive<Omit<OptionalFull<ChiefResponse>, "result">>({
unitSet: undefined,
});
const { year, month, turnTerm, date, chiefList, troopList, officerLevel, commandList } = toRefs(tableObj);
const { year, month, turnTerm, date, clockMode, chiefList, troopList, officerLevel, commandList } = toRefs(tableObj);
let postFilterNationCommand = function (turnObj: TurnObj): TurnObj {
return turnObj;
+1 -9
View File
@@ -195,7 +195,6 @@ import { onMounted, reactive, ref, watch, computed } from "vue";
import type { VoteInfo, VoteDetailResult } from "@/defs/API/Vote";
import { SammoAPI } from "@/SammoAPI";
import { isString, range, sum } from "lodash-es";
import { formatTime } from "@/util/formatTime";
import { isBrightColor } from "@/util/isBrightColor";
import { formatVoteColor } from "@/utilGame/formatVoteColor";
@@ -239,14 +238,7 @@ const canVote = computed(() => {
if (currentVote.value.myVote) {
return false;
}
const endDate = currentVote.value.voteInfo.endDate;
if (endDate) {
const now = formatTime(new Date());
if (now > endDate) {
return false;
}
}
return true;
return currentVote.value.isOpen;
});
const currentVoteID = ref<number>();
+3 -1
View File
@@ -13,7 +13,7 @@
class="col alert alert-primary m-0 p-0"
style="text-align: center; display: flex; justify-content: center; align-items: center"
>
<SimpleClock :serverTime="serverNow" />
<SimpleClock :serverTime="serverNow" :running="clockRunning" />
</div>
<div class="col d-grid">
<BDropdown right text="반복">
@@ -467,6 +467,7 @@ async function pushGeneralCommand(amount: number) {
}
const serverNow = ref(new Date());
const clockRunning = ref(true);
function pushGeneralCommandSingle(e: Event) {
//NOTE: split 구현에 버그가 있어서, 수동으로 구분해야함
@@ -548,6 +549,7 @@ async function reloadCommandList() {
}
serverNow.value = parseTime(result.date);
clockRunning.value = result.clockMode === "realtime";
}
async function reserveCommandDirect(args: [number[], TurnObj][], reload = true): Promise<boolean> {
+5 -1
View File
@@ -27,7 +27,7 @@
<div class="row gx-1 gy-1 py-1">
<div class="col-lg-4 mx-0 mb-0 mt-1 d-grid">
<div class="alert alert-primary mb-0 center" style="padding: 0.5rem 0">
<SimpleClock :serverTime="parseTime(props.date)" />
<SimpleClock :serverTime="parseTime(props.date)" :running="props.clockMode === 'realtime'" />
</div>
</div>
@@ -289,6 +289,10 @@ const props = defineProps({
maxTurn: VueTypes.integer.isRequired,
maxPushTurn: VueTypes.integer.isRequired,
date: VueTypes.string.isRequired,
clockMode: {
type: String as PropType<"realtime" | "manual">,
required: true,
},
year: VueTypes.integer.isRequired,
month: VueTypes.integer.isRequired,
turnTerm: VueTypes.integer.isRequired,
+2 -5
View File
@@ -126,8 +126,6 @@
<script setup lang="ts">
import type { MsgItem, MsgTarget, MsgType } from "@/defs/API/Message";
import { parseTime } from "@/util/parseTime";
import { differenceInMilliseconds, addMinutes } from "date-fns/esm";
import { computed, onMounted, ref, toRef, watch, type ComputedRef, type Ref } from "vue";
import linkifyStr from "linkify-string";
import { SammoAPI } from "@/SammoAPI";
@@ -205,11 +203,10 @@ function testDeletable(msg: MsgItem): boolean {
if (msg.option.invalid) return false;
if (!(msg.option.deletable ?? true)) return false;
const now = new Date();
const last5min = addMinutes(parseTime(msg.time), 5);
const timeDiff = differenceInMilliseconds(last5min, now);
const timeDiff = msg.deleteRemainingMilliseconds;
if (timeDiff <= 0) return false;
if (msg.clockMode === "manual") return true;
deletableTimer.value = window.setTimeout(() => {
deletable.value = testDeletable(msg);
+27 -7
View File
@@ -4,7 +4,7 @@
<script lang="ts" setup>
import { addMilliseconds } from "date-fns";
import { type PropType, ref, onMounted, watch } from "vue";
import { type PropType, ref, onMounted, onUnmounted, watch } from "vue";
import { formatTime } from "@/util/formatTime";
const props = defineProps({
serverTime: {
@@ -17,25 +17,39 @@ const props = defineProps({
required: false,
default: "HH:mm:ss",
},
running: {
type: Boolean,
default: true,
},
});
const timeDiff = ref(0);
const serverNow = ref("");
watch(
() => props.serverTime,
(newValue) => {
() => [props.serverTime, props.running] as const,
([newValue]) => {
const clientNow = new Date();
timeDiff.value = newValue.getTime() - clientNow.getTime();
updateNow();
}
);
let timer: ReturnType<typeof setTimeout> | undefined;
function updateNow() {
const serverNowObj = addMilliseconds(new Date(), timeDiff.value);
if (timer !== undefined) {
clearTimeout(timer);
timer = undefined;
}
const serverNowObj = props.running
? addMilliseconds(new Date(), timeDiff.value)
: props.serverTime;
serverNow.value = formatTime(serverNowObj, props.timeFormat);
setTimeout(() => {
updateNow();
}, 1000 - serverNowObj.getMilliseconds());
if (props.running) {
timer = setTimeout(() => {
updateNow();
}, 1000 - serverNowObj.getMilliseconds());
}
}
onMounted(() => {
@@ -43,4 +57,10 @@ onMounted(() => {
timeDiff.value = props.serverTime.getTime() - clientNow.getTime();
updateNow();
});
onUnmounted(() => {
if (timer !== undefined) {
clearTimeout(timer);
}
});
</script>
+1
View File
@@ -7,6 +7,7 @@ export type ReservedCommandResponse = {
year: number;
month: number;
date: string;
clockMode: "realtime" | "manual";
turn: TurnObj[];
autorun_limit: null | number;
};
+3 -1
View File
@@ -34,6 +34,8 @@ export type MsgItem = {
delete?: number;
};
time: string;
deleteRemainingMilliseconds: number;
clockMode: "realtime" | "manual";
};
export type MsgPrintItem = MsgItem & {
@@ -77,4 +79,4 @@ export type MailboxItem = {
export type MabilboxListResponse = {
result: true,
nation: MailboxItem[]
}
}
+2 -1
View File
@@ -7,6 +7,7 @@ export type ChiefResponse = {
month: number;
turnTerm: number;
date: string;
clockMode: "realtime" | "manual";
chiefList: Record<
number,
{
@@ -28,4 +29,4 @@ export type ChiefResponse = {
}[];
mapName: string,
unitSet: string,
};
};
+2 -1
View File
@@ -30,4 +30,5 @@ export type VoteDetailResult = ValidResponse & {
comments: VoteComment[],
myVote: null|number[],
userCnt: number,
}
isOpen: boolean,
}
+4 -5
View File
@@ -5,7 +5,6 @@ import axios from 'axios';
import { initTooltip } from "@/legacy/initTooltip";
import { TemplateEngine } from '@util/TemplateEngine';
import type { InvalidResponse } from '@/defs';
import { getDateTimeNow } from '@util/getDateTimeNow';
import { setAxiosXMLHttpRequest } from '@util/setAxiosXMLHttpRequest';
import { loadPlugin as loadAdminPlugin } from '@/gateway/admin_server';
import '@/gateway/common';
@@ -108,6 +107,7 @@ type ReservedGameInfo = {
type GameInfo = {
isUnited: number,
isOpen: boolean,
npcMode: '불가' | '가능' | '선택 생성',
year: number,
month: number,
@@ -175,7 +175,6 @@ async function Entrance_UpdateServer() {
async function Entrance_drawServerList(serverInfos: ServerResponseItem[]) {
const $serverList = $('#server_list');
const now = getDateTimeNow();
const serverDetailInfoP: Record<string, Promise<ServerDetailResponse>> = {};
@@ -239,7 +238,7 @@ async function Entrance_drawServerList(serverInfos: ServerResponseItem[]) {
} else if (game.isUnited == 2) {
$serverHtml.find('.n_country').html('§천하통일§');
$serverHtml.find('.server_date').html(`${game.starttime} <br>~ ${game.turntime}`);
} else if (game.opentime <= now) {
} else if (game.isOpen) {
$serverHtml.find('.n_country').html(`<${game.nationCnt}국 경쟁중>`);
$serverHtml.find('.server_date').html(`${game.starttime} ~`);
} else {
@@ -247,7 +246,7 @@ async function Entrance_drawServerList(serverInfos: ServerResponseItem[]) {
$serverHtml.find('.server_date').html(`${game.starttime} ~`);
}
if (game.opentime <= now) {
if (game.isOpen) {
$serverHtml.append(
TemplateEngine(serverTextInfo, game)
);
@@ -294,4 +293,4 @@ async function Entrance_Logout() {
return;
}
location.href = "../";
}
}
+9 -14
View File
@@ -1,15 +1,11 @@
import $ from 'jquery';
import type { InvalidResponse } from '@/defs';
import { getDateTimeNow } from '@util/getDateTimeNow';
import axios from 'axios';
import { convertFormData } from '@util/convertFormData';
import { isBrightColor } from "@util/isBrightColor";
import { unwrap } from '@util/unwrap';
import _, { isError, isString } from 'lodash-es';
import { addMinutes } from 'date-fns';
import { parseTime } from '@util/parseTime';
import { formatTime } from '@util/formatTime';
import { TemplateEngine } from '@util/TemplateEngine';
import { isNotNull } from '@util/isNotNull';
import { unwrap_any } from '@util/unwrap_any';
@@ -32,8 +28,8 @@ const messageTemplate = `<div
</div>
<div class="msg_body">
<div class="msg_header">
<%if(!this.option.action && src.id == myGeneralID && now <= last5min && invalidType == 'msg_valid' && !deletable){%>
<button type="button" data-erase_until="<%last5min%>" class="btn btn btn-outline-warning btn-sm btn-delete-msg" style='float:right'>❌</button>
<%if(!this.option.action && src.id == myGeneralID && deleteRemainingMilliseconds > 0 && invalidType == 'msg_valid' && !deletable){%>
<button type="button" data-erase_until="<%eraseUntil%>" class="btn btn btn-outline-warning btn-sm btn-delete-msg" style='float:right'>❌</button>
<%}%>
<%if(msgType == 'private') {%>
<%if(src.name == generalName){%>
@@ -101,8 +97,7 @@ type MsgPrintItem = MsgItem & {
nationType: 'local' | 'src' | 'dest';
myGeneralID: number;
allowButton: boolean;
last5min: string;
now: string;
eraseUntil: number;
invalidType: 'msg_invalid' | 'msg_valid';
deletable: boolean;
src: MsgTarget & { colorType: 'bright' | 'dark' },
@@ -217,7 +212,7 @@ async function showOldMsg(msgType: MsgType): Promise<MsgResponse> {
function redrawMsg(msgResponse: MsgResponse, addFront: boolean): MsgResponse {
function checkErasable(obj: MsgResponse) {
const now = getDateTimeNow();
const now = Date.now();
$('.btn-delete-msg').each(function () {
const $btn = $(this);
const eraseUntil = $btn.data('erase_until');
@@ -257,7 +252,6 @@ function redrawMsg(msgResponse: MsgResponse, addFront: boolean): MsgResponse {
let needRefreshLastContact = (msgType == 'private');
const now = getDateTimeNow();
//list의 맨 앞이 가장 최신 메시지임.
const $msgs: JQuery<HTMLElement>[] = msgSource.map(function (msg) {
@@ -311,7 +305,9 @@ function redrawMsg(msgResponse: MsgResponse, addFront: boolean): MsgResponse {
allowButton = true;
}
const last5min = formatTime(addMinutes(parseTime(msg.time), 5));
const eraseUntil = msg.clockMode === "manual"
? Number.MAX_SAFE_INTEGER
: Date.now() + msg.deleteRemainingMilliseconds;
let invalidType: MsgPrintItem['invalidType'];
if (msg.option && msg.option.invalid) {
invalidType = 'msg_invalid';
@@ -336,9 +332,8 @@ function redrawMsg(msgResponse: MsgResponse, addFront: boolean): MsgResponse {
myGeneralID: unwrap(myGeneralID),
src,
dest,
now,
allowButton,
last5min,
eraseUntil,
invalidType,
deletable,
defaultIcon,
@@ -653,4 +648,4 @@ $(async function ($) {
const msgType = $this.data('msg_type');
void showOldMsg(msgType);
})
});
});
+13 -5
View File
@@ -42,8 +42,12 @@ type CardItem = {
type GeneralPoolResponse = {
result: true,
pick: CardItem[],
validUntil: string,
}
validUntil: string,
validForSeconds: number,
clockMode: "realtime" | "manual",
}
let logicalClockRunning = true;
declare const characterInfo: Record<string, { name: string, info: string }>;
declare const hasGeneralID: number;
@@ -185,7 +189,10 @@ async function buildGeneral(e: JQuery.Event) {
location.href = './';
}
function updateOutdateTimer() {
function updateOutdateTimer() {
if (!logicalClockRunning) {
return;
}
const $validUntilText = $('#valid_until_text');
const now = Date.now();
const validUntil = $validUntilText.data('until');
@@ -204,10 +211,11 @@ function updateOutdateTimer() {
setTimeout(updateOutdateTimer, 1000);
}
function printGenerals(value: GeneralPoolResponse) {
function printGenerals(value: GeneralPoolResponse) {
logicalClockRunning = value.clockMode === "realtime";
$('.card_holder').empty();
$('#valid_until').show();
$('#valid_until_text').html(value.validUntil).data('until', (new Date(value.validUntil)).getTime()).css('color', 'white');
$('#valid_until_text').html(value.validUntil).data('until', Date.now() + value.validForSeconds * 1000).css('color', 'white');
$('#outdate_token').hide();
const pick = value.pick.map(v => v);//XXX: 의도가 뭐였지? clone?
+24 -9
View File
@@ -68,9 +68,13 @@ type NPCToken = {
result: true,
pick: Record<number, NPCPick>,
pickMoreFrom: string,
pickMoreSeconds: number,
validUntil: string,
}
pickMoreSeconds: number,
validUntil: string,
validForSeconds: number,
clockMode: "realtime" | "manual",
}
let logicalClockRunning = true;
const templateGeneralCard =
'<div class="general_card">\
@@ -146,7 +150,10 @@ async function pickGeneral(this: HTMLElement, e: JQuery.Event) {
location.href = './';
}
function updateOutdateTimer() {
function updateOutdateTimer() {
if (!logicalClockRunning) {
return;
}
const $validUntilText = $('#valid_until_text');
const now = Date.now();
const validUntil = $validUntilText.data('until');
@@ -165,8 +172,15 @@ function updateOutdateTimer() {
setTimeout(updateOutdateTimer, 1000);
}
function updatePickMoreTimer() {
const $btn = $('#btn_pick_more');
function updatePickMoreTimer() {
const $btn = $('#btn_pick_more');
if (!logicalClockRunning) {
const remain = Number($btn.data('remaining'));
$btn.prop('disabled', remain > 0);
$btn.html(remain > 0 ? '다른 장수 보기(논리 시계 대기)' : '다른 장수 보기');
return;
}
const now = Date.now();
const remain = ($btn.data('available') - now) / 1000;
@@ -181,13 +195,14 @@ function updatePickMoreTimer() {
setTimeout(updatePickMoreTimer, 250);
}
function printGenerals(value: NPCToken) {
function printGenerals(value: NPCToken) {
logicalClockRunning = value.clockMode === "realtime";
$('.card_holder').empty();
$('#valid_until').show();
$('#valid_until_text').html(value.validUntil).data('until', (new Date(value.validUntil)).getTime()).css('color', 'white');
$('#valid_until_text').html(value.validUntil).data('until', Date.now() + value.validForSeconds * 1000).css('color', 'white');
$('#outdate_token').hide();
const time = Date.now() + value.pickMoreSeconds * 1000;
$('#btn_pick_more').data('available', time).prop('disabled', true);
$('#btn_pick_more').data('available', time).data('remaining', value.pickMoreSeconds).prop('disabled', true);
const pick = $.map(value.pick, function (value) {
return value;