Compare commits
No commits in common. "main" and "4d5dbfd25424211d036c6042276e3680669008a1" have entirely different histories.
main
...
4d5dbfd254
|
@ -9,8 +9,8 @@ tasks:
|
|||
echo 'source $HOME/.nix-profile/etc/profile.d/nix.sh' >> $HOME/.buildenv
|
||||
- build: |
|
||||
cd remote-party-finder/server
|
||||
nix-shell --command 'cargo build --release --target x86_64-unknown-linux-musl'
|
||||
strip -s target/x86_64-unknown-linux-musl/release/remote-party-finder
|
||||
# nix-shell --command 'patchelf --remove-rpath --set-interpreter /usr/lib64/ld-linux-x86-64.so.2 target/x86_64-unknown-linux-musl/release/remote-party-finder'
|
||||
nix-shell --command 'cargo build --release'
|
||||
strip -s target/release/remote-party-finder
|
||||
nix-shell --command 'patchelf --remove-rpath --set-interpreter /usr/lib64/ld-linux-x86-64.so.2 target/release/remote-party-finder'
|
||||
artifacts:
|
||||
- remote-party-finder/server/target/x86_64-unknown-linux-musl/release/remote-party-finder
|
||||
- remote-party-finder/server/target/release/remote-party-finder
|
||||
|
|
|
@ -6,67 +6,68 @@ using System.Linq;
|
|||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading.Tasks;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.Gui.PartyFinder.Types;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace RemotePartyFinder;
|
||||
|
||||
internal class Gatherer : IDisposable {
|
||||
#if DEBUG
|
||||
namespace RemotePartyFinder {
|
||||
internal class Gatherer : IDisposable {
|
||||
#if DEBUG
|
||||
private const string UploadUrl = "http://192.168.174.240:7878/contribute/multiple";
|
||||
#elif RELEASE
|
||||
private const string UploadUrl = "https://xivpf.com/contribute/multiple";
|
||||
#endif
|
||||
#elif RELEASE
|
||||
private const string UploadUrl = "https://xivpf.com/contribute/multiple";
|
||||
#endif
|
||||
|
||||
private Plugin Plugin { get; }
|
||||
private Plugin Plugin { get; }
|
||||
|
||||
private ConcurrentDictionary<int, List<PartyFinderListing>> Batches { get; } = new();
|
||||
private Stopwatch UploadTimer { get; } = new();
|
||||
private HttpClient Client { get; } = new();
|
||||
private ConcurrentDictionary<int, List<PartyFinderListing>> Batches { get; } = new();
|
||||
private Stopwatch UploadTimer { get; } = new();
|
||||
private HttpClient Client { get; } = new();
|
||||
|
||||
internal Gatherer(Plugin plugin) {
|
||||
this.Plugin = plugin;
|
||||
internal Gatherer(Plugin plugin) {
|
||||
this.Plugin = plugin;
|
||||
|
||||
this.UploadTimer.Start();
|
||||
this.UploadTimer.Start();
|
||||
|
||||
this.Plugin.PartyFinderGui.ReceiveListing += this.OnListing;
|
||||
this.Plugin.Framework.Update += this.OnUpdate;
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
this.Plugin.Framework.Update -= this.OnUpdate;
|
||||
this.Plugin.PartyFinderGui.ReceiveListing -= this.OnListing;
|
||||
}
|
||||
|
||||
private void OnListing(PartyFinderListing listing, PartyFinderListingEventArgs args) {
|
||||
if (!this.Batches.ContainsKey(args.BatchNumber)) {
|
||||
this.Batches[args.BatchNumber] = new List<PartyFinderListing>();
|
||||
this.Plugin.PartyFinderGui.ReceiveListing += this.OnListing;
|
||||
this.Plugin.Framework.Update += this.OnUpdate;
|
||||
}
|
||||
|
||||
this.Batches[args.BatchNumber].Add(listing);
|
||||
}
|
||||
|
||||
private void OnUpdate(IFramework framework1) {
|
||||
if (this.UploadTimer.Elapsed < TimeSpan.FromSeconds(10)) {
|
||||
return;
|
||||
public void Dispose() {
|
||||
this.Plugin.Framework.Update -= this.OnUpdate;
|
||||
this.Plugin.PartyFinderGui.ReceiveListing -= this.OnListing;
|
||||
}
|
||||
|
||||
this.UploadTimer.Restart();
|
||||
private void OnListing(PartyFinderListing listing, PartyFinderListingEventArgs args) {
|
||||
if (!this.Batches.ContainsKey(args.BatchNumber)) {
|
||||
this.Batches[args.BatchNumber] = new List<PartyFinderListing>();
|
||||
}
|
||||
|
||||
foreach (var (batch, listings) in this.Batches.ToList()) {
|
||||
this.Batches.Remove(batch, out _);
|
||||
Task.Run(async () => {
|
||||
var uploadable = listings
|
||||
.Select(listing => new UploadableListing(listing))
|
||||
.ToList();
|
||||
var json = JsonConvert.SerializeObject(uploadable);
|
||||
var resp = await this.Client.PostAsync(UploadUrl, new StringContent(json) {
|
||||
Headers = { ContentType = MediaTypeHeaderValue.Parse("application/json") },
|
||||
this.Batches[args.BatchNumber].Add(listing);
|
||||
}
|
||||
|
||||
private void OnUpdate(Framework framework) {
|
||||
if (this.UploadTimer.Elapsed < TimeSpan.FromSeconds(10)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.UploadTimer.Restart();
|
||||
|
||||
foreach (var (batch, listings) in this.Batches.ToList()) {
|
||||
this.Batches.Remove(batch, out _);
|
||||
Task.Run(async () => {
|
||||
var uploadable = listings
|
||||
.Select(listing => new UploadableListing(listing))
|
||||
.ToList();
|
||||
var json = JsonConvert.SerializeObject(uploadable);
|
||||
var resp = await this.Client.PostAsync(UploadUrl, new StringContent(json) {
|
||||
Headers = { ContentType = MediaTypeHeaderValue.Parse("application/json") },
|
||||
});
|
||||
var output = await resp.Content.ReadAsStringAsync();
|
||||
PluginLog.Log(output);
|
||||
});
|
||||
var output = await resp.Content.ReadAsStringAsync();
|
||||
Plugin.Log.Info(output);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +1,26 @@
|
|||
using Dalamud.IoC;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.Gui.PartyFinder;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace RemotePartyFinder;
|
||||
namespace RemotePartyFinder {
|
||||
public class Plugin : IDalamudPlugin {
|
||||
public string Name => "Remote Party Finder";
|
||||
|
||||
public class Plugin : IDalamudPlugin {
|
||||
[PluginService]
|
||||
internal static IPluginLog Log { get; private set; }
|
||||
[PluginService]
|
||||
internal Framework Framework { get; private init; }
|
||||
|
||||
[PluginService]
|
||||
internal IFramework Framework { get; private init; }
|
||||
[PluginService]
|
||||
internal PartyFinderGui PartyFinderGui { get; private init; }
|
||||
|
||||
[PluginService]
|
||||
internal IPartyFinderGui PartyFinderGui { get; private init; }
|
||||
private Gatherer Gatherer { get; }
|
||||
|
||||
private Gatherer Gatherer { get; }
|
||||
public Plugin() {
|
||||
this.Gatherer = new Gatherer(this);
|
||||
}
|
||||
|
||||
public Plugin() {
|
||||
this.Gatherer = new Gatherer(this);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
this.Gatherer.Dispose();
|
||||
public void Dispose() {
|
||||
this.Gatherer.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,47 +1,45 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Version>1.0.11</Version>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<Version>1.0.3</Version>
|
||||
<TargetFramework>net5.0-windows</TargetFramework>
|
||||
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<DalamudLibPath>$(AppData)\XIVLauncher\addon\Hooks\dev</DalamudLibPath>
|
||||
<Dalamud>$(AppData)\XIVLauncher\addon\Hooks\dev</Dalamud>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))'">
|
||||
<DalamudLibPath>$(DALAMUD_HOME)</DalamudLibPath>
|
||||
<PropertyGroup Condition="$([MSBuild]::IsOSPlatform('Linux'))">
|
||||
<Dalamud>$(HOME)/games/final-fantasy-xiv-online/drive_c/users/$(USER)/AppData/Roaming/XIVLauncher/addon/Hooks/dev</Dalamud>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(IsCI)' == 'true'">
|
||||
<DalamudLibPath>$(HOME)/dalamud</DalamudLibPath>
|
||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||
<Dalamud>$(HOME)/dalamud</Dalamud>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Dalamud">
|
||||
<HintPath>$(DalamudLibPath)\Dalamud.dll</HintPath>
|
||||
<HintPath>$(Dalamud)\Dalamud.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Lumina">
|
||||
<HintPath>$(DalamudLibPath)\Lumina.dll</HintPath>
|
||||
<HintPath>$(Dalamud)\Lumina.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Lumina.Excel">
|
||||
<HintPath>$(DalamudLibPath)\Lumina.Excel.dll</HintPath>
|
||||
<HintPath>$(Dalamud)\Lumina.Excel.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json">
|
||||
<HintPath>$(DalamudLibPath)\Newtonsoft.Json.dll</HintPath>
|
||||
<HintPath>$(Dalamud)\Newtonsoft.Json.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DalamudPackager" Version="2.1.12" />
|
||||
<PackageReference Include="DalamudLinter" Version="1.0.3"/>
|
||||
<PackageReference Include="DalamudPackager" Version="2.1.4"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
author: Anna
|
||||
author: ascclemens
|
||||
name: Remote Party Finder Uploader
|
||||
description: |-
|
||||
Uploads the PF listings you retrieve to the crowdsourced Remote
|
||||
Party Finder website (https://xivpf.com/).
|
||||
punchline: Crowdsourced PF website (upload PF listings to xivpf.com)
|
||||
repo_url: https://git.anna.lgbt/anna/remote-party-finder
|
||||
repo_url: https://git.sr.ht/~jkcclemens/remote-party-finder
|
||||
|
|
|
@ -5,68 +5,68 @@ using Dalamud.Game.Gui.PartyFinder.Types;
|
|||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace RemotePartyFinder;
|
||||
namespace RemotePartyFinder {
|
||||
[Serializable]
|
||||
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
|
||||
internal class UploadableListing {
|
||||
public uint Id { get; }
|
||||
public uint ContentIdLower { get; }
|
||||
public byte[] Name { get; }
|
||||
public byte[] Description { get; }
|
||||
public byte CreatedWorld { get; }
|
||||
public byte HomeWorld { get; }
|
||||
public byte CurrentWorld { get; }
|
||||
public DutyCategory Category { get; }
|
||||
public ushort Duty { get; }
|
||||
public DutyType DutyType { get; }
|
||||
public bool BeginnersWelcome { get; }
|
||||
public ushort SecondsRemaining { get; }
|
||||
public ushort MinItemLevel { get; }
|
||||
public byte NumParties { get; }
|
||||
public byte SlotsAvailable { get; }
|
||||
public uint LastServerRestart { get; }
|
||||
public ObjectiveFlags Objective { get; }
|
||||
public ConditionFlags Conditions { get; }
|
||||
public DutyFinderSettingsFlags DutyFinderSettings { get; }
|
||||
public LootRuleFlags LootRules { get; }
|
||||
public SearchAreaFlags SearchArea { get; }
|
||||
public List<UploadableSlot> Slots { get; }
|
||||
public List<byte> JobsPresent { get; }
|
||||
|
||||
[Serializable]
|
||||
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
|
||||
internal class UploadableListing {
|
||||
public uint Id { get; }
|
||||
public uint ContentIdLower { get; }
|
||||
public byte[] Name { get; }
|
||||
public byte[] Description { get; }
|
||||
public ushort CreatedWorld { get; }
|
||||
public ushort HomeWorld { get; }
|
||||
public ushort CurrentWorld { get; }
|
||||
public DutyCategory Category { get; }
|
||||
public ushort Duty { get; }
|
||||
public DutyType DutyType { get; }
|
||||
public bool BeginnersWelcome { get; }
|
||||
public ushort SecondsRemaining { get; }
|
||||
public ushort MinItemLevel { get; }
|
||||
public byte NumParties { get; }
|
||||
public byte SlotsAvailable { get; }
|
||||
public uint LastServerRestart { get; }
|
||||
public ObjectiveFlags Objective { get; }
|
||||
public ConditionFlags Conditions { get; }
|
||||
public DutyFinderSettingsFlags DutyFinderSettings { get; }
|
||||
public LootRuleFlags LootRules { get; }
|
||||
public SearchAreaFlags SearchArea { get; }
|
||||
public List<UploadableSlot> Slots { get; }
|
||||
public List<byte> JobsPresent { get; }
|
||||
internal UploadableListing(PartyFinderListing listing) {
|
||||
this.Id = listing.Id;
|
||||
this.ContentIdLower = listing.ContentIdLower;
|
||||
this.Name = listing.Name.Encode();
|
||||
this.Description = listing.Description.Encode();
|
||||
this.CreatedWorld = (byte) listing.World.Value.RowId;
|
||||
this.HomeWorld = (byte) listing.HomeWorld.Value.RowId;
|
||||
this.CurrentWorld = (byte) listing.CurrentWorld.Value.RowId;
|
||||
this.Category = listing.Category;
|
||||
this.Duty = listing.RawDuty;
|
||||
this.DutyType = listing.DutyType;
|
||||
this.BeginnersWelcome = listing.BeginnersWelcome;
|
||||
this.SecondsRemaining = listing.SecondsRemaining;
|
||||
this.MinItemLevel = listing.MinimumItemLevel;
|
||||
this.NumParties = listing.Parties;
|
||||
this.SlotsAvailable = listing.SlotsAvailable;
|
||||
this.LastServerRestart = listing.LastPatchHotfixTimestamp;
|
||||
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(slot => new UploadableSlot(slot)).ToList();
|
||||
this.JobsPresent = listing.RawJobsPresent.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
internal UploadableListing(PartyFinderListing listing) {
|
||||
this.Id = listing.Id;
|
||||
this.ContentIdLower = listing.ContentIdLower;
|
||||
this.Name = listing.Name.Encode();
|
||||
this.Description = listing.Description.Encode();
|
||||
this.CreatedWorld = (ushort) listing.World.Value.RowId;
|
||||
this.HomeWorld = (ushort) listing.HomeWorld.Value.RowId;
|
||||
this.CurrentWorld = (ushort) listing.CurrentWorld.Value.RowId;
|
||||
this.Category = listing.Category;
|
||||
this.Duty = listing.RawDuty;
|
||||
this.DutyType = listing.DutyType;
|
||||
this.BeginnersWelcome = listing.BeginnersWelcome;
|
||||
this.SecondsRemaining = listing.SecondsRemaining;
|
||||
this.MinItemLevel = listing.MinimumItemLevel;
|
||||
this.NumParties = listing.Parties;
|
||||
this.SlotsAvailable = listing.SlotsAvailable;
|
||||
this.LastServerRestart = listing.LastPatchHotfixTimestamp;
|
||||
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(slot => new UploadableSlot(slot)).ToList();
|
||||
this.JobsPresent = listing.RawJobsPresent.ToList();
|
||||
[Serializable]
|
||||
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
|
||||
internal class UploadableSlot {
|
||||
public JobFlags Accepting { get; }
|
||||
|
||||
internal UploadableSlot(PartyFinderSlot slot) {
|
||||
this.Accepting = slot.Accepting.Aggregate((JobFlags) 0, ((agg, flag) => agg | flag));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
|
||||
internal class UploadableSlot {
|
||||
public JobFlags Accepting { get; }
|
||||
|
||||
internal UploadableSlot(PartyFinderSlot slot) {
|
||||
this.Accepting = slot.Accepting.Aggregate((JobFlags) 0, (agg, flag) => agg | flag);
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"version": 1,
|
||||
"dependencies": {
|
||||
"net7.0-windows7.0": {
|
||||
"DalamudPackager": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.1.12, )",
|
||||
"resolved": "2.1.12",
|
||||
"contentHash": "Sc0PVxvgg4NQjcI8n10/VfUQBAS4O+Fw2pZrAqBdRMbthYGeogzu5+xmIGCGmsEZ/ukMOBuAqiNiB5qA3MRalg=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,83 +3,83 @@ using Pidgin;
|
|||
using static Pidgin.Parser;
|
||||
using static Pidgin.Parser<char>;
|
||||
|
||||
namespace SourceGenerator;
|
||||
namespace SourceGenerator {
|
||||
internal static class AutoTranslate {
|
||||
internal static Parser<char, (string name, Maybe<IEnumerable<ISelectorPart>> selector)> Parser() {
|
||||
var sheetName = Any
|
||||
.AtLeastOnceUntil(Lookahead(Char('[').IgnoreResult().Or(End)))
|
||||
.Select(string.Concat)
|
||||
.Labelled("sheetName");
|
||||
|
||||
internal static class AutoTranslate {
|
||||
internal static Parser<char, (string name, Maybe<IEnumerable<ISelectorPart>> selector)> Parser() {
|
||||
var sheetName = Any
|
||||
.AtLeastOnceUntil(Lookahead(Char('[').IgnoreResult().Or(End)))
|
||||
.Select(string.Concat)
|
||||
.Labelled("sheetName");
|
||||
var numPair = Map(
|
||||
(first, second) => (ISelectorPart) new IndexRange(
|
||||
uint.Parse(string.Concat(first)),
|
||||
uint.Parse(string.Concat(second))
|
||||
),
|
||||
Digit.AtLeastOnce().Before(Char('-')),
|
||||
Digit.AtLeastOnce()
|
||||
)
|
||||
.Labelled("numPair");
|
||||
var singleRow = Digit
|
||||
.AtLeastOnce()
|
||||
.Select(string.Concat)
|
||||
.Select(num => (ISelectorPart) new SingleRow(uint.Parse(num)));
|
||||
var column = String("col-")
|
||||
.Then(Digit.AtLeastOnce())
|
||||
.Select(string.Concat)
|
||||
.Select(num => (ISelectorPart) new ColumnSpecifier(uint.Parse(num)));
|
||||
var noun = String("noun")
|
||||
.Select(_ => (ISelectorPart) new NounMarker());
|
||||
|
||||
var numPair = Map(
|
||||
(first, second) => (ISelectorPart) new IndexRange(
|
||||
uint.Parse(string.Concat(first)),
|
||||
uint.Parse(string.Concat(second))
|
||||
),
|
||||
Digit.AtLeastOnce().Before(Char('-')),
|
||||
Digit.AtLeastOnce()
|
||||
)
|
||||
.Labelled("numPair");
|
||||
var singleRow = Digit
|
||||
.AtLeastOnce()
|
||||
.Select(string.Concat)
|
||||
.Select(num => (ISelectorPart) new SingleRow(uint.Parse(num)));
|
||||
var column = String("col-")
|
||||
.Then(Digit.AtLeastOnce())
|
||||
.Select(string.Concat)
|
||||
.Select(num => (ISelectorPart) new ColumnSpecifier(uint.Parse(num)));
|
||||
var noun = String("noun")
|
||||
.Select(_ => (ISelectorPart) new NounMarker());
|
||||
var selectorItems = OneOf(
|
||||
Try(numPair),
|
||||
singleRow,
|
||||
column,
|
||||
noun
|
||||
)
|
||||
.Separated(Char(','))
|
||||
.Labelled("selectorItems");
|
||||
var selector = selectorItems
|
||||
.Between(Char('['), Char(']'))
|
||||
.Labelled("selector");
|
||||
|
||||
var selectorItems = OneOf(
|
||||
Try(numPair),
|
||||
singleRow,
|
||||
column,
|
||||
noun
|
||||
)
|
||||
.Separated(Char(','))
|
||||
.Labelled("selectorItems");
|
||||
var selector = selectorItems
|
||||
.Between(Char('['), Char(']'))
|
||||
.Labelled("selector");
|
||||
return Map(
|
||||
(name, selector) => (name, selector),
|
||||
sheetName,
|
||||
selector.Optional()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Map(
|
||||
(name, selector) => (name, selector),
|
||||
sheetName,
|
||||
selector.Optional()
|
||||
);
|
||||
internal interface ISelectorPart {
|
||||
}
|
||||
|
||||
internal class SingleRow : ISelectorPart {
|
||||
public uint Row { get; }
|
||||
|
||||
public SingleRow(uint row) {
|
||||
this.Row = row;
|
||||
}
|
||||
}
|
||||
|
||||
internal class IndexRange : ISelectorPart {
|
||||
public uint Start { get; }
|
||||
public uint End { get; }
|
||||
|
||||
public IndexRange(uint start, uint end) {
|
||||
this.Start = start;
|
||||
this.End = end;
|
||||
}
|
||||
}
|
||||
|
||||
internal class NounMarker : ISelectorPart {
|
||||
}
|
||||
|
||||
internal class ColumnSpecifier : ISelectorPart {
|
||||
public uint Column { get; }
|
||||
|
||||
public ColumnSpecifier(uint column) {
|
||||
this.Column = column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal interface ISelectorPart {
|
||||
}
|
||||
|
||||
internal class SingleRow : ISelectorPart {
|
||||
public uint Row { get; }
|
||||
|
||||
public SingleRow(uint row) {
|
||||
this.Row = row;
|
||||
}
|
||||
}
|
||||
|
||||
internal class IndexRange : ISelectorPart {
|
||||
public uint Start { get; }
|
||||
public uint End { get; }
|
||||
|
||||
public IndexRange(uint start, uint end) {
|
||||
this.Start = start;
|
||||
this.End = end;
|
||||
}
|
||||
}
|
||||
|
||||
internal class NounMarker : ISelectorPart {
|
||||
}
|
||||
|
||||
internal class ColumnSpecifier : ISelectorPart {
|
||||
public uint Column { get; }
|
||||
|
||||
public ColumnSpecifier(uint column) {
|
||||
this.Column = column;
|
||||
}
|
||||
}
|
|
@ -10,452 +10,452 @@ using Lumina.Excel.GeneratedSheets;
|
|||
using Lumina.Text;
|
||||
using Pidgin;
|
||||
|
||||
namespace SourceGenerator;
|
||||
|
||||
internal class Program {
|
||||
private static void Main(string[] args) {
|
||||
var data = new Dictionary<Language, GameData>(4);
|
||||
foreach (var lang in Languages.Keys) {
|
||||
data[lang] = new GameData(args[0], new LuminaOptions {
|
||||
PanicOnSheetChecksumMismatch = false,
|
||||
DefaultExcelLanguage = lang,
|
||||
});
|
||||
}
|
||||
|
||||
var prog = new Program(data);
|
||||
|
||||
File.WriteAllText(Path.Join(args[1], "duties.rs"), prog.GenerateDuties());
|
||||
File.WriteAllText(Path.Join(args[1], "jobs.rs"), prog.GenerateJobs());
|
||||
File.WriteAllText(Path.Join(args[1], "roulettes.rs"), prog.GenerateRoulettes());
|
||||
File.WriteAllText(Path.Join(args[1], "worlds.rs"), prog.GenerateWorlds());
|
||||
File.WriteAllText(Path.Join(args[1], "territory_names.rs"), prog.GenerateTerritoryNames());
|
||||
File.WriteAllText(Path.Join(args[1], "auto_translate.rs"), prog.GenerateAutoTranslate());
|
||||
File.WriteAllText(Path.Join(args[1], "treasure_maps.rs"), prog.GenerateTreasureMaps());
|
||||
}
|
||||
|
||||
private Dictionary<Language, GameData> Data { get; }
|
||||
|
||||
private Program(Dictionary<Language, GameData> data) {
|
||||
this.Data = data;
|
||||
}
|
||||
|
||||
private static StringBuilder DefaultHeader(bool localisedText = false) {
|
||||
var sb = new StringBuilder("use std::collections::HashMap;\n");
|
||||
|
||||
if (localisedText) {
|
||||
sb.Append("use super::LocalisedText;\n");
|
||||
}
|
||||
|
||||
return sb;
|
||||
}
|
||||
|
||||
private static readonly Dictionary<Language, string> Languages = new() {
|
||||
[Language.English] = "en",
|
||||
[Language.Japanese] = "ja",
|
||||
[Language.German] = "de",
|
||||
[Language.French] = "fr",
|
||||
};
|
||||
|
||||
private string? GetLocalisedStruct<T>(uint rowId, Func<T, SeString?> nameFunc, uint indent = 0, bool capitalise = false) where T : ExcelRow {
|
||||
var def = this.Data[Language.English].GetExcelSheet<T>()!.GetRow(rowId)!;
|
||||
var defName = nameFunc(def)?.TextValue();
|
||||
if (string.IsNullOrEmpty(defName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.Append("LocalisedText {\n");
|
||||
|
||||
foreach (var (language, key) in Languages) {
|
||||
var row = this.Data[language].GetExcelSheet<T>(language)?.GetRow(rowId);
|
||||
var name = row == null
|
||||
? defName
|
||||
: nameFunc(row)?.TextValue().Replace("\"", "\\\"");
|
||||
name ??= defName;
|
||||
if (capitalise) {
|
||||
name = name[..1].ToUpperInvariant() + name[1..];
|
||||
namespace SourceGenerator {
|
||||
internal class Program {
|
||||
private static void Main(string[] args) {
|
||||
var data = new Dictionary<Language, GameData>(4);
|
||||
foreach (var lang in Languages.Keys) {
|
||||
data[lang] = new GameData(args[0], new LuminaOptions {
|
||||
DefaultExcelLanguage = lang,
|
||||
});
|
||||
}
|
||||
|
||||
for (var i = 0; i < indent + 4; i++) {
|
||||
var prog = new Program(data);
|
||||
|
||||
File.WriteAllText(Path.Join(args[1], "duties.rs"), prog.GenerateDuties());
|
||||
File.WriteAllText(Path.Join(args[1], "jobs.rs"), prog.GenerateJobs());
|
||||
File.WriteAllText(Path.Join(args[1], "roulettes.rs"), prog.GenerateRoulettes());
|
||||
File.WriteAllText(Path.Join(args[1], "worlds.rs"), prog.GenerateWorlds());
|
||||
File.WriteAllText(Path.Join(args[1], "territory_names.rs"), prog.GenerateTerritoryNames());
|
||||
File.WriteAllText(Path.Join(args[1], "auto_translate.rs"), prog.GenerateAutoTranslate());
|
||||
File.WriteAllText(Path.Join(args[1], "treasure_maps.rs"), prog.GenerateTreasureMaps());
|
||||
}
|
||||
|
||||
private Dictionary<Language, GameData> Data { get; }
|
||||
|
||||
private Program(Dictionary<Language, GameData> data) {
|
||||
this.Data = data;
|
||||
}
|
||||
|
||||
private static StringBuilder DefaultHeader(bool localisedText = false) {
|
||||
var sb = new StringBuilder("use std::collections::HashMap;\n");
|
||||
|
||||
if (localisedText) {
|
||||
sb.Append("use super::LocalisedText;\n");
|
||||
}
|
||||
|
||||
return sb;
|
||||
}
|
||||
|
||||
private static readonly Dictionary<Language, string> Languages = new() {
|
||||
[Language.English] = "en",
|
||||
[Language.Japanese] = "ja",
|
||||
[Language.German] = "de",
|
||||
[Language.French] = "fr",
|
||||
};
|
||||
|
||||
private string? GetLocalisedStruct<T>(uint rowId, Func<T, SeString?> nameFunc, uint indent = 0, bool capitalise = false) where T : ExcelRow {
|
||||
var def = this.Data[Language.English].GetExcelSheet<T>()!.GetRow(rowId)!;
|
||||
var defName = nameFunc(def)?.TextValue();
|
||||
if (string.IsNullOrEmpty(defName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.Append("LocalisedText {\n");
|
||||
|
||||
foreach (var (language, key) in Languages) {
|
||||
var row = this.Data[language].GetExcelSheet<T>(language)?.GetRow(rowId);
|
||||
var name = row == null
|
||||
? defName
|
||||
: nameFunc(row)?.TextValue().Replace("\"", "\\\"");
|
||||
name ??= defName;
|
||||
if (capitalise) {
|
||||
name = name[..1].ToUpperInvariant() + name[1..];
|
||||
}
|
||||
|
||||
for (var i = 0; i < indent + 4; i++) {
|
||||
sb.Append(' ');
|
||||
}
|
||||
|
||||
sb.Append($"{key}: \"{name}\",\n");
|
||||
}
|
||||
|
||||
for (var i = 0; i < indent; i++) {
|
||||
sb.Append(' ');
|
||||
}
|
||||
|
||||
sb.Append($"{key}: \"{name}\",\n");
|
||||
sb.Append('}');
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
for (var i = 0; i < indent; i++) {
|
||||
sb.Append(' ');
|
||||
private string GenerateDuties() {
|
||||
var sb = DefaultHeader(true);
|
||||
sb.Append('\n');
|
||||
|
||||
sb.Append("#[derive(Debug)]\n");
|
||||
sb.Append("pub struct DutyInfo {\n");
|
||||
sb.Append(" pub name: LocalisedText,\n");
|
||||
sb.Append(" pub high_end: bool,\n");
|
||||
sb.Append(" pub content_kind: ContentKind,\n");
|
||||
sb.Append("}\n\n");
|
||||
|
||||
sb.Append("#[derive(Debug, Clone, Copy)]\n");
|
||||
sb.Append("#[allow(unused)]\n");
|
||||
sb.Append("#[repr(u32)]\n");
|
||||
sb.Append("pub enum ContentKind {\n");
|
||||
foreach (var kind in this.Data[Language.English].GetExcelSheet<ContentType>()!) {
|
||||
var name = kind.Name.TextValue().Replace(" ", "");
|
||||
if (name.Length > 0) {
|
||||
sb.Append($" {name} = {kind.RowId},\n");
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append(" Other(u32),\n");
|
||||
sb.Append("}\n\n");
|
||||
|
||||
sb.Append("impl ContentKind {\n");
|
||||
|
||||
sb.Append(" fn from_u32(kind: u32) -> Self {\n");
|
||||
sb.Append(" match kind {\n");
|
||||
foreach (var kind in this.Data[Language.English].GetExcelSheet<ContentType>()!) {
|
||||
var name = kind.Name.TextValue().Replace(" ", "");
|
||||
if (name.Length > 0) {
|
||||
sb.Append($" {kind.RowId} => Self::{name},\n");
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append(" x => Self::Other(x),\n");
|
||||
sb.Append(" }\n");
|
||||
sb.Append(" }\n\n");
|
||||
|
||||
sb.Append(" pub fn as_u32(self) -> u32 {\n");
|
||||
sb.Append(" match self {\n");
|
||||
foreach (var kind in this.Data[Language.English].GetExcelSheet<ContentType>()!) {
|
||||
var name = kind.Name.TextValue().Replace(" ", "");
|
||||
if (name.Length > 0) {
|
||||
sb.Append($" Self::{name} => {kind.RowId},\n");
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append(" Self::Other(x) => x,\n");
|
||||
sb.Append(" }\n");
|
||||
sb.Append(" }\n");
|
||||
|
||||
sb.Append("}\n\n");
|
||||
|
||||
sb.Append("lazy_static::lazy_static! {\n");
|
||||
sb.Append(" pub static ref DUTIES: HashMap<u32, DutyInfo> = maplit::hashmap! {\n");
|
||||
|
||||
foreach (var cfc in this.Data[Language.English].GetExcelSheet<ContentFinderCondition>()!) {
|
||||
if (cfc.RowId == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = this.GetLocalisedStruct<ContentFinderCondition>(cfc.RowId, row => row.Name, 12, true);
|
||||
if (name == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var highEnd = cfc.HighEndDuty ? "true" : "false";
|
||||
var contentType = cfc.ContentType.Value;
|
||||
var contentKind = contentType?.Name?.TextValue().Replace(" ", "");
|
||||
if (string.IsNullOrEmpty(contentKind)) {
|
||||
contentKind = $"Other({contentType?.RowId ?? 0})";
|
||||
}
|
||||
|
||||
sb.Append($" {cfc.RowId} => DutyInfo {{\n");
|
||||
sb.Append($" name: {name},\n");
|
||||
sb.Append($" high_end: {highEnd},\n");
|
||||
sb.Append($" content_kind: ContentKind::{contentKind},\n");
|
||||
sb.Append(" },\n");
|
||||
}
|
||||
|
||||
sb.Append(" };\n");
|
||||
sb.Append("}\n");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
sb.Append('}');
|
||||
private string GenerateJobs() {
|
||||
var sb = DefaultHeader();
|
||||
sb.Append("use ffxiv_types::jobs::{ClassJob, Class, Job, NonCombatJob};\n\n");
|
||||
sb.Append("lazy_static::lazy_static! {\n");
|
||||
sb.Append(" pub static ref JOBS: HashMap<u32, ClassJob> = maplit::hashmap! {\n");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
foreach (var cj in this.Data[Language.English].GetExcelSheet<ClassJob>()!) {
|
||||
if (cj.RowId == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
private string GenerateDuties() {
|
||||
var sb = DefaultHeader(true);
|
||||
sb.Append('\n');
|
||||
var name = cj.NameEnglish.TextValue().Replace(" ", "");
|
||||
if (name.Length <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
sb.Append("#[derive(Debug)]\n");
|
||||
sb.Append("pub struct DutyInfo {\n");
|
||||
sb.Append(" pub name: LocalisedText,\n");
|
||||
sb.Append(" pub high_end: bool,\n");
|
||||
sb.Append(" pub content_kind: ContentKind,\n");
|
||||
sb.Append("}\n\n");
|
||||
var isCombat = cj.Role != 0;
|
||||
var isClass = cj.JobIndex == 0;
|
||||
|
||||
sb.Append("#[derive(Debug, Clone, Copy)]\n");
|
||||
sb.Append("#[allow(unused)]\n");
|
||||
sb.Append("#[repr(u32)]\n");
|
||||
sb.Append("pub enum ContentKind {\n");
|
||||
foreach (var kind in this.Data[Language.English].GetExcelSheet<ContentType>()!) {
|
||||
var name = kind.Name.TextValue().Replace(" ", "").Replace("&", "");
|
||||
if (name.Length > 0) {
|
||||
sb.Append($" {name} = {kind.RowId},\n");
|
||||
string value;
|
||||
if (isCombat) {
|
||||
value = isClass
|
||||
? $"ClassJob::Class(Class::{name})"
|
||||
: $"ClassJob::Job(Job::{name})";
|
||||
} else {
|
||||
value = $"ClassJob::NonCombat(NonCombatJob::{name})";
|
||||
}
|
||||
|
||||
sb.Append($" {cj.RowId} => {value},\n");
|
||||
}
|
||||
|
||||
sb.Append(" };\n");
|
||||
sb.Append("}\n");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
sb.Append(" Other(u32),\n");
|
||||
sb.Append("}\n\n");
|
||||
private string GenerateRoulettes() {
|
||||
var sb = DefaultHeader(true);
|
||||
sb.Append('\n');
|
||||
sb.Append("#[derive(Debug)]\n");
|
||||
sb.Append("pub struct RouletteInfo {\n");
|
||||
sb.Append(" pub name: LocalisedText,\n");
|
||||
sb.Append(" pub pvp: bool,\n");
|
||||
sb.Append("}\n\n");
|
||||
|
||||
sb.Append("impl ContentKind {\n");
|
||||
sb.Append("lazy_static::lazy_static! {\n");
|
||||
sb.Append(" pub static ref ROULETTES: HashMap<u32, RouletteInfo> = maplit::hashmap! {\n");
|
||||
|
||||
sb.Append(" fn from_u32(kind: u32) -> Self {\n");
|
||||
sb.Append(" match kind {\n");
|
||||
foreach (var kind in this.Data[Language.English].GetExcelSheet<ContentType>()!) {
|
||||
var name = kind.Name.TextValue().Replace(" ", "").Replace("&", "");
|
||||
if (name.Length > 0) {
|
||||
sb.Append($" {kind.RowId} => Self::{name},\n");
|
||||
foreach (var cr in this.Data[Language.English].GetExcelSheet<ContentRoulette>()!) {
|
||||
if (cr.RowId == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = this.GetLocalisedStruct<ContentRoulette>(cr.RowId, row => row.Name, 12);
|
||||
if (name == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var pvp = cr.Unknown28 == 6
|
||||
? "true"
|
||||
: "false";
|
||||
|
||||
sb.Append($" {cr.RowId} => RouletteInfo {{\n");
|
||||
sb.Append($" name: {name},\n");
|
||||
sb.Append($" pvp: {pvp},\n");
|
||||
sb.Append(" },\n");
|
||||
}
|
||||
|
||||
sb.Append(" };\n");
|
||||
sb.Append("}\n");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
sb.Append(" x => Self::Other(x),\n");
|
||||
sb.Append(" }\n");
|
||||
sb.Append(" }\n\n");
|
||||
private string GenerateWorlds() {
|
||||
var sb = DefaultHeader();
|
||||
sb.Append("use ffxiv_types::World;\n\n");
|
||||
sb.Append("lazy_static::lazy_static! {\n");
|
||||
sb.Append(" pub static ref WORLDS: HashMap<u32, World> = maplit::hashmap! {\n");
|
||||
|
||||
sb.Append(" pub fn as_u32(self) -> u32 {\n");
|
||||
sb.Append(" match self {\n");
|
||||
foreach (var kind in this.Data[Language.English].GetExcelSheet<ContentType>()!) {
|
||||
var name = kind.Name.TextValue().Replace(" ", "").Replace("&", "");
|
||||
if (name.Length > 0) {
|
||||
sb.Append($" Self::{name} => {kind.RowId},\n");
|
||||
foreach (var world in this.Data[Language.English].GetExcelSheet<World>()!) {
|
||||
if (world.RowId == 0 || !world.IsPublic || world.DataCenter.Row == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = world.Name.TextValue();
|
||||
if (name.Length <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
sb.Append($" {world.RowId} => World::{name},\n");
|
||||
}
|
||||
|
||||
sb.Append(" };\n");
|
||||
sb.Append("}\n");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
sb.Append(" Self::Other(x) => x,\n");
|
||||
sb.Append(" }\n");
|
||||
sb.Append(" }\n");
|
||||
private string GenerateTerritoryNames() {
|
||||
var sb = DefaultHeader(true);
|
||||
sb.Append("\nlazy_static::lazy_static! {\n");
|
||||
sb.Append(" pub static ref TERRITORY_NAMES: HashMap<u32, LocalisedText> = maplit::hashmap! {\n");
|
||||
|
||||
sb.Append("}\n\n");
|
||||
foreach (var tt in this.Data[Language.English].GetExcelSheet<TerritoryType>()!) {
|
||||
if (tt.RowId == 0 || tt.PlaceName.Row == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
sb.Append("lazy_static::lazy_static! {\n");
|
||||
sb.Append(" pub static ref DUTIES: HashMap<u32, DutyInfo> = maplit::hashmap! {\n");
|
||||
var name = this.GetLocalisedStruct<TerritoryType>(
|
||||
tt.RowId,
|
||||
row => row.PlaceName.Value!.Name,
|
||||
8
|
||||
);
|
||||
if (name == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var cfc in this.Data[Language.English].GetExcelSheet<ContentFinderCondition>()!) {
|
||||
if (cfc.RowId == 0) {
|
||||
continue;
|
||||
sb.Append($" {tt.RowId} => {name},\n");
|
||||
}
|
||||
|
||||
var name = this.GetLocalisedStruct<ContentFinderCondition>(cfc.RowId, row => row.Name, 12, true);
|
||||
if (name == null) {
|
||||
continue;
|
||||
}
|
||||
sb.Append(" };\n");
|
||||
sb.Append("}\n");
|
||||
|
||||
var highEnd = cfc.HighEndDuty ? "true" : "false";
|
||||
var contentType = cfc.ContentType.Value;
|
||||
var contentKind = contentType?.Name?.TextValue().Replace(" ", "").Replace("&", "");
|
||||
if (string.IsNullOrEmpty(contentKind)) {
|
||||
contentKind = $"Other({contentType?.RowId ?? 0})";
|
||||
}
|
||||
|
||||
sb.Append($" {cfc.RowId} => DutyInfo {{\n");
|
||||
sb.Append($" name: {name},\n");
|
||||
sb.Append($" high_end: {highEnd},\n");
|
||||
sb.Append($" content_kind: ContentKind::{contentKind},\n");
|
||||
sb.Append(" },\n");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
sb.Append(" };\n");
|
||||
sb.Append("}\n");
|
||||
private string GenerateAutoTranslate() {
|
||||
var sb = DefaultHeader(true);
|
||||
sb.Append("\nlazy_static::lazy_static! {\n");
|
||||
sb.Append(" pub static ref AUTO_TRANSLATE: HashMap<(u32, u32), LocalisedText> = maplit::hashmap! {\n");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
var parser = AutoTranslate.Parser();
|
||||
foreach (var row in this.Data[Language.English].GetExcelSheet<Completion>()!) {
|
||||
var lookup = row.LookupTable.TextValue();
|
||||
if (lookup is not ("" or "@")) {
|
||||
var (sheetName, selector) = parser.ParseOrThrow(lookup);
|
||||
var sheetType = typeof(Completion)
|
||||
.Assembly
|
||||
.GetType($"Lumina.Excel.GeneratedSheets.{sheetName}")!;
|
||||
var getSheet = this.Data[Language.English]
|
||||
.GetType()
|
||||
.GetMethod("GetExcelSheet", Type.EmptyTypes)!
|
||||
.MakeGenericMethod(sheetType);
|
||||
var sheets = this.Data.ToDictionary(
|
||||
pair => pair.Key,
|
||||
pair => {
|
||||
var sheet = (ExcelSheetImpl) getSheet.Invoke(pair.Value, null)!;
|
||||
return (sheet, sheet.EnumerateRowParsers().ToArray());
|
||||
});
|
||||
|
||||
private string GenerateJobs() {
|
||||
var sb = DefaultHeader();
|
||||
sb.Append("use ffxiv_types::jobs::{ClassJob, Class, Job, NonCombatJob};\n\n");
|
||||
sb.Append("lazy_static::lazy_static! {\n");
|
||||
sb.Append(" pub static ref JOBS: HashMap<u32, ClassJob> = maplit::hashmap! {\n");
|
||||
var columns = new List<int>();
|
||||
var rows = new List<Range>();
|
||||
if (selector.HasValue) {
|
||||
columns.Clear();
|
||||
rows.Clear();
|
||||
|
||||
foreach (var cj in this.Data[Language.English].GetExcelSheet<ClassJob>()!) {
|
||||
if (cj.RowId == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = cj.NameEnglish.TextValue().Replace(" ", "");
|
||||
if (name.Length <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var isCombat = cj.Role != 0;
|
||||
var isClass = cj.JobIndex == 0;
|
||||
|
||||
string value;
|
||||
if (isCombat) {
|
||||
value = isClass
|
||||
? $"ClassJob::Class(Class::{name})"
|
||||
: $"ClassJob::Job(Job::{name})";
|
||||
} else {
|
||||
value = $"ClassJob::NonCombat(NonCombatJob::{name})";
|
||||
}
|
||||
|
||||
sb.Append($" {cj.RowId} => {value},\n");
|
||||
}
|
||||
|
||||
sb.Append(" };\n");
|
||||
sb.Append("}\n");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private string GenerateRoulettes() {
|
||||
var sb = DefaultHeader(true);
|
||||
sb.Append('\n');
|
||||
sb.Append("#[derive(Debug)]\n");
|
||||
sb.Append("pub struct RouletteInfo {\n");
|
||||
sb.Append(" pub name: LocalisedText,\n");
|
||||
sb.Append(" pub pvp: bool,\n");
|
||||
sb.Append("}\n\n");
|
||||
|
||||
sb.Append("lazy_static::lazy_static! {\n");
|
||||
sb.Append(" pub static ref ROULETTES: HashMap<u32, RouletteInfo> = maplit::hashmap! {\n");
|
||||
|
||||
foreach (var cr in this.Data[Language.English].GetExcelSheet<ContentRoulette>()!) {
|
||||
if (cr.RowId == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = this.GetLocalisedStruct<ContentRoulette>(cr.RowId, row => row.Name, 12);
|
||||
if (name == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var pvp = cr.IsPvP
|
||||
? "true"
|
||||
: "false";
|
||||
|
||||
sb.Append($" {cr.RowId} => RouletteInfo {{\n");
|
||||
sb.Append($" name: {name},\n");
|
||||
sb.Append($" pvp: {pvp},\n");
|
||||
sb.Append(" },\n");
|
||||
}
|
||||
|
||||
sb.Append(" };\n");
|
||||
sb.Append("}\n");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private string GenerateWorlds() {
|
||||
var sb = DefaultHeader();
|
||||
sb.Append("use ffxiv_types::World;\n\n");
|
||||
sb.Append("lazy_static::lazy_static! {\n");
|
||||
sb.Append(" pub static ref WORLDS: HashMap<u32, World> = maplit::hashmap! {\n");
|
||||
|
||||
foreach (var world in this.Data[Language.English].GetExcelSheet<World>()!) {
|
||||
if (world.RowId == 0 || !world.IsPublic || world.UserType == 0 || world.DataCenter.Row == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = world.Name.TextValue();
|
||||
if (name.Length <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
sb.Append($" {world.RowId} => World::{name},\n");
|
||||
}
|
||||
|
||||
sb.Append(" };\n");
|
||||
sb.Append("}\n");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private string GenerateTerritoryNames() {
|
||||
var sb = DefaultHeader(true);
|
||||
sb.Append("\nlazy_static::lazy_static! {\n");
|
||||
sb.Append(" pub static ref TERRITORY_NAMES: HashMap<u32, LocalisedText> = maplit::hashmap! {\n");
|
||||
|
||||
foreach (var tt in this.Data[Language.English].GetExcelSheet<TerritoryType>()!) {
|
||||
if (tt.RowId == 0 || tt.PlaceName.Row == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = this.GetLocalisedStruct<TerritoryType>(
|
||||
tt.RowId,
|
||||
row => row.PlaceName.Value!.Name,
|
||||
8
|
||||
);
|
||||
if (name == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
sb.Append($" {tt.RowId} => {name},\n");
|
||||
}
|
||||
|
||||
sb.Append(" };\n");
|
||||
sb.Append("}\n");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private string GenerateAutoTranslate() {
|
||||
var sb = DefaultHeader(true);
|
||||
sb.Append("\nlazy_static::lazy_static! {\n");
|
||||
sb.Append(" pub static ref AUTO_TRANSLATE: HashMap<(u32, u32), LocalisedText> = maplit::hashmap! {\n");
|
||||
|
||||
var parser = AutoTranslate.Parser();
|
||||
foreach (var row in this.Data[Language.English].GetExcelSheet<Completion>()!) {
|
||||
var lookup = row.LookupTable.TextValue();
|
||||
if (lookup is not ("" or "@")) {
|
||||
var (sheetName, selector) = parser.ParseOrThrow(lookup);
|
||||
var sheetType = typeof(Completion)
|
||||
.Assembly
|
||||
.GetType($"Lumina.Excel.GeneratedSheets.{sheetName}")!;
|
||||
var getSheet = this.Data[Language.English]
|
||||
.GetType()
|
||||
.GetMethod("GetExcelSheet", Type.EmptyTypes)!
|
||||
.MakeGenericMethod(sheetType);
|
||||
var sheets = this.Data.ToDictionary(
|
||||
pair => pair.Key,
|
||||
pair => {
|
||||
var sheet = (ExcelSheetImpl) getSheet.Invoke(pair.Value, null)!;
|
||||
return (sheet, sheet.GetRowParsers().ToArray());
|
||||
});
|
||||
|
||||
var columns = new List<int>();
|
||||
var rows = new List<Range>();
|
||||
if (selector.HasValue) {
|
||||
columns.Clear();
|
||||
rows.Clear();
|
||||
|
||||
foreach (var part in selector.Value) {
|
||||
switch (part) {
|
||||
case IndexRange range: {
|
||||
var start = (int) range.Start;
|
||||
var end = (int) (range.End + 1);
|
||||
rows.Add(start..end);
|
||||
break;
|
||||
foreach (var part in selector.Value) {
|
||||
switch (part) {
|
||||
case IndexRange range: {
|
||||
var start = (int) range.Start;
|
||||
var end = (int) (range.End + 1);
|
||||
rows.Add(start..end);
|
||||
break;
|
||||
}
|
||||
case SingleRow single: {
|
||||
var idx = (int) single.Row;
|
||||
rows.Add(idx..(idx + 1));
|
||||
break;
|
||||
}
|
||||
case ColumnSpecifier col:
|
||||
columns.Add((int) col.Column);
|
||||
break;
|
||||
}
|
||||
case SingleRow single: {
|
||||
var idx = (int) single.Row;
|
||||
rows.Add(idx..(idx + 1));
|
||||
break;
|
||||
}
|
||||
case ColumnSpecifier col:
|
||||
columns.Add((int) col.Column);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (columns.Count == 0) {
|
||||
columns.Add(0);
|
||||
}
|
||||
if (columns.Count == 0) {
|
||||
columns.Add(0);
|
||||
}
|
||||
|
||||
if (rows.Count == 0) {
|
||||
rows.Add(..);
|
||||
}
|
||||
if (rows.Count == 0) {
|
||||
rows.Add(..);
|
||||
}
|
||||
|
||||
var builder = new StringBuilder();
|
||||
foreach (var range in rows) {
|
||||
var validRows = sheets[Language.English]
|
||||
.Item2
|
||||
.Select(parser => parser.RowId)
|
||||
.ToArray();
|
||||
for (var i = range.Start.Value; i < range.End.Value; i++) {
|
||||
if (!validRows.Contains((uint) i)) {
|
||||
continue;
|
||||
}
|
||||
var builder = new StringBuilder();
|
||||
foreach (var range in rows) {
|
||||
var validRows = sheets[Language.English]
|
||||
.Item2
|
||||
.Select(parser => parser.Row)
|
||||
.ToArray();
|
||||
for (var i = range.Start.Value; i < range.End.Value; i++) {
|
||||
if (!validRows.Contains((uint) i)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.Clear();
|
||||
builder.Clear();
|
||||
|
||||
builder.Append($" ({row.Group}, {i}) => LocalisedText {{\n");
|
||||
builder.Append($" ({row.Group}, {i}) => LocalisedText {{\n");
|
||||
|
||||
var lines = 0;
|
||||
foreach (var (lang, (_, parsers)) in sheets) {
|
||||
// take the first column that works
|
||||
foreach (var col in columns) {
|
||||
var rowParser = parsers.FirstOrDefault(parser => parser.RowId == i);
|
||||
if (rowParser != null) {
|
||||
var name = rowParser.ReadColumn<SeString>(col)!;
|
||||
var text = name.TextValue().Replace("\"", "\\\"");
|
||||
if (text.Length > 0) {
|
||||
builder.Append($" {Languages[lang]}: \"{text}\",\n");
|
||||
lines += 1;
|
||||
break;
|
||||
}
|
||||
var lines = 0;
|
||||
foreach (var (lang, (_, parsers)) in sheets) {
|
||||
// take the first column that works
|
||||
foreach (var col in columns) {
|
||||
var rowParser = parsers.FirstOrDefault(parser => parser.Row == i);
|
||||
if (rowParser != null) {
|
||||
var name = rowParser.ReadColumn<SeString>(col)!;
|
||||
var text = name.TextValue().Replace("\"", "\\\"");
|
||||
if (text.Length > 0) {
|
||||
builder.Append($" {Languages[lang]}: \"{text}\",\n");
|
||||
lines += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builder.Append(" },\n");
|
||||
|
||||
if (lines != 4) {
|
||||
continue;
|
||||
}
|
||||
|
||||
sb.Append(builder);
|
||||
}
|
||||
|
||||
builder.Append(" },\n");
|
||||
|
||||
if (lines != 4) {
|
||||
continue;
|
||||
}
|
||||
|
||||
sb.Append(builder);
|
||||
}
|
||||
// TODO: do lookup
|
||||
} else {
|
||||
var text = this.GetLocalisedStruct<Completion>(row.RowId, row => row.Text, 8);
|
||||
if (text != null) {
|
||||
sb.Append($" ({row.Group}, {row.RowId}) => {text},\n");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var text = this.GetLocalisedStruct<Completion>(row.RowId, row => row.Text, 8);
|
||||
if (text != null) {
|
||||
sb.Append($" ({row.Group}, {row.RowId}) => {text},\n");
|
||||
}
|
||||
|
||||
sb.Append(" };\n");
|
||||
sb.Append("}\n");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private string GenerateTreasureMaps() {
|
||||
var sb = DefaultHeader(true);
|
||||
sb.Append("\nlazy_static::lazy_static! {\n");
|
||||
sb.Append(" pub static ref TREASURE_MAPS: HashMap<u32, LocalisedText> = maplit::hashmap! {\n");
|
||||
sb.Append(" 0 => LocalisedText {\n");
|
||||
sb.Append(" en: \"All Levels\",\n");
|
||||
sb.Append(" ja: \"レベルを指定しない\",\n");
|
||||
sb.Append(" de: \"Jede Stufe\",\n");
|
||||
sb.Append(" fr: \"Tous niveaux\",\n");
|
||||
sb.Append(" },\n");
|
||||
|
||||
var i = 1;
|
||||
foreach (var row in this.Data[Language.English].GetExcelSheet<TreasureHuntRank>()!) {
|
||||
// IS THIS RIGHT?
|
||||
if (row.TreasureHuntTexture != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SeString? GetMapName(TreasureHuntRank thr) {
|
||||
var name = thr.KeyItemName.Value?.Name;
|
||||
return string.IsNullOrEmpty(name?.TextValue())
|
||||
? thr.ItemName.Value?.Name
|
||||
: name;
|
||||
}
|
||||
|
||||
var name = this.GetLocalisedStruct<TreasureHuntRank>(row.RowId, GetMapName, 8);
|
||||
if (!string.IsNullOrEmpty(name)) {
|
||||
sb.Append($" {i++} => {name},\n");
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append(" };\n");
|
||||
sb.Append("}\n");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
sb.Append(" };\n");
|
||||
sb.Append("}\n");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private string GenerateTreasureMaps() {
|
||||
var sb = DefaultHeader(true);
|
||||
sb.Append("\nlazy_static::lazy_static! {\n");
|
||||
sb.Append(" pub static ref TREASURE_MAPS: HashMap<u32, LocalisedText> = maplit::hashmap! {\n");
|
||||
sb.Append(" 0 => LocalisedText {\n");
|
||||
sb.Append(" en: \"All Levels\",\n");
|
||||
sb.Append(" ja: \"レベルを指定しない\",\n");
|
||||
sb.Append(" de: \"Jede Stufe\",\n");
|
||||
sb.Append(" fr: \"Tous niveaux\",\n");
|
||||
sb.Append(" },\n");
|
||||
|
||||
var i = 1;
|
||||
foreach (var row in this.Data[Language.English].GetExcelSheet<TreasureHuntRank>()!) {
|
||||
// IS THIS RIGHT?
|
||||
if (row.TreasureHuntTexture != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SeString? GetMapName(TreasureHuntRank thr) {
|
||||
var name = thr.KeyItemName.Value?.Name;
|
||||
return string.IsNullOrEmpty(name?.TextValue())
|
||||
? thr.ItemName.Value?.Name
|
||||
: name;
|
||||
}
|
||||
|
||||
var name = this.GetLocalisedStruct<TreasureHuntRank>(row.RowId, GetMapName, 8);
|
||||
if (!string.IsNullOrEmpty(name)) {
|
||||
sb.Append($" {i++} => {name},\n");
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append(" };\n");
|
||||
sb.Append("}\n");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,36 +2,36 @@
|
|||
using Lumina.Text;
|
||||
using Lumina.Text.Payloads;
|
||||
|
||||
namespace SourceGenerator;
|
||||
namespace SourceGenerator {
|
||||
internal static class SeStringExtensions {
|
||||
internal static string TextValue(this SeString str) {
|
||||
var payloads = str.Payloads
|
||||
.Select(p => {
|
||||
if (p is TextPayload text) {
|
||||
return p.Data[0] == 0x03
|
||||
? text.RawString[1..]
|
||||
: text.RawString;
|
||||
}
|
||||
|
||||
internal static class SeStringExtensions {
|
||||
internal static string TextValue(this SeString str) {
|
||||
var payloads = str.Payloads
|
||||
.Select(p => {
|
||||
if (p is TextPayload text) {
|
||||
return p.Data[0] == 0x03
|
||||
? text.RawString[1..]
|
||||
: text.RawString;
|
||||
}
|
||||
if (p.Data.Length <= 1) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (p.Data[1] == 0x1F) {
|
||||
return "-";
|
||||
}
|
||||
|
||||
if (p.Data.Length > 2 && p.Data[1] == 0x20) {
|
||||
var value = p.Data.Length > 4
|
||||
? p.Data[3] - 1
|
||||
: p.Data[2];
|
||||
return ((char) (48 + value)).ToString();
|
||||
}
|
||||
|
||||
if (p.Data.Length <= 1) {
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
if (p.Data[1] == 0x1F) {
|
||||
return "-";
|
||||
}
|
||||
|
||||
if (p.Data.Length > 2 && p.Data[1] == 0x20) {
|
||||
var value = p.Data.Length > 4
|
||||
? p.Data[3] - 1
|
||||
: p.Data[2];
|
||||
return ((char) (48 + value)).ToString();
|
||||
}
|
||||
|
||||
return "";
|
||||
});
|
||||
|
||||
return string.Join("", payloads);
|
||||
return string.Join("", payloads);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Lumina" Version="3.10.0"/>
|
||||
<PackageReference Include="Lumina.Excel" Version="6.3.2"/>
|
||||
<PackageReference Include="Pidgin" Version="3.2.1"/>
|
||||
<PackageReference Include="Lumina" Version="3.4.1"/>
|
||||
<PackageReference Include="Lumina.Excel" Version="5.50.0"/>
|
||||
<PackageReference Include="Pidgin" Version="3.0.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
#use nix
|
||||
#unset RUSTC_WRAPPER
|
||||
use nix
|
||||
unset RUSTC_WRAPPER
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,14 +1,14 @@
|
|||
[package]
|
||||
name = "remote-party-finder"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
askama = { version = "0.11", features = ["with-warp"] }
|
||||
askama_warp = "0.12"
|
||||
askama = { version = "0.10", features = ["with-warp"] }
|
||||
askama_warp = "0.11"
|
||||
base64 = "0.13"
|
||||
bitflags = "1"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
@ -18,13 +18,13 @@ lazy_static = "1"
|
|||
maplit = "1"
|
||||
mime = "0.3"
|
||||
mongodb = { version = "2", features = ["bson-chrono-0_4"] }
|
||||
sestring = { version = "0.3", features = ["serde"] }
|
||||
sestring = { version = "0.1", features = ["serde"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_repr = "0.1"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||
tokio-stream = "0.1"
|
||||
toml = "0.7"
|
||||
toml = "0.5"
|
||||
warp = { version = "0.3", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -33,8 +33,8 @@ body {
|
|||
margin: 0;
|
||||
|
||||
font-family: sans-serif;
|
||||
/* background-color: var(--background); */
|
||||
/* color: var(--text); */
|
||||
background-color: var(--background);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.js body {
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
(function () {
|
||||
function setUpLanguage() {
|
||||
let language = document.getElementById('language');
|
||||
for (let elem of language.querySelectorAll('[data-value]')) {
|
||||
elem.addEventListener('click', () => {
|
||||
document.cookie = `lang=${encodeURIComponent(elem.dataset.value)};path=/;max-age=31536000;samesite=lax`;
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setUpLanguage();
|
||||
})();
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 64 KiB |
File diff suppressed because one or more lines are too long
|
@ -28,15 +28,11 @@
|
|||
margin-top: 1em;
|
||||
}
|
||||
|
||||
#container > .settings > .controls {
|
||||
#container > .settings {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#container > .settings > .controls > .search {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
#listings > .no-listings {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
@ -49,12 +45,11 @@
|
|||
margin: 0 -1em;
|
||||
padding: 1em;
|
||||
|
||||
/* background-color: var(--row-background); */
|
||||
background-color: var(--row-background);
|
||||
}
|
||||
|
||||
#listings > .listing:nth-child(2n) {
|
||||
background-color: var(--muted-border-color);
|
||||
/* background-color: var(--row-background-alternate); */
|
||||
background-color: var(--row-background-alternate);
|
||||
}
|
||||
|
||||
#listings > .listing .description {
|
||||
|
|
|
@ -64,14 +64,15 @@
|
|||
let language = document.getElementById('language');
|
||||
if (state.lang === null) {
|
||||
state.lang = language.dataset.accept;
|
||||
let cookie = document.cookie
|
||||
.split(';')
|
||||
.find(row => row.trim().startsWith('lang='));
|
||||
if (cookie !== undefined) {
|
||||
state.lang = decodeURIComponent(cookie.split('=')[1]);
|
||||
}
|
||||
}
|
||||
|
||||
let cookie = document.cookie
|
||||
.split(';')
|
||||
.find(row => row.trim().startsWith('lang='));
|
||||
if (cookie !== undefined) {
|
||||
state.lang = decodeURIComponent(cookie.split('=')[1]);
|
||||
}
|
||||
language.value = state.lang;
|
||||
}
|
||||
|
||||
function setUpList() {
|
||||
|
@ -155,9 +156,19 @@
|
|||
});
|
||||
}
|
||||
|
||||
function setUpLanguage() {
|
||||
let language = document.getElementById('language');
|
||||
language.addEventListener('change', () => {
|
||||
state.lang = language.value;
|
||||
document.cookie = `lang=${encodeURIComponent(language.value)};path=/;max-age=31536000;samesite=lax`;
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
addJsClass();
|
||||
saveLoadState();
|
||||
reflectState();
|
||||
setUpLanguage();
|
||||
state.list = setUpList();
|
||||
setUpDataCentreFilter();
|
||||
setUpCategoryFilter();
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,5 +1,5 @@
|
|||
body {
|
||||
margin-top: 1em;
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
.total {
|
||||
|
@ -10,15 +10,10 @@ body {
|
|||
}
|
||||
|
||||
.chart {
|
||||
height: 50vh;
|
||||
max-height: 50vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chart svg {
|
||||
.chart canvas {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
@ -40,12 +35,11 @@ body {
|
|||
}
|
||||
|
||||
.chart-containers .container table tr {
|
||||
/* background-color: var(--row-background); */
|
||||
background-color: var(--row-background);
|
||||
}
|
||||
|
||||
.chart-containers .container table tr:nth-child(2n) {
|
||||
/* background-color: var(--row-background-alternate); */
|
||||
background-color: var(--muted-border-color);
|
||||
background-color: var(--row-background-alternate);
|
||||
}
|
||||
|
||||
.chart-containers .container table tr td {
|
||||
|
|
|
@ -85,252 +85,42 @@
|
|||
return newData;
|
||||
}
|
||||
|
||||
function extractData(tableId, extractor = null) {
|
||||
if (extractor === null) {
|
||||
extractor = (cols) => {
|
||||
return {
|
||||
label: cols[0].innerHTML,
|
||||
value: Number(cols[1].innerHTML),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function makeChart(tableId, chartId, chartType, combine = false) {
|
||||
let table = document.getElementById(tableId);
|
||||
let data = [];
|
||||
for (let row of table.querySelectorAll('tbody > tr')) {
|
||||
let cols = row.querySelectorAll('td');
|
||||
data.push(extractor(cols));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function wrap(text) {
|
||||
text.each(function () {
|
||||
var text = d3.select(this),
|
||||
words = text.text().split(/\s+/).reverse(),
|
||||
word,
|
||||
line = [],
|
||||
lineNumber = 0,
|
||||
lineHeight = 1.1, // ems
|
||||
x = text.attr("x"),
|
||||
y = text.attr("y"),
|
||||
dy = 0, //parseFloat(text.attr("dy")),
|
||||
tspan = text.text(null)
|
||||
.append("tspan")
|
||||
.attr("x", x)
|
||||
.attr("y", y)
|
||||
.attr("dy", dy + "em");
|
||||
while (word = words.pop()) {
|
||||
line.push(word);
|
||||
tspan.text(line.join(" "));
|
||||
if (tspan.node().getComputedTextLength() > text.attr('width') - 6) {
|
||||
line.pop();
|
||||
tspan.text(line.join(" "));
|
||||
line = [word];
|
||||
tspan = text.append("tspan")
|
||||
.attr("x", x)
|
||||
.attr("y", y)
|
||||
.attr("dy", ++lineNumber * lineHeight + dy + "em")
|
||||
.text(word);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function makeTreeMap(data, graphId, opts = {}) {
|
||||
let drawLabels = opts.drawLabels === undefined ? true : opts.drawLabels;
|
||||
let grouped = opts.grouped === undefined ? false : opts.grouped;
|
||||
|
||||
let elem = document.getElementById(graphId);
|
||||
const [width, height] = [elem.offsetWidth, elem.offsetHeight];
|
||||
|
||||
let svg = d3.select(`#${graphId}`)
|
||||
.append('svg')
|
||||
.attr('viewBox', `0 0 ${width} ${height}`);
|
||||
|
||||
if (grouped) {
|
||||
d3.treemap()
|
||||
.size([width, height])
|
||||
.paddingInner(2)
|
||||
.paddingTop(4)
|
||||
.paddingRight(4)
|
||||
.paddingLeft(4)
|
||||
.paddingBottom(4)
|
||||
(data);
|
||||
} else {
|
||||
d3.treemap()
|
||||
.size([width, height])
|
||||
.paddingInner(2)
|
||||
(data);
|
||||
}
|
||||
|
||||
let a = 0;
|
||||
let group = svg.selectAll('g')
|
||||
.data(data.leaves())
|
||||
.enter()
|
||||
.append('g');
|
||||
|
||||
let title = (d) => `${d.data.label} (${(d.data.value / d.parent.value * 100).toFixed(2)}% - ${d.data.value.toLocaleString()})`;
|
||||
|
||||
group
|
||||
.append('title')
|
||||
.text(title);
|
||||
|
||||
group
|
||||
.append('rect')
|
||||
.attr('x', d => d.x0)
|
||||
.attr('y', d => d.y0)
|
||||
.attr('width', d => d.x1 - d.x0)
|
||||
.attr('height', d => d.y1 - d.y0)
|
||||
.style('fill', d => {
|
||||
let colour = colours[a];
|
||||
a += 1;
|
||||
a %= colours.length;
|
||||
return colour;
|
||||
data.push({
|
||||
x: cols[0].innerText,
|
||||
y: Number(cols[1].innerText),
|
||||
});
|
||||
|
||||
if (drawLabels) {
|
||||
svg.selectAll('text')
|
||||
.data(data.leaves().filter(d => d.data.value / d.parent.value >= .05 ))
|
||||
.enter()
|
||||
.append('text')
|
||||
.attr('x', d => d.x0 + 5)
|
||||
.attr('y', d => d.y0 + 20)
|
||||
.attr('width', d => d.x1 - d.x0)
|
||||
.text(title)
|
||||
.attr('font-size', '1em')
|
||||
.attr('fill', 'white')
|
||||
.call(wrap);
|
||||
}
|
||||
|
||||
if (grouped) {
|
||||
svg.selectAll('borders')
|
||||
.data(data.descendants().filter(d => d.depth === 1))
|
||||
.enter()
|
||||
.append('rect')
|
||||
.attr('x', d => d.x0)
|
||||
.attr('y', d => d.y0)
|
||||
.attr('width', d => d.x1 - d.x0)
|
||||
.attr('height', d => d.y1 - d.y0)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', '#374956');
|
||||
|
||||
svg.selectAll('titles')
|
||||
.data(data.descendants().filter(d => d.depth === 1))
|
||||
.enter()
|
||||
.append('text')
|
||||
.attr('x', d => d.x1 - 2)
|
||||
.attr('y', d => d.y1 - 2)
|
||||
.attr('text-anchor', 'end')
|
||||
.style('transform', d => {
|
||||
if ((d.x1 - d.x0) < (d.y1 - d.y0)) {
|
||||
return 'rotate(90deg)';
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.style('transform-box', 'fill-box')
|
||||
.style('transform-origin', '95%')
|
||||
.text(d => d.data[0])
|
||||
.attr("font-size", d => {
|
||||
if (d === data) {
|
||||
return "1em";
|
||||
}
|
||||
let width = d.x1 - d.x0, height = d.y1 - d.y0;
|
||||
return Math.max(Math.min(width/5, height/2, Math.sqrt((width*width + height*height))/10), 9)
|
||||
})
|
||||
.attr('fill', 'white');
|
||||
if (combine) {
|
||||
data = combineTopN(data, 15);
|
||||
}
|
||||
|
||||
new Chart(
|
||||
document.getElementById(chartId),
|
||||
{
|
||||
type: chartType,
|
||||
data: {
|
||||
datasets: [{
|
||||
data: data.map(entry => entry.y),
|
||||
backgroundColor: colours,
|
||||
}],
|
||||
labels: data.map(entry => entry.x),
|
||||
},
|
||||
options: {
|
||||
borderWidth: chartType === 'doughnut' ? 0 : 2,
|
||||
...options,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function makeBarPlot(data, graphId) {
|
||||
let elem = document.getElementById(graphId);
|
||||
const [marginLeft, marginRight, marginTop, marginBottom] = [100, 0, 16, 50];
|
||||
const [width, height] = [elem.offsetWidth - marginLeft - marginRight, elem.offsetHeight - marginTop - marginBottom];
|
||||
|
||||
let svg = d3.select(`#${graphId}`)
|
||||
.append('svg')
|
||||
.attr('viewBox', `0 0 ${width + marginLeft + marginRight} ${height + marginTop + marginBottom}`)
|
||||
.append('g')
|
||||
.attr('transform', `translate(${marginLeft}, ${marginTop})`);
|
||||
|
||||
let x = d3.scaleBand()
|
||||
.range([0, width])
|
||||
.domain(data.map(d => d.label))
|
||||
.padding(0.2);
|
||||
svg.append('g')
|
||||
.attr('transform', `translate(0, ${height})`)
|
||||
.call(d3.axisBottom(x))
|
||||
.attr('font-size', '1em')
|
||||
.selectAll('text')
|
||||
.style('text-anchor', 'middle')
|
||||
.attr('font-size', '1em');
|
||||
|
||||
let y = d3.scaleLinear()
|
||||
.domain([0, data.map(d => d.value).reduce((max, a) => Math.max(max, a))])
|
||||
.range([height, 0]);
|
||||
svg.append('g')
|
||||
.call(d3.axisLeft(y))
|
||||
.attr('font-size', '1em')
|
||||
.selectAll('text')
|
||||
.attr('font-size', '1em');
|
||||
|
||||
let sum = data.map(d => d.value).reduce((total, a) => total + a);
|
||||
let colourIdx = 0;
|
||||
let group = svg.selectAll('mybar')
|
||||
.data(data)
|
||||
.enter()
|
||||
.append('g');
|
||||
group.append('title')
|
||||
.text(d => `${d.value} (${(d.value / sum * 100).toFixed(2)}%)`);
|
||||
group.append('rect')
|
||||
.attr('x', d => x(d.label))
|
||||
.attr('y', d => y(d.value))
|
||||
.attr('width', x.bandwidth())
|
||||
.attr('height', d => height - y(d.value))
|
||||
.attr('fill', d => {
|
||||
let colour = colours[colourIdx];
|
||||
colourIdx += 1;
|
||||
colourIdx %= colours.length;
|
||||
return colour;
|
||||
});
|
||||
}
|
||||
|
||||
makeTreeMap(
|
||||
d3.hierarchy({
|
||||
children: extractData('duties'),
|
||||
}).sum(d => d.value),
|
||||
'dutiesChart',
|
||||
);
|
||||
makeTreeMap(
|
||||
d3.hierarchy(
|
||||
d3.group(
|
||||
extractData(
|
||||
'hosts',
|
||||
(cols) => {
|
||||
return {
|
||||
label: cols[1].innerHTML,
|
||||
world: cols[0].innerHTML,
|
||||
value: Number(cols[2].innerHTML),
|
||||
};
|
||||
},
|
||||
),
|
||||
d => d.world,
|
||||
)
|
||||
).sum(d => d.value),
|
||||
'hostsChart',
|
||||
{
|
||||
drawLabels: false,
|
||||
grouped: true,
|
||||
},
|
||||
);
|
||||
makeBarPlot(
|
||||
extractData('hours'),
|
||||
'hoursChart',
|
||||
);
|
||||
makeBarPlot(
|
||||
extractData('days'),
|
||||
'daysChart',
|
||||
);
|
||||
makeChart('duties', 'dutiesChart', 'pie', true);
|
||||
makeChart('hosts', 'hostsChart', 'doughnut');
|
||||
makeChart('hours', 'hoursChart', 'bar');
|
||||
makeChart('days', 'daysChart', 'bar');
|
||||
})();
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
let
|
||||
sources = import ./nix/sources.nix { };
|
||||
pkgs = import sources.nixpkgs { overlays = [ (import sources.mozilla) ]; };
|
||||
rust = pkgs.rustChannelOfTargets "nightly" "2022-12-02" [ "x86_64-unknown-linux-musl" ];
|
||||
in
|
||||
pkgs.mkShell {
|
||||
buildInputs = [
|
||||
rust
|
||||
(pkgs.rustChannelOf { date = "2021-10-03"; channel = "nightly"; }).rust
|
||||
pkgs.gcc
|
||||
pkgs.glibc
|
||||
pkgs.patchelf
|
||||
pkgs.musl
|
||||
pkgs.musl.dev
|
||||
];
|
||||
}
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
"homepage": "",
|
||||
"owner": "mozilla",
|
||||
"repo": "nixpkgs-mozilla",
|
||||
"rev": "e1f7540fc0a8b989fb8cf701dc4fd7fc76bcf168",
|
||||
"sha256": "1b6p0rly0rywq60ks84ghc0n5zrqiafc2r64nlbnlkh9whmh5fmj",
|
||||
"rev": "0510159186dd2ef46e5464484fbdf119393afa58",
|
||||
"sha256": "1c6r5ldkh71v6acsfhni7f9sxvi7xrqzshcwd8w0hl2rrqyzi58w",
|
||||
"type": "tarball",
|
||||
"url": "https://github.com/mozilla/nixpkgs-mozilla/archive/e1f7540fc0a8b989fb8cf701dc4fd7fc76bcf168.tar.gz",
|
||||
"url": "https://github.com/mozilla/nixpkgs-mozilla/archive/0510159186dd2ef46e5464484fbdf119393afa58.tar.gz",
|
||||
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
|
||||
},
|
||||
"niv": {
|
||||
|
@ -17,22 +17,22 @@
|
|||
"homepage": "https://github.com/nmattia/niv",
|
||||
"owner": "nmattia",
|
||||
"repo": "niv",
|
||||
"rev": "82e5cd1ad3c387863f0545d7591512e76ab0fc41",
|
||||
"sha256": "090l219mzc0gi33i3psgph6s2pwsc8qy4lyrqjdj4qzkvmaj65a7",
|
||||
"rev": "65a61b147f307d24bfd0a5cd56ce7d7b7cc61d2e",
|
||||
"sha256": "17mirpsx5wyw262fpsd6n6m47jcgw8k2bwcp1iwdnrlzy4dhcgqh",
|
||||
"type": "tarball",
|
||||
"url": "https://github.com/nmattia/niv/archive/82e5cd1ad3c387863f0545d7591512e76ab0fc41.tar.gz",
|
||||
"url": "https://github.com/nmattia/niv/archive/65a61b147f307d24bfd0a5cd56ce7d7b7cc61d2e.tar.gz",
|
||||
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
|
||||
},
|
||||
"nixpkgs": {
|
||||
"branch": "release-22.05",
|
||||
"branch": "release-20.03",
|
||||
"description": "Nix Packages collection",
|
||||
"homepage": "",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "edaee6c8d8d126dee69eaf1c515f9e39d26515d8",
|
||||
"sha256": "04krg1idccks34x4p27kfgqg3yl15ihh4j00lg7njxhjms6pjw51",
|
||||
"rev": "eb73405ecceb1dc505b7cbbd234f8f94165e2696",
|
||||
"sha256": "06k21wbyhhvq2f1xczszh3c2934p0m02by3l2ixvd6nkwrqklax7",
|
||||
"type": "tarball",
|
||||
"url": "https://github.com/NixOS/nixpkgs/archive/edaee6c8d8d126dee69eaf1c515f9e39d26515d8.tar.gz",
|
||||
"url": "https://github.com/NixOS/nixpkgs/archive/eb73405ecceb1dc505b7cbbd234f8f94165e2696.tar.gz",
|
||||
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
use std::{
|
||||
cmp::Ordering,
|
||||
str::FromStr,
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::listing::{DutyCategory, DutyType};
|
||||
pub mod auto_translate;
|
||||
pub mod duties;
|
||||
pub mod jobs;
|
||||
pub mod roulettes;
|
||||
pub mod territory_names;
|
||||
pub mod treasure_maps;
|
||||
pub mod worlds;
|
||||
|
||||
pub use self::{
|
||||
auto_translate::AUTO_TRANSLATE,
|
||||
|
@ -16,13 +16,12 @@ pub use self::{
|
|||
worlds::WORLDS,
|
||||
};
|
||||
|
||||
pub mod auto_translate;
|
||||
pub mod duties;
|
||||
pub mod jobs;
|
||||
pub mod roulettes;
|
||||
pub mod territory_names;
|
||||
pub mod treasure_maps;
|
||||
pub mod worlds;
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
str::FromStr,
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
use crate::listing::{DutyCategory, DutyType};
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum Language {
|
||||
|
@ -42,15 +41,6 @@ impl Language {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::English => "english",
|
||||
Self::Japanese => "日本語",
|
||||
Self::German => "deutsch",
|
||||
Self::French => "français",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_codes(val: Option<&str>) -> Self {
|
||||
let val = match val {
|
||||
Some(v) => v,
|
||||
|
@ -81,7 +71,7 @@ impl Language {
|
|||
"ja" => return Self::Japanese,
|
||||
"de" => return Self::German,
|
||||
"fr" => return Self::French,
|
||||
_ => {}
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,16 +98,6 @@ impl LocalisedText {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn duty(duty: u32) -> Option<&'static duties::DutyInfo> {
|
||||
crate::ffxiv::DUTIES.get(&duty)
|
||||
.or_else(|| old::OLD_DUTIES.get(&duty))
|
||||
}
|
||||
|
||||
pub fn roulette(roulette: u32) -> Option<&'static roulettes::RouletteInfo> {
|
||||
crate::ffxiv::ROULETTES.get(&roulette)
|
||||
.or_else(|| old::OLD_ROULETTES.get(&roulette))
|
||||
}
|
||||
|
||||
pub fn duty_name<'a>(duty_type: DutyType, category: DutyCategory, duty: u16, lang: Language) -> Cow<'a, str> {
|
||||
match (duty_type, category) {
|
||||
(DutyType::Other, DutyCategory::Fates) => {
|
||||
|
@ -151,19 +131,13 @@ pub fn duty_name<'a>(duty_type: DutyType, category: DutyCategory, duty: u16, lan
|
|||
Language::German => "Himmelssäule",
|
||||
Language::French => "Pilier des Cieux",
|
||||
}),
|
||||
(DutyType::Other, DutyCategory::DeepDungeons) if duty == 3 => return Cow::from(match lang {
|
||||
Language::English => "Eureka Orthos",
|
||||
Language::Japanese => "オルト・エウレカ",
|
||||
Language::German => "Eureka Orthos",
|
||||
Language::French => "Eurêka Orthos",
|
||||
}),
|
||||
(DutyType::Normal, _) => {
|
||||
if let Some(info) = crate::ffxiv::duty(u32::from(duty)) {
|
||||
if let Some(info) = crate::ffxiv::DUTIES.get(&u32::from(duty)) {
|
||||
return Cow::from(info.name.text(&lang));
|
||||
}
|
||||
}
|
||||
(DutyType::Roulette, _) => {
|
||||
if let Some(info) = roulette(u32::from(duty)) {
|
||||
if let Some(info) = crate::ffxiv::ROULETTES.get(&u32::from(duty)) {
|
||||
return Cow::from(info.name.text(&lang));
|
||||
}
|
||||
}
|
||||
|
@ -179,211 +153,5 @@ pub fn duty_name<'a>(duty_type: DutyType, category: DutyCategory, duty: u16, lan
|
|||
_ => {}
|
||||
}
|
||||
|
||||
eprintln!("unknown type/category/duty: {:?}/{:?}/{}", duty_type, category, duty);
|
||||
Cow::from(format!("{:?}", category))
|
||||
}
|
||||
|
||||
mod old {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::ffxiv::{
|
||||
duties::{ContentKind, DutyInfo},
|
||||
LocalisedText,
|
||||
roulettes::RouletteInfo,
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref OLD_DUTIES: HashMap<u32, DutyInfo> = maplit::hashmap! {
|
||||
62 => DutyInfo {
|
||||
name: LocalisedText {
|
||||
en: "Cape Westwind",
|
||||
ja: "リットアティン強襲戦",
|
||||
de: "Kap Westwind",
|
||||
fr: "Le Cap Vendouest",
|
||||
},
|
||||
high_end: false,
|
||||
content_kind: ContentKind::Trials,
|
||||
},
|
||||
83 => DutyInfo {
|
||||
name: LocalisedText {
|
||||
en: "The Steps of Faith",
|
||||
ja: "皇都イシュガルド防衛戦",
|
||||
de: "Der Schicksalsweg",
|
||||
fr: "Le Siège de la sainte Cité d'Ishgard",
|
||||
},
|
||||
high_end: false,
|
||||
content_kind: ContentKind::Trials,
|
||||
},
|
||||
143 => DutyInfo {
|
||||
name: LocalisedText {
|
||||
en: "The Feast (4 on 4 - Training)",
|
||||
ja: "ザ・フィースト (4対4 / カジュアルマッチ)",
|
||||
de: "The Feast (4 gegen 4, Übungskampf)",
|
||||
fr: "The Feast (4x4/entraînement)",
|
||||
},
|
||||
high_end: false,
|
||||
content_kind: ContentKind::PvP,
|
||||
},
|
||||
145 => DutyInfo {
|
||||
name: LocalisedText {
|
||||
en: "The Feast (4 on 4 - Ranked)",
|
||||
ja: "ザ・フィースト (4対4 / ランクマッチ)",
|
||||
de: "The Feast (4 gegen 4, gewertet)",
|
||||
fr: "The Feast (4x4/classé)",
|
||||
},
|
||||
high_end: false,
|
||||
content_kind: ContentKind::PvP,
|
||||
},
|
||||
201 => DutyInfo {
|
||||
name: LocalisedText {
|
||||
en: "The Feast (Custom Match - Feasting Grounds)",
|
||||
ja: "ザ・フィースト (ウルヴズジェイル演習場:カスタムマッチ)",
|
||||
de: "The Feast (Wolfshöhle: Schaukampf)",
|
||||
fr: "The Feast (personnalisé/Festin des loups)",
|
||||
},
|
||||
high_end: false,
|
||||
content_kind: ContentKind::PvP,
|
||||
},
|
||||
228 => DutyInfo {
|
||||
name: LocalisedText {
|
||||
en: "The Feast (4 on 4 - Training)",
|
||||
ja: "ザ・フィースト (4対4 / カジュアルマッチ)",
|
||||
de: "The Feast (4 gegen 4, Übungskampf)",
|
||||
fr: "The Feast (4x4/entraînement)",
|
||||
},
|
||||
high_end: false,
|
||||
content_kind: ContentKind::PvP,
|
||||
},
|
||||
230 => DutyInfo {
|
||||
name: LocalisedText {
|
||||
en: "The Feast (4 on 4 - Ranked)",
|
||||
ja: "ザ・フィースト (4対4 / ランクマッチ)",
|
||||
de: "The Feast (4 gegen 4, gewertet)",
|
||||
fr: "The Feast (4x4/classé)",
|
||||
},
|
||||
high_end: false,
|
||||
content_kind: ContentKind::PvP,
|
||||
},
|
||||
233 => DutyInfo {
|
||||
name: LocalisedText {
|
||||
en: "The Feast (Custom Match - Lichenweed)",
|
||||
ja: "ザ・フィースト (ライケンウィード演習場:カスタムマッチ)",
|
||||
de: "The Feast (Flechtenhain: Schaukampf)",
|
||||
fr: "The Feast (personnalisé/Pré-de-lichen)",
|
||||
},
|
||||
high_end: false,
|
||||
content_kind: ContentKind::PvP,
|
||||
},
|
||||
476 => DutyInfo {
|
||||
name: LocalisedText {
|
||||
en: "The Feast (Team Ranked)",
|
||||
ja: "ザ・フィースト (チーム用ランクマッチ)",
|
||||
de: "The Feast (Team, gewertet)",
|
||||
fr: "The Feast (classé/équipe JcJ)",
|
||||
},
|
||||
high_end: false,
|
||||
content_kind: ContentKind::PvP,
|
||||
},
|
||||
478 => DutyInfo {
|
||||
name: LocalisedText {
|
||||
en: "The Feast (Ranked)",
|
||||
ja: "ザ・フィースト (ランクマッチ)",
|
||||
de: "The Feast (gewertet)",
|
||||
fr: "The Feast (classé)",
|
||||
},
|
||||
high_end: false,
|
||||
content_kind: ContentKind::PvP,
|
||||
},
|
||||
479 => DutyInfo {
|
||||
name: LocalisedText {
|
||||
en: "The Feast (Training)",
|
||||
ja: "ザ・フィースト (カジュアルマッチ)",
|
||||
de: "The Feast (Übungskampf)",
|
||||
fr: "The Feast (entraînement)",
|
||||
},
|
||||
high_end: false,
|
||||
content_kind: ContentKind::PvP,
|
||||
},
|
||||
480 => DutyInfo {
|
||||
name: LocalisedText {
|
||||
en: "The Feast (Custom Match - Crystal Tower)",
|
||||
ja: "ザ・フィースト (クリスタルタワー演習場:カスタムマッチ)",
|
||||
de: "The Feast (Kristallturm-Arena: Schaukampf)",
|
||||
fr: "The Feast (personnalisé/Tour de Cristal)",
|
||||
},
|
||||
high_end: false,
|
||||
content_kind: ContentKind::PvP,
|
||||
},
|
||||
580 => DutyInfo {
|
||||
name: LocalisedText {
|
||||
en: "The Feast (Team Custom Match - Crystal Tower)",
|
||||
ja: "ザ・フィースト (クリスタルタワー演習場:チーム用カスタムマッチ)",
|
||||
de: "The Feast (Kristallturm-Arena: Team-Schaukampf) ",
|
||||
fr: "The Feast (personnalisé/équipe JcJ/Tour de Cristal)",
|
||||
},
|
||||
high_end: false,
|
||||
content_kind: ContentKind::PvP,
|
||||
},
|
||||
776 => DutyInfo {
|
||||
name: LocalisedText {
|
||||
en: "The Whorleater (Unreal)",
|
||||
ja: "幻リヴァイアサン討滅戦",
|
||||
de: "Traumprüfung - Leviathan",
|
||||
fr: "Le Briseur de marées (irréel)",
|
||||
},
|
||||
high_end: true,
|
||||
content_kind: ContentKind::Trials,
|
||||
},
|
||||
821 => DutyInfo {
|
||||
name: LocalisedText {
|
||||
en: "Ultima's Bane (Unreal)",
|
||||
ja: "幻アルテマウェポン破壊作戦",
|
||||
de: "Traumprüfung - Ultima",
|
||||
fr: "Le fléau d'Ultima (irréel)",
|
||||
},
|
||||
high_end: true,
|
||||
content_kind: ContentKind::Trials,
|
||||
},
|
||||
875 => DutyInfo {
|
||||
name: LocalisedText {
|
||||
en: "Containment Bay S1T7 (Unreal)",
|
||||
ja: "幻魔神セフィロト討滅戦",
|
||||
de: "Traumprüfung - Sephirot",
|
||||
fr: "Unité de contention S1P7 (irréel)",
|
||||
},
|
||||
high_end: true,
|
||||
content_kind: ContentKind::Trials,
|
||||
},
|
||||
};
|
||||
|
||||
pub static ref OLD_ROULETTES: HashMap<u32, RouletteInfo> = maplit::hashmap! {
|
||||
11 => RouletteInfo {
|
||||
name: LocalisedText {
|
||||
en: "The Feast (Training Match)",
|
||||
ja: "ザ・フィースト (カジュアルマッチ)",
|
||||
de: "The Feast (Übungskampf)",
|
||||
fr: "The Feast (entraînement)",
|
||||
},
|
||||
pvp: true,
|
||||
},
|
||||
13 => RouletteInfo {
|
||||
name: LocalisedText {
|
||||
en: "The Feast (Ranked Match)",
|
||||
ja: "ザ・フィースト (ランクマッチ)",
|
||||
de: "The Feast (gewertet)",
|
||||
fr: "The Feast (classé)",
|
||||
},
|
||||
pvp: true,
|
||||
},
|
||||
16 => RouletteInfo {
|
||||
name: LocalisedText {
|
||||
en: "The Feast (Team Ranked Match)",
|
||||
ja: "ザ・フィースト (チーム用ランクマッチ)",
|
||||
de: "The Feast (Team, gewertet)",
|
||||
fr: "The Feast (classé/équipe JcJ)",
|
||||
},
|
||||
pvp: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -41,7 +41,5 @@ lazy_static::lazy_static! {
|
|||
36 => ClassJob::Job(Job::BlueMage),
|
||||
37 => ClassJob::Job(Job::Gunbreaker),
|
||||
38 => ClassJob::Job(Job::Dancer),
|
||||
39 => ClassJob::Job(Job::Reaper),
|
||||
40 => ClassJob::Job(Job::Sage),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -20,10 +20,10 @@ lazy_static::lazy_static! {
|
|||
},
|
||||
2 => RouletteInfo {
|
||||
name: LocalisedText {
|
||||
en: "Duty Roulette: Level 50/60/70/80 Dungeons",
|
||||
ja: "コンテンツルーレット:レベル50・60・70・80ダンジョン",
|
||||
de: "Zufallsinhalt: Stufe 50/60/70/80",
|
||||
fr: "Mission aléatoire : donjons nv 50/60/70/80",
|
||||
en: "Duty Roulette: Level 50/60/70 Dungeons",
|
||||
ja: "コンテンツルーレット:レベル50・60・70ダンジョン",
|
||||
de: "Zufallsinhalt: Stufe 50/60/70",
|
||||
fr: "Mission aléatoire : donjons nv 50/60/70",
|
||||
},
|
||||
pvp: false,
|
||||
},
|
||||
|
@ -70,14 +70,14 @@ lazy_static::lazy_static! {
|
|||
de: "Tagesherausforderung: PvP-Front",
|
||||
fr: "Challenge quotidien : Front",
|
||||
},
|
||||
pvp: true,
|
||||
pvp: false,
|
||||
},
|
||||
8 => RouletteInfo {
|
||||
name: LocalisedText {
|
||||
en: "Duty Roulette: Level 90 Dungeons",
|
||||
ja: "コンテンツルーレット:レベル90ダンジョン",
|
||||
de: "Zufallsinhalt: Stufe 90",
|
||||
fr: "Mission aléatoire : donjons nv 90",
|
||||
en: "Duty Roulette: Level 80 Dungeons",
|
||||
ja: "コンテンツルーレット:レベル80ダンジョン",
|
||||
de: "Zufallsinhalt: Stufe 80",
|
||||
fr: "Mission aléatoire : donjons nv 80",
|
||||
},
|
||||
pvp: false,
|
||||
},
|
||||
|
@ -90,6 +90,24 @@ lazy_static::lazy_static! {
|
|||
},
|
||||
pvp: false,
|
||||
},
|
||||
11 => RouletteInfo {
|
||||
name: LocalisedText {
|
||||
en: "The Feast (Training Match)",
|
||||
ja: "ザ・フィースト (カジュアルマッチ)",
|
||||
de: "The Feast (Übungskampf)",
|
||||
fr: "The Feast (entraînement)",
|
||||
},
|
||||
pvp: true,
|
||||
},
|
||||
13 => RouletteInfo {
|
||||
name: LocalisedText {
|
||||
en: "The Feast (Ranked Match)",
|
||||
ja: "ザ・フィースト (ランクマッチ)",
|
||||
de: "The Feast (gewertet)",
|
||||
fr: "The Feast (classé)",
|
||||
},
|
||||
pvp: true,
|
||||
},
|
||||
15 => RouletteInfo {
|
||||
name: LocalisedText {
|
||||
en: "Duty Roulette: Alliance Raids",
|
||||
|
@ -99,6 +117,15 @@ lazy_static::lazy_static! {
|
|||
},
|
||||
pvp: false,
|
||||
},
|
||||
16 => RouletteInfo {
|
||||
name: LocalisedText {
|
||||
en: "The Feast (Team Ranked Match)",
|
||||
ja: "ザ・フィースト (チーム用ランクマッチ)",
|
||||
de: "The Feast (Team, gewertet)",
|
||||
fr: "The Feast (classé/équipe JcJ)",
|
||||
},
|
||||
pvp: true,
|
||||
},
|
||||
17 => RouletteInfo {
|
||||
name: LocalisedText {
|
||||
en: "Duty Roulette: Normal Raids",
|
||||
|
@ -297,23 +324,5 @@ lazy_static::lazy_static! {
|
|||
},
|
||||
pvp: false,
|
||||
},
|
||||
40 => RouletteInfo {
|
||||
name: LocalisedText {
|
||||
en: "Crystalline Conflict (Casual Match)",
|
||||
ja: "クリスタルコンフリクト(カジュアルマッチ)",
|
||||
de: "Crystalline Conflict: Freies Spiel",
|
||||
fr: "Crystalline Conflict (partie non classée)",
|
||||
},
|
||||
pvp: true,
|
||||
},
|
||||
41 => RouletteInfo {
|
||||
name: LocalisedText {
|
||||
en: "Crystalline Conflict (Ranked Match)",
|
||||
ja: "クリスタルコンフリクト(ランクマッチ)",
|
||||
de: "Crystalline Conflict: Gewertetes Spiel",
|
||||
fr: "Crystalline Conflict (partie classée)",
|
||||
},
|
||||
pvp: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -100,40 +100,10 @@ lazy_static::lazy_static! {
|
|||
fr: "Carte au trésor en peau de glaucus",
|
||||
},
|
||||
16 => LocalisedText {
|
||||
en: "Ostensibly Special Treasure Map",
|
||||
en: "Presumably Special Treasure Map",
|
||||
ja: "古ぼけた地図S2",
|
||||
de: "Mythenleder-Schatzkarte",
|
||||
fr: "Carte au trésor inhabituelle II",
|
||||
},
|
||||
17 => LocalisedText {
|
||||
en: "Saigaskin Treasure Map",
|
||||
ja: "古ぼけた地図G13",
|
||||
de: "Gajaleder-Schatzkarte",
|
||||
fr: "Carte au trésor en peau de gaja",
|
||||
},
|
||||
18 => LocalisedText {
|
||||
en: "Kumbhiraskin Treasure Map",
|
||||
ja: "古ぼけた地図G14",
|
||||
de: "Kumbhilaleder-Schatzkarte",
|
||||
fr: "Carte au trésor en peau de kumbhira",
|
||||
},
|
||||
19 => LocalisedText {
|
||||
en: "Ophiotauroskin Treasure Map",
|
||||
ja: "古ぼけた地図G15",
|
||||
de: "Ophiotaurosleder-Schatzkarte",
|
||||
fr: "Carte au trésor en peau d'ophiotauros",
|
||||
},
|
||||
20 => LocalisedText {
|
||||
en: "Potentially Special Treasure Map",
|
||||
ja: "古ぼけた地図S3",
|
||||
de: "Legendenleder-Schatzkarte",
|
||||
fr: "Carte au trésor inhabituelle III",
|
||||
},
|
||||
21 => LocalisedText {
|
||||
en: "Conceivably Special Treasure Map",
|
||||
ja: "古ぼけた地図S4",
|
||||
de: "Sagenleder-Schatzkarte",
|
||||
fr: "Carte au trésor inhabituelle IV",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -3,8 +3,6 @@ use ffxiv_types::World;
|
|||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref WORLDS: HashMap<u32, World> = maplit::hashmap! {
|
||||
21 => World::Ravana,
|
||||
22 => World::Bismarck,
|
||||
23 => World::Asura,
|
||||
24 => World::Belias,
|
||||
28 => World::Pandaemonium,
|
||||
|
@ -63,9 +61,6 @@ lazy_static::lazy_static! {
|
|||
82 => World::Mandragora,
|
||||
83 => World::Louisoix,
|
||||
85 => World::Spriggan,
|
||||
86 => World::Sephirot,
|
||||
87 => World::Sophia,
|
||||
88 => World::Zurvan,
|
||||
90 => World::Aegis,
|
||||
91 => World::Balmung,
|
||||
92 => World::Durandal,
|
||||
|
@ -76,13 +71,5 @@ lazy_static::lazy_static! {
|
|||
97 => World::Ragnarok,
|
||||
98 => World::Ridill,
|
||||
99 => World::Sargatanas,
|
||||
400 => World::Sagittarius,
|
||||
401 => World::Phantom,
|
||||
402 => World::Alpha,
|
||||
403 => World::Raiden,
|
||||
404 => World::Marilith,
|
||||
405 => World::Seraph,
|
||||
406 => World::Halicarnassus,
|
||||
407 => World::Maduin,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -16,9 +16,9 @@ pub struct PartyFinderListing {
|
|||
pub name: SeString,
|
||||
#[serde(with = "crate::base64_sestring")]
|
||||
pub description: SeString,
|
||||
pub created_world: u16,
|
||||
pub home_world: u16,
|
||||
pub current_world: u16,
|
||||
pub created_world: u8,
|
||||
pub home_world: u8,
|
||||
pub current_world: u8,
|
||||
pub category: DutyCategory,
|
||||
pub duty: u16,
|
||||
pub duty_type: DutyType,
|
||||
|
@ -134,7 +134,7 @@ impl PartyFinderListing {
|
|||
return false;
|
||||
}
|
||||
|
||||
crate::ffxiv::duty(u32::from(self.duty))
|
||||
crate::ffxiv::DUTIES.get(&u32::from(self.duty))
|
||||
.map(|info| info.high_end)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
@ -144,14 +144,14 @@ impl PartyFinderListing {
|
|||
return 0;
|
||||
}
|
||||
|
||||
crate::ffxiv::duty(u32::from(self.duty))
|
||||
crate::ffxiv::DUTIES.get(&u32::from(self.duty))
|
||||
.map(|info| info.content_kind.as_u32())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn pf_category(&self) -> Option<PartyFinderCategory> {
|
||||
let duty_type = self.duty_type;
|
||||
let duty_info = crate::ffxiv::duty(u32::from(self.duty));
|
||||
let duty_info = crate::ffxiv::DUTIES.get(&u32::from(self.duty));
|
||||
let duty_category = self.category;
|
||||
|
||||
let category = match (duty_type, duty_info, duty_category) {
|
||||
|
@ -162,7 +162,6 @@ impl PartyFinderListing {
|
|||
(DutyType::Normal, _, DutyCategory::GatheringForays) => PartyFinderCategory::GatheringForays,
|
||||
(DutyType::Other, _, DutyCategory::DeepDungeons) => PartyFinderCategory::DeepDungeons,
|
||||
(DutyType::Normal, _, DutyCategory::AdventuringForays) => PartyFinderCategory::AdventuringForays,
|
||||
(DutyType::Normal, _, DutyCategory::VariantAndCriterionDungeonFinder) => PartyFinderCategory::VariantAndCriterionDungeonFinder,
|
||||
(DutyType::Normal, Some(DutyInfo { high_end: true, .. }), _) => PartyFinderCategory::HighEndDuty,
|
||||
(DutyType::Normal, Some(DutyInfo { content_kind: ContentKind::Dungeons, .. }), _) => PartyFinderCategory::Dungeons,
|
||||
(DutyType::Normal, Some(DutyInfo { content_kind: ContentKind::Guildhests, .. }), _) => PartyFinderCategory::Guildhests,
|
||||
|
@ -236,7 +235,6 @@ pub enum DutyCategory {
|
|||
GatheringForays = 1 << 4,
|
||||
DeepDungeons = 1 << 5,
|
||||
AdventuringForays = 1 << 6,
|
||||
VariantAndCriterionDungeonFinder = 1 << 7,
|
||||
}
|
||||
|
||||
impl DutyCategory {
|
||||
|
@ -367,8 +365,6 @@ bitflags! {
|
|||
const BLUE_MAGE = 1 << 25;
|
||||
const GUNBREAKER = 1 << 26;
|
||||
const DANCER = 1 << 27;
|
||||
const REAPER = 1 << 28;
|
||||
const SAGE = 1 << 29;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -484,14 +480,6 @@ impl JobFlags {
|
|||
cjs.push(ClassJob::Job(Job::Dancer));
|
||||
}
|
||||
|
||||
if self.contains(Self::REAPER) {
|
||||
cjs.push(ClassJob::Job(Job::Reaper));
|
||||
}
|
||||
|
||||
if self.contains(Self::SAGE) {
|
||||
cjs.push(ClassJob::Job(Job::Sage));
|
||||
}
|
||||
|
||||
cjs
|
||||
}
|
||||
}
|
||||
|
@ -512,12 +500,11 @@ pub enum PartyFinderCategory {
|
|||
GatheringForays,
|
||||
DeepDungeons,
|
||||
AdventuringForays,
|
||||
VariantAndCriterionDungeonFinder,
|
||||
None,
|
||||
}
|
||||
|
||||
impl PartyFinderCategory {
|
||||
pub const ALL: [Self; 16] = [
|
||||
pub const ALL: [Self; 15] = [
|
||||
Self::DutyRoulette,
|
||||
Self::Dungeons,
|
||||
Self::Guildhests,
|
||||
|
@ -532,7 +519,6 @@ impl PartyFinderCategory {
|
|||
Self::GatheringForays,
|
||||
Self::DeepDungeons,
|
||||
Self::AdventuringForays,
|
||||
Self::VariantAndCriterionDungeonFinder,
|
||||
Self::None,
|
||||
];
|
||||
|
||||
|
@ -552,7 +538,6 @@ impl PartyFinderCategory {
|
|||
Self::GatheringForays => "GatheringForays",
|
||||
Self::DeepDungeons => "DeepDungeons",
|
||||
Self::AdventuringForays => "AdventuringForays",
|
||||
Self::VariantAndCriterionDungeonFinder => "V&C Dungeon Finder",
|
||||
Self::None => "None",
|
||||
}
|
||||
}
|
||||
|
@ -643,12 +628,6 @@ impl PartyFinderCategory {
|
|||
de: "Feldexkursion",
|
||||
fr: "Missions d'exploration",
|
||||
},
|
||||
Self::VariantAndCriterionDungeonFinder => LocalisedText {
|
||||
en: "V&C Dungeon Finder",
|
||||
ja: "特殊ダンジョン探索",
|
||||
de: "Gewölbesuche",
|
||||
fr: "Donjons spéciaux",
|
||||
},
|
||||
Self::None => LocalisedText {
|
||||
en: "None",
|
||||
ja: "設定なし",
|
||||
|
|
|
@ -41,7 +41,7 @@ async fn main() {
|
|||
if let Err(e) = self::web::start(Arc::new(config)).await {
|
||||
eprintln!("error: {}", e);
|
||||
eprintln!(" {:?}", e);
|
||||
// eprintln!("{}", e.backtrace());
|
||||
eprintln!("{}", e.backtrace());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,30 +11,24 @@ pub struct CachedStatistics {
|
|||
pub seven_days: Statistics,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Aliases {
|
||||
#[serde(deserialize_with = "alias_de")]
|
||||
pub aliases: HashMap<u32, Alias>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Statistics {
|
||||
pub count: Vec<Count>,
|
||||
#[serde(default)]
|
||||
pub aliases: HashMap<u32, Alias>,
|
||||
#[serde(deserialize_with = "alias_de")]
|
||||
pub aliases: HashMap<u32, Vec<Alias>>,
|
||||
pub duties: Vec<DutyInfo>,
|
||||
pub hosts: Vec<HostInfo>,
|
||||
pub hours: Vec<HourInfo>,
|
||||
pub days: Vec<DayInfo>,
|
||||
}
|
||||
|
||||
fn alias_de<'de, D>(de: D) -> std::result::Result<HashMap<u32, Alias>, D::Error>
|
||||
where D: Deserializer<'de>
|
||||
fn alias_de<'de, D>(de: D) -> std::result::Result<HashMap<u32, Vec<Alias>>, D::Error>
|
||||
where D: Deserializer<'de>
|
||||
{
|
||||
let aliases: Vec<AliasInfo> = Deserialize::deserialize(de)?;
|
||||
let map = aliases
|
||||
.into_iter()
|
||||
.map(|info| (info.content_id, info.alias))
|
||||
.map(|info| (info.content_id_lower, info.aliases))
|
||||
.collect();
|
||||
Ok(map)
|
||||
}
|
||||
|
@ -49,17 +43,25 @@ impl Statistics {
|
|||
}
|
||||
|
||||
pub fn player_name(&self, cid: &u32) -> Cow<str> {
|
||||
let alias = match self.aliases.get(cid) {
|
||||
let aliases = match self.aliases.get(cid) {
|
||||
Some(a) => a,
|
||||
None => return "<unknown>".into(),
|
||||
};
|
||||
|
||||
let world = match crate::ffxiv::WORLDS.get(&alias.home_world) {
|
||||
if aliases.is_empty() {
|
||||
return "<unknown>".into();
|
||||
}
|
||||
|
||||
let world = match crate::ffxiv::WORLDS.get(&aliases[0].home_world) {
|
||||
Some(world) => world.name(),
|
||||
None => "<unknown>",
|
||||
};
|
||||
|
||||
format!("{} @ {}", alias.name.text(), world).into()
|
||||
format!("{} @ {}", aliases[0].name.text(), world).into()
|
||||
}
|
||||
|
||||
pub fn num_host_listings(&self) -> usize {
|
||||
self.hosts.iter().map(|info| info.count).sum()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,8 +73,8 @@ pub struct Count {
|
|||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct AliasInfo {
|
||||
#[serde(rename = "_id")]
|
||||
pub content_id: u32,
|
||||
pub alias: Alias,
|
||||
pub content_id_lower: u32,
|
||||
pub aliases: Vec<Alias>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
|
@ -106,28 +108,7 @@ impl DutyInfo {
|
|||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct HostInfo {
|
||||
#[serde(rename = "_id")]
|
||||
pub created_world: u32,
|
||||
pub count: usize,
|
||||
pub content_ids: Vec<HostInfoInfo>,
|
||||
}
|
||||
|
||||
impl HostInfo {
|
||||
pub fn num_other(&self) -> usize {
|
||||
let top15: usize = self.content_ids.iter().map(|info| info.count).sum();
|
||||
self.count - top15
|
||||
}
|
||||
|
||||
pub fn world_name(&self) -> &'static str {
|
||||
match crate::ffxiv::WORLDS.get(&self.created_world) {
|
||||
Some(world) => world.name(),
|
||||
None => "<unknown>",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct HostInfoInfo {
|
||||
pub content_id: u32,
|
||||
pub content_id_lower: u32,
|
||||
pub count: usize,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
mod stats;
|
||||
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
convert::Infallible,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::{Result, Context};
|
||||
use chrono::Utc;
|
||||
use mongodb::{
|
||||
bson::doc,
|
||||
Client as MongoClient,
|
||||
Collection,
|
||||
IndexModel,
|
||||
bson::doc,
|
||||
options::{IndexOptions, UpdateOptions},
|
||||
results::UpdateResult,
|
||||
};
|
||||
|
@ -19,11 +20,10 @@ use tokio::sync::RwLock;
|
|||
use tokio_stream::StreamExt;
|
||||
use warp::{
|
||||
Filter,
|
||||
Reply,
|
||||
filters::BoxedFilter,
|
||||
http::Uri,
|
||||
Reply,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
ffxiv::Language,
|
||||
|
@ -34,8 +34,6 @@ use crate::{
|
|||
template::stats::StatsTemplate,
|
||||
};
|
||||
|
||||
mod stats;
|
||||
|
||||
pub async fn start(config: Arc<Config>) -> Result<()> {
|
||||
let state = State::new(Arc::clone(&config)).await?;
|
||||
|
||||
|
@ -151,10 +149,6 @@ fn assets() -> BoxedFilter<(impl Reply, )> {
|
|||
.or(listings_js())
|
||||
.or(stats_css())
|
||||
.or(stats_js())
|
||||
.or(d3())
|
||||
.or(pico())
|
||||
.or(common_js())
|
||||
.or(list_js())
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
|
@ -208,34 +202,6 @@ fn stats_js() -> BoxedFilter<(impl Reply, )> {
|
|||
.boxed()
|
||||
}
|
||||
|
||||
fn d3() -> BoxedFilter<(impl Reply, )> {
|
||||
warp::path("d3.js")
|
||||
.and(warp::path::end())
|
||||
.and(warp::fs::file("./assets/d3.v7.min.js"))
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn pico() -> BoxedFilter<(impl Reply, )> {
|
||||
warp::path("pico.css")
|
||||
.and(warp::path::end())
|
||||
.and(warp::fs::file("./assets/pico.min.css"))
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn common_js() -> BoxedFilter<(impl Reply, )> {
|
||||
warp::path("common.js")
|
||||
.and(warp::path::end())
|
||||
.and(warp::fs::file("./assets/common.js"))
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn list_js() -> BoxedFilter<(impl Reply, )> {
|
||||
warp::path("list.js")
|
||||
.and(warp::path::end())
|
||||
.and(warp::fs::file("./assets/list.min.js"))
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn index() -> BoxedFilter<(impl Reply, )> {
|
||||
let route = warp::path::end()
|
||||
.map(|| warp::redirect(Uri::from_static("/listings")));
|
||||
|
@ -270,12 +236,6 @@ fn listings(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
|
|||
"updated_at": { "$gte": two_hours_ago },
|
||||
}
|
||||
},
|
||||
doc! {
|
||||
"$match": {
|
||||
// filter private pfs
|
||||
"listing.search_area": { "$bitsAllClear": 2 },
|
||||
}
|
||||
},
|
||||
doc! {
|
||||
"$set": {
|
||||
"time_left": {
|
||||
|
@ -450,11 +410,7 @@ fn contribute_multiple(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
|
|||
warp::post().and(route).boxed()
|
||||
}
|
||||
|
||||
async fn insert_listing(state: &State, mut listing: PartyFinderListing) -> Result<UpdateResult> {
|
||||
if listing.created_world >= 1_000 || listing.home_world >= 1_000 || listing.current_world >= 1_000 {
|
||||
anyhow::bail!("invalid listing");
|
||||
}
|
||||
|
||||
async fn insert_listing(state: &State, listing: PartyFinderListing) -> mongodb::error::Result<UpdateResult> {
|
||||
let opts = UpdateOptions::builder()
|
||||
.upsert(true)
|
||||
.build();
|
||||
|
@ -482,5 +438,4 @@ async fn insert_listing(state: &State, mut listing: PartyFinderListing) -> Resul
|
|||
opts,
|
||||
)
|
||||
.await
|
||||
.context("could not insert record")
|
||||
}
|
||||
|
|
|
@ -1,19 +1,12 @@
|
|||
use anyhow::Result;
|
||||
use chrono::{Duration, Utc};
|
||||
use mongodb::bson::{Document, doc};
|
||||
use mongodb::options::AggregateOptions;
|
||||
use tokio_stream::StreamExt;
|
||||
use crate::stats::{Aliases, Statistics};
|
||||
use crate::stats::Statistics;
|
||||
use crate::web::State;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref QUERY: [Document; 2] = [
|
||||
doc! {
|
||||
"$match": {
|
||||
// filter private pfs
|
||||
"listing.search_area": { "$bitsAllClear": 2 },
|
||||
}
|
||||
},
|
||||
static ref QUERY: [Document; 1] = [
|
||||
doc! {
|
||||
"$facet": {
|
||||
"count": [
|
||||
|
@ -21,6 +14,19 @@ lazy_static::lazy_static! {
|
|||
"$count": "count",
|
||||
},
|
||||
],
|
||||
"aliases": [
|
||||
{
|
||||
"$group": {
|
||||
"_id": "$listing.content_id_lower",
|
||||
"aliases": {
|
||||
"$addToSet": {
|
||||
"name": "$listing.name",
|
||||
"home_world": "$listing.home_world",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
],
|
||||
"duties": [
|
||||
{
|
||||
"$group": {
|
||||
|
@ -43,42 +49,20 @@ lazy_static::lazy_static! {
|
|||
"hosts": [
|
||||
{
|
||||
"$group": {
|
||||
"_id": {
|
||||
"world": "$listing.created_world",
|
||||
"content_id": "$listing.content_id_lower",
|
||||
"_id": "$listing.content_id_lower",
|
||||
"count": {
|
||||
"$sum": 1
|
||||
},
|
||||
"count": { "$sum": 1 },
|
||||
}
|
||||
},
|
||||
{
|
||||
"$sort": {
|
||||
"count": -1,
|
||||
"count": -1
|
||||
}
|
||||
},
|
||||
{
|
||||
"$group": {
|
||||
"_id": "$_id.world",
|
||||
"count": {
|
||||
"$sum": "$count",
|
||||
},
|
||||
"content_ids": {
|
||||
"$push": {
|
||||
"content_id": "$_id.content_id",
|
||||
"count": "$count",
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"$addFields": {
|
||||
"content_ids": {
|
||||
"$slice": ["$content_ids", 0, 15],
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
"$sort": { "count": -1 }
|
||||
},
|
||||
"$limit": 15
|
||||
}
|
||||
],
|
||||
"hours": [
|
||||
{
|
||||
|
@ -117,31 +101,6 @@ lazy_static::lazy_static! {
|
|||
}
|
||||
},
|
||||
];
|
||||
|
||||
static ref ALIASES_QUERY: [Document; 1] = [
|
||||
doc! {
|
||||
"$facet": {
|
||||
"aliases": [
|
||||
{
|
||||
"$sort": {
|
||||
"created_at": -1,
|
||||
}
|
||||
},
|
||||
{
|
||||
"$group": {
|
||||
"_id": "$listing.content_id_lower",
|
||||
"alias": {
|
||||
"$first": {
|
||||
"name": "$listing.name",
|
||||
"home_world": "$listing.home_world",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
pub async fn get_stats(state: &State) -> Result<Statistics> {
|
||||
|
@ -166,34 +125,10 @@ pub async fn get_stats_seven_days(state: &State) -> Result<Statistics> {
|
|||
async fn get_stats_internal(state: &State, docs: impl IntoIterator<Item = Document>) -> Result<Statistics> {
|
||||
let mut cursor = state
|
||||
.collection()
|
||||
.aggregate(docs, AggregateOptions::builder()
|
||||
.allow_disk_use(true)
|
||||
.build())
|
||||
.aggregate(docs, None)
|
||||
.await?;
|
||||
let doc = cursor.try_next().await?;
|
||||
let doc = doc.ok_or_else(|| anyhow::anyhow!("missing document"))?;
|
||||
let mut stats: Statistics = mongodb::bson::from_document(doc)?;
|
||||
|
||||
let ids: Vec<u32> = stats.hosts.iter().flat_map(|host| host.content_ids.iter().map(|entry| entry.content_id)).collect();
|
||||
let mut aliases_query: Vec<Document> = ALIASES_QUERY.iter().cloned().collect();
|
||||
aliases_query.insert(0, doc! {
|
||||
"$match": {
|
||||
"listing.content_id_lower": {
|
||||
"$in": ids,
|
||||
}
|
||||
}
|
||||
});
|
||||
let mut cursor = state
|
||||
.collection()
|
||||
.aggregate(aliases_query, AggregateOptions::builder()
|
||||
.allow_disk_use(true)
|
||||
.build())
|
||||
.await?;
|
||||
let doc = cursor.try_next().await?;
|
||||
let doc = doc.ok_or_else(|| anyhow::anyhow!("missing document"))?;
|
||||
let aliases: Aliases = mongodb::bson::from_document(doc)?;
|
||||
|
||||
stats.aliases = aliases.aliases;
|
||||
|
||||
let stats = mongodb::bson::from_document(doc)?;
|
||||
Ok(stats)
|
||||
}
|
||||
|
|
|
@ -1,43 +1,11 @@
|
|||
<!doctype html>
|
||||
<html lang="{{ lang.code() }}" class="no-js" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<link rel="stylesheet" href="/assets/minireset.css"/>
|
||||
<link rel="stylesheet" href="/assets/pico.css"/>
|
||||
<script defer src="/assets/common.js"></script>
|
||||
{%- block head %}{% endblock -%}
|
||||
</head>
|
||||
<body>
|
||||
<nav class="container">
|
||||
<ul>
|
||||
<li><strong><a href="/" class="contrast">xivpf</a></strong></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://www.patreon.com/join/lojewalo">patreon</a>
|
||||
</li>
|
||||
<li role="list" dir="rtl">
|
||||
<a href="javascript:void(0)" aria-haspopup="listbox">stats</a>
|
||||
<ul role="listbox">
|
||||
<li><a href="/stats">all time</a></li>
|
||||
<li><a href="/stats/7days">7 days</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li role="list" dir="rtl">
|
||||
<a href="javascript:void(0)" aria-haspopup="listbox">{{ lang.name() }}</a>
|
||||
<ul role="listbox" aria-haspopup="listbox" id="language" data-accept="{{ lang.code() }}">
|
||||
<li><a href="javascript:void(0)" data-value="en">english</a></li>
|
||||
<li><a href="javascript:void(0)" data-value="ja">日本語</a></li>
|
||||
<li><a href="javascript:void(0)" data-value="de">deutsch</a></li>
|
||||
<li><a href="javascript:void(0)" data-value="fr">français</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="container">
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
<html lang="{{ lang.code() }}" class="no-js">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<link rel="stylesheet" href="/assets/minireset.css"/>
|
||||
{%- block head %}{% endblock -%}
|
||||
</head>
|
||||
<body>{% block body %}{% endblock %}</body>
|
||||
</html>
|
||||
|
|
|
@ -1,54 +1,50 @@
|
|||
{% extends "_frame.html" %}
|
||||
|
||||
{% block title -%}
|
||||
xivpf - listings
|
||||
Remote Party Finder
|
||||
{%- endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="/assets/common.css"/>
|
||||
<link rel="stylesheet" href="/assets/listings.css"/>
|
||||
<script defer src="/assets/list.js"></script>
|
||||
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/list.js/2.3.1/list.min.js"></script>
|
||||
<script defer src="/assets/listings.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div id="container">
|
||||
<div class="requires-js settings">
|
||||
<div class="controls">
|
||||
<input type="search" class="search" placeholder="search"/>
|
||||
<div class="left">
|
||||
<input type="search" class="search" placeholder="Search"/>
|
||||
<select id="data-centre-filter">
|
||||
<option value="All">all</option>
|
||||
<optgroup label="north america">
|
||||
<option value="Aether">aether</option>
|
||||
<option value="Crystal">crystal</option>
|
||||
<option value="Dynamis">dynamis</option>
|
||||
<option value="Primal">primal</option>
|
||||
<option value="All">All</option>
|
||||
<optgroup label="North America">
|
||||
<option value="Aether">Aether</option>
|
||||
<option value="Crystal">Crystal</option>
|
||||
<option value="Primal">Primal</option>
|
||||
</optgroup>
|
||||
<optgroup label="europe">
|
||||
<option value="Chaos">chaos</option>
|
||||
<option value="Light">light</option>
|
||||
<optgroup label="Europe">
|
||||
<option value="Chaos">Chaos</option>
|
||||
<option value="Light">Light</option>
|
||||
</optgroup>
|
||||
<optgroup label="japan">
|
||||
<option value="Elemental">elemental</option>
|
||||
<option value="Gaia">gaia</option>
|
||||
<option value="Mana">mana</option>
|
||||
<option value="Meteor">meteor</option>
|
||||
<optgroup label="Japan">
|
||||
<option value="Elemental">Elemental</option>
|
||||
<option value="Gaia">Gaia</option>
|
||||
<option value="Mana">Mana</option>
|
||||
</optgroup>
|
||||
<optgroup label="oceania">
|
||||
<option value="Materia">materia</option>
|
||||
<optgroup label="Oceania">
|
||||
<option disabled value="">Not yet lmao</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<details class="filter-controls">
|
||||
<summary>advanced</summary>
|
||||
<summary>Advanced</summary>
|
||||
<div>
|
||||
<div class="control">
|
||||
<label>
|
||||
Categories
|
||||
<select multiple id="category-filter">
|
||||
{%- for category in PartyFinderCategory::ALL %}
|
||||
<option value="{{ category.as_str() }}">{{ category.name().text(lang) }}</option>
|
||||
<option value="{{ category.as_str() }}">{{ category.name().text(lang) }}</option>
|
||||
{%- endfor %}
|
||||
</select>
|
||||
</label>
|
||||
|
@ -56,6 +52,14 @@ xivpf - listings
|
|||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="right">
|
||||
<select id="language" data-accept="{{ lang.code() }}">
|
||||
<option value="en">English</option>
|
||||
<option value="ja">日本語</option>
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="fr">Français</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="listings" class="list">
|
||||
{%- if containers.is_empty() %}
|
||||
|
@ -64,10 +68,10 @@ xivpf - listings
|
|||
{%- for container in containers %}
|
||||
{%- let listing = container.listing.borrow() %}
|
||||
<div
|
||||
class="listing"
|
||||
data-id="{{ listing.id }}"
|
||||
data-centre="{{ listing.data_centre_name().unwrap_or_default() }}"
|
||||
data-pf-category="{{ listing.html_pf_category() }}">
|
||||
class="listing"
|
||||
data-id="{{ listing.id }}"
|
||||
data-centre="{{ listing.data_centre_name().unwrap_or_default() }}"
|
||||
data-pf-category="{{ listing.html_pf_category() }}">
|
||||
<div class="left">
|
||||
{%- let duty_class %}
|
||||
{%- if listing.is_cross_world() %}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
{% extends "_frame.html" %}
|
||||
|
||||
{% block title -%}
|
||||
xivpf - stats
|
||||
Remote Party Finder
|
||||
{%- endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="/assets/common.css"/>
|
||||
<link rel="stylesheet" href="/assets/stats.css"/>
|
||||
<script defer src="/assets/d3.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.5.1/dist/chart.min.js"></script>
|
||||
<script defer src="/assets/stats.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
|
@ -19,7 +19,8 @@ xivpf - stats
|
|||
<div class="chart-containers">
|
||||
<div class="container">
|
||||
<h1>Top categories</h1>
|
||||
<div id="dutiesChart" class="chart">
|
||||
<div class="chart">
|
||||
<canvas id="dutiesChart"></canvas>
|
||||
</div>
|
||||
<details>
|
||||
<summary>Details</summary>
|
||||
|
@ -44,33 +45,29 @@ xivpf - stats
|
|||
|
||||
<div class="container">
|
||||
<h1>Top hosts</h1>
|
||||
<div id="hostsChart" class="chart">
|
||||
<div class="chart">
|
||||
<canvas id="hostsChart"></canvas>
|
||||
</div>
|
||||
<details>
|
||||
<summary>Details</summary>
|
||||
<table id="hosts">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>World (created)</th>
|
||||
<th>Name</th>
|
||||
<th>Count</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for info in stats.hosts %}
|
||||
{%- for entry in info.content_ids %}
|
||||
<tr>
|
||||
<td>{{ info.world_name() }}</td>
|
||||
<td>{{ stats.player_name(entry.content_id) }}</td>
|
||||
<td>{{ entry.count }}</td>
|
||||
</tr>
|
||||
{%- endfor %}
|
||||
<tr>
|
||||
<td>{{ info.world_name() }}</td>
|
||||
<td>Other</td>
|
||||
<td>{{ info.num_other() }}
|
||||
</tr>
|
||||
{%- endfor %}
|
||||
{%- for info in stats.hosts %}
|
||||
<tr>
|
||||
<td>{{ stats.player_name(info.content_id_lower) }}</td>
|
||||
<td>{{ info.count }}</td>
|
||||
</tr>
|
||||
{%- endfor %}
|
||||
<tr>
|
||||
<td>Other</td>
|
||||
<td>{{ stats.num_listings() - stats.num_host_listings() }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</details>
|
||||
|
@ -78,7 +75,8 @@ xivpf - stats
|
|||
|
||||
<div class="container">
|
||||
<h1>Top hours (UTC)</h1>
|
||||
<div id="hoursChart" class="chart">
|
||||
<div class="chart">
|
||||
<canvas id="hoursChart"></canvas>
|
||||
</div>
|
||||
<details>
|
||||
<summary>Details</summary>
|
||||
|
@ -103,7 +101,8 @@ xivpf - stats
|
|||
|
||||
<div class="container">
|
||||
<h1>Top days (UTC)</h1>
|
||||
<div id="daysChart" class="chart">
|
||||
<div class="chart">
|
||||
<canvas id="daysChart"></canvas>
|
||||
</div>
|
||||
<details>
|
||||
<summary>Details</summary>
|
||||
|
|
Loading…
Reference in New Issue