Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(serve): add nicoliveComment plugin (server side) #1459

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1c783bd
feat(serve): add nicoliveComment plugin on server
xnv Nov 15, 2024
4715b03
feat(serve): rename NicoliveCommentConfigTemplate#startBy value. add …
xnv Nov 18, 2024
585deb2
refactor(serve): rename NiconicoDevtool to NiconicoDevtoolRankingPage
xnv Nov 18, 2024
2d805d7
feat(serve): add NiconicoDevtoolCommentPage (WIP)
xnv Nov 25, 2024
dd7e31b
chore(serve): move NicoliveComment related types to common/
xnv Nov 29, 2024
a9d651b
fix(serve): exclude unused files from 'module' build
xnv Nov 29, 2024
e004ba3
fix(serve): fix broken imports
xnv Nov 29, 2024
5c9af78
refactor(serve): introduce SandboxConfigExternalDefinition
xnv Nov 29, 2024
66483af
feat(serve): introduce niconicoToolActivePage in queryParameters and …
xnv Nov 29, 2024
48a2a14
feat(serve): add NicoliveCommentClientPlugin, DevtoolUiommentPageStore
xnv Nov 29, 2024
98f32af
fix(serve): fix wrong argument for broadcaster in -s nicolive
xnv Dec 4, 2024
fdc4e66
feat(serve): send comments by enter, cosmetic changes
xnv Dec 4, 2024
d728cdb
feat(serve): enable to send nicolive comments from devtool
xnv Dec 4, 2024
ad7a3c4
fix(serve): avoid to throw error in NicoliveCommentPlugin#start(), wa…
xnv Dec 5, 2024
7c15fe2
merge feat-serve-cmt into feat-serve-cmt-ui
xnv Dec 5, 2024
a35590b
fix(serve): revert to throw error in NicoliveCommentPlugin#start()
xnv Dec 5, 2024
5638321
feat(serve): throw error in NicoliveCommentPlugin#start() only when c…
xnv Dec 5, 2024
b211b33
Merge branch 'feat-serve-cmt' into feat-serve-cmt-ui
xnv Dec 5, 2024
3980789
fix(serve): disable enter key on input while composing
xnv Dec 17, 2024
d7243be
chore(serve): fix comments
xnv Dec 17, 2024
4747c8f
Merge branch 'feat-serve-cmt' into feat-serve-cmt-ui
xnv Dec 17, 2024
f0fa91e
fix(serve): fix some bugs around nicoliveComment plugin
xnv Dec 17, 2024
b701590
chore(serve): fix comment
xnv Dec 20, 2024
1230ea2
feat(serve): introduce Store#hashedPlayerId
xnv Dec 20, 2024
aaf3539
feat(serve): reject invalid use of nicoliveComment
xnv Dec 20, 2024
7e25485
Merge pull request #1464 from akashic-games/feat-serve-cmt-ui
xnv Dec 24, 2024
41da412
refactor(serve): rename several types around comments
xnv Dec 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ declare module agv {
coe?: any;
send?: any;
nico?: any;
nicoliveComment?: any;
coeLimited?: any;
instanceStorage?: any;
}
Expand Down
25 changes: 17 additions & 8 deletions packages/akashic-cli-serve/src/client/akashic/ServeGameContent.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type * as amf from "@akashic/amflow";
import type * as realPlaylog from "@akashic/playlog";
import { Trigger } from "@akashic/trigger";
import type { EDumpItem } from "../common/types/EDumpItem";
import type { ProfilerValue } from "../common/types/Profiler";
Expand Down Expand Up @@ -51,9 +52,14 @@ function makeEDumpItem(e: ae.ELike): EDumpItem {
};
}

export interface OnTickArguments {
game: agv.GameLike;
events: realPlaylog.Event[] | undefined;
}

