using Gs2.Unity.Util; using Gs2.Unity.Core; using System; using Cysharp.Threading.Tasks; using System.Collections.Generic; using Gs2.Unity.Gs2Inventory.Model; using Gs2.Unity.Gs2Formation.Model; using UnityEngine; public sealed class AdventureCharacters : MonoBehaviour { [SerializeField] private AdventureLogin login; private Gs2Domain Client => login.Gs2; private IGameSession Session => login == null ? null : login.Session; private readonly List owned = new List(); private readonly string[] party = new string[3]; private int selected; private bool busy; private Func pendingOperation; private bool ready; private bool destroyed; private Action unsubscribe; private Action stopExperience; private Action stopParty; private string message = "読み込み中です"; private string experienceText = ""; private Vector2 scroll; private const string InventoryNamespace = "adventure-character"; private const string ExperienceNamespace = "adventure-character-experience"; private const string FormationNamespace = "adventure-character-formation"; private const string GachaNamespace = "adventure-character-gacha"; private const string KeyId = "grn:gs2:{region}:{ownerId}:key:default:key:default"; private void OnGUI() { if (Session == null) return; GUILayout.BeginArea(new Rect(700, 20, 650, 650), GUI.skin.box); scroll = GUILayout.BeginScrollView(scroll); GUILayout.Label("キャラクター"); GUILayout.Label(message); GUILayout.Label(experienceText); if (!ready) { GUILayout.EndScrollView(); GUILayout.EndArea(); return; } GUI.enabled = !busy && pendingOperation == null; if (GUILayout.Button("20 コインでガチャを引く")) RunWrite(Draw).Forget(); GUI.enabled = !busy && pendingOperation == null; for (int i = 0; i < owned.Count; i++) if (GUILayout.Toggle(selected == i, owned[i].ItemName + " / " + owned[i].Name) && selected != i) { stopExperience?.Invoke(); stopExperience = null; selected = i; experienceText = ""; } GUI.enabled = !busy && pendingOperation == null && owned.Count > 0; if (GUILayout.Button("選んだ個体の経験値を読む")) Run(() => ReadExperience()).Forget(); GUI.enabled = !busy && owned.Count > 0 && pendingOperation == null; if (GUILayout.Button("10 コインで経験値を 10 増やす")) { var itemSetId = owned[selected].ItemSetId; RunWrite(id => Train(itemSetId, id)).Forget(); } GUI.enabled = !busy && pendingOperation == null; if (GUILayout.Button("編成を読み込む")) Run(ReadParty).Forget(); GUI.enabled = !busy && pendingOperation == null && owned.Count > 0 && stopParty != null; for (int i = 0; i < 3; i++) { if (GUILayout.Button("スロット " + (i + 1) + " に選んだ個体を入れる")) party[i] = owned[selected].ItemSetId; GUILayout.Label(party[i] ?? "空き"); } GUI.enabled = !busy && pendingOperation == null && stopParty != null; if (GUILayout.Button("編成を保存する")) Run(() => SaveParty()).Forget(); GUI.enabled = !busy && pendingOperation != null; if (GUILayout.Button("同じ操作を再試行")) RetryWrite().Forget(); GUI.enabled = true; GUILayout.EndScrollView(); GUILayout.EndArea(); } private async UniTask Run(System.Func action) { if (busy || pendingOperation != null) return; busy = true; try { await action(); } catch (System.Exception error) { ShowError(error); } finally { busy = false; } } private void Start() => Load().Forget(); private void OnDestroy() { destroyed = true; unsubscribe?.Invoke(); stopExperience?.Invoke(); stopParty?.Invoke(); } private async UniTask Load() { try { await UniTask.WaitUntil(() => Session != null, cancellationToken: this.GetCancellationTokenOnDestroy()); var inventory = Client.Inventory.Namespace(InventoryNamespace).Me(Session).Inventory("Character"); await foreach (var item in inventory.ItemSetsAsync()) if (item != null) owned.Add(item); if (destroyed) return; var itemsId = inventory.SubscribeItemSets(UpdateOwned); unsubscribe = () => inventory.UnsubscribeItemSets(itemsId); ready = true; message = ""; } catch (OperationCanceledException) { } catch (Exception error) { ShowError(error); } } private void UpdateOwned(EzItemSet[] items) { var selectedId = owned.Count > 0 ? owned[selected].ItemSetId : null; owned.Clear(); owned.AddRange(items); selected = Math.Max(0, owned.FindIndex(item => item.ItemSetId == selectedId)); } private async UniTask Draw(string requestId) { var transaction = await Client.Showcase.Namespace(GachaNamespace).Me(Session) .Showcase("Showcase").DisplayItem("normal").BuyAsync( 1, speculativeExecute: false, duplicationAvoider: requestId); if (transaction != null) await transaction.WaitAsync(all: true); } private async UniTask Train(string itemSetId, string requestId) { var transaction = await Client.Exchange.Namespace("adventure-character-training").Me(Session).Exchange() .ExchangeAsync("train", 1, config: new[] { new Gs2.Unity.Gs2Exchange.Model.EzConfig { Key = "propertyId", Value = itemSetId + ":level" } }, speculativeExecute: false, duplicationAvoider: requestId); if (transaction != null) await transaction.WaitAsync(all: true); } private async UniTask ReadExperience() { stopExperience?.Invoke(); stopExperience = null; var domain = Client.Experience.Namespace(ExperienceNamespace).Me(Session) .Status("Experience", owned[selected].ItemSetId + ":level"); var id = await domain.SubscribeWithInitialCallAsync(value => experienceText = value == null ? "経験値: 0" : "経験値: " + value.ExperienceValue + " / レベル: " + value.RankValue); stopExperience = () => domain.Unsubscribe(id); if (destroyed) stopExperience(); } private async UniTask SaveParty() { var seen = new HashSet(); var slots = new List(); for (int i = 0; i < 3; i++) { if (string.IsNullOrEmpty(party[i])) continue; if (!seen.Add(party[i])) { message = "同じ個体を二つのスロットには置けません"; return; } var item = owned.Find(x => x.ItemSetId == party[i]); if (item == null) { message = "所持一覧を読み直して、配置し直してください"; return; } var signature = await Client.Inventory.Namespace(InventoryNamespace).Me(Session) .Inventory("Character").ItemSet(item.ItemName, item.Name).GetItemWithSignatureAsync(KeyId); slots.Add(new EzSlotWithSignature { Name = "slot" + (i + 1), PropertyType = "gs2_inventory", Body = signature.Body, Signature = signature.Signature }); } var request = await Client.Formation.Namespace(FormationNamespace).Me(Session) .Mold("CharacterFormation").Form(0).SetFormAsync(slots.ToArray(), KeyId); message = "編成を保存しました"; } private async UniTask ReadParty() { stopParty?.Invoke(); stopParty = null; var form = Client.Formation.Namespace(FormationNamespace).Me(Session).Mold("CharacterFormation").Form(0); var id = await form.SubscribeWithInitialCallAsync(UpdateParty); stopParty = () => form.Unsubscribe(id); if (destroyed) stopParty(); } private void UpdateParty(EzForm form) { Array.Clear(party, 0, party.Length); if (form?.Slots != null) foreach (var slot in form.Slots) for (int i = 0; i < 3; i++) if (slot.Name == "slot" + (i + 1)) party[i] = slot.PropertyId; } private void ShowError(Exception error) { message = "操作できませんでした: " + error.GetType().Name; } private async UniTask RunWrite(Func operation) { if (busy || pendingOperation != null || Session == null) return; var requestId = Guid.NewGuid().ToString(); pendingOperation = () => operation(requestId); await RetryWrite(); } private async UniTask RetryWrite() { if (busy || pendingOperation == null) return; busy = true; message = "処理中です"; try { await pendingOperation(); pendingOperation = null; message = "完了しました"; } catch (Exception error) when ( error is Gs2.Core.Exception.BadRequestException || error is Gs2.Gs2Money2.Exception.InsufficientException || error is Gs2.Gs2Inventory.Exception.InsufficientException || error is Gs2.Gs2Stamina.Exception.InsufficientException) { pendingOperation = null; message = "操作できませんでした: " + error.Message; } catch (Exception error) { message = "通信を確認して、同じ操作を再試行してください: " + error.GetType().Name; } finally { busy = false; } } }