Initial Commit
This commit is contained in:
commit
161bf7e39c
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/.idea
|
||||
*.user
|
16
ARControl.sln
Normal file
16
ARControl.sln
Normal file
@ -0,0 +1,16 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ARControl", "ARControl\ARControl.csproj", "{B33BF820-56C2-45A1-AEEC-3DCF526DBF42}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{B33BF820-56C2-45A1-AEEC-3DCF526DBF42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B33BF820-56C2-45A1-AEEC-3DCF526DBF42}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B33BF820-56C2-45A1-AEEC-3DCF526DBF42}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B33BF820-56C2-45A1-AEEC-3DCF526DBF42}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
3
ARControl/.gitignore
vendored
Normal file
3
ARControl/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/dist
|
||||
/obj
|
||||
/bin
|
83
ARControl/ARControl.csproj
Normal file
83
ARControl/ARControl.csproj
Normal file
@ -0,0 +1,83 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<Version>1.0</Version>
|
||||
<LangVersion>11.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<OutputPath>dist</OutputPath>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<DebugType>portable</DebugType>
|
||||
<PathMap Condition="$(SolutionDir) != ''">$(SolutionDir)=X:\</PathMap>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<DalamudLibPath>$(appdata)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath>
|
||||
<AutoRetainerLibPath>$(appdata)\XIVLauncher\installedPlugins\AutoRetainer\4.1.2.5\</AutoRetainerLibPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))'">
|
||||
<DalamudLibPath>$(DALAMUD_HOME)/</DalamudLibPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dalamud.ContextMenu" Version="1.2.3"/>
|
||||
<PackageReference Include="DalamudPackager" Version="2.1.11"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Dalamud">
|
||||
<HintPath>$(DalamudLibPath)Dalamud.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="ImGui.NET">
|
||||
<HintPath>$(DalamudLibPath)ImGui.NET.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="ImGuiScene">
|
||||
<HintPath>$(DalamudLibPath)ImGuiScene.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Lumina">
|
||||
<HintPath>$(DalamudLibPath)Lumina.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Lumina.Excel">
|
||||
<HintPath>$(DalamudLibPath)Lumina.Excel.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json">
|
||||
<HintPath>$(DalamudLibPath)Newtonsoft.Json.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="FFXIVClientStructs">
|
||||
<HintPath>$(DalamudLibPath)FFXIVClientStructs.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="FFXIVClientStructs">
|
||||
<HintPath>$(DalamudLibPath)FFXIVClientStructs.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="AutoRetainerAPI">
|
||||
<HintPath>$(AutoRetainerLibPath)AutoRetainerAPI.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="ECommons">
|
||||
<HintPath>$(AutoRetainerLibPath)ECommons.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="ClickLib">
|
||||
<HintPath>$(AutoRetainerLibPath)ClickLib.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="External\" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="RenameLatestZip" AfterTargets="PackagePlugin">
|
||||
<Exec Command="rename $(OutDir)$(AssemblyName)\latest.zip $(AssemblyName)-$(Version).zip"/>
|
||||
</Target>
|
||||
</Project>
|
7
ARControl/ARControl.json
Normal file
7
ARControl/ARControl.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"Name": "ARC",
|
||||
"Author": "Liza Carvelli",
|
||||
"Punchline": "Better AutoRetainer Venture Distribution",
|
||||
"Description": "",
|
||||
"RepoUrl": "https://git.carvel.li/liza/ARControl"
|
||||
}
|
303
ARControl/AutoRetainerControlPlugin.cs
Normal file
303
ARControl/AutoRetainerControlPlugin.cs
Normal file
@ -0,0 +1,303 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using ARControl.GameData;
|
||||
using ARControl.Windows;
|
||||
using AutoRetainerAPI;
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Game.ClientState;
|
||||
using Dalamud.Game.Command;
|
||||
using Dalamud.Game.Gui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Components;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Logging;
|
||||
using Dalamud.Plugin;
|
||||
using ECommons;
|
||||
using ImGuiNET;
|
||||
|
||||
namespace ARControl;
|
||||
|
||||
public sealed class AutoRetainerControlPlugin : IDalamudPlugin
|
||||
{
|
||||
private readonly WindowSystem _windowSystem = new(nameof(AutoRetainerControlPlugin));
|
||||
|
||||
private readonly DalamudPluginInterface _pluginInterface;
|
||||
private readonly DataManager _dataManager;
|
||||
private readonly ClientState _clientState;
|
||||
private readonly ChatGui _chatGui;
|
||||
private readonly CommandManager _commandManager;
|
||||
|
||||
private readonly Configuration _configuration;
|
||||
private readonly GameCache _gameCache;
|
||||
private readonly ConfigWindow _configWindow;
|
||||
private readonly AutoRetainerApi _autoRetainerApi;
|
||||
|
||||
public AutoRetainerControlPlugin(DalamudPluginInterface pluginInterface, DataManager dataManager,
|
||||
ClientState clientState, ChatGui chatGui, CommandManager commandManager)
|
||||
{
|
||||
_pluginInterface = pluginInterface;
|
||||
_dataManager = dataManager;
|
||||
_clientState = clientState;
|
||||
_chatGui = chatGui;
|
||||
_commandManager = commandManager;
|
||||
|
||||
_configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration();
|
||||
|
||||
_gameCache = new GameCache(_dataManager);
|
||||
_configWindow = new ConfigWindow(_pluginInterface, _configuration, _gameCache, _clientState, _commandManager);
|
||||
_windowSystem.AddWindow(_configWindow);
|
||||
|
||||
ECommonsMain.Init(_pluginInterface, this);
|
||||
_autoRetainerApi = new();
|
||||
|
||||
_pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
|
||||
_pluginInterface.UiBuilder.OpenConfigUi += _configWindow.Toggle;
|
||||
_autoRetainerApi.OnSendRetainerToVenture += SendRetainerToVenture;
|
||||
_autoRetainerApi.OnRetainerPostVentureTaskDraw += RetainerTaskButtonDraw;
|
||||
_clientState.TerritoryChanged += TerritoryChanged;
|
||||
_commandManager.AddHandler("/arc", new CommandInfo(ProcessCommand));
|
||||
|
||||
if (_autoRetainerApi.Ready)
|
||||
Sync();
|
||||
}
|
||||
|
||||
public string Name => "ARC";
|
||||
|
||||
private void SendRetainerToVenture(string retainerName)
|
||||
{
|
||||
var ch = _configuration.Characters.SingleOrDefault(x => x.LocalContentId == _clientState.LocalContentId);
|
||||
if (ch == null)
|
||||
{
|
||||
PluginLog.Information("No character information found");
|
||||
}
|
||||
else if (!ch.Managed)
|
||||
{
|
||||
PluginLog.Information("Character is not managed");
|
||||
}
|
||||
else
|
||||
{
|
||||
var retainer = ch.Retainers.SingleOrDefault(x => x.Name == retainerName);
|
||||
if (retainer == null)
|
||||
{
|
||||
PluginLog.Information("No retainer information found");
|
||||
}
|
||||
else if (!retainer.Managed)
|
||||
{
|
||||
PluginLog.Information("Retainer is not managed");
|
||||
}
|
||||
else
|
||||
{
|
||||
PluginLog.Information("Checking tasks...");
|
||||
Sync();
|
||||
foreach (var queuedItem in _configuration.QueuedItems.Where(x => x.RemainingQuantity > 0))
|
||||
{
|
||||
PluginLog.Information($"Checking venture info for itemId {queuedItem.ItemId}");
|
||||
var venture = _gameCache.Ventures
|
||||
.Where(x => retainer.Level >= x.Level)
|
||||
.FirstOrDefault(x => x.ItemId == queuedItem.ItemId && x.MatchesJob(retainer.Job));
|
||||
if (venture == null)
|
||||
{
|
||||
PluginLog.Information($"No applicable venture found for itemId {queuedItem.ItemId}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var itemToGather = _gameCache.ItemsToGather.FirstOrDefault(x => x.ItemId == queuedItem.ItemId);
|
||||
if (itemToGather != null && !ch.GatheredItems.Contains(itemToGather.GatheredItemId))
|
||||
{
|
||||
PluginLog.Information($"Character hasn't gathered {venture.Name} yet");
|
||||
}
|
||||
else
|
||||
{
|
||||
PluginLog.Information(
|
||||
$"Found venture {venture.Name}, row = {venture.RowId}, checking if it is suitable");
|
||||
VentureReward? reward = null;
|
||||
if (venture.CategoryName is "MIN" or "BTN")
|
||||
{
|
||||
if (retainer.Gathering >= venture.RequiredGathering)
|
||||
reward = venture.Rewards.Last(
|
||||
x => retainer.Perception >= x.PerceptionMinerBotanist);
|
||||
}
|
||||
else if (venture.CategoryName == "FSH")
|
||||
{
|
||||
if (retainer.Gathering >= venture.RequiredGathering)
|
||||
reward = venture.Rewards.Last(
|
||||
x => retainer.Perception >= x.PerceptionFisher);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (retainer.ItemLevel >= venture.ItemLevelCombat)
|
||||
reward = venture.Rewards.Last(
|
||||
x => retainer.ItemLevel >= x.ItemLevelCombat);
|
||||
}
|
||||
|
||||
if (reward == null)
|
||||
{
|
||||
PluginLog.Information(
|
||||
"Retainer doesn't have enough stats for the venture and would return no items");
|
||||
}
|
||||
else
|
||||
{
|
||||
_chatGui.Print(
|
||||
$"ARC → Overriding venture to collect {reward.Quantity}x {venture.Name}.");
|
||||
PluginLog.Information(
|
||||
$"Setting AR to use venture {venture.RowId}, which should retrieve {reward.Quantity}x {venture.Name}");
|
||||
_autoRetainerApi.SetVenture(venture.RowId);
|
||||
|
||||
queuedItem.RemainingQuantity =
|
||||
Math.Max(0, queuedItem.RemainingQuantity - reward.Quantity);
|
||||
_pluginInterface.SavePluginConfig(_configuration);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fallback: managed but no venture found
|
||||
if (retainer.LastVenture != 395)
|
||||
{
|
||||
_chatGui.Print("ARC → No tasks left, using QC");
|
||||
PluginLog.Information($"No tasks left (previous venture = {retainer.LastVenture}), using QC");
|
||||
_autoRetainerApi.SetVenture(395);
|
||||
}
|
||||
else
|
||||
PluginLog.Information("Not changing venture plan, already 395");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RetainerTaskButtonDraw(ulong characterId, string retainerName)
|
||||
{
|
||||
Configuration.CharacterConfiguration? characterConfiguration = _configuration.Characters.FirstOrDefault(x => x.LocalContentId == characterId);
|
||||
if (characterConfiguration is not { Managed: true })
|
||||
return;
|
||||
|
||||
Configuration.RetainerConfiguration? retainer = characterConfiguration.Retainers.FirstOrDefault(x => x.Name == retainerName);
|
||||
if (retainer is not { Managed: true })
|
||||
return;
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGuiComponents.IconButton(FontAwesomeIcon.Book);
|
||||
}
|
||||
|
||||
private void TerritoryChanged(object? sender, ushort e) => Sync();
|
||||
|
||||
public void Sync()
|
||||
{
|
||||
bool save = false;
|
||||
|
||||
// FIXME This should have a way to get blacklisted character ids
|
||||
foreach (ulong registeredCharacterId in _autoRetainerApi.GetRegisteredCharacters())
|
||||
{
|
||||
PluginLog.Information($"ch → {registeredCharacterId:X}");
|
||||
var offlineCharacterData = _autoRetainerApi.GetOfflineCharacterData(registeredCharacterId);
|
||||
if (offlineCharacterData.ExcludeRetainer)
|
||||
continue;
|
||||
|
||||
var character = _configuration.Characters.SingleOrDefault(x => x.LocalContentId == registeredCharacterId);
|
||||
if (character == null)
|
||||
{
|
||||
character = new Configuration.CharacterConfiguration
|
||||
{
|
||||
LocalContentId = registeredCharacterId,
|
||||
CharacterName = offlineCharacterData.Name,
|
||||
WorldName = offlineCharacterData.World,
|
||||
Managed = false,
|
||||
};
|
||||
|
||||
save = true;
|
||||
_configuration.Characters.Add(character);
|
||||
}
|
||||
|
||||
if (character.GatheredItems != offlineCharacterData.UnlockedGatheringItems)
|
||||
{
|
||||
character.GatheredItems = offlineCharacterData.UnlockedGatheringItems;
|
||||
save = true;
|
||||
}
|
||||
|
||||
foreach (var retainerData in offlineCharacterData.RetainerData)
|
||||
{
|
||||
var retainer = character.Retainers.SingleOrDefault(x => x.Name == retainerData.Name);
|
||||
if (retainer == null)
|
||||
{
|
||||
retainer = new Configuration.RetainerConfiguration
|
||||
{
|
||||
Name = retainerData.Name,
|
||||
Managed = false,
|
||||
};
|
||||
|
||||
save = true;
|
||||
character.Retainers.Add(retainer);
|
||||
}
|
||||
|
||||
if (retainer.DisplayOrder != retainerData.DisplayOrder)
|
||||
{
|
||||
retainer.DisplayOrder = retainerData.DisplayOrder;
|
||||
save = true;
|
||||
}
|
||||
|
||||
if (retainer.Level != retainerData.Level)
|
||||
{
|
||||
retainer.Level = retainerData.Level;
|
||||
save = true;
|
||||
}
|
||||
|
||||
if (retainer.Job != retainerData.Job)
|
||||
{
|
||||
retainer.Job = retainerData.Job;
|
||||
save = true;
|
||||
}
|
||||
|
||||
if (retainer.LastVenture != retainerData.VentureID)
|
||||
{
|
||||
retainer.LastVenture = retainerData.VentureID;
|
||||
save = true;
|
||||
}
|
||||
|
||||
var additionalData =
|
||||
_autoRetainerApi.GetAdditionalRetainerData(registeredCharacterId, retainerData.Name);
|
||||
if (retainer.ItemLevel != additionalData.Ilvl)
|
||||
{
|
||||
retainer.ItemLevel = additionalData.Ilvl;
|
||||
save = true;
|
||||
}
|
||||
|
||||
if (retainer.Gathering != additionalData.Gathering)
|
||||
{
|
||||
retainer.Gathering = additionalData.Gathering;
|
||||
save = true;
|
||||
}
|
||||
|
||||
if (retainer.Perception != additionalData.Perception)
|
||||
{
|
||||
retainer.Perception = additionalData.Perception;
|
||||
save = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (save)
|
||||
_pluginInterface.SavePluginConfig(_configuration);
|
||||
}
|
||||
|
||||
private void ProcessCommand(string command, string arguments)
|
||||
{
|
||||
if (arguments == "sync")
|
||||
Sync();
|
||||
else
|
||||
_configWindow.Toggle();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_commandManager.RemoveHandler("/arc");
|
||||
_clientState.TerritoryChanged -= TerritoryChanged;
|
||||
_autoRetainerApi.OnRetainerPostVentureTaskDraw -= RetainerTaskButtonDraw;
|
||||
_autoRetainerApi.OnSendRetainerToVenture -= SendRetainerToVenture;
|
||||
_pluginInterface.UiBuilder.OpenConfigUi -= _configWindow.Toggle;
|
||||
_pluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
|
||||
|
||||
_autoRetainerApi.Dispose();
|
||||
ECommonsMain.Dispose();
|
||||
|
||||
}
|
||||
}
|
45
ARControl/Configuration.cs
Normal file
45
ARControl/Configuration.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Dalamud.Configuration;
|
||||
|
||||
namespace ARControl;
|
||||
|
||||
internal sealed class Configuration : IPluginConfiguration
|
||||
{
|
||||
public int Version { get; set; } = 1;
|
||||
|
||||
public List<QueuedItem> QueuedItems { get; set; } = new();
|
||||
public List<CharacterConfiguration> Characters { get; set; } = new();
|
||||
|
||||
public sealed class QueuedItem
|
||||
{
|
||||
public required uint ItemId { get; set; }
|
||||
public required int RemainingQuantity { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CharacterConfiguration
|
||||
{
|
||||
public required ulong LocalContentId { get; set; }
|
||||
public required string CharacterName { get; set; }
|
||||
public required string WorldName { get; set; }
|
||||
public required bool Managed { get; set; }
|
||||
|
||||
public List<RetainerConfiguration> Retainers { get; set; } = new();
|
||||
public HashSet<uint> GatheredItems { get; set; } = new();
|
||||
|
||||
public override string ToString() => $"{CharacterName} @ {WorldName}";
|
||||
}
|
||||
|
||||
public sealed class RetainerConfiguration
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
public required bool Managed { get; set; }
|
||||
public int DisplayOrder { get; set; }
|
||||
public int Level { get; set; }
|
||||
public uint Job { get; set; }
|
||||
public uint LastVenture { get; set; }
|
||||
public int ItemLevel { get; set; }
|
||||
public int Gathering { get; set; }
|
||||
public int Perception { get; set; }
|
||||
}
|
||||
}
|
21
ARControl/DalamudPackager.targets
Normal file
21
ARControl/DalamudPackager.targets
Normal file
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project>
|
||||
<Target Name="PackagePlugin" AfterTargets="Build" Condition="'$(Configuration)' == 'Debug'">
|
||||
<DalamudPackager
|
||||
ProjectDir="$(ProjectDir)"
|
||||
OutputPath="$(OutputPath)"
|
||||
AssemblyName="$(AssemblyName)"
|
||||
MakeZip="false"
|
||||
VersionComponents="2"/>
|
||||
</Target>
|
||||
|
||||
<Target Name="PackagePlugin" AfterTargets="Build" Condition="'$(Configuration)' == 'Release'">
|
||||
<DalamudPackager
|
||||
ProjectDir="$(ProjectDir)"
|
||||
OutputPath="$(OutputPath)"
|
||||
AssemblyName="$(AssemblyName)"
|
||||
MakeZip="true"
|
||||
VersionComponents="2"
|
||||
Exclude="ARDiscard.deps.json;AutoRetainerAPI.pdb;ClickLib.pdb;ClickLib.xml;ECommons.pdb;ECommons.xml"/>
|
||||
</Target>
|
||||
</Project>
|
30
ARControl/GameData/GameCache.cs
Normal file
30
ARControl/GameData/GameCache.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dalamud.Data;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
|
||||
namespace ARControl.GameData;
|
||||
|
||||
internal sealed class GameCache
|
||||
{
|
||||
public GameCache(DataManager dataManager)
|
||||
{
|
||||
Jobs = dataManager.GetExcelSheet<ClassJob>()!.ToDictionary(x => x.RowId, x => x.Abbreviation.ToString());
|
||||
Ventures = dataManager.GetExcelSheet<RetainerTask>()!
|
||||
.Where(x => x.RowId > 0 && !x.IsRandom && x.Task != 0)
|
||||
.Select(x => new Venture(dataManager, x))
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
ItemsToGather = dataManager.GetExcelSheet<GatheringItem>()!
|
||||
.Where(x => x.RowId > 0 && x.RowId < 10_000 && x.Item != 0 && x.Quest.Row == 0)
|
||||
.Where(x => Ventures.Any(y => y.ItemId == x.Item))
|
||||
.Select(x => new ItemToGather(dataManager, x))
|
||||
.OrderBy(x => x.Name)
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<uint, string> Jobs { get; }
|
||||
public IReadOnlyList<Venture> Ventures { get; }
|
||||
public IReadOnlyList<ItemToGather> ItemsToGather { get; }
|
||||
}
|
19
ARControl/GameData/ItemToGather.cs
Normal file
19
ARControl/GameData/ItemToGather.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using Dalamud.Data;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
|
||||
namespace ARControl.GameData;
|
||||
|
||||
internal sealed class ItemToGather
|
||||
{
|
||||
public ItemToGather(DataManager dataManager, GatheringItem item)
|
||||
{
|
||||
GatheredItemId = item.RowId;
|
||||
ItemId = item.Item;
|
||||
Name = dataManager.GetExcelSheet<Item>()!.GetRow((uint)item.Item)!.Name.ToString();
|
||||
}
|
||||
|
||||
|
||||
public uint GatheredItemId { get; }
|
||||
public int ItemId { get; }
|
||||
public string Name { get; }
|
||||
}
|
93
ARControl/GameData/Venture.cs
Normal file
93
ARControl/GameData/Venture.cs
Normal file
@ -0,0 +1,93 @@
|
||||
using System.Collections.Generic;
|
||||
using Dalamud.Data;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
|
||||
namespace ARControl.GameData;
|
||||
|
||||
internal sealed class Venture
|
||||
{
|
||||
public Venture(DataManager dataManager, RetainerTask retainerTask)
|
||||
{
|
||||
RowId = retainerTask.RowId;
|
||||
Category = retainerTask.ClassJobCategory.Value!;
|
||||
|
||||
var taskDetails = dataManager.GetExcelSheet<RetainerTaskNormal>()!.GetRow(retainerTask.Task)!;
|
||||
var taskParameters = retainerTask.RetainerTaskParameter.Value!;
|
||||
ItemId = taskDetails.Item.Row;
|
||||
Name = taskDetails.Item.Value!.Name.ToString();
|
||||
Level = retainerTask.RetainerLevel;
|
||||
ItemLevelCombat = retainerTask.RequiredItemLevel;
|
||||
RequiredGathering = retainerTask.RequiredGathering;
|
||||
Rewards = new List<VentureReward>
|
||||
{
|
||||
new VentureReward
|
||||
{
|
||||
Quantity = taskDetails.Quantity[0],
|
||||
ItemLevelCombat = 0,
|
||||
PerceptionMinerBotanist = 0,
|
||||
PerceptionFisher = 0,
|
||||
},
|
||||
new VentureReward
|
||||
{
|
||||
Quantity = taskDetails.Quantity[1],
|
||||
ItemLevelCombat = taskParameters.ItemLevelDoW[0],
|
||||
PerceptionMinerBotanist = taskParameters.PerceptionDoL[0],
|
||||
PerceptionFisher = taskParameters.PerceptionFSH[0],
|
||||
},
|
||||
new VentureReward
|
||||
{
|
||||
Quantity = taskDetails.Quantity[2],
|
||||
ItemLevelCombat = taskParameters.ItemLevelDoW[1],
|
||||
PerceptionMinerBotanist = taskParameters.PerceptionDoL[1],
|
||||
PerceptionFisher = taskParameters.PerceptionFSH[1],
|
||||
},
|
||||
new VentureReward
|
||||
{
|
||||
Quantity = taskDetails.Quantity[3],
|
||||
ItemLevelCombat = taskParameters.ItemLevelDoW[2],
|
||||
PerceptionMinerBotanist = taskParameters.PerceptionDoL[2],
|
||||
PerceptionFisher = taskParameters.PerceptionFSH[2],
|
||||
},
|
||||
new VentureReward
|
||||
{
|
||||
Quantity = taskDetails.Quantity[4],
|
||||
ItemLevelCombat = taskParameters.ItemLevelDoW[3],
|
||||
PerceptionMinerBotanist = taskParameters.PerceptionDoL[3],
|
||||
PerceptionFisher = taskParameters.PerceptionFSH[3],
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public uint RowId { get; }
|
||||
public ClassJobCategory Category { get; }
|
||||
|
||||
public string? CategoryName
|
||||
{
|
||||
get
|
||||
{
|
||||
return Category.RowId switch
|
||||
{
|
||||
17 => "MIN",
|
||||
18 => "BTN",
|
||||
19 => "FSH",
|
||||
_ => "DoWM",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public uint ItemId { get; }
|
||||
public string Name { get; }
|
||||
public byte Level { get; }
|
||||
public ushort ItemLevelCombat { get; }
|
||||
public ushort RequiredGathering { get; set; }
|
||||
|
||||
public List<VentureReward> Rewards { get; }
|
||||
|
||||
public bool MatchesJob(uint job)
|
||||
{
|
||||
if (Category.RowId >= 17 && Category.RowId <= 19)
|
||||
return Category.RowId == job + 1;
|
||||
else
|
||||
return job is < 16 or > 18;
|
||||
}
|
||||
}
|
9
ARControl/GameData/VentureReward.cs
Normal file
9
ARControl/GameData/VentureReward.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace ARControl.GameData;
|
||||
|
||||
internal sealed class VentureReward
|
||||
{
|
||||
public required byte Quantity { get; init; }
|
||||
public required short ItemLevelCombat { get; init; }
|
||||
public required short PerceptionMinerBotanist { get; init; }
|
||||
public required short PerceptionFisher { get; init; }
|
||||
}
|
408
ARControl/Windows/ConfigWindow.cs
Normal file
408
ARControl/Windows/ConfigWindow.cs
Normal file
@ -0,0 +1,408 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using ARControl.GameData;
|
||||
using Dalamud.Game.ClientState;
|
||||
using Dalamud.Game.Command;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Components;
|
||||
using Dalamud.Interface.Style;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Logging;
|
||||
using Dalamud.Plugin;
|
||||
using ECommons.ImGuiMethods;
|
||||
using ImGuiNET;
|
||||
|
||||
namespace ARControl.Windows;
|
||||
|
||||
internal sealed class ConfigWindow : Window
|
||||
{
|
||||
private const byte MaxLevel = 90;
|
||||
|
||||
private static readonly Vector4 ColorGreen = ImGuiColors.HealerGreen;
|
||||
private static readonly Vector4 ColorRed = ImGuiColors.DalamudRed;
|
||||
private static readonly Vector4 ColorGrey = ImGuiColors.DalamudGrey;
|
||||
|
||||
private readonly DalamudPluginInterface _pluginInterface;
|
||||
private readonly Configuration _configuration;
|
||||
private readonly GameCache _gameCache;
|
||||
private readonly ClientState _clientState;
|
||||
private readonly CommandManager _commandManager;
|
||||
|
||||
private string _searchString = string.Empty;
|
||||
private Configuration.QueuedItem? _dragDropSource;
|
||||
private bool _enableDragDrop;
|
||||
private bool _checkPerCharacter = true;
|
||||
private bool _onlyShowMissing = true;
|
||||
|
||||
public ConfigWindow(
|
||||
DalamudPluginInterface pluginInterface,
|
||||
Configuration configuration,
|
||||
GameCache gameCache,
|
||||
ClientState clientState,
|
||||
CommandManager commandManager)
|
||||
: base("ARC###ARControlConfig")
|
||||
{
|
||||
_pluginInterface = pluginInterface;
|
||||
_configuration = configuration;
|
||||
_gameCache = gameCache;
|
||||
_clientState = clientState;
|
||||
_commandManager = commandManager;
|
||||
}
|
||||
|
||||
public override void Draw()
|
||||
{
|
||||
if (ImGui.BeginTabBar("ARConfigTabs"))
|
||||
{
|
||||
DrawItemQueue();
|
||||
DrawCharacters();
|
||||
DrawGatheredItemsToCheck();
|
||||
ImGui.EndTabBar();
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void DrawItemQueue()
|
||||
{
|
||||
if (ImGui.BeginTabItem("Venture Queue"))
|
||||
{
|
||||
if (ImGui.BeginCombo("Venture...##VentureSelection", ""))
|
||||
{
|
||||
ImGuiEx.SetNextItemFullWidth();
|
||||
ImGui.InputTextWithHint("", "Filter...", ref _searchString, 256);
|
||||
|
||||
foreach (var ventures in _gameCache.Ventures
|
||||
.Where(x => x.Name.ToLower().Contains(_searchString.ToLower()))
|
||||
.OrderBy(x => x.Level)
|
||||
.ThenBy(x => x.Name)
|
||||
.ThenBy(x => x.ItemId)
|
||||
.GroupBy(x => x.ItemId))
|
||||
{
|
||||
var venture = ventures.First();
|
||||
if (ImGui.Selectable(
|
||||
$"{venture.Name} ({string.Join(" ", ventures.Select(x => x.CategoryName))})##SelectVenture{venture.RowId}"))
|
||||
{
|
||||
_configuration.QueuedItems.Add(new Configuration.QueuedItem
|
||||
{
|
||||
ItemId = venture.ItemId,
|
||||
RemainingQuantity = 0,
|
||||
});
|
||||
_searchString = string.Empty;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndCombo();
|
||||
}
|
||||
|
||||
ImGui.Checkbox("Enable Drag&Drop", ref _enableDragDrop);
|
||||
ImGui.Separator();
|
||||
|
||||
ImGui.Indent(30);
|
||||
|
||||
Configuration.QueuedItem? itemToRemove = null;
|
||||
Configuration.QueuedItem? itemToAdd = null;
|
||||
int indexToAdd = 0;
|
||||
for (int i = 0; i < _configuration.QueuedItems.Count; ++i)
|
||||
{
|
||||
var item = _configuration.QueuedItems[i];
|
||||
ImGui.PushID($"QueueItem{i}");
|
||||
var ventures = _gameCache.Ventures.Where(x => x.ItemId == item.ItemId).ToList();
|
||||
var venture = ventures.First();
|
||||
|
||||
if (!_enableDragDrop)
|
||||
{
|
||||
ImGui.SetNextItemWidth(130);
|
||||
int quantity = item.RemainingQuantity;
|
||||
if (ImGui.InputInt($"{venture.Name} ({string.Join(" ", ventures.Select(x => x.CategoryName))})",
|
||||
ref quantity, 100))
|
||||
{
|
||||
item.RemainingQuantity = quantity;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.Selectable($"{item.RemainingQuantity}x {venture.Name}");
|
||||
|
||||
if (ImGui.BeginDragDropSource())
|
||||
{
|
||||
ImGui.SetDragDropPayload("ArcDragDrop", nint.Zero, 0);
|
||||
_dragDropSource = item;
|
||||
|
||||
ImGui.EndDragDropSource();
|
||||
}
|
||||
|
||||
if (ImGui.BeginDragDropTarget())
|
||||
{
|
||||
if (_dragDropSource != null && ImGui.AcceptDragDropPayload("ArcDragDrop").NativePtr != null)
|
||||
{
|
||||
itemToAdd = _dragDropSource;
|
||||
indexToAdd = i;
|
||||
|
||||
_dragDropSource = null;
|
||||
}
|
||||
|
||||
ImGui.EndDragDropTarget();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.OpenPopupOnItemClick($"###ctx{i}", ImGuiPopupFlags.MouseButtonRight);
|
||||
if (ImGui.BeginPopup($"###ctx{i}"))
|
||||
{
|
||||
if (ImGui.Selectable($"Remove {venture.Name}"))
|
||||
itemToRemove = item;
|
||||
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
|
||||
ImGui.PopID();
|
||||
}
|
||||
|
||||
if (itemToRemove != null)
|
||||
{
|
||||
_configuration.QueuedItems.Remove(itemToRemove);
|
||||
Save();
|
||||
}
|
||||
|
||||
if (itemToAdd != null)
|
||||
{
|
||||
PluginLog.Information($"Updating {itemToAdd.ItemId} → {indexToAdd}");
|
||||
_configuration.QueuedItems.Remove(itemToAdd);
|
||||
_configuration.QueuedItems.Insert(indexToAdd, itemToAdd);
|
||||
Save();
|
||||
}
|
||||
|
||||
ImGui.Unindent(30);
|
||||
ImGui.EndTabItem();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawCharacters()
|
||||
{
|
||||
if (ImGui.BeginTabItem("Retainers"))
|
||||
{
|
||||
foreach (var world in _configuration.Characters
|
||||
.Where(x => x.Retainers.Any(y => y.Job != 0))
|
||||
.OrderBy(x => x.LocalContentId)
|
||||
.GroupBy(x => x.WorldName))
|
||||
{
|
||||
ImGui.CollapsingHeader(world.Key, ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.OpenOnArrow | ImGuiTreeNodeFlags.Bullet);
|
||||
foreach (var character in world)
|
||||
{
|
||||
ImGui.PushID($"Char{character.LocalContentId}");
|
||||
|
||||
ImGui.PushItemWidth(ImGui.GetFontSize() * 30);
|
||||
Vector4 buttonColor = new Vector4();
|
||||
if (character.Managed && character.Retainers.Count > 0)
|
||||
{
|
||||
if (character.Retainers.All(x => x.Managed))
|
||||
buttonColor = ImGuiColors.HealerGreen;
|
||||
else if (character.Retainers.All(x => !x.Managed))
|
||||
buttonColor = ImGuiColors.DalamudRed;
|
||||
else
|
||||
buttonColor = ImGuiColors.DalamudOrange;
|
||||
}
|
||||
|
||||
if (ImGuiComponents.IconButton(FontAwesomeIcon.Book, buttonColor))
|
||||
{
|
||||
character.Managed = !character.Managed;
|
||||
Save();
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (ImGui.CollapsingHeader(
|
||||
$"{character.CharacterName} {(character.Managed ? $"({character.Retainers.Count(x => x.Managed)} / {character.Retainers.Count})" : "")}###{character.LocalContentId}"))
|
||||
{
|
||||
ImGui.Indent(30);
|
||||
foreach (var retainer in character.Retainers.Where(x => x.Job > 0).OrderBy(x => x.DisplayOrder))
|
||||
{
|
||||
ImGui.BeginDisabled(retainer.Level < MaxLevel);
|
||||
|
||||
bool managed = retainer.Managed && retainer.Level == MaxLevel;
|
||||
ImGui.Text(_gameCache.Jobs[retainer.Job]);
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Checkbox($"{retainer.Name}###Retainer{retainer.Name}{retainer.DisplayOrder}",
|
||||
ref managed))
|
||||
{
|
||||
retainer.Managed = managed;
|
||||
Save();
|
||||
}
|
||||
|
||||
ImGui.EndDisabled();
|
||||
}
|
||||
|
||||
ImGui.Unindent(30);
|
||||
}
|
||||
|
||||
ImGui.PopID();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndTabItem();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawGatheredItemsToCheck()
|
||||
{
|
||||
if (ImGui.BeginTabItem("Locked Items"))
|
||||
{
|
||||
ImGui.Checkbox("Group by character", ref _checkPerCharacter);
|
||||
ImGui.Checkbox("Only show missing items", ref _onlyShowMissing);
|
||||
ImGui.Separator();
|
||||
|
||||
var itemsToCheck =
|
||||
_configuration.QueuedItems
|
||||
.Select(x => x.ItemId)
|
||||
.Distinct()
|
||||
.Select(itemId => new
|
||||
{
|
||||
GatheredItem = _gameCache.ItemsToGather.SingleOrDefault(x => x.ItemId == itemId),
|
||||
Ventures = _gameCache.Ventures.Where(x => x.ItemId == itemId).ToList()
|
||||
})
|
||||
.Where(x => x.GatheredItem != null && x.Ventures.Count > 0)
|
||||
.Select(x => new CheckedItem
|
||||
{
|
||||
GatheredItem = x.GatheredItem!,
|
||||
Ventures = x.Ventures,
|
||||
ItemId = x.Ventures.First().ItemId,
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var charactersToCheck = _configuration.Characters
|
||||
.Where(x => x.Managed)
|
||||
.OrderBy(x => x.WorldName)
|
||||
.ThenBy(x => x.LocalContentId)
|
||||
.Select(x => new CheckedCharacter(x, itemsToCheck))
|
||||
.ToList();
|
||||
|
||||
if (_checkPerCharacter)
|
||||
{
|
||||
foreach (var ch in charactersToCheck.Where(x => x.ToCheck(_onlyShowMissing).Any()))
|
||||
{
|
||||
bool currentCharacter = _clientState.LocalContentId == ch.Character.LocalContentId;
|
||||
ImGui.BeginDisabled(currentCharacter);
|
||||
if (ImGuiComponents.IconButton($"SwitchChacters{ch.Character.LocalContentId}",
|
||||
FontAwesomeIcon.DoorOpen))
|
||||
{
|
||||
_commandManager.ProcessCommand($"/ays relog {ch.Character.CharacterName}@{ch.Character.WorldName}");
|
||||
}
|
||||
|
||||
ImGui.EndDisabled();
|
||||
ImGui.SameLine();
|
||||
|
||||
if (currentCharacter)
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.HealerGreen);
|
||||
|
||||
bool expanded = ImGui.CollapsingHeader($"{ch.Character}###GatheredCh{ch.Character.LocalContentId}");
|
||||
if (currentCharacter)
|
||||
ImGui.PopStyleColor();
|
||||
|
||||
if (expanded)
|
||||
{
|
||||
ImGui.Indent(30);
|
||||
foreach (var item in itemsToCheck.Where(x =>
|
||||
ch.ToCheck(_onlyShowMissing).ContainsKey(x.ItemId)))
|
||||
{
|
||||
var color = ch.Items[item.ItemId];
|
||||
if (color != ColorGrey)
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, color);
|
||||
if (currentCharacter && color == ColorRed)
|
||||
{
|
||||
ImGui.Selectable(item.GatheredItem.Name);
|
||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
|
||||
{
|
||||
uint classJob = _clientState.LocalPlayer!.ClassJob.Id;
|
||||
if (classJob == 16)
|
||||
_commandManager.ProcessCommand($"/gathermin {item.GatheredItem.Name}");
|
||||
else if (classJob == 17)
|
||||
_commandManager.ProcessCommand($"/gatherbtn {item.GatheredItem.Name}");
|
||||
else if (classJob == 18)
|
||||
_commandManager.ProcessCommand($"/gatherfish {item.GatheredItem.Name}");
|
||||
else
|
||||
_commandManager.ProcessCommand($"/gather {item.GatheredItem.Name}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.Text(item.GatheredItem.Name);
|
||||
}
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.Unindent(30);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var item in itemsToCheck.Where(x =>
|
||||
charactersToCheck.Any(y => y.ToCheck(_onlyShowMissing).ContainsKey(x.ItemId))))
|
||||
{
|
||||
if (ImGui.CollapsingHeader($"{item.GatheredItem.Name}##Gathered{item.GatheredItem.ItemId}"))
|
||||
{
|
||||
ImGui.Indent(30);
|
||||
foreach (var ch in charactersToCheck)
|
||||
{
|
||||
var color = ch.Items[item.ItemId];
|
||||
if (color == ColorRed || (color == ColorGreen && !_onlyShowMissing))
|
||||
ImGui.TextColored(color, ch.Character.ToString());
|
||||
}
|
||||
|
||||
ImGui.Unindent(30);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndTabItem();
|
||||
}
|
||||
}
|
||||
|
||||
private void Save()
|
||||
{
|
||||
_pluginInterface.SavePluginConfig(_configuration);
|
||||
}
|
||||
|
||||
private sealed class CheckedCharacter
|
||||
{
|
||||
public CheckedCharacter(Configuration.CharacterConfiguration character,
|
||||
List<CheckedItem> itemsToCheck)
|
||||
{
|
||||
Character = character;
|
||||
|
||||
foreach (var item in itemsToCheck)
|
||||
{
|
||||
bool enabled = character.Retainers.Any(x => item.Ventures.Any(v => v.MatchesJob(x.Job)));
|
||||
if (enabled)
|
||||
{
|
||||
if (character.GatheredItems.Contains(item.GatheredItem.GatheredItemId))
|
||||
Items[item.ItemId] = ColorGreen;
|
||||
else
|
||||
Items[item.ItemId] = ColorRed;
|
||||
}
|
||||
else
|
||||
Items[item.ItemId] = ColorGrey;
|
||||
}
|
||||
}
|
||||
|
||||
public Configuration.CharacterConfiguration Character { get; }
|
||||
public Dictionary<uint, Vector4> Items { get; } = new();
|
||||
|
||||
public Dictionary<uint, Vector4> ToCheck(bool onlyShowMissing)
|
||||
{
|
||||
return Items
|
||||
.Where(x => x.Value == ColorRed || (x.Value == ColorGreen && !onlyShowMissing))
|
||||
.ToDictionary(x => x.Key, x => x.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CheckedItem
|
||||
{
|
||||
public required ItemToGather GatheredItem { get; init; }
|
||||
public required List<Venture> Ventures { get; init; }
|
||||
public required uint ItemId { get; init; }
|
||||
}
|
||||
}
|
19
ARControl/packages.lock.json
Normal file
19
ARControl/packages.lock.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"version": 1,
|
||||
"dependencies": {
|
||||
"net7.0-windows7.0": {
|
||||
"Dalamud.ContextMenu": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.2.3, )",
|
||||
"resolved": "1.2.3",
|
||||
"contentHash": "ydemplF7DNcA/LLeongDVzWUD/JV0Fw3EwA2+P0jYq3Le2ZYSt4U8qyJq4FyoChqt0lFG8BxYCAzfeWp4Jmnqw=="
|
||||
},
|
||||
"DalamudPackager": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.1.11, )",
|
||||
"resolved": "2.1.11",
|
||||
"contentHash": "9qlAWoRRTiL/geAvuwR/g6Bcbrd/bJJgVnB/RurBiyKs6srsP0bvpoo8IK+Eg8EA6jWeM6/YJWs66w4FIAzqPw=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
13
README.md
Normal file
13
README.md
Normal file
@ -0,0 +1,13 @@
|
||||
# ARC
|
||||
|
||||
Instead of manually managing retainer plans for each individual character,
|
||||
which can get fairly tedious fairly quickly, we optimize this a bit and
|
||||
only manage "high-level" plans.
|
||||
|
||||
This means we create a list a la:
|
||||
|
||||
- 5000 Cobalt Ore
|
||||
- 2000 Gold Ore
|
||||
|
||||
... and ARC distributes each venture automatically amongst all characters
|
||||
which are included.
|
7
global.json
Normal file
7
global.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "7.0.0",
|
||||
"rollForward": "latestMinor",
|
||||
"allowPrerelease": false
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user