From 2793b94040a473538f01723d5ca5f53c4535e2af Mon Sep 17 00:00:00 2001 From: Sarah Bradley Date: Fri, 1 Dec 2023 20:33:42 -0800 Subject: What I've got so far --- .gitignore | 4 ++ Bitmap.cs | 113 ++++++++++++++++++++++++++++++++ ConfigFile.cs | 84 ++++++++++++++++++++++++ Drawable.cs | 38 +++++++++++ EmbeddedResources.cs | 16 +++++ Main.cs | 57 +++++++++++++++++ MathExtensions.cs | 33 ++++++++++ Net.cs | 95 +++++++++++++++++++++++++++ NetClient.cs | 78 ++++++++++++++++++++++ NetServer.cs | 110 +++++++++++++++++++++++++++++++ Oneko.cs | 178 +++++++++++++++++++++++++++++++++++++++++++++++++++ OnekoOnline.csproj | 25 ++++++++ dog.png | Bin 0 -> 3377 bytes oneko.png | Bin 0 -> 3055 bytes petduck.png | Bin 0 -> 4501 bytes tora.png | Bin 0 -> 3474 bytes 16 files changed, 831 insertions(+) create mode 100644 .gitignore create mode 100644 Bitmap.cs create mode 100644 ConfigFile.cs create mode 100644 Drawable.cs create mode 100644 EmbeddedResources.cs create mode 100644 Main.cs create mode 100644 MathExtensions.cs create mode 100644 Net.cs create mode 100644 NetClient.cs create mode 100644 NetServer.cs create mode 100644 Oneko.cs create mode 100644 OnekoOnline.csproj create mode 100644 dog.png create mode 100644 oneko.png create mode 100644 petduck.png create mode 100644 tora.png 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 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 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 linesToWrite = [TopLine]; + foreach (KeyValuePair pair in ConfigOptions.OrderBy(k => k.Key)) linesToWrite.Add(pair.Key + "=" + pair.Value); + File.WriteAllLines(Path, linesToWrite); + + OptionChanged = false; + } + + public T GetValue(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(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 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 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 span) + { + span[0] = (byte)Type; + BinaryPrimitives.WriteInt32LittleEndian(span[1..], DataSize); + BinaryPrimitives.WriteInt32LittleEndian(span[(1+sizeof(int))..], FromId); + } + + public static PacketInfo Deserialize(ReadOnlySpan 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 Users = []; + static readonly Dictionary 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 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 ToSend = Channel.CreateUnbounded(); + 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 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 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 @@ + + + + + Exe + + net8.0 + enable + enable + true + false + + + + + + + + + + + + + + diff --git a/dog.png b/dog.png new file mode 100644 index 0000000..e9d6214 Binary files /dev/null and b/dog.png differ diff --git a/oneko.png b/oneko.png new file mode 100644 index 0000000..f4deb8e Binary files /dev/null and b/oneko.png differ diff --git a/petduck.png b/petduck.png new file mode 100644 index 0000000..bc09004 Binary files /dev/null and b/petduck.png differ diff --git a/tora.png b/tora.png new file mode 100644 index 0000000..cbc3873 Binary files /dev/null and b/tora.png differ -- cgit