From 863d7322118632b6d1631a4bccbf230765d9c736 Mon Sep 17 00:00:00 2001 From: Jay Malhotra <5047192+SapiensAnatis@users.noreply.github.com> Date: Wed, 9 Aug 2023 18:46:02 +0100 Subject: [PATCH] Plugin retry fixes and refactoring (#377) - Closes #244, a.k.a. [Golden Experience Requiem](https://www.youtube.com/watch?v=r_mfpy2ZyQQ) bug by returning players to the lobby when retrying after at least one player has given up. - Retries when failing a quest are still a bit glitchy -- it's supposed to remove players who voted no and allow players who voted yes to optionally rejoin the room similar to the prompt on a successful clear. Currently any retry where all players are dead will go back to the lobby. #378 raised to track. Plugin refactoring: - Remove HeroParam and other custom actor properties and persist this state in the plugin class, since only the plugin needs to know about these. It's tidier and avoids Photon having to serialize them (and crashing in the case of HeroParam since we didn't register this type). - Use enums for event codes. - Improve logging and add info logs which can provide basic diagnostics, particularly around potential problem areas such as GoToIngameState --- .../Constants/ActorPropertyKeys.cs | 13 - DragaliaAPI.Photon.Plugin/Constants/Event.cs | 45 --- DragaliaAPI.Photon.Plugin/Event.cs | 63 ++++ .../GluonPlugin.Helper.cs | 29 +- DragaliaAPI.Photon.Plugin/GluonPlugin.cs | 319 +++++++++++------- .../Helpers/ActorExtensions.cs | 11 - .../Helpers/CollectionExtensions.cs | 10 + .../Helpers/InfoExtensions.cs | 9 +- .../Models/ActorState.cs | 25 ++ DragaliaAPI.Photon.Plugin/Models/RoomState.cs | 15 + DragaliaAPI.Photon.StateManager/Program.cs | 9 + 11 files changed, 330 insertions(+), 218 deletions(-) delete mode 100644 DragaliaAPI.Photon.Plugin/Constants/Event.cs create mode 100644 DragaliaAPI.Photon.Plugin/Event.cs create mode 100644 DragaliaAPI.Photon.Plugin/Models/ActorState.cs create mode 100644 DragaliaAPI.Photon.Plugin/Models/RoomState.cs diff --git a/DragaliaAPI.Photon.Plugin/Constants/ActorPropertyKeys.cs b/DragaliaAPI.Photon.Plugin/Constants/ActorPropertyKeys.cs index 8a4bad5a9..e3fb7ab2c 100644 --- a/DragaliaAPI.Photon.Plugin/Constants/ActorPropertyKeys.cs +++ b/DragaliaAPI.Photon.Plugin/Constants/ActorPropertyKeys.cs @@ -9,9 +9,6 @@ namespace DragaliaAPI.Photon.Plugin.Constants /// /// Actor property keys. /// - /// - /// The PLUGIN_ prefix denotes properties that are used for plugin logic and not by the game. - /// public class ActorPropertyKeys { public const string PlayerId = "PlayerId"; @@ -21,15 +18,5 @@ public class ActorPropertyKeys public const string UsePartySlot = "UsePartySlot"; public const string GoToIngameState = "GoToIngameState"; - - public const string StartQuest = "PLUGIN_StartQuest"; - - public const string RemovedFromRedis = "PLUGIN_RemovedFromRedis"; - - public const string HeroParam = "PLUGIN_HeroParam"; - - public const string HeroParamCount = "PLUGIN_HeroParamCount"; - - public const string MemberCount = "PLUGIN_MemberCount"; } } diff --git a/DragaliaAPI.Photon.Plugin/Constants/Event.cs b/DragaliaAPI.Photon.Plugin/Constants/Event.cs deleted file mode 100644 index a083ba1e2..000000000 --- a/DragaliaAPI.Photon.Plugin/Constants/Event.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace DragaliaAPI.Photon.Plugin.Constants -{ - public static class Event - { - public static class Codes - { - public const int GameEnd = 0x2; - - public const int Ready = 0x3; - - public const int CharacterData = 0x14; - - public const int StartQuest = 0x15; - - public const int RoomBroken = 0x17; - - public const int GameSucceed = 0x18; - - public const int WillLeave = 0x1e; - - public const int Party = 0x3e; - - public const int ClearQuestRequest = 0x3f; - - public const int ClearQuestResponse = 0x40; - - public const int FailQuestRequest = 0x43; - - public const int FailQuestResponse = 0x44; - - public const int SuccessiveGameTimer = 0x53; - } - - public static class Constants - { - public const int EventDataKey = 245; - } - } -} diff --git a/DragaliaAPI.Photon.Plugin/Event.cs b/DragaliaAPI.Photon.Plugin/Event.cs new file mode 100644 index 000000000..95839a173 --- /dev/null +++ b/DragaliaAPI.Photon.Plugin/Event.cs @@ -0,0 +1,63 @@ +namespace DragaliaAPI.Photon.Plugin +{ + /// + /// Dragalia Lost event codes. + /// + public enum Event : byte + { + /// + /// Event sent by clients when it has finished loading. + /// + Ready = 0x3, + + /// + /// Event sent by the server containing other player's loadout information. + /// + CharacterData = 0x14, + + /// + /// Event sent by the server when players should be allowed to start moving. + /// + StartQuest = 0x15, + + /// + /// Event sent by the server when the room should be destroyed. + /// + RoomBroken = 0x17, + + /// + /// Event sent by clients and the server when re-using a room. + /// + GameSucceed = 0x18, + + /// + /// Event sent by the server containing information about how many units each player will control. + /// + Party = 0x3e, + + /// + /// Event sent by clients when clearing a quest successfully. + /// + ClearQuestRequest = 0x3f, + + /// + /// Event sent by the server after forwarding a event. + /// + ClearQuestResponse = 0x40, + + /// + /// Event sent by clients when failing/retrying a quest. + /// + FailQuestRequest = 0x43, + + /// + /// Event sent by the server after acknowledging a event. + /// + FailQuestResponse = 0x44, + + /// + /// Event sent by clients when their character dies. + /// + Dead = 0x48, + } +} diff --git a/DragaliaAPI.Photon.Plugin/GluonPlugin.Helper.cs b/DragaliaAPI.Photon.Plugin/GluonPlugin.Helper.cs index 958693723..e8a333bef 100644 --- a/DragaliaAPI.Photon.Plugin/GluonPlugin.Helper.cs +++ b/DragaliaAPI.Photon.Plugin/GluonPlugin.Helper.cs @@ -17,35 +17,32 @@ namespace DragaliaAPI.Photon.Plugin /// public partial class GluonPlugin { + private const int EventDataKey = 245; + private const int EventActorNrKey = 254; + /// /// Helper method to raise events. /// /// The event code to raise. /// The event data. /// The actor to target -- if null, all actors will be targeted. - public void RaiseEvent(byte eventCode, object eventData, int? target = null) + public void RaiseEvent(Event eventCode, object eventData, int? target = null) { - byte[] serializedEvent = MessagePackSerializer.Serialize( - eventData, - MessagePackSerializerOptions.Standard.WithCompression( - MessagePackCompression.Lz4Block - ) - ); + byte[] serializedEvent = MessagePackSerializer.Serialize(eventData, MessagePackOptions); Dictionary props = new Dictionary() { - { 245, serializedEvent }, - { 254, 0 } // Server actor number + { EventDataKey, serializedEvent }, + { EventActorNrKey, 0 } }; - this.logger.DebugFormat( - "Raising event 0x{0} with data {1}", - eventCode.ToString("X"), - JsonConvert.SerializeObject(eventData) - ); + this.logger.InfoFormat("Raising event {0} (0x{1})", eventCode, eventCode.ToString("X")); +#if DEBUG + this.logger.DebugFormat("Event data: {0}", JsonConvert.SerializeObject(eventData)); +#endif if (target is null) { - this.BroadcastEvent(eventCode, props); + this.BroadcastEvent((byte)eventCode, props); } else { @@ -53,7 +50,7 @@ public void RaiseEvent(byte eventCode, object eventData, int? target = null) this.PluginHost.BroadcastEvent( new List() { target.Value }, 0, - eventCode, + (byte)eventCode, props, CacheOperations.DoNotCache ); diff --git a/DragaliaAPI.Photon.Plugin/GluonPlugin.cs b/DragaliaAPI.Photon.Plugin/GluonPlugin.cs index 575815656..63fb9dc05 100644 --- a/DragaliaAPI.Photon.Plugin/GluonPlugin.cs +++ b/DragaliaAPI.Photon.Plugin/GluonPlugin.cs @@ -25,7 +25,9 @@ public partial class GluonPlugin : PluginBase private IPluginLogger logger; private PluginConfiguration config; private Random rdm; - private int minGoToIngameState = 0; + + private Dictionary actorState; + private RoomState roomState; private static readonly MessagePackSerializerOptions MessagePackOptions = MessagePackSerializerOptions.Standard.WithCompression(MessagePackCompression.Lz4Block); @@ -42,6 +44,9 @@ out string errorMsg this.config = new PluginConfiguration(config); this.rdm = new Random(); + this.actorState = new Dictionary(4); + this.roomState = new RoomState(); + return base.SetupInstance(host, config, out errorMsg); } @@ -55,7 +60,8 @@ public override void OnCreateGame(ICreateGameCallInfo info) { info.Request.ActorProperties.InitializeViewerId(); - info.Request.GameProperties.Add(GamePropertyKeys.RoomId, rdm.Next(100_0000, 999_9999)); + int roomId = this.GenerateRoomId(); + info.Request.GameProperties.Add(GamePropertyKeys.RoomId, roomId); #if DEBUG this.logger.DebugFormat( @@ -67,6 +73,16 @@ public override void OnCreateGame(ICreateGameCallInfo info) // https://doc.photonengine.com/server/current/plugins/plugins-faq#how_to_get_the_actor_number_in_plugin_callbacks_ // This is only invalid if the room is recreated from an inactive state, which Dragalia doesn't do (hopefully!) const int actorNr = 1; + this.actorState[actorNr] = new ActorState(); + + info.Continue(); + + this.logger.InfoFormat( + "Viewer ID {0} created room {1} with room ID {2}", + info.Request.ActorProperties.GetInt(ActorPropertyKeys.ViewerId), + this.PluginHost.GameId, + roomId + ); this.PostStateManagerRequest( GameCreateEndpoint, @@ -80,17 +96,22 @@ public override void OnCreateGame(ICreateGameCallInfo info) }, info ); - - info.Continue(); } /// /// Photon handler for when a player joins an existing game. /// - /// Event information/ + /// Event information. public override void OnJoin(IJoinGameCallInfo info) { info.Request.ActorProperties.InitializeViewerId(); + this.actorState[info.ActorNr] = new ActorState(); + + this.logger.InfoFormat( + "Viewer ID {0} joined game {1}", + info.Request.ActorProperties.GetInt(ActorPropertyKeys.ViewerId), + this.PluginHost.GameId + ); this.PostStateManagerRequest( GameJoinEndpoint, @@ -132,7 +153,7 @@ public override void OnLeave(ILeaveGameCallInfo info) if (info.ActorNr == 1) { this.RaiseEvent( - Event.Codes.RoomBroken, + Event.RoomBroken, new RoomBroken() { Reason = RoomBroken.RoomBrokenType.HostDisconnected } ); @@ -145,7 +166,7 @@ public override void OnLeave(ILeaveGameCallInfo info) // the actor or certain properties attached to them. if (actor is null) { - this.logger.InfoFormat( + this.logger.WarnFormat( "OnLeave: could not find actor {0} -- GameLeave request aborted", info.ActorNr ); @@ -154,11 +175,16 @@ public override void OnLeave(ILeaveGameCallInfo info) if ( actor.TryGetViewerId(out int viewerId) - && !( - actor.Properties.GetProperty(ActorPropertyKeys.RemovedFromRedis)?.Value is true - ) + && this.actorState.TryGetValue(info.ActorNr, out ActorState actorState) + && !actorState.RemovedFromRedis ) { + this.logger.InfoFormat( + "Viewer ID {0} left game {1}", + viewerId, + this.PluginHost.GameId + ); + this.PostStateManagerRequest( GameLeaveEndpoint, new GameModifyRequest @@ -172,7 +198,7 @@ public override void OnLeave(ILeaveGameCallInfo info) // For some strange reason on completing a quest this appears to be raised twice for each actor. // Prevent duplicate requests by setting a flag. - actor.Properties.SetProperty(ActorPropertyKeys.RemovedFromRedis, true); + actorState.RemovedFromRedis = true; } } @@ -198,7 +224,7 @@ public override void OnCloseGame(ICloseGameCallInfo info) /// Event information. public override void OnRaiseEvent(IRaiseEventCallInfo info) { - base.OnRaiseEvent(info); + info.Continue(); #if DEBUG this.logger.DebugFormat( @@ -213,42 +239,36 @@ public override void OnRaiseEvent(IRaiseEventCallInfo info) ); #endif - switch (info.Request.EvCode) + switch ((Event)info.Request.EvCode) { - case Event.Codes.Ready: + case Event.Ready: this.OnActorReady(info); break; - case Event.Codes.ClearQuestRequest: + case Event.ClearQuestRequest: this.OnClearQuestRequest(info); break; - case Event.Codes.GameSucceed: + case Event.GameSucceed: this.OnGameSucceed(info); break; - case Event.Codes.FailQuestRequest: + case Event.FailQuestRequest: this.OnFailQuestRequest(info); break; + case Event.Dead: + // !!! TODO: How does this behave with AI units? + this.actorState[info.ActorNr].Dead = true; + break; default: break; } } + /// + /// Handler for when the client calls . + /// + /// Event call info. private void OnFailQuestRequest(IRaiseEventCallInfo info) { - this.minGoToIngameState = 0; - - // Clear StartQuest so quests don't start instantly next time. - // Also clear same HeroParam properties that cause serialization issues. - this.PluginHost.SetProperties( - info.ActorNr, - new Hashtable() - { - { ActorPropertyKeys.HeroParam, null }, - { ActorPropertyKeys.HeroParamCount, null }, - { ActorPropertyKeys.StartQuest, false }, - }, - null, - false - ); + this.actorState[info.ActorNr].Ready = false; FailQuestRequest request = info.DeserializeEvent(); @@ -257,8 +277,6 @@ private void OnFailQuestRequest(IRaiseEventCallInfo info) request.FailType.ToString() ); - // I assumed this would need to be POSTed to /dungeon/fail, but the event doesn't contain - // a request body with dungeon_key... so the API server couldn't really do anything. FailQuestResponse response = new FailQuestResponse() { ResultType = @@ -267,57 +285,118 @@ private void OnFailQuestRequest(IRaiseEventCallInfo info) : FailQuestResponse.ResultTypes.Clear }; - this.RaiseEvent(Event.Codes.FailQuestResponse, response); + this.RaiseEvent(Event.FailQuestResponse, response, info.ActorNr); + + if ( + this.PluginHost.GameActors.Count < this.roomState.StartActorCount + || this.actorState.All(x => x.Value.Dead) + ) + { + // Return to lobby + this.logger.DebugFormat("FailQuestRequest: returning to lobby"); + this.actorState[info.ActorNr] = new ActorState(); + + this.PluginHost.SetProperties( + 0, + new Hashtable() + { + { GamePropertyKeys.GoToIngameInfo, null }, + // { GamePropertyKeys.RoomId, -1 } TODO: Show 'play again with the same players?' screen on failed retry after wipe + }, + null, + true + ); - // TODO: Retrying a quest without a full team should kick you back to the lobby. + this.SetRoomVisibility(info, true); + } + + this.roomState = new RoomState(); } + /// + /// Handler for when a client calls . + /// + /// Info from . private void OnGameSucceed(IRaiseEventCallInfo info) { + this.logger.InfoFormat("Received GameSucceed from actor {0}", info.ActorNr); + if (info.ActorNr == 1) { - this.RaiseEvent(Event.Codes.GameSucceed, new { }); + this.roomState = new RoomState(); + this.RaiseEvent(Event.GameSucceed, new { }); this.SetRoomId(info, this.GenerateRoomId()); this.SetRoomVisibility(info, true); } } /// - /// Photon handler for when an actor sets properties. + /// Photon handler for when a client requests to set a property. /// /// Event information. - public override void OnSetProperties(ISetPropertiesCallInfo info) + public override void BeforeSetProperties(IBeforeSetPropertiesCallInfo info) { - base.OnSetProperties(info); - -#if DEBUG - this.logger.DebugFormat("Actor {0} set properties", info.ActorNr); - this.logger.Debug(JsonConvert.SerializeObject(info.Request.Properties)); -#endif - - if (info.Request.Properties.ContainsKey(ActorPropertyKeys.GoToIngameState)) + if ( + info.Request.Properties.TryGetValue( + ActorPropertyKeys.GoToIngameState, + out object objValue + ) && objValue is int value + ) { // Wait for everyone to reach a particular GoToIngameState value before doing anything. // But let the host set GoToIngameState = 1 unilaterally to signal the game start process. - int value = info.Request.Properties.GetInt(ActorPropertyKeys.GoToIngameState); - int minValue = this.PluginHost.GameActors - .Select(x => x.Properties.GetInt(ActorPropertyKeys.GoToIngameState)) + .Where(x => x.ActorNr != info.ActorNr) // Exclude the value which we are in the BeforeSet handler for + .Select(x => x.Properties.GetIntOrDefault(ActorPropertyKeys.GoToIngameState)) + .Concat(new[] { value }) // Fun fact: Enumerable.Append() was added in .NET 4.7.1 .Min(); - if (minValue > this.minGoToIngameState) + this.logger.InfoFormat( + "Received GoToIngameState {0} from actor {1}", + value, + info.ActorNr + ); + +#if DEBUG + this.logger.DebugFormat( + "Calculated minimum value: {0}, instance minimum value {1}", + minValue, + this.roomState.MinGoToIngameState + ); +#endif + + if (minValue > this.roomState.MinGoToIngameState) { - this.minGoToIngameState = minValue; + this.roomState.MinGoToIngameState = minValue; this.OnSetGoToIngameState(info); } else if (value == 1 && info.ActorNr == 1) { - this.minGoToIngameState = value; + this.roomState.MinGoToIngameState = value; this.OnSetGoToIngameState(info); } } + if (!info.IsProcessed) + { + info.Continue(); + } + } + + /// + /// Photon handler for when an actor sets properties. + /// + /// Event information. + public override void OnSetProperties(ISetPropertiesCallInfo info) + { + base.OnSetProperties(info); + +#if DEBUG + this.logger.DebugFormat("Actor {0} set properties", info.ActorNr); + this.logger.Debug(JsonConvert.SerializeObject(info.Request.Properties)); +#endif + if (info.Request.Properties.ContainsKey(GamePropertyKeys.EntryConditions)) this.OnSetEntryConditions(info); @@ -335,22 +414,16 @@ public override void OnSetProperties(ISetPropertiesCallInfo info) /// Info from . private void OnActorReady(IRaiseEventCallInfo info) { - this.logger.DebugFormat("Received Ready event from actor {0}", info.ActorNr); - - this.PluginHost.SetProperties( - info.ActorNr, - new Hashtable { { ActorPropertyKeys.StartQuest, true } }, - null, - true - ); + this.logger.InfoFormat("Received Ready event from actor {0}", info.ActorNr); + this.actorState[info.ActorNr].Ready = true; - if (this.PluginHost.GameActors.All(x => x.IsReady())) + if (this.actorState.All(x => x.Value.Ready)) { - this.logger.DebugFormat( - "All clients were ready, raising {0}", - Event.Codes.StartQuest - ); - this.RaiseEvent(Event.Codes.StartQuest, new Dictionary { }); + this.logger.Info("All clients were ready, raising StartQuest"); + + this.RaiseEvent(Event.StartQuest, new Dictionary { }); + + this.roomState.StartActorCount = this.PluginHost.GameActors.Count; } } @@ -378,7 +451,7 @@ private void OnSetMatchingType(ISetPropertiesCallInfo info) /// /// Custom handler for when an actor sets the RoomEntryCondition property (i.e. allowed weapon/element types). /// - /// + /// Info from . private void OnSetEntryConditions(ISetPropertiesCallInfo info) { EntryConditions newEntryConditions = DtoHelpers.CreateEntryConditions( @@ -406,38 +479,44 @@ private void OnSetEntryConditions(ISetPropertiesCallInfo info) /// /// Represents various stages of loading into a quest, during which events/properties need to be raised/set. /// - /// Info from . - private void OnSetGoToIngameState(ISetPropertiesCallInfo info) + /// Info from . + private void OnSetGoToIngameState(IBeforeSetPropertiesCallInfo info) { - switch (this.minGoToIngameState) + this.logger.InfoFormat( + "OnSetGoToIngameState: updating with value {0}", + this.roomState.MinGoToIngameState + ); + + switch (this.roomState.MinGoToIngameState) { case 1: - this.SetGoToIngameInfo(info); + this.SetGoToIngameInfo(); this.SetRoomVisibility(info, false); break; case 2: this.RequestHeroParam(info); break; case 3: - this.RaisePartyEvent(info); - this.RaiseCharacterDataEvent(info); + this.RaisePartyEvent(); + this.RaiseCharacterDataEvent(); break; default: break; } } - private void RaiseCharacterDataEvent(ISetPropertiesCallInfo info) + /// + /// Raise using cached . + /// + private void RaiseCharacterDataEvent() { foreach (IActor actor in this.PluginHost.GameActors) { - IEnumerable> heroParamsList = - (IEnumerable>) - actor.Properties.GetProperty(ActorPropertyKeys.HeroParam).Value; + ActorState actorState = this.actorState[actor.ActorNr]; - int memberCount = actor.Properties.GetInt(ActorPropertyKeys.MemberCount); - - foreach (IEnumerable heroParams in heroParamsList) + foreach ( + IEnumerable heroParams in actorState.HeroParamData.HeroParamLists + ) { CharacterData evt = new CharacterData() { @@ -452,10 +531,10 @@ private void RaiseCharacterDataEvent(ISetPropertiesCallInfo info) } ) .ToArray(), - heroParams = heroParams.Take(memberCount).ToArray() + heroParams = heroParams.Take(actorState.MemberCount).ToArray() }; - this.RaiseEvent(Event.Codes.CharacterData, evt); + this.RaiseEvent(Event.CharacterData, evt); } } } @@ -463,8 +542,7 @@ private void RaiseCharacterDataEvent(ISetPropertiesCallInfo info) /// /// Sets the GoToIngameInfo room property by gathering data from connected actors. /// - /// Info from . - private void SetGoToIngameInfo(ISetPropertiesCallInfo info) + private void SetGoToIngameInfo() { IEnumerable actorData = this.PluginHost.GameActors.Select( x => new ActorData() { ActorId = x.ActorNr, ViewerId = (ulong)x.GetViewerId() } @@ -487,10 +565,10 @@ private void SetGoToIngameInfo(ISetPropertiesCallInfo info) } /// - /// Raises the CharacterData event by making requests to the main API server for party information. + /// Makes an outgoing request for for each player in the room. /// /// Info from . - private void RequestHeroParam(ISetPropertiesCallInfo info) + private void RequestHeroParam(IBeforeSetPropertiesCallInfo info) { IEnumerable heroParamRequest = this.PluginHost.GameActors.Select( x => @@ -509,7 +587,7 @@ private void RequestHeroParam(ISetPropertiesCallInfo info) Url = requestUri.AbsoluteUri, ContentType = "application/json", Callback = HeroParamRequestCallback, - Async = true, + Async = false, Accept = "application/json", DataStream = new MemoryStream( Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(heroParamRequest)) @@ -521,7 +599,7 @@ private void RequestHeroParam(ISetPropertiesCallInfo info) } /// - /// HTTP request callback for the HeroParam request sent in . + /// HTTP request callback for the HeroParam request sent in . /// /// The HTTP response. /// The arguments passed from the calling function. @@ -534,35 +612,19 @@ private void HeroParamRequestCallback(IHttpResponse response, object userState) ); foreach (HeroParamData data in responseObject) - { - this.PluginHost.SetProperties( - data.ActorNr, - new Hashtable() - { - { ActorPropertyKeys.HeroParam, data.HeroParamLists }, - { ActorPropertyKeys.HeroParamCount, data.HeroParamLists.First().Count() } - }, - null, - false - ); - } + this.actorState[data.ActorNr].HeroParamData = data; } /// - /// Raises the Party event containing information about how many characters each player owns. + /// Raises the event. /// /// Info from . - private void RaisePartyEvent(ISetPropertiesCallInfo info) + private void RaisePartyEvent() { Dictionary memberCountTable = this.GetMemberCountTable(); foreach (IActor actor in this.PluginHost.GameActors) - { - actor.Properties.Set( - ActorPropertyKeys.MemberCount, - memberCountTable[actor.ActorNr] - ); - } + this.actorState[actor.ActorNr].MemberCount = memberCountTable[actor.ActorNr]; PartyEvent evt = new PartyEvent() { @@ -570,7 +632,7 @@ private void RaisePartyEvent(ISetPropertiesCallInfo info) ReBattleCount = this.config.ReplayTimeoutSeconds }; - this.RaiseEvent(Event.Codes.Party, evt); + this.RaiseEvent(Event.Party, evt); } /// @@ -624,10 +686,12 @@ private void SetRoomVisibility(ICallInfo info, bool visible) ); } + /// + /// Handler for when the client raises . + /// + /// Event call info. private void OnClearQuestRequest(IRaiseEventCallInfo info) { - this.minGoToIngameState = 0; - // These properties must be set for the client to successfully rejoin the room. this.PluginHost.SetProperties( 0, @@ -640,19 +704,7 @@ private void OnClearQuestRequest(IRaiseEventCallInfo info) true ); - // Clear HeroParam or else Photon complains about not being able to serialize it - // if a player joins the next room. - this.PluginHost.SetProperties( - info.ActorNr, - new Hashtable() - { - { ActorPropertyKeys.HeroParam, null }, - { ActorPropertyKeys.HeroParamCount, null }, - { ActorPropertyKeys.StartQuest, null } - }, - null, - false - ); + this.actorState[info.ActorNr] = new ActorState(); ClearQuestRequest evt = info.DeserializeEvent(); @@ -665,6 +717,11 @@ private void OnClearQuestRequest(IRaiseEventCallInfo info) ); } + /// + /// Callback for HTTP request sent in . + /// + /// The HTTP response. + /// The user state. private void ClearQuestRequestCallback(IHttpResponse response, object userState) { this.LogIfFailedCallback(response, userState); @@ -672,7 +729,7 @@ private void ClearQuestRequestCallback(IHttpResponse response, object userState) HttpRequestUserState typedUserState = (HttpRequestUserState)userState; this.RaiseEvent( - Event.Codes.ClearQuestResponse, + Event.ClearQuestResponse, new ClearQuestResponse() { RecordMultiResponse = response.ResponseData }, typedUserState.RequestActorNr ); @@ -693,7 +750,7 @@ private Dictionary GetMemberCountTable() // Everyone uses all of their units in a raid return this.PluginHost.GameActors.ToDictionary( x => x.ActorNr, - x => x.Properties.GetInt(ActorPropertyKeys.HeroParamCount) + x => this.actorState[x.ActorNr].HeroParamCount ); } @@ -702,12 +759,18 @@ private Dictionary GetMemberCountTable() x => new ValueTuple( x.ActorNr, - x.Properties.GetInt(ActorPropertyKeys.HeroParamCount) + this.actorState[x.ActorNr].HeroParamCount ) ) ); + ; } + /// + /// Static unit-testable method to build the member count table. + /// + /// List of actors and how many hero params they have. + /// The member count table. public static Dictionary BuildMemberCountTable( IEnumerable<(int ActorNr, int HeroParamCount)> actorData ) diff --git a/DragaliaAPI.Photon.Plugin/Helpers/ActorExtensions.cs b/DragaliaAPI.Photon.Plugin/Helpers/ActorExtensions.cs index c3f657f1d..3ab5b2904 100644 --- a/DragaliaAPI.Photon.Plugin/Helpers/ActorExtensions.cs +++ b/DragaliaAPI.Photon.Plugin/Helpers/ActorExtensions.cs @@ -7,17 +7,6 @@ namespace DragaliaAPI.Photon.Plugin.Helpers { public static class ActorExtensions { - public static bool IsHost(this IActor actor) - { - return actor.ActorNr == 1; - } - - public static bool IsReady(this IActor actor) - { - return actor.Properties.TryGetBool(ActorPropertyKeys.StartQuest, out bool ready) - && ready; - } - public static bool TryGetViewerId(this IActor actor, out int viewerId) { return actor.Properties.TryGetInt(ActorPropertyKeys.PlayerId, out viewerId); diff --git a/DragaliaAPI.Photon.Plugin/Helpers/CollectionExtensions.cs b/DragaliaAPI.Photon.Plugin/Helpers/CollectionExtensions.cs index c0bdc3cf6..58ce2dfa1 100644 --- a/DragaliaAPI.Photon.Plugin/Helpers/CollectionExtensions.cs +++ b/DragaliaAPI.Photon.Plugin/Helpers/CollectionExtensions.cs @@ -119,6 +119,16 @@ public static int GetInt(this PropertyBag properties, string key) return value; } + public static int GetIntOrDefault(this PropertyBag properties, string key) + { + if (!properties.TryGetInt(key, out int value)) + { + return 0; + } + + return value; + } + public static bool TryGetBool( this PropertyBag properties, string key, diff --git a/DragaliaAPI.Photon.Plugin/Helpers/InfoExtensions.cs b/DragaliaAPI.Photon.Plugin/Helpers/InfoExtensions.cs index e8784c954..38a78958d 100644 --- a/DragaliaAPI.Photon.Plugin/Helpers/InfoExtensions.cs +++ b/DragaliaAPI.Photon.Plugin/Helpers/InfoExtensions.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using DragaliaAPI.Photon.Plugin.Constants; using DragaliaAPI.Photon.Plugin.Models.Events; using MessagePack; using Photon.Hive.Plugin; @@ -12,14 +11,14 @@ namespace DragaliaAPI.Photon.Plugin.Helpers { public static class InfoExtensions { + private const int EventDataKey = 245; + public static TEvent DeserializeEvent(this IRaiseEventCallInfo info) where TEvent : EventBase { if ( - !info.Request.Parameters.TryGetValue( - Event.Constants.EventDataKey, - out object eventDataObj - ) || !(eventDataObj is byte[] blob) + !info.Request.Parameters.TryGetValue(EventDataKey, out object eventDataObj) + || !(eventDataObj is byte[] blob) ) { throw new ArgumentException( diff --git a/DragaliaAPI.Photon.Plugin/Models/ActorState.cs b/DragaliaAPI.Photon.Plugin/Models/ActorState.cs new file mode 100644 index 000000000..b1482152e --- /dev/null +++ b/DragaliaAPI.Photon.Plugin/Models/ActorState.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DragaliaAPI.Photon.Shared.Models; + +namespace DragaliaAPI.Photon.Plugin.Models +{ + internal class ActorState + { + public HeroParamData HeroParamData { get; set; } + + public int HeroParamCount => + this.HeroParamData is null ? 0 : this.HeroParamData.HeroParamLists.First().Count(); + + public int MemberCount { get; set; } + + public bool Dead { get; set; } + + public bool Ready { get; set; } + + public bool RemovedFromRedis { get; set; } + } +} diff --git a/DragaliaAPI.Photon.Plugin/Models/RoomState.cs b/DragaliaAPI.Photon.Plugin/Models/RoomState.cs new file mode 100644 index 000000000..9eb91fcce --- /dev/null +++ b/DragaliaAPI.Photon.Plugin/Models/RoomState.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DragaliaAPI.Photon.Plugin.Models +{ + internal class RoomState + { + public int MinGoToIngameState { get; set; } + + public int StartActorCount { get; set; } + } +} diff --git a/DragaliaAPI.Photon.StateManager/Program.cs b/DragaliaAPI.Photon.StateManager/Program.cs index 59f5a3555..55b871605 100644 --- a/DragaliaAPI.Photon.StateManager/Program.cs +++ b/DragaliaAPI.Photon.StateManager/Program.cs @@ -84,6 +84,15 @@ RedisIndexInfo? info = await provider.Connection.GetIndexInfoAsync(typeof(RedisGame)); Log.Logger.Information("Index created: {created}", created); Log.Logger.Information("Index info: {@info}", info); + + if (builder.Environment.IsDevelopment()) + { + Log.Logger.Information("App is in development mode -- clearing all pre-existing games"); + + await provider + .RedisCollection() + .DeleteAsync(provider.RedisCollection()); + } } WebApplication app = builder.Build();