Difficulty based sorting and analytics fixes

This commit is contained in:
Savya Bikram Shah
2026-07-06 13:20:27 +05:45
parent b28e1f637d
commit 3df3d923fb
43 changed files with 4231 additions and 64 deletions

View File

@@ -30,6 +30,12 @@ namespace Darkmatter.Features.AppBoot.Flow
public async UniTask StartAsync(CancellationToken cancellation = default)
{
// Entry points run in serviceModules registration order and this module is near the front.
// Everything below LoadAsync completes synchronously, so without a real yield the
// IntroStartedSignal publish would happen before later-registered subscribers
// (e.g. AnalyticsTracker) have run Start() — the event bus has no replay, so they'd miss it.
await UniTask.Yield();
#if UNITY_ANDROID || UNITY_IOS
Application.targetFrameRate = 60;
#endif

View File

@@ -7,6 +7,7 @@ using Darkmatter.Core.Contracts.Features.DrawingCatalog;
using Darkmatter.Core.Contracts.Features.Progression;
using Darkmatter.Core.Contracts.Features.SaveGate;
using Darkmatter.Core.Contracts.Services.Gallery;
using Darkmatter.Core.Data.Signals.Features.Capture;
using Darkmatter.Core.Data.Signals.Features.Drawing;
using Darkmatter.Libs.Observer;
using UnityEngine;
@@ -209,7 +210,20 @@ namespace Darkmatter.Features.Artbook
texture = await _progression.GetCachedThumbnailAsync(entry.Value.Id);
if (texture == null) return;
await _gallery.SaveImageAsync(texture, entry.Value.Name, ct);
// Mirror CaptureSystem's pairing: Started before the write, Completed(success) in
// finally — art-book saves land in the same gallery_save_* analytics funnel as
// gameplay saves.
_eventBus.Publish(new GallerySaveStartedSignal());
var success = false;
try
{
await _gallery.SaveImageAsync(texture, entry.Value.Name, ct);
success = true;
}
finally
{
_eventBus.Publish(new GallerySaveCompletedSignal(success, entry.Value.Id));
}
// The art book has no success popup of its own, so use the shared toast.
await _saveGate.ShowSavedAsync(ct);
}

View File

@@ -60,12 +60,14 @@ namespace Darkmatter.Features.Capture
var tex = new Texture2D(2, 2, TextureFormat.RGBA32, mipChain: false);
var success = false;
// Started must pair 1:1 with the Completed publish in finally — a failed LoadImage or a
// throw in BuildFileNameAsync would otherwise emit Completed(false) with no Started.
_bus.Publish(new GallerySaveStartedSignal());
try
{
if (tex.LoadImage(png))
{
var fileName = await BuildFileNameAsync();
_bus.Publish(new GallerySaveStartedSignal());
await _galleryService.SaveImageAsync(tex, fileName, ct);
success = true;
}
@@ -73,7 +75,7 @@ namespace Darkmatter.Features.Capture
finally
{
UnityEngine.Object.Destroy(tex);
_bus.Publish(new GallerySaveCompletedSignal(success));
_bus.Publish(new GallerySaveCompletedSignal(success, _progression.LastOpenedTemplateId));
}
return png;
}

View File

@@ -68,7 +68,13 @@ public class ColorbookFlowController : IAsyncStartable, IDisposable
// one VContainer cancels on teardown, and the linked _scopeCts may lag the callback order.
if (cancellation.IsCancellationRequested || ct.IsCancellationRequested) return;
if (!_navigatingToGameplay) _loadingScreen.Hide();
if (!_navigatingToGameplay)
{
_loadingScreen.Hide();
// Catalog genuinely on screen — not a transit hop toward gameplay (edit/next relays land
// in this scene with _navigatingToGameplay already latched and never reveal the catalog).
_bus.Publish(new ColorbookShownSignal());
}
PrewarmInterstitialAdAsync().Forget();
}

View File

@@ -57,8 +57,8 @@ public sealed class DrawingCatalogController : IDrawingCatalogController
private void Refresh()
{
_visible.Clear();
// Catalog already orders ids by difficulty (easiest first), then id.
_visible.AddRange(_catalog.AllTemplateIds);
_visible.Sort(StringComparer.OrdinalIgnoreCase);
ListChanged?.Invoke();
}
}

View File

