Funkční komunikace mezi jedním clientem a serverem

This commit is contained in:
Perry 2025-12-19 17:54:50 +01:00
commit 3f235f6e04
44 changed files with 1030 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "MonoGameLibrary"]
path = MonoGameLibrary
url = ./MonoGameLibrary

13
.idea/.idea.FNAF_Clone/.idea/.gitignore generated vendored Normal file
View file

@ -0,0 +1,13 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/modules.xml
/contentModel.xml
/.idea.FNAF_Clone.iml
/projectSettingsUpdater.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
</project>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes>
<Path>FNAF_Server</Path>
</explicitIncludes>
<explicitExcludes />
</component>
</project>

7
.idea/.idea.FNAF_Clone/.idea/vcs.xml generated Normal file
View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/MonoGameLibrary" vcs="Git" />
</component>
</project>

28
FNAF_Clone.sln Normal file
View file

@ -0,0 +1,28 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FNAF_Clone", "FNAF_Clone\FNAF_Clone.csproj", "{069FFFDB-7D61-4AC5-9195-D3664F0B05A0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FNAF_Server", "FNAF_Server\FNAF_Server.csproj", "{5E19BD54-CFE0-453A-9CDB-BDB67A4ECB20}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PacketLib", "PacketLib\PacketLib.csproj", "{EDC8B98F-D2F7-4BDB-8E3B-E3120A743023}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{069FFFDB-7D61-4AC5-9195-D3664F0B05A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{069FFFDB-7D61-4AC5-9195-D3664F0B05A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{069FFFDB-7D61-4AC5-9195-D3664F0B05A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{069FFFDB-7D61-4AC5-9195-D3664F0B05A0}.Release|Any CPU.Build.0 = Release|Any CPU
{5E19BD54-CFE0-453A-9CDB-BDB67A4ECB20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5E19BD54-CFE0-453A-9CDB-BDB67A4ECB20}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5E19BD54-CFE0-453A-9CDB-BDB67A4ECB20}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5E19BD54-CFE0-453A-9CDB-BDB67A4ECB20}.Release|Any CPU.Build.0 = Release|Any CPU
{EDC8B98F-D2F7-4BDB-8E3B-E3120A743023}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EDC8B98F-D2F7-4BDB-8E3B-E3120A743023}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EDC8B98F-D2F7-4BDB-8E3B-E3120A743023}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EDC8B98F-D2F7-4BDB-8E3B-E3120A743023}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View file

@ -0,0 +1,4 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/AddReferences/RecentPaths/=_002Fhome_002Fperry_002FRiderProjects_002FFNAF_005FClone_002FFNAF_005FClone_002Flib_002FMonoGameLibrary_002Edll/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AINetSerializable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F3d47105f47a240929625d0f531812b9e1c000_003F0f_003Fe2eeb2cd_003FINetSerializable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANetSerializer_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F3d47105f47a240929625d0f531812b9e1c000_003Fcc_003F8a34584a_003FNetSerializer_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>

View file

@ -0,0 +1,36 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-mgcb": {
"version": "3.8.4",
"commands": [
"mgcb"
]
},
"dotnet-mgcb-editor": {
"version": "3.8.4",
"commands": [
"mgcb-editor"
]
},
"dotnet-mgcb-editor-linux": {
"version": "3.8.4",
"commands": [
"mgcb-editor-linux"
]
},
"dotnet-mgcb-editor-windows": {
"version": "3.8.4",
"commands": [
"mgcb-editor-windows"
]
},
"dotnet-mgcb-editor-mac": {
"version": "3.8.4",
"commands": [
"mgcb-editor-mac"
]
}
}
}

14
FNAF_Clone/.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,14 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "C#: FNAF_Clone Debug",
"type": "dotnet",
"request": "launch",
"projectPath": "${workspaceFolder}/FNAF_Clone.csproj"
}
],
}

View file

@ -0,0 +1,18 @@
namespace FNAF_Clone;
public class CameraSystem {
public bool Enabled { get; private set; }
public int CurrentCamera { get; private set; }
public void FlipUp() {
Enabled = true;
}
public void FlipDown() {
Enabled = false;
}
public void SetCamera(int camera) {
}
}

103
FNAF_Clone/Client.cs Normal file
View file

@ -0,0 +1,103 @@
using System;
using System.Net;
using System.Net.Sockets;
using LiteNetLib;
using LiteNetLib.Utils;
using PacketLib;
namespace FNAF_Clone;
public class Client {
private static EventBasedNetListener listener = new();
private static NetManager client;
private static NetPeer server;
private static NetDataWriter writer;
private static NetPacketProcessor processor;
public static ClientPlayer Player { get; } = new();
public static void Connect(string endPoint, int port) {
writer = new NetDataWriter();
processor = new NetPacketProcessor();
NestedTypeManager.AutoRegister(processor);
processor.SubscribeReusable<JoinAcceptPacket>(OnJoinAccept);
processor.SubscribeReusable<UpdatePlayerPacket>(OnPlayerUpdate);
client = new NetManager(listener){
AutoRecycle = true
};
client.Start();
Console.WriteLine($"Connecting to server @ {endPoint}:{port}");
listener.NetworkReceiveEvent += (peer, reader, channel, method) => {
OnNetworkReceive(peer, reader, method);
};
listener.PeerConnectedEvent += peer => {
Console.WriteLine("Connected to Server");
server = peer;
SendPacket(new JoinPacket {username = "Player1"}, DeliveryMethod.ReliableOrdered);
};
client.Connect(endPoint, port, ""); // TODO: figure out how keys work
}
public static void SendPacket<T>(T packet, DeliveryMethod deliveryMethod) where T : class, new() {
if (server != null) {
writer.Reset();
processor.Write(writer, packet);
server.Send(writer, deliveryMethod);
}
}
public static void SendCommands(PlayerCommand[] pCommands) {
SendPacket(new PlayerCommandPacket{commands = pCommands}, DeliveryMethod.ReliableOrdered);
}
public static void OnNetworkReceive(NetPeer peer, NetPacketReader reader, DeliveryMethod deliveryMethod) {
processor.ReadAllPackets(reader, peer);
}
public static void OnJoinAccept(JoinAcceptPacket packet) {
Console.WriteLine($"Accepted by server, pid: {packet.state.pid}");
Player.state = packet.state;
}
public static void Update() {
client.PollEvents();
}
public static void OnPlayerUpdate(UpdatePlayerPacket packet) {
//Player.state = Player.state.pid == 0 ? packet.stateP1 : packet.stateP2;
foreach (var e in packet.events){
switch (e.ID){
case 0: // join
Console.WriteLine("E: Player joined");
break;
case 1: // leave
Console.WriteLine("E: Player left");
break;
case 2: // switch cam
if (Player.state.pid == e.Args[0]){
Player.state.camera = e.Args[1];
}
Console.WriteLine($"E: player {e.Args[0]} switched to camera {e.Args[1]}");
break;
case 3: // toggle cam
Player.state.monitorUp = e.Args[1] == 1;
Console.WriteLine($"E: Player {e.Args[0]} toggled monitor {(e.Args[1] == 0 ? "off" : "on")}");
break;
case -1: // movement
throw new NotImplementedException();
}
}
}
}

View file

@ -0,0 +1,8 @@
using PacketLib;
namespace FNAF_Clone;
public class ClientPlayer {
public PlayerState state;
public string username;
}

View file

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework.Input;
using MonoGameLibrary.Input;
using PacketLib;
namespace FNAF_Clone;
public class CommandManager {
private static (string label, Keys key, Action action)[] keybinds = [
("Toggle Camera", Keys.S, ToggleCamera)
];
private static InputListenerHook toggleCamHook = new(true);
public static void InitInputListeners() {
Array.ForEach(keybinds, tuple => InputManager.AddListener(tuple.label, tuple.key, _ => tuple.action(), InputTiming.PRESS, toggleCamHook));
}
private static void ToggleCamera() {
Client.SendCommands([PlayerCommand.TOGGLE_MONITOR()]);
}
}

View file

@ -0,0 +1,15 @@
#----------------------------- Global Properties ----------------------------#
/outputDir:bin/$(Platform)
/intermediateDir:obj/$(Platform)
/platform:DesktopGL
/config:
/profile:Reach
/compress:False
#-------------------------------- References --------------------------------#
#---------------------------------- Content ---------------------------------#

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
This file contains an xml description of a font, and will be read by the XNA
Framework Content Pipeline. Follow the comments to customize the appearance
of the font in your game, and to change the characters which are available to draw
with.
-->
<XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics">
<Asset Type="Graphics:FontDescription">
<!--
Modify this string to change the font that will be imported.
-->
<FontName>Arial</FontName>
<!--
Size is a float value, measured in points. Modify this value to change
the size of the font.
-->
<Size>12</Size>
<!--
Spacing is a float value, measured in pixels. Modify this value to change
the amount of spacing in between characters.
-->
<Spacing>0</Spacing>
<!--
UseKerning controls the layout of the font. If this value is true, kerning information
will be used when placing characters.
-->
<UseKerning>true</UseKerning>
<!--
Style controls the style of the font. Valid entries are "Regular", "Bold", "Italic",
and "Bold, Italic", and are case sensitive.
-->
<Style>Regular</Style>
<!--
If you uncomment this line, the default character will be substituted if you draw
or measure text that contains characters which were not included in the font.
-->
<!-- <DefaultCharacter>*</DefaultCharacter> -->
<!--
CharacterRegions control what letters are available in the font. Every
character from Start to End will be built and made available for drawing. The
default range is from 32, (ASCII space), to 126, ('~'), covering the basic Latin
character set. The characters are ordered according to the Unicode standard.
See the documentation for more information.
-->
<CharacterRegions>
<CharacterRegion>
<Start>&#32;</Start>
<End>&#126;</End>
</CharacterRegion>
</CharacterRegions>
</Asset>
</XnaContent>

Binary file not shown.

View file

@ -0,0 +1,50 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<RollForward>Major</RollForward>
<PublishReadyToRun>false</PublishReadyToRun>
<TieredCompilation>false</TieredCompilation>
</PropertyGroup>
<PropertyGroup>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>Icon.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<None Remove="Icon.ico"/>
<None Remove="Icon.bmp"/>
<None Remove="Input\**" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Icon.ico">
<LogicalName>Icon.ico</LogicalName>
</EmbeddedResource>
<EmbeddedResource Include="Icon.bmp">
<LogicalName>Icon.bmp</LogicalName>
</EmbeddedResource>
<EmbeddedResource Remove="Input\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="LiteNetLib" Version="1.3.1" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.*"/>
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.*"/>
</ItemGroup>
<ItemGroup>
<Folder Include="lib\" />
</ItemGroup>
<ItemGroup>
<Reference Include="MonoGameLibrary">
<HintPath>lib\MonoGameLibrary.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PacketLib\PacketLib.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Input\**" />
</ItemGroup>
<Target Name="RestoreDotnetTools" BeforeTargets="CollectPackageReferences">
<Message Text="Restoring dotnet tools (this might take a while depending on your internet speed and should only happen upon building your project for the first time, or after upgrading MonoGame, or clearing your nuget cache)" Importance="High"/>
<Exec Command="dotnet tool restore"/>
</Target>
</Project>

47
FNAF_Clone/GameMain.cs Normal file
View file

@ -0,0 +1,47 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using MonoGameLibrary;
using MonoGameLibrary.Input;
namespace FNAF_Clone;
public class GameMain() : Core("fnafkooo", 1920, 1080, false) {
private GraphicsDeviceManager _graphics;
private SpriteBatch _spriteBatch;
protected override void Initialize() {
Client.Connect("127.0.0.1", 9012);
CommandManager.InitInputListeners();
base.Initialize();
}
protected override void LoadContent() {
_spriteBatch = new SpriteBatch(GraphicsDevice);
// TODO: use this.Content to load your game content here
}
protected override void Update(GameTime gameTime) {
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed ||
Keyboard.GetState().IsKeyDown(Keys.Escape))
Exit();
// TODO: Add your update logic here
InputManager.NextInputCycle();
Client.Update();
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime) {
GraphicsDevice.Clear(Color.CornflowerBlue);
// TODO: Add your drawing code here
base.Draw(gameTime);
}
}

BIN
FNAF_Clone/Icon.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

BIN
FNAF_Clone/Icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View file

@ -0,0 +1,6 @@
namespace FNAF_Clone.Input;
public class InputListenerHook(bool enabled, bool oneTimeOnly = false) {
public bool Enabled { get; set; } = enabled;
public bool RemoveOnNextTrigger { get; set; } = oneTimeOnly;
}

View file

@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework.Input;
namespace FNAF_Clone.Input;
public static class InputManager {
private static Dictionary<Keys, List<(KeypressHandler, InputListenerHook)>> keyPressListeners = new();
private static Dictionary<Keys, List<(KeypressHandler, InputListenerHook)>> keyHoldListeners = new(); // The bool is for enabling/disabling that method
private static Dictionary<Keys, List<(KeypressHandler, InputListenerHook)>> keyReleaseListeners = new();
private static KeyboardState oldState = Keyboard.GetState();
private static KeyboardState newState;
// public static ConcurrentQueue<Action> inputEventQueue = new();
public delegate void KeypressHandler(KeyboardState state);
// public static void StartListening() {
// processInputThread.Start();
// Console.WriteLine("Started listening for input");
// }
public static void NextInputCycle() {
Queue<KeypressHandler> inputEventQueue = new();
// Read
newState = Keyboard.GetState();
Keys[] pressed = newState.GetPressedKeys();
Keys[] pressedLastFrame = oldState.GetPressedKeys();
Array.ForEach(pressed, key => {
if (oldState.IsKeyUp(key)){
if (keyPressListeners.TryGetValue(key, out var pressHandlers)){
pressHandlers.ForEach(t => {
if (t.Item2.Enabled){
inputEventQueue.Enqueue(t.Item1);
}
if (t.Item2.RemoveOnNextTrigger){
pressHandlers.Remove(t);
}
});
}
}
if (keyHoldListeners.TryGetValue(key, out var holdHandlers)){
holdHandlers.ForEach(t => {
if (t.Item2.Enabled){
inputEventQueue.Enqueue(t.Item1);
}
if (t.Item2.RemoveOnNextTrigger){
holdHandlers.Remove(t);
}
});
}
});
Array.ForEach(pressedLastFrame, key => {
if (!keyReleaseListeners.ContainsKey(key)){
return;
}
if (newState.IsKeyUp(key)){
if (keyReleaseListeners.TryGetValue(key, out var releaseHandlers))
releaseHandlers.ForEach(t => {
if (t.Item2.Enabled){
inputEventQueue.Enqueue(t.Item1);
}
if (t.Item2.RemoveOnNextTrigger){
releaseHandlers.Remove(t);
}
});
}
});
oldState = newState;
// Execute
foreach (KeypressHandler handler in inputEventQueue){
handler(newState);
}
}
// private static Thread processInputThread = new(() => {
//
// });
public static void AddListener(Keys key, KeypressHandler action, InputTiming timing, InputListenerHook hook) {
Dictionary<Keys, List<(KeypressHandler, InputListenerHook)>> workingDict;
switch (timing){
case InputTiming.PRESS:
workingDict = keyPressListeners;
break;
case InputTiming.HOLD:
workingDict = keyHoldListeners;
break;
case InputTiming.RELEASE:
workingDict = keyReleaseListeners;
break;
default:
throw new ArgumentOutOfRangeException(nameof(timing), timing, null);
}
if (!workingDict.ContainsKey(key)){
workingDict.Add(key, new());
}
workingDict[key].Add((action, hook));
}
}

View file

@ -0,0 +1,7 @@
namespace FNAF_Clone.Input;
public enum InputTiming {
PRESS,
HOLD,
RELEASE
}

2
FNAF_Clone/Program.cs Normal file
View file

@ -0,0 +1,2 @@
using var game = new FNAF_Clone.GameMain();
game.Run();

43
FNAF_Clone/app.manifest Normal file
View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="FNAF_Clone"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on and is
is designed to work with. Uncomment the appropriate elements and Windows will
automatically selected the most compatible environment. -->
<!-- Windows Vista -->
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness>
</windowsSettings>
</application>
</assembly>

View file

@ -0,0 +1 @@
../../MonoGameLibrary/MonoGameLibrary/bin/Debug/net8.0/MonoGameLibrary.dll

View file

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LiteNetLib" Version="1.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PacketLib\PacketLib.csproj" />
</ItemGroup>
</Project>

12
FNAF_Server/GameLogic.cs Normal file
View file

@ -0,0 +1,12 @@
using PacketLib;
namespace FNAF_Server;
public class GameLogic {
public static void Update() {
}
}

9
FNAF_Server/Program.cs Normal file
View file

@ -0,0 +1,9 @@
using System.Security.Cryptography.X509Certificates;
namespace FNAF_Server;
public class Program {
public static void Main(string[] args) {
Server.Start(9012);
}
}

197
FNAF_Server/Server.cs Normal file
View file

@ -0,0 +1,197 @@
using System;
using System.Collections;
using System.Diagnostics;
using System.Threading;
using LiteNetLib;
using LiteNetLib.Utils;
using PacketLib;
namespace FNAF_Server;
public class Server {
public static ServerPlayer P1;
public static ServerPlayer P2;
private static EventBasedNetListener listener;
private static NetManager server;
private const int TargetTickRate = 30;
private const double TargetDeltaTime = 1.0 / TargetTickRate;
private const long TargetTickTimeMs = 1000 / TargetTickRate;
private static NetDataWriter writer;
private static NetPacketProcessor processor;
private static Dictionary<uint, ServerPlayer> players = new();
private static bool isRunning;
public static void Start(int port) {
writer = new NetDataWriter();
processor = new NetPacketProcessor();
NestedTypeManager.AutoRegister(processor);
processor.SubscribeReusable<JoinPacket, NetPeer>(OnJoinReceived);
processor.SubscribeReusable<PlayerCommandPacket, NetPeer>(OnCommandReceived);
listener = new EventBasedNetListener(); // ← Initialize the listener!
server = new NetManager(listener){
AutoRecycle = true
};
Console.WriteLine($"Starting server on {port}");
listener.ConnectionRequestEvent += request => {
Console.WriteLine($"Connection Request from {request.RemoteEndPoint}");
if (players.Count >= 2){
Console.WriteLine($"{request.RemoteEndPoint} denied, server full");
return;
}
request.Accept();
};
listener.NetworkReceiveEvent += (peer, reader, channel, method) => {
OnNetworkReceive(peer, reader, method);
};
listener.PeerDisconnectedEvent += (peer, info) => {
OnPeerDisconnected(peer, info);
};
server.Start(port);
Run();
}
public static void SendPacket<T>(T packet, NetPeer peer, DeliveryMethod deliveryMethod = DeliveryMethod.ReliableOrdered) where T : class, new() {
if (peer != null) {
writer.Reset();
processor.Write(writer, packet);
peer.Send(writer, deliveryMethod);
}
}
public static void SendPacket<T>(T packet, uint pid, DeliveryMethod deliveryMethod = DeliveryMethod.ReliableOrdered) where T : class, new() {
SendPacket(packet, players[pid].peer, deliveryMethod);
}
public static void SendPacketToAll<T>(T packet, DeliveryMethod deliveryMethod = DeliveryMethod.ReliableOrdered) where T : class, new() {
foreach (var player in players.Values){
SendPacket(packet, player.peer, deliveryMethod);
}
}
public static void SendUpdate(GameEvent[] gevents, uint pid) {
// SendPacket(new UpdatePlayerPacket{eventQueue = new EventQueue{Events = events}}, pid);
SendPacket(new UpdatePlayerPacket{events = gevents}, pid);
}
public static void SendUpdateToAll(GameEvent[] gevents) {
// SendPacketToAll(new UpdatePlayerPacket{eventQueue = new EventQueue{Events = events}});
SendPacketToAll(new UpdatePlayerPacket{events = gevents});
}
public static void OnJoinReceived(JoinPacket packet, NetPeer peer) {
Console.WriteLine($"Received join from {packet.username} (pid: {(uint)peer.Id})");
ServerPlayer newPlayer = (players[(uint)peer.Id] = new ServerPlayer {
peer = peer,
state = new PlayerState {
pid = (uint)peer.Id,
camera = 0
},
username = packet.username
});
SendPacket(new JoinAcceptPacket { state = newPlayer.state }, peer);
if (players.Count == 1){
P1 = newPlayer;
}
else{
P2 = newPlayer;
}
}
public static void OnNetworkReceive(NetPeer peer, NetPacketReader reader, DeliveryMethod deliveryMethod) {
processor.ReadAllPackets(reader, peer);
}
public static void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) {
if (peer.Tag != null) {
players.Remove((uint)peer.Id);
}
}
public static void OnCommandReceived(PlayerCommandPacket packet, NetPeer peer) {
PlayerCommand[] commands = packet.commands;
foreach (var playerCommand in commands){
switch (playerCommand.ID){
case 0:
Console.WriteLine($"C: Player {peer.Id} switched to camera {playerCommand.Args[0]}");
SendUpdateToAll([GameEvent.SWITCH_CAM(peer.Id, playerCommand.Args[0])]);
break;
case 1:
bool newState = !players[(uint)peer.Id].state.monitorUp;
players[(uint)peer.Id].state.monitorUp = newState;
Console.WriteLine($"C: Player {peer.Id} toggled camera {(newState ? "on" : "off")}");
SendUpdateToAll([GameEvent.TOGGLE_MONITOR(peer.Id, newState)]);
break;
}
}
}
public static void Update() {
server.PollEvents();
// Console.WriteLine("update");
GameLogic.Update(); // TODO: add a parameter for player input
}
public static void Run(CancellationToken cancellationToken = default)
{
isRunning = true;
var stopwatch = Stopwatch.StartNew();
long previousTicks = 0;
while (isRunning && !cancellationToken.IsCancellationRequested)
{
long currentTicks = stopwatch.ElapsedMilliseconds;
long elapsed = currentTicks - previousTicks;
if (elapsed >= TargetTickTimeMs)
{
Update();
previousTicks = currentTicks;
if (elapsed > TargetTickTimeMs * 2)
{
Console.WriteLine($"Warning: Tick took {elapsed}ms (target: {TargetTickTimeMs}ms)");
}
}
else
{
int sleepTime = (int)(TargetTickTimeMs - elapsed - 2);
if (sleepTime > 0)
{
Thread.Sleep(sleepTime);
}
while (stopwatch.ElapsedMilliseconds - previousTicks < TargetTickTimeMs){
Thread.SpinWait(1);
}
}
}
}
public static void Stop()
{
isRunning = false;
}
}

View file

@ -0,0 +1,10 @@
using LiteNetLib;
using PacketLib;
namespace FNAF_Server;
public class ServerPlayer {
public NetPeer peer;
public PlayerState state;
public string username;
}

1
MonoGameLibrary Submodule

@ -0,0 +1 @@
Subproject commit a2b524ee04772d483750047c997172cc62090a9a

17
PacketLib/EventQueue.cs Normal file
View file

@ -0,0 +1,17 @@
using LiteNetLib.Utils;
namespace PacketLib;
public struct EventQueue : INetSerializable {
public GameEvent[] Events{ get; set; }
public void Serialize(NetDataWriter writer) {
writer.PutArray(Events);
}
public void Deserialize(NetDataReader reader) {
Events = reader.GetArray<GameEvent>();
}
// public GameEvent this[int index] => Events[index];
}

29
PacketLib/GameEvent.cs Normal file
View file

@ -0,0 +1,29 @@
using System.Diagnostics.CodeAnalysis;
using LiteNetLib.Utils;
namespace PacketLib;
#nullable disable
public struct GameEvent : INetSerializable{
public static GameEvent PLAYER_JOIN(int pid) => new(){ID = 0, Args = [pid] };
public static GameEvent PLAYER_LEAVE(int pid) => new(){ID = 1, Args = [pid] };
public static GameEvent SWITCH_CAM(int pid, int camId) => new(){ID = 2, Args = [pid, camId] };
public static GameEvent TOGGLE_MONITOR(int pid, bool state) => new(){ID = 3, Args = [pid, state ? 1 : 0]};
public static GameEvent ENEMY_MOVEMENT(int enemyId, int camId) => new(){ID = -1, Args = [enemyId, camId]};
public int ID{ get; set; }
public bool Hideable => ID < 0;
public int[] Args{ get; private set; }
public void Serialize(NetDataWriter writer) {
writer.Put(ID);
writer.PutArray(Args);
}
public void Deserialize(NetDataReader reader) {
ID = reader.GetInt();
Args = reader.GetIntArray();
}
}

View file

@ -0,0 +1,5 @@
namespace PacketLib;
public class JoinAcceptPacket {
public PlayerState state { get; set; }
}

5
PacketLib/JoinPacket.cs Normal file
View file

@ -0,0 +1,5 @@
namespace PacketLib;
public class JoinPacket {
public string username { get; set; }
}

View file

@ -0,0 +1,12 @@
using LiteNetLib.Utils;
namespace PacketLib;
public static class NestedTypeManager {
public static void AutoRegister(NetPacketProcessor processor) {
processor.RegisterNestedType<PlayerState>();
processor.RegisterNestedType<GameEvent>();
processor.RegisterNestedType<EventQueue>();
processor.RegisterNestedType<PlayerCommand>();
}
}

View file

@ -0,0 +1,12 @@
using System.Xml;
using LiteNetLib.Utils;
namespace PacketLib;
public static class NetDataWriterExtensions {
public static GameEvent GetGameEvent(this NetDataReader reader) {
GameEvent gevent = new();
gevent.Deserialize(reader);
return gevent;
}
}

View file

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LiteNetLib" Version="1.3.1" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,23 @@
using LiteNetLib.Utils;
namespace PacketLib;
public struct PlayerCommand : INetSerializable {
public static PlayerCommand SWITCH_CAM(int camId) => new(){ID = 0, Args = [camId] };
public static PlayerCommand TOGGLE_MONITOR() => new(){ID = 1, Args = []};
public int ID{ get; set; }
public bool Hideable => ID < 0;
public int[] Args{ get; private set; }
public void Serialize(NetDataWriter writer) {
writer.Put(ID);
writer.PutArray(Args);
}
public void Deserialize(NetDataReader reader) {
ID = reader.GetInt();
Args = reader.GetIntArray();
}
}

View file

@ -0,0 +1,5 @@
namespace PacketLib;
public class PlayerCommandPacket {
public PlayerCommand[] commands { get; set; }
}

29
PacketLib/PlayerState.cs Normal file
View file

@ -0,0 +1,29 @@
using LiteNetLib;
using LiteNetLib.Utils;
namespace PacketLib;
public struct PlayerState : INetSerializable {
public uint pid;
public int camera;
public bool monitorUp;
public void Serialize(NetDataWriter writer) {
writer.Put(pid);
writer.Put(camera);
writer.Put(monitorUp);
}
public void Deserialize(NetDataReader reader) {
pid = reader.GetUInt();
camera = reader.GetInt();
monitorUp = reader.GetBool();
}
}

View file

@ -0,0 +1,11 @@
namespace PacketLib;
public class UpdatePlayerPacket {
// public PlayerState stateP1{ get; set; }
// public PlayerState stateP2{ get; set; }
// TODO: implement anti-desync measures by comparing server and client states
public GameEvent[] events { get; set; }
// public EventQueue eventQueue { get; set; }
}