Skip to content

Commit 0e9fcd5

Browse files
Merge pull request #176 from UchuServer/enhancement/temporary-caching
2 parents 90e8f9d + ccffa72 commit 0e9fcd5

34 files changed

+299
-255
lines changed

Uchu.StandardScripts/BlockYard/BlockYardProperty.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System;
1111
using InfectedRose.Luz;
1212
using System.Numerics;
13+
using Uchu.World.Client;
1314

1415
namespace Uchu.StandardScripts.BlockYard
1516
{
@@ -189,8 +190,7 @@ private void WithdrawSpider(Player Player, bool Withdraw) // This removes the sp
189190

190191
SpiderQueen.Animate("withdraw");
191192

192-
using var cdClient = new CdClientContext();
193-
var Animation = cdClient.AnimationsTable.FirstOrDefault(
193+
var Animation = ClientCache.GetTable<Animations>().FirstOrDefault(
194194
a => a.Animationname == "withdraw"
195195
);
196196

Uchu.StandardScripts/General/LaunchpadEvent.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
using System.Linq;
12
using System.Threading.Tasks;
23
using Microsoft.EntityFrameworkCore;
34
using Uchu.Core;
45
using Uchu.Core.Client;
56
using Uchu.World;
7+
using Uchu.World.Client;
68
using Uchu.World.Scripting.Native;
79

810
namespace Uchu.StandardScripts.General
@@ -19,11 +21,9 @@ public override Task LoadAsync()
1921
{
2022
var launchpad = message.Associate.GetComponent<RocketLaunchpadComponent>();
2123

22-
await using var cdClient = new CdClientContext();
23-
2424
var id = launchpad.GameObject.Lot.GetComponentId(ComponentId.RocketLaunchComponent);
2525

26-
var launchpadComponent = await cdClient.RocketLaunchpadControlComponentTable.FirstOrDefaultAsync(
26+
var launchpadComponent = (await ClientCache.GetTableAsync<RocketLaunchpadControlComponent>()).FirstOrDefault(
2727
r => r.Id == id
2828
);
2929

Uchu.World/Client/CdClient/ClientCache.cs

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
using System;
2+
using System.Collections.Generic;
13
using System.Linq;
4+
using System.Reflection;
25
using System.Threading.Tasks;
6+
using Microsoft.EntityFrameworkCore;
37
using Uchu.Core;
48
using Uchu.Core.Client;
59
using Uchu.World.Systems.Missions;
@@ -11,6 +15,11 @@ namespace Uchu.World.Client
1115
/// </summary>
1216
public static class ClientCache
1317
{
18+
/// <summary>
19+
/// Cache of the table objects used by GetTable and GetTableAsync
20+
/// </summary>
21+
private static Dictionary<string,object> cacheTables = new Dictionary<string,object>();
22+
1423
/// <summary>
1524
/// All missions in the cd client
1625
/// </summary>
@@ -21,17 +30,18 @@ public static class ClientCache
2130
/// </summary>
2231
public static MissionInstance[] Achievements { get; private set; } = { };
2332

33+
/// <summary>
34+
/// Loads the initial cache
35+
/// </summary>
2436
public static async Task LoadAsync()
2537
{
26-
await using var cdContext = new CdClientContext();
27-
2838
Logger.Debug("Setting up missions cache");
29-
var missionTasks = cdContext.MissionsTable
39+
var missionTasks = GetTable<Missions>()
3040
.ToArray()
3141
.Select(async m =>
3242
{
3343
var instance = new MissionInstance(m.Id ?? 0);
34-
await instance.LoadAsync(cdContext);
44+
await instance.LoadAsync();
3545
return instance;
3646
}).ToList();
3747

@@ -40,5 +50,28 @@ public static async Task LoadAsync()
4050
Missions = missionTasks.Select(t => t.Result).ToArray();
4151
Achievements = Missions.Where(m => !m.IsMission).ToArray();
4252
}
53+
54+
/// <summary>
55+
/// Fetches the values of a table asynchronously.
56+
/// Will return without waiting if the data is already stored.
57+
/// </summary>
58+
public static async Task<T[]> GetTableAsync<T>() where T : class
59+
{
60+
var tableName = typeof(T).Name + "Table";
61+
if (!cacheTables.ContainsKey(tableName))
62+
{
63+
cacheTables[tableName] = new TableCache<T>(tableName);
64+
}
65+
66+
return await ((TableCache<T>) cacheTables[tableName]).GetValuesAsync();
67+
}
68+
69+
/// <summary>
70+
/// Fetches the values of a table.
71+
/// </summary>
72+
public static T[] GetTable<T>() where T : class
73+
{
74+
return GetTableAsync<T>().Result;
75+
}
4376
}
4477
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
using System;
2+
using System.Linq;
3+
using System.Reflection;
4+
using System.Threading.Tasks;
5+
using Microsoft.EntityFrameworkCore;
6+
using Uchu.Core.Client;
7+
8+
namespace Uchu.World.Client
9+
{
10+
/// <summary>
11+
/// Temporarily caches data for when a lot of client database
12+
/// reads are done at once, such as when the world starts.
13+
/// </summary>
14+
public class TableCache<T> where T : class
15+
{
16+
private PropertyInfo CdClientContextProperty;
17+
private T[] CachedData;
18+
private long LastAccessTime = 0;
19+
private bool ClearDataQueued = false;
20+
private int ClearTimeMilliseconds = 3000;
21+
22+
/// <summary>
23+
/// Creates the table cache.
24+
/// </summary>
25+
public TableCache(string tableName)
26+
{
27+
// Get the CdClient property info for fetching.
28+
foreach (var property in typeof(CdClientContext).GetProperties())
29+
{
30+
if (property.Name != tableName) continue;
31+
CdClientContextProperty = property;
32+
break;
33+
}
34+
35+
// Throw an exception if the table name is invalid.
36+
if (CdClientContextProperty == null)
37+
{
38+
throw new InvalidOperationException("Table name doesn't exist: " + tableName);
39+
}
40+
}
41+
42+
/// <summary>
43+
/// Returns the values of the table asynchronously.
44+
/// </summary>
45+
public async Task<T[]> GetValuesAsync()
46+
{
47+
// Update the last access time and fetch the data if it isn't stored.
48+
LastAccessTime = DateTimeOffset.Now.ToUnixTimeSeconds();
49+
if (CachedData == default)
50+
{
51+
// Fetch the data.
52+
await using var cdContext = new CdClientContext();
53+
CachedData = ((DbSet<T>) CdClientContextProperty.GetValue(cdContext))?.ToArray();
54+
55+
// Queue clearing the data.
56+
if (!ClearDataQueued)
57+
{
58+
ClearDataQueued = true;
59+
Task.Run(ClearQueueTimeAsync);
60+
}
61+
}
62+
63+
// Return the cached data.
64+
return CachedData;
65+
}
66+
67+
/// <summary>
68+
/// Queues clearing the cached data to save memory
69+
/// after a period of the data not being read from.
70+
/// </summary>
71+
private async void ClearQueueTimeAsync()
72+
{
73+
while (true)
74+
{
75+
// Wait the delay before checking to clear the table.
76+
var startLastAccessTime = LastAccessTime;
77+
await Task.Delay(ClearTimeMilliseconds);
78+
79+
// Clear the table if it hasn't been accessed recently.
80+
if (startLastAccessTime != LastAccessTime) continue;
81+
CachedData = default;
82+
ClearDataQueued = false;
83+
GC.Collect();
84+
return;
85+
}
86+
}
87+
}
88+
}

Uchu.World/Client/ZoneParser.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,7 @@ public async Task LoadZoneDataAsync(int seek)
3535

3636
Logger.Information("Parsing zone info...");
3737

38-
await using var ctx = new CdClientContext();
39-
40-
var zone = ctx.ZoneTableTable.FirstOrDefault(zone => zone.ZoneID == seek);
38+
var zone = (await ClientCache.GetTableAsync<ZoneTable>()).FirstOrDefault(zone => zone.ZoneID == seek);
4139

4240
if (zone == default)
4341
{

Uchu.World/Handlers/Commands/CharacterCommandHandler.cs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using Uchu.Core;
1313
using Uchu.Core.Client;
1414
using Uchu.Core.Resources;
15+
using Uchu.World.Client;
1516
using Uchu.World.Filters;
1617
using Uchu.World.Scripting.Native;
1718
using Uchu.World.Social;
@@ -748,9 +749,7 @@ public string World(string[] arguments, Player player)
748749

749750
if (!int.TryParse(arguments[0], out var id)) return "Invalid <zoneId>";
750751

751-
using CdClientContext ctx = new CdClientContext();
752-
753-
ZoneTable WorldTable = ctx.ZoneTableTable.FirstOrDefault(t => t.ZoneID == id);
752+
ZoneTable WorldTable = ClientCache.GetTable<ZoneTable>().FirstOrDefault(t => t.ZoneID == id);
754753

755754
if (WorldTable == default)
756755
{
@@ -888,17 +887,15 @@ public async Task<string> GetEmote(string[] arguments, Player player)
888887
if (arguments.Length == default)
889888
return "getemote <emote>";
890889

891-
await using var cdClient = new CdClientContext();
892-
893890
Emotes emote;
894891

895892
if (int.TryParse(arguments[0], out var id))
896893
{
897-
emote = await cdClient.EmotesTable.FirstOrDefaultAsync(c => c.Id == id);
894+
emote = (await ClientCache.GetTableAsync<Emotes>()).FirstOrDefault(c => c.Id == id);
898895
}
899896
else
900897
{
901-
emote = await cdClient.EmotesTable.FirstOrDefaultAsync(c => c.AnimationName == arguments[0].ToLower());
898+
emote = (await ClientCache.GetTableAsync<Emotes>()).FirstOrDefault(c => c.AnimationName == arguments[0].ToLower());
902899
}
903900

904901
if (emote?.Id == default)

Uchu.World/Handlers/GameMessages/GeneralHandler.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
using System.Linq;
12
using System.Threading.Tasks;
23
using Microsoft.EntityFrameworkCore;
34
using Uchu.Core;
45
using Uchu.Core.Client;
6+
using Uchu.World.Client;
57

68
namespace Uchu.World.Handlers.GameMessages
79
{
@@ -77,9 +79,7 @@ public async Task ReadyForUpdatesHandler(ReadyForUpdateMessage message, Player p
7779
[PacketHandler]
7880
public async Task PlayEmoteHandler(PlayEmoteMessage message, Player player)
7981
{
80-
await using var ctx = new CdClientContext();
81-
82-
var animation = await ctx.EmotesTable.FirstOrDefaultAsync(e => e.Id == message.EmoteId);
82+
var animation = (await ClientCache.GetTableAsync<Emotes>()).FirstOrDefault(e => e.Id == message.EmoteId);
8383

8484
player.Zone.BroadcastMessage(new PlayAnimationMessage
8585
{
@@ -109,13 +109,12 @@ public async Task PlayEmoteHandler(PlayEmoteMessage message, Player player)
109109
public async Task NotifyServerLevelProcessingCompleteHandler(NotifyServerLevelProcessingCompleteMessage message, Player player)
110110
{
111111
await using var ctx = new UchuContext();
112-
await using var cdClient = new CdClientContext();
113112

114113
var character = await ctx.Characters.FirstAsync(c => c.Id == player.Id);
115114

116115
var lookup_val = 0;
117116

118-
foreach (var levelProgressionLookup in cdClient.LevelProgressionLookupTable)
117+
foreach (var levelProgressionLookup in (await ClientCache.GetTableAsync<LevelProgressionLookup>()))
119118
{
120119
if (levelProgressionLookup.RequiredUScore > character.UniverseScore) break;
121120

Uchu.World/Handlers/WorldInitializationHandler.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using RakDotNet;
1111
using Uchu.Core;
1212
using Uchu.Core.Client;
13+
using Uchu.World.Client;
1314

1415
namespace Uchu.World.Handlers
1516
{
@@ -331,7 +332,6 @@ private static LevelNode LevelNode(Character character)
331332
private static FlagNode[] FlagNodes(Character character)
332333
{
333334
var flags = new Dictionary<int, FlagNode>();
334-
using var cdContext = new CdClientContext();
335335

336336
/*
337337
// Keep a list of all tasks ids that belong to flag tasks
@@ -427,8 +427,7 @@ private static MissionProgressNode[] ProgressArrayForMission(Mission mission)
427427
var progressNodes = new List<MissionProgressNode>
428428
{new MissionProgressNode {Value = task.Values.Sum(v => v.Count)}};
429429

430-
using var cdClient = new CdClientContext();
431-
var cdTask = cdClient.MissionTasksTable.First(t => t.Uid == task.TaskId);
430+
var cdTask = ClientCache.GetTable<MissionTasks>().First(t => t.Uid == task.TaskId);
432431

433432
// If the task type is collectible, also send all collectible ids
434433
if (cdTask.TaskType != null && ((MissionTaskType) cdTask.TaskType) == MissionTaskType.Collect)

Uchu.World/Objects/Components/DestroyableComponent.cs

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using RakDotNet.IO;
66
using Uchu.Core;
77
using Uchu.Core.Client;
8+
using Uchu.World.Client;
89

910
namespace Uchu.World
1011
{
@@ -213,11 +214,10 @@ protected DestroyableComponent()
213214
{
214215
if (GameObject is Player) CollectPlayerStats();
215216
else CollectObjectStats();
216-
217-
await using var cdClient = new CdClientContext();
218217

219-
var destroyable = cdClient.DestructibleComponentTable.FirstOrDefault(
220-
c => c.Id == GameObject.Lot.GetComponentId(ComponentId.DestructibleComponent)
218+
var componentId = GameObject.Lot.GetComponentId(ComponentId.DestructibleComponent);
219+
var destroyable = (await ClientCache.GetTableAsync<Core.Client.DestructibleComponent>()).FirstOrDefault(
220+
c => c.Id == componentId
221221
);
222222

223223
if (destroyable == default) return;
@@ -226,7 +226,7 @@ protected DestroyableComponent()
226226

227227
Smashable = destroyable.IsSmashable ?? false;
228228

229-
var faction = await cdClient.FactionsTable.FirstOrDefaultAsync(
229+
var faction = (await ClientCache.GetTableAsync<Factions>()).FirstOrDefault(
230230
f => f.Faction == Factions[0]
231231
);
232232

@@ -379,10 +379,9 @@ public void BoostBaseImagination(UchuContext context, uint delta)
379379

380380
private void CollectObjectStats()
381381
{
382-
using var cdClient = new CdClientContext();
383-
384-
var stats = cdClient.DestructibleComponentTable.FirstOrDefault(
385-
o => o.Id == GameObject.Lot.GetComponentId(ComponentId.DestructibleComponent)
382+
var componentId = GameObject.Lot.GetComponentId(ComponentId.DestructibleComponent);
383+
var stats = ClientCache.GetTable<Core.Client.DestructibleComponent>().FirstOrDefault(
384+
o => o.Id == componentId
386385
);
387386

388387
if (stats == default) return;

0 commit comments

Comments
 (0)