export class ServeGameContent {
readonly agvGameContent: agv.GameContent;
onTick: Trigger<agv.GameLike>;
onTick: Trigger<OnTickArguments>;
onReset: Trigger<amf.StartPoint>;
onWarn: Trigger<RuntimeWarning>;
private _game: agv.GameLike;
Expand All @@ -65,9 +71,9 @@ export class ServeGameContent {
this._game = null!;
this._gameDriver = null!;
this._highlightedEntityId = null;
this.onTick = new Trigger<agv.GameLike>();
this.onReset = new Trigger<amf.StartPoint>();
this.onWarn = new Trigger<RuntimeWarning>();
this.onTick = new Trigger();
this.onReset = new Trigger();
this.onWarn = new Trigger();
}

get id(): number {
Expand Down Expand Up @@ -114,9 +120,12 @@ export class ServeGameContent {
};

const tickOriginal = game.tick;
game.tick = function (_advanceAge: boolean, _omittedTickCount?: number, _events?: playlog.EventLike[]) {
self.onTick.fire(game);
return tickOriginal.apply(this, [_advanceAge, _omittedTickCount, _events]);
game.tick = function (advanceAge: boolean, omittedTickCount?: number, events?: playlog.EventLike[]) {
// EventLike は AkashicGameViewWeb.d.ts の記述の都合上導入した型なので、外部に渡す時は本物の playlog に読み替える
const evs = events as unknown as realPlaylog.Event[] | undefined;
self.onTick.fire({ game, events: evs });

return tickOriginal.call(this, advanceAge, omittedTickCount, events);
};

const gameDriver = this._gameDriver;
Expand Down Expand Up @@ -211,7 +220,7 @@ export class ServeGameContent {
}
}

sendEvents(events: playlog.EventLike[]): void {
sendEvents(events: realPlaylog.Event[]): void {
this.agvGameContent.sendEvents(events);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Trigger } from "@akashic/trigger";
import { callOrThrow } from "../../../common/callOrThrow";
import type { NicoliveCommentPlugin } from "../../../common/types/NicoliveCommentPlugin";

export class NicoliveCommentClientPlugin implements agv.ExternalPlugin {
readonly name: string = "nicoliveComment";
readonly onStartStop: Trigger<boolean> = new Trigger();

onload(game: agv.GameLike, _dataBus: unknown, _gameContent: agv.GameContent): void {
let started = false;

const exposed: NicoliveCommentPlugin = {
start: (_opts, callback) => {
if (started) {
callOrThrow(callback, new Error("g.game.nicoliveComment.start(): already started"));
return;
}
started = true;
this.onStartStop.fire(true);
if (callback)
setTimeout(callback, 0);
},
stop: () => {
if (!started)
throw new Error("g.game.nicoliveComment.stop(): not started");
started = false;
this.onStartStop.fire(false);
},
};
game.external.nicoliveComment = exposed;
}
}
18 changes: 17 additions & 1 deletion packages/akashic-cli-serve/src/client/api/ApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import type {
SandboxConfigApiResponse,
OptionsApiResponse,
PlayerPostApiResponse,
StartPointHeaderListResponse
StartPointHeaderListResponse,
PlaySendNicoliveCommentResponse
} from "../../common/types/ApiResponse";
import type { ContentLocatorData } from "../../common/types/ContentLocatorData";
import type { NicoliveCommentEventComment } from "../../common/types/NicoliveCommentPlugin";
import type { PlayAudioState } from "../../common/types/PlayAudioState";
import type { Player } from "../../common/types/Player";
import * as ApiRequest from "./ApiRequest";
Expand Down Expand Up @@ -125,6 +127,20 @@ export class ApiClient {
);
};

async requestToSendNicoliveCommentByTemplate(playId: string, templateName: string): Promise<PlaySendNicoliveCommentResponse> {
return ApiRequest.post<PlaySendNicoliveCommentResponse>(
`${this._baseUrl}/api/plays/${playId}/comment-template`,
{name: templateName}
);
};

async requestToSendNicoliveComment(playId: string, comment: NicoliveCommentEventComment): Promise<PlaySendNicoliveCommentResponse> {
return ApiRequest.post<PlaySendNicoliveCommentResponse>(
`${this._baseUrl}/api/plays/${playId}/comment`,
{ ...comment, isAnonymous: comment.isAnonymous?.toString(), isOperatorComment: comment.isOperatorComment?.toString() }
);
};

async getContents (): Promise<ContentGetAllApiResponse> {
return ApiRequest.get<ContentGetAllApiResponse>(`${this._baseUrl}/contents/`);
};
Expand Down
7 changes: 6 additions & 1 deletion packages/akashic-cli-serve/src/client/api/Subscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import type {
ClientInstanceDisappearTestbedEvent,
PlayBroadcastTestbedEvent,
PutStartPointEvent,
MessageEncodeTestbedEvent
MessageEncodeTestbedEvent,
NicoliveCommentPluginStartStopTestbedEvent
} from "../../common/types/TestbedEvent";
import { socketInstance } from "./socketInstance";

Expand All @@ -34,6 +35,7 @@ export const onClientInstanceDisappear = new Trigger<ClientInstanceDisappearTest
export const onBroadcast = new Trigger<any>();
export const onDisconnect = new Trigger<void>();
export const onPutStartPoint = new Trigger<PutStartPointEvent>();
export const onNicoliveCommentPluginStartStop = new Trigger<NicoliveCommentPluginStartStopTestbedEvent>();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このトリガーまでは fire していますがこの先の利用者は未実装です。


const socket = socketInstance;
socket.on("playCreate", (arg: PlayCreateTestbedEvent) => onPlayCreate.fire(arg));
Expand All @@ -51,6 +53,9 @@ socket.on("clientInstanceDisappear", (arg: ClientInstanceDisappearTestbedEvent)
socket.on("playBroadcast", (arg: PlayBroadcastTestbedEvent) => onBroadcast.fire(arg));
socket.on("disconnect", () => onDisconnect.fire());
socket.on("putStartPoint", (arg: PutStartPointEvent) => onPutStartPoint.fire(arg));
socket.on("nicoliveCommentPluginStartStop", (arg: NicoliveCommentPluginStartStopTestbedEvent) => {
onNicoliveCommentPluginStartStop.fire(arg);
});

export const onMessageEncode = new Trigger<MessageEncodeTestbedEvent>();

Expand Down
16 changes: 11 additions & 5 deletions packages/akashic-cli-serve/src/client/common/queryParameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,27 +149,32 @@ export interface ServeQueryParameters {
showsHiddenEntity: boolean | null;

/**
* Niconico ツールが有効な時に、セッションパラメータのイベントを送るか。
* Niconico ツールのアクティブなページ。
*/
niconicoToolActigePage: "ranking" | "comment" | null;

/**
* Niconico ツールの Ranking ページが有効な時に、セッションパラメータのイベントを送るか。
*/
isAutoSendEvents: boolean | null;

/**
* Niconico ツールで、ranking モードのカウントダウンを行うか。
* Niconico ツール Ranking ページで、ranking モードのカウントダウンを行うか。
*/
emulatingShinichibaMode: string | null;

/**
* Niconico ツールで、ranking モードの totalTimeLimit を game.json の preferredSessionParameters から決定するか。
* Niconico ツール Ranking ページで、ranking モードの totalTimeLimit を game.json の preferredSessionParameters から決定するか。
*/
usePreferredTotalTimeLimit: boolean | null;

/**
* Niconico ツールで、ranking モードの制限時間経過時にゲームを停止するか。
* Niconico ツール Ranking ページで、ranking モードの制限時間経過時にゲームを停止するか。
*/
stopsGameOnTimeout: boolean | null;

/**
* Niconico ツールの totalTimeLimit 入力欄の内容。
* Niconico ツール Ranking ページの totalTimeLimit 入力欄の内容。
*/
totalTimeLimitInputValue: number | null;

Expand Down Expand Up @@ -291,6 +296,7 @@ export function makeServeQueryParameters(query: RawParsedQuery): ServeQueryParam
showsBackgroundImage: asBool(query.showsBackgroundImage),
showsBackgroundColor: asBool(query.showsBackgroundColor),
showsGrid: asBool(query.showsGrid),
niconicoToolActigePage: asEnum(query.niconicoToolActigePage, ["ranking", "comment"]),
isAutoSendEvents: asBool(query.isAutoSendEvents),
emulatingShinichibaMode: asString(query.emulatingShinichibaMode),
usePreferredTotalTimeLimit: asBool(query.usePreferredTotalTimeLimit),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { isNicoliveCommentEvent, MessageEventIndexData } from "../../common/PlaylogShim";
import type { OnTickArguments } from "../akashic/ServeGameContent";
import type { EDumpItem } from "../common/types/EDumpItem";
import type { NiconicoDevtoolCommentPageSenderLimitation, NiconicoDevtoolCommentPageSenderType } from "../store/DevtoolUiCommentPageStore";
import type { Store } from "../store/Store";
import type { NiconicoDevtoolPageType } from "../view/molecule/NiconicoDevtool";

function consoleLog(value: any): void {
console.log(value);
Expand Down Expand Up @@ -145,7 +149,61 @@ export class DevtoolOperator {
gameContent.onTick.add(this.tickHandler, this);
};

private async tickHandler(game: agv.GameLike): Promise<void> {
setNiconicoDevtoolActivePage = (pageType: NiconicoDevtoolPageType): void => {
this.store.devtoolUiStore.setNiconicoToolActivePage(pageType);
};

setNiconicoDevtoolSelectorWidth = (w: number): void => {
this.store.devtoolUiStore.setNiconicoToolSelectorWidth(w);
};

resetCommentPage = (
templates: string[],
senderType: NiconicoDevtoolCommentPageSenderType,
senderLimitation: NiconicoDevtoolCommentPageSenderLimitation
): void => {
const commentPageStore = this.store.devtoolUiStore.commentPage;
commentPageStore.resetComments();
commentPageStore.setTemplates(templates);
commentPageStore.setIsEnabled(false);
commentPageStore.setSenderType(senderType);
commentPageStore.setSenderLimitation(senderLimitation);
};

setCommentPageIsEnabled = (isEnabled: boolean): void => {
this.store.devtoolUiStore.commentPage.setIsEnabled(isEnabled);
};

setCommentPageSenderType = (senderType: NiconicoDevtoolCommentPageSenderType): void => {
this.store.devtoolUiStore.commentPage.setSenderType(senderType);
};

setCommentPageCommandInput = (input: string): void => {
this.store.devtoolUiStore.commentPage.setCommandInput(input);
};

setCommentPageCommentInput = (input: string): void => {
this.store.devtoolUiStore.commentPage.setCommentInput(input);
};

startWatchNicoliveComment = (): void => {
this.store.currentLocalInstance?.gameContent.onTick.add(this.nicoliveCommentWatcher);
};

stopWatchNicoliveComment = (): void => {
this.store.currentLocalInstance?.gameContent.onTick.remove(this.nicoliveCommentWatcher);
};

private nicoliveCommentWatcher = ({ events }: OnTickArguments): void => {
if (!events) return;
for (let i = 0; i < events.length; ++i) {
const ev = events[i];
if (isNicoliveCommentEvent(ev))
this.store.devtoolUiStore.commentPage.addComments(ev[MessageEventIndexData].comments);
}
};

private async tickHandler({ game }: OnTickArguments): Promise<void> {
if (this.store.devtoolUiStore.stopsGameOnTimeout)
await this.updateRemainingTime();

Expand Down
47 changes: 33 additions & 14 deletions packages/akashic-cli-serve/src/client/operator/Operator.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import type { NormalizedSandboxConfiguration } from "@akashic/sandbox-configuration";
import { isServiceTypeNicoliveLike } from "../../common/targetServiceUtil";
import type { SandboxConfigExternalDefinition } from "../../common/types/NicoliveCommentConfig";
import type { Player } from "../../common/types/Player";
import type { PlayBroadcastTestbedEvent } from "../../common/types/TestbedEvent";
import type { GameViewManager } from "../akashic/GameViewManager";
import type { PlayerInfoResolverResultMessage } from "../akashic/plugin/CoeLimitedPlugin";
import { CoeLimitedPlugin } from "../akashic/plugin/CoeLimitedPlugin";
import type { CreateCoeLocalInstanceParameterObject } from "../akashic/plugin/CoePlugin";
import { CoePlugin } from "../akashic/plugin/CoePlugin";
import { NicoliveCommentClientPlugin } from "../akashic/plugin/NicoliveCommentClientPlugin";
import { NicoPlugin } from "../akashic/plugin/NicoPlugin";
import { SendPlugin } from "../akashic/plugin/SendPlugin";
import { apiClient } from "../api/apiClientInstance";
Expand Down Expand Up @@ -77,9 +80,10 @@ export class Operator {
play = await this._createServerLoop(loc, initialJoinPlayer, false, false); // TODO: (起動時の最初のプレイで) audioState を指定する方法
}
}
await this.setCurrentPlay(play, query.mode === "replay");
const isReplay = query.mode === "replay";
await this.setCurrentPlay(play, isReplay);

if (query.mode === "replay") {
if (isReplay) {
if (query.replayResetAge != null) {
await this.localInstance.resetByAge(query.replayResetAge);
}
Expand Down Expand Up @@ -109,9 +113,7 @@ export class Operator {
if (store.currentPlay === play)
return;

let previousPlay;
if (store.currentPlay) {
previousPlay = store.currentPlay;
await store.currentPlay.deleteAllLocalInstances();
store.setCurrentLocalInstance(null);
}
Expand All @@ -120,25 +122,28 @@ export class Operator {

store.setCurrentPlay(play);

let argument = undefined;
if (isServiceTypeNicoliveLike(store.targetService)) {
let isBroadcaster = false;
if (previousPlay) {
isBroadcaster = previousPlay.joinedPlayerTable.has(store.player!.id);
} else {
isBroadcaster = play.joinedPlayerTable.size === 0;
}
argument = this._createInstanceArgumentForNicolive(isBroadcaster);
}
const isServiceNicolive = isServiceTypeNicoliveLike(store.targetService);
const isNicoliveBroadcaster = isServiceNicolive && play.joinedPlayerTable.has(store.player!.id);

let argument = undefined;
if (query.argumentsValue) {
argument = JSON.parse(query.argumentsValue);
store.startupScreenUiStore.setInstanceArgumentEditContent(query.argumentsValue, true);
} else if (query.argumentsName && !!store.currentPlay!.content.argumentsTable[query.argumentsName]) {
argument = JSON.parse(store.currentPlay!.content.argumentsTable[query.argumentsName]);
this.ui.selectInstanceArguments(query.argumentsName, true);
} else if (isServiceNicolive) {
argument = this._createInstanceArgumentForNicolive(isNicoliveBroadcaster);
}

const sandboxConfig = play.content.sandboxConfig as NormalizedSandboxConfiguration & SandboxConfigExternalDefinition;
const commentTemplateNames = Object.keys(sandboxConfig.external?.nicoliveComment?.templates || []);
this.devtool.resetCommentPage(
commentTemplateNames,
isNicoliveBroadcaster ? "operator" : "anonymous",
isServiceNicolive ? (isNicoliveBroadcaster ? "operator" : "audience") : "none",
);

if (store.appOptions.autoStart) {
await this.startContent({
joinsSelf: false,
Expand Down Expand Up @@ -174,6 +179,9 @@ export class Operator {
this.store.profilerStore.pushProfilerValueResult("rendering", value.renderingTime);
});

this.store.devtoolUiStore.initTotalTimeLimit(play!.content.preferredSessionParameters.totalTimeLimit!);
this.devtool.setupNiconicoDevtoolValueWatcher();

if (params != null && params.joinsSelf) {
store.currentPlay!.join(store.player!.id, store.player!.name);
}
Expand Down Expand Up @@ -271,7 +279,18 @@ export class Operator {

// TODO: 複数のコンテンツ対応。引数の contentLocator は複数コンテンツに対応していないが暫定とする
private async _initializePlugins(contentLocator: ClientContentLocator): Promise<void> {
const commentPlugin = new NicoliveCommentClientPlugin();
commentPlugin.onStartStop.add(started => {
this.devtool.setCommentPageIsEnabled(started);
if (started) {
this.devtool.startWatchNicoliveComment();
} else {
this.devtool.stopWatchNicoliveComment();
}
});

this.gameViewManager.registerExternalPlugin(new NicoPlugin());
this.gameViewManager.registerExternalPlugin(commentPlugin);
this.gameViewManager.registerExternalPlugin(new SendPlugin());
this.gameViewManager.registerExternalPlugin(new CoePlugin({
onLocalInstanceCreate: this._createLocalSessionInstance,
Expand Down
Loading
Loading