Compare commits

..

26 Commits

Author SHA1 Message Date
defnotken
e8f598e695 bumpity bump
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m7s
2026-01-19 12:36:24 -06:00
defnotken
861a337029 Merge branch '2.0.3' into dev
Some checks failed
Tag and Release Lightless / tag-and-release (push) Has been cancelled
2026-01-19 12:35:29 -06:00
06f89955d3 Merge pull request 'clr-fix-attempt' (#141) from clr-fix-attempt into 2.0.3
Reviewed-on: #141
2026-01-19 18:33:55 +00:00
cake
367af2c3d0 Remove blankspace 2026-01-19 19:26:00 +01:00
cake
19a238c808 Removal of unsafe and check if PTR is correctly aligning, checking in virtual query instead of memory 2026-01-19 19:24:44 +01:00
defnotken
c7a2b679f2 bumpity bump
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m2s
2026-01-19 11:23:49 -06:00
defnotken
bec69074a5 Merge branch '2.0.3' into dev 2026-01-19 11:23:23 -06:00
7d86b41cee Merge pull request 'update-decimation-filters' (#140) from update-decimation-filters into 2.0.3
Reviewed-on: #140
2026-01-19 17:22:27 +00:00
0185e6b534 Merge pull request 'Implement Lifestream With Location Sharing.' (#139) from lifestream-location-share into 2.0.3
Reviewed-on: #139
Reviewed-by: cake <cake@noreply.git.lightless-sync.org>
2026-01-19 17:02:13 +00:00
defnotken
90bf84f8eb Implement Lifestream With Location Sharing. 2026-01-19 10:58:37 -06:00
defnotken
ac711d9a43 Bump plugin testing
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m19s
2026-01-18 20:53:14 -06:00
defnotken
b875e0c3a1 Merge branch '2.0.3' into dev 2026-01-18 20:51:58 -06:00
defnotken
46e76bbfe6 Merge branch '2.0.3' into dev 2026-01-18 20:29:53 -06:00
cake
3654365f2a bump version
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m6s
2026-01-06 14:45:23 +01:00
cake
9b256dd185 Merge branch '2.0.3' into dev 2026-01-06 14:45:02 +01:00
defnotken
223ade39cb another push
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m13s
2026-01-05 20:48:24 -06:00
defnotken
5aca9e70b2 Merge branch '2.0.3' into dev 2026-01-05 20:47:38 -06:00
defnotken
92772cf334 dev push
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m12s
2026-01-05 20:21:26 -06:00
defnotken
0395e81a9f Merge branch '2.0.3' into dev 2026-01-05 20:17:12 -06:00
defnotken
7734a7bf7e dev build
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m8s
2026-01-05 17:42:21 -06:00
defnotken
db2d19bb1e Merge branch '2.0.3' into dev 2026-01-05 17:41:48 -06:00
defnotken
ab305a249c more checks
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m7s
2026-01-05 15:48:54 -06:00
defnotken
9d104a9dd8 Merge branch '2.0.3' into dev 2026-01-05 15:42:15 -06:00
defnotken
bcd3bd5ca2 add more checks
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m8s
2026-01-05 15:08:26 -06:00
defnotken
c1829a9837 Merge branch '2.0.3' into dev
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m14s
2026-01-05 14:48:47 -06:00
defnotken
cca23f6e05 Building Dev
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m27s
2026-01-05 10:50:25 -06:00
7 changed files with 347 additions and 26 deletions

View File

@@ -0,0 +1,51 @@
namespace Lifestream.Enums;
public enum TerritoryTypeIdHousing
{
None = -1,
// Mist (Limsa Lominsa)
Mist = 339,
MistSmall = 282,
MistMedium = 283,
MistLarge = 284,
MistFCRoom = 384,
MistFCWorkshop = 423,
MistApartment = 608,
// Lavender Beds (Gridania)
Lavender = 340,
LavenderSmall = 342,
LavenderMedium = 343,
LavenderLarge = 344,
LavenderFCRoom = 385,
LavenderFCWorkshop = 425,
LavenderApartment = 609,
// Goblet (Ul'dah)
Goblet = 341,
GobletSmall = 345,
GobletMedium = 346,
GobletLarge = 347,
GobletFCRoom = 386,
GobletFCWorkshop = 424,
GobletApartment = 610,
// Shirogane (Kugane)
Shirogane = 641,
ShiroganeSmall = 649,
ShiroganeMedium = 650,
ShiroganeLarge = 651,
ShiroganeFCRoom = 652,
ShiroganeFCWorkshop = 653,
ShiroganeApartment = 655,
// Empyreum (Ishgard)
Empyream = 979,
EmpyreamSmall = 980,
EmpyreamMedium = 981,
EmpyreamLarge = 982,
EmpyreamFCRoom = 983,
EmpyreamFCWorkshop = 984,
EmpyreamApartment = 999,
}

View File

@@ -3,7 +3,7 @@
<PropertyGroup>
<Authors></Authors>
<Company></Company>
<Version>2.0.3</Version>
<Version>2.0.2.80</Version>
<Description></Description>
<Copyright></Copyright>
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>

View File

@@ -1,6 +1,5 @@
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using LightlessSync.API.Data.Enum;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using LightlessSync.FileCache;
using LightlessSync.Interop.Ipc;
using LightlessSync.LightlessConfiguration;
@@ -14,6 +13,7 @@ using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Runtime.InteropServices;
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
namespace LightlessSync.PlayerData.Factories;
@@ -119,39 +119,48 @@ public class PlayerDataFactory
return null;
}
private static readonly int _drawObjectOffset =
(int)Marshal.OffsetOf<GameObject>(nameof(GameObject.DrawObject));
private async Task<bool> CheckForNullDrawObject(IntPtr playerPointer)
=> await _dalamudUtil.RunOnFrameworkThread(() => CheckForNullDrawObjectUnsafe(playerPointer)).ConfigureAwait(false);
=> await _dalamudUtil.RunOnFrameworkThread(() =>
{
nint basePtr = playerPointer;
private unsafe static bool CheckForNullDrawObjectUnsafe(IntPtr playerPointer)
if (!LooksLikeUserPtr(basePtr))
return true;
nint drawObjAddr = basePtr + _drawObjectOffset;
if (!TryReadIntPtr(drawObjAddr, out var drawObj))
return true;
return drawObj == 0;
}).ConfigureAwait(false);
private static bool LooksLikeUserPtr(nint p)
{
if (playerPointer == IntPtr.Zero)
return true;
if (p == 0) return false;
if (!IsPointerValid(playerPointer))
return true;
ulong u = (ulong)p;
var character = (Character*)playerPointer;
if (character == null)
return true;
if (u < 0x0000_0001_0000UL) return false;
if (u > 0x0000_7FFF_FFFF_FFFFUL) return false;
if ((u & 0x7UL) != 0) return false;
var gameObject = &character->GameObject;
if (gameObject == null)
return true;
if (!IsPointerValid((IntPtr)gameObject))
return true;
return gameObject->DrawObject == null;
return true;
}
private static bool IsPointerValid(IntPtr ptr)
private static bool TryReadIntPtr(nint addr, out nint value)
{
if (ptr == IntPtr.Zero)
value = 0;
if (!VirtualReadable(addr))
return false;
try
{
_ = Marshal.ReadByte(ptr);
value = Marshal.ReadIntPtr(addr);
return true;
}
catch
@@ -160,6 +169,37 @@ public class PlayerDataFactory
}
}
private static bool VirtualReadable(nint addr)
{
if (VirtualQuery(addr, out var mbi, (nuint)Marshal.SizeOf<MEMORY_BASIC_INFORMATION>()) == 0)
return false;
const uint MEM_COMMIT = 0x1000;
const uint PAGE_NOACCESS = 0x01;
const uint PAGE_GUARD = 0x100;
if (mbi.State != MEM_COMMIT) return false;
if ((mbi.Protect & PAGE_GUARD) != 0) return false;
if (mbi.Protect == PAGE_NOACCESS) return false;
return true;
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern nuint VirtualQuery(nint lpAddress, out MEMORY_BASIC_INFORMATION lpBuffer, nuint dwLength);
[StructLayout(LayoutKind.Sequential)]
private struct MEMORY_BASIC_INFORMATION
{
public nint BaseAddress;
public nint AllocationBase;
public uint AllocationProtect;
public nuint RegionSize;
public uint State;
public uint Protect;
public uint Type;
}
private static bool IsCacheFresh(CacheEntry entry)
=> (DateTime.UtcNow - entry.CreatedUtc) <= _characterCacheTtl;

View File

@@ -701,7 +701,23 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
str += $" Room #{location.RoomId}";
}
}
return str;
}
public string LocationToLifestream(LocationInfo location)
{
if (location.ServerId is 0 || location.TerritoryId is 0 || ContentFinderData.Value.ContainsKey(location.TerritoryId)) return String.Empty;
var str = WorldData.Value[(ushort)location.ServerId];
if (location.HouseId is 0 && location.MapId is not 0)
{
var mapName = MapData.Value[(ushort)location.MapId].MapName;
var parts = mapName.Split(" - ", StringSplitOptions.RemoveEmptyEntries);
var locationName = parts.Length > 0 ? parts[^1] : mapName;
str += $", tp {locationName}";
string message = $"LocationToLifestream: {str}";
_logger.LogInformation(message);
}
return str;
}

View File

@@ -1,3 +1,4 @@
using Lifestream.Enums;
using LightlessSync.API.Data;
using LightlessSync.API.Dto.CharaData;
using LightlessSync.API.Dto.User;
@@ -108,6 +109,144 @@ namespace LightlessSync.Services
}
}
public LocationInfo? GetLocationForLifestreamByUid(string uid)
{
try
{
if (_locations.TryGetValue<LocationInfo>(uid, out var location))
{
return location;
}
return null;
}
catch (Exception e)
{
Logger.LogError(e,"GetLocationInfoByUid error : ");
throw;
}
}
public AddressBookEntryTuple? GetAddressBookEntryByLocation(LocationInfo location)
{
if (location.ServerId is 0 || location.TerritoryId is 0)
{
return null;
}
var territoryHousing = (TerritoryTypeIdHousing)location.TerritoryId;
if (territoryHousing == TerritoryTypeIdHousing.None || !Enum.IsDefined(typeof(TerritoryTypeIdHousing), territoryHousing))
{
return null;
}
var city = GetResidentialAetheryteKind(territoryHousing);
if (city == ResidentialAetheryteKind.None)
{
return null;
}
if (location.HouseId is not 0 and not 100)
{
AddressBookEntryTuple addressEntry = (
Name: "",
World: (int)location.ServerId,
City: (int)city,
Ward: (int)location.WardId,
PropertyType: 0,
Plot: (int)location.HouseId,
Apartment: 0,
ApartmentSubdivision: location.DivisionId == 2,
AliasEnabled: false,
Alias: ""
);
return addressEntry;
}
else if (location.HouseId is 100)
{
AddressBookEntryTuple addressEntry = (
Name: "",
World: (int)location.ServerId,
City: (int)city,
Ward: (int)location.WardId,
PropertyType: 1,
Plot: 0,
Apartment: (int)location.RoomId,
ApartmentSubdivision: location.DivisionId == 2,
AliasEnabled: false,
Alias: ""
);
return addressEntry;
}
return null;
}
private ResidentialAetheryteKind GetResidentialAetheryteKind(TerritoryTypeIdHousing territoryHousing)
{
return territoryHousing switch
{
TerritoryTypeIdHousing.Shirogane or
TerritoryTypeIdHousing.ShiroganeApartment or
TerritoryTypeIdHousing.ShiroganeSmall or
TerritoryTypeIdHousing.ShiroganeMedium or
TerritoryTypeIdHousing.ShiroganeLarge or
TerritoryTypeIdHousing.ShiroganeFCRoom or
TerritoryTypeIdHousing.ShiroganeFCWorkshop
=> ResidentialAetheryteKind.Kugane,
TerritoryTypeIdHousing.Lavender or
TerritoryTypeIdHousing.LavenderSmall or
TerritoryTypeIdHousing.LavenderMedium or
TerritoryTypeIdHousing.LavenderLarge or
TerritoryTypeIdHousing.LavenderApartment or
TerritoryTypeIdHousing.LavenderFCRoom or
TerritoryTypeIdHousing.LavenderFCWorkshop
=> ResidentialAetheryteKind.Gridania,
TerritoryTypeIdHousing.Mist or
TerritoryTypeIdHousing.MistSmall or
TerritoryTypeIdHousing.MistMedium or
TerritoryTypeIdHousing.MistLarge or
TerritoryTypeIdHousing.MistApartment or
TerritoryTypeIdHousing.MistFCRoom or
TerritoryTypeIdHousing.MistFCWorkshop
=> ResidentialAetheryteKind.Limsa,
TerritoryTypeIdHousing.Goblet or
TerritoryTypeIdHousing.GobletSmall or
TerritoryTypeIdHousing.GobletMedium or
TerritoryTypeIdHousing.GobletLarge or
TerritoryTypeIdHousing.GobletApartment or
TerritoryTypeIdHousing.GobletFCRoom or
TerritoryTypeIdHousing.GobletFCWorkshop
=> ResidentialAetheryteKind.Uldah,
TerritoryTypeIdHousing.Empyream or
TerritoryTypeIdHousing.EmpyreamSmall or
TerritoryTypeIdHousing.EmpyreamMedium or
TerritoryTypeIdHousing.EmpyreamLarge or
TerritoryTypeIdHousing.EmpyreamApartment or
TerritoryTypeIdHousing.EmpyreamFCRoom or
TerritoryTypeIdHousing.EmpyreamFCWorkshop
=> ResidentialAetheryteKind.Foundation,
_ => ResidentialAetheryteKind.None
};
}
public string? GetMapAddressByLocation(LocationInfo location)
{
string? liString = null;
var territoryHousing = (TerritoryTypeIdHousing)location.TerritoryId;
if (GetResidentialAetheryteKind(territoryHousing) == ResidentialAetheryteKind.None)
{
liString = _dalamudUtilService.LocationToLifestream(location);
}
return liString;
}
public DateTimeOffset GetSharingStatus(string uid)
{
try

View File

@@ -4,8 +4,10 @@ using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto.CharaData;
using LightlessSync.API.Dto.Group;
using LightlessSync.API.Dto.User;
using LightlessSync.Interop.Ipc;
using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services;
@@ -40,6 +42,7 @@ public class DrawUserPair
private readonly LocationShareService _locationShareService;
private readonly CharaDataManager _charaDataManager;
private readonly PairLedger _pairLedger;
private readonly IpcCallerLifestream _lifestreamIpc;
private float _menuWidth = -1;
private bool _wasHovered = false;
private TooltipSnapshot _tooltipSnapshot = TooltipSnapshot.Empty;
@@ -60,7 +63,8 @@ public class DrawUserPair
LightlessConfigService configService,
LocationShareService locationShareService,
CharaDataManager charaDataManager,
PairLedger pairLedger)
PairLedger pairLedger,
IpcCallerLifestream lifestreamIpc)
{
_id = id;
_uiEntry = uiEntry;
@@ -79,6 +83,7 @@ public class DrawUserPair
_locationShareService = locationShareService;
_charaDataManager = charaDataManager;
_pairLedger = pairLedger;
_lifestreamIpc = lifestreamIpc;
}
public PairDisplayEntry DisplayEntry => _displayEntry;
@@ -656,6 +661,13 @@ public class DrawUserPair
using (ImRaii.PushColor(ImGuiCol.Text, shareColor, shareLocation || shareLocationToOther))
_uiSharedService.IconText(shareLocationIcon);
var popupId = $"LocationPopup_{_pair.UserData.UID}";
if (ImGui.IsItemClicked(ImGuiMouseButton.Left) && shareLocation && !string.IsNullOrEmpty(location))
{
ImGui.OpenPopup(popupId);
}
if (ImGui.IsItemHovered())
{
ImGui.BeginTooltip();
@@ -669,6 +681,8 @@ public class DrawUserPair
_uiSharedService.IconText(FontAwesomeIcon.LocationArrow);
ImGui.SameLine();
ImGui.TextUnformatted(location);
ImGui.Separator();
ImGui.TextUnformatted("Click to teleport to this location");
}
else
{
@@ -700,6 +714,62 @@ public class DrawUserPair
}
ImGui.EndTooltip();
}
if (ImGui.BeginPopup(popupId))
{
var locationInfo = _locationShareService.GetLocationForLifestreamByUid(_pair.UserData.UID);
if (locationInfo != null)
{
var locationLi = locationInfo.Value;
var housingAddress = _locationShareService.GetAddressBookEntryByLocation(locationLi);
var mapAddress = _locationShareService.GetMapAddressByLocation(locationLi);
ImGui.TextUnformatted("Teleport to user?");
ImGui.Separator();
if (!_lifestreamIpc.APIAvailable)
{
ImGui.TextUnformatted("Lifestream IPC is not available. Please ensure Lifestream is enabled");
}
else if (housingAddress != null || mapAddress != null)
{
ImGui.TextUnformatted($"Go to {location}?");
ImGui.TextUnformatted($"NOTE: Teleporting to maps with multiple aetherytes or instances may not be accurate currently. (ie. Thavnair, Yanxia)");
}
else
{
ImGui.TextUnformatted("Lifestream cannot teleport here. If you are in a residential area, please make sure you're inside a plot.");
}
ImGui.Separator();
if (_lifestreamIpc.APIAvailable && (housingAddress != null || mapAddress != null))
{
if (locationLi.HouseId is not 0 && housingAddress != null)
{
if (ImGui.Button("Navigate"))
{
_lifestreamIpc.GoToHousingAddress(housingAddress.Value);
ImGui.CloseCurrentPopup();
}
}
else if (mapAddress != null && locationLi.HouseId is 0)
{
if (ImGui.Button("Navigate"))
{
_lifestreamIpc.ExecuteLifestreamCommand(mapAddress);
ImGui.CloseCurrentPopup();
}
}
ImGui.SameLine();
}
if (ImGui.Button("Close"))
{
ImGui.CloseCurrentPopup();
}
ImGui.EndPopup();
}
}
}
if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled || individualIsSticky)

View File

@@ -16,6 +16,7 @@ using LightlessSync.UI.Handlers;
using LightlessSync.UI.Models;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Logging;
using LightlessSync.Interop.Ipc;
namespace LightlessSync.UI;
@@ -40,6 +41,7 @@ public class DrawEntityFactory
private readonly IdDisplayHandler _uidDisplayHandler;
private readonly PairLedger _pairLedger;
private readonly PairFactory _pairFactory;
private readonly IpcCallerLifestream _lifestreamIpc;
public DrawEntityFactory(
ILogger<DrawEntityFactory> logger,
@@ -60,7 +62,8 @@ public class DrawEntityFactory
RenameSyncshellTagUi renameSyncshellTagUi,
SelectSyncshellForTagUi selectSyncshellForTagUi,
PairLedger pairLedger,
PairFactory pairFactory)
PairFactory pairFactory,
IpcCallerLifestream lifestreamIpc)
{
_logger = logger;
_apiController = apiController;
@@ -81,6 +84,7 @@ public class DrawEntityFactory
_selectSyncshellForTagUi = selectSyncshellForTagUi;
_pairLedger = pairLedger;
_pairFactory = pairFactory;
_lifestreamIpc = lifestreamIpc;
}
public DrawFolderGroup CreateGroupFolder(
@@ -167,7 +171,8 @@ public class DrawEntityFactory
_configService,
_locationShareService,
_charaDataManager,
_pairLedger);
_pairLedger,
_lifestreamIpc);
}
public IReadOnlyList<PairUiEntry> GetAllEntries()