diff --git a/MP.Data/Repository/IOC/IIocRepository.cs b/MP.Data/Repository/IOC/IIocRepository.cs index cfa5824f..19ad3f4a 100644 --- a/MP.Data/Repository/IOC/IIocRepository.cs +++ b/MP.Data/Repository/IOC/IIocRepository.cs @@ -102,6 +102,14 @@ namespace MP.Data.Repository.IOC /// Task> StateMachineIngressiAsync(int idxFam); + /// + /// Stato prod macchina (completo) Async + /// + /// + /// + /// + Task StatoProdMacchinaAsync(string idxMacchina, DateTime dtReq); + /// /// Vista v_MSFD x singola macchina (da stored) - singolo record /// diff --git a/MP.Data/Repository/IOC/IocRepository.cs b/MP.Data/Repository/IOC/IocRepository.cs index de11638e..2a13858d 100644 --- a/MP.Data/Repository/IOC/IocRepository.cs +++ b/MP.Data/Repository/IOC/IocRepository.cs @@ -316,6 +316,23 @@ namespace MP.Data.Repository.IOC return dbResult; } + /// + public async Task StatoProdMacchinaAsync(string idxMacchina, DateTime dtReq) + { + await using var dbCtx = await CreateContextAsync(); + + var IdxMacchina = new SqlParameter("@IdxMacchina", idxMacchina); + var DataOra = new SqlParameter("@DataOra ", dtReq); + var dbResult = (await dbCtx + .DbSetStatoProd + .FromSqlRaw("EXEC stp_StatoProd_getByMacchina @IdxMacchina, @DataOra ", IdxMacchina, DataOra) + .AsNoTracking() + .ToListAsync()) + .FirstOrDefault(); + + return dbResult; + } + /// public async Task VMSFDGetByMaccAsync(string idxMacc) { diff --git a/MP.Data/Services/IOC/IIocService.cs b/MP.Data/Services/IOC/IIocService.cs index 3f3b48a0..8e7f6a76 100644 --- a/MP.Data/Services/IOC/IIocService.cs +++ b/MP.Data/Services/IOC/IIocService.cs @@ -1,5 +1,6 @@ using MP.Data.DbModels; using System; +using System.Collections.Generic; using System.Threading.Tasks; using static MP.Core.Objects.Enums; @@ -31,6 +32,21 @@ namespace MP.Data.Services.IOC /// Task EvListMicroStatoInsertAsync(MicroStatoMacchinaModel newRecMsm, EventListModel newRecEv); + /// + /// Restituisce il valOut dell'ODL corrente (ODL deve esserci per gestione contapezzi, senza + /// ODL NO invio/gestione ODL) + /// + /// + /// + Task GetCurrOdlAsync(string idxMacchina); + + /// + /// Restitusice elenco KVP dei TASK (da passare a IOB-WIN) per l'impianto indicato + /// + /// + /// + Task> GetTask2ExeMacchinaAsync(string idxMacchina); + /// /// Restituisce il valOut booleano se la macchina sia abilitata all'input /// @@ -56,6 +72,21 @@ namespace MP.Data.Services.IOC /// Task ProcessInputAsync(string idxMacchina, string valore, string dtEve, string dtCurr, string contatore); + /// + /// Restituisce il contapezzi come CONTEGGIO da TCRilevati per la macchina - ASYNC + /// + /// + /// + Task PzCounterTcAsync(string idxMacchina); + + /// + /// Processa registrazione di un counter x una data macchina IOB + /// + /// + /// contapezzi + /// + Task SaveCounterAsync(string idxMacchina, string counter); + /// /// scrive un evento di keepalive sulla tabella /// diff --git a/MP.Data/Services/IOC/IocService.cs b/MP.Data/Services/IOC/IocService.cs index 8c5b9407..d4041c5f 100644 --- a/MP.Data/Services/IOC/IocService.cs +++ b/MP.Data/Services/IOC/IocService.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using MP.Core.Objects; using MP.Data.DbModels; @@ -23,13 +24,15 @@ namespace MP.Data.Services.IOC IConfiguration config, IConnectionMultiplexer redis, IIocRepository repo, - IServiceScopeFactory scopeFactory) : base(config, redis) + IServiceScopeFactory scopeFactory, + Microsoft.Extensions.Caching.Memory.IMemoryCache cache) : base(config, redis) { _className = "IocServ"; int.TryParse(config.GetValue("ServerConf:redisLongTimeCache"), out redisLongTimeCache); int.TryParse(config.GetValue("ServerConf:redisShortTimeCache"), out redisShortTimeCache); _repo = repo; _scopeFactory = scopeFactory; + _cache = cache; } #endregion Public Constructors @@ -50,20 +53,52 @@ namespace MP.Data.Services.IOC return success; } + /// + public async Task GetCurrOdlAsync(string idxMacchina) + { + string result = ""; + string currKey = $"{MP.Data.Utils.redisOdlCurrByMac}:{idxMacchina}"; + // cerco in redis dato valOut sel macchina... + RedisValue rawData = _redisDb.StringGet(currKey); + if (rawData.HasValue) + { + result = $"{rawData}"; + } + else + { + result = await GetCurrOdlByProdAsync(idxMacchina); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + _redisDb.StringSet(currKey, rawData, getRandTOut(redisLongTimeCache)); + } + return result; + } + + /// + public async Task> GetTask2ExeMacchinaAsync(string idxMacchina) + { + var currHash = MP.Data.Utils.RedKeyTask2ExeMacc(idxMacchina, MpIoNS); + return await RedisGetHashDictAsync(currHash); + } + /// public async Task IobInsEnabAsync(string idxMacchina) { - var key = MP.Data.Utils.RedKeyDatiMacc(idxMacchina, MpIoNS); - - string? val = await _redisDb.HashGetAsync(key, "insEnabled"); - - if (val == null) + string cacheKey = $"IOC_IobInsEnab_{idxMacchina}"; + return await GetOrFetchAsync(cacheKey, async () => { - var data = await ResetDatiMacchinaAsync(idxMacchina); - data.TryGetValue("insEnabled", out val); - } + var key = MP.Data.Utils.RedKeyDatiMacc(idxMacchina, MpIoNS); - return val != null && (val == "1" || val.ToLower() == "true"); + string? val = await _redisDb.HashGetAsync(key, "insEnabled"); + + if (val == null) + { + var data = await ResetDatiMacchinaAsync(idxMacchina); + data.TryGetValue("insEnabled", out val); + } + + return val != null && (val == "1" || val.ToLower() == "true"); + }, TimeSpan.FromSeconds(5)); } /// @@ -77,7 +112,7 @@ namespace MP.Data.Services.IOC public async Task ProcessInputAsync(string idxMacchina, string valore, string dtEve, string dtCurr, string contatore) { string answ = ""; - if (ValidateinputParams(idxMacchina, valore, dtEve, dtCurr)) + if (ValidateInputParams(idxMacchina, valore, dtEve, dtCurr)) { DateTime dataOraEvento = ParseEventTime(dtEve, dtCurr); @@ -130,6 +165,66 @@ namespace MP.Data.Services.IOC return answ; } + /// + public async Task PzCounterTcAsync(string idxMacchina) + { + int answ = -1; + DateTime dataRif = DateTime.Now; + var datiProd = await StatoProdMacchinaAsync(idxMacchina, dataRif); + if (datiProd != null) + { + answ = datiProd.PzTotODL; + } + return answ; + } + + /// + public async Task SaveCounterAsync(string idxMacchina, string counter) + { + string answ = "0"; + // inizio processing vero e proprio INPUT... + if (string.IsNullOrEmpty(idxMacchina)) + { + string errore = "Errore: parametro macchina vuoto"; + Log.Warn(errore); + answ = errore; + } + else + { + if (string.IsNullOrEmpty(counter)) + { + string errore = "Errore: parametro counter vuoto"; + Log.Warn(errore); + answ = errore; + } + else + { + int newCounter = -1; + int.TryParse(counter, out newCounter); + // se il conteggio è >= 0 SALVO come nuovo conteggio... + if (newCounter >= 0) + { + var currKey = MP.Data.Utils.RedKeyPzCount(idxMacchina, MpIoNS); + RedisValue rawData = await _redisDb.StringGetAsync(currKey); + if (rawData.HasValue) + { + // salvo per + tempo... + await _redisDb.StringSetAsync(currKey, answ.ToString()); + answ = counter; + } + else + { + int currCount = await PzCounterTcAsync(idxMacchina); + answ = currCount.ToString(); + // salvo per meno tempo... + await _redisDb.StringSetAsync(currKey, answ); + } + } + } + } + return answ; + } + /// public async Task ScriviKeepAliveAsync(string IdxMacchina, DateTime oraMacchina) { @@ -173,14 +268,15 @@ namespace MP.Data.Services.IOC private static Logger Log = LogManager.GetCurrentClassLogger(); + private readonly Microsoft.Extensions.Caching.Memory.IMemoryCache _cache; private readonly string _className; private readonly IIocRepository _repo; - private readonly IServiceScopeFactory _scopeFactory; + private readonly System.Threading.SemaphoreSlim _semaphore = new System.Threading.SemaphoreSlim(1, 1); /// - /// Provider CultureInfo x parse valori (es dataora) + /// Provider CultureInfo x parse valori(es dataora) /// private CultureInfo ciProvider = CultureInfo.InvariantCulture; @@ -294,32 +390,34 @@ namespace MP.Data.Services.IOC /// /// Elenco completo config da DB /// - /// private async Task> ConfigGetAllAsync() { - List? result = new List(); - // cerco in redis... - RedisValue rawData = await _redisDb.StringGetAsync(MP.Data.Utils.redisConfKey); - string source = "DB"; - if (!string.IsNullOrEmpty($"{rawData}")) + return await GetOrFetchAsync("IOC_ConfigAll", async () => { - result = JsonConvert.DeserializeObject>($"{rawData}"); - source = "REDIS"; - } - else - { - result = await _repo.ConfigGetAllAsync(); - //result = await Task.FromResult(SpecDbController.ConfigGetAllAsync()); - // serializzo e salvo... - rawData = JsonConvert.SerializeObject(result); - await _redisDb.StringSetAsync(MP.Data.Utils.redisConfKey, rawData, getRandTOut(redisLongTimeCache)); - } - Log.Debug($"ConfigGetAllAsync Read from {source}"); - if (result == null) - { - result = new List(); - } - return result; + List? result = new List(); + // cerco in redis... + RedisValue rawData = await _redisDb.StringGetAsync(MP.Data.Utils.redisConfKey); + string source = "DB"; + if (!string.IsNullOrEmpty($"{rawData}")) + { + result = JsonConvert.DeserializeObject>($"{rawData}"); + source = "REDIS"; + } + else + { + result = await _repo.ConfigGetAllAsync(); + //result = await Task.FromResult(SpecDbController.ConfigGetAllAsync()); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + await _redisDb.StringSetAsync(MP.Data.Utils.redisConfKey, rawData, getRandTOut(redisLongTimeCache)); + } + Log.Debug($"ConfigGetAllAsync Read from {source}"); + if (result == null) + { + result = new List(); + } + return result; + }, TimeSpan.FromMinutes(10)); } /// @@ -332,6 +430,59 @@ namespace MP.Data.Services.IOC return s?.Length > 17 ? s[..17] : s?.PadRight(17, '0') ?? string.Empty; } + /// + /// Recupero info ODL corrente da dati prod macchina + /// + /// + /// + private async Task GetCurrOdlByProdAsync(string idxMacchina) + { + string answ = ""; + // recupero stato... + var datiProd = await StatoProdMacchinaAsync(idxMacchina, DateTime.Now); + if (datiProd != null) + { + answ = datiProd.IdxOdl.ToString(); + } + // ultimo controllo su idxOdl... + answ = answ == "" ? "0" : answ; + // restituisco! + return answ; + } + + /// + /// Implementa gestione recupero cache da memoria o da obj esterno + cache memoria + /// + /// + /// + /// + /// + /// + private async Task GetOrFetchAsync(string cacheKey, Func> fetchFunc, TimeSpan expiration) + { + if (_cache.TryGetValue(cacheKey, out T? cachedValue)) + { + return cachedValue!; + } + + await _semaphore.WaitAsync(); + try + { + if (_cache.TryGetValue(cacheKey, out cachedValue)) + { + return cachedValue!; + } + + T newValue = await fetchFunc(); + _cache.Set(cacheKey, newValue, expiration); + return newValue; + } + finally + { + _semaphore.Release(); + } + } + /// /// Restituisce il valOut booleano se la macchina sia abilitata all'inserimento COMPLETO nel /// Signal Log @@ -387,42 +538,46 @@ namespace MP.Data.Services.IOC private async Task> ListMasterAsync() { - HashSet result = new(); - string currKey = $"{MP.Data.Utils.redisBaseAddr}:ListMaster"; - RedisValue rawData = await _redisDb.StringGetAsync(currKey); - if (rawData.HasValue) + return await GetOrFetchAsync("IOC_ListMaster", async () => { - result = JsonConvert.DeserializeObject>($"{rawData}") ?? new(); - } - else - { - var fullList = await Macchine2SlaveGetAllAsync(); - result = fullList.Select(x => x.IdxMacchina).ToHashSet(); - // serializzo e salvo... - rawData = JsonConvert.SerializeObject(result); - await _redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache * 10)); - } - return result; + HashSet result = new(); + string currKey = $"{MP.Data.Utils.redisBaseAddr}:ListMaster"; + RedisValue rawData = await _redisDb.StringGetAsync(currKey); + if (rawData.HasValue) + { + result = JsonConvert.DeserializeObject>($"{rawData}") ?? new(); + } + else + { + var fullList = await Macchine2SlaveGetAllAsync(); + result = fullList.Select(x => x.IdxMacchina).ToHashSet(); + rawData = JsonConvert.SerializeObject(result); + await _redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache * 10)); + } + return result; + }, TimeSpan.FromMinutes(5)); } private async Task> ListSlaveAsync() { - HashSet result = new(); - string currKey = $"{MP.Data.Utils.redisBaseAddr}:ListSlave"; - RedisValue rawData = await _redisDb.StringGetAsync(currKey); - if (rawData.HasValue) + return await GetOrFetchAsync("IOC_ListSlave", async () => { - result = JsonConvert.DeserializeObject>($"{rawData}") ?? new(); - } - else - { - var fullList = await Macchine2SlaveGetAllAsync(); - result = fullList.Select(x => x.IdxMacchinaSlave).Distinct().ToHashSet(); - // serializzo e salvo... - rawData = JsonConvert.SerializeObject(result); - await _redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache * 10)); - } - return result; + HashSet result = new(); + string currKey = $"{MP.Data.Utils.redisBaseAddr}:ListSlave"; + RedisValue rawData = await _redisDb.StringGetAsync(currKey); + if (rawData.HasValue) + { + result = JsonConvert.DeserializeObject>($"{rawData}") ?? new(); + } + else + { + var fullList = await Macchine2SlaveGetAllAsync(); + result = fullList.Select(x => x.IdxMacchinaSlave).Distinct().ToHashSet(); + rawData = JsonConvert.SerializeObject(result); + await _redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache * 10)); + } + return result; + }, TimeSpan.FromMinutes(5)); } /// @@ -431,26 +586,29 @@ namespace MP.Data.Services.IOC /// private async Task> Macchine2SlaveGetAllAsync() { - List? result = new List(); - string currKey = $"{MP.Data.Utils.redisBaseAddr}:M2STab"; - // cerco in redis dato valOut sel macchina... - RedisValue rawData = await _redisDb.StringGetAsync(currKey); - if (rawData.HasValue) + return await GetOrFetchAsync("IOC_M2SlaveGetAll", async () => { - result = JsonConvert.DeserializeObject>($"{rawData}"); - } - else - { - result = await _repo.Macchine2SlaveAsync(); - // serializzo e salvo... - rawData = JsonConvert.SerializeObject(result); - await _redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache * 10)); - } - if (result == null) - { - result = new List(); - } - return result; + List? result = new List(); + string currKey = $"{MP.Data.Utils.redisBaseAddr}:M2STab"; + // cerco in redis dato valOut sel macchina... + RedisValue rawData = await _redisDb.StringGetAsync(currKey); + if (rawData.HasValue) + { + result = JsonConvert.DeserializeObject>($"{rawData}"); + } + else + { + result = await _repo.Macchine2SlaveAsync(); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + await _redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache * 10)); + } + if (result == null) + { + result = new List(); + } + return result; + }, TimeSpan.FromMinutes(15)); } /// @@ -847,6 +1005,40 @@ namespace MP.Data.Services.IOC return answ; } + /// + /// Stato prod macchina (completo) + /// + /// + /// + /// + private async Task StatoProdMacchinaAsync(string idxMacchina, DateTime dtReq, bool forceDb = false) + { + string cacheKey = $"IOC_StatoProd_{idxMacchina}"; + return await GetOrFetchAsync(cacheKey, async () => + { + StatoProdModel? result = new StatoProdModel(); + // cerco in _redisConn... + string currKey = $"{MP.Data.Utils.redisStatoProd}:{idxMacchina}:{dtReq:HHmm}"; + RedisValue rawData = await _redisDb.StringGetAsync(currKey); + if (rawData.HasValue && !forceDb) + { + result = JsonConvert.DeserializeObject($"{rawData}"); + } + else + { + result = await _repo.StatoProdMacchinaAsync(idxMacchina, dtReq); + // serializzo e salvo... + rawData = JsonConvert.SerializeObject(result); + await _redisDb.StringSetAsync(currKey, rawData, TimeSpan.FromSeconds(30)); + } + if (result == null) + { + result = new StatoProdModel(); + } + return result; + }, TimeSpan.FromSeconds(10)); + } + /// /// Restituisce valOut della stringa (SE disponibile) /// @@ -873,7 +1065,7 @@ namespace MP.Data.Services.IOC /// /// /// - private bool ValidateinputParams(string idxMacchina, string valore, string dtEve, string dtCurr) + private bool ValidateInputParams(string idxMacchina, string valore, string dtEve, string dtCurr) { bool isValid = false; // preparo stringa valori correnti diff --git a/MP.IOC/Controllers/IOBController.cs b/MP.IOC/Controllers/IOBController.cs index 7df179a4..4a486ff9 100644 --- a/MP.IOC/Controllers/IOBController.cs +++ b/MP.IOC/Controllers/IOBController.cs @@ -70,7 +70,7 @@ namespace MP.IOC.Controllers } catch (Exception exc) { - Log.Error($"Errore in GetTask2Exe{Environment.NewLine}{exc}"); + Log.Error($"Errore in AddTask2Exe{Environment.NewLine}{exc}"); return StatusCode(StatusCodes.Status500InternalServerError, "NO"); } return Ok(answ); @@ -100,7 +100,6 @@ namespace MP.IOC.Controllers { // Il metodo ora restituisce direttamente il booleano logico bool isEnabled = await IOCService.IobInsEnabAsync(id); - //bool isEnabled = await DService.IobInsEnabAsync(id); return isEnabled ? Ok("OK") @@ -430,7 +429,8 @@ namespace MP.IOC.Controllers id = id.Replace("|", "#"); try { - var pzCount = await DService.pzCounterTcAsync(id); + var pzCount = await IOCService.PzCounterTcAsync(id); + //var pzCount = await DService.PzCounterTcAsync(id); return Ok(pzCount); } catch (Exception exc) @@ -479,7 +479,8 @@ namespace MP.IOC.Controllers id = id.Replace("|", "#"); try { - var odl = await DService.GetCurrOdlAsync(id); + var odl = await IOCService.GetCurrOdlAsync(id); + //var odl = await DService.GetCurrOdlAsync(id); return Ok($"{odl}"); } catch (Exception exc) @@ -816,7 +817,8 @@ namespace MP.IOC.Controllers try { await DService.ScriviKeepAliveAsync(id, DateTime.UtcNow); - var valori = await DService.GetTask2ExeMacchinaAsync(id); + var valori = await IOCService.GetTask2ExeMacchinaAsync(id); + //var valori = await DService.GetTask2ExeMacchinaAsync(id); if (valori == null || valori.Count == 0) return Ok(new Dictionary()); @@ -1207,8 +1209,7 @@ namespace MP.IOC.Controllers /// /// SALVA Counter x macchina restituendo il valore appena inviato o, se mancasse chiave - /// redis, del valore da DB - /// + /// redis, valore da DB /// /// GET: IOB/setCounter/5?counter=10 /// /// cod macchina @@ -1227,7 +1228,8 @@ namespace MP.IOC.Controllers Log.Debug($"Salvataggio counter | id: {id} | pzCount: {counter}"); try { - answ = await DService.saveCounterAsync(id, counter); + answ = await IOCService.SaveCounterAsync(id, counter); + //answ = await DService.SaveCounterAsync(id, counter); } catch (Exception exc) { diff --git a/MP.IOC/Data/MpDataService.cs b/MP.IOC/Data/MpDataService.cs index 6dc5d273..abce85b4 100644 --- a/MP.IOC/Data/MpDataService.cs +++ b/MP.IOC/Data/MpDataService.cs @@ -3147,7 +3147,7 @@ namespace MP.IOC.Data } else { - answ = await pzCounterTcAsync(idxMacchina); + answ = await PzCounterTcAsync(idxMacchina); // salvo in _redisConn... await redisDb.StringSetAsync(currKey, answ.ToString(), TimeSpan.FromSeconds(1)); } @@ -3164,7 +3164,7 @@ namespace MP.IOC.Data /// /// /// - public async Task pzCounterTcAsync(string idxMacchina) + public async Task PzCounterTcAsync(string idxMacchina) { int answ = -1; DateTime dataRif = DateTime.Now; @@ -3741,7 +3741,7 @@ namespace MP.IOC.Data /// /// contapezzi /// - public async Task saveCounterAsync(string idxMacchina, string counter) + public async Task SaveCounterAsync(string idxMacchina, string counter) { string answ = "0"; // inizio processing vero e proprio INPUT... @@ -3776,7 +3776,7 @@ namespace MP.IOC.Data } else { - int currCount = await pzCounterTcAsync(idxMacchina); + int currCount = await PzCounterTcAsync(idxMacchina); answ = currCount.ToString(); // salvo per meno tempo... await redisDb.StringSetAsync(currKey, answ); diff --git a/MP.IOC/MP.IOC.csproj b/MP.IOC/MP.IOC.csproj index 09b2a86c..216a00d5 100644 --- a/MP.IOC/MP.IOC.csproj +++ b/MP.IOC/MP.IOC.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 8.16.2605.415 + 8.16.2605.610 diff --git a/MP.IOC/Program.cs b/MP.IOC/Program.cs index 207578c7..c0ccbfda 100644 --- a/MP.IOC/Program.cs +++ b/MP.IOC/Program.cs @@ -67,6 +67,8 @@ logger.Info("RedisScript Provider configured"); var connStr = builder.Configuration.GetConnectionString("MP.Data") ?? throw new InvalidOperationException("ConnString 'MP.Data' mancante."); +builder.Services.AddMemoryCache(); + builder.Services.AddDbContextFactory(options => options.UseSqlServer(connStr) .EnableSensitiveDataLogging(false) // true solo in Sviluppo diff --git a/MP.IOC/Resources/ChangeLog.html b/MP.IOC/Resources/ChangeLog.html index e967c5a4..e707de72 100644 --- a/MP.IOC/Resources/ChangeLog.html +++ b/MP.IOC/Resources/ChangeLog.html @@ -1,6 +1,6 @@ Modulo MP-IOC -

Versione: 8.16.2605.415

+

Versione: 8.16.2605.610


Note di rilascio:
  • diff --git a/MP.IOC/Resources/VersNum.txt b/MP.IOC/Resources/VersNum.txt index c9e2711c..311dfe39 100644 --- a/MP.IOC/Resources/VersNum.txt +++ b/MP.IOC/Resources/VersNum.txt @@ -1 +1 @@ -8.16.2605.415 +8.16.2605.610 diff --git a/MP.IOC/Resources/manifest.xml b/MP.IOC/Resources/manifest.xml index 39fb4c88..3dbadca7 100644 --- a/MP.IOC/Resources/manifest.xml +++ b/MP.IOC/Resources/manifest.xml @@ -1,6 +1,6 @@ - 8.16.2605.415 + 8.16.2605.610 https://nexus.steamware.net/repository/SWS/MP-IOC/stable/LAST/MP.IOC.zip https://nexus.steamware.net/repository/SWS/MP-IOC/stable/LAST/ChangeLog.html false