Merge remote-tracking branch 'lmcintyre/master' into viewport

This commit is contained in:
Liam 2021-03-29 12:35:38 -04:00
commit fd9b145756
12 changed files with 657 additions and 25 deletions

View file

@ -34,6 +34,8 @@ namespace Dalamud
public bool PrintPluginsWelcomeMsg { get; set; } = true;
public bool AutoUpdatePlugins { get; set; } = false;
public bool LogAutoScroll { get; set; } = true;
public bool LogOpenAtStartup { get; set; }
[JsonIgnore]
public string ConfigPath;

View file

@ -40,22 +40,16 @@ namespace Dalamud.Game.ClientState
}
public bool Any() {
var didAny = false;
for (var i = 0; i < MaxConditionEntries; i++)
{
var typedCondition = (ConditionFlag)i;
var cond = this[typedCondition];
if (!cond)
{
continue;
}
didAny = true;
if (cond)
return true;
}
return didAny;
return false;
}
}
}

View file

@ -11,6 +11,7 @@ namespace Dalamud.Game.Internal.Gui {
private GameGuiAddressResolver Address { get; }
public ChatGui Chat { get; private set; }
public PartyFinderGui PartyFinder { get; private set; }
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr SetGlobalBgmDelegate(UInt16 bgmKey, byte a2, UInt32 a3, UInt32 a4, UInt32 a5, byte a6);
@ -107,6 +108,7 @@ namespace Dalamud.Game.Internal.Gui {
Log.Verbose("GetUIObject address {Address}", Address.GetUIObject);
Chat = new ChatGui(Address.ChatManager, scanner, dalamud);
PartyFinder = new PartyFinderGui(scanner, dalamud);
this.setGlobalBgmHook =
new Hook<SetGlobalBgmDelegate>(Address.SetGlobalBgm,
@ -432,6 +434,7 @@ namespace Dalamud.Game.Internal.Gui {
public void Enable() {
Chat.Enable();
PartyFinder.Enable();
this.setGlobalBgmHook.Enable();
this.handleItemHoverHook.Enable();
this.handleItemOutHook.Enable();
@ -442,6 +445,7 @@ namespace Dalamud.Game.Internal.Gui {
public void Dispose() {
Chat.Dispose();
PartyFinder.Dispose();
this.setGlobalBgmHook.Dispose();
this.handleItemHoverHook.Dispose();
this.handleItemOutHook.Dispose();

View file

@ -0,0 +1,11 @@
using System;
namespace Dalamud.Game.Internal.Gui {
class PartyFinderAddressResolver : BaseAddressResolver {
public IntPtr ReceiveListing { get; private set; }
protected override void Setup64Bit(SigScanner sig) {
ReceiveListing = sig.ScanText("40 53 41 57 48 83 EC 28 48 8B D9");
}
}
}

View file

@ -0,0 +1,117 @@
using System;
using System.Runtime.InteropServices;
using Dalamud.Game.Internal.Gui.Structs;
using Dalamud.Hooking;
using Serilog;
namespace Dalamud.Game.Internal.Gui {
public sealed class PartyFinderGui : IDisposable {
#region Events
public delegate void PartyFinderListingEventDelegate(PartyFinderListing listing, PartyFinderListingEventArgs args);
/// <summary>
/// Event fired each time the game receives an individual Party Finder listing. Cannot modify listings but can
/// hide them.
/// </summary>
public event PartyFinderListingEventDelegate ReceiveListing;
#endregion
#region Hooks
private readonly Hook<ReceiveListingDelegate> receiveListingHook;
#endregion
#region Delegates
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void ReceiveListingDelegate(IntPtr managerPtr, IntPtr data);
#endregion
private Dalamud Dalamud { get; }
private PartyFinderAddressResolver Address { get; }
private IntPtr Memory { get; }
public PartyFinderGui(SigScanner scanner, Dalamud dalamud) {
Dalamud = dalamud;
Address = new PartyFinderAddressResolver();
Address.Setup(scanner);
Memory = Marshal.AllocHGlobal(PartyFinder.PacketInfo.PacketSize);
this.receiveListingHook = new Hook<ReceiveListingDelegate>(Address.ReceiveListing, new ReceiveListingDelegate(HandleReceiveListingDetour));
}
public void Enable() {
this.receiveListingHook.Enable();
}
public void Dispose() {
this.receiveListingHook.Dispose();
Marshal.FreeHGlobal(Memory);
}
private void HandleReceiveListingDetour(IntPtr managerPtr, IntPtr data) {
try {
HandleListingEvents(data);
} catch (Exception ex) {
Log.Error(ex, "Exception on ReceiveListing hook.");
}
this.receiveListingHook.Original(managerPtr, data);
}
private void HandleListingEvents(IntPtr data) {
var dataPtr = data + 0x10;
var packet = Marshal.PtrToStructure<PartyFinder.Packet>(dataPtr);
// rewriting is an expensive operation, so only do it if necessary
var needToRewrite = false;
for (var i = 0; i < packet.listings.Length; i++) {
// these are empty slots that are not shown to the player
if (packet.listings[i].IsNull()) {
continue;
}
var listing = new PartyFinderListing(packet.listings[i], Dalamud.Data, Dalamud.SeStringManager);
var args = new PartyFinderListingEventArgs();
ReceiveListing?.Invoke(listing, args);
if (args.Visible) {
continue;
}
// hide the listing from the player by setting it to a null listing
packet.listings[i] = new PartyFinder.Listing();
needToRewrite = true;
}
if (!needToRewrite) {
return;
}
// write our struct into the memory (doing this directly crashes the game)
Marshal.StructureToPtr(packet, Memory, false);
// copy our new memory over the game's
unsafe {
Buffer.MemoryCopy(
(void*) Memory,
(void*) dataPtr,
PartyFinder.PacketInfo.PacketSize,
PartyFinder.PacketInfo.PacketSize
);
}
}
}
public class PartyFinderListingEventArgs {
public bool Visible { get; set; } = true;
}
}

View file

@ -0,0 +1,450 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Dalamud.Data;
using Dalamud.Game.Chat.SeStringHandling;
using Lumina.Excel.GeneratedSheets;
namespace Dalamud.Game.Internal.Gui.Structs {
#region Raw structs
internal static class PartyFinder {
public static class PacketInfo {
public static readonly int PacketSize = Marshal.SizeOf<Packet>();
}
[StructLayout(LayoutKind.Sequential)]
public readonly struct Packet {
private readonly int unk0;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
private readonly byte[] padding1;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
public readonly Listing[] listings;
}
[StructLayout(LayoutKind.Sequential)]
public readonly struct Listing {
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
private readonly byte[] header1;
internal readonly uint id;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
private readonly byte[] header2;
private readonly uint unknownInt1;
private readonly ushort unknownShort1;
private readonly ushort unknownShort2;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)]
private readonly byte[] header3;
internal readonly byte category;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
private readonly byte[] header4;
internal readonly ushort duty;
internal readonly byte dutyType;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 11)]
private readonly byte[] header5;
internal readonly ushort world;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
private readonly byte[] header6;
internal readonly byte objective;
internal readonly byte beginnersWelcome;
internal readonly byte conditions;
internal readonly byte dutyFinderSettings;
internal readonly byte lootRules;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
private readonly byte[] header7; // all zero in every pf I've examined
private readonly uint lastPatchHotfixTimestamp; // last time the servers were restarted?
internal readonly ushort secondsRemaining;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
private readonly byte[] header8; // 00 00 01 00 00 00 in every pf I've examined
internal readonly ushort minimumItemLevel;
internal readonly ushort homeWorld;
internal readonly ushort currentWorld;
private readonly byte header9;
internal readonly byte numSlots;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
private readonly byte[] header10;
internal readonly byte searchArea;
private readonly byte header11;
internal readonly byte numParties;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
private readonly byte[] header12; // 00 00 00 always. maybe numParties is a u32?
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
internal readonly uint[] slots;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
internal readonly byte[] jobsPresent;
// Note that ByValTStr will not work here because the strings are UTF-8 and there's only a CharSet for UTF-16 in C#.
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
internal readonly byte[] name;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 192)]
internal readonly byte[] description;
internal bool IsNull() {
// a valid party finder must have at least one slot set
return this.slots.All(slot => slot == 0);
}
}
}
#endregion
#region Read-only classes
public class PartyFinderListing {
/// <summary>
/// The ID assigned to this listing by the game's server.
/// </summary>
public uint Id { get; }
/// <summary>
/// The name of the player hosting this listing.
/// </summary>
public SeString Name { get; }
/// <summary>
/// The description of this listing as set by the host. May be multiple lines.
/// </summary>
public SeString Description { get; }
/// <summary>
/// The world that this listing was created on.
/// </summary>
public Lazy<World> World { get; }
/// <summary>
/// The home world of the listing's host.
/// </summary>
public Lazy<World> HomeWorld { get; }
/// <summary>
/// The current world of the listing's host.
/// </summary>
public Lazy<World> CurrentWorld { get; }
/// <summary>
/// The Party Finder category this listing is listed under.
/// </summary>
public Category Category { get; }
/// <summary>
/// The row ID of the duty this listing is for. May be 0 for non-duty listings.
/// </summary>
public ushort RawDuty { get; }
/// <summary>
/// The duty this listing is for. May be null for non-duty listings.
/// </summary>
public Lazy<ContentFinderCondition> Duty { get; }
/// <summary>
/// The type of duty this listing is for.
/// </summary>
public DutyType DutyType { get; }
/// <summary>
/// If this listing is beginner-friendly. Shown with a sprout icon in-game.
/// </summary>
public bool BeginnersWelcome { get; }
/// <summary>
/// How many seconds this listing will continue to be available for. It may end before this time if the party
/// fills or the host ends it early.
/// </summary>
public ushort SecondsRemaining { get; }
/// <summary>
/// The minimum item level required to join this listing.
/// </summary>
public ushort MinimumItemLevel { get; }
/// <summary>
/// The number of parties this listing is recruiting for.
/// </summary>
public byte Parties { get; }
/// <summary>
/// The number of player slots this listing is recruiting for.
/// </summary>
public byte SlotsAvailable { get; }
/// <summary>
/// A list of player slots that the Party Finder is accepting.
/// </summary>
public IReadOnlyCollection<PartyFinderSlot> Slots => this.slots;
/// <summary>
/// The objective of this listing.
/// </summary>
public ObjectiveFlags Objective => (ObjectiveFlags) this.objective;
/// <summary>
/// The conditions of this listing.
/// </summary>
public ConditionFlags Conditions => (ConditionFlags) this.conditions;
/// <summary>
/// The Duty Finder settings that will be used for this listing.
/// </summary>
public DutyFinderSettingsFlags DutyFinderSettings => (DutyFinderSettingsFlags) this.dutyFinderSettings;
/// <summary>
/// The loot rules that will be used for this listing.
/// </summary>
public LootRuleFlags LootRules => (LootRuleFlags) this.lootRules;
/// <summary>
/// Where this listing is searching. Note that this is also used for denoting alliance raid listings and one
/// player per job.
/// </summary>
public SearchAreaFlags SearchArea => (SearchAreaFlags) this.searchArea;
/// <summary>
/// A list of the class/job IDs that are currently present in the party.
/// </summary>
public IReadOnlyCollection<byte> RawJobsPresent => this.jobsPresent;
/// <summary>
/// A list of the classes/jobs that are currently present in the party.
/// </summary>
public IReadOnlyCollection<Lazy<ClassJob>> JobsPresent { get; }
#region Backing fields
private readonly byte objective;
private readonly byte conditions;
private readonly byte dutyFinderSettings;
private readonly byte lootRules;
private readonly byte searchArea;
private readonly PartyFinderSlot[] slots;
private readonly byte[] jobsPresent;
#endregion
#region Indexers
public bool this[ObjectiveFlags flag] => this.objective == 0 || (this.objective & (uint) flag) > 0;
public bool this[ConditionFlags flag] => this.conditions == 0 || (this.conditions & (uint) flag) > 0;
public bool this[DutyFinderSettingsFlags flag] => this.dutyFinderSettings == 0 || (this.dutyFinderSettings & (uint) flag) > 0;
public bool this[LootRuleFlags flag] => this.lootRules == 0 || (this.lootRules & (uint) flag) > 0;
public bool this[SearchAreaFlags flag] => this.searchArea == 0 || (this.searchArea & (uint) flag) > 0;
#endregion
internal PartyFinderListing(PartyFinder.Listing listing, DataManager dataManager, SeStringManager seStringManager) {
this.objective = listing.objective;
this.conditions = listing.conditions;
this.dutyFinderSettings = listing.dutyFinderSettings;
this.lootRules = listing.lootRules;
this.searchArea = listing.searchArea;
this.slots = listing.slots.Select(accepting => new PartyFinderSlot(accepting)).ToArray();
this.jobsPresent = listing.jobsPresent;
Id = listing.id;
Name = seStringManager.Parse(listing.name.TakeWhile(b => b != 0).ToArray());
Description = seStringManager.Parse(listing.description.TakeWhile(b => b != 0).ToArray());
World = new Lazy<World>(() => dataManager.GetExcelSheet<World>().GetRow(listing.world));
HomeWorld = new Lazy<World>(() => dataManager.GetExcelSheet<World>().GetRow(listing.homeWorld));
CurrentWorld = new Lazy<World>(() => dataManager.GetExcelSheet<World>().GetRow(listing.currentWorld));
Category = (Category) listing.category;
RawDuty = listing.duty;
Duty = new Lazy<ContentFinderCondition>(() => dataManager.GetExcelSheet<ContentFinderCondition>().GetRow(listing.duty));
DutyType = (DutyType) listing.dutyType;
BeginnersWelcome = listing.beginnersWelcome == 1;
SecondsRemaining = listing.secondsRemaining;
MinimumItemLevel = listing.minimumItemLevel;
Parties = listing.numParties;
SlotsAvailable = listing.numSlots;
JobsPresent = listing.jobsPresent
.Select(id => new Lazy<ClassJob>(() => id == 0
? null
: dataManager.GetExcelSheet<ClassJob>().GetRow(id)))
.ToArray();
}
}
/// <summary>
/// A player slot in a Party Finder listing.
/// </summary>
public class PartyFinderSlot {
private readonly uint accepting;
private JobFlags[] listAccepting;
/// <summary>
/// List of jobs that this slot is accepting.
/// </summary>
public IReadOnlyCollection<JobFlags> Accepting {
get {
if (this.listAccepting != null) {
return this.listAccepting;
}
this.listAccepting = Enum.GetValues(typeof(JobFlags))
.Cast<JobFlags>()
.Where(flag => this[flag])
.ToArray();
return this.listAccepting;
}
}
/// <summary>
/// Tests if this slot is accepting a job.
/// </summary>
/// <param name="flag">Job to test</param>
public bool this[JobFlags flag] => (this.accepting & (uint) flag) > 0;
internal PartyFinderSlot(uint accepting) {
this.accepting = accepting;
}
}
[Flags]
public enum SearchAreaFlags : uint {
DataCentre = 1 << 0,
Private = 1 << 1,
AllianceRaid = 1 << 2,
World = 1 << 3,
OnePlayerPerJob = 1 << 5,
}
[Flags]
public enum JobFlags {
Gladiator = 1 << 1,
Pugilist = 1 << 2,
Marauder = 1 << 3,
Lancer = 1 << 4,
Archer = 1 << 5,
Conjurer = 1 << 6,
Thaumaturge = 1 << 7,
Paladin = 1 << 8,
Monk = 1 << 9,
Warrior = 1 << 10,
Dragoon = 1 << 11,
Bard = 1 << 12,
WhiteMage = 1 << 13,
BlackMage = 1 << 14,
Arcanist = 1 << 15,
Summoner = 1 << 16,
Scholar = 1 << 17,
Rogue = 1 << 18,
Ninja = 1 << 19,
Machinist = 1 << 20,
DarkKnight = 1 << 21,
Astrologian = 1 << 22,
Samurai = 1 << 23,
RedMage = 1 << 24,
BlueMage = 1 << 25,
Gunbreaker = 1 << 26,
Dancer = 1 << 27,
}
public static class JobFlagsExt {
/// <summary>
/// Get the actual ClassJob from the in-game sheets for this JobFlags.
/// </summary>
/// <param name="job">A JobFlags enum member</param>
/// <param name="data">A DataManager to get the ClassJob from</param>
/// <returns>A ClassJob if found or null if not</returns>
public static ClassJob ClassJob(this JobFlags job, DataManager data) {
var jobs = data.GetExcelSheet<ClassJob>();
uint? row = job switch {
JobFlags.Gladiator => 1,
JobFlags.Pugilist => 2,
JobFlags.Marauder => 3,
JobFlags.Lancer => 4,
JobFlags.Archer => 5,
JobFlags.Conjurer => 6,
JobFlags.Thaumaturge => 7,
JobFlags.Paladin => 19,
JobFlags.Monk => 20,
JobFlags.Warrior => 21,
JobFlags.Dragoon => 22,
JobFlags.Bard => 23,
JobFlags.WhiteMage => 24,
JobFlags.BlackMage => 25,
JobFlags.Arcanist => 26,
JobFlags.Summoner => 27,
JobFlags.Scholar => 28,
JobFlags.Rogue => 29,
JobFlags.Ninja => 30,
JobFlags.Machinist => 31,
JobFlags.DarkKnight => 32,
JobFlags.Astrologian => 33,
JobFlags.Samurai => 34,
JobFlags.RedMage => 35,
JobFlags.BlueMage => 36,
JobFlags.Gunbreaker => 37,
JobFlags.Dancer => 38,
_ => null,
};
return row == null ? null : jobs.GetRow((uint) row);
}
}
[Flags]
public enum ObjectiveFlags : uint {
None = 0,
DutyCompletion = 1,
Practice = 2,
Loot = 4,
}
[Flags]
public enum ConditionFlags : uint {
None = 1,
DutyComplete = 2,
DutyIncomplete = 4,
}
[Flags]
public enum DutyFinderSettingsFlags : uint {
None = 0,
UndersizedParty = 1 << 0,
MinimumItemLevel = 1 << 1,
SilenceEcho = 1 << 2,
}
[Flags]
public enum LootRuleFlags : uint {
None = 0,
GreedOnly = 1,
Lootmaster = 2,
}
public enum Category {
Duty = 0,
QuestBattles = 1 << 0,
Fates = 1 << 1,
TreasureHunt = 1 << 2,
TheHunt = 1 << 3,
GatheringForays = 1 << 4,
DeepDungeons = 1 << 5,
AdventuringForays = 1 << 6,
}
public enum DutyType {
Other = 0,
Roulette = 1 << 0,
Normal = 1 << 1,
}
#endregion
}

View file

@ -42,6 +42,8 @@ namespace Dalamud.Interface
private UIDebug UIDebug = null;
private uint copyButtonIndex = 0;
public DalamudDataWindow(Dalamud dalamud) {
this.dalamud = dalamud;
@ -57,6 +59,7 @@ namespace Dalamud.Interface
}
public bool Draw() {
this.copyButtonIndex = 0;
ImGui.SetNextWindowSize(new Vector2(500, 500), ImGuiCond.FirstUseEver);
var isOpen = true;
@ -101,7 +104,7 @@ namespace Dalamud.Interface
}
ImGui.Text($"Result: {this.sigResult.ToInt64():X}");
ImGui.SameLine();
if (ImGui.Button("C")) {
if (ImGui.Button($"C{this.copyButtonIndex++}")) {
ImGui.SetClipboardText(this.sigResult.ToInt64().ToString("x"));
}
@ -112,7 +115,7 @@ namespace Dalamud.Interface
$" {valueTuple.Item1} - 0x{valueTuple.Item2.ToInt64():x}");
ImGui.SameLine();
if (ImGui.Button("C")) {
if (ImGui.Button($"C##copyAddress{copyButtonIndex++}")) {
ImGui.SetClipboardText(valueTuple.Item2.ToInt64().ToString("x"));
}
}
@ -442,7 +445,7 @@ namespace Dalamud.Interface
ImGui.TextUnformatted(actorString);
ImGui.SameLine();
if (ImGui.Button("C")) {
if (ImGui.Button($"C##{this.copyButtonIndex++}")) {
ImGui.SetClipboardText(actor.Address.ToInt64().ToString("X"));
}

View file

@ -26,6 +26,9 @@ namespace Dalamud.Interface
public DalamudInterface(Dalamud dalamud) {
this.dalamud = dalamud;
if (dalamud.Configuration.LogOpenAtStartup) {
OpenLog();
}
}
private bool isImguiDrawDemoWindow = false;
@ -97,7 +100,7 @@ namespace Dalamud.Interface
ImGui.Separator();
if (ImGui.MenuItem("Open Log window"))
{
this.logWindow = new DalamudLogWindow(this.dalamud.CommandManager);
this.logWindow = new DalamudLogWindow(this.dalamud.CommandManager, this.dalamud.Configuration);
this.isImguiDrawLogWindow = true;
}
if (ImGui.BeginMenu("Set log level..."))
@ -223,7 +226,7 @@ namespace Dalamud.Interface
{
if (ImGui.MenuItem("From Fallbacks"))
{
Loc.SetupWithFallbacks();
this.dalamud.LocalizationManager.SetupWithFallbacks();
}
if (ImGui.MenuItem("From UICulture"))
@ -336,7 +339,7 @@ namespace Dalamud.Interface
}
public void OpenLog() {
this.logWindow = new DalamudLogWindow(this.dalamud.CommandManager);
this.logWindow = new DalamudLogWindow(this.dalamud.CommandManager, this.dalamud.Configuration);
this.isImguiDrawLogWindow = true;
}

View file

@ -13,15 +13,20 @@ namespace Dalamud.Interface
{
class DalamudLogWindow : IDisposable {
private readonly CommandManager commandManager;
private bool autoScroll = true;
private readonly DalamudConfiguration configuration;
private bool autoScroll;
private bool openAtStartup;
private readonly List<(string line, Vector4 color)> logText = new List<(string line, Vector4 color)>();
private readonly object renderLock = new object();
private string commandText = string.Empty;
public DalamudLogWindow(CommandManager commandManager) {
public DalamudLogWindow(CommandManager commandManager, DalamudConfiguration configuration) {
this.commandManager = commandManager;
this.configuration = configuration;
this.autoScroll = configuration.LogAutoScroll;
this.openAtStartup = configuration.LogOpenAtStartup;
SerilogEventSink.Instance.OnLogLine += Serilog_OnLogLine;
}
@ -71,7 +76,14 @@ namespace Dalamud.Interface
// Options menu
if (ImGui.BeginPopup("Options"))
{
ImGui.Checkbox("Auto-scroll", ref this.autoScroll);
if (ImGui.Checkbox("Auto-scroll", ref this.autoScroll)) {
this.configuration.LogAutoScroll = this.autoScroll;
this.configuration.Save();
};
if (ImGui.Checkbox("Open at startup", ref this.openAtStartup)) {
this.configuration.LogOpenAtStartup = this.openAtStartup;
this.configuration.Save();
};
ImGui.EndPopup();
}

View file

@ -13,8 +13,11 @@ namespace Dalamud
class Localization {
private readonly string workingDirectory;
private const string FallbackLangCode = "en";
public static readonly string[] ApplicableLangCodes = { "de", "ja", "fr", "it", "es", "ko", "no", "ru" };
public delegate void LocalizationChangedDelegate(string langCode);
public event LocalizationChangedDelegate OnLocalizationChanged;
public Localization(string workingDirectory) {
this.workingDirectory = workingDirectory;
}
@ -28,22 +31,28 @@ namespace Dalamud
if (ApplicableLangCodes.Any(x => currentUiLang.TwoLetterISOLanguageName == x)) {
SetupWithLangCode(currentUiLang.TwoLetterISOLanguageName);
} else {
Loc.SetupWithFallbacks();
SetupWithFallbacks();
}
}
catch (Exception ex)
{
Log.Error(ex, "Could not get language information. Setting up fallbacks.");
Loc.SetupWithFallbacks();
SetupWithFallbacks();
}
}
public void SetupWithFallbacks() {
OnLocalizationChanged?.Invoke(FallbackLangCode);
Loc.SetupWithFallbacks();
}
public void SetupWithLangCode(string langCode) {
if (langCode.ToLower() == "en") {
Loc.SetupWithFallbacks();
if (langCode.ToLower() == FallbackLangCode) {
SetupWithFallbacks();
return;
}
OnLocalizationChanged?.Invoke(langCode);
Loc.Setup(File.ReadAllText(Path.Combine(this.workingDirectory, "UIRes", "loc", "dalamud", $"dalamud_{langCode}.json")));
}
}

View file

@ -87,7 +87,23 @@ namespace Dalamud.Plugin
#else
public bool IsDebugging => this.dalamud.DalamudUi.IsDevMenu;
#endif
/// <summary>
/// Event that gets fired when loc is changed
/// </summary>
public event LanguageChangedDelegate OnLanguageChanged;
/// <summary>
/// Delegate for localization change with two-letter iso lang code
/// </summary>
/// <param name="langCode"></param>
public delegate void LanguageChangedDelegate(string langCode);
/// <summary>
/// Current ui language in two-letter iso format
/// </summary>
public string UiLanguage { get; private set; }
private readonly Dalamud dalamud;
private readonly string pluginName;
private readonly PluginConfigurations configs;
@ -109,6 +125,14 @@ namespace Dalamud.Plugin
this.dalamud = dalamud;
this.pluginName = pluginName;
this.configs = configs;
this.UiLanguage = this.dalamud.Configuration.LanguageOverride;
dalamud.LocalizationManager.OnLocalizationChanged += OnLocalizationChanged;
}
private void OnLocalizationChanged(string langCode) {
this.UiLanguage = langCode;
OnLanguageChanged?.Invoke(langCode);
}
/// <summary>
@ -117,6 +141,7 @@ namespace Dalamud.Plugin
public void Dispose() {
this.UiBuilder.Dispose();
this.Framework.Gui.Chat.RemoveChatLinkHandler(this.pluginName);
this.dalamud.LocalizationManager.OnLocalizationChanged -= OnLocalizationChanged;
}
/// <summary>

View file

@ -460,7 +460,9 @@ namespace Dalamud.Plugin
if (installedPlugin.IsRaw) {
ImGui.SameLine();
ImGui.TextColored(new Vector4(1.0f, 0.0f, 0.0f, 1.0f),
" To update or disable this plugin, please remove it from the devPlugins folder.");
this.dalamud.PluginRepository.PluginMaster.Any(x => x.InternalName == installedPlugin.Definition.InternalName)
? " This plugin is available in one of your repos, please remove it from the devPlugins folder."
: " To disable this plugin, please remove it from the devPlugins folder.");
}
}