Optimizations
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<T></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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user