// 定数 ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // 座標チャンネル入室パターン const string CHANNEL_JOIN_TYPE_USE_URL_ROOMID = "roomid"; const string CHANNEL_JOIN_TYPE_BY_LOBBYING_CHANNEL = "lobby"; const string CHANNEL_JOIN_TYPE_RANDOM_CHANNEL = "random"; const string CHANNEL_JOIN_TYPE_SELECT_ANY_CHANNEL = "select"; const string CHANNEL_JOIN_TYPE_CREATE_NEW_RANDOM_CHANNEL = "createrandom"; const string CHANNEL_JOIN_TYPE_REDIRECT_TO_OUTGAME = "redirect"; const string CHANNEL_JOIN_TYPE_CUSTOM = "custom"; const string CHANNEL_JOIN_TYPE_NONE = "none"; delegate void JoinChannelDelegate(JsVal); delegate bool CheckAvailableDelegate(JsVal); JoinChannelDelegate g_FirstJoinFunc; JoinChannelDelegate g_FailedJoinFunc; string g_authorityJwt; string g_ticketJwt; string g_AfterJwtChannelId; bool g_RequiredTickets; fVoidCallback g_ReConnectCallback; class ChSBridge { // 初回の heliport.v3.api.channelSessionApi.initChannelSession() がすでに実行されているか bool m_HasStartedFirstChannelSessionInitialization; // 入室ダイアログ処理を通ったか bool m_DecidedEnterFlag; // ワールド情報のFetchが終わったか bool m_LoadedWorldInfo; // プロフィールの初期化が終わったか bool m_InitedProfile; // 初回入室処理を行ったか bool m_FirstJoined; bool m_AllowMultiVoiceChannel; ChSModel m_SubModel; VCCModel m_VCCModel; string m_ReConnectChannelId; ChSErrorDialog m_ErrorDialog; public ChSBridge() { m_DecidedEnterFlag = false; m_LoadedWorldInfo = false; m_InitedProfile = false; m_FirstJoined = false; m_AllowMultiVoiceChannel = false; m_SubModel = system.Layer_GetComponentByName("vcc_icon_only_base"); m_VCCModel = system.Layer_GetComponentByName("vcc_icon_only_base"); // FirstEnterをHeliScript側で行うように設定する hsNetSetScriptControllableEnter(true); m_ErrorDialog = new ChSErrorDialog(); hsAddEventListener("login-state-change", OnLoginStateChange); } // ログイン状態が変化したときのイベントリスナー。 // (HeliScriptのコールバック関数ではない) void OnLoginStateChange(string jsonParam) { if (!m_HasStartedFirstChannelSessionInitialization) { m_HasStartedFirstChannelSessionInitialization = true; // ChS-APIの初期化 JsVal options = makeJsObj(); options.AddPropertyByVal("enableHeartbeatInterval", makeJsBoolFrom(false)); heliport.v3.api.channelSessionApi.initChannelSession(_initChannelSessionCallbackForFirstConnect, options); } else { // ログインステータスが変わっているのでユーザー情報を初期化する // ここの実装もapi.channelSessionApi.refreshChannelSessionに置き換えられそうだが、リリースが近いのでいったんそのままにしておく // initChannelSessionはChS-API内部のユーザー情報を書き換えるために呼んでいるが、Profileクラスで好き勝手に呼ばれると // ChS-APIに入室できなくなってしまうのでVCCクラスに『いったん退出 → initChannelSession → 再入室』の処理を行うようにリクエストを投げている // refreshChannelSessionでこの問題が起こらなくなるとすると置き換えが可能ということに(時間がある時に詳しくチェックする) ReConnectIfJoining(_ResponseInitSession); } } void _ResponseInitSession() { hsCallCanvasComponentMethod("config_profile", "ProfileMenuViewModel", "ResponseInitSession", ""); } public void OnNetEstablishmentFail(string ErrorCode) { // エラーが発生したのでエラーダイアログを表示 m_ErrorDialog.ShowFailedConnection(ErrorCode); } public void OnNetConnectionError(string ErrorCode) { // エラーが発生したのでエラーダイアログを表示 m_ErrorDialog.ShowFailedConnection(ErrorCode); } public void OnNetOperationError(string ErrorCode) { // エラーが発生したのでエラーダイアログを表示 m_ErrorDialog.ShowFailedConnection(ErrorCode); } void OnNetLobbyingUpdate(string lobbyDataJson) { // TODO: 必ず発火されるログなので、実装が安定したら削除する // https://discord.com/channels/596169759045976094/1371444516829003849/1384733395774472294 hsSystemOutputLog("OnNetLobbyingUpdate: " + lobbyDataJson); } // ChSModel._CreatePosChannelCallback() // ChSJoinProcess._JoinRandomPositionChannelCallback() // から呼び出される共通処理 public void HandlePositionChannelCreationError(string ErrorCode) { m_ErrorDialog.ShowFailedCreatePositionChanel(ErrorCode); } public void SetInitedProfile(string param) { m_InitedProfile = (param == "true"); } public void Update() { if(!m_FirstJoined) { // 入室ダイアログ・ワールド情報のFetch・プロフィールの初期化が終わっていれば初回入室処理を実行する if(m_DecidedEnterFlag && m_LoadedWorldInfo && m_InitedProfile) { m_FirstJoined = true; // 初回入室処理 m_SubModel.FirstEnterPositionChannel(); } } } // 利用規約の同意の入室ボタン押下、もしくはログイン済み処理をスキップしたとき public void OnDecidedEnterDialog() { m_DecidedEnterFlag = true; hel_net_can_access_microphone(_accesMicroPhoneCheckCallback); } public bool IsAllowMultiVoiceChannel() { return m_AllowMultiVoiceChannel; } void _initChannelSessionCallback(ReturnType result) { JsVal channelSession = result.data.GetProperty("channel_session"); m_VCCModel.SetSelfChannelSessionCode(channelSession.GetProperty("channel_session_code").GetStr()); heliport.v3.api.apiNotification.unWatchPersonal(); heliport.v3.api.apiNotification.watchPersonal(channelSession.GetProperty("user_code").GetStr()); } void _initChannelSessionCallbackForFirstConnect(JsVal val) { ReturnType result = hsLoadReturnType(val); if(!result.IsValid()) return; _initChannelSessionCallback(result); hel_net_initialize(_initHeliNetwork); hsCallCanvasComponentMethod("action_palette", "ActionPaletteWithServer", "CheckLoggedInForEmote"); } void _initHeliNetwork() { heliport.v3.api.channelSessionApi.fetchWorldDetail(_fetchWorldDetailCallback, hsNetGetSpatiumCode(), hsGetCurrentWorldId()); } // マイク使用可否判定コールバック void _accesMicroPhoneCheckCallback(bool result) { if (result) { // マイクアクセス可能ならデバイス一覧を取得 hel_net_init_mic_device(_initMicCallback); } else { // マイク使用不可の場合、ブロックダイアログを表示 hsCanvasSetLayerShow("mic_block_info_layer",true); } } void _initMicCallback(JsVal val) { } void _fetchWorldDetailCallback(JsVal val) { ReturnType result = hsLoadReturnType(val); if(!result.IsValid()) return; // カーネリアンのような複数音声チャンネルを許可するか m_AllowMultiVoiceChannel = result.data.GetProperty("world").GetProperty("allow_multi_voice_channel").GetBool(); if(m_AllowMultiVoiceChannel) { // 複数ボイスチャンネルようのGUIをオンにする hsCanvasResetToggleDefault("Toggle_VCC_VCh_Single_Multiple"); hsCanvasToggleChange("Toggle_VCC_VCh_Single_Multiple"); } // 入退出処理の登録 m_SubModel.RegistJointProcess(); m_LoadedWorldInfo = true; // プロフィールを初期化する // initChannelSession → プロフィール取得 → 初回チャンネル入室 // の順番で必ず行わないと、ゲストプロフィールが取得できなくなったり(名前がguestのまま・プロフィールが何も描画されない)、 // 入室時にプレゼンターかどうかの判定ができなくなってしまう hsCallCanvasComponentMethod("config_profile", "ProfileMenuViewModel", "InitProfile"); } // ChannelSessionを初期化し再接続する void ReConnectIfJoining(fVoidCallback callback) { VCCChannelData CurrentChannel = m_SubModel.GetCurrentPositionChannel(); if(CurrentChannel === null) { callback(); return; } m_ReConnectChannelId = CurrentChannel.ChannelId; // 一度退出する m_SubModel.ExitPositionChannel(_ExitPositionChannelCallback); g_ReConnectCallback = callback; } void _ExitPositionChannelCallback() { // ChannelSessionを初期化する JsVal options = makeJsObj(); options.AddPropertyByVal("enableHeartbeatInterval", makeJsBoolFrom(false)); heliport.v3.api.channelSessionApi.initChannelSession(_initChannelSessionCallbackForReConnect, options); } void _initChannelSessionCallbackForReConnect(JsVal val) { ReturnType result = hsLoadReturnType(val); if(!result.IsValid()) return; _initChannelSessionCallback(result); // if(!m_ReConnectChannelId.IsEmpty()) { string ChannelId = m_ReConnectChannelId; if(!ChannelId.IsEmpty()) { // 再入室する m_SubModel.EnterPositionChannel(ChannelId); } m_ReConnectChannelId = ""; } if(!g_ReConnectCallback.Empty()) { g_ReConnectCallback(); g_ReConnectCallback.Clear(); } } // 音声チャンネルに再接続する public void ReConnectVoiceChannel() { VCCChannelData currentChannel = GetCurrentChannel(); if (currentChannel === null) { return; } string channelId = currentChannel.ChannelId; if(m_AllowMultiVoiceChannel) { m_SubModel.EnterVoiceChannel(channelId); } else { m_SubModel.EnterPositionChannel(channelId); } } // 座標チャンネル入室処理 public void DoHelJoinPositionChannel(string ChannelId, bool RequiredTickets) { g_AfterJwtChannelId = ChannelId; g_RequiredTickets = RequiredTickets; // authorityJwtのチェック if(g_AfterJwtChannelId.IsEmpty()) { hsSystemOutputLog("[Error] Failed to check AuthorityJwt because ChannelId is empty.\n"); return; } _CheckAuthorityJwt(); } void _HelJoinPosChannel() { // チケットJWT必須ワールドでTicketJwtが無効な時はアウトゲームに飛ばす if(g_RequiredTickets && g_ticketJwt.IsEmpty()) { m_ErrorDialog.ShowFailedJoin("RTC01010105"); ResetJwt(); return; } hel_net_join_pos_channel(_JoinPosChannelCallback, g_AfterJwtChannelId, g_authorityJwt, g_ticketJwt); ResetJwt(); } void _JoinPosChannelCallback(JsVal val) { if(val === null || val.IsNull()) return; if (val.HasProperty("errorCode")) { string errorCode = val.GetProperty("errorCode").GetStr(); if (errorCode == "HEL03000000") { // hel_net_join_pos_channel() が reject された場合 hel_show_fatal_error_dialog(errorCode, val.GetProperty("data").ToString()); return; } } VCCPositionChannel Channel = new VCCPositionChannel(); if(!Channel.Analyse(val, false)) return; if(!Channel.Data.IsValid) { // 入室に失敗 hsSystemOutputLog("[Error] Failed to Join PosChannel - ErrorCode: %s\n" % Channel.Data.ErrorCode); m_ErrorDialog.ShowFailedJoin(Channel.Data.ErrorCode); return; } } void ResetJwt() { // Jwt情報をリセットする g_authorityJwt = ""; g_ticketJwt = ""; g_AfterJwtChannelId = ""; g_RequiredTickets = false; } void _CheckAuthorityJwt() { // authorityJwt、ticketJwtが使えるのはログインユーザーだけである // NOTE: ConfM-APIの仕様上はゲストユーザーでも権限JWT、チケットJWTを使えるが、Heliodor側では現状ログインユーザーのみ使えるという扱いにしている if (hel_net_is_logged_in()) { JsVal args = makeJsObj(); args.AddPropertyByVal("spatiumCode", makeJsStrFrom(hsNetGetSpatiumCode())); args.AddPropertyByVal("worldCode", makeJsStrFrom(hsGetCurrentWorldId())); heliport.v3.api.conferenceManagerApi.fetchHierarchicalAuthorizationJwts(_CheckAuthorityJwtCallback, args); } else { g_authorityJwt = ""; g_ticketJwt = ""; _HelJoinPosChannel(); } } void _CheckAuthorityJwtCallback(JsVal val) { ReturnType result = hsLoadReturnType(val); if (result.IsValid()) { JsVal args = makeJsObj(); args.AddPropertyByVal("jwtDataList", result.data.GetProperty("jwts")); args.AddPropertyByVal("spatiumCode", makeJsStrFrom(hsNetGetSpatiumCode())); args.AddPropertyByVal("worldCode", makeJsStrFrom(hsGetCurrentWorldId())); args.AddPropertyByVal("positionChannelId", makeJsStrFrom(g_AfterJwtChannelId)); JsVal authorizationJwt = hel_net_select_authorization_jwt(args); g_authorityJwt = !authorizationJwt.IsNull() ? authorizationJwt.GetStr() : ""; } else { g_authorityJwt = ""; } // ticketJwtのチェック _CheckTicketJwt(); } void _CheckTicketJwt() { if(g_RequiredTickets) { heliport.v3.api.conferenceManagerApi.fetchTicketJwt(_FetchTicketJwtCallback, hsNetGetSpatiumCode(), hsGetCurrentWorldId(), g_AfterJwtChannelId); } else { // TicketJwtのチェックは不要なのですぐにIHeliNetworkのJoinを実行する g_ticketJwt = ""; _HelJoinPosChannel(); } } void _FetchTicketJwtCallback(JsVal val) { ReturnType result = hsLoadReturnType(val); if(!result.IsValid()) { // TicketJwtの取得に失敗したのでJwtは空として入室処理を実行する g_ticketJwt = ""; _HelJoinPosChannel(); } else { g_ticketJwt = result.data.GetProperty("jwt").GetStr(); _HelJoinPosChannel(); } } public void FetchPositionChannelList(int PagerIndex, fJsValCallback callback, bool UseOffset) { if(m_AllowMultiVoiceChannel) { m_SubModel.FetchPositionChannelList(PagerIndex, callback, UseOffset); } } public void FetchChannelList(int PagerIndex, fJsValCallback callback, bool UseOffset) { if(m_AllowMultiVoiceChannel) { m_SubModel.FetchVoiceChannelList(PagerIndex, callback, UseOffset); } else { m_SubModel.FetchPositionChannelList(PagerIndex, callback, UseOffset); } } public void FetchChannelDetail(fJsValCallback callback) { VCCChannelData currentChannel = GetCurrentChannel(); if (currentChannel === null) { return; } string channelId = currentChannel.ChannelId; if (channelId.IsEmpty()) { return; } if (m_AllowMultiVoiceChannel) { hel_net_get_voice_channel(callback, channelId); } else { hel_net_get_position_channel(callback, channelId); } } public void EnterSpace(string ChannelId) { if(m_AllowMultiVoiceChannel) { m_SubModel.EnterPositionChannel(ChannelId); } } public void EnterChannel(string ChannelId) { if(m_AllowMultiVoiceChannel) { m_SubModel.EnterVoiceChannel(ChannelId); } else { m_SubModel.EnterPositionChannel(ChannelId); } } public void ExitSpace() { if(m_AllowMultiVoiceChannel) { m_SubModel.ExitPositionChannel(_DummyVoidCallback); } } void _DummyVoidCallback() { } public void ExitChannel() { if(m_AllowMultiVoiceChannel) { m_SubModel.ExitVoiceChannel(); } else { m_SubModel.ExitPositionChannel(_DummyVoidCallback); } } // 左上の一覧からの退室用 public void ExitChannelByUserList() { if(m_AllowMultiVoiceChannel) { m_SubModel.ExitVoiceChannelAfterJoinDefaultVoiceChannel(); } else { m_SubModel.ExitPositionChannel(_DummyVoidCallback); } } public void CreateNewChannel(string ChannelName, string SelectedChannelType) { if (m_AllowMultiVoiceChannel) { m_SubModel.CreateNewVoiceChannel(ChannelName, SelectedChannelType); } else { m_SubModel.CreateNewPositionChannel(ChannelName, SelectedChannelType); } } public void UpdateChannel(string ChannelName, string SelectedChannelType) { if(m_AllowMultiVoiceChannel) { m_SubModel.UpdateVoiceChannel(ChannelName, SelectedChannelType); } else { m_SubModel.UpdatePositionChannel(ChannelName, SelectedChannelType); } } public bool IsChannelCreator(string UserCode) { return m_SubModel.IsChannelCreator(UserCode); } public VCCChannelData GetCurrentChannel() { VCCChannelData CurrentChannel = null; if(m_AllowMultiVoiceChannel) { CurrentChannel = m_SubModel.GetCurrentVoiceChannel(); } else { CurrentChannel = m_SubModel.GetCurrentPositionChannel(); } return CurrentChannel; } public bool IsConnected() { return m_SubModel.IsConnected(); } public void UpdateSpaceName(string SpaceName) { if(m_AllowMultiVoiceChannel) { m_SubModel.UpdateSpaceName(SpaceName); } } public void UpdateChannelName(string ChannelName) { m_SubModel.UpdateChannelName(ChannelName); } public void ClearUserTable() { m_VCCModel.ClearUserTable(); } public string GetSelfUserCode() { return m_VCCModel.GetSelfUserCode(); } public void UpdateCreateUserCode(string UserCode) { m_VCCModel.UpdateCreateUserCode(UserCode); } public void InitUserTable(VCCChannelData Channel) { m_VCCModel.InitUserTable(Channel); } public void AddSelfVoiceID(string VoiceID) { m_VCCModel.AddSelfVoiceID(VoiceID); } }