@@ -48,8 +48,14 @@ namespace Darkmatter.Features.DrawingCatalog
}
private void HandleOpenColorBookSignal(OpenColorBookSignal signal)
{
ShowCatalog();
}
private void ShowCatalog()
{
_view.Show();
_eventBus.Publish(new ColorbookShownSignal());
}
private void OnRightPageBtnClicked()
@@ -84,7 +90,7 @@ namespace Darkmatter.Features.DrawingCatalog
private void OnArtBookBtnClicked()
{
_eventBus.Publish(new OpenArtBookSignal(()=> _view.Show()));
_eventBus.Publish(new OpenArtBookSignal(ShowCatalog));
_view.Hide();
}

View File

@@ -68,7 +68,9 @@ namespace Darkmatter.Features.DrawingTemplates.Editor
_templates.Clear();
_shapes.Clear();
_palettes.Clear();
_templates.AddRange(FindAllOfType<DrawingTemplateSO>());
_templates.AddRange(FindAllOfType<DrawingTemplateSO>()
.OrderBy(t => t.Difficulty)
.ThenBy(t => t.Id, StringComparer.OrdinalIgnoreCase));
_shapes.AddRange(FindAllOfType<ShapeSO>());
_palettes.AddRange(FindAllOfType<ColorPaletteSO>());
}
@@ -169,7 +171,7 @@ namespace Darkmatter.Features.DrawingTemplates.Editor
using (new EditorGUILayout.VerticalScope())
{
GUILayout.Label(string.IsNullOrEmpty(t.DisplayName) ? t.name : t.DisplayName, EditorStyles.boldLabel);
GUILayout.Label("id: " + (string.IsNullOrEmpty(t.Id) ? "<unset>" : t.Id), EditorStyles.miniLabel);
GUILayout.Label($"id: {(string.IsNullOrEmpty(t.Id) ? "<unset>" : t.Id)} • diff: {t.Difficulty}{(t.HasDifficultyOverride ? "*" : $" ({t.Pieces.Count}p+{t.AuthoredRegions.Count}r)")}", EditorStyles.miniLabel);
}
if (GUILayout.Button("Select", GUILayout.Width(54)))
Select(t);
@@ -206,6 +208,13 @@ namespace Darkmatter.Features.DrawingTemplates.Editor
EditorGUILayout.LabelField("Basics", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(so.FindProperty("id"), new GUIContent("Id (Addressable Key)"));
EditorGUILayout.PropertyField(so.FindProperty("displayName"));
EditorGUILayout.PropertyField(so.FindProperty("difficultyOverride"),
new GUIContent("Difficulty Override", "0 = auto (pieces + regions). Any positive value overrides."));
EditorGUILayout.LabelField(
new GUIContent("Difficulty (effective)", "Sorts the catalog, lowest first."),
new GUIContent(t.HasDifficultyOverride
? $"{t.Difficulty} (override; auto would be {t.AutoDifficulty})"
: $"{t.Difficulty} (auto: {t.Pieces.Count} pieces + {t.AuthoredRegions.Count} regions)"));
EditorGUILayout.PropertyField(so.FindProperty("defaultThumbnail"));
EditorGUILayout.Space(6);

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
@@ -48,7 +49,7 @@ namespace Darkmatter.Features.DrawingTemplates.Systems
_byId[t.Id] = t;
}
_allIds.Sort();
SortIds();
_initialized = true;
}
@@ -84,12 +85,22 @@ namespace Darkmatter.Features.DrawingTemplates.Systems
if (!_allIds.Contains(id))
{
_allIds.Add(id);
_allIds.Sort();
SortIds();
}
return t;
}
// Easiest drawings first; ties broken by id so the order is stable.
private void SortIds()
{
_allIds.Sort((a, b) =>
{
var byDifficulty = _byId[a].Difficulty.CompareTo(_byId[b].Difficulty);
return byDifficulty != 0 ? byDifficulty : string.Compare(a, b, StringComparison.OrdinalIgnoreCase);
});
}
public void ReleaseAll()
{
if (!_initialized) return;

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using Darkmatter.Core;
using Darkmatter.Core.Contracts.Features.Capture;
using Darkmatter.Core.Contracts.Features.Coloring;
using Darkmatter.Core.Contracts.Features.DrawingCatalog;
@@ -28,6 +29,7 @@ namespace Darkmatter.Features.GameplayFlow.Systems
public sealed class GameplayFlowController : IAsyncStartable, IGameplayFlowController, IDisposable
{
private const int AutosaveDebounceMs = 500;
private const float ThumbnailMinIntervalSec = 30f;
private readonly IProgressionSystem _progression;
private readonly IDrawingTemplateCatalog _catalog;
@@ -44,10 +46,12 @@ namespace Darkmatter.Features.GameplayFlow.Systems
private string _templateId;
private DrawingPhase _phase = DrawingPhase.ShapeBuilding;
private bool _allContentReported;
private float _lastThumbnailTime = float.NegativeInfinity;
private IDisposable _assembledSub;
private IDisposable _colorAppliedSub;
private IDisposable _drawingSelectedSub;
private IDisposable _openColorbookSub;
private CancellationTokenSource _autosaveCts;
private CancellationTokenSource _scopeCts;
@@ -104,6 +108,10 @@ namespace Darkmatter.Features.GameplayFlow.Systems
_assembledSub = _bus.Subscribe<ShapeAssembledSignal>(OnShapeAssembled);
_colorAppliedSub = _bus.Subscribe<ColorAppliedSignal>(OnColorApplied);
_drawingSelectedSub = _bus.Subscribe<DrawingSelectedSignal>(OnDrawingSelected);
// The art book's "colorbook" button publishes OpenColorBookSignal. In the Colorbook
// scene DrawingCatalogPresenter shows the catalog; during gameplay that presenter is
// dead, so treat the signal as "leave gameplay for the colorbook" (same as back).
_openColorbookSub = _bus.Subscribe<OpenColorBookSignal>(OnOpenColorBook);
_loadingScreen.SetProgress(1f);
if (_phase == DrawingPhase.Coloring)
@@ -131,8 +139,10 @@ namespace Darkmatter.Features.GameplayFlow.Systems
public async UniTask SaveAsync(CancellationToken ct)
{
await SaveCurrentAsync(ct);
await _capture.CapturePngAsync(saveToGallery: true, ct);
// One capture serves both the gallery write and the progress thumbnail — this used to
// run the full capture pipeline twice back to back.
var png = await _capture.CapturePngAsync(saveToGallery: true, ct);
await SaveCurrentAsync(ct, captureThumbnail: false, precapturedThumbnail: png);
}
public async UniTask NextAsync(CancellationToken ct)
@@ -209,6 +219,11 @@ namespace Darkmatter.Features.GameplayFlow.Systems
DebouncedAutosaveAsync(_autosaveCts.Token).Forget();
}
private void OnOpenColorBook(OpenColorBookSignal _)
{
BackAsync(CancellationToken.None).Forget();
}
private void OnDrawingSelected(DrawingSelectedSignal signal)
{
if (string.IsNullOrEmpty(signal.TemplateId)) return;
@@ -232,7 +247,13 @@ namespace Darkmatter.Features.GameplayFlow.Systems
try
{
await UniTask.Delay(AutosaveDebounceMs, cancellationToken: ct);
await SaveCurrentAsync(ct);
// Autosaves fire after every paint pause; capturing a thumbnail each time renders the
// scene to an RT and PNG-encodes it, which OOM-kills long sessions on low-RAM devices.
// Colors always persist; the thumbnail refreshes at most every ThumbnailMinIntervalSec
// here and unconditionally on the exit paths (back / next / select-away).
bool refreshThumbnail =
Time.realtimeSinceStartup - _lastThumbnailTime >= ThumbnailMinIntervalSec;
await SaveCurrentAsync(ct, captureThumbnail: refreshThumbnail);
}
catch (OperationCanceledException)
{
@@ -240,7 +261,8 @@ namespace Darkmatter.Features.GameplayFlow.Systems
}
}
private async UniTask SaveCurrentAsync(CancellationToken ct)
private async UniTask SaveCurrentAsync(CancellationToken ct, bool captureThumbnail = true,
byte[] precapturedThumbnail = null)
{
if (string.IsNullOrEmpty(_templateId)) return;
@@ -263,7 +285,10 @@ namespace Darkmatter.Features.GameplayFlow.Systems
FirstCompletedUtc = existing?.FirstCompletedUtc,
};
var thumbnailPng = await _capture.CapturePngAsync(saveToGallery: false, ct);
byte[] thumbnailPng = precapturedThumbnail;
if (thumbnailPng == null && captureThumbnail)
thumbnailPng = await _capture.CapturePngAsync(saveToGallery: false, ct);
if (thumbnailPng != null) _lastThumbnailTime = Time.realtimeSinceStartup;
await _progression.SaveProgressAsync(progress, thumbnailPng);
}
@@ -284,6 +309,7 @@ namespace Darkmatter.Features.GameplayFlow.Systems
_assembledSub?.Dispose();
_colorAppliedSub?.Dispose();
_drawingSelectedSub?.Dispose();
_openColorbookSub?.Dispose();
_autosaveCts?.Cancel();
_autosaveCts?.Dispose();
_scopeCts?.Cancel();