Skip to content
This repository has been archived by the owner on May 15, 2023. It is now read-only.

Commit

Permalink
LFS: add V2 screenshot event API with ValueTuple instead of Tuple and…
Browse files Browse the repository at this point in the history
… clearer orientation enum, bump version
  • Loading branch information
knah committed Dec 10, 2021
1 parent 18f7d8a commit d78873a
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 28 deletions.
17 changes: 17 additions & 0 deletions LagFreeScreenshots/API/LfsApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#nullable enable

namespace LagFreeScreenshots.API
{
public static class LfsApi
{
/// <summary>
/// Called after a creenshot is taken and written to disk
/// </summary>
public static event ScreenshotSavedEventV2? OnScreenshotSavedV2;

public delegate void ScreenshotSavedEventV2(string filePath, int width, int height, MetadataV2? metadata);

internal static void InvokeScreenshotSaved(string filePath, int width, int height, MetadataV2? metadataV2) =>
OnScreenshotSavedV2?.Invoke(filePath, width, height, metadataV2);
}
}
53 changes: 53 additions & 0 deletions LagFreeScreenshots/API/MetadataV2.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using UnityEngine;
using VRC;
using VRC.Core;

namespace LagFreeScreenshots.API
{
public class MetadataV2
{
public readonly ScreenshotRotation ImageRotation;
public readonly APIUser ApiUser;
public readonly ApiWorldInstance WorldInstance;
public Vector3 Position;
public readonly List<(Player, Vector3)> PlayerList;

public MetadataV2(ScreenshotRotation imageRotation, APIUser apiUser, ApiWorldInstance apiWorldInstance, Vector3 position, List<(Player, Vector3)> playerList)
{
ImageRotation = imageRotation;
ApiUser = apiUser;
WorldInstance = apiWorldInstance;
Position = position;
PlayerList = playerList;
}

public override string ToString()
{
var worldString = "null,0,Not in any world";
if (WorldInstance != null && WorldInstance.world != null)
worldString = WorldInstance.world.id + "," + WorldInstance.name + "," + WorldInstance.world.name;

var positionString = Position.x.ToString(CultureInfo.InvariantCulture) + "," + Position.y.ToString(CultureInfo.InvariantCulture) + "," + Position.z.ToString(CultureInfo.InvariantCulture);

return "lfs|2|author:"
+ ApiUser.id + "," + ApiUser.displayName
+ "|world:" + worldString
+ "|pos:" + positionString
+ (ImageRotation != ScreenshotRotation.NoRotation ? "|rq:" + ImageRotation : "")
+ "|players:" + string.Join(";", PlayerList.Select(PlayerListToString));
}

private static string PlayerListToString((Player, Vector3) playerData)
{
if (playerData.Item1 == null || playerData.Item1.prop_APIUser_0 == null) return "null,0,0,0,null";
return playerData.Item1.prop_APIUser_0.id + "," +
playerData.Item2.x.ToString("0.00", CultureInfo.InvariantCulture) + "," +
playerData.Item2.y.ToString("0.00", CultureInfo.InvariantCulture) + "," +
playerData.Item2.z.ToString("0.00", CultureInfo.InvariantCulture) + "," +
playerData.Item1.prop_APIUser_0.displayName;
}
}
}
11 changes: 11 additions & 0 deletions LagFreeScreenshots/API/ScreenshotRotation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace LagFreeScreenshots.API
{
public enum ScreenshotRotation
{
AutoRotationDisabled = -1,
NoRotation = 0,
Clockwise90 = 1,
Clockwise180 = 2,
CounterClockwise90 = 3
}
}
7 changes: 6 additions & 1 deletion LagFreeScreenshots/EventHandler.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
using System;
using LagFreeScreenshots.API;

namespace LagFreeScreenshots
{
[Obsolete("Use LagFreeScreenshots.API.LfsApi")]
public static class EventHandler
{
/// <summary>
/// Calls when an screenshot is saved
/// </summary>
public static event Action<string, int, int, Metadata> OnScreenshotSaved;

internal static void InvokeScreenshotSaved(string filePath, int width, int height, Metadata metadata) => OnScreenshotSaved?.Invoke(filePath, width, height, metadata);
internal static void InvokeScreenshotSaved(string filePath, int width, int height, MetadataV2 metadata)
{
OnScreenshotSaved?.Invoke(filePath, width, height, new Metadata(metadata));
}
}
}
4 changes: 2 additions & 2 deletions LagFreeScreenshots/LagFreeScreenshots.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
<TargetFramework>net472</TargetFramework>
<VrcReferences>true</VrcReferences>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>8</LangVersion>
<Version>1.2.7.0</Version>
<LangVersion>latest</LangVersion>
<Version>1.3.0.0</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
51 changes: 26 additions & 25 deletions LagFreeScreenshots/LagFreeScreenshotsMod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@
using Object = UnityEngine.Object;
using CameraTakePhotoEnumerator = VRC.UserCamera.CameraUtil._TakeScreenShot_d__5;
using System.Collections.Generic;
using System.Globalization;
using LagFreeScreenshots.API;
using Unity.Collections.LowLevel.Unsafe;

// using CameraUtil = ObjectPublicCaSiVeUnique;

[assembly:MelonInfo(typeof(LagFreeScreenshotsMod), "Lag Free Screenshots", "1.2.7", "knah, Protected", "https://github.com/knah/VRCMods")]
[assembly:MelonInfo(typeof(LagFreeScreenshotsMod), "Lag Free Screenshots", "1.3.0", "knah, Protected", "https://github.com/knah/VRCMods")]
[assembly:MelonGame("VRChat", "VRChat")]

namespace LagFreeScreenshots
Expand Down Expand Up @@ -81,27 +81,27 @@ private static void AddEnumSettings()
ExpansionKitApi.RegisterSettingAsStringEnum(SettingsCategory, SettingScreenshotFormat, new []{("png", "PNG"), ("jpeg", "JPEG")});
}

private static int GetPictureAutorotation(Camera camera)
private static ScreenshotRotation GetPictureAutorotation(Camera camera)
{
var pitch = Vector3.Angle(camera.transform.forward, new Vector3(0, 1, 0));
if (pitch < 45 || pitch > 135) return 0; //Pointing up/down, rotation doesn't matter
if (pitch < 45 || pitch > 135) return ScreenshotRotation.NoRotation; //Pointing up/down, rotation doesn't matter

var rot = camera.transform.localEulerAngles.z;
if (rot >= 45 && rot < 135) return 3;
if (rot >= 135 && rot < 225) return 2;
if (rot >= 225 && rot < 315) return 1;
return 0;
if (rot >= 45 && rot < 135) return ScreenshotRotation.CounterClockwise90;
if (rot >= 135 && rot < 225) return ScreenshotRotation.Clockwise180;
if (rot >= 225 && rot < 315) return ScreenshotRotation.Clockwise90;
return ScreenshotRotation.NoRotation;
}

private static List<Tuple<Player, Vector3>> GetPlayerList(Camera camera)
private static List<(Player, Vector3)> GetPlayerList(Camera camera)
{
var playerManager = PlayerManager.field_Private_Static_PlayerManager_0;
if (playerManager == null) return new List<Tuple<Player, Vector3>>();
if (playerManager == null) return new();

var localPlayer = VRCPlayer.field_Internal_Static_VRCPlayer_0;
if (localPlayer == null) return new List<Tuple<Player, Vector3>>();
if (localPlayer == null) return new();

var result = new List<Tuple<Player, Vector3>>();
var result = new List<(Player, Vector3)>();

var localPosition = localPlayer.gameObject.transform.position;

Expand All @@ -115,12 +115,12 @@ private static List<Tuple<Player, Vector3>> GetPlayerList(Camera camera)
if (viewPos.z < 2 && Vector3.Distance(localPosition, playerPosition) < 2)
{
//User standing right next to photographer, might be visible (approx.)
result.Add(Tuple.Create(p, viewPos));
result.Add((p, viewPos));
}
else if (viewPos.x > -0.03 && viewPos.x < 1.03 && viewPos.y > -0.03 && viewPos.y < 1.03 && viewPos.z > 2 && viewPos.z < 30)
{
//User in viewport, might be obstructed but still...
result.Add(Tuple.Create(p, viewPos));
result.Add((p, viewPos));
}
}

Expand Down Expand Up @@ -256,14 +256,14 @@ public static async Task TakeScreenshot(Camera camera, int w, int h, bool hasAlp
if (!Directory.Exists(targetDir))
Directory.CreateDirectory(targetDir);

Metadata metadata = null;
int rotationQuarters = 0;
MetadataV2 metadata = null;
var rotationQuarters = ScreenshotRotation.AutoRotationDisabled;

if (ourAutorotation.Value)
rotationQuarters = GetPictureAutorotation(camera);

if (ourMetadata.Value)
metadata = new Metadata(ourAutorotation.Value ? rotationQuarters : -1, APIUser.CurrentUser, RoomManager.field_Internal_Static_ApiWorldInstance_0, VRCPlayer.field_Internal_Static_VRCPlayer_0 == null ? new Vector3(0, 0, 0) : VRCPlayer.field_Internal_Static_VRCPlayer_0.transform.position, GetPlayerList(camera));
metadata = new MetadataV2(rotationQuarters, APIUser.CurrentUser, RoomManager.field_Internal_Static_ApiWorldInstance_0, VRCPlayer.field_Internal_Static_VRCPlayer_0 == null ? new Vector3(0, 0, 0) : VRCPlayer.field_Internal_Static_VRCPlayer_0.transform.position, GetPlayerList(camera));

await EncodeAndSavePicture(targetFile, data, w, h, hasAlpha, rotationQuarters, metadata)
.ConfigureAwait(false);
Expand Down Expand Up @@ -340,7 +340,7 @@ private static unsafe void FlipHorInPlace((IntPtr, int Length) data, int w, int


private static async Task EncodeAndSavePicture(string filePath, (IntPtr, int Length) pixelsPair, int w, int h,
bool hasAlpha, int rotationQuarters, Metadata metadata)
bool hasAlpha, ScreenshotRotation rotationQuarters, MetadataV2 metadata)
{
if (pixelsPair.Item1 == IntPtr.Zero) return;

Expand Down Expand Up @@ -369,18 +369,18 @@ private static async Task EncodeAndSavePicture(string filePath, (IntPtr, int Len
}
}

if (rotationQuarters == 1) //90deg cw
if (rotationQuarters == ScreenshotRotation.Clockwise90) //90deg cw
{
pixelsPair = TransposeAndDestroyOriginal(pixelsPair, w, h, step);
var t = w;
w = h;
h = t;
}
else if (rotationQuarters == 2) //180deg cw
else if (rotationQuarters == ScreenshotRotation.Clockwise180) //180deg cw
{
FlipHorInPlace(pixelsPair, w, h, step);
}
else if (rotationQuarters == 3) //270deg cw
else if (rotationQuarters == ScreenshotRotation.CounterClockwise90) //270deg cw
{
FlipHorInPlace(pixelsPair, w, h, step);
FlipVertInPlace(pixelsPair, w, h, step);
Expand All @@ -404,13 +404,14 @@ private static async Task EncodeAndSavePicture(string filePath, (IntPtr, int Len
bitmap.UnlockBits(bitmapData);
Marshal.FreeHGlobal(pixelsPair.Item1);

var description = metadata?.ToString();

// https://docs.microsoft.com/en-us/windows/win32/gdiplus/-gdiplus-constant-property-item-descriptions
if (metadata != null)
if (description != null)
{
// png description is saved as iTXt chunk manually
if (ourFormat.Value == "jpeg")
{
var description = metadata.ConvertToString();
var stringBytesCount = Encoding.Unicode.GetByteCount(description);
var allBytes = new byte[8 + stringBytesCount];
Encoding.ASCII.GetBytes("UNICODE\0", 0, 8, allBytes, 0);
Expand Down Expand Up @@ -438,9 +439,8 @@ private static async Task EncodeAndSavePicture(string filePath, (IntPtr, int Len
else
{
bitmap.Save(filePath, ImageFormat.Png);
if (metadata != null)
if (description != null)
{
var description = metadata.ConvertToString();
using var pngStream = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite);
var originalEndChunkBytes = new byte[12];
pngStream.Position = pngStream.Length - 12;
Expand All @@ -460,6 +460,7 @@ private static async Task EncodeAndSavePicture(string filePath, (IntPtr, int Len
UnityEngine.Debug.Log($"Took screenshot to: {filePath}");

EventHandler.InvokeScreenshotSaved(filePath, w, h, metadata);
LfsApi.InvokeScreenshotSaved(filePath, w, h, metadata);

// yield to background thread for disposes
await Task.Delay(1).ConfigureAwait(false);
Expand Down
9 changes: 9 additions & 0 deletions LagFreeScreenshots/Metadata.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using LagFreeScreenshots.API;
using UnityEngine;
using VRC;
using VRC.Core;

namespace LagFreeScreenshots
{
[Obsolete("Use LagFreeScreenshots.API.MetadataV2")]
public class Metadata
{
public int ImageRotation;
Expand All @@ -24,6 +26,13 @@ public Metadata(int imageRotation, APIUser apiUser, ApiWorldInstance apiWorldIns
PlayerList = playerList;
}

public Metadata(MetadataV2 newMetadata) : this((int) newMetadata.ImageRotation, newMetadata.ApiUser,
newMetadata.WorldInstance, newMetadata.Position,
newMetadata.PlayerList.ConvertAll(it => Tuple.Create(it.Item1, it.Item2)))
{

}

public string ConvertToString()
{
var worldString = "null,0,Not in any world";
Expand Down
1 change: 1 addition & 0 deletions ReleaseChangelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ Read the [Malicious Mods and you](https://github.com/knah/VRCMods/blob/master/Ma

Changes:
* Advanced Safety: added protection for uncommon skid crashes (contributed by Requi and Ben)
* Lag Free Screenshots: added screenshot taken event for other mods (contributed by @DragonPlayerX in #178)
* Mirror Resolution Unlimiter: added an option to show UI in mirrors when using Optimize/Beautify buttons, support new UI layer
* Styletor: added export for default menu audio clips (replacement is not supported yet)
* Styletor: added support for Action Menu recoloring (the round one)
Expand Down

0 comments on commit d78873a

Please sign in to comment.