fix: complete logical clock wall-time isolation
This commit is contained in:
@@ -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
@@ -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>();
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -7,6 +7,7 @@ export type ReservedCommandResponse = {
|
||||
year: number;
|
||||
month: number;
|
||||
date: string;
|
||||
clockMode: "realtime" | "manual";
|
||||
turn: TurnObj[];
|
||||
autorun_limit: null | number;
|
||||
};
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -30,4 +30,5 @@ export type VoteDetailResult = ValidResponse & {
|
||||
comments: VoteComment[],
|
||||
myVote: null|number[],
|
||||
userCnt: number,
|
||||
}
|
||||
isOpen: boolean,
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user