Skip to content

Commit

Permalink
add support for collaboratively scouting with turtle scouter (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
dit-zy authored Jul 20, 2024
1 parent 22ea7e1 commit d7b5d44
Show file tree
Hide file tree
Showing 18 changed files with 572 additions and 147 deletions.
54 changes: 42 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,43 +1,73 @@
# SCOUT HELPER

a dalamud helper plugin for making it easier to interact with scout trackers
a dalamud helper plugin for making it easier to interact with scout trackers.

* [CONTACT](#contact)
* [FEATURES](#features)
* [HOW TO USE](#how-to-use)
* [COLLABORATIVE SCOUTING](#collaborative-scouting)

# CONTACT

if you have feedback or questions about the plugin, you can reach me at ditzy.
[email protected], or find us in the
[light trains discord](https://discord.gg/9YjuHqVG)'s #tech channel.

## FEATURES
# FEATURES

* integrates with multiple scout trackers:
* [bear toolkit](https://tracker.beartoolkit.com/train)
* [siren hunts](https://www.sirenhunts.com/scouting)
* [turtle scouter](https://scout.wobbuffet.net/)
* automatically pulls scouted marks from the
[hunt helper](https://github.com/imaginary-png/HuntHelper) train recorder
* create a whole template of text including multiple additional fields such
as the number of marks and the patch. e.g.:
* divide and conquer! scout collaboratively with other scouters, using turtle
scouter, contributing marks to the same turtle train.
* create a whole template of text including multiple additional fields such as
the number of marks and the patch. e.g.:

![full text example](./images/full-text-example.png)
![full text example](./images/full-text-example.png)

## HOW TO USE
# HOW TO USE

1. use hunt helper's train recorder to record hunt marks while you scout

![hunt helper train recorder](./images/hunt-helper-train.png)
![hunt helper train recorder](./images/hunt-helper-train.png)

2. open scout helper with the `/scouth` (or `/sch`) command

![scout helper main window](./images/main-window.png)
![scout helper main window](./images/main-window.png)

3. pick a copy mode
* link -- only copies the generated tracker link to your clipboard.
* full-text -- copies an entire template to your clipboard including
multiple train fields beyond just the tracker link.
* full-text -- copies an entire template to your clipboard including multiple
train fields beyond just the tracker link.

4. select the tracker website you want to generate a link for. this will copy
the link to your clipboard and echo it to the chat log as a backup
the link to your clipboard and echo it to the chat log as a backup

![chat output](./images/output.png)
![chat output](./images/output.png)

5. share the link with your friends ⸜(≧▽≦)⸝

### COLLABORATIVE SCOUTING

with turtle scouter, it is possible to have multiple scouters contribute to the
same train.

1. click the "COLLAB" button next to the turtle scouter generator.

![turtle collab button](./images/turtle-collab-1.png)
2. either generate a new turtle session, or join an existing session using its
collaboration link.

![turtle collab popup](./images/turtle-collab-2.png)
3. use the turtle scouter generator button to push your scouted marks to the
active turtle session.

![turtle collab generator button](./images/turtle-collab-3.png)

![turtle collab chat log](./images/turtle-collab-4.png)
* NOTE: when you first push scouted marks to a session after joining it, your
whole train will be sent. but after that, only the latest marks that you've
seen will be sent (the marks scouted since the last time you pushed marks).
2 changes: 2 additions & 0 deletions ScoutHelper/Config/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public class Configuration : IPluginConfiguration {

public string SirenBaseUrl { get; set; } = "https://sirenhunts.com/scouting/";

public string TurtleBaseUrl { get; set; } = "https://scout.wobbuffet.net";
public string TurtleTrainPath { get; set; } = "/scout";
public string TurtleApiBaseUrl { get; set; } = "https://scout.wobbuffet.net";
public string TurtleApiTrainPath { get; set; } = "/api/v1/scout";
public TimeSpan TurtleApiTimeout { get; set; } = TimeSpan.FromSeconds(5);
Expand Down
22 changes: 20 additions & 2 deletions ScoutHelper/Localization/Strings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 8 additions & 2 deletions ScoutHelper/Localization/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@
<value>Turtle Scouter</value>
</data>
<data name="TurtleButtonTooltip" xml:space="preserve">
<value>generate a Turtle Scouter link from the Hunt Helper train recorder data.</value>
<value>generate a turtle scouter link from the hunt helper train recorder data.</value>
</data>
<data name="TurtleButtonActiveCollabTooltip" xml:space="preserve">
<value>update the turtle collab session with the latest scouted marks in your hunt helper train recorder.</value>
</data>
<data name="CopyModeLinkButton" xml:space="preserve">
<value>link</value>
Expand Down Expand Up @@ -109,7 +112,10 @@
<value> COLLAB </value>
</data>
<data name="TurtleCollabButtonTooltip" xml:space="preserve">
<value>collaborative scouting through turtle is not yet supported by scout helper, but is being explored :D.</value>
<value>start or join a collaborative scouting session on turtle.</value>
</data>
<data name="TurtleCollabButtonActiveTooltip" xml:space="preserve">
<value>click to leave the current session.</value>
</data>
<data name="ConfigWindowTweaksSectionLabelInstances" xml:space="preserve">
<value>INSTANCES</value>
Expand Down
156 changes: 112 additions & 44 deletions ScoutHelper/Managers/TurtleManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Numerics;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using CSharpFunctionalExtensions;
using Dalamud.Plugin.Services;
Expand All @@ -12,23 +14,30 @@
using ScoutHelper.Models;
using ScoutHelper.Models.Http;
using ScoutHelper.Models.Json;
using ScoutHelper.Utils;
using ScoutHelper.Utils.Functional;
using static ScoutHelper.Managers.TurtleHttpStatus;
using static ScoutHelper.Utils.Utils;

namespace ScoutHelper.Managers;

using PatchDict = IDictionary<Patch, uint>;
using MobDict = IDictionary<uint, (Patch patch, uint turtleMobId)>;
using TerritoryDict = IDictionary<uint, TurtleMapData>;

public class TurtleManager {
public partial class TurtleManager {
[GeneratedRegex(@"(?:/scout)?/?(?<session>\w+)/(?<password>\w+)/?\s*$")]
private static partial Regex CollabLinkRegex();

private readonly IPluginLog _log;
private readonly Configuration _conf;
private static HttpClient HttpClient { get; } = new();

private MobDict MobIdToTurtleId { get; }
private TerritoryDict TerritoryIdToTurtleData { get; }

private string _currentCollabSession = "";
private string _currentCollabPassword = "";

public TurtleManager(
IPluginLog log,
Configuration conf,
Expand All @@ -47,56 +56,98 @@ MobManager mobManager
= LoadData(options.TurtleDataFile, territoryManager, mobManager);
}

public Maybe<(string slug, string password)> JoinCollabSession(string sessionLink) {
var match = CollabLinkRegex().Match(sessionLink);
if (!match.Success) return Maybe.None;

_currentCollabSession = match.Groups["session"].Value;
_currentCollabPassword = match.Groups["password"].Value;
return (_currentCollabSession, _currentCollabPassword);
}

public async Task<TurtleHttpStatus> UpdateCurrentSession(IList<TrainMob> train) {
var turtleSupportedMobs = train.Where(mob => MobIdToTurtleId.ContainsKey(mob.MobId)).AsList();
if (turtleSupportedMobs.IsEmpty())
return NoSupportedMobs;

var httpResult = await
HttpUtils.DoRequest(
_log,
new TurtleTrainUpdateRequest(
_currentCollabPassword,
turtleSupportedMobs.Select(
mob =>
(TerritoryIdToTurtleData[mob.TerritoryId].TurtleId,
mob.Instance.AsTurtleInstance(),
MobIdToTurtleId[mob.MobId].turtleMobId,
mob.Position)
)
),
requestContent => HttpClient.PutAsync($"{_conf.TurtleApiTrainPath}/{_currentCollabSession}", requestContent)
).TapError(
error => {
if (error.ErrorType == HttpErrorType.Timeout) {
_log.Warning("timed out while trying to post updates to turtle session.");
} else if (error.ErrorType == HttpErrorType.Canceled) {
_log.Warning("operation canceled while trying to post updates to turtle session.");
} else if (error.ErrorType == HttpErrorType.HttpException) {
_log.Error(error.Exception, "http exception while trying to post updates to turtle session.");
} else {
_log.Error(error.Exception, "unknown exception while trying to post updates to turtle session.");
}
}
);

return httpResult.IsSuccess ? Success : TurtleHttpStatus.HttpError;
}

public async Task<Result<TurtleLinkData, string>> GenerateTurtleLink(
IList<TrainMob> trainMobs
IList<TrainMob> trainMobs,
bool allowEmpty = false
) {
var turtleSupportedMobs = trainMobs.Where(mob => MobIdToTurtleId.ContainsKey(mob.MobId)).AsList();
if (turtleSupportedMobs.IsEmpty())
if (!allowEmpty && turtleSupportedMobs.IsEmpty())
return "No mobs supported by Turtle Scouter were found in the Hunt Helper train recorder ;-;";

var spawnPoints = turtleSupportedMobs.SelectMaybe(GetRequestInfoForMob).ToList();
var highestPatch = turtleSupportedMobs
.Select(mob => MobIdToTurtleId[mob.MobId].patch)
.Max();

var requestPayload = JsonConvert.SerializeObject(TurtleTrainRequest.CreateRequest(spawnPoints));
_log.Debug("Request payload: {0}", requestPayload);
var requestContent = new StringContent(requestPayload, Encoding.UTF8, Constants.MediaTypeJson);

try {
var response = await HttpClient.PostAsync(_conf.TurtleApiTrainPath, requestContent);
_log.Debug(
"Request: {0}\n\nResponse: {1}",
response.RequestMessage!.ToString(),
response.ToString()
);

response.EnsureSuccessStatusCode();
var highestPatch = allowEmpty
? Patch.DT
: turtleSupportedMobs
.Select(mob => MobIdToTurtleId[mob.MobId].patch)
.Max();

var responseJson = await response.Content.ReadAsStringAsync();
var trainInfo = JsonConvert.DeserializeObject<TurtleTrainResponse>(responseJson)!;
var trainResult = await HttpUtils.DoRequest<TurtleTrainRequest, TurtleTrainResponse>(
_log,
TurtleTrainRequest.CreateRequest(spawnPoints),
requestContent => HttpClient.PostAsync(_conf.TurtleApiTrainPath, requestContent)
);

return new TurtleLinkData(
trainInfo.ReadonlyUrl,
trainInfo.CollaborateUrl,
highestPatch
return trainResult
.Map(trainInfo => TurtleLinkData.From(trainInfo, highestPatch))
.MapError<TurtleLinkData, HttpError, string>(
error => {
string message;
switch (error.ErrorType) {
case HttpErrorType.Timeout: {
message = "Timed out posting the train to Turtle ;-;";
_log.Error(message);
return message;
}
case HttpErrorType.Canceled: {
message = "Generating the Turtle link was canceled >_>";
_log.Warning(message);
return message;
}
case HttpErrorType.HttpException:
_log.Error(error.Exception, "Posting the train to Turtle failed.");
return "Something failed when communicating with Turtle :T";
default:
message = "An unknown error happened while generating the Turtle link D:";
_log.Error(error.Exception, message);
return message;
}
}
);
} catch (TimeoutException) {
const string message = "Timed out posting the train to Turtle ;-;";
_log.Error(message);
return message;
} catch (OperationCanceledException e) {
const string message = "Generating the Turtle link was canceled >_>";
_log.Warning(e, message);
return message;
} catch (HttpRequestException e) {
_log.Error(e, "Posting the train to Turtle failed.");
return "Something failed when communicating with Turtle :T";
} catch (Exception e) {
const string message = "An unknown error happened while generating the Turtle link D:";
_log.Error(e, message);
return message;
}
}

private Maybe<(uint mapId, uint instance, uint pointId, uint mobId)> GetRequestInfoForMob(TrainMob mob) =>
Expand Down Expand Up @@ -202,11 +253,28 @@ KeyValuePair<string, TurtleJsonPatchData> patchData
}
}

public enum TurtleHttpStatus {
Success,
NoSupportedMobs,
HttpError,
}

public record struct TurtleLinkData(
string Slug,
string CollabPassword,
string ReadonlyUrl,
string CollabUrl,
Patch HighestPatch
);
) {
public static TurtleLinkData From(TurtleTrainResponse response, Patch highestPatch) =>
new(
response.Slug,
response.CollaboratorPassword,
response.ReadonlyUrl,
response.CollaborateUrl,
highestPatch
);
}

public static class TurtleExtensions {
public static uint AsTurtleInstance(this uint? instance) {
Expand Down
18 changes: 18 additions & 0 deletions ScoutHelper/Models/Http/HttpError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;
using CSharpFunctionalExtensions;

namespace ScoutHelper.Models.Http;

public record HttpError(
HttpErrorType ErrorType,
Exception? Exception = null
) {
public static implicit operator HttpError(HttpErrorType errorType) => new(errorType);
}

public enum HttpErrorType {
Unknown,
Timeout,
Canceled,
HttpException,
}
Loading

0 comments on commit d7b5d44

Please sign in to comment.