Skip to content

Commit

Permalink
Festival rework
Browse files Browse the repository at this point in the history
  • Loading branch information
AsgardXIV committed Jan 27, 2023
1 parent 07f5be4 commit 5277128
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 121 deletions.
100 changes: 100 additions & 0 deletions Brio/Game/World/FestivalInterop.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.LayoutEngine;
using System;
using System.Runtime.InteropServices;

namespace Brio.Game.World;

// TODO: Move into ClientStructs
// Tracking: https://github.com/aers/FFXIVClientStructs/pull/310 & https://github.com/aers/FFXIVClientStructs/pull/311

[StructLayout(LayoutKind.Explicit, Size = 0x98)]
public unsafe struct LayoutManager
{
[FieldOffset(0x38)] public uint FestivalStatus; // SetActiveFestivals will not allow a change when not 5 or 0
[FieldOffset(0x40)] public fixed uint ActiveFestivals[4];
}

public unsafe class LayoutManagerInterop
{
private delegate void SetActiveFestivalsDelegate(LayoutManager* instance, uint* festivalArray);

[Signature("E8 ?? ?? ?? ?? 8B C5 EB 6A", ScanType = ScanType.Text)]
private SetActiveFestivalsDelegate _setActiveFestivals = null!;

public LayoutManagerInterop()
{
SignatureHelper.Initialise(this);
}

public unsafe void SetActiveFestivals(uint* festivalArray)
{
var world = LayoutWorld.Instance();
if(world != null)
{
var manager = (LayoutManager*) world->ActiveLayout;
if(manager != null)
{
_setActiveFestivals(manager, festivalArray);
}
}
}

public uint[] GetActiveFestivals()
{
var result = new uint[4];

var world = LayoutWorld.Instance();
if(world != null)
{
var manager = (LayoutManager*)world->ActiveLayout;
if(manager != null)
{
for(int i = 0; i < 4; ++i)
{
result[i] = manager->ActiveFestivals[i];
}
}
}

return result;
}

public bool IsBusy
{
get
{
var world = LayoutWorld.Instance();
if(world != null)
{
var manager = (LayoutManager*)world->ActiveLayout;
if(manager != null)
{
return manager->FestivalStatus != 0 && manager->FestivalStatus != 5;
}
}

return true;
}
}
}

public unsafe class GameMainInterop
{
private delegate void SetActiveFestivalsDelegate(GameMain* instance, uint festival1, uint festival2, uint festival3, uint festival4);

[Signature("E8 ?? ?? ?? ?? 80 63 50 FE", ScanType = ScanType.Text)]
private SetActiveFestivalsDelegate _setActiveFestivals = null!;
public void SetActiveFestivals(uint festival1, uint festival2, uint festival3, uint festival4) => _setActiveFestivals(GameMain.Instance(), festival1, festival2, festival3, festival4);

[Signature("E8 ?? ?? ?? ?? E9 08 29 00 00", ScanType = ScanType.Text)]
private SetActiveFestivalsDelegate _queueActiveFestivals = null!;
public void QueueActiveFestivals(uint festival1, uint festival2, uint festival3, uint festival4) => _queueActiveFestivals(GameMain.Instance(), festival1, festival2, festival3, festival4);


public GameMainInterop()
{
SignatureHelper.Initialise(this);
}
}
205 changes: 101 additions & 104 deletions Brio/Game/World/FestivalService.cs
Original file line number Diff line number Diff line change
@@ -1,93 +1,141 @@
using Brio.Core;
using Brio.Game.GPose;
using Brio.Utils;
using FFXIVClientStructs.FFXIV.Client.LayoutEngine;
using Lumina.Excel.GeneratedSheets;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text.Json;

namespace Brio.Game.World;
public class FestivalService : ServiceBase<FestivalService>
{
// TODO: Submit the layout changes back to ClientStructs
public const int MaxFestivals = 4;

public ReadOnlyCollection<FestivalEntry> FestivalEntries => new(_festivalEntries);
public bool IsOverriden => _originalFestival != null;

public ReadOnlyCollection<FestivalEntry> ActiveFestivals
{
get
{
var result = new List<FestivalEntry>();
var active = _festivalInterop.GetActiveFestivals();
for(int idx = 0; idx < MaxFestivals; ++idx)
{
var entry = _festivalEntries.First(i => i.Id == active[idx]);
if(entry.Id != 0)
result.Add(entry);
}
return new(result);
}
}

public bool HasMoreSlots => _festivalInterop.GetActiveFestivals().Count(i => i == 0) > 0;
public bool IsOverridden => _originalState != null;

private List<FestivalEntry> _festivalEntries = new();

private Queue<ushort> _queuedTransitions = new();
private ushort? _originalFestival;
private LayoutManagerInterop _festivalInterop = new();
private GameMainInterop _gameMainInterop = new();

private unsafe delegate* unmanaged<LayoutManager*, FestivalArgs, void> _updateFestival;

public FestivalEntry CurrentFestival
private Queue<uint[]?> _pendingChanges = new();
private uint[]? _originalState;

public override void Start()
{
get
UpdateFestivalList();
GPoseService.Instance.OnGPoseStateChange += Instance_OnGPoseStateChange;
Dalamud.ClientState.TerritoryChanged += ClientState_TerritoryChanged;
base.Start();
}

public bool AddFestival(uint festival)
{
if(!CheckFestivalRestrictions(festival))
return false;

var active = _festivalInterop.GetActiveFestivals();
var copy = active.ToArray();

for(int i = 0; i < MaxFestivals; ++i)
{
var currentId = CurrentFestivalId;
return _festivalEntries.First(i => i.Id == currentId);
if(active[i] == 0)
{
SnapshotFestivals();
copy[i] = festival;
_pendingChanges.Enqueue(copy);
return true;
}
}

return false;
}

public unsafe ushort CurrentFestivalId
public bool RemoveFestival(uint festival)
{
get
var active = _festivalInterop.GetActiveFestivals();
var copy = active.ToArray();

for(int i = 0; i < MaxFestivals; ++i)
{
var layoutWorld = LayoutWorld.Instance();
if(layoutWorld != null)
if(active[i] == festival)
{
var layoutManager = layoutWorld->ActiveLayout;
SnapshotFestivals();
copy[i] = 0;
_pendingChanges.Enqueue(copy);
return true;
}
}

if(layoutManager != null)
{
return *(ushort*)(((nint)layoutManager) + 0x40);
}
return false;
}

public unsafe void ResetFestivals(bool tryThisFrame = false)
{
if(_originalState != null)
{
if(tryThisFrame)
{
InternalApply(_originalState, false);
}
else
{
_pendingChanges.Enqueue(_originalState.ToArray());
}

return 0;
_originalState = null;
}
}

public unsafe FestivalService()
public unsafe override void Tick()
{
var updateFestivalAddress = Dalamud.SigScanner.ScanText("E8 ?? ?? ?? ?? 8B C5 EB 6A");
_updateFestival = (delegate* unmanaged<LayoutManager*, FestivalArgs, void>)updateFestivalAddress;
if(_pendingChanges.Count > 0 && !_festivalInterop.IsBusy)
{
var pending = _pendingChanges.Dequeue();
if(pending != null)
InternalApply(pending);
}
}

public override void Start()
private void SnapshotFestivals()
{
UpdateFestivalList();
GPoseService.Instance.OnGPoseStateChange += Instance_OnGPoseStateChange;
Dalamud.ClientState.TerritoryChanged += ClientState_TerritoryChanged;
base.Start();
if(_originalState == null)
_originalState = _festivalInterop.GetActiveFestivals().ToArray();
}

public unsafe override void Tick()
private unsafe void InternalApply(uint[] festivals, bool applyNow = true)
{
if(_queuedTransitions.Count == 0)
return;

var layoutWorld = LayoutWorld.Instance();
if(layoutWorld != null )
if(applyNow)
{
var layoutManager = layoutWorld->ActiveLayout;

if( layoutManager != null )
{
byte status = *(byte*)(((nint)layoutManager) + 0x38);
if(status != 0 && status != 5)
return;

var next = _queuedTransitions.Dequeue();
SetFestivalImmediately(next);
}
_gameMainInterop.SetActiveFestivals(festivals[0], festivals[1], festivals[2], festivals[3]);
}
else
{
_gameMainInterop.QueueActiveFestivals(festivals[0], festivals[1], festivals[2], festivals[3]);
}
}

Expand Down Expand Up @@ -137,47 +185,8 @@ private void UpdateFestivalList()
}
}

public void SetFestivalOverride(ushort festivalId)
{
if(!CheckFestivalRestrictions(festivalId))
return;

if(_originalFestival == null)
_originalFestival = CurrentFestivalId;

_queuedTransitions.Enqueue(festivalId);
}

public void ResetFestivalOverride()
{
if(_originalFestival != null)
{
SetFestivalOverride((ushort) _originalFestival);
_originalFestival = null;
}
}

private unsafe void SetFestivalImmediately(ushort festivalId)
{
var layoutWorld = LayoutWorld.Instance();
if(layoutWorld != null)
{
var layoutManager = layoutWorld->ActiveLayout;

if(layoutManager != null)
{
var layout = new FestivalArgs
{
FestivalId = festivalId
};


_updateFestival(layoutManager, layout);
}
}
}

private bool CheckFestivalRestrictions(ushort festivalId)
private bool CheckFestivalRestrictions(uint festivalId)
{
var festival = _festivalEntries.SingleOrDefault(i => i.Id == festivalId);
if(festival == null) return false;
Expand Down Expand Up @@ -208,38 +217,26 @@ private void Instance_OnGPoseStateChange(GPoseState gposeState)
{
if(gposeState == GPoseState.Exiting)
{
ResetFestivalOverride();
ResetFestivals();
}
}

private void ClientState_TerritoryChanged(object? sender, ushort e)
{
_queuedTransitions.Clear();
_originalFestival = null;
_pendingChanges.Clear();
_originalState = null;
}

public override void Stop()
{
ResetFestivals(true);
GPoseService.Instance.OnGPoseStateChange -= Instance_OnGPoseStateChange;
Dalamud.ClientState.TerritoryChanged -= ClientState_TerritoryChanged;

if(_originalFestival != null && CurrentFestivalId != _originalFestival)
SetFestivalImmediately((ushort) _originalFestival);

}

[StructLayout(LayoutKind.Explicit, Size = 0x10)]
private struct FestivalArgs
{
[FieldOffset(0x0)] public uint FestivalId;
[FieldOffset(0x4)] public uint a1;
[FieldOffset(0x8)] public uint a2;
[FieldOffset(0xC)] public uint a3;
}

private class FestivalFileEntry
{
public ushort Id { get; set; }
public uint Id { get; set; }
public string Name { get; set; } = null!;
public bool Unsafe { get; set; }
public FestivalAreaExclusion? AreaExclusion { get; set; }
Expand All @@ -262,7 +259,7 @@ public class FestivalAreaExclusionBoundary

public class FestivalEntry
{
public ushort Id { get; set; }
public uint Id { get; set; }
public string Name { get; set; } = "Unknown";
public bool Unknown { get; set; }
public bool Unsafe { get; set; }
Expand Down
Loading

0 comments on commit 5277128

Please sign in to comment.