Optimizations

This commit is contained in:
Sebastian Bularca
2026-04-20 09:51:08 +02:00
parent 2b48af8d3e
commit e7595bdc89
7 changed files with 152 additions and 48 deletions

View File

@@ -34,12 +34,13 @@ namespace Jovian.EncounterSystem {
return;
}
cache = new Dictionary<string, string>();
cache = new Dictionary<string, string>(lines?.Count ?? 0);
if(lines == null) {
return;
}
foreach(var line in lines) {
for(int i = 0; i < lines.Count; i++) {
var line = lines[i];
if(line != null && !string.IsNullOrEmpty(line.id)) {
cache[line.id] = line.text;
}

View File

@@ -7,18 +7,13 @@ namespace Jovian.EncounterSystem {
public EncounterTable table;
public string internalId;
/// <summary>O(1) lookup via <see cref="EncounterTable.Resolve"/>. Returns <c>null</c> if the
/// table is missing or the id can't be found.</summary>
public IEncounter Resolve() {
if(table == null || table.encounters == null || string.IsNullOrEmpty(internalId)) {
if(table == null || string.IsNullOrEmpty(internalId)) {
return null;
}
foreach(var encounter in table.encounters) {
if(encounter?.EncounterDefinition?.internalId == internalId) {
return encounter;
}
}
return null;
return table.Resolve(internalId);
}
}
}

View File

@@ -14,11 +14,19 @@ namespace Jovian.EncounterSystem {
public Dictionary<string, IEncounter> GetEncounters() => encounters;
public void RegisterEncounter(IEncounter encounter) {
encounters?.TryAdd(encounter?.EncounterDefinition?.internalId, encounter);
var id = encounter?.EncounterDefinition?.internalId;
if(string.IsNullOrEmpty(id)) {
return;
}
encounters.TryAdd(id, encounter);
}
public void UnregisterEncounter(IEncounter encounter) {
encounters.Remove(encounter.EncounterDefinition.internalId);
var id = encounter?.EncounterDefinition?.internalId;
if(string.IsNullOrEmpty(id)) {
return;
}
encounters.Remove(id);
}
public void ClearEncounters() {
@@ -26,10 +34,24 @@ namespace Jovian.EncounterSystem {
}
public void PopulateEncounters() {
foreach(var collection in encounterCollections) {
foreach(var encounter in collection.encounterTables) {
foreach(var encounterInstance in encounter.encounters) {
RegisterEncounter(encounterInstance);
if(encounterCollections == null) {
return;
}
for(int c = 0; c < encounterCollections.Length; c++) {
var collection = encounterCollections[c];
if(collection?.encounterTables == null) {
continue;
}
for(int t = 0; t < collection.encounterTables.Length; t++) {
var table = collection.encounterTables[t];
if(table?.encounters == null) {
continue;
}
for(int i = 0; i < table.encounters.Count; i++) {
RegisterEncounter(table.encounters[i]);
}
}
}

View File

@@ -16,12 +16,17 @@ namespace Jovian.EncounterSystem {
handlers.Remove(typeof(T));
}
public void Resolve(IEnumerable<IEncounterEvent> events, EncounterContext context) {
/// <summary>Indexed iteration over <paramref name="events"/> — avoids the boxed enumerator
/// that an <c>IEnumerable&lt;T&gt;</c> parameter would force. <see cref="EncounterDialogOption.events"/>
/// is a <c>List</c>, which implements <c>IReadOnlyList</c>, so call sites just pass it directly.</summary>
public void Resolve(IReadOnlyList<IEncounterEvent> events, EncounterContext context) {
if(events == null) {
return;
}
foreach(var evt in events) {
var count = events.Count;
for(int i = 0; i < count; i++) {
var evt = events[i];
if(evt == null) {
continue;
}

View File

@@ -8,6 +8,24 @@ namespace Jovian.EncounterSystem {
public string id;
public List<Encounter> encounters;
private Dictionary<string, Encounter> idCache;
/// <summary>O(1) lookup by <see cref="EncounterDefinition.internalId"/>. Used by <see cref="EncounterLink"/>.
/// Call <see cref="InvalidateCache"/> if you mutate the <see cref="encounters"/> list at runtime.</summary>
public IEncounter Resolve(string internalId) {
if(string.IsNullOrEmpty(internalId) || encounters == null || encounters.Count == 0) {
return null;
}
EnsureCache();
return idCache.TryGetValue(internalId, out var encounter) ? encounter : null;
}
/// <summary>Force the id lookup cache to rebuild on next use.</summary>
public void InvalidateCache() {
idCache = null;
}
public IEncounter GetRandomEncounter() {
if(encounters == null || encounters.Count == 0) {
return null;
@@ -16,50 +34,92 @@ namespace Jovian.EncounterSystem {
return encounters[UnityEngine.Random.Range(0, encounters.Count)];
}
/// <summary>Random pick restricted to encounters whose runtime type matches <paramref name="type"/>.
/// Zero-alloc — uses reservoir sampling instead of building an intermediate filtered list.</summary>
public IEncounter GetRandomEncounter(Type type) {
if(encounters == null || encounters.Count == 0 || type == null) {
return null;
}
IEncounter selected = null;
var seen = 0;
for(int i = 0; i < encounters.Count; i++) {
var encounter = encounters[i];
if(encounter == null || encounter.GetType() != type) {
continue;
}
seen++;
if(UnityEngine.Random.Range(0, seen) == 0) {
selected = encounter;
}
}
return selected;
}
/// <summary>Random pick restricted by <paramref name="filter"/>. Used with
/// <see cref="QuestProgress.IsGated"/> to exclude gated encounters. Zero-alloc via reservoir sampling.</summary>
public IEncounter GetRandomEncounter(Predicate<IEncounter> filter) {
if(encounters == null || encounters.Count == 0) {
return null;
}
var encountersOfType = encounters.FindAll(encounter => encounter.GetType() == type);
if(encountersOfType.Count > 0) {
return encountersOfType[UnityEngine.Random.Range(0, encountersOfType.Count)];
}
return null;
}
/// <summary>Random pick limited by a predicate. Used with <see cref="QuestProgress.IsGated"/> to exclude gated encounters.</summary>
public IEncounter GetRandomEncounter(Predicate<IEncounter> filter) {
if(encounters == null || encounters.Count == 0 || filter == null) {
if(filter == null) {
return GetRandomEncounter();
}
var pool = encounters.FindAll(filter);
if(pool.Count == 0) {
return null;
IEncounter selected = null;
var seen = 0;
for(int i = 0; i < encounters.Count; i++) {
var encounter = encounters[i];
if(encounter == null || !filter(encounter)) {
continue;
}
seen++;
if(UnityEngine.Random.Range(0, seen) == 0) {
selected = encounter;
}
}
return selected;
}
private void EnsureCache() {
if(idCache != null) {
return;
}
return pool[UnityEngine.Random.Range(0, pool.Count)];
idCache = new Dictionary<string, Encounter>(encounters?.Count ?? 0);
if(encounters == null) {
return;
}
for(int i = 0; i < encounters.Count; i++) {
var encounter = encounters[i];
var internalId = encounter?.EncounterDefinition?.internalId;
if(!string.IsNullOrEmpty(internalId)) {
idCache[internalId] = encounter;
}
}
}
#if UNITY_EDITOR
// Unity's inspector "+" duplicates the previous list element, including nested internalId
// GUIDs. Regenerate any duplicates so every encounter carries a unique internalId.
private void OnValidate() {
InvalidateCache();
if(encounters == null) {
return;
}
var seen = new HashSet<string>();
var seen = new HashSet<string>(encounters.Count);
var changed = false;
foreach(var encounter in encounters) {
for(int i = 0; i < encounters.Count; i++) {
var encounter = encounters[i];
if(encounter?.EncounterDefinition == null) {
continue;
}
var id = encounter.EncounterDefinition.internalId;
if(string.IsNullOrEmpty(id) || !seen.Add(id)) {
var entryId = encounter.EncounterDefinition.internalId;
if(string.IsNullOrEmpty(entryId) || !seen.Add(entryId)) {
encounter.EncounterDefinition.internalId = Guid.NewGuid().ToString();
seen.Add(encounter.EncounterDefinition.internalId);
changed = true;

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Jovian.EncounterSystem {
public enum QuestLogEventType {
@@ -46,9 +45,12 @@ namespace Jovian.EncounterSystem {
}
public IEnumerable<string> ResolvedEncounterIds() {
return entries
.Select(entry => entry.encounterInternalId)
.Where(id => !string.IsNullOrEmpty(id));
for(int i = 0; i < entries.Count; i++) {
var id = entries[i].encounterInternalId;
if(!string.IsNullOrEmpty(id)) {
yield return id;
}
}
}
private void Record(QuestLogEventType type, IEncounter from, IEncounter to) {

View File

@@ -8,8 +8,8 @@ namespace Jovian.EncounterSystem {
/// construction; rolling and advancement are O(1).
/// </summary>
public class QuestProgress {
private readonly HashSet<string> resolvedIds = new();
private readonly Dictionary<string, IEncounter> predecessorOf = new();
private readonly HashSet<string> resolvedIds;
private readonly Dictionary<string, IEncounter> predecessorOf;
public event Action<IEncounter> QuestStarted;
public event Action<IEncounter, IEncounter> QuestAdvanced;
@@ -18,9 +18,26 @@ namespace Jovian.EncounterSystem {
public IReadOnlyCollection<string> ResolvedIds => resolvedIds;
public QuestProgress(EncountersCollection encountersCollection) {
var capacity = EstimateEncounterCount(encountersCollection);
resolvedIds = new HashSet<string>(capacity);
predecessorOf = new Dictionary<string, IEncounter>(capacity);
IndexQuests(encountersCollection);
}
private static int EstimateEncounterCount(EncountersCollection collection) {
if(collection?.encounterTables == null) {
return 0;
}
var total = 0;
for(int i = 0; i < collection.encounterTables.Length; i++) {
var table = collection.encounterTables[i];
if(table?.encounters != null) {
total += table.encounters.Count;
}
}
return total;
}
public bool IsGated(IEncounter encounter) {
var id = encounter?.EncounterDefinition?.internalId;
if(string.IsNullOrEmpty(id)) {
@@ -77,13 +94,15 @@ namespace Jovian.EncounterSystem {
return;
}
foreach(var table in collection.encounterTables) {
for(int t = 0; t < collection.encounterTables.Length; t++) {
var table = collection.encounterTables[t];
if(table?.encounters == null) {
continue;
}
foreach(var encounter in table.encounters) {
if(encounter?.EncounterDefinition.Kind is not QuestKind questKind) {
for(int i = 0; i < table.encounters.Count; i++) {
var encounter = table.encounters[i];
if(encounter?.EncounterDefinition?.Kind is not QuestKind questKind) {
continue;
}