Skip to content

Commit

Permalink
Merge pull request #29 from Wooza/share-via-chat
Browse files Browse the repository at this point in the history
Share data via chat
  • Loading branch information
p3t3rix authored Apr 26, 2023
2 parents fe832e8 + 0710382 commit 7d81c51
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 9 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Just drop the file into your mods folder. You just need this mod on the client,
No argument resets the heatmap back to all ores. Can only handle the ore name in your selected language or the ore tag.
Examples: game:ore-emerald, game:ore-bituminouscoal, Cassiterite.
.pi setsaveintervalminutes [1-60] - Periodically store the prospecting data every x minutes.
.pi share - Share your prospecting data via the chat. Clients with this mod will update their own data based on the new information.
.pi acceptchatsharing [true,false] - Accept prospecting data from the chat.

Each command updates the respective configuration option.

Expand All @@ -33,6 +35,7 @@ Each command updates the respective configuration option.
HeatMapOre [oreName] - The ore selected for the heatmap.
MapMode [0-1] - The mode of the map.
SaveIntervalMinutes [1-60] - Periodically store the prospecting data every x minutes. Default: 1
AcceptChatSharing [bool] - Accept prospecting data shared by other players in the chat? Default: false

## Usage

Expand Down
26 changes: 20 additions & 6 deletions src/Map/ProspectInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace ProspectorInfo.Map
{
[ProtoBuf.ProtoContract(ImplicitFields = ProtoBuf.ImplicitFields.None)]
internal class ProspectInfo
{
public static IEnumerable<KeyValuePair<string, string>> FoundOres { get { return _allOres.Where((pair) => _foundOres.Contains(pair.Value)); } }
Expand Down Expand Up @@ -61,7 +62,9 @@ private static Dictionary<string, RelativeDensity> GetDensityMappingForLang(stri
private static readonly Regex _absoluteDensityRegex = new Regex("([0-9]+[.,]?[0-9]*)",
RegexOptions.Compiled);

[ProtoBuf.ProtoMember(1)]
public readonly int X;
[ProtoBuf.ProtoMember(2)]
public readonly int Z;

/// <summary>
Expand All @@ -85,6 +88,7 @@ public ChunkCoordinate ChunkCoordinate
/// <summary>
/// A sorted list of all ore occurencies in this chunk. The ore with the highest relative density is first.
/// </summary>
[ProtoBuf.ProtoMember(3)]
public readonly List<OreOccurence> Values;

/// <summary>
Expand All @@ -99,6 +103,13 @@ public ChunkCoordinate ChunkCoordinate
[Newtonsoft.Json.JsonIgnore]
private string _messageCache;

/// <summary>
/// Required for ProtoBuf deserialization
/// </summary>
private ProspectInfo()
{
}

[Newtonsoft.Json.JsonConstructor]
public ProspectInfo(int x, int z, string message, List<OreOccurence> values)
{
Expand All @@ -113,8 +124,7 @@ public ProspectInfo(int x, int z, string message, List<OreOccurence> values)
}
else
{
foreach (var val in values)
_foundOres.Add(val.Name);
AddFoundOres();
}
}

Expand Down Expand Up @@ -153,6 +163,12 @@ public override int GetHashCode()
}
}

public void AddFoundOres()
{
foreach (var val in Values)
_foundOres.Add(val.Name);
}

/// <summary>
/// The absolute density values that we get from the server are formatted with the servers system/OS locale (not game locale).
/// Thus, we might get either ',' or '.' as a decimal separator.
Expand Down Expand Up @@ -209,8 +225,7 @@ private void ParseMessage(string message)
}
}

foreach (var val in Values)
_foundOres.Add(val.Name);
AddFoundOres();
}

/// <summary>
Expand Down Expand Up @@ -280,8 +295,7 @@ private void ParseLegacyMessage()
}
}

foreach (var val in Values)
_foundOres.Add(val.Name);
AddFoundOres();

Message = null;
}
Expand Down
35 changes: 32 additions & 3 deletions src/Map/ProspectorOverlayLayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ internal class ProspectorOverlayLayer : MapLayer
private readonly IWorldMapManager _worldMapManager;
private readonly LoadedTexture[] _colorTextures = new LoadedTexture[8];
private bool _temporaryRenderOverride = false;
private readonly ChatDataSharing _chatDataSharing;
private static ModConfig _config;
private static GuiDialog _settingsDialog;

Expand All @@ -47,6 +48,7 @@ public ProspectorOverlayLayer(ICoreAPI api, IWorldMapManager mapSink) : base(api
if (api.Side == EnumAppSide.Client)
{
_clientApi = (ICoreClientAPI)api;
_chatDataSharing = new ChatDataSharing(_clientApi, this, _config);
_clientApi.Event.ChatMessage += OnChatMessage;
_clientApi.Event.AfterActiveSlotChanged += Event_AfterActiveSlotChanged;
_clientApi.Event.PlayerJoin += (p) =>
Expand All @@ -59,9 +61,7 @@ public ProspectorOverlayLayer(ICoreAPI api, IWorldMapManager mapSink) : base(api
};

// Save data when leaving and periodically.
_clientApi.Event.LeaveWorld += () => {
SaveProspectingData();
};
_clientApi.Event.LeaveWorld += SaveProspectingData;
_clientApi.World.RegisterGameTickListener((_) => SaveProspectingData(),
(int) TimeSpan.FromMinutes(_config.SaveIntervalMinutes).TotalMilliseconds);

Expand Down Expand Up @@ -123,6 +123,15 @@ public ProspectorOverlayLayer(ICoreAPI api, IWorldMapManager mapSink) : base(api
"Sets the \"SaveIntervalMinutes\" config option (default = 1)")
.WithArgs(api.ChatCommands.Parsers.IntRange("interval", 1, 60))
.HandleWith(OnSetSaveIntervalMinutes)
.EndSubCommand()
.BeginSubCommand("share")
.WithDescription(".pi share - Share your prospecting data in the chat")
.HandleWith(OnShare)
.EndSubCommand()
.BeginSubCommand("acceptchatsharing")
.WithDescription(".pi acceptchatsharing [bool] - Accept prospecting data shared by other players via '.pi share'.")
.WithArgs(api.ChatCommands.Parsers.OptionalBool("accept"))
.HandleWith(OnAcceptChatSharing)
.EndSubCommand();

for (int i = 0; i < _colorTextures.Length; i++)
Expand Down Expand Up @@ -170,6 +179,7 @@ public void AddOrUpdateProspectingData(params ProspectInfo[] information) {
_prospectInfos[info.ChunkCoordinate] = info;
var newComponent = new ProspectorOverlayMapComponent(_clientApi, info.ChunkCoordinate, info.GetMessage(), _colorTextures[(int)GetRelativeDensity(info)]);
_components[info.ChunkCoordinate] = newComponent;
info.AddFoundOres();
}
_prospectInfos.HasChanged = true;
}
Expand Down Expand Up @@ -282,6 +292,25 @@ private TextCommandResult OnSetSaveIntervalMinutes(TextCommandCallingArgs args)
return TextCommandResult.Success($"Set SaveIntervalMinutes to {_config.SaveIntervalMinutes}.");
}

private TextCommandResult OnShare(TextCommandCallingArgs args)
{
lock(_prospectInfos)
{
_chatDataSharing.ShareData(_prospectInfos);
}
return TextCommandResult.Success("Shared data in chat.");
}

private TextCommandResult OnAcceptChatSharing(TextCommandCallingArgs args)
{
if (args.Parsers[0].IsMissing)
_config.AcceptChatSharing = !_config.AcceptChatSharing;
else
_config.AcceptChatSharing = (bool)args.Parsers[0].GetValue();
_config.Save(api);
return TextCommandResult.Success($"Set AcceptChatSharing to {_config.AcceptChatSharing}.");
}

private void Event_SlotModified(int slotId)
{
UpdateRenderOverride();
Expand Down
9 changes: 9 additions & 0 deletions src/Map/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,16 @@ public OreNames()
}
}

[ProtoBuf.ProtoContract(ImplicitFields = ProtoBuf.ImplicitFields.None)]
internal struct OreOccurence
{
[ProtoBuf.ProtoMember(1)]
public readonly string Name;
[ProtoBuf.ProtoMember(2)]
public readonly string PageCode;
[ProtoBuf.ProtoMember(3, IsRequired = true)]
public readonly RelativeDensity RelativeDensity;
[ProtoBuf.ProtoMember(4)]
public readonly double AbsoluteDensity;

[Newtonsoft.Json.JsonConstructor]
Expand Down Expand Up @@ -57,9 +62,13 @@ public enum MapMode
Default,
Heatmap
}

[ProtoBuf.ProtoContract(ImplicitFields = ProtoBuf.ImplicitFields.None)]
public struct ChunkCoordinate
{
[ProtoBuf.ProtoMember(1)]
public int X;
[ProtoBuf.ProtoMember(2)]
public int Z;

public ChunkCoordinate(int x, int z)
Expand Down
1 change: 1 addition & 0 deletions src/ModConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ public class ModConfig : ModConfigBase
public string HeatMapOre { get; set; } = null;
public bool ShowGui { get; set; } = true;
public int SaveIntervalMinutes { get; set; } = 1;
public bool AcceptChatSharing { get; set; } = false;
}
}
117 changes: 117 additions & 0 deletions src/Utils/ChatDataSharing.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using ProtoBuf;
using System;
using System.IO.Compression;
using System.IO;
using Vintagestory.API.Client;
using Vintagestory.API.Common;
using ProspectorInfo.Map;
using System.Linq;
using System.Text;

namespace ProspectorInfo.Utils
{
internal class ChatDataSharing
{
private static readonly string CHAT_PREFIX = "ProspectingData;";

private readonly ICoreClientAPI api;
private readonly ProspectorOverlayLayer prospectorOverlayLayer;
private readonly ModConfig modConfig;
public ChatDataSharing(ICoreClientAPI api, ProspectorOverlayLayer prospectorOverlayLayer, ModConfig modConfig)
{
this.api = api;
this.prospectorOverlayLayer = prospectorOverlayLayer;
this.modConfig = modConfig;
api.Event.ChatMessage += OnChatMessage;
}

private void OnChatMessage(int groupId, string message, EnumChatType chattype, string data) {
if (chattype != EnumChatType.OthersMessage)
return;

int startIdx = message.IndexOf(CHAT_PREFIX);
if (startIdx == -1)
return;

if (!modConfig.AcceptChatSharing)
{
api.ShowChatMessage("Someone shared prospecting data in chat but you currently don't accept data from the chat.<br/>" +
"Use '.pi acceptchatsharing true' to accept prospecting data in the future.");
return;
}

var result = DeserializeFromBase64<ProspectorMessages>(message.Substring(startIdx + CHAT_PREFIX.Length));
if (result != null)
prospectorOverlayLayer.AddOrUpdateProspectingData(result.Values.ToArray());
}

public void ShareData(ProspectorMessages messages)
{
string data = SerializeToBase64<ProspectorMessages>(messages);
if (data == null)
return;

// Add some whitespace before sending, to avoid lag spikes when rendering the chat message.
// The base64 deserializer ignores whitespace.
api.SendChatMessage(CHAT_PREFIX + AddWhitespace(data, 64));
}

private string SerializeToBase64<T>(T data) {
try
{
using (var resultStream = new MemoryStream())
{
using (var compressionStream = new DeflateStream(resultStream, CompressionLevel.Optimal))
{
Serializer.Serialize(compressionStream, data);
}
byte[] result = resultStream.ToArray();
return Convert.ToBase64String(result);
}
}
catch (Exception ex)
{
api.Logger.Error("Failed to serialize prospecting data", ex);
return null;
}
}

/// <summary>
/// Little hackery to avoid lag spikes when rendering a single long string without whitespace.
/// </summary>
private static string AddWhitespace(string input, int interval)
{
StringBuilder stringBuilder = new StringBuilder();
int currentPosition = 0;
while (currentPosition + interval < input.Length)
{
stringBuilder.Append(input.Substring(currentPosition, interval)).Append(" ");
currentPosition += interval;
}
if (currentPosition < input.Length)
{
stringBuilder.Append(input.Substring(currentPosition));
}
return stringBuilder.ToString();
}

private T DeserializeFromBase64<T>(string message) where T : class
{
try
{
using (var resultStream = new MemoryStream(Convert.FromBase64String(message)))
{
using (var decompressionStream = new DeflateStream(resultStream, CompressionMode.Decompress))
{
return Serializer.Deserialize<T>(decompressionStream);
}
}
}
catch (Exception ex)
{
api.Logger.Error("Failed to deserialize shared prospecting data from chat", ex);
return null;
}
}
}
}

0 comments on commit 7d81c51

Please sign in to comment.