diff options
| author | Sarah Bradley <git@sarahduck.ca> | 2023-12-01 20:33:42 -0800 |
|---|---|---|
| committer | Sarah Bradley <git@sarahduck.ca> | 2023-12-01 20:33:42 -0800 |
| commit | 2793b94040a473538f01723d5ca5f53c4535e2af (patch) | |
| tree | cb30f0dae20bda6ef9d1c005325bfd9c986b3c8f | |
What I've got so far
| -rw-r--r-- | .gitignore | 4 | ||||
| -rw-r--r-- | Bitmap.cs | 113 | ||||
| -rw-r--r-- | ConfigFile.cs | 84 | ||||
| -rw-r--r-- | Drawable.cs | 38 | ||||
| -rw-r--r-- | EmbeddedResources.cs | 16 | ||||
| -rw-r--r-- | Main.cs | 57 | ||||
| -rw-r--r-- | MathExtensions.cs | 33 | ||||
| -rw-r--r-- | Net.cs | 95 | ||||
| -rw-r--r-- | NetClient.cs | 78 | ||||
| -rw-r--r-- | NetServer.cs | 110 | ||||
| -rw-r--r-- | Oneko.cs | 178 | ||||
| -rw-r--r-- | OnekoOnline.csproj | 25 | ||||
| -rw-r--r-- | dog.png | bin | 0 -> 3377 bytes | |||
| -rw-r--r-- | oneko.png | bin | 0 -> 3055 bytes | |||
| -rw-r--r-- | petduck.png | bin | 0 -> 4501 bytes | |||
| -rw-r--r-- | tora.png | bin | 0 -> 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 @@ -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 @@ -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> Binary files differdiff --git a/oneko.png b/oneko.png Binary files differnew file mode 100644 index 0000000..f4deb8e --- /dev/null +++ b/oneko.png diff --git a/petduck.png b/petduck.png Binary files differnew file mode 100644 index 0000000..bc09004 --- /dev/null +++ b/petduck.png diff --git a/tora.png b/tora.png Binary files differnew file mode 100644 index 0000000..cbc3873 --- /dev/null +++ b/tora.png |
