2 Commits
Author SHA1 Message Date
Daemon 04711c4369 Merge pull request #25 from MoltenMods/2021
Added Reactor back
2021-04-14 12:30:46 +01:00
Daemon 913a02ddb6 Merge pull request #23 from MoltenMods/2021
Updated code for Reactor 0.4.0-ci.53
2021-04-13 19:09:07 +01:00
12 changed files with 117 additions and 392 deletions

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<component name="ContentModelUserStore">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
+4 -7
View File
@@ -1,9 +1,8 @@
# Unify
An Among Us mod to add extra regions to the region menu.
An Among Us mod to add extra regions to the regions menu.
![Region menu](images/RegionsMenu.png)
![Region menu editor](images/RegionsMenuEditor.png)
![Regions menu](images/RegionsMenu.png)
# Table of Contents
- [Features](#features)
@@ -16,7 +15,6 @@ The following regions have been added:
- [skeld.net](https://skeld.net/)
- localhost *(for playing on a server running on your computer)*
- matux.fr
- Przebot (may be unreliable or outdated)
Regions can be connected to directly using an IP address.
@@ -24,7 +22,7 @@ Custom regions can be added as detailed below in the
[configuration](#configuration) section.
Other mods can add regions via the
`AddRegion(string name, string ip, ushort port)` API method.
`AddRegion(string name, string ip)` method.
## Installation
@@ -38,8 +36,7 @@ Other mods can add regions via the
> If you are still using 2021.3.5s and Reactor fails to load, try using the Reactor version located
> here: https://github.com/NuclearPowered/Reactor/actions/runs/636023321.
1. Ensure that BepInEx and Reactor (Reactor is not required for pre releases) are installed
([installation guide](INSTALLATION.md)).
1. Ensure that BepInEx and Reactor are installed ([installation guide](INSTALLATION.md)).
2. Download the latest mod on the [releases](https://github.com/DaemonBeast/Unify/releases) page
for the version of Among Us you are using.
3. Move the mod to the `(Among Us game files)/BepInEx/plugins` folder.
-125
View File
@@ -1,125 +0,0 @@
using System;
using System.Threading.Tasks;
using Reactor.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;
using Object = UnityEngine.Object;
namespace Unify.Controls
{
public class Button
{
private static readonly TaskCompletionSource<bool> Initialized = new TaskCompletionSource<bool>();
private static GameObject _instance;
private readonly SpriteRenderer _spriteRenderer;
private readonly BoxCollider2D _boxCollider2D;
public GameObject GameObject { get; }
public TextMeshPro Text { get; }
public Vector2 Size
{
get => _spriteRenderer.size;
set
{
Text.rectTransform.sizeDelta = value;
_spriteRenderer.size = value;
_boxCollider2D.size = value;
}
}
public AspectPosition Position { get; }
public UnityEngine.UI.Button.ButtonClickedEvent OnClick { get; }
private Button(string text)
{
GameObject = Object.Instantiate(_instance).DontDestroy();
Text = GameObject.GetComponentInChildren<TextMeshPro>();
Position = GameObject.GetComponent<AspectPosition>();
OnClick = GameObject.GetComponent<PassiveButton>().OnClick;
_spriteRenderer = GameObject.GetComponent<SpriteRenderer>();
_boxCollider2D = GameObject.GetComponent<BoxCollider2D>();
var size = new Vector2(Text.preferredWidth, Text.preferredHeight);
Text.text = text;
Text.rectTransform.sizeDelta = size;
Position.DistanceFromEdge =
new Vector3(Text.preferredWidth / 2f + 0.1f, Text.preferredHeight / 2f + 0.1f, -100f);
_spriteRenderer.size = size;
_boxCollider2D.size = size;
}
public static async Task<Button> Create(string text)
{
await Initialized.Task;
return new Button(text);
}
public Button SetText(string newText)
{
Text.text = newText;
return this;
}
public Button SetSize(Vector2 newSize)
{
Size = newSize;
return this;
}
public Button SetDistanceFromEdge(Vector2 newPosition)
{
Position.DistanceFromEdge = new Vector3(newPosition.x, newPosition.y, -100f);
return this;
}
public Button SetAlignment(AspectPosition.EdgeAlignments newAlignment)
{
Position.Alignment = newAlignment;
return this;
}
public static void InitializeBaseButton()
{
BaseButton.Initialize();
}
private static class BaseButton
{
public static void Initialize()
{
SceneManager.add_sceneLoaded((Action<Scene, LoadSceneMode>) OnSceneLoaded);
}
private static void OnSceneLoaded(Scene scene, LoadSceneMode loadSceneMode)
{
Instantiate();
SceneManager.remove_sceneLoaded((Action<Scene, LoadSceneMode>) OnSceneLoaded);
}
private static void Instantiate()
{
if (Initialized.Task.IsCompleted) return;
var exitButton = GameObject.Find("ExitGameButton");
if (!exitButton) return;
_instance = Object.Instantiate(exitButton).DontDestroy();
_instance.SetActive(false);
_instance.name = "Button";
_instance.GetComponent<PassiveButton>().OnClick = new UnityEngine.UI.Button.ButtonClickedEvent();
_instance.GetComponent<SceneChanger>().Destroy();
_instance.GetComponent<ConditionalHide>().Destroy();
_instance.GetComponentInChildren<TextTranslatorTMP>().Destroy();
_instance.GetComponentInChildren<TextMeshPro>().text = "Button";
Initialized.SetResult(true);
}
}
}
}
+89 -225
View File
@@ -1,11 +1,9 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using BepInEx.Configuration;
using HarmonyLib;
using Reactor.Extensions;
using UnityEngine;
using Button = UnityEngine.UI.Button;
using UnityEngine.UI;
using Object = UnityEngine.Object;
using String = System.String;
@@ -13,44 +11,43 @@ namespace Unify.Patches
{
public static class RegionsPatch
{
internal static IRegionInfo[] OldRegions { get; } = ServerManager.DefaultRegions;
internal static IRegionInfo[] NewRegions { get; } = new IRegionInfo[]
private static IRegionInfo[] _oldRegions { get; } = ServerManager.DefaultRegions;
private static IRegionInfo[] _newRegions { get; } = new IRegionInfo[]
{
new DnsRegionInfo("192.241.154.115", "skeld.net", StringNames.NoTranslation, "192.241.154.115", 22023)
.Cast<IRegionInfo>(),
new DnsRegionInfo("localhost", "localhost", StringNames.NoTranslation, "127.0.0.1", 22023)
.Cast<IRegionInfo>(),
new DnsRegionInfo("152.228.160.91", "matux.fr", StringNames.NoTranslation, "152.228.160.91", 22023)
.Cast<IRegionInfo>(),
new DnsRegionInfo("77.55.217.159", "Przebot", StringNames.NoTranslation, "77.55.217.159", 22023)
.Cast<IRegionInfo>()
};
internal static List<IRegionInfo> ModRegions { get; } = new List<IRegionInfo>();
public static List<IRegionInfo> ModRegions { get; } = new List<IRegionInfo>();
public static IRegionInfo DirectRegion { get; set; }
internal static TextBoxTMP DirectConnect { get; set; }
private static TextBoxTMP DirectConnect { get; set; }
public static void Patch()
{
ServerManager serverManager = ServerManager.Instance;
IRegionInfo[] patchedRegions = OldRegions
.AddRangeToArray(NewRegions)
IRegionInfo[] patchedRegions = _oldRegions
.AddRangeToArray(_newRegions)
.AddRangeToArray(ModRegions.ToArray())
.AddRangeToArray(LoadCustomUserRegions());
if (DirectRegion != null) patchedRegions = patchedRegions.AddToArray(DirectRegion);
ServerManager.DefaultRegions = patchedRegions;
serverManager.AvailableRegions = patchedRegions;
serverManager.SaveServers();
}
private static IRegionInfo[] LoadCustomUserRegions()
{
List<IRegionInfo> customRegions = new List<IRegionInfo>();
for (int x = 0; x < 9; x++)
{
ConfigEntry<string> regionName = UnifyPlugin.ConfigFile.Bind(
@@ -63,20 +60,19 @@ namespace Unify.Patches
if (String.IsNullOrWhiteSpace(regionIp.Value)) continue;
IRegionInfo regionInfo = new DnsRegionInfo(
regionIp.Value, regionName.Value,
StringNames.NoTranslation, regionIp.Value, regionPort.Value)
regionIp.Value, regionName.Value, StringNames.NoTranslation, regionIp.Value, regionPort.Value)
.Cast<IRegionInfo>();
customRegions.Add(regionInfo);
}
return customRegions.ToArray();
}
private static void UpdateRegion()
{
RegionMenu regionMenu = DestroyableSingleton<RegionMenu>.Instance;
RegionMenu regionMenu = GameObject.Find("RegionMenu").GetComponent<RegionMenu>();
bool success = UnifyPlugin.SetDirectRegion(DirectConnect.text, out IRegionInfo newRegion);
if (!success)
@@ -84,22 +80,84 @@ namespace Unify.Patches
DirectConnect.StartCoroutine(Effects.SwayX(DirectConnect.transform, 0.75f, 0.25f));
return;
}
regionMenu.ChooseOption(newRegion);
regionMenu.Close();
}
[HarmonyPatch(typeof(ServerManager), nameof(ServerManager.LoadServers))]
public static class LoadNewRegionsPatch
public static class PatchRegionMenuPatch
{
public static void Postfix()
{
Patch();
}
}
[HarmonyPatch(typeof(RegionMenu), nameof(RegionMenu.Open))]
public static class DirectConnectButtonPatch
{
public static void Postfix()
{
JoinGameButton joinGameButton = DestroyableSingleton<JoinGameButton>.Instance;
RegionMenu regionMenu = DestroyableSingleton<RegionMenu>.Instance;
DirectConnect = Object.Instantiate(joinGameButton.GameIdText, regionMenu.transform);
DirectConnect.gameObject.SetActive(false);
DirectConnect.IpMode = true;
DirectConnect.characterLimit = 15;
DirectConnect.ClearOnFocus = false;
DirectConnect.OnEnter = new Button.ButtonClickedEvent();
DirectConnect.OnEnter.AddListener((Action) UpdateRegion);
int offset = _newRegions.Length + ModRegions.Count;
DirectConnect.transform.localPosition = new Vector3(0, 1f - (offset / 2f), -100f);
}
}
[HarmonyPatch(typeof(RegionMenu), nameof(RegionMenu.Open))]
public static class RegionMenuLayoutPatch
{
public static void Postfix(RegionMenu __instance)
{
DirectConnect.gameObject.SetActive(true);
var regionButtons = __instance.ButtonPool.activeChildren.ToArray();
for (int i = 0; i < regionButtons.Length; i++)
{
ServerListButton regionButton = regionButtons[i].Cast<ServerListButton>();
(float x, float y) = CalculatePosition(i);
regionButton.transform.localPosition = new Vector3(1.25f + x, 2f - 0.5f * y, 0f);
}
}
private static (float x, float y) CalculatePosition(int index)
{
// currently broken
/*if (DirectRegion != null && ServerManager.DefaultRegions[index].Name == DirectRegion.Name)
{
int offset = Math.Max(
Math.Max(LoadCustomUserRegions().Length, _newRegions.Length + ModRegions.Count),
_oldRegions.Length);
return (1.25f, 2f - offset);
}*/
bool isCustomRegion = index >= _oldRegions.Length;
bool isCustomUserRegion = index >= _oldRegions.Length + _newRegions.Length + ModRegions.Count;
float x = (isCustomRegion ? 3f : 0) + (isCustomUserRegion ? 3f : 0) - 4.25f;
float y = index - (isCustomRegion ? _oldRegions.Length : 0) -
(isCustomUserRegion ? _newRegions.Length + ModRegions.Count : 0);
return (x, y);
}
}
[HarmonyPatch(typeof(JoinGameButton), nameof(JoinGameButton.OnClick))]
public static class DirectConnectEnterButtonPatch {
public static class DirectConnectEnterButtonPatch
{
public static bool Prefix(JoinGameButton __instance)
{
GameObject regionMenuGameObject = GameObject.Find("RegionMenu");
@@ -111,215 +169,21 @@ namespace Unify.Patches
}
}
[HarmonyPatch(typeof(RegionMenu))]
public static class RegionMenuPatches
{
[HarmonyPatch(nameof(RegionMenu.Open))]
[HarmonyPostfix]
public static void DirectConnectButtonPatch()
{
RefreshDirectConnectButton();
}
[HarmonyPatch(nameof(RegionMenu.Open))]
[HarmonyPostfix]
public static void RegionMenuLayoutPatch()
{
RefreshRegionMenu();
}
internal static void RefreshRegionMenu()
{
if (!GameObject.Find("RegionMenu")) return;
var regionButtons = DestroyableSingleton<RegionMenu>.Instance.ButtonPool.activeChildren.ToArray();
var defaultRegions = ServerManager.DefaultRegions;
var showOfficials = UnifyPlugin.ShowOfficialRegions.Value;
var showExtras = UnifyPlugin.ShowExtraRegions.Value;
for (int i = 0; i < regionButtons.Length; i++)
{
ServerListButton regionButton = regionButtons[i].Cast<ServerListButton>();
IRegionInfo region = defaultRegions[i];
var textTranslatorTemp = regionButton.GetComponentInChildren<TextTranslatorTMP>();
if (textTranslatorTemp) textTranslatorTemp.Destroy();
int extraRegionCount = OldRegions.Length + NewRegions.Length + ModRegions.Count;
bool isCustomRegion = i >= OldRegions.Length;
bool isCustomUserRegion = i >= extraRegionCount;
float x = (!isCustomRegion && !showExtras ? 3f : 0) + (isCustomRegion ? 3f : 0) +
(isCustomUserRegion ? 3f : 0) - 4.25f - (!showOfficials || OldRegions.Length == 0 ? 1.5f : 0) -
(!showExtras || extraRegionCount == 0 ? 1.5f : 0);
float y = i - (isCustomRegion ? OldRegions.Length : 0) -
(isCustomUserRegion ? NewRegions.Length + ModRegions.Count : 0);
var regionButtonPosition = new Vector3(1.25f + x, 2f - 0.5f * y, 0);
regionButton.transform.localPosition = regionButtonPosition;
regionButton.gameObject.SetActive(
(showOfficials || i >= OldRegions.Length) &&
(showExtras || !isCustomRegion || isCustomUserRegion));
var buttonRolloverHandler = regionButton.GetComponent<ButtonRolloverHandler>();
if (RegionsEditor.IsActive)
{
buttonRolloverHandler.OverColor =
ServerManager.Instance.CurrentRegion.Name == region.Name ? Color.white : Color.black;
regionButton.Text.color = Color.gray;
}
else
{
buttonRolloverHandler.OverColor = Color.green;
regionButton.Text.color = Color.white;
}
if (isCustomUserRegion)
{
regionButton.Text.text =
RegionsEditor.IsActive ? $"{region.Name}\n{region.PingServer} {region.Servers[0].Port}" :
region.Name;
}
}
}
internal static void RefreshDirectConnectButton()
{
if (!GameObject.Find("RegionMenu")) return;
if (DirectConnect) DirectConnect.gameObject.Destroy();
RegionMenu regionMenu = DestroyableSingleton<RegionMenu>.Instance;
JoinGameButton joinGameButton = DestroyableSingleton<JoinGameButton>.Instance;
DirectConnect = Object.Instantiate(joinGameButton.GameIdText, regionMenu.transform);
DirectConnect.GetComponentInChildren<TextTranslatorTMP>().Destroy();
DirectConnect.GetComponent<JoinGameButton>().Destroy();
DirectConnect.outputText.text = "Enter IP";
DirectConnect.IpMode = true;
DirectConnect.characterLimit = 15;
DirectConnect.ClearOnFocus = false;
DirectConnect.OnEnter = new Button.ButtonClickedEvent();
DirectConnect.OnEnter.AddListener((Action) UpdateRegion);
int offset = NewRegions.Length + ModRegions.Count;
DirectConnect.transform.localPosition = new Vector3(0, 1f - (offset / 2f), -100f);
}
}
}
internal static class RegionsEditor
{
private static Controls.Button _switchButton;
private static Controls.Button _toggleOfficialsButton;
private static Controls.Button _toggleExtrasButton;
internal static bool IsActive;
internal static async void SetUp()
{
await SetUpSwitchButton();
await SetUpToggleOfficialsButton();
await SetUpToggleExtrasButton();
}
private static void SetRegionsEditorVisibility(bool visible)
{
IsActive = visible;
_toggleOfficialsButton.GameObject.SetActive(visible);
_toggleExtrasButton.GameObject.SetActive(visible);
RegionsPatch.RegionMenuPatches.RefreshRegionMenu();
if (visible)
{
_switchButton.Text.text = "Back";
RegionsPatch.DirectConnect.gameObject.SetActive(false);
}
else
{
_switchButton.Text.text = "Edit";
RegionsPatch.RegionMenuPatches.RefreshDirectConnectButton();
}
}
private static async Task SetUpSwitchButton()
{
_switchButton = await Controls.Button.Create("Edit");
_switchButton.SetSize(new Vector2(0.8f, 0.4f)).SetDistanceFromEdge(new Vector2(0.5f, 0.3f));
_switchButton.OnClick.AddListener((Action) (() =>
{
SetRegionsEditorVisibility(!IsActive);
}));
}
private static async Task SetUpToggleOfficialsButton()
{
_toggleOfficialsButton = await Controls.Button.Create("Toggle Official Regions");
_toggleOfficialsButton.SetSize(new Vector2(2f, 0.4f)).SetDistanceFromEdge(new Vector2(1.1f, 1.3f));
_toggleOfficialsButton.OnClick.AddListener((Action) (() =>
{
UnifyPlugin.ShowOfficialRegions.Value = !UnifyPlugin.ShowOfficialRegions.Value;
RegionsPatch.RegionMenuPatches.RefreshRegionMenu();
}));
}
private static async Task SetUpToggleExtrasButton()
{
_toggleExtrasButton = await Controls.Button.Create("Toggle Extra Regions");
_toggleExtrasButton.SetSize(new Vector2(2f, 0.4f)).SetDistanceFromEdge(new Vector2(1.1f, 1.8f));
_toggleExtrasButton.OnClick.AddListener((Action) (() =>
{
UnifyPlugin.ShowExtraRegions.Value = !UnifyPlugin.ShowExtraRegions.Value;
RegionsPatch.RegionMenuPatches.RefreshRegionMenu();
}));
}
[HarmonyPatch(typeof(RegionMenu), nameof(RegionMenu.Open))]
public static class OpenRegionsEditorPatch
[HarmonyPatch(typeof(RegionMenu), nameof(RegionMenu.Close))]
public static class HideDirectConnectPatch
{
public static void Postfix()
{
if (!GameObject.Find("RegionMenu")) return;
_switchButton.GameObject.SetActive(true);
DirectConnect.gameObject.SetActive(false);
}
}
[HarmonyPatch(typeof(RegionMenu))]
public static class HideRegionsEditorPatch
{
[HarmonyPatch(nameof(RegionMenu.Close))]
[HarmonyPostfix]
public static void HideOnClosePatch()
{
Hide();
}
[HarmonyPatch(nameof(RegionMenu.ChooseOption))]
[HarmonyPrefix]
public static bool DisableRegionButtonsPatch()
{
if (IsActive) return false;
Hide();
return true;
}
private static void Hide()
[HarmonyPatch(typeof(RegionMenu), nameof(RegionMenu.ChooseOption))]
public static class HideDirectConnectOnSelectPatch
{
public static void Postfix()
{
if (!RegionsPatch.DirectConnect) return;
SetRegionsEditorVisibility(false);
_switchButton.GameObject.SetActive(false);
RegionsPatch.DirectConnect.gameObject.SetActive(false);
DirectConnect.gameObject.SetActive(false);
}
}
}
+19 -30
View File
@@ -8,7 +8,6 @@ using BepInEx.IL2CPP;
using HarmonyLib;
using Hazel.Udp;
using Reactor;
using Unify.Controls;
using Unify.Patches;
namespace Unify
@@ -21,31 +20,35 @@ namespace Unify
{
public const string Id = "daemon.unify";
private const string Name = "Unify";
private const string Version = "5.0.0";
private const string Version = "4.0.0";
public static ConfigFile ConfigFile { get; private set; }
internal static ConfigEntry<bool> ShowOfficialRegions;
internal static ConfigEntry<bool> ShowExtraRegions;
public Harmony Harmony { get; } = new Harmony(Id);
public override void Load()
{
FixCompatibility();
ShowOfficialRegions = ConfigFile.Bind("Preferences", "Show Official Regions", true,
"If the official regions should be shown when displaying the regions menu");
ShowExtraRegions = ConfigFile.Bind("Preferences", "Show Extra Regions", true,
"If the extra regions added by default in Unify should be shown when displaying the regions menu");
// ===== For compatibility reasons =====
string oldConfigPath = Path.Combine(Paths.ConfigPath, "daemon.unify.reactor.cfg");
string newConfigPath = Path.Combine(Paths.ConfigPath, $"{UnifyPlugin.Id}.cfg");
bool oldConfigExists = File.Exists(oldConfigPath);
if (oldConfigExists)
{
File.Copy(oldConfigPath, newConfigPath, true);
File.Delete(oldConfigPath);
}
ConfigFile = new ConfigFile(Path.Combine(Paths.ConfigPath, $"{UnifyPlugin.Id}.cfg"), true);
// =====================================
// Unpatches the modded handshake, because Impostor is STILL not fully updated yet
Harmony.Unpatch(typeof(UdpConnection).GetMethod("HandleSend"), HarmonyPatchType.Prefix,
ReactorPlugin.Id);
Harmony.PatchAll();
Button.InitializeBaseButton();
RegionsEditor.SetUp();
}
public static IRegionInfo AddRegion(string name, string ip, ushort port)
public static IRegionInfo AddRegion(string name, string ip)
{
if (Uri.CheckHostName(ip) != UriHostNameType.IPv4) return ServerManager.Instance.CurrentRegion;
@@ -53,7 +56,7 @@ namespace Unify
ServerManager.DefaultRegions.ToArray().FirstOrDefault(region => region.PingServer == ip);
if (existingRegion != null) return existingRegion;
IRegionInfo newRegion = new DnsRegionInfo(ip, name, StringNames.NoTranslation, ip, port)
IRegionInfo newRegion = new DnsRegionInfo(ip, name, StringNames.NoTranslation, ip, 22023)
.Cast<IRegionInfo>();
RegionsPatch.ModRegions.Add(newRegion);
@@ -76,19 +79,5 @@ namespace Unify
return true;
}
private static void FixCompatibility()
{
string oldConfigPath = Path.Combine(Paths.ConfigPath, "daemon.unify.reactor.cfg");
string newConfigPath = Path.Combine(Paths.ConfigPath, $"{UnifyPlugin.Id}.cfg");
bool oldConfigExists = File.Exists(oldConfigPath);
if (oldConfigExists)
{
File.Copy(oldConfigPath, newConfigPath, true);
File.Delete(oldConfigPath);
}
ConfigFile = new ConfigFile(Path.Combine(Paths.ConfigPath, $"{UnifyPlugin.Id}.cfg"), true);
}
}
}
+4 -4
View File
@@ -1,20 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<Version>5.0.1</Version>
<Mappings>DaemonBeast/Mappings:0.6.0</Mappings>
<Version>4.0.0</Version>
<Mappings>NuclearPowered/Mappings:0.4.0</Mappings>
<Description>Region mod</Description>
<Authors>Daemon</Authors>
</PropertyGroup>
<PropertyGroup Condition="'$(GamePlatform)' == 'Steam'">
<GameVersion>2021.5.25s</GameVersion>
<GameVersion>2021.4.12s</GameVersion>
<DefineConstants>$(DefineConstants);STEAM</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(GamePlatform)' == 'Itch'">
<GameVersion>2021.5.25i</GameVersion>
<GameVersion>2021.4.12i</GameVersion>
<DefineConstants>$(DefineConstants);ITCH</DefineConstants>
</PropertyGroup>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB