From 8c995d4c440375de7106e66e21e1e5c0d486a2bc Mon Sep 17 00:00:00 2001 From: Samuele Locatelli Date: Tue, 26 May 2026 19:01:01 +0200 Subject: [PATCH] Update caching SPEC --- MP.Data/Controllers/MpSpecController.cs | 26 +-- MP.Data/Services/ListSelectDataSrv.cs | 7 +- MP.SPEC/Components/ListDossiers.razor.cs | 2 +- MP.SPEC/Components/ListPODL.razor.cs | 27 +-- MP.SPEC/Data/MpDataService.cs | 271 +++++++++++++++++++---- MP.SPEC/MP.SPEC.csproj | 2 +- MP.SPEC/Pages/Articoli.razor.cs | 2 +- MP.SPEC/Pages/PODL.razor.cs | 12 +- MP.SPEC/Program.cs | 8 +- MP.SPEC/Resources/ChangeLog.html | 2 +- MP.SPEC/Resources/VersNum.txt | 2 +- MP.SPEC/Resources/manifest.xml | 2 +- 12 files changed, 272 insertions(+), 91 deletions(-) diff --git a/MP.Data/Controllers/MpSpecController.cs b/MP.Data/Controllers/MpSpecController.cs index bdfc3780..9847a5ad 100644 --- a/MP.Data/Controllers/MpSpecController.cs +++ b/MP.Data/Controllers/MpSpecController.cs @@ -456,18 +456,18 @@ namespace MP.Data.Controllers /// /// /// - public List ArticoliGetSearch(int numRecord, string azienda = "*", string searchVal = "") + public async Task> ArticoliGetSearchAsync(int numRecord, string azienda = "*", string searchVal = "") { List dbResult = new List(); using (var dbCtx = new MoonProContext(options)) { - dbResult = dbCtx - .DbSetArticoli - .AsNoTracking() - .Where(x => (azienda == "*" || x.Azienda.ToUpper() == azienda.ToUpper()) && (string.IsNullOrEmpty(searchVal) || x.CodArticolo.Contains(searchVal) || x.DescArticolo.Contains(searchVal) || x.Disegno.Contains(searchVal))) - .OrderBy(x => x.CodArticolo) - .Take(numRecord) - .ToList(); + dbResult = await dbCtx + .DbSetArticoli + .AsNoTracking() + .Where(x => (azienda == "*" || x.Azienda.ToUpper() == azienda.ToUpper()) && (string.IsNullOrEmpty(searchVal) || x.CodArticolo.Contains(searchVal) || x.DescArticolo.Contains(searchVal) || x.Disegno.Contains(searchVal))) + .OrderBy(x => x.CodArticolo) + .Take(numRecord) + .ToListAsync(); } return dbResult; } @@ -1891,15 +1891,15 @@ namespace MP.Data.Controllers /// Recupero Odl CORRENTI /// /// - public List OdlGetCurrent() + public async Task> OdlGetCurrentAsync() { List dbResult = new List(); using (var dbCtx = new MoonProContext(options)) { - dbResult = dbCtx - .DbSetODL - .Where(x => x.DataInizio != null && x.DataFine == null) - .ToList(); + dbResult = await dbCtx + .DbSetODL + .Where(x => x.DataInizio != null && x.DataFine == null) + .ToListAsync(); } return dbResult; } diff --git a/MP.Data/Services/ListSelectDataSrv.cs b/MP.Data/Services/ListSelectDataSrv.cs index b541ee06..2e49fc21 100644 --- a/MP.Data/Services/ListSelectDataSrv.cs +++ b/MP.Data/Services/ListSelectDataSrv.cs @@ -70,10 +70,7 @@ namespace MP.Data.Services } else { - result = dbController.ArticoliGetSearch(numRecord, azienda, searchVal); -#if false - result = await Task.FromResult(dbController.ArticoliGetSearch(numRecord, azienda, searchVal)); -#endif + result = await dbController.ArticoliGetSearchAsync(numRecord, azienda, searchVal); // serializzp e salvo... rawData = JsonConvert.SerializeObject(result); await _redisDb.StringSetAsync(currKey, rawData, LongCache); @@ -83,7 +80,7 @@ namespace MP.Data.Services result = new List(); } sw.Stop(); - Log.Debug($"ArticoliGetSearch | azienda: {azienda} | searchVal: {searchVal} | numRecord: {numRecord} | {source} | {sw.Elapsed.TotalMilliseconds}ms"); + Log.Debug($"ArticoliGetSearchAsync | azienda: {azienda} | searchVal: {searchVal} | numRecord: {numRecord} | {source} | {sw.Elapsed.TotalMilliseconds}ms"); return result; } diff --git a/MP.SPEC/Components/ListDossiers.razor.cs b/MP.SPEC/Components/ListDossiers.razor.cs index 1da69a82..f26032dd 100644 --- a/MP.SPEC/Components/ListDossiers.razor.cs +++ b/MP.SPEC/Components/ListDossiers.razor.cs @@ -234,7 +234,7 @@ namespace MP.SPEC.Components ListStati = await MDService.AnagStatiComm(); selAzienda = await MDService.ConfigTryGetAsync("AZIENDA"); giacenzeConf = await MDService.ConfigTryGetAsync("SPEC_ShowGiacenze"); - ListArticoli = await MDService.ArticoliGetSearch(100000, selAzienda, ""); + ListArticoli = await MDService.ArticoliGetSearchAsync(100000, selAzienda, ""); ListMacchine = MDService.MacchineGetFilt("*"); await ReloadData(true); } diff --git a/MP.SPEC/Components/ListPODL.razor.cs b/MP.SPEC/Components/ListPODL.razor.cs index 29151abe..44897fbc 100644 --- a/MP.SPEC/Components/ListPODL.razor.cs +++ b/MP.SPEC/Components/ListPODL.razor.cs @@ -204,6 +204,9 @@ namespace MP.SPEC.Components ListRecords = null; isLoading = true; + var list = await MDService.OdlGetCurrentAsync(); + _odlCurrSet = list.ToHashSet(); + var machines = await MDService.MacchineGetFiltAsync("*"); _machinesWithConf = machines @@ -244,7 +247,7 @@ namespace MP.SPEC.Components { ListRecords = null; isLoading = true; - + await UpdateOdlList(); // verifico filtro odl... if (actFilter.ShowKit) { @@ -259,8 +262,7 @@ namespace MP.SPEC.Components totalCount = SearchRecords.Count; } ListRecords = SearchRecords.Skip(numRecord * (currPage - 1)).Take(numRecord).ToList(); - await Task.Delay(1); - await InvokeAsync(() => StateHasChanged()); + //await InvokeAsync(() => StateHasChanged()); isLoading = false; } @@ -356,6 +358,7 @@ namespace MP.SPEC.Components private static Logger Log = LogManager.GetCurrentClassLogger(); private HashSet _machinesWithArch = new(); private HashSet _machinesWithConf = new(); + private HashSet _odlCurrSet = new(); private string currRecipeArchPath = ""; /// @@ -567,18 +570,16 @@ namespace MP.SPEC.Components /// private bool canStartOdl(string idxMacchina) { - // controllo se lista scaduta... - bool answ = false; - DateTime adesso = DateTime.Now; - if (adesso > odlCurrExp || odlCurrList == null || odlCurrList.Count == 0) - { - odlCurrList = MDService.OdlGetCurrent(); - odlCurrExp = adesso.AddSeconds(2); - } - answ = !odlCurrList.Contains(idxMacchina); - return answ; + return !_odlCurrSet.Contains(idxMacchina); } + private async Task UpdateOdlList() + { + var list = await MDService.OdlGetCurrentAsync(); + _odlCurrSet = list.ToHashSet(); + } + + /// /// Verifica se la idxMaccSel abbia associata un path x ricette (elenco) /// diff --git a/MP.SPEC/Data/MpDataService.cs b/MP.SPEC/Data/MpDataService.cs index 5d7405d1..79499830 100644 --- a/MP.SPEC/Data/MpDataService.cs +++ b/MP.SPEC/Data/MpDataService.cs @@ -1,5 +1,6 @@ using EgwCoreLib.Utils; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; using MP.Core.Conf; using MP.Core.DTO; using MP.Core.Objects; @@ -20,10 +21,11 @@ namespace MP.SPEC.Data { #region Public Constructors - public MpDataService(IConfiguration configuration) + public MpDataService(IConfiguration configuration, IMemoryCache memoryCache) { - // fix oggetto configurazion + // salvataggio oggetti _configuration = configuration; + _memoryCache = memoryCache; // Verifica conf trace... traceEnabled = _configuration.GetValue("Otel:EnableTracing", false); Log.Info($"MpDataService | INIT | Trace enabled: {traceEnabled}"); @@ -298,7 +300,18 @@ namespace MP.SPEC.Data LogTrace($"AnagKeyValGetAll Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); return result; } + public async Task> AnagStatiComm() + { + return await GetOrCreateCachedAsync( + operationName: "AnagStatiComm", + memKey: "ANAG_STATI_COMM_MEM", + redisKey: Utils.redisStatoCom, + memoryTtl: TimeSpan.FromMinutes(5), + dbFactory: () => Task.FromResult(dbController.AnagStatiComm() ?? new List()) + ); + } +#if false public async Task> AnagStatiComm() { using var activity = ActivitySource.StartActivity("AnagStatiComm"); @@ -327,7 +340,8 @@ namespace MP.SPEC.Data activity?.Stop(); LogTrace($"AnagStatiComm Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); return result; - } + } +#endif public async Task> AnagTipoArtLV() { @@ -417,6 +431,25 @@ namespace MP.SPEC.Data /// /// /// + public async Task> ArticoliGetByTipoAsync(string tipo, string azienda = "*") + { + string sKey = string.IsNullOrWhiteSpace(tipo) ? "ALL" : tipo.Trim(); + + string redisKey = $"{Utils.redisArtList}:{azienda}:Tipo:{sKey}"; + string memKey = $"MEM:{redisKey}"; + + return await GetOrCreateCachedAsync( + operationName: "ArticoliGetByTipoAsync", + memKey: memKey, + redisKey: redisKey, + // ✅ TTL lungo (dato abbastanza statico) + memoryTtl: TimeSpan.FromMinutes(5), + dbFactory: async () => + await dbController.ArticoliGetByTipoAsync(tipo, azienda) + ?? new List() + ); + } +#if false public async Task> ArticoliGetByTipoAsync(string tipo, string azienda = "*") { using var activity = ActivitySource.StartActivity("ArticoliGetByTipoAsync"); @@ -447,7 +480,8 @@ namespace MP.SPEC.Data activity?.Stop(); LogTrace($"ArticoliGetByTipoAsync | Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); return result; - } + } +#endif /// /// Restitusice elenco articoli cercati @@ -455,38 +489,115 @@ namespace MP.SPEC.Data /// /// /// - public async Task> ArticoliGetSearch(int numRecord, string azienda, string searchVal) + public async Task> ArticoliGetSearchAsync(int numRecord, string azienda, string searchVal) { - using var activity = ActivitySource.StartActivity("ArticoliGetSearch"); - List? result = new List(); - string source = "DB"; - string sKey = string.IsNullOrEmpty(searchVal) ? "***" : searchVal; - string currKey = $"{Utils.redisArtList}:{azienda}:{sKey}"; - // cerco in redis dato valore sel idxMaccSel... - RedisValue rawData = redisDb.StringGet(currKey); - if (rawData.HasValue) + string sKey = string.IsNullOrWhiteSpace(searchVal) ? "***" : searchVal.Trim(); + + string memKey = $"ART_SEARCH_MEM:{azienda}:{sKey}:{numRecord}"; + string redisKey = $"{Utils.redisArtList}:{azienda}:{sKey}:{numRecord}"; + + return await GetOrCreateCachedAsync( + operationName: "ArticoliGetSearchAsync", + memKey: memKey, + redisKey: redisKey, + memoryTtl: TimeSpan.FromMinutes(2), + dbFactory: async () => + await dbController.ArticoliGetSearchAsync(numRecord, azienda, searchVal) + ?? new List() + ); + } + + + /// + /// Helper per gestione cache a 3 livelli: MEMORY, REDIS e DB con opzioni + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + private async Task GetOrCreateCachedAsync(string operationName, string memKey, string redisKey, TimeSpan memoryTtl, Func> dbFactory, Func? serialize = null, Func? deserialize = null) + { + using var activity = ActivitySource.StartActivity(operationName); + + string source = "NA"; + T result; + + // ✅ default serializer (fallback) + serialize ??= (obj) => JsonConvert.SerializeObject(obj); + deserialize ??= (str) => JsonConvert.DeserializeObject(str)!; + + // ✅ 1. MEMORY + if (_memoryCache.TryGetValue(memKey, out T cached)) { - result = JsonConvert.DeserializeObject>($"{rawData}"); - source = "REDIS"; + result = cached; + source = "MEMORY"; } else { - result = await Task.FromResult(dbController.ArticoliGetSearch(numRecord, azienda, searchVal)); - // serializzo e salvo... - rawData = JsonConvert.SerializeObject(result); - redisDb.StringSet(currKey, rawData, getRandTOut(redisLongTimeCache / 5)); - } - if (result == null) - { - result = new List(); + // ✅ 2. MISS → factory + result = await _memoryCache.GetOrCreateAsync(memKey, async entry => + { + entry.AbsoluteExpirationRelativeToNow = memoryTtl; + + // 👉 REDIS + try + { + var rawData = await redisDb.StringGetAsync(redisKey); + + if (rawData.HasValue) + { + source = "REDIS"; + return deserialize(rawData!); + } + } + catch (Exception ex) + { + LogTrace($"Redis error on {operationName}: {ex.Message}"); + } + + // 👉 DB + source = "DB"; + + var dbResult = await dbFactory(); + + var safeResult = dbResult == null ? default! : dbResult; + + try + { + await redisDb.StringSetAsync( + redisKey, + serialize(safeResult), + getRandTOut(redisLongTimeCache) + ); + } + catch (Exception ex) + { + LogTrace($"Redis SET error on {operationName}: {ex.Message}"); + } + + return safeResult; + })!; } + + // ✅ logging e tracing centralizzati activity?.SetTag("data.source", source); - activity?.SetTag("result.count", result.Count); - activity?.Stop(); - LogTrace($"ArticoliGetSearch | Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); + + if (result is System.Collections.ICollection coll) + activity?.SetTag("result.count", coll.Count); + else + activity?.SetTag("result.count", result != null ? 1 : 0); + + LogTrace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds}ms"); + return result; } + /// /// Aggiornamento record selezionato /// @@ -704,7 +815,7 @@ namespace MP.SPEC.Data activity?.SetTag("data.source", source); activity?.Stop(); - LogTrace($"ConfigTryGetAsync | {keyName} | Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); + LogTrace($"ConfigTryGetAsync | {keyName} | {source} | {activity?.Duration.TotalMilliseconds}ms"); return value ?? ""; } @@ -1687,7 +1798,7 @@ namespace MP.SPEC.Data LogTrace($"MacchineGetFilt | Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); return result; } - + private readonly IMemoryCache _memoryCache; /// /// Elenco di tutte le macchine filtrate x gruppo /// @@ -1695,32 +1806,51 @@ namespace MP.SPEC.Data /// public async Task> MacchineGetFiltAsync(string codGruppo) { - using var activity = ActivitySource.StartActivity("MacchineGetFilt"); - List? result = new List(); + using var activity = ActivitySource.StartActivity("MacchineGetFiltAsync"); string source = "DB"; + string keyGrp = codGruppo != "*" ? codGruppo : "ALL"; - string currKey = $"{Utils.redisMacList}:{keyGrp}"; - // cerco in redis dato valore sel idxMaccSel... - RedisValue rawData = await redisDb.StringGetAsync(currKey); + string memKey = $"MACCHINE_MEM:{keyGrp}"; + string redisKey = $"{Utils.redisMacList}:{keyGrp}"; + + // ✅ 1. MEMORY CACHE + if (_memoryCache.TryGetValue(memKey, out List cached)) + { + source = "MEMORY"; + activity?.SetTag("data.source", source); + activity?.Stop(); + LogTrace($"MacchineGetFiltAsync | Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); + return cached; + } + + List result; + + // ✅ 2. REDIS + var rawData = await redisDb.StringGetAsync(redisKey); + if (rawData.HasValue) { - result = JsonConvert.DeserializeObject>($"{rawData}"); + result = JsonConvert.DeserializeObject>(rawData!) ?? new(); source = "REDIS"; } else { + // ✅ 3. DB result = await dbController.MacchineGetFiltAsync(codGruppo); - // serializzo e salvo... - rawData = JsonConvert.SerializeObject(result); - await redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache)); - } - if (result == null) - { - result = new List(); + + await redisDb.StringSetAsync( + redisKey, + JsonConvert.SerializeObject(result), + getRandTOut(redisLongTimeCache) + ); } + + // ✅ salva in RAM (IMPORTANTISSIMO), TTL 1 minuto + _memoryCache.Set(memKey, result, TimeSpan.FromMinutes(1)); + activity?.SetTag("data.source", source); - activity?.SetTag("result.count", 1); activity?.Stop(); + LogTrace($"MacchineGetFiltAsync | Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); return result; } @@ -2040,6 +2170,32 @@ namespace MP.SPEC.Data /// /// /// + + public async Task> OdlGetCurrentAsync() + { + string redisKey = Utils.redisOdlCurrByMac; + string memKey = $"MEM:{redisKey}"; + + return await GetOrCreateCachedAsync( + operationName: "OdlGetCurrentAsync", + memKey: memKey, + redisKey: redisKey, + // ✅ TTL molto corto (come avevi: 3 secondi) + memoryTtl: TimeSpan.FromSeconds(3), + dbFactory: async () => + { + var rawData = await dbController.OdlGetCurrentAsync(); + var dbResult = rawData + .Select(x => x.IdxMacchina) + .Distinct() + .ToList(); + + return dbResult ?? new List(); + } + ); + } + +#if false public List OdlGetCurrent() { using var activity = ActivitySource.StartActivity("OdlGetCurrent"); @@ -2068,7 +2224,8 @@ namespace MP.SPEC.Data activity?.Stop(); LogTrace($"OdlGetCurrent | Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); return dbResult; - } + } +#endif /// /// elenco TUTTI gli ODL @@ -2479,6 +2636,31 @@ namespace MP.SPEC.Data /// Data inizio /// Data fine /// + public async Task> POdlToKitListGetFiltAsync(bool lanciato, string keyRichPart, string idxMacchina, string codGruppo, DateTime startDate, DateTime endDate) + { + string currKey = $"{Utils.redisPOdlList}_kit:{codGruppo}:{idxMacchina}:{keyRichPart}:{lanciato}:{startDate:yyyyMMdd_HHmmss}:{endDate:yyyyMMdd_HHmmss}"; + + // ✅ stessa chiave per memoria (puoi anche prefissare) + string memKey = $"MEM:{currKey}"; + + return await GetOrCreateCachedAsync( + operationName: "POdlToKitListGetFiltAsync", + memKey: memKey, + redisKey: currKey, + // ✅ TTL RAM breve (coerente con redisShortTimeCache) + memoryTtl: TimeSpan.FromSeconds(redisShortTimeCache), + dbFactory: async () => + await dbController.ListPODL_KitFiltAsync( + lanciato, + keyRichPart, + idxMacchina, + codGruppo, + startDate, + endDate + ) ?? new List() + ); + } +#if false public async Task> POdlToKitListGetFiltAsync(bool lanciato, string keyRichPart, string idxMacchina, string codGruppo, DateTime startDate, DateTime endDate) { using var activity = ActivitySource.StartActivity("POdlToKitListGetFiltAsync"); @@ -2508,7 +2690,8 @@ namespace MP.SPEC.Data activity?.Stop(); LogTrace($"POdlToKitListGetFiltAsync | Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); return result; - } + } +#endif /// /// Chiamata salvataggio ricetta + refresh REDIS diff --git a/MP.SPEC/MP.SPEC.csproj b/MP.SPEC/MP.SPEC.csproj index e978a1c1..8ce4107b 100644 --- a/MP.SPEC/MP.SPEC.csproj +++ b/MP.SPEC/MP.SPEC.csproj @@ -5,7 +5,7 @@ enable enable MP.SPEC - 8.16.2605.2617 + 8.16.2605.2619 1800a78a-6ff1-40f9-b490-87fb8bfc1394 en diff --git a/MP.SPEC/Pages/Articoli.razor.cs b/MP.SPEC/Pages/Articoli.razor.cs index e3b23914..008bbc19 100644 --- a/MP.SPEC/Pages/Articoli.razor.cs +++ b/MP.SPEC/Pages/Articoli.razor.cs @@ -335,7 +335,7 @@ namespace MP.SPEC.Pages private async Task ReloadData() { isLoading = true; - SearchRecords = await MDService.ArticoliGetSearch(100000, selAzienda, SearchVal); + SearchRecords = await MDService.ArticoliGetSearchAsync(100000, selAzienda, SearchVal); ListRecords = SearchRecords.Skip(numRecord * (currPage - 1)).Take(numRecord).ToList(); isLoading = false; } diff --git a/MP.SPEC/Pages/PODL.razor.cs b/MP.SPEC/Pages/PODL.razor.cs index ce20588b..23054a1a 100644 --- a/MP.SPEC/Pages/PODL.razor.cs +++ b/MP.SPEC/Pages/PODL.razor.cs @@ -1,17 +1,14 @@ #if false using Blazored.LocalStorage; #endif +using EgwCoreLib.Razor; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; using MP.Data.DbModels; -using MP.SPEC.Components; +using MP.Data.Services; using MP.SPEC.Data; using MP.SPEC.Services; using NLog; -using System.Reflection.PortableExecutable; -using EgwCoreLib.Razor; -using Microsoft.AspNetCore.DataProtection; -using MP.Data.Services; namespace MP.SPEC.Pages { @@ -501,9 +498,8 @@ namespace MP.SPEC.Pages private async Task ReloadData() { isLoading = true; - await Task.Delay(1); - ListMacchine = MDService.MacchineGetFilt(selReparto); - ListArticoli = await MDService.ArticoliGetSearch(100, currAzienda, artSearch); + ListMacchine = await MDService.MacchineGetFiltAsync(selReparto); + ListArticoli = await MDService.ArticoliGetSearchAsync(100, currAzienda, artSearch); if (ListGruppiFase != null) { var firstGroup = ListGruppiFase.Where(x => x.CodGruppo == selReparto).FirstOrDefault(); diff --git a/MP.SPEC/Program.cs b/MP.SPEC/Program.cs index b91a66a0..5b8365ff 100644 --- a/MP.SPEC/Program.cs +++ b/MP.SPEC/Program.cs @@ -132,9 +132,13 @@ builder.Services.AddAuthorization(options => builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); -// redis preliminare -builder.Services.AddSingleton(redisMultiplexer); builder.Services.AddRazorPages(); + +// memory + redis preliminare +builder.Services.AddMemoryCache(); +builder.Services.AddSingleton(redisMultiplexer); + +// altri servizi builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/MP.SPEC/Resources/ChangeLog.html b/MP.SPEC/Resources/ChangeLog.html index 323e79be..dca5987a 100644 --- a/MP.SPEC/Resources/ChangeLog.html +++ b/MP.SPEC/Resources/ChangeLog.html @@ -1,6 +1,6 @@ Modulo MAPOSPEC -

Versione: 8.16.2605.2617

+

Versione: 8.16.2605.2619


Note di rilascio:
  • diff --git a/MP.SPEC/Resources/VersNum.txt b/MP.SPEC/Resources/VersNum.txt index e207479d..5ea9eaa9 100644 --- a/MP.SPEC/Resources/VersNum.txt +++ b/MP.SPEC/Resources/VersNum.txt @@ -1 +1 @@ -8.16.2605.2617 +8.16.2605.2619 diff --git a/MP.SPEC/Resources/manifest.xml b/MP.SPEC/Resources/manifest.xml index 68083a5c..b079c1c6 100644 --- a/MP.SPEC/Resources/manifest.xml +++ b/MP.SPEC/Resources/manifest.xml @@ -1,6 +1,6 @@ - 8.16.2605.2617 + 8.16.2605.2619 https://nexus.steamware.net/repository/SWS/MP-SPEC/stable/LAST/MP.SPEC.zip https://nexus.steamware.net/repository/SWS/MP-SPEC/stable/LAST/ChangeLog.html false