Skip to content

Commit 2539af2

Browse files
committed
CDH UI improvements
1 parent bbdaa8c commit 2539af2

10 files changed

Lines changed: 159 additions & 86 deletions

.github/workflows/build.yml

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,4 @@ jobs:
3131
uses: actions/upload-artifact@v4
3232
with:
3333
path: |
34-
./Snowcloak/bin/*
35-
36-
test:
37-
runs-on: ubuntu-latest
38-
steps:
39-
- uses: actions/checkout@v4
40-
with:
41-
submodules: true
42-
- name: Setup .NET
43-
uses: actions/setup-dotnet@v4
44-
with:
45-
dotnet-version: '10.x'
46-
- name: Unit tests (Core + Infrastructure)
47-
run: dotnet test Snowcloak.Tests/Snowcloak.Tests.csproj --configuration Release --nologo
34+
./Snowcloak/bin/*

Snowcloak/Interop/Ipc/IpcCallerBrio.cs

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -88,29 +88,38 @@ public async Task<bool> DespawnActorAsync(nint address)
8888
if (!APIAvailable) return false;
8989
var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false);
9090
if (gameObject == null) return false;
91-
_logger.LogDebug("Despawning Brio Actor {actor}", gameObject.Name.TextValue);
92-
return await Service.RunOnFrameworkAsync(() => _despawnActor.Invoke(gameObject)).ConfigureAwait(false);
91+
return await Service.RunOnFrameworkAsync(() =>
92+
{
93+
if (!_dalamudUtilService.IsGameObjectPresent(address)) return false;
94+
_logger.LogDebug("Despawning Brio Actor {actor}", gameObject.Name.TextValue);
95+
return _despawnActor.Invoke(gameObject);
96+
}).ConfigureAwait(false);
9397
}
9498

9599
public async Task<bool> ApplyTransformAsync(nint address, WorldData data)
96100
{
97101
if (!APIAvailable) return false;
98102
var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false);
99103
if (gameObject == null) return false;
100-
_logger.LogDebug("Applying Transform to Actor {actor}", gameObject.Name.TextValue);
101104

102-
return await Service.RunOnFrameworkAsync(() => _setModelTransform.Invoke(gameObject,
103-
new Vector3(data.PositionX, data.PositionY, data.PositionZ),
104-
new Quaternion(data.RotationX, data.RotationY, data.RotationZ, data.RotationW),
105-
new Vector3(data.ScaleX, data.ScaleY, data.ScaleZ), false)).ConfigureAwait(false);
105+
return await Service.RunOnFrameworkAsync(() =>
106+
{
107+
if (!_dalamudUtilService.IsGameObjectPresent(address)) return false;
108+
_logger.LogDebug("Applying Transform to Actor {actor}", gameObject.Name.TextValue);
109+
return _setModelTransform.Invoke(gameObject,
110+
new Vector3(data.PositionX, data.PositionY, data.PositionZ),
111+
new Quaternion(data.RotationX, data.RotationY, data.RotationZ, data.RotationW),
112+
new Vector3(data.ScaleX, data.ScaleY, data.ScaleZ), false);
113+
}).ConfigureAwait(false);
106114
}
107115

108116
public async Task<WorldData> GetTransformAsync(nint address)
109117
{
110118
if (!APIAvailable) return default;
111119
var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false);
112120
if (gameObject == null) return default;
113-
var data = await Service.RunOnFrameworkAsync(() => _getModelTransform.Invoke(gameObject)).ConfigureAwait(false);
121+
var data = await Service.RunOnFrameworkAsync(() =>
122+
_dalamudUtilService.IsGameObjectPresent(address) ? _getModelTransform.Invoke(gameObject) : default).ConfigureAwait(false);
114123
if (data.Item1 == null || data.Item2 == null || data.Item3 == null) return default;
115124

116125
return new WorldData()
@@ -133,20 +142,20 @@ public async Task<WorldData> GetTransformAsync(nint address)
133142
if (!APIAvailable) return null;
134143
var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false);
135144
if (gameObject == null) return null;
136-
_logger.LogDebug("Getting Pose from Actor {actor}", gameObject.Name.TextValue);
137145

138-
return await Service.RunOnFrameworkAsync(() => _getPoseAsJson.Invoke(gameObject)).ConfigureAwait(false);
146+
return await Service.RunOnFrameworkAsync(() =>
147+
_dalamudUtilService.IsGameObjectPresent(address) ? _getPoseAsJson.Invoke(gameObject) : null).ConfigureAwait(false);
139148
}
140149

141150
public async Task<bool> SetPoseAsync(nint address, string pose)
142151
{
143152
if (!APIAvailable) return false;
144153
var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false);
145154
if (gameObject == null) return false;
146-
_logger.LogDebug("Setting Pose to Actor {actor}", gameObject.Name.TextValue);
147155

148156
var applicablePose = JsonNode.Parse(pose)!;
149-
var currentPose = await Service.RunOnFrameworkAsync(() => _getPoseAsJson.Invoke(gameObject)).ConfigureAwait(false);
157+
var currentPose = await Service.RunOnFrameworkAsync(() =>
158+
_dalamudUtilService.IsGameObjectPresent(address) ? _getPoseAsJson.Invoke(gameObject) : null).ConfigureAwait(false);
150159
if (string.IsNullOrWhiteSpace(currentPose))
151160
{
152161
return false;
@@ -160,13 +169,15 @@ public async Task<bool> SetPoseAsync(nint address, string pose)
160169
}
161170

162171
applicablePose["ModelDifference"] = JsonNode.Parse(modelDifference.ToJsonString());
163-
164-
await Service.RunOnFrameworkAsync(() =>
172+
173+
return await Service.RunOnFrameworkAsync(() =>
165174
{
175+
if (!_dalamudUtilService.IsGameObjectPresent(address)) return false;
176+
_logger.LogDebug("Setting Pose to Actor {actor}", gameObject.Name.TextValue);
166177
_freezeActor.Invoke(gameObject);
167178
_freezePhysics.Invoke();
179+
return _loadPoseFromJson.Invoke(gameObject, applicablePose.ToJsonString(), false);
168180
}).ConfigureAwait(false);
169-
return await Service.RunOnFrameworkAsync(() => _loadPoseFromJson.Invoke(gameObject, applicablePose.ToJsonString(), false)).ConfigureAwait(false);
170181
}
171182

172183
public void Dispose()

Snowcloak/Services/CharaData/CharaDataApplicationService.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,9 @@ private async Task RevertExistingAsync(GameObjectHandler tempHandler)
331331
LogRevertingChara(Logger, tempHandler.Name);
332332
bool reverted = await _characterHandler.RevertHandledChara(tempHandler.Name).ConfigureAwait(false);
333333
if (reverted)
334-
await Task.Delay(TimeSpan.FromSeconds(3)).ConfigureAwait(false);
334+
{
335+
await ObjectTableCache.WaitWhileCharacterIsDrawing(Logger, tempHandler, Guid.NewGuid()).ConfigureAwait(false);
336+
}
335337
}
336338

337339
private async Task<Guid?> ApplyAppearanceAsync(Guid applicationId, GameObjectHandler tempHandler, CharaDataMetaInfoExtendedDto metaInfo,

Snowcloak/Services/CharaData/GposeLobbySession.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,12 @@ public async Task SpawnAndApplyData(GposeLobbyUserData member)
174174

175175
private async Task LeaveGPoseLobbyAsync()
176176
{
177-
var left = await _apiController.GposeLobbyLeave().ConfigureAwait(false);
178-
if (!left) return;
177+
var lobbyId = CurrentGPoseLobbyId;
178+
179+
await _apiController.GposeLobbyLeave().ConfigureAwait(false);
179180

180-
if (!string.IsNullOrEmpty(CurrentGPoseLobbyId))
181-
LastGPoseLobbyId = CurrentGPoseLobbyId;
181+
if (!string.IsNullOrEmpty(lobbyId))
182+
LastGPoseLobbyId = lobbyId;
182183

183184
ClearLobby(revertCharas: true);
184185
}

Snowcloak/UI/CharaDataHubDataApplicationTab.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ private void DrawTargetStatus()
144144
if (!_ctx.HasValidGposeTarget)
145145
{
146146
ImGuiHelpers.ScaledDummy(3);
147-
ElezenImgui.DrawGroupedCenteredColorText("Applying data is only available in GPose with a valid selected GPose target.", ImGuiColors.DalamudYellow, 350);
147+
CharaDataHubCard.Warning("Applying data is only available in GPose with a valid selected GPose target.");
148148
}
149149
}
150150

@@ -172,7 +172,7 @@ private void DrawFavoritesTab()
172172

173173
if (_stateConfigService.Current.FavoriteCodes.Count == 0)
174174
{
175-
ElezenImgui.ColouredWrappedText("You have no favorites added. Add Favorites through the other tabs before you can use this tab.", ImGuiColors.DalamudYellow);
175+
CharaDataHubCard.Info("You have no favorites yet. Add favorites from the other tabs to use this one.");
176176
}
177177
}
178178

@@ -631,10 +631,8 @@ private void DrawLoadedMcdf()
631631

632632
private static void DrawMcdfFailure()
633633
{
634-
ElezenImgui.ColouredWrappedText("Failure to read MCDF file. MCDF file is possibly corrupt. Re-export the MCDF file and try again.",
635-
ImGuiColors.DalamudRed);
636-
ElezenImgui.ColouredWrappedText("Note: if this is your MCDF, try redrawing yourself, wait and re-export the file. If you received it from someone else have them do the same.",
637-
ImGuiColors.DalamudYellow);
634+
CharaDataHubCard.Error("Failed to read the MCDF file — it may be corrupt. Re-export it and try again.");
635+
CharaDataHubCard.Info("If this is your MCDF, try redrawing yourself, wait a moment, then re-export. If someone sent it to you, ask them to do the same.");
638636
}
639637

640638
private void DrawMetaInfoCard(CharaDataMetaInfoExtendedDto data, bool canOpen = false)

Snowcloak/UI/CharaDataHubGposeTogetherTab.cs

Lines changed: 48 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,14 @@ public void Draw()
4444
if (!_charaDataManager.BrioAvailable)
4545
{
4646
ImGuiHelpers.ScaledDummy(5);
47-
ElezenImgui.DrawGroupedCenteredColorText("BRIO IS MANDATORY FOR GPOSE TOGETHER.", ImGuiColors.DalamudRed);
47+
CharaDataHubCard.Error("Brio is required for GPose Together. Install and enable Brio to use this feature.");
4848
ImGuiHelpers.ScaledDummy(5);
4949
}
5050

5151
if (!_apiController.IsConnected)
5252
{
5353
ImGuiHelpers.ScaledDummy(5);
54-
ElezenImgui.DrawGroupedCenteredColorText("CANNOT USE GPOSE TOGETHER WHILE DISCONNECTED FROM THE SERVER.", ImGuiColors.DalamudRed);
54+
CharaDataHubCard.Error("GPose Together is unavailable while you are disconnected from the server.");
5555
ImGuiHelpers.ScaledDummy(5);
5656
}
5757

@@ -61,7 +61,8 @@ public void Draw()
6161
+ "To use GPose together you either create or join a GPose Together Lobby. After you and other people have joined, make sure that everyone is on the same map. "
6262
+ "It is not required for you to be on the same server, DC or instance. Users that are on the same map will be drawn as moving purple wisps in the overworld, so you can easily find each other." + (Environment.NewLine + Environment.NewLine)
6363
+ "Once you are close to each other you can initiate GPose. You must either assign or spawn characters for each of the lobby users. Their own poses and positions to their character will be automatically applied." + Environment.NewLine
64-
+ "Pose and location data during GPose are updated approximately every few seconds.");
64+
+ "Pose and location data during GPose are updated approximately every few seconds." + (Environment.NewLine + Environment.NewLine)
65+
+ "If you delete an actor that is assigned to a user, it will be unassigned automatically. You can also unassign it manually with the trash button before deleting.");
6566

6667
using var disabled = ImRaii.Disabled(!_charaDataManager.BrioAvailable || !_apiController.IsConnected);
6768

@@ -76,16 +77,21 @@ public void Draw()
7677
ImGuiHelpers.ScaledDummy(5);
7778
ImGui.SetNextItemWidth(250);
7879
ImGui.InputTextWithHint("##lobbyId", "GPose Lobby ID", ref _joinLobbyId, 30);
79-
if (ElezenImgui.ShowIconButton(FontAwesomeIcon.ArrowRight, "Join GPose Together Lobby"))
80+
using (ImRaii.Disabled(string.IsNullOrWhiteSpace(_joinLobbyId)))
8081
{
81-
_session.JoinGPoseLobby(_joinLobbyId);
82-
_joinLobbyId = string.Empty;
82+
if (ElezenImgui.ShowIconButton(FontAwesomeIcon.ArrowRight, "Join GPose Together Lobby"))
83+
{
84+
_session.JoinGPoseLobby(_joinLobbyId);
85+
_joinLobbyId = string.Empty;
86+
}
8387
}
8488
if (!string.IsNullOrEmpty(_session.LastGPoseLobbyId)
8589
&& ElezenImgui.ShowIconButton(FontAwesomeIcon.LongArrowAltRight, string.Format("Rejoin Last Lobby {0}", _session.LastGPoseLobbyId)))
8690
{
8791
_session.JoinGPoseLobby(_session.LastGPoseLobbyId);
8892
}
93+
ImGuiHelpers.ScaledDummy(5);
94+
CharaDataHubCard.Info("Create or join a lobby to start a GPose Together session.");
8995
}
9096
else
9197
{
@@ -108,39 +114,43 @@ public void Draw()
108114
}
109115
ElezenImgui.AttachTooltip("Leave the current GPose lobby." + ElezenImgui.TooltipSeparator + "Hold CTRL and click to leave.");
110116
}
117+
118+
// Sending data and assigning users only makes sense once you are in a lobby; rendering these
119+
// controls disabled (and an empty user list) outside a lobby just reads as broken UI.
120+
if (string.IsNullOrEmpty(_session.CurrentGPoseLobbyId))
121+
return;
122+
111123
SnowcloakUi.DistanceSeparator();
112-
using (ImRaii.Disabled(string.IsNullOrEmpty(_session.CurrentGPoseLobbyId)))
124+
if (ElezenImgui.ShowIconButton(FontAwesomeIcon.ArrowUp, "Send Updated Character Data"))
113125
{
114-
if (ElezenImgui.ShowIconButton(FontAwesomeIcon.ArrowUp, "Send Updated Character Data"))
115-
{
116-
_ = _session.PushCharacterDownloadDto();
117-
}
118-
ElezenImgui.AttachTooltip("This will send your current appearance, pose and world data to all users in the lobby.");
119-
if (!_dalamudUtilService.IsInGpose)
126+
_ = _session.PushCharacterDownloadDto();
127+
}
128+
ElezenImgui.AttachTooltip("This will send your current appearance, pose and world data to all users in the lobby.");
129+
if (!_dalamudUtilService.IsInGpose)
130+
{
131+
ImGuiHelpers.ScaledDummy(5);
132+
CharaDataHubCard.Warning("Assigning users to characters is only available in GPose.");
133+
ImGuiHelpers.ScaledDummy(5);
134+
}
135+
SnowcloakUi.DistanceSeparator();
136+
ImGui.TextUnformatted("Users In Lobby");
137+
var gposeCharas = _dalamudUtilService.GetGposeCharactersFromObjectTable();
138+
var self = _dalamudUtilService.GetPlayerCharacter();
139+
gposeCharas = gposeCharas.Where(c => c != null && !string.Equals(c.Name.TextValue, self.Name.TextValue, StringComparison.Ordinal)).ToList();
140+
141+
using (ImRaii.Child("charaChild", new(0, 0), false, ImGuiWindowFlags.AlwaysAutoResize))
142+
{
143+
ImGuiHelpers.ScaledDummy(3);
144+
145+
if (!_session.UsersInLobby.Any())
120146
{
121-
ImGuiHelpers.ScaledDummy(5);
122-
ElezenImgui.DrawGroupedCenteredColorText("Assigning users to characters is only available in GPose.", ImGuiColors.DalamudYellow, 300);
147+
CharaDataHubCard.Info("No other users in the current GPose lobby yet. Share the Lobby ID above to invite people.");
123148
}
124-
SnowcloakUi.DistanceSeparator();
125-
ImGui.TextUnformatted("Users In Lobby");
126-
var gposeCharas = _dalamudUtilService.GetGposeCharactersFromObjectTable();
127-
var self = _dalamudUtilService.GetPlayerCharacter();
128-
gposeCharas = gposeCharas.Where(c => c != null && !string.Equals(c.Name.TextValue, self.Name.TextValue, StringComparison.Ordinal)).ToList();
129-
130-
using (ImRaii.Child("charaChild", new(0, 0), false, ImGuiWindowFlags.AlwaysAutoResize))
149+
else
131150
{
132-
ImGuiHelpers.ScaledDummy(3);
133-
134-
if (!_session.UsersInLobby.Any() && !string.IsNullOrEmpty(_session.CurrentGPoseLobbyId))
135-
{
136-
ElezenImgui.DrawGroupedCenteredColorText("No other users in current GPose lobby", ImGuiColors.DalamudYellow);
137-
}
138-
else
151+
foreach (var user in _session.UsersInLobby)
139152
{
140-
foreach (var user in _session.UsersInLobby)
141-
{
142-
DrawLobbyUser(user, gposeCharas);
143-
}
153+
DrawLobbyUser(user, gposeCharas);
144154
}
145155
}
146156
}
@@ -182,7 +192,7 @@ private void DrawLobbyUser(GposeLobbyUserData user,
182192
_ = _session.SpawnAndApplyData(user);
183193
}
184194
}
185-
ElezenImgui.AttachTooltip("Spawn new actor, apply character data and and assign it to this user." + ElezenImgui.TooltipSeparator + "Note: If the button is grayed out, " +
195+
ElezenImgui.AttachTooltip("Spawn new actor, apply character data and assign it to this user." + ElezenImgui.TooltipSeparator + "Note: If the button is grayed out, " +
186196
"the user has not sent any character data or you are on the same map, server and instance. If the latter is the case, join a group with that user and assign the character to them.");
187197

188198

@@ -210,12 +220,15 @@ private void DrawLobbyUser(GposeLobbyUserData user,
210220

211221
ImGui.SameLine();
212222
ElezenImgui.ShowIcon(FontAwesomeIcon.Running, sameMapAndServer.SameEverything ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed);
213-
ElezenImgui.AttachTooltip((sameMapAndServer.SameEverything ? "You are in the same instanced area." : "You are not the same instanced area.") + ElezenImgui.TooltipSeparator +
223+
ElezenImgui.AttachTooltip((sameMapAndServer.SameEverything ? "You are in the same instanced area." : "You are not in the same instanced area.") + ElezenImgui.TooltipSeparator +
214224
"Note: Users not in your instance, but on the same map, will be drawn as floating wisps." + Environment.NewLine
215225
+ "Note: GPose synchronization is not dependent on the current instance, but you will have to spawn a character for the other lobby users.");
216226

217227
using (ImRaii.Disabled(!_dalamudUtilService.IsInGpose))
218228
{
229+
ImGui.AlignTextToFramePadding();
230+
ImGui.TextUnformatted("Actor");
231+
ImGui.SameLine();
219232
ImGui.SetNextItemWidth(200);
220233
using (var combo = ImRaii.Combo("##character", string.IsNullOrEmpty(user.AssociatedCharaName) ? "No character assigned" : _ctx.CharaName(user.AssociatedCharaName)))
221234
{

0 commit comments

Comments
 (0)