summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSarah Bradley <git@sarahduck.ca>2023-12-01 20:33:42 -0800
committerSarah Bradley <git@sarahduck.ca>2023-12-01 20:33:42 -0800
commit2793b94040a473538f01723d5ca5f53c4535e2af (patch)
treecb30f0dae20bda6ef9d1c005325bfd9c986b3c8f
What I've got so far
-rw-r--r--.gitignore4
-rw-r--r--Bitmap.cs113
-rw-r--r--ConfigFile.cs84
-rw-r--r--Drawable.cs38
-rw-r--r--EmbeddedResources.cs16
-rw-r--r--Main.cs57
-rw-r--r--MathExtensions.cs33
-rw-r--r--Net.cs95
-rw-r--r--NetClient.cs78
-rw-r--r--NetServer.cs110
-rw-r--r--Oneko.cs178
-rw-r--r--OnekoOnline.csproj25
-rw-r--r--dog.pngbin0 -> 3377 bytes
-rw-r--r--oneko.pngbin0 -> 3055 bytes
-rw-r--r--petduck.pngbin0 -> 4501 bytes
-rw-r--r--tora.pngbin0 -> 3474 bytes
16 files changed, 831 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c614c3a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+.vscode/
+bin/
+obj/
+config.conf \ No newline at end of file
diff --git a/Bitmap.cs b/Bitmap.cs
new file mode 100644
index 0000000..e508d3d
--- /dev/null
+++ b/Bitmap.cs
@@ -0,0 +1,113 @@
+using System.Buffers.Binary;
+using System.IO.Compression;
+using Raylib_cs;
+
+namespace OnekoOnline;
+
+class Bitmap : IDisposable
+{
+ public readonly int Width;
+ public readonly int Height;
+ public readonly Texture2D Texture;
+
+ readonly byte[] SerializedData;
+
+ public Bitmap(Image img)
+ {
+ Width = img.Width;
+ Height = img.Height;
+ Texture = Raylib.LoadTextureFromImage(img);
+
+ //Get Data for Serialization
+ Color[] colors = new Color[Width*Height];
+ int i = 0;
+ for (int x = 0; x < Width; x++) {
+ for (int y = 0; y < Height; y++) {
+ colors[i] = Raylib.GetImageColor(img, x, y);
+ i++;
+ }
+ }
+
+ byte[] data = new byte[(colors.Length*4) + (sizeof(short)*2)];
+ BinaryPrimitives.WriteInt16LittleEndian(data.AsSpan(0, sizeof(short)), (short)Width);
+ BinaryPrimitives.WriteInt16LittleEndian(data.AsSpan(sizeof(short), sizeof(short)), (short)Height);
+ int position = sizeof(short)*2;
+ for (int c = 0; c < colors.Length; c++) {
+ data[position] = colors[c].R;
+ data[position+1] = colors[c].G;
+ data[position+2] = colors[c].B;
+ data[position+3] = colors[c].A;
+ position += 4;
+ }
+
+ using MemoryStream stream = new();
+ using DeflateStream compressor = new(stream, CompressionLevel.Optimal);
+ compressor.Write(data);
+ compressor.Close();
+ SerializedData = stream.ToArray();
+
+ Raylib.UnloadImage(img);
+ }
+
+ Bitmap(Image img, byte[] serializedData)
+ {
+ Width = img.Width;
+ Height = img.Height;
+ SerializedData = serializedData;
+
+ Texture = Raylib.LoadTextureFromImage(img);
+ Raylib.UnloadImage(img);
+ }
+
+ public static Bitmap FromFile(string path)
+ {
+ if (!File.Exists(path) || new FileInfo(path).Length > 40000 || !path.Contains(".png"))
+ return new Bitmap(Raylib.GenImageChecked(32, 32, 4, 4, Color.BLACK, Color.PINK));
+
+ byte[] memory = File.ReadAllBytes(path);
+ return FromPNGMemory(memory);
+ }
+
+ public static Bitmap FromPNGMemory(byte[] memory)
+ {
+ return new Bitmap(Raylib.LoadImageFromMemory(".png", memory));
+ }
+
+ public static Bitmap Deserialize(ReadOnlySpan<byte> span)
+ {
+ byte[] compressed = span.ToArray();
+ byte[] data;
+ {
+ using MemoryStream input = new(compressed);
+ using MemoryStream output = new();
+ using DeflateStream decompressor = new(input, CompressionMode.Decompress);
+ decompressor.CopyTo(output);
+ data = output.ToArray();
+ }
+
+ int width = BinaryPrimitives.ReadInt16LittleEndian(data.AsSpan(0, sizeof(short)));
+ int height = BinaryPrimitives.ReadInt16LittleEndian(data.AsSpan(sizeof(short), sizeof(short)));
+ Image img = Raylib.GenImageChecked(width, height, 4, 4, Color.PINK, Color.BLACK);
+
+ int i = sizeof(short)*2;
+ for (int x = 0; x < width; x++) {
+ for (int y = 0; y < height; y++) {
+ Color color = new(data[i], data[i+1], data[i+2], data[i+3]);
+ Raylib.ImageDrawPixel(ref img, x, y, color);
+ i += 4;
+ }
+ }
+
+ return new Bitmap(img, compressed);
+ }
+
+ public byte[] Serialize()
+ {
+ return SerializedData;
+ }
+
+ public void Dispose()
+ {
+ Raylib.UnloadTexture(Texture);
+ }
+} \ No newline at end of file
diff --git a/ConfigFile.cs b/ConfigFile.cs
new file mode 100644
index 0000000..e0f1293
--- /dev/null
+++ b/ConfigFile.cs
@@ -0,0 +1,84 @@
+namespace OnekoOnline;
+
+class ConfigFile
+{
+ public readonly string Path;
+ readonly Dictionary<string, string> ConfigOptions = [];
+ bool OptionChanged = false;
+ const string TopLine = "# OnekoOnline Config File";
+
+ public ConfigFile(string path)
+ {
+ Path = path;
+
+ if (!File.Exists(Path)) {
+ Console.WriteLine("Config file does not exist, creating one.");
+ File.WriteAllText(Path, TopLine);
+ OptionChanged = true;
+ return;
+ }
+
+ string[] fileContents = File.ReadAllLines(Path);
+
+ foreach (string line in fileContents) {
+ if (line[0] == '#' || !line.Contains('=') || line.Length < 3) continue;
+
+ string key = line[0..line.IndexOf('=')];
+ string value = line[(line.IndexOf('=')+1)..];
+ ConfigOptions.Add(key, value);
+ }
+ }
+
+ public void SaveFile()
+ {
+ if (!OptionChanged) return;
+
+ List<string> linesToWrite = [TopLine];
+ foreach (KeyValuePair<string, string> pair in ConfigOptions.OrderBy(k => k.Key)) linesToWrite.Add(pair.Key + "=" + pair.Value);
+ File.WriteAllLines(Path, linesToWrite);
+
+ OptionChanged = false;
+ }
+
+ public T GetValue<T>(string Key, T defaultValue) where T : IConvertible
+ {
+ if (ConfigOptions.TryGetValue(Key, out string? value)) {
+ try {
+ return (T)Convert.ChangeType(value, typeof(T));
+ } catch {
+ Console.WriteLine($"Config option {Key} is invalid, setting to default.");
+ }
+ }
+
+ SetValue(Key, defaultValue);
+ return defaultValue;
+ }
+
+ public string GetValue(string Key, string defaultValue)
+ {
+ if (ConfigOptions.TryGetValue(Key, out string? value)) {
+ return value;
+ }
+
+ SetValue(Key, defaultValue);
+ return defaultValue;
+ }
+
+ public void SetValue<T>(string Key, T Value) where T : IConvertible
+ {
+ string? ValueString = Value.ToString();
+ if (ValueString is null) return;
+
+ SetValue(Key, ValueString);
+ }
+
+ public void SetValue(string Key, string Value)
+ {
+ if (ConfigOptions.TryAdd(Key, Value)) {
+ OptionChanged = true;
+ } else if (ConfigOptions[Key] != Value) {
+ ConfigOptions[Key] = Value;
+ OptionChanged = true;
+ }
+ }
+} \ No newline at end of file
diff --git a/Drawable.cs b/Drawable.cs
new file mode 100644
index 0000000..f6e5e1b
--- /dev/null
+++ b/Drawable.cs
@@ -0,0 +1,38 @@
+using System.Numerics;
+using Raylib_cs;
+
+namespace OnekoOnline;
+
+abstract class Drawable : IDisposable
+{
+ public Vector2 Position {
+ get => _position;
+ set => _position = value.Round();
+ }
+ private Vector2 _position;
+
+ public Vector2 Size;
+ public float Rotation = 0f;
+
+ public static readonly List<Drawable> Drawables = [];
+
+ public static void DrawAll()
+ {
+ float delta = Raylib.GetFrameTime();
+ foreach (Drawable drawable in Drawables.OrderBy(d => -d.Position.Y)) {
+ drawable?.Update(delta);
+ drawable?.Draw();
+ }
+ }
+
+ public static void DisposeAll()
+ {
+ foreach (Drawable drawable in Drawables.ToArray()) drawable?.Dispose();
+ }
+
+ public abstract void Draw();
+
+ public abstract void Update(float delta);
+
+ public abstract void Dispose();
+} \ No newline at end of file
diff --git a/EmbeddedResources.cs b/EmbeddedResources.cs
new file mode 100644
index 0000000..7daa6c0
--- /dev/null
+++ b/EmbeddedResources.cs
@@ -0,0 +1,16 @@
+using System.Reflection;
+
+namespace OnekoOnline;
+
+static class EmbeddedResources
+{
+ static readonly Assembly assembly = Assembly.GetExecutingAssembly();
+ static readonly string assemblyName = assembly.GetName().Name ?? "";
+
+ public static byte[] GetResource(string name)
+ {
+ using Stream EmbeddedFile = Assembly.GetExecutingAssembly().GetManifestResourceStream($"{assemblyName}.{name}")!;
+ using BinaryReader reader = new(EmbeddedFile);
+ return reader.ReadBytes((int)EmbeddedFile.Length);
+ }
+} \ No newline at end of file
diff --git a/Main.cs b/Main.cs
new file mode 100644
index 0000000..8b3e9be
--- /dev/null
+++ b/Main.cs
@@ -0,0 +1,57 @@
+using System.Numerics;
+using Raylib_cs;
+
+namespace OnekoOnline;
+
+static class OnekoOnline
+{
+ public static readonly ConfigFile Config = new("config.conf");
+
+ const ConfigFlags raylibConfFlags =
+ //ConfigFlags.FLAG_WINDOW_UNDECORATED |
+ //ConfigFlags.FLAG_WINDOW_TRANSPARENT |
+ //ConfigFlags.FLAG_WINDOW_MOUSE_PASSTHROUGH |
+ //ConfigFlags.FLAG_WINDOW_TOPMOST |
+ //ConfigFlags.FLAG_WINDOW_RESIZABLE |
+ ConfigFlags.FLAG_VSYNC_HINT;
+
+ public static void Main()
+ {
+ Raylib.SetConfigFlags(raylibConfFlags);
+ Raylib.InitWindow(640, 480, "OnekoOnline");
+ Raylib.SetTargetFPS(30);
+ //Raylib.MaximizeWindow();
+
+ Oneko LocalOneko = new();
+
+ RenderTexture2D RenderTexture = Raylib.LoadRenderTexture(320,240);
+
+ int port = Config.GetValue("ServerPort", 42069);
+ if (Config.GetValue("HostServer", false)) {
+ Net.Server.Init(port);
+ Net.Client.Init("127.0.0.1", port);
+ } else {
+ Net.Client.Init(Config.GetValue("ServerIP", "pond.sarahduck.ca"), port);
+ }
+
+ while (!Raylib.WindowShouldClose())
+ {
+ Raylib.BeginTextureMode(RenderTexture);
+
+ Raylib.ClearBackground(Color.GRAY);
+ Raylib.DrawText("Oneko Online",12, 12, 8, Color.WHITE);
+
+ Drawable.DrawAll();
+
+ Raylib.EndTextureMode();
+
+ Raylib.BeginDrawing();
+ Raylib.DrawTexturePro(RenderTexture.Texture, new Rectangle(0f,0f,320,-240), new Rectangle(0,0,640,480), Vector2.Zero,0f,Color.WHITE);
+ Raylib.EndDrawing();
+ }
+
+ Drawable.DisposeAll();
+ Raylib.CloseWindow();
+ Config.SaveFile();
+ }
+} \ No newline at end of file
diff --git a/MathExtensions.cs b/MathExtensions.cs
new file mode 100644
index 0000000..81db592
--- /dev/null
+++ b/MathExtensions.cs
@@ -0,0 +1,33 @@
+using System.Numerics;
+
+namespace OnekoOnline;
+
+static class MathExtensions
+{
+ public static Vector2 LimitLength(this Vector2 toLimit, float lengthLimit)
+ {
+ float length = toLimit.Length();
+ if (toLimit == Vector2.Zero) return Vector2.Zero;
+ return Vector2.Normalize(toLimit) * MathF.Min(length, lengthLimit);
+ }
+
+ public static Vector2 Round(this Vector2 toRound)
+ {
+ return new(MathF.Round(toRound.X), MathF.Round(toRound.Y));
+ }
+}
+
+public static class Directions
+{
+ public static readonly Vector2 Up = new(0,-1);
+ public static readonly Vector2 Down = new(0,1);
+ public static readonly Vector2 Left = new(-1,0);
+ public static readonly Vector2 Right = new(1,0);
+
+ public static readonly Vector2 UpLeft = Vector2.Normalize(Up+Left);
+ public static readonly Vector2 UpRight = Vector2.Normalize(Up+Right);
+ public static readonly Vector2 DownLeft = Vector2.Normalize(Down+Left);
+ public static readonly Vector2 DownRight = Vector2.Normalize(Down+Right);
+
+ public static readonly Vector2[] AllDirections = [Up,Down,Left,Right,UpLeft,UpRight,DownLeft,DownRight];
+} \ No newline at end of file
diff --git a/Net.cs b/Net.cs
new file mode 100644
index 0000000..497c66d
--- /dev/null
+++ b/Net.cs
@@ -0,0 +1,95 @@
+using System.Buffers.Binary;
+using System.Net.Sockets;
+
+namespace OnekoOnline.Net;
+
+static class NetBase
+{
+ public static async Task<Packet> GetReliableData(NetworkStream stream, CancellationToken token = default)
+ {
+ byte[] infoBytes = new byte[PacketInfo.SizeOf];
+ await stream.ReadExactlyAsync(infoBytes, token);
+ PacketInfo info = PacketInfo.Deserialize(infoBytes);
+ if (info.Type == PacketType.Ping || info.Type == PacketType.Disconnect) return new(info.Type, [], info.FromId);
+
+ byte[] data = new byte[info.DataSize];
+ await stream.ReadExactlyAsync(data, token);
+
+ return new Packet(info.Type, data, info.FromId);
+ }
+
+ public static async Task SendReliableData(Packet packet, NetworkStream stream, CancellationToken token = default)
+ {
+ byte[] info = new byte[PacketInfo.SizeOf];
+ new PacketInfo(packet).Serialize(info);
+
+ await stream.WriteAsync(info, token);
+ await stream.WriteAsync(packet.Data, token);
+ }
+}
+
+public readonly struct Packet(PacketType type, byte[] data, int id)
+{
+ public readonly PacketType Type = type;
+ public readonly byte[] Data = data;
+ public readonly int FromId = id;
+}
+
+public enum PacketType : byte
+{
+ MousePosition,
+ OnekoState,
+ Ping,
+ OnekoSpritesheet,
+ Username,
+ UserId,
+ Disconnect
+}
+
+public struct PacketInfo
+{
+ public readonly int DataSize;
+ public readonly PacketType Type;
+ public readonly int FromId;
+ public const int SizeOf = (sizeof(int)*2) + 1;
+
+ public PacketInfo(Packet packet)
+ {
+ DataSize = packet.Data.Length;
+ Type = packet.Type;
+ FromId = packet.FromId;
+ }
+
+ public PacketInfo(PacketType type, int size, int id)
+ {
+ Type = type;
+ DataSize = size;
+ FromId = id;
+ }
+
+ public readonly void Serialize(Span<byte> span)
+ {
+ span[0] = (byte)Type;
+ BinaryPrimitives.WriteInt32LittleEndian(span[1..], DataSize);
+ BinaryPrimitives.WriteInt32LittleEndian(span[(1+sizeof(int))..], FromId);
+ }
+
+ public static PacketInfo Deserialize(ReadOnlySpan<byte> span)
+ {
+ PacketType type = (PacketType)span[0];
+ int size = BinaryPrimitives.ReadInt32LittleEndian(span[1..]);
+ int id = BinaryPrimitives.ReadInt32LittleEndian(span[(1+sizeof(int))..]);
+ return new PacketInfo(type, size, id);
+ }
+}
+
+class User(int id)
+{
+ public readonly int Id = id;
+
+ //Oneko Stuff
+ public byte[]? SpriteSheet;
+ public string? Username;
+
+ public bool ExchangedData => SpriteSheet != null && Username != null;
+} \ No newline at end of file
diff --git a/NetClient.cs b/NetClient.cs
new file mode 100644
index 0000000..31ea258
--- /dev/null
+++ b/NetClient.cs
@@ -0,0 +1,78 @@
+using System.Buffers.Binary;
+using System.Net.Sockets;
+using System.Text;
+
+namespace OnekoOnline.Net;
+
+static class Client
+{
+ static readonly TcpClient Tcp = new();
+ static readonly UdpClient Udp = new();
+
+ public static readonly string UserName = OnekoOnline.Config.GetValue("UserName", "Oneko");
+ public static int Id {get; private set;} = 0;
+ public static bool Connected => Tcp.Connected && Id != 0;
+
+ public static Dictionary<int, User> Users = [];
+ static readonly Dictionary<int, User> allUsers = [];
+
+ public static async void Init(string ServerAddress, int port)
+ {
+ if (ServerAddress == "") return;
+
+ await Task.WhenAny(Tcp.ConnectAsync(ServerAddress, port), Task.Delay(3000));
+
+ if (Tcp.Connected)
+ {
+ Console.WriteLine("Connected to Server!");
+ Udp.Connect(ServerAddress, port);
+
+ CancellationTokenSource tokenSource = new();
+ tokenSource.CancelAfter(5000);
+
+ //Send Username
+ Packet packet = new(PacketType.Username, Encoding.UTF8.GetBytes(UserName), Id);
+ await NetBase.SendReliableData(packet, Tcp.GetStream(), tokenSource.Token);
+ //Send Oneko
+ packet = new(PacketType.OnekoSpritesheet, Oneko.LocalOneko!.SpriteSheet.Serialize(), Id);
+ await NetBase.SendReliableData(packet, Tcp.GetStream(), tokenSource.Token);
+ //Get Id
+ Packet idPacket = await NetBase.GetReliableData(Tcp.GetStream());
+ Id = BinaryPrimitives.ReadInt32LittleEndian(idPacket.Data);
+ Console.WriteLine($"My ID is {Id}! {Connected}");
+
+ while (Connected)
+ {
+ if (Tcp.Available > 0) {
+ packet = await NetBase.GetReliableData(Tcp.GetStream());
+ if (packet.FromId == 0) continue; //From server, ill implement later
+
+ if (!allUsers.ContainsKey(packet.FromId)) {
+ allUsers.Add(packet.FromId, new User(packet.FromId));
+ }
+ User sentFrom = allUsers[packet.FromId];
+
+ if (packet.Type == PacketType.OnekoSpritesheet) {
+ sentFrom.SpriteSheet = packet.Data;
+ } else if (packet.Type == PacketType.Username) {
+ sentFrom.Username = Encoding.UTF8.GetString(packet.Data);
+ }
+
+ if (sentFrom.ExchangedData && !Users.ContainsKey(sentFrom.Id)) Users.Add(sentFrom.Id, sentFrom);
+ }
+
+ if (Udp.Available > 0) {
+ }
+
+ await Task.Delay(10);
+ }
+ }
+ else
+ {
+ Console.WriteLine("Connection to server failed.");
+ }
+
+ Tcp.Dispose();
+ Udp.Dispose();
+ }
+} \ No newline at end of file
diff --git a/NetServer.cs b/NetServer.cs
new file mode 100644
index 0000000..cab6a00
--- /dev/null
+++ b/NetServer.cs
@@ -0,0 +1,110 @@
+using System.Buffers.Binary;
+using System.Collections.Concurrent;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading.Channels;
+
+namespace OnekoOnline.Net;
+
+static class Server
+{
+ public static bool ServerRunning = false;
+
+ static readonly ConcurrentDictionary<int, ServerUser> users = [];
+
+ public static async void Init(int port)
+ {
+ TcpListener listener = new(IPAddress.Any, port);
+ listener.Start(10);
+ ServerRunning = true;
+
+ while (ServerRunning)
+ {
+ TcpClient tcpSocket = await listener.AcceptTcpClientAsync();
+ UdpClient udpSocket = new((IPEndPoint)tcpSocket.Client.RemoteEndPoint!);
+
+ ServerUser newUser = new(tcpSocket.GetHashCode(), tcpSocket, udpSocket);
+
+ Console.WriteLine("Accepted a connection from: " + tcpSocket.Client.RemoteEndPoint);
+
+ await Task.Factory.StartNew(() => SetupUser(newUser));
+ }
+ }
+
+ static async Task SetupUser(ServerUser user)
+ {
+ await Task.WhenAny(ExchangeUserData(user), Task.Delay(7000));
+
+ if (user.ExchangedData) {
+ //Send current user data
+ foreach (ServerUser otherUser in users.Values.Where(u => u.ExchangedData)) {
+ //Send username and spritesheet
+ await NetBase.SendReliableData(new Packet(PacketType.OnekoSpritesheet, otherUser.SpriteSheet!, otherUser.Id), user.Tcp.GetStream());
+ await NetBase.SendReliableData(new Packet(PacketType.Username, Encoding.UTF8.GetBytes(otherUser.Username!), otherUser.Id), user.Tcp.GetStream());
+ //Ask users to take your data
+ await otherUser.ToSend.Writer.WriteAsync(new Packet(PacketType.Username, Encoding.UTF8.GetBytes(user.Username!), user.Id));
+ await otherUser.ToSend.Writer.WriteAsync(new Packet(PacketType.OnekoSpritesheet, user.SpriteSheet!, user.Id));
+ Console.WriteLine($"Sent {otherUser.Username}'s Data to {user.Username}");
+ }
+
+ users.GetOrAdd(user.Id, user);
+ await Task.Factory.StartNew(() => UpdateUser(user));
+ } else {
+ Console.WriteLine($"{user.Tcp.Client.RemoteEndPoint} failed to send required data, terminating connection.");
+ user.Dispose();
+ }
+ }
+
+ static async Task ExchangeUserData(ServerUser user)
+ {
+ while (!user.ExchangedData)
+ {
+ Packet packet = await NetBase.GetReliableData(user.Tcp.GetStream());
+
+ if (packet.Data.Length > 50000) break;
+
+ if (packet.Type == PacketType.Username) {
+ user.Username = Encoding.UTF8.GetString(packet.Data);
+ } else if (packet.Type == PacketType.OnekoSpritesheet) {
+ user.SpriteSheet = packet.Data;
+ }
+ }
+
+ if (!user.ExchangedData) return;
+
+ //Send ID
+ byte[] idData = new byte[sizeof(int)];
+ BinaryPrimitives.WriteInt32LittleEndian(idData, user.Id);
+ await NetBase.SendReliableData(new Packet(PacketType.UserId, idData, 0), user.Tcp.GetStream());
+
+ Console.WriteLine($"{user.Tcp.Client.RemoteEndPoint} is {user.Username}!");
+ }
+
+ static async Task UpdateUser(ServerUser user)
+ {
+ while (user.Tcp.Connected) {
+ Packet toSend = await user.ToSend.Reader.ReadAsync(user.UpdateCancel.Token);
+ await NetBase.SendReliableData(toSend, user.Tcp.GetStream(), user.UpdateCancel.Token);
+ }
+ //users.TryRemove(user.Id, out _);
+ //user.Dispose();
+ }
+}
+
+class ServerUser(int id, TcpClient tcp, UdpClient udp) : User(id), IDisposable
+{
+ //Network
+ public readonly TcpClient Tcp = tcp;
+ public readonly UdpClient Udp = udp;
+ public readonly Channel<Packet> ToSend = Channel.CreateUnbounded<Packet>();
+ public CancellationTokenSource UpdateCancel = new();
+
+ public void Dispose()
+ {
+ UpdateCancel.Cancel();
+ Tcp.Dispose();
+ Udp.Dispose();
+ }
+} \ No newline at end of file
diff --git a/Oneko.cs b/Oneko.cs
new file mode 100644
index 0000000..274782c
--- /dev/null
+++ b/Oneko.cs
@@ -0,0 +1,178 @@
+using System.Numerics;
+using OnekoOnline.Net;
+using Raylib_cs;
+
+namespace OnekoOnline;
+
+class Oneko : Drawable
+{
+ public readonly Bitmap SpriteSheet;
+ Vector2 TargetPosition;
+ OnekoAnimation Animation = ScratchSelf;
+
+ float updateTimer = 0f;
+ const float updateRate = 1f/5f;
+ int Frame = 0;
+
+ public static Oneko? LocalOneko;
+
+ public static Dictionary<int, Oneko> NetNekos = [];
+
+ public Oneko()
+ {
+ Size = new(32,32);
+ Position = new(320/2, 240/2);
+
+ string SpriteSheetPath = OnekoOnline.Config.GetValue("SpriteSheetPath", "oneko.png");
+
+ if (File.Exists(SpriteSheetPath) && new FileInfo(SpriteSheetPath).Length < 128*256*3) {
+ SpriteSheet = Bitmap.FromPNGMemory(File.ReadAllBytes(SpriteSheetPath));
+ } else {
+ Console.WriteLine("Path to spritesheet was invalid, using the default.");
+ SpriteSheet = Bitmap.FromPNGMemory(EmbeddedResources.GetResource("oneko.png"));
+ }
+
+ LocalOneko ??= this;
+
+ Drawables.Add(this);
+ }
+
+ public Oneko(Bitmap spriteSheet)
+ {
+ Size = new(32,32);
+ Position = new(0, 0);
+ SpriteSheet = spriteSheet;
+
+ Drawables.Add(this);
+ }
+
+ public override void Draw()
+ {
+ Raylib.DrawTexturePro(SpriteSheet.Texture, Animation.GetFrame(Frame), new Rectangle(Position.X, Position.Y, Size.X, Size.Y), Size/2, Rotation, Color.WHITE);
+ }
+
+ public override void Update(float delta)
+ {
+ if (this == LocalOneko) {
+ foreach (User user in Client.Users.Values) {
+ if (!NetNekos.ContainsKey(user.Id)) {
+ Bitmap spriteSheet = Bitmap.Deserialize(user.SpriteSheet);
+ NetNekos.Add(user.Id, new Oneko(spriteSheet));
+ }
+ }
+ }
+
+ updateTimer += delta;
+ if (updateTimer < updateRate) return;
+
+ if (Raylib.IsWindowFocused()) {
+ TargetPosition = Raylib.GetMousePosition()/2;
+ } else {
+ TargetPosition = new Vector2(320/2, 240/2);
+ }
+ Vector2 TargetDirection = (TargetPosition-Position).LimitLength(10f);
+ if (TargetDirection.Length() > 1) Animation = GetDirectionalRun(TargetDirection);
+ else Animation = Idle;
+ Position += TargetDirection;
+
+ Frame = (Frame + 1) % 2;
+
+ updateTimer = 0f;
+ }
+
+ public override void Dispose()
+ {
+ SpriteSheet.Dispose();
+ Drawables.Remove(this);
+ }
+
+ struct OnekoAnimation(Rectangle frame1, Rectangle frame2)
+ {
+ public Rectangle Frame1 = frame1;
+ Rectangle Frame2 = frame2;
+
+ public readonly Rectangle GetFrame(int frame) {
+ return (frame % 2 == 0) ? Frame1 : Frame2;
+ }
+ }
+
+ static OnekoAnimation GetDirectionalRun(Vector2 direction)
+ {
+ direction = Vector2.Normalize(direction);
+
+ Vector2 nearestDir = Directions.Up;
+ float nearestDistance = 10f;
+ foreach (Vector2 dir in Directions.AllDirections) {
+ float distanceCheck = Vector2.Distance(direction, dir);
+ if (distanceCheck < nearestDistance) {
+ nearestDir = dir;
+ nearestDistance = distanceCheck;
+ }
+ }
+
+ return RunDirections[nearestDir];
+ }
+
+ static readonly Rectangle Idle1 = new(32*3, 32*3, 32, 32);
+ static readonly Rectangle Alert1 = new(32*7, 32*3, 32, 32);
+ static readonly Rectangle Yawn1 = new(32*3, 32*2, 32, 32);
+ static readonly Rectangle Clean1 = new(32*7, 0, 32, 32);
+ static readonly Rectangle Scratch1 = new(32*5, 0, 32, 32);
+ static readonly Rectangle Scratch2 = new(32*6, 0, 32, 32);
+ static readonly Rectangle Sleep1 = new(32*2, 0, 32, 32);
+ static readonly Rectangle Sleep2 = new(32*2, 32, 32, 32);
+ static readonly Rectangle RunUp1 = new(32, 32*3, 32, 32);
+ static readonly Rectangle RunUp2 = new(32, 32*2, 32, 32);
+ static readonly Rectangle RunUpLeft1 = new(32, 32, 32, 32);
+ static readonly Rectangle RunUpLeft2 = new(32, 0, 32, 32);
+ static readonly Rectangle RunLeft1 = new(32*4, 32*3, 32, 32);
+ static readonly Rectangle RunLeft2 = new(32*4, 32*2, 32, 32);
+ static readonly Rectangle RunDownLeft1 = new(32*6, 32, 32, 32);
+ static readonly Rectangle RunDownLeft2 = new(32*5, 32*3, 32, 32);
+ static readonly Rectangle RunDown1 = new(32*7, 32*2, 32, 32);
+ static readonly Rectangle RunDown2 = new(32*6, 32*3, 32, 32);
+ static readonly Rectangle RunDownRight1 = new(32*5, 32*2, 32, 32);
+ static readonly Rectangle RunDownRight2 = new(32*5, 32, 32, 32);
+ static readonly Rectangle RunRight1 = new(32*3, 32, 32, 32);
+ static readonly Rectangle RunRight2 = new(32*3, 0, 32, 32);
+ static readonly Rectangle RunUpRight1 = new(0, 32*3, 32, 32);
+ static readonly Rectangle RunUpRight2 = new(0, 32*2, 32, 32);
+ static readonly Rectangle ScratchUp1 = new(0, 0, 32, 32);
+ static readonly Rectangle ScratchUp2 = new(0, 32, 32, 32);
+ static readonly Rectangle ScratchLeft1 = new(32*4, 0, 32, 32);
+ static readonly Rectangle ScratchLeft2 = new(32*4, 32, 32, 32);
+ static readonly Rectangle ScratchDown1 = new(32*7, 32, 32, 32);
+ static readonly Rectangle ScratchDown2 = new(32*6, 32*2, 32, 32);
+ static readonly Rectangle ScratchRight1 = new(32*2, 32*2, 32, 32);
+ static readonly Rectangle ScratchRight2 = new(32*2, 32*3, 32, 32);
+
+ static readonly OnekoAnimation Idle = new(Idle1, Idle1);
+ static readonly OnekoAnimation Alert = new(Alert1, Alert1);
+ static readonly OnekoAnimation Yawn = new(Yawn1, Yawn1);
+ static readonly OnekoAnimation Clean = new(Clean1, Clean1);
+ static readonly OnekoAnimation ScratchSelf = new(Scratch1, Scratch2);
+ static readonly OnekoAnimation Sleep = new(Sleep1, Sleep2);
+ static readonly OnekoAnimation RunUp = new(RunUp1, RunUp2);
+ static readonly OnekoAnimation RunUpLeft = new(RunUpLeft1, RunUpLeft2);
+ static readonly OnekoAnimation RunLeft = new(RunLeft1, RunLeft2);
+ static readonly OnekoAnimation RunDownLeft = new(RunDownLeft1, RunDownLeft2);
+ static readonly OnekoAnimation RunDown = new(RunDown1, RunDown2);
+ static readonly OnekoAnimation RunDownRight = new(RunDownRight1, RunDownRight2);
+ static readonly OnekoAnimation RunRight = new(RunRight1, RunRight2);
+ static readonly OnekoAnimation RunUpRight = new(RunUpRight1, RunUpRight2);
+ static readonly OnekoAnimation ScratchUp = new(ScratchUp1, ScratchUp2);
+ static readonly OnekoAnimation ScratchLeft = new(ScratchLeft1, ScratchLeft2);
+ static readonly OnekoAnimation ScratchDown = new(ScratchDown1, ScratchDown2);
+ static readonly OnekoAnimation ScratchRight = new(ScratchRight1, ScratchRight2);
+
+ static readonly Dictionary<Vector2, OnekoAnimation> RunDirections = new() {
+ {Directions.Up, RunUp},
+ {Directions.Down, RunDown},
+ {Directions.Left, RunLeft},
+ {Directions.Right, RunRight},
+ {Directions.UpLeft, RunUpLeft},
+ {Directions.UpRight, RunUpRight},
+ {Directions.DownRight, RunDownRight},
+ {Directions.DownLeft, RunDownLeft},
+ };
+} \ No newline at end of file
diff --git a/OnekoOnline.csproj b/OnekoOnline.csproj
new file mode 100644
index 0000000..57f77e6
--- /dev/null
+++ b/OnekoOnline.csproj
@@ -0,0 +1,25 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+
+ <OutputType>Exe</OutputType>
+
+ <TargetFramework>net8.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ <PublishAot>true</PublishAot>
+ <StripSymbols>false</StripSymbols>
+
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Raylib-cs" Version="5.0.0" />
+
+ <EmbeddedResource Include="oneko.png" />
+
+ <Content Include="oneko.png" CopyToOutputDirectory="PreserveNewest" />
+ <Content Include="dog.png" CopyToOutputDirectory="PreserveNewest" />
+ <Content Include="tora.png" CopyToOutputDirectory="PreserveNewest" />
+ </ItemGroup>
+
+</Project>
diff --git a/dog.png b/dog.png
new file mode 100644
index 0000000..e9d6214
--- /dev/null
+++ b/dog.png
Binary files differ
diff --git a/oneko.png b/oneko.png
new file mode 100644
index 0000000..f4deb8e
--- /dev/null
+++ b/oneko.png
Binary files differ
diff --git a/petduck.png b/petduck.png
new file mode 100644
index 0000000..bc09004
--- /dev/null
+++ b/petduck.png
Binary files differ
diff --git a/tora.png b/tora.png
new file mode 100644
index 0000000..cbc3873
--- /dev/null
+++ b/tora.png
Binary files differ