mirror of
https://github.com/goatcorp/Dalamud.git
synced 2026-02-24 21:51:49 +01:00
* Use new Lock objects * Fix CA1513: Use ObjectDisposedException.ThrowIf * Fix CA1860: Avoid using 'Enumerable.Any()' extension method * Fix IDE0028: Use collection initializers or expressions * Fix CA2263: Prefer generic overload when type is known * Fix CA1862: Use the 'StringComparison' method overloads to perform case-insensitive string comparisons * Fix IDE0270: Null check can be simplified * Fix IDE0280: Use 'nameof' * Fix IDE0009: Add '.this' * Fix IDE0007: Use 'var' instead of explicit type * Fix IDE0062: Make local function static * Fix CA1859: Use concrete types when possible for improved performance * Fix IDE0066: Use switch expression Only applied to where it doesn't look horrendous. * Use is over switch * Fix CA1847: Use String.Contains(char) instead of String.Contains(string) with single characters * Fix SYSLIB1045: Use 'GeneratedRegexAttribute' to generate the regular expression implementation at compile-time. * Fix CA1866: Use 'string.EndsWith(char)' instead of 'string.EndsWith(string)' when you have a string with a single char * Fix IDE0057: Substring can be simplified * Fix IDE0059: Remove unnecessary value assignment * Fix CA1510: Use ArgumentNullException throw helper * Fix IDE0300: Use collection expression for array * Fix IDE0250: Struct can be made 'readonly' * Fix IDE0018: Inline variable declaration * Fix CA1850: Prefer static HashData method over ComputeHash * Fi CA1872: Prefer 'Convert.ToHexString' and 'Convert.ToHexStringLower' over call chains based on 'BitConverter.ToString' * Update ModuleLog instantiations * Organize usings
245 lines
9.8 KiB
C#
245 lines
9.8 KiB
C#
using System.Collections.Generic;
|
|
using System.IO;
|
|
|
|
using Dalamud.Data;
|
|
|
|
using Lumina.Excel;
|
|
using Lumina.Excel.Sheets;
|
|
|
|
using Newtonsoft.Json;
|
|
|
|
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
|
|
|
|
/// <summary>
|
|
/// An SeString Payload representing an interactable map position link.
|
|
/// </summary>
|
|
public class MapLinkPayload : Payload
|
|
{
|
|
[JsonProperty]
|
|
private uint territoryTypeId;
|
|
|
|
[JsonProperty]
|
|
private uint mapId;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="MapLinkPayload"/> class.
|
|
/// Creates an interactable MapLinkPayload from a human-readable position.
|
|
/// </summary>
|
|
/// <param name="territoryTypeId">The id of the TerritoryType entry for this link.</param>
|
|
/// <param name="mapId">The id of the Map entry for this link.</param>
|
|
/// <param name="niceXCoord">The human-readable x-coordinate for this link.</param>
|
|
/// <param name="niceYCoord">The human-readable y-coordinate for this link.</param>
|
|
/// <param name="fudgeFactor">An optional offset to account for rounding and truncation errors; it is best to leave this untouched in most cases.</param>
|
|
public MapLinkPayload(uint territoryTypeId, uint mapId, float niceXCoord, float niceYCoord, float fudgeFactor = 0.05f)
|
|
{
|
|
this.territoryTypeId = territoryTypeId;
|
|
this.mapId = mapId;
|
|
// this fudge is necessary basically to ensure we don't shift down a full tenth
|
|
// because essentially values are truncated instead of rounded, so 3.09999f will become
|
|
// 3.0f and not 3.1f
|
|
this.RawX = this.ConvertMapCoordinateToRawPosition(niceXCoord + fudgeFactor, this.Map.Value.SizeFactor, this.Map.Value.OffsetX);
|
|
this.RawY = this.ConvertMapCoordinateToRawPosition(niceYCoord + fudgeFactor, this.Map.Value.SizeFactor, this.Map.Value.OffsetY);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="MapLinkPayload"/> class.
|
|
/// Creates an interactable MapLinkPayload from a raw position.
|
|
/// </summary>
|
|
/// <param name="territoryTypeId">The id of the TerritoryType entry for this link.</param>
|
|
/// <param name="mapId">The id of the Map entry for this link.</param>
|
|
/// <param name="rawX">The internal raw x-coordinate for this link.</param>
|
|
/// <param name="rawY">The internal raw y-coordinate for this link.</param>
|
|
public MapLinkPayload(uint territoryTypeId, uint mapId, int rawX, int rawY)
|
|
{
|
|
this.territoryTypeId = territoryTypeId;
|
|
this.mapId = mapId;
|
|
this.RawX = rawX;
|
|
this.RawY = rawY;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="MapLinkPayload"/> class.
|
|
/// Creates an interactable MapLinkPayload from a human-readable position.
|
|
/// </summary>
|
|
internal MapLinkPayload()
|
|
{
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override PayloadType Type => PayloadType.MapLink;
|
|
|
|
/// <summary>
|
|
/// Gets the Map specified for this map link.
|
|
/// </summary>
|
|
[JsonIgnore]
|
|
public RowRef<Map> Map => LuminaUtils.CreateRef<Map>(this.mapId);
|
|
|
|
/// <summary>
|
|
/// Gets the TerritoryType specified for this map link.
|
|
/// </summary>
|
|
[JsonIgnore]
|
|
public RowRef<TerritoryType> TerritoryType => LuminaUtils.CreateRef<TerritoryType>(this.territoryTypeId);
|
|
|
|
/// <summary>
|
|
/// Gets the internal x-coordinate for this map position.
|
|
/// </summary>
|
|
public int RawX { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets the internal y-coordinate for this map position.
|
|
/// </summary>
|
|
public int RawY { get; private set; }
|
|
|
|
// these could be cached, but this isn't really too egregious
|
|
|
|
/// <summary>
|
|
/// Gets the readable x-coordinate position for this map link. This value is approximate and unrounded.
|
|
/// </summary>
|
|
public float XCoord => this.ConvertRawPositionToMapCoordinate(this.RawX, this.Map.Value.SizeFactor, this.Map.Value.OffsetX);
|
|
|
|
/// <summary>
|
|
/// Gets the readable y-coordinate position for this map link. This value is approximate and unrounded.
|
|
/// </summary>
|
|
[JsonIgnore]
|
|
public float YCoord => this.ConvertRawPositionToMapCoordinate(this.RawY, this.Map.Value.SizeFactor, this.Map.Value.OffsetY);
|
|
|
|
// there is no Z; it's purely in the text payload where applicable
|
|
|
|
/// <summary>
|
|
/// Gets the printable map coordinates for this link. This value tries to match the in-game printable text as closely
|
|
/// as possible but is an approximation and may be slightly off for some positions.
|
|
/// </summary>
|
|
[JsonIgnore]
|
|
public string CoordinateString
|
|
{
|
|
get
|
|
{
|
|
// this truncates the values to one decimal without rounding, which is what the game does
|
|
// the fudge also just attempts to correct the truncated/displayed value for rounding/fp issues
|
|
// TODO: should this fudge factor be the same as in the ctor? currently not since that is customizable
|
|
const float fudge = 0.02f;
|
|
var x = Math.Truncate((this.XCoord + fudge) * 10.0f) / 10.0f;
|
|
var y = Math.Truncate((this.YCoord + fudge) * 10.0f) / 10.0f;
|
|
|
|
// the formatting and spacing the game uses
|
|
var clientState = Service<ClientState.ClientState>.Get();
|
|
return clientState.ClientLanguage switch
|
|
{
|
|
ClientLanguage.German => $"( {x:0.0}, {y:0.0} )",
|
|
ClientLanguage.Japanese => $"({x:0.0}, {y:0.0})",
|
|
_ => $"( {x:0.0} , {y:0.0} )",
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the region name for this map link. This corresponds to the upper zone name found in the actual in-game map UI. eg, "La Noscea".
|
|
/// </summary>
|
|
[JsonIgnore]
|
|
public string PlaceNameRegion => this.TerritoryType.Value.PlaceNameRegion.Value.Name.ExtractText();
|
|
|
|
/// <summary>
|
|
/// Gets the place name for this map link. This corresponds to the lower zone name found in the actual in-game map UI. eg, "Limsa Lominsa Upper Decks".
|
|
/// </summary>
|
|
[JsonIgnore]
|
|
public string PlaceName => this.TerritoryType.Value.PlaceName.Value.Name.ExtractText();
|
|
|
|
/// <summary>
|
|
/// Gets the data string for this map link, for use by internal game functions that take a string variant and not a binary payload.
|
|
/// </summary>
|
|
public string DataString => $"m:{this.territoryTypeId},{this.mapId},{this.RawX},{this.RawY}";
|
|
|
|
/// <inheritdoc/>
|
|
public override string ToString()
|
|
{
|
|
return $"{this.Type} - TerritoryTypeId: {this.territoryTypeId}, MapId: {this.mapId}, RawX: {this.RawX}, RawY: {this.RawY}, display: {this.PlaceName} {this.CoordinateString}";
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
protected override byte[] EncodeImpl()
|
|
{
|
|
var packedTerritoryAndMapBytes = MakePackedInteger(this.territoryTypeId, this.mapId);
|
|
var xBytes = MakeInteger(unchecked((uint)this.RawX));
|
|
var yBytes = MakeInteger(unchecked((uint)this.RawY));
|
|
|
|
var chunkLen = 4 + packedTerritoryAndMapBytes.Length + xBytes.Length + yBytes.Length;
|
|
|
|
var bytes = new List<byte>()
|
|
{
|
|
START_BYTE,
|
|
(byte)SeStringChunkType.Interactable, (byte)chunkLen, (byte)EmbeddedInfoType.MapPositionLink,
|
|
};
|
|
|
|
bytes.AddRange(packedTerritoryAndMapBytes);
|
|
bytes.AddRange(xBytes);
|
|
bytes.AddRange(yBytes);
|
|
|
|
// unk
|
|
bytes.AddRange([0xFF, 0x01, END_BYTE]);
|
|
|
|
return bytes.ToArray();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
|
|
{
|
|
// for debugging for now
|
|
var oldPos = reader.BaseStream.Position;
|
|
var bytes = reader.ReadBytes((int)(endOfStream - reader.BaseStream.Position));
|
|
reader.BaseStream.Position = oldPos;
|
|
|
|
try
|
|
{
|
|
(this.territoryTypeId, this.mapId) = GetPackedIntegers(reader);
|
|
this.RawX = unchecked((int)GetInteger(reader));
|
|
this.RawY = unchecked((int)GetInteger(reader));
|
|
// the Z coordinate is never in this chunk, just the text (if applicable)
|
|
|
|
// seems to always be FF 01
|
|
reader.ReadBytes(2);
|
|
}
|
|
catch (NotSupportedException)
|
|
{
|
|
Serilog.Log.Information($"Unsupported map bytes {BitConverter.ToString(bytes).Replace("-", " ")}");
|
|
// we still want to break here for now, or we'd just throw again later
|
|
throw;
|
|
}
|
|
}
|
|
|
|
#region ugliness
|
|
|
|
// from https://github.com/xivapi/ffxiv-datamining/blob/master/docs/MapCoordinates.md
|
|
// from https://github.com/xivapi/xivapi-mappy/blob/master/Mappy/Helpers/MapHelper.cs
|
|
// the raw scale from the map needs to be scaled down by a factor of 100
|
|
// the raw pos also needs to be scaled down by a factor of 1000
|
|
// the tile scale is ~50, but is exactly 2048/41, more info in the md file
|
|
private float ConvertRawPositionToMapCoordinate(int pos, float scale, short offset)
|
|
{
|
|
// extra 1/1000 because that is how the network ints are done
|
|
const float networkAdjustment = 1f;
|
|
|
|
// scaling
|
|
var trueScale = scale / 100f;
|
|
var truePos = pos / 1000f;
|
|
|
|
var computedPos = (truePos + offset) * trueScale;
|
|
// pretty weird formula, but obviously has something to do with the tile scaling
|
|
return (41f / trueScale * ((computedPos + 1024f) / 2048f)) + networkAdjustment;
|
|
}
|
|
|
|
// Created as the inverse of ConvertRawPositionToMapCoordinate(), since no one seemed to have a version of that
|
|
private int ConvertMapCoordinateToRawPosition(float pos, float scale, short offset)
|
|
{
|
|
const float networkAdjustment = 1f;
|
|
|
|
// scaling
|
|
var trueScale = scale / 100f;
|
|
|
|
var num2 = (((pos - networkAdjustment) * trueScale / 41f * 2048f) - 1024f) / trueScale;
|
|
// (pos - offset) / scale, with the scaling on num2 done before for precision
|
|
num2 *= 1000f;
|
|
return (int)num2 - (offset * 1000);
|
|
}
|
|
|
|
#endregion
|
|
}
|