using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Dalamud.Data.LuminaExtensions; using Lumina.Data; using Lumina.Data.Files; using Lumina.Excel; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Serilog; using LuminaOptions = Lumina.LuminaOptions; using ParsedFilePath = Lumina.ParsedFilePath; namespace Dalamud.Data { /// /// This class provides data for Dalamud-internal features, but can also be used by plugins if needed. /// public class DataManager : IDisposable { /// /// OpCodes sent by the server to the client. /// public ReadOnlyDictionary ServerOpCodes { get; private set; } /// /// OpCodes sent by the client to the server. /// public ReadOnlyDictionary ClientOpCodes { get; private set; } /// /// An object which gives access to any of the game's sheet data. /// public ExcelModule Excel => this.gameData?.Excel; /// /// Indicates whether Game Data is ready to be read. /// public bool IsDataReady { get; private set; } /// /// A object which gives access to any excel/game data. /// private Lumina.Lumina gameData; private ClientLanguage language; private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex"; private Thread luminaResourceThread; public DataManager(ClientLanguage language) { // Set up default values so plugins do not null-reference when data is being loaded. this.ServerOpCodes = new ReadOnlyDictionary(new Dictionary()); this.language = language; } public async Task Initialize(string baseDir) { try { Log.Verbose("Starting data load..."); var zoneOpCodeDict = JsonConvert.DeserializeObject>(File.ReadAllText(Path.Combine(baseDir, "UIRes", "serveropcode.json"))); ServerOpCodes = new ReadOnlyDictionary(zoneOpCodeDict); Log.Verbose("Loaded {0} ServerOpCodes.", zoneOpCodeDict.Count); var clientOpCodeDict = JsonConvert.DeserializeObject>(File.ReadAllText(Path.Combine(baseDir, "UIRes", "clientopcode.json"))); ClientOpCodes = new ReadOnlyDictionary(clientOpCodeDict); Log.Verbose("Loaded {0} ClientOpCodes.", clientOpCodeDict.Count); var luminaOptions = new LuminaOptions { CacheFileResources = true }; luminaOptions.DefaultExcelLanguage = this.language switch { ClientLanguage.Japanese => Language.Japanese, ClientLanguage.English => Language.English, ClientLanguage.German => Language.German, ClientLanguage.French => Language.French, _ => throw new ArgumentOutOfRangeException(nameof(this.language), "Unknown Language: " + this.language) }; gameData = new Lumina.Lumina(Path.Combine(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName), "sqpack"), luminaOptions); Log.Information("Lumina is ready: {0}", gameData.DataPath); IsDataReady = true; this.luminaResourceThread = new Thread( () => { while( true ) { gameData.ProcessFileHandleQueue(); Thread.Yield(); } // ReSharper disable once FunctionNeverReturns }); this.luminaResourceThread.Start(); } catch (Exception ex) { Log.Error(ex, "Could not download data."); } } #region Lumina Wrappers /// /// Get an with the given Excel sheet row type. /// /// The excel sheet type to get. /// The , giving access to game rows. public ExcelSheet GetExcelSheet() where T : IExcelRow { return this.Excel.GetSheet(); } /// /// Get an with the given Excel sheet row type with a specified language. /// /// Language of the sheet to get. /// The excel sheet type to get. /// The , giving access to game rows. public ExcelSheet GetExcelSheet(ClientLanguage language) where T : IExcelRow { var lang = language switch { ClientLanguage.Japanese => Language.Japanese, ClientLanguage.English => Language.English, ClientLanguage.German => Language.German, ClientLanguage.French => Language.French, _ => throw new ArgumentOutOfRangeException(nameof(this.language), "Unknown Language: " + this.language) }; return this.Excel.GetSheet(lang); } /// /// Get a with the given path. /// /// The path inside of the game files. /// The of the file. public FileResource GetFile(string path) { return this.GetFile(path); } /// /// Get a with the given path, of the given type. /// /// The type of resource /// The path inside of the game files. /// The of the file. public T GetFile(string path) where T : FileResource { ParsedFilePath filePath = Lumina.Lumina.ParseFilePath(path); if (filePath == null) return default(T); Repository repository; return this.gameData.Repositories.TryGetValue(filePath.Repository, out repository) ? repository.GetFile(filePath.Category, filePath) : default(T); } /// /// Get a containing the icon with the given ID. /// /// The icon ID. /// The containing the icon. public TexFile GetIcon(int iconId) { return GetIcon(this.language, iconId); } /// /// Get a containing the icon with the given ID, of the given language. /// /// The requested language. /// The icon ID. /// The containing the icon. public TexFile GetIcon(ClientLanguage iconLanguage, int iconId) { var type = iconLanguage switch { ClientLanguage.Japanese => "ja/", ClientLanguage.English => "en/", ClientLanguage.German => "de/", ClientLanguage.French => "fr/", _ => throw new ArgumentOutOfRangeException(nameof(this.language), "Unknown Language: " + this.language) }; return GetIcon(type, iconId); } /// /// Get a containing the icon with the given ID, of the given type. /// /// The type of the icon (e.g. 'hq' to get the HQ variant of an item icon). /// The icon ID. /// The containing the icon. public TexFile GetIcon(string type, int iconId) { type ??= string.Empty; if (type.Length > 0 && !type.EndsWith("/")) type += "/"; var filePath = string.Format(IconFileFormat, iconId / 1000, type, iconId); var file = this.GetFile(filePath); if (file != default(TexFile) || type.Length <= 0) return file; // Couldn't get specific type, try for generic version. filePath = string.Format(IconFileFormat, iconId / 1000, string.Empty, iconId); file = this.GetFile(filePath); return file; } #endregion public void Dispose() { this.luminaResourceThread.Abort(); } } }