1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2024-06-22 23:25:23 +00:00

Added client/server architecture support.

This commit is contained in:
Tim Sarbin 2018-11-30 23:37:08 -05:00
parent 0a3eb44248
commit 0cabaceb48
24 changed files with 467 additions and 103 deletions

View File

@ -0,0 +1,19 @@
using System;
using OpenDiablo2.Common.Enums;
namespace OpenDiablo2.Common.Attributes
{
[AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)]
public sealed class MessageFrameAttribute : Attribute
{
public eMessageFrameType FrameType { get; private set; }
// This is a positional argument
public MessageFrameAttribute(eMessageFrameType frameType)
{
this.FrameType = frameType;
}
}
}

View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OpenDiablo2.Common.Enums
{
public enum eMessageFrameType
{
None = 0x00,
SetSeed = 0x01,
JoinGame = 0x02,
MAX = 0xFF, // NOTE:
// You absolutely cannot have a higher ID than this without
// changing the message header to multi-byte for ALL frame types!!!
}
}

View File

@ -1,11 +1,12 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Drawing;
using OpenDiablo2.Common.Enums;
using OpenDiablo2.Common.Models;
namespace OpenDiablo2.Common.Interfaces
{
public interface IGameState
public interface IGameState : IDisposable
{
int Act { get; }
int Seed { get; }

View File

@ -6,7 +6,9 @@ using System.Threading.Tasks;
namespace OpenDiablo2.Common.Interfaces
{
public interface ISessionServer : IDisposable
public interface IMessageFrame
{
byte[] Data { get; set; }
void Process(object sender, ISessionEventProvider sessionEventProvider);
}
}

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OpenDiablo2.Common.Interfaces
{
public delegate void OnSetSeedEvent(object sender, int seed);
public delegate void OnJoinGameEvent(object sender, Guid playerId, string playerName); // TODO: Not the final version..
public interface ISessionEventProvider
{
OnSetSeedEvent OnSetSeed { get; set; }
OnJoinGameEvent OnJoinGame { get; set; }
}
}

View File

@ -6,9 +6,11 @@ using System.Threading.Tasks;
namespace OpenDiablo2.Common.Interfaces
{
public interface ISessionManager : IDisposable
public interface ISessionManager : ISessionEventProvider, IDisposable
{
void Initialize();
void Stop();
void JoinGame(string playerName);
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Threading;
namespace OpenDiablo2.Common.Interfaces
{
public interface ISessionServer : IDisposable
{
AutoResetEvent WaitServerStartEvent { get; set; }
void Start();
void Stop();
}
}

View File

@ -101,7 +101,6 @@ namespace OpenDiablo2.Common.Models
// TODO: DI magic please
public MPQDS1(Stream stream, LevelPreset level, LevelDetail levelDetail, LevelType levelType, IEngineDataManager engineDataManager, IResourceManager resourceManager)
{
log.Debug($"Loading {level.Name} (Act {levelDetail.Act})...");
var br = new BinaryReader(stream);
Version = br.ReadInt32();
Width = br.ReadInt32() + 1;
@ -296,8 +295,6 @@ namespace OpenDiablo2.Common.Models
if (!isMasked || levelType.File[i] == "0")
continue;
log.Debug($"Loading DT resource {levelType.File[i]}");
DT1s[i] = resourceManager.GetMPQDT1("data\\global\\tiles\\" + levelType.File[i].Replace("/", "\\"));
}

View File

@ -11,6 +11,7 @@ namespace OpenDiablo2.Common.Models.Mobs
{
public class PlayerState : MobState
{
public Guid Id { get; protected set; }
public eHero HeroType { get; protected set; }
private IHeroTypeConfig HeroTypeConfig;
private ILevelExperienceConfig ExperienceConfig;

View File

@ -52,8 +52,10 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Attributes\MessageFrameAttribute.cs" />
<Compile Include="Attributes\SceneAttribute.cs" />
<Compile Include="AutofacModule.cs" />
<Compile Include="Enums\eMessageFrameType.cs" />
<Compile Include="Enums\ePanelFrameType.cs" />
<Compile Include="Enums\eButtonType.cs" />
<Compile Include="Enums\eHero.cs" />
@ -67,8 +69,10 @@
<Compile Include="Enums\Mobs\eDamageTypes.cs" />
<Compile Include="Enums\Mobs\eMobFlags.cs" />
<Compile Include="Enums\Mobs\eStatModifierType.cs" />
<Compile Include="Interfaces\ISessionManager.cs" />
<Compile Include="Interfaces\ISessionServer.cs" />
<Compile Include="Interfaces\MessageBus\ISessionEventProvider.cs" />
<Compile Include="Interfaces\MessageBus\IMessageFrame.cs" />
<Compile Include="Interfaces\MessageBus\ISessionManager.cs" />
<Compile Include="Interfaces\MessageBus\ISessionServer.cs" />
<Compile Include="Interfaces\UI\IButton.cs" />
<Compile Include="Interfaces\UI\IPanelFrame.cs" />
<Compile Include="Interfaces\UI\IInventoryPanel.cs" />
@ -137,9 +141,6 @@
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<Folder Include="Message Frames\Requests\" />
<Folder Include="Message Frames\Responses\" />
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -27,7 +27,6 @@ namespace OpenDiablo2.Core
builder.RegisterType<ResourceManager>().As<IResourceManager>().SingleInstance();
builder.RegisterType<TextDictionary>().As<ITextDictionary>().SingleInstance();
builder.RegisterType<TextBox>().As<ITextBox>().InstancePerDependency();
builder.RegisterType<SessionServer>().As<ISessionServer>().InstancePerLifetimeScope();
}
}
}

View File

@ -60,14 +60,20 @@ namespace OpenDiablo2.Core.GameState_
sessionManager = getSessionManager(sessionType);
sessionManager.Initialize();
var random = new Random();
Seed = random.Next(); // TODO: Seed does not go here ;-(
sessionManager.OnSetSeed += OnSetSeedEvent;
sceneManager.ChangeScene("Game");
mapInfo = new List<MapInfo>();
(new MapGenerator(this)).Generate();
sceneManager.ChangeScene("Game");
sessionManager.JoinGame(characterName); // TODO: we need more attributes...
}
private void OnSetSeedEvent(object sender, int seed)
{
log.Info($"Setting seed to {seed}");
this.Seed = seed;
(new MapGenerator(this)).Generate();
}
public MapInfo LoadSubMap(int levelDefId, Point origin)
{
@ -391,5 +397,10 @@ namespace OpenDiablo2.Core.GameState_
animationTime += ((float)ms / 1000f);
animationTime -= (float)Math.Truncate(animationTime);
}
public void Dispose()
{
sessionManager?.Dispose();
}
}
}

View File

@ -65,7 +65,6 @@
<Compile Include="MPQProvider.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ResourceManager.cs" />
<Compile Include="SessionServer.cs" />
<Compile Include="TextDictionary.cs" />
<Compile Include="UI\Button.cs" />
<Compile Include="UI\PanelFrame.cs" />

View File

@ -1,18 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using OpenDiablo2.Common.Interfaces;
namespace OpenDiablo2.Core
{
public sealed class SessionServer : ISessionServer
{
public void Dispose()
{
}
}
}

View File

@ -1,5 +1,7 @@
using System;
using System.Linq;
using Autofac;
using OpenDiablo2.Common.Attributes;
using OpenDiablo2.Common.Enums;
using OpenDiablo2.Common.Interfaces;
@ -9,24 +11,38 @@ namespace OpenDiablo2.ServiceBus
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<LocalSessionManager>().AsSelf().InstancePerLifetimeScope();
builder.RegisterType<SessionManager>().As<ISessionManager>().InstancePerLifetimeScope();
builder.RegisterType<SessionServer>().As<ISessionServer>().InstancePerLifetimeScope();
var types = ThisAssembly.GetTypes().Where(x => typeof(IMessageFrame).IsAssignableFrom(x) && x.IsClass);
foreach (var type in types)
{
var att = type.GetCustomAttributes(true).First(x => typeof(MessageFrameAttribute).IsAssignableFrom(x.GetType())) as MessageFrameAttribute;
builder
.RegisterType(type)
.Keyed<IMessageFrame>(att.FrameType)
.InstancePerDependency();
}
builder.Register<Func<eMessageFrameType, IMessageFrame>>(c =>
{
var componentContext = c.Resolve<IComponentContext>();
return (frameType) => componentContext.ResolveKeyed<IMessageFrame>(frameType);
});
builder.Register<Func<eSessionType, ISessionManager>>(c =>
{
var componentContext = c.Resolve<IComponentContext>();
return (sessionType) =>
{
switch (sessionType)
{
case eSessionType.Local:
return componentContext.Resolve<LocalSessionManager>();
case eSessionType.Server:
case eSessionType.Remote:
default:
throw new ApplicationException("Unsupported session type.");
}
};
return (sessionType) => componentContext.Resolve<ISessionManager>(new NamedParameter("sessionType", sessionType));
});
builder.Register<Func<eSessionType, ISessionServer>>(c =>
{
var componentContext = c.Resolve<IComponentContext>();
return (sessionType) => componentContext.Resolve<ISessionServer>(new NamedParameter("sessionType", sessionType));
});
}
}
}

View File

@ -1,47 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using OpenDiablo2.Common.Interfaces;
namespace OpenDiablo2.ServiceBus
{
public sealed class LocalSessionManager : ISessionManager
{
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
private readonly ISessionServer sessionServer;
volatile bool running = false;
public LocalSessionManager(ISessionServer sessionServer)
{
this.sessionServer = sessionServer;
}
public void Initialize()
{
log.Info("Initializing a local multiplayer session.");
running = true;
Task.Run(() => Listen());
}
private void Listen()
{
log.Info("Local session manager is starting.");
while (running)
{
}
log.Info("Local session manager has stopped.");
}
public void Dispose()
{
}
public void Stop() => running = false;
}
}

View File

@ -0,0 +1,49 @@
using System;
using System.Linq;
using System.Text;
using OpenDiablo2.Common.Attributes;
using OpenDiablo2.Common.Enums;
using OpenDiablo2.Common.Interfaces;
namespace OpenDiablo2.ServiceBus.Message_Frames
{
[MessageFrame(eMessageFrameType.JoinGame)]
public sealed class MFJoinGame : IMessageFrame
{
public Guid PlayerId { get; set; }
public string PlayerName { get; set; }
public byte[] Data
{
get
{
return PlayerId.ToByteArray()
.Concat(BitConverter.GetBytes((UInt16)PlayerName.Length))
.Concat(Encoding.UTF8.GetBytes(PlayerName))
.ToArray();
}
set
{
PlayerId = new Guid(value.Take(16).ToArray());
var playerNameLen = BitConverter.ToUInt16(value, 16);
PlayerName = Encoding.UTF8.GetString(value, 18, value.Length - 18);
if (PlayerName.Length != playerNameLen)
throw new ApplicationException("Invalid player length!");
}
}
public MFJoinGame() { }
public MFJoinGame(string playerName)
{
PlayerId = Guid.NewGuid();
PlayerName = playerName;
}
public void Process(object sender, ISessionEventProvider sessionEventProvider)
{
sessionEventProvider.OnJoinGame(sender, PlayerId, PlayerName);
}
}
}

View File

@ -0,0 +1,35 @@
using System;
using OpenDiablo2.Common.Attributes;
using OpenDiablo2.Common.Enums;
using OpenDiablo2.Common.Interfaces;
namespace OpenDiablo2.ServiceBus.Message_Frames
{
[MessageFrame(eMessageFrameType.SetSeed)]
public sealed class MFSetSeed : IMessageFrame
{
public byte[] Data
{
get => BitConverter.GetBytes(Seed);
set => BitConverter.ToInt32(value, 0);
}
public Int32 Seed { get; private set; }
public MFSetSeed()
{
Seed = (new Random()).Next();
}
public MFSetSeed(int seed)
{
Seed = seed;
}
public void Process(object sender, ISessionEventProvider sessionEventProvider)
{
sessionEventProvider.OnSetSeed?.Invoke(sender, Seed);
}
}
}

View File

@ -56,8 +56,11 @@
</ItemGroup>
<ItemGroup>
<Compile Include="AutofacModule.cs" />
<Compile Include="LocalSessionManager.cs" />
<Compile Include="Message Frames\MFJoinGame.cs" />
<Compile Include="Message Frames\MFSetSeed.cs" />
<Compile Include="SessionManager.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SessionServer.cs" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />

View File

@ -0,0 +1,129 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NetMQ;
using NetMQ.Sockets;
using OpenDiablo2.Common.Attributes;
using OpenDiablo2.Common.Enums;
using OpenDiablo2.Common.Interfaces;
using OpenDiablo2.ServiceBus.Message_Frames;
namespace OpenDiablo2.ServiceBus
{
public sealed class SessionManager : ISessionManager
{
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
private readonly Func<eSessionType, ISessionServer> getSessionServer;
private readonly eSessionType sessionType;
private readonly Func<eMessageFrameType, IMessageFrame> getMessageFrame;
private RequestSocket requestSocket;
private AutoResetEvent resetEvent = new AutoResetEvent(false);
private ISessionServer sessionServer;
private Guid playerId;
private bool running = false;
public OnSetSeedEvent OnSetSeed { get; set; }
public OnJoinGameEvent OnJoinGame { get; set; }
public SessionManager(eSessionType sessionType, Func<eSessionType, ISessionServer> getSessionServer, Func<eMessageFrameType, IMessageFrame> getMessageFrame)
{
this.getSessionServer = getSessionServer;
this.sessionType = sessionType;
this.getMessageFrame = getMessageFrame;
}
public void Initialize()
{
if (sessionType == eSessionType.Local || sessionType == eSessionType.Server)
{
sessionServer = getSessionServer(sessionType);
sessionServer.Start();
sessionServer.WaitServerStartEvent.WaitOne(); // Wait until the server starts...
}
else sessionServer = null;
log.Info("Initializing a local multiplayer session.");
Task.Run(() => Listen());
}
private void Listen()
{
log.Info("Session manager is starting.");
requestSocket = new RequestSocket();
switch (sessionType)
{
case eSessionType.Local:
requestSocket.Connect("inproc://opendiablo2-session");
break;
case eSessionType.Server:
case eSessionType.Remote:
default:
throw new ApplicationException("This session type is currently unsupported.");
}
//var bytes = message.First().ToByteArray();
//var frameType = (eMessageFrameType)bytes[0];
//var frameData = bytes.Skip(1).ToArray(); // TODO: Can we maybe use pointers? This seems wasteful
//var messageFrame = getMessageFrame(frameType);
//messageFrame.Data = frameData;
//messageFrame.Process(socket, this);
running = true;
resetEvent.WaitOne();
running = false;
requestSocket.Dispose();
log.Info("Session manager has stopped.");
}
public void Stop()
{
if (!running)
return;
resetEvent.Set();
if (sessionType == eSessionType.Local || sessionType == eSessionType.Server)
sessionServer?.Stop();
}
public void Dispose()
{
Stop();
}
public void Send(IMessageFrame messageFrame)
{
var attr = messageFrame.GetType().GetCustomAttributes(true).First(x => typeof(MessageFrameAttribute).IsAssignableFrom(x.GetType())) as MessageFrameAttribute;
requestSocket.SendFrame(new byte[] { (byte)attr.FrameType }.Concat(messageFrame.Data).ToArray());
}
private void ProcessMessageFrame<T>() where T : IMessageFrame, new()
{
if (!running)
throw new ApplicationException("You have made a terrible mistake. Cannot get a message frame if you are not connected.");
var bytes = requestSocket.ReceiveFrameBytes();
var frameType = (eMessageFrameType)bytes[0];
var frameData = bytes.Skip(1).ToArray(); // TODO: Can we maybe use pointers? This seems wasteful
var messageFrame = getMessageFrame(frameType);
if (messageFrame.GetType() != typeof(T))
throw new ApplicationException("Recieved unexpected message frame!");
messageFrame.Data = frameData;
messageFrame.Process(requestSocket, this);
}
public void JoinGame(string playerName)
{
var mf = new MFJoinGame(playerName);
playerId = mf.PlayerId;
Send(mf);
ProcessMessageFrame<MFSetSeed>();
}
}
}

View File

@ -0,0 +1,104 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NetMQ;
using NetMQ.Sockets;
using OpenDiablo2.Common.Attributes;
using OpenDiablo2.Common.Enums;
using OpenDiablo2.Common.Interfaces;
using OpenDiablo2.ServiceBus.Message_Frames;
namespace OpenDiablo2.ServiceBus
{
public sealed class SessionServer : ISessionServer, ISessionEventProvider
{
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
private readonly eSessionType sessionType;
private readonly Func<eMessageFrameType, IMessageFrame> getMessageFrame;
private AutoResetEvent resetEvent = new AutoResetEvent(false);
public AutoResetEvent WaitServerStartEvent { get; set; } = new AutoResetEvent(false);
private int gameSeed;
private bool running = false;
private ResponseSocket responseSocket;
public OnSetSeedEvent OnSetSeed { get; set; }
public OnJoinGameEvent OnJoinGame { get; set; }
public SessionServer(eSessionType sessionType, Func<eMessageFrameType, IMessageFrame> getMessageFrame)
{
this.sessionType = sessionType;
this.getMessageFrame = getMessageFrame;
}
public void Start()
{
gameSeed = (new Random()).Next();
Task.Run(() => Serve());
}
private void Serve()
{
log.Info("Session server is starting.");
responseSocket = new ResponseSocket();
switch (sessionType)
{
case eSessionType.Local:
responseSocket.Bind("inproc://opendiablo2-session");
break;
case eSessionType.Server:
case eSessionType.Remote:
default:
throw new ApplicationException("This session type is currently unsupported.");
}
OnJoinGame += OnJoinGameHandler;
var proactor = new NetMQProactor(responseSocket, (socket, message) =>
{
var bytes = message.First().ToByteArray();
var frameType = (eMessageFrameType)bytes[0];
var frameData = bytes.Skip(1).ToArray(); // TODO: Can we maybe use pointers? This seems wasteful
var messageFrame = getMessageFrame(frameType);
messageFrame.Data = frameData;
messageFrame.Process(socket, this);
});
running = true;
WaitServerStartEvent.Set();
resetEvent.WaitOne();
proactor.Dispose();
running = false;
responseSocket.Dispose();
log.Info("Session server has stopped.");
}
public void Stop()
{
if (!running)
return;
resetEvent.Set();
}
public void Dispose()
{
Stop();
}
private void Send(NetMQSocket target, IMessageFrame messageFrame)
{
var attr = messageFrame.GetType().GetCustomAttributes(true).First(x => typeof(MessageFrameAttribute).IsAssignableFrom(x.GetType())) as MessageFrameAttribute;
responseSocket.SendFrame(new byte[] { (byte)attr.FrameType }.Concat(messageFrame.Data).ToArray());
}
private void OnJoinGameHandler(object sender, Guid playerId, string playerName)
{
// TODO: Try to make this less stupid
Send(sender as NetMQSocket, new MFSetSeed(gameSeed));
}
}
}

View File

@ -5,7 +5,7 @@
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{2B0CF1AC-06DD-4322-AE8B-FF8A8C70A3CD}</ProjectGuid>
<OutputType>WinExe</OutputType>
<OutputType>Exe</OutputType>
<RootNamespace>OpenDiablo2</RootNamespace>
<AssemblyName>OpenDiablo2</AssemblyName>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>

View File

@ -34,9 +34,20 @@ namespace OpenDiablo2
try
{
#endif
BuildContainer()
.Resolve<IGameEngine>()
.Run();
var container = BuildContainer();
try
{
using (var gameEngine = container.Resolve<IGameEngine>())
{
gameEngine.Run();
}
}
finally
{
container.Dispose();
}
#if !DEBUG
}
catch (Exception ex)

View File

@ -29,7 +29,7 @@ You need to have MonoDevelop installed, as well as any depenencies for that. You
## Command Line Parameters
| Long Name | Description |
| ------------ | ------------------------------------------------------------ |
| --datapath | (-d) Defines the path where the data files can be found |
| --datapath | (-p) Defines the path where the data files can be found |
| --hwmouse | Use the hardware mouse instead of software |
| --mousescale | When hardware mouse is enabled, this defines the pixel scale of the mouse. No effect for software mode |
| --fullscreen | (-f) When set, the game launches in full screen mode at 800x600. |