From f7398085207f4ee39b1d67d7ed8c9f979fd558f8 Mon Sep 17 00:00:00 2001 From: Regalis Date: Mon, 31 Aug 2015 19:57:49 +0300 Subject: [PATCH] Progress on tutorial, gap tweaking (water flows faster from room to room), UPnP error messages, input keys in array, underwater aiming tweaking, tons of misc stuff commit more often ffs --- Launcher/Launcher.csproj.user | 2 +- .../Documentation/ChangedFromV2.txt | 10 + Lidgren.Network/Documentation/Discovery.html | 113 + .../Documentation/Improvements.txt | 22 + .../Documentation/PacketLayout.txt | 17 + .../Documentation/SimulatingBadNetwork.html | 115 + Lidgren.Network/Documentation/TODO.txt | 17 + Lidgren.Network/Documentation/Tutorial.html | 206 ++ Lidgren.Network/Encryption/INetEncryption.cs | 21 + .../Encryption/NetAESEncryption.cs | 172 ++ .../Encryption/NetBlockEncryptionBase.cs | 88 + .../Encryption/NetDESEncryption.cs | 164 ++ .../Encryption/NetRC2Encryption.cs | 165 ++ .../Encryption/NetTripleDESEncryption.cs | 164 ++ .../Encryption/NetXorEncryption.cs | 58 + .../Encryption/NetXteaEncryption.cs | 146 + Lidgren.Network/Lidgren.Network.csproj | 165 ++ Lidgren.Network/Lidgren.snk | Bin 0 -> 596 bytes Lidgren.Network/NamespaceDoc.cs | 14 + Lidgren.Network/NetBigInteger.cs | 2337 +++++++++++++++++ Lidgren.Network/NetBitVector.cs | 172 ++ Lidgren.Network/NetBitWriter.cs | 514 ++++ Lidgren.Network/NetBuffer.Peek.cs | 312 +++ Lidgren.Network/NetBuffer.Read.Reflection.cs | 103 + Lidgren.Network/NetBuffer.Read.cs | 657 +++++ Lidgren.Network/NetBuffer.Write.Reflection.cs | 91 + Lidgren.Network/NetBuffer.Write.cs | 619 +++++ Lidgren.Network/NetBuffer.cs | 98 + Lidgren.Network/NetClient.cs | 171 ++ Lidgren.Network/NetConnection.Handshake.cs | 479 ++++ Lidgren.Network/NetConnection.Latency.cs | 147 ++ Lidgren.Network/NetConnection.MTU.cs | 175 ++ Lidgren.Network/NetConnection.cs | 512 ++++ Lidgren.Network/NetConnectionStatistics.cs | 204 ++ Lidgren.Network/NetConnectionStatus.cs | 68 + Lidgren.Network/NetConstants.cs | 57 + Lidgren.Network/NetDeliveryMethod.cs | 46 + Lidgren.Network/NetException.cs | 83 + Lidgren.Network/NetFragmentationHelper.cs | 175 ++ Lidgren.Network/NetFragmentationInfo.cs | 12 + Lidgren.Network/NetIncomingMessage.cs | 115 + Lidgren.Network/NetIncomingMessageType.cs | 105 + Lidgren.Network/NetMessageType.cs | 175 ++ Lidgren.Network/NetNatIntroduction.cs | 110 + Lidgren.Network/NetOutgoingMessage.cs | 135 + Lidgren.Network/NetPeer.Discovery.cs | 60 + Lidgren.Network/NetPeer.Fragmentation.cs | 161 ++ Lidgren.Network/NetPeer.Internal.cs | 691 +++++ Lidgren.Network/NetPeer.LatencySimulation.cs | 304 +++ Lidgren.Network/NetPeer.Logging.cs | 71 + Lidgren.Network/NetPeer.MessagePools.cs | 228 ++ Lidgren.Network/NetPeer.Send.cs | 227 ++ Lidgren.Network/NetPeer.cs | 337 +++ Lidgren.Network/NetPeerConfiguration.cs | 471 ++++ Lidgren.Network/NetPeerStatistics.cs | 161 ++ Lidgren.Network/NetPeerStatus.cs | 49 + Lidgren.Network/NetQueue.cs | 326 +++ Lidgren.Network/NetRandom.cs | 373 +++ Lidgren.Network/NetReceiverChannelBase.cs | 18 + Lidgren.Network/NetReliableOrderedReceiver.cs | 87 + Lidgren.Network/NetReliableSenderChannel.cs | 261 ++ .../NetReliableSequencedReceiver.cs | 63 + .../NetReliableUnorderedReceiver.cs | 87 + Lidgren.Network/NetSRP.cs | 204 ++ Lidgren.Network/NetSendResult.cs | 30 + Lidgren.Network/NetSenderChannelBase.cs | 19 + Lidgren.Network/NetServer.cs | 70 + Lidgren.Network/NetStoredReliableMessage.cs | 19 + Lidgren.Network/NetTime.cs | 61 + Lidgren.Network/NetTuple.cs | 19 + Lidgren.Network/NetUPnP.cs | 266 ++ Lidgren.Network/NetUnreliableSenderChannel.cs | 125 + .../NetUnreliableSequencedReceiver.cs | 29 + .../NetUnreliableUnorderedReceiver.cs | 20 + Lidgren.Network/NetUtility.cs | 592 +++++ Lidgren.Network/Properties/AssemblyInfo.cs | 37 + Lidgren.Network/SenderChannelBase.cs | 11 + Subsurface/Content/Content.mgcb | 13 + Subsurface/Content/LargeFont.xnb | Bin 0 -> 177755 bytes Subsurface/Content/Map/locationTypes.xml | 8 +- .../Content/Particles/ParticlePrefabs.xml | 10 +- Subsurface/Content/Sounds/sounds.xml | 6 +- Subsurface/Content/UI/style.xml | 18 +- Subsurface/Content/effects_linux.mgfx | Bin 0 -> 3699 bytes Subsurface/Content/randomevents.xml | 5 +- .../Source/Characters/AI/AIController.cs | 1 + .../Source/Characters/AI/EnemyAIController.cs | 4 +- Subsurface/Source/Characters/Character.cs | 333 +-- .../Source/Characters/FishAnimController.cs | 2 +- .../Characters/HumanoidAnimController.cs | 32 +- Subsurface/Source/Characters/Ragdoll.cs | 40 +- Subsurface/Source/ContentPackage.cs | 7 +- Subsurface/Source/CoroutineManager.cs | 15 +- Subsurface/Source/DebugConsole.cs | 26 +- Subsurface/Source/EventInput/EventInput.cs | 14 +- Subsurface/Source/Events/MonsterEvent.cs | 32 +- .../Source/Events/Quests/MonsterQuest.cs | 2 +- Subsurface/Source/Events/Quests/Quest.cs | 2 +- Subsurface/Source/Events/ScriptedEvent.cs | 2 +- Subsurface/Source/Events/ScriptedTask.cs | 5 + Subsurface/Source/Events/Task.cs | 5 + Subsurface/Source/Events/TaskManager.cs | 81 +- Subsurface/Source/GUI/GUIMessage.cs | 4 +- Subsurface/Source/GUI/GUIMessageBox.cs | 7 +- Subsurface/Source/Game1.cs | 28 +- .../Source/GameSession/GameModes/QuestMode.cs | 2 +- .../GameSession/GameModes/TutorialMode.cs | 226 +- Subsurface/Source/GameSession/GameSession.cs | 3 +- Subsurface/Source/GameSettings.cs | 51 +- .../Items/Components/Holdable/RangedWeapon.cs | 2 +- .../Items/Components/Holdable/RepairTool.cs | 16 +- .../Items/Components/Holdable/Throwable.cs | 4 +- .../Source/Items/Components/ItemComponent.cs | 4 +- .../Items/Components/Machines/MiniMap.cs | 18 +- .../Source/Items/Components/Machines/Pump.cs | 21 +- .../Items/Components/Machines/Steering.cs | 8 +- Subsurface/Source/Items/Components/Turret.cs | 19 +- .../Source/Items/Components/Wearable.cs | 2 +- Subsurface/Source/Items/Item.cs | 8 +- Subsurface/Source/Map/Explosion.cs | 4 +- Subsurface/Source/Map/Gap.cs | 20 +- Subsurface/Source/Map/Levels/Level.cs | 6 +- Subsurface/Source/Map/Lights/ConvexHull.cs | 2 +- Subsurface/Source/Map/Map.cs | 4 +- Subsurface/Source/Map/Md5Hash.cs | 5 + Subsurface/Source/Map/Structure.cs | 56 +- Subsurface/Source/Map/Submarine.cs | 9 +- Subsurface/Source/Map/WaterRenderer.cs | 7 +- Subsurface/Source/Networking/GameClient.cs | 48 +- Subsurface/Source/Networking/GameServer.cs | 101 +- Subsurface/Source/Networking/NetConfig.cs | 24 + Subsurface/Source/Networking/NetworkEvent.cs | 12 +- Subsurface/Source/Networking/NetworkMember.cs | 18 +- Subsurface/Source/Particles/Particle.cs | 4 +- .../Source/Particles/ParticleManager.cs | 5 +- Subsurface/Source/Particles/ParticlePrefab.cs | 20 +- Subsurface/Source/Physics/PhysicsBody.cs | 8 +- Subsurface/Source/PlayerInput.cs | 15 + Subsurface/Source/Program.cs | 74 +- Subsurface/Source/Screens/MainMenu.cs | 8 +- Subsurface/Source/Screens/NetLobbyScreen.cs | 64 +- Subsurface/Source/Screens/ServerListScreen.cs | 16 +- .../Source/Sounds/AmbientSoundManager.cs | 25 +- Subsurface/Source/Sounds/SoundManager.cs | 2 - Subsurface/Source/Sprite.cs | 2 +- Subsurface/Source/Utils/TextureLoader.cs | 2 +- Subsurface/Source/Utils/ToolBox.cs | 9 + Subsurface/Subsurface.csproj | 10 +- Subsurface_Solution.sln | 47 + Subsurface_Solution.v12.suo | Bin 610816 -> 650752 bytes 150 files changed, 15933 insertions(+), 588 deletions(-) create mode 100644 Lidgren.Network/Documentation/ChangedFromV2.txt create mode 100644 Lidgren.Network/Documentation/Discovery.html create mode 100644 Lidgren.Network/Documentation/Improvements.txt create mode 100644 Lidgren.Network/Documentation/PacketLayout.txt create mode 100644 Lidgren.Network/Documentation/SimulatingBadNetwork.html create mode 100644 Lidgren.Network/Documentation/TODO.txt create mode 100644 Lidgren.Network/Documentation/Tutorial.html create mode 100644 Lidgren.Network/Encryption/INetEncryption.cs create mode 100644 Lidgren.Network/Encryption/NetAESEncryption.cs create mode 100644 Lidgren.Network/Encryption/NetBlockEncryptionBase.cs create mode 100644 Lidgren.Network/Encryption/NetDESEncryption.cs create mode 100644 Lidgren.Network/Encryption/NetRC2Encryption.cs create mode 100644 Lidgren.Network/Encryption/NetTripleDESEncryption.cs create mode 100644 Lidgren.Network/Encryption/NetXorEncryption.cs create mode 100644 Lidgren.Network/Encryption/NetXteaEncryption.cs create mode 100644 Lidgren.Network/Lidgren.Network.csproj create mode 100644 Lidgren.Network/Lidgren.snk create mode 100644 Lidgren.Network/NamespaceDoc.cs create mode 100644 Lidgren.Network/NetBigInteger.cs create mode 100644 Lidgren.Network/NetBitVector.cs create mode 100644 Lidgren.Network/NetBitWriter.cs create mode 100644 Lidgren.Network/NetBuffer.Peek.cs create mode 100644 Lidgren.Network/NetBuffer.Read.Reflection.cs create mode 100644 Lidgren.Network/NetBuffer.Read.cs create mode 100644 Lidgren.Network/NetBuffer.Write.Reflection.cs create mode 100644 Lidgren.Network/NetBuffer.Write.cs create mode 100644 Lidgren.Network/NetBuffer.cs create mode 100644 Lidgren.Network/NetClient.cs create mode 100644 Lidgren.Network/NetConnection.Handshake.cs create mode 100644 Lidgren.Network/NetConnection.Latency.cs create mode 100644 Lidgren.Network/NetConnection.MTU.cs create mode 100644 Lidgren.Network/NetConnection.cs create mode 100644 Lidgren.Network/NetConnectionStatistics.cs create mode 100644 Lidgren.Network/NetConnectionStatus.cs create mode 100644 Lidgren.Network/NetConstants.cs create mode 100644 Lidgren.Network/NetDeliveryMethod.cs create mode 100644 Lidgren.Network/NetException.cs create mode 100644 Lidgren.Network/NetFragmentationHelper.cs create mode 100644 Lidgren.Network/NetFragmentationInfo.cs create mode 100644 Lidgren.Network/NetIncomingMessage.cs create mode 100644 Lidgren.Network/NetIncomingMessageType.cs create mode 100644 Lidgren.Network/NetMessageType.cs create mode 100644 Lidgren.Network/NetNatIntroduction.cs create mode 100644 Lidgren.Network/NetOutgoingMessage.cs create mode 100644 Lidgren.Network/NetPeer.Discovery.cs create mode 100644 Lidgren.Network/NetPeer.Fragmentation.cs create mode 100644 Lidgren.Network/NetPeer.Internal.cs create mode 100644 Lidgren.Network/NetPeer.LatencySimulation.cs create mode 100644 Lidgren.Network/NetPeer.Logging.cs create mode 100644 Lidgren.Network/NetPeer.MessagePools.cs create mode 100644 Lidgren.Network/NetPeer.Send.cs create mode 100644 Lidgren.Network/NetPeer.cs create mode 100644 Lidgren.Network/NetPeerConfiguration.cs create mode 100644 Lidgren.Network/NetPeerStatistics.cs create mode 100644 Lidgren.Network/NetPeerStatus.cs create mode 100644 Lidgren.Network/NetQueue.cs create mode 100644 Lidgren.Network/NetRandom.cs create mode 100644 Lidgren.Network/NetReceiverChannelBase.cs create mode 100644 Lidgren.Network/NetReliableOrderedReceiver.cs create mode 100644 Lidgren.Network/NetReliableSenderChannel.cs create mode 100644 Lidgren.Network/NetReliableSequencedReceiver.cs create mode 100644 Lidgren.Network/NetReliableUnorderedReceiver.cs create mode 100644 Lidgren.Network/NetSRP.cs create mode 100644 Lidgren.Network/NetSendResult.cs create mode 100644 Lidgren.Network/NetSenderChannelBase.cs create mode 100644 Lidgren.Network/NetServer.cs create mode 100644 Lidgren.Network/NetStoredReliableMessage.cs create mode 100644 Lidgren.Network/NetTime.cs create mode 100644 Lidgren.Network/NetTuple.cs create mode 100644 Lidgren.Network/NetUPnP.cs create mode 100644 Lidgren.Network/NetUnreliableSenderChannel.cs create mode 100644 Lidgren.Network/NetUnreliableSequencedReceiver.cs create mode 100644 Lidgren.Network/NetUnreliableUnorderedReceiver.cs create mode 100644 Lidgren.Network/NetUtility.cs create mode 100644 Lidgren.Network/Properties/AssemblyInfo.cs create mode 100644 Lidgren.Network/SenderChannelBase.cs create mode 100644 Subsurface/Content/Content.mgcb create mode 100644 Subsurface/Content/LargeFont.xnb create mode 100644 Subsurface/Content/effects_linux.mgfx create mode 100644 Subsurface/Source/Networking/NetConfig.cs diff --git a/Launcher/Launcher.csproj.user b/Launcher/Launcher.csproj.user index fe7dc2232..a4a6cdff4 100644 --- a/Launcher/Launcher.csproj.user +++ b/Launcher/Launcher.csproj.user @@ -1,6 +1,6 @@  - ShowAllFiles + ProjectFiles \ No newline at end of file diff --git a/Lidgren.Network/Documentation/ChangedFromV2.txt b/Lidgren.Network/Documentation/ChangedFromV2.txt new file mode 100644 index 000000000..41c274fc0 --- /dev/null +++ b/Lidgren.Network/Documentation/ChangedFromV2.txt @@ -0,0 +1,10 @@ + + +* The NetBuffer object is gone; instead there are NetOutgoingMessage and NetIncomingMessage objects + +* No need to allocate a read buffer before calling ReadMessage + + + + + diff --git a/Lidgren.Network/Documentation/Discovery.html b/Lidgren.Network/Documentation/Discovery.html new file mode 100644 index 000000000..19261fd74 --- /dev/null +++ b/Lidgren.Network/Documentation/Discovery.html @@ -0,0 +1,113 @@ + + + + + Peer/server discovery + + + + + + +
+

Peer/server discovery

+

+ Peer discovery is the process of clients detecting what servers are available. Discovery requests can be made in two ways; + locally as a broadcast, which will send a signal to all peers on your subnet. Secondly you can contact an ip address directly + and query it if a server is running. +

+

Responding to discovery requests are done in the same way regardless of how the request is made.

+ +

Here's how to do on the client side; ie. the side which makes a request:

+
+
// Enable DiscoveryResponse messages
+
config.EnableMessageType(NetIncomingMessageType.DiscoveryResponse);
+
 
+
// Emit a discovery signal
+
Client.DiscoverLocalPeers(14242);
+
+ +

This will send a discovery signal to your subnet; Here's how to receive the signal on the server side, and send a response back to the client:

+ +
+
// Enable DiscoveryRequest messages
+
config.EnableMessageType(NetIncomingMessageType.DiscoveryRequest);
+
 
+
// Standard message reading loop
+
while ((inc = Server.ReadMessage()) != null)
+
{
+
    switch (inc.MessageType)
+
    {
+
        case NetIncomingMessageType.DiscoveryRequest:
+
 
+
            // Create a response and write some example data to it
+
            NetOutgoingMessage response = Server.CreateMessage();
+
            response.Write("My server name");
+
 
+
            // Send the response to the sender of the request
+
            Server.SendDiscoveryResponse(response, inc.SenderEndpoint);
+
            break;
+
+ +

When the response then reaches the client, you can read the data you wrote on the server:

+ +
+
// Standard message reading loop
+
while ((inc = Client.ReadMessage()) != null)
+
{
+
    switch (inc.MessageType)
+
    {
+
        case NetIncomingMessageType.DiscoveryResponse:
+
 
+
            Console.WriteLine("Found server at " + inc.SenderEndpoint + " name: " + inc.ReadString());
+
            break;
+
+
+ + diff --git a/Lidgren.Network/Documentation/Improvements.txt b/Lidgren.Network/Documentation/Improvements.txt new file mode 100644 index 000000000..5ef9228a8 --- /dev/null +++ b/Lidgren.Network/Documentation/Improvements.txt @@ -0,0 +1,22 @@ + +Improvements over last version of library: + +* New delivery type: Reliable sequenced (Lost packets are resent but late arrivals are dropped) +* Disconnects and shutdown requests are now queued properly, so calling shutdown will still send any queued messages before shutting down +* All messages are pooled/recycled for zero garbage +* Reduced CPU usage and lower latencies (in the <1 ms range, but still) due to better socket polling +* All public members of NetPeer/NetConnection are completely thread safe +* Larger number of delivery channels +* More exact roundtrip measurement +* Method serialize entire objects via reflection +* Unique identifier now exists for all peers/connections +* More flexible peer discovery; filters possible and arbitrary data can be sent with response +* Much better protection against malformed messages crashing the app + +API enhancements: +* NetPeerConfiguration immutable properties now locked once NetPeer is initialized +* Messages cannot be send twice by accident +* Impossible to confuse sending and receiving buffers since they're different classes +* No more confusion if user should create a buffer or preallocate and reuse + + diff --git a/Lidgren.Network/Documentation/PacketLayout.txt b/Lidgren.Network/Documentation/PacketLayout.txt new file mode 100644 index 000000000..d70961926 --- /dev/null +++ b/Lidgren.Network/Documentation/PacketLayout.txt @@ -0,0 +1,17 @@ + +PER MESSAGE: +7 bits - NetMessageType +1 bit - Is a message fragment? + +[8 bits NetMessageLibraryType, if NetMessageType == Library] + +[16 bits sequence number, if NetMessageType >= UserSequenced] + +8/16 bits - Payload length in bits (variable size ushort) + +[16 bits fragments group id, if fragmented] +[16 bits fragments total count, if fragmented] +[16 bits fragment number, if fragmented] + +[x - Payload] if length > 0 + diff --git a/Lidgren.Network/Documentation/SimulatingBadNetwork.html b/Lidgren.Network/Documentation/SimulatingBadNetwork.html new file mode 100644 index 000000000..80c6a186b --- /dev/null +++ b/Lidgren.Network/Documentation/SimulatingBadNetwork.html @@ -0,0 +1,115 @@ + + + + + Lidgren tutorial + + + + + + +
+

Simulating bad network conditions

+

+ On the internet, your packets are likely to run in to all kinds of trouble. They will be delayed and lost and they might even arrive multiple times at the destination. Lidgren has a few option to simulate how your application or game will react when this happens.
+ They are all configured using the NetPeerConfiguration class - these properties exists:

+

+

+ + + + + + + + + + + + + + + + + + + + + + +
+ SimulatedLoss + +   + + This is a float which simulates lost packets. A value of 0 will disable this feature, a value of 0.5f will make half of your sent packets disappear, chosen randomly. Note that packets may contain several messages - this is the amount of packets lost. +
+   +
+ SimulatedDuplicatesChance + +   + + This is a float which determines the chance that a packet will be duplicated at the destination. 0 means no packets will be duplicated, 0.5f means that on average, every other packet will be duplicated. +
+   +
+ SimulatedMinimumLatency
+ SimulatedRandomLatency +
+   + + These two properties control simulating delay of packets in seconds (not milliseconds, use 0.05 for 50 ms of lag). They work on top of the actual network delay and the total delay will be:
+ Actual one way latency + SimulatedMinimumLatency + [Randomly per packet 0 to SimulatedRandomLatency seconds] +
+ +
+

It's recommended to assume symmetric condtions and configure server and client with the same simulation settings.

+

Simulating bad network conditions only works in DEBUG builds.

+ +
+ + diff --git a/Lidgren.Network/Documentation/TODO.txt b/Lidgren.Network/Documentation/TODO.txt new file mode 100644 index 000000000..e8a7597f5 --- /dev/null +++ b/Lidgren.Network/Documentation/TODO.txt @@ -0,0 +1,17 @@ + +Completed features: +* Message coalescing +* Peer, connection statistics +* Lag, loss and duplication simulation for testing +* Connection approval +* Throttling +* Clock synchronization to detect jitter per packet (NetTime.RemoteNow) +* Peer discovery +* Message fragmentation + +Missing features: +* Receipts 25% done, need design +* More realistic lag/loss (lumpy) +* Detect estimated packet loss +* More advanced ack packet + diff --git a/Lidgren.Network/Documentation/Tutorial.html b/Lidgren.Network/Documentation/Tutorial.html new file mode 100644 index 000000000..af1c7d7a7 --- /dev/null +++ b/Lidgren.Network/Documentation/Tutorial.html @@ -0,0 +1,206 @@ + + + + + Lidgren basics tutorial + + + + + + +
+

+ Lidgren basics

+

+ Lidgren network library is all about messages. There are two types of messages:

+
  • Library messages telling you things like a peer has connected or diagnostics messages (warnings, errors) when unexpected things happen.
  • +
  • Data messages which is data sent from a remote (connected or unconnected) peer.
  • +

    + The base class for establishing connections, receiving and sending message are the NetPeer class. Using it you can make a peer-to-peer network, but if you are creating a server/client topology there are special classes called NetServer and NetClient. They inherit NetPeer but sets some defaults and includes some helper methods/properties.

    +

    + Here's how to set up a NetServer:

    +
    +
    NetPeerConfiguration config = new NetPeerConfiguration("MyExampleName");
    +
    config.Port = 14242;
    +
     
    +
    NetServer server = new NetServer(config);
    +
    server.Start();
    +
    +

    + The code above first creates a configuration. It has lots of properties you can change, but the default values should be pretty good for most applications. The string you provide in the constructor (MyExampleName) is an identifier to distinquish it from other applications using the lidgren library. Just make sure you use the same string in both server and client - or you will be unable to communicate between them.

    +

    + Secondly we've set the local port the server should listen to. This is the port number we tell the client(s) what port number to connect to. The local port can be set for a client too, but it's not needed and not recommended.

    +

    + Thirdly we create our server object and fourth we Start() it. Starting the server will create a new network thread and bind to a socket and start listening for connections.

    +

    + Early on we spoke about messages; now is the time to start receiving and sending some. Here's a code snippet for receiving messages:

    +
    +
    NetIncomingMessage msg;
    +
    while ((msg = server.ReadMessage()) != null)
    +
    {
    +
        switch (msg.MessageType)
    +
        {
    +
            case NetIncomingMessageType.VerboseDebugMessage:
    +
            case NetIncomingMessageType.DebugMessage:
    +
            case NetIncomingMessageType.WarningMessage:
    +
            case NetIncomingMessageType.ErrorMessage:
    +
                Console.WriteLine(msg.ReadString());
    +
                break;
    +
            default:
    +
                Console.WriteLine("Unhandled type: " + msg.MessageType);
    +
                break;
    +
        }
    +
        server.Recycle(msg);
    +
    }
    +
    +

    + So, lets dissect the above code. First we declare a NetIncomingMessage, which is the type of incoming messages. Then we read a message and handles it, looping back as long as there are messages to fetch. For each message we find, we switch on sometime called MessageType - it's a description what the message contains. In this code example we only catch messages of type VerboseDebugMessage, DebugMessage, WarningMessage and ErrorMessage. All those four types are emitted by the library to inform about various events. They all contains a single string, so we use the method ReadString() to extract a copy of that string and print it in the console.

    +

    + Reading data will increment the internal message pointer so you can read subsequent data using the Read*() methods.

    +

    + For all other message type we just print that it's currently unhandled.

    +

    + Finally, we recycle the message after we're done with it - this will enable the library to reuse the object and create less garbage.

    +

    + Sending messages are even easier:

    +
    +
    NetOutgoingMessage sendMsg = server.CreateMessage();
    +
    sendMsg.Write("Hello");
    +
    sendMsg.Write(42);
    +
     
    +
    server.SendMessage(sendMsg, recipient, NetDeliveryMethod.ReliableOrdered);
    +
    +

    + The above code first creates a new message, or uses a recycled message, which is why it's not possible to just create a message using new(). It then writes a string ("Hello") and an integer (System.Int32, 4 bytes in size) to the message.

    +

    + Then the message is sent using the SendMessage() method. The first argument is the message to send, the second argument is the recipient connection - which we'll not go into detail about just yet - and the third argument are HOW to deliver the message, or rather how to behave if network conditions are bad and a packet gets lost, duplicated or reordered.

    +

    + There are five delivery methods available:

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Unreliable + +   + + This is just UDP. Messages can be lost, received more than once and messages sent after other messages may be received before them. +
    +   +
    + UnreliableSequenced + +   + + Using this delivery method messages can still be lost; but you're protected against duplicated messages and if a message arrives late; that is, if a message sent after this one has already been received - it will be dropped. This means you will never receive "older" data than what you already have received. +
    +   +
    + ReliableUnordered + +   + + This delivery method ensures that every message sent will be received eventually. It does not however guarantee what order they will be received; late messages may be delivered before older ones. +
    +   +
    + ReliableSequenced + +   + + This delivery method is similar to UnreliableSequenced; except that is guarantees that SOME messages will be received - if you only send one message - it will be received. If you sent two messages quickly, and they get reordered in transit, only the newest message will be received - but at least ONE of them will be received guaranteed. +
    +   +
    ReliableOrdered  + This delivery method guarantees that messages will always be received in the exact order they were sent. +
    +
    +

    + Here's how to read and decode the message above:

    +
    +
    NetIncomingMessage incMsg = server.ReadMessage();
    +
    string str = incMsg.ReadString();
    +
    int a = incMsg.ReadInt32();
    +
    +
    + + diff --git a/Lidgren.Network/Encryption/INetEncryption.cs b/Lidgren.Network/Encryption/INetEncryption.cs new file mode 100644 index 000000000..f82ebcf43 --- /dev/null +++ b/Lidgren.Network/Encryption/INetEncryption.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; + +namespace Lidgren.Network +{ + /// + /// Interface for an encryption algorithm + /// + public interface INetEncryption + { + /// + /// Encrypt an outgoing message in place + /// + bool Encrypt(NetOutgoingMessage msg); + + /// + /// Decrypt an incoming message in place + /// + bool Decrypt(NetIncomingMessage msg); + } +} diff --git a/Lidgren.Network/Encryption/NetAESEncryption.cs b/Lidgren.Network/Encryption/NetAESEncryption.cs new file mode 100644 index 000000000..c8c4854a5 --- /dev/null +++ b/Lidgren.Network/Encryption/NetAESEncryption.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace Lidgren.Network +{ + /// + /// AES encryption + /// + public class NetAESEncryption : INetEncryption + { + private readonly byte[] m_key; + private readonly byte[] m_iv; + private readonly int m_bitSize; + private static readonly List m_keysizes; + private static readonly List m_blocksizes; + + static NetAESEncryption() + { +#if !IOS && !__ANDROID__ + AesCryptoServiceProvider aes = new AesCryptoServiceProvider(); + List temp = new List(); + foreach (KeySizes keysize in aes.LegalKeySizes) + { + for (int i = keysize.MinSize; i <= keysize.MaxSize; i += keysize.SkipSize) + { + if (!temp.Contains(i)) + temp.Add(i); + if (i == keysize.MaxSize) + break; + } + } + m_keysizes = temp; + temp = new List(); + foreach (KeySizes keysize in aes.LegalBlockSizes) + { + for (int i = keysize.MinSize; i <= keysize.MaxSize; i += keysize.SkipSize) + { + + if (!temp.Contains(i)) + temp.Add(i); + if (i == keysize.MaxSize) + break; + } + } + m_blocksizes = temp; +#endif + } + + /// + /// NetAESEncryption constructor + /// + public NetAESEncryption(byte[] key, byte[] iv) + { + if (!m_keysizes.Contains(key.Length * 8)) + throw new NetException(string.Format("Not a valid key size. (Valid values are: {0})", NetUtility.MakeCommaDelimitedList(m_keysizes))); + + if (!m_blocksizes.Contains(iv.Length * 8)) + throw new NetException(string.Format("Not a valid iv size. (Valid values are: {0})", NetUtility.MakeCommaDelimitedList(m_blocksizes))); + + m_key = key; + m_iv = iv; + m_bitSize = m_key.Length * 8; + } + + /// + /// NetAESEncryption constructor + /// + public NetAESEncryption(string key, int bitsize) + { + if (!m_keysizes.Contains(bitsize)) + throw new NetException(string.Format("Not a valid key size. (Valid values are: {0})", NetUtility.MakeCommaDelimitedList(m_keysizes))); + + byte[] entropy = Encoding.UTF32.GetBytes(key); + // I know hardcoding salts is bad, but in this case I think it is acceptable. + HMACSHA512 hmacsha512 = new HMACSHA512(Convert.FromBase64String("i88NEiez3c50bHqr3YGasDc4p8jRrxJAaiRiqixpvp4XNAStP5YNoC2fXnWkURtkha6M8yY901Gj07IRVIRyGL==")); + hmacsha512.Initialize(); + for (int i = 0; i < 1000; i++) + { + entropy = hmacsha512.ComputeHash(entropy); + } + int keylen = bitsize / 8; + m_key = new byte[keylen]; + Buffer.BlockCopy(entropy, 0, m_key, 0, keylen); + m_iv = new byte[m_blocksizes[0] / 8]; + + Buffer.BlockCopy(entropy, entropy.Length - m_iv.Length - 1, m_iv, 0, m_iv.Length); + m_bitSize = bitsize; + } + + /// + /// NetAESEncryption constructor + /// + public NetAESEncryption(string key) + : this(key, m_keysizes[0]) + { + } + + /// + /// Encrypt outgoing message + /// + public bool Encrypt(NetOutgoingMessage msg) + { +#if !IOS && !__ANDROID__ + try + { + // nested usings are fun! + using (AesCryptoServiceProvider aesCryptoServiceProvider = new AesCryptoServiceProvider { KeySize = m_bitSize, Mode = CipherMode.CBC }) + { + using (ICryptoTransform cryptoTransform = aesCryptoServiceProvider.CreateEncryptor(m_key, m_iv)) + { + using (MemoryStream memoryStream = new MemoryStream()) + { + using (CryptoStream cryptoStream = new CryptoStream(memoryStream, cryptoTransform, CryptoStreamMode.Write)) + { + cryptoStream.Write(msg.m_data, 0, msg.m_data.Length); + } + msg.m_data = memoryStream.ToArray(); + } + } + } + + } + catch + { + return false; + } + return true; +#else + return false; +#endif + } + + /// + /// Decrypt incoming message + /// + public bool Decrypt(NetIncomingMessage msg) + { +#if !IOS && !__ANDROID__ + try + { + // nested usings are fun! + using (AesCryptoServiceProvider aesCryptoServiceProvider = new AesCryptoServiceProvider { KeySize = m_bitSize, Mode = CipherMode.CBC }) + { + using (ICryptoTransform cryptoTransform = aesCryptoServiceProvider.CreateDecryptor(m_key, m_iv)) + { + using (MemoryStream memoryStream = new MemoryStream()) + { + using (CryptoStream cryptoStream = new CryptoStream(memoryStream, cryptoTransform, + CryptoStreamMode.Write)) + { + cryptoStream.Write(msg.m_data, 0, msg.m_data.Length); + } + msg.m_data = memoryStream.ToArray(); + } + } + } + + } + catch + { + return false; + } + return true; +#else + return false; +#endif + } + } +} diff --git a/Lidgren.Network/Encryption/NetBlockEncryptionBase.cs b/Lidgren.Network/Encryption/NetBlockEncryptionBase.cs new file mode 100644 index 000000000..ed68d1723 --- /dev/null +++ b/Lidgren.Network/Encryption/NetBlockEncryptionBase.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; + +namespace Lidgren.Network +{ + /// + /// Base for a non-threadsafe encryption class + /// + public abstract class NetBlockEncryptionBase : INetEncryption + { + // temporary space for one block to avoid reallocating every time + private byte[] m_tmp; + + /// + /// Block size in bytes for this cipher + /// + public abstract int BlockSize { get; } + + /// + /// NetBlockEncryptionBase constructor + /// + public NetBlockEncryptionBase() + { + m_tmp = new byte[BlockSize]; + } + + /// + /// Encrypt am outgoing message with this algorithm; no writing can be done to the message after encryption, or message will be corrupted + /// + public bool Encrypt(NetOutgoingMessage msg) + { + int payloadBitLength = msg.LengthBits; + int numBytes = msg.LengthBytes; + int blockSize = BlockSize; + int numBlocks = (int)Math.Ceiling((double)numBytes / (double)blockSize); + int dstSize = numBlocks * blockSize; + + msg.EnsureBufferSize(dstSize * 8 + (4 * 8)); // add 4 bytes for payload length at end + msg.LengthBits = dstSize * 8; // length will automatically adjust +4 bytes when payload length is written + + for(int i=0;i + /// Decrypt an incoming message encrypted with corresponding Encrypt + /// + /// message to decrypt + /// true if successful; false if failed + public bool Decrypt(NetIncomingMessage msg) + { + int numEncryptedBytes = msg.LengthBytes - 4; // last 4 bytes is true bit length + int blockSize = BlockSize; + int numBlocks = numEncryptedBytes / blockSize; + if (numBlocks * blockSize != numEncryptedBytes) + return false; + + for (int i = 0; i < numBlocks; i++) + { + DecryptBlock(msg.m_data, (i * blockSize), m_tmp); + Buffer.BlockCopy(m_tmp, 0, msg.m_data, (i * blockSize), m_tmp.Length); + } + + // read 32 bits of true payload length + uint realSize = NetBitWriter.ReadUInt32(msg.m_data, 32, (numEncryptedBytes * 8)); + msg.m_bitLength = (int)realSize; + return true; + } + + /// + /// Encrypt a block of bytes + /// + protected abstract void EncryptBlock(byte[] source, int sourceOffset, byte[] destination); + + /// + /// Decrypt a block of bytes + /// + protected abstract void DecryptBlock(byte[] source, int sourceOffset, byte[] destination); + } +} diff --git a/Lidgren.Network/Encryption/NetDESEncryption.cs b/Lidgren.Network/Encryption/NetDESEncryption.cs new file mode 100644 index 000000000..02fe040e0 --- /dev/null +++ b/Lidgren.Network/Encryption/NetDESEncryption.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace Lidgren.Network +{ + /// + /// DES encryption + /// + public class NetDESEncryption : INetEncryption + { + private readonly byte[] m_key; + private readonly byte[] m_iv; + private readonly int m_bitSize; + private static readonly List m_keysizes; + private static readonly List m_blocksizes; + + static NetDESEncryption() + { + + DESCryptoServiceProvider des = new DESCryptoServiceProvider(); + List temp = new List(); + foreach (KeySizes keysize in des.LegalKeySizes) + { + for (int i = keysize.MinSize; i <= keysize.MaxSize; i += keysize.SkipSize) + { + if (!temp.Contains(i)) + temp.Add(i); + if (i == keysize.MaxSize) + break; + } + } + m_keysizes = temp; + temp = new List(); + foreach (KeySizes keysize in des.LegalBlockSizes) + { + for (int i = keysize.MinSize; i <= keysize.MaxSize; i += keysize.SkipSize) + { + + if (!temp.Contains(i)) + temp.Add(i); + if (i == keysize.MaxSize) + break; + } + } + m_blocksizes = temp; + } + + /// + /// NetDESEncryption constructor + /// + public NetDESEncryption(byte[] key, byte[] iv) + { + if (!m_keysizes.Contains(key.Length * 8)) + throw new NetException(string.Format("Not a valid key size. (Valid values are: {0})", NetUtility.MakeCommaDelimitedList(m_keysizes))); + + if (!m_blocksizes.Contains(iv.Length * 8)) + throw new NetException(string.Format("Not a valid iv size. (Valid values are: {0})", NetUtility.MakeCommaDelimitedList(m_blocksizes))); + + m_key = key; + m_iv = iv; + m_bitSize = m_key.Length * 8; + } + + /// + /// NetDESEncryption constructor + /// + public NetDESEncryption(string key, int bitsize) + { + if (!m_keysizes.Contains(bitsize)) + throw new NetException(string.Format("Not a valid key size. (Valid values are: {0})", NetUtility.MakeCommaDelimitedList(m_keysizes))); + + byte[] entropy = Encoding.UTF32.GetBytes(key); + // I know hardcoding salts is bad, but in this case I think it is acceptable. + HMACSHA512 hmacsha512 = new HMACSHA512(Convert.FromBase64String("i88NEiez3c50bHqr3YGasDc4p8jRrxJAaiRiqixpvp4XNAStP5YNoC2fXnWkURtkha6M8yY901Gj07IRVIRyGL==")); + hmacsha512.Initialize(); + for (int i = 0; i < 1000; i++) + { + entropy = hmacsha512.ComputeHash(entropy); + } + int keylen = bitsize / 8; + m_key = new byte[keylen]; + Buffer.BlockCopy(entropy, 0, m_key, 0, keylen); + m_iv = new byte[m_blocksizes[0] / 8]; + + Buffer.BlockCopy(entropy, entropy.Length - m_iv.Length - 1, m_iv, 0, m_iv.Length); + m_bitSize = bitsize; + } + + /// + /// NetDESEncryption constructor + /// + public NetDESEncryption(string key) + : this(key, m_keysizes[0]) + { + } + + /// + /// Encrypt outgoing message + /// + public bool Encrypt(NetOutgoingMessage msg) + { + try + { + // nested usings are fun! + using (DESCryptoServiceProvider desCryptoServiceProvider = new DESCryptoServiceProvider { KeySize = m_bitSize, Mode = CipherMode.CBC }) + { + using (ICryptoTransform cryptoTransform = desCryptoServiceProvider.CreateEncryptor(m_key, m_iv)) + { + using (MemoryStream memoryStream = new MemoryStream()) + { + using (CryptoStream cryptoStream = new CryptoStream(memoryStream, cryptoTransform, + CryptoStreamMode.Write)) + { + cryptoStream.Write(msg.m_data, 0, msg.m_data.Length); + } + msg.m_data = memoryStream.ToArray(); + } + } + } + + } + catch + { + return false; + } + return true; + } + + /// + /// Decrypt incoming message + /// + public bool Decrypt(NetIncomingMessage msg) + { + try + { + // nested usings are fun! + using (DESCryptoServiceProvider desCryptoServiceProvider = new DESCryptoServiceProvider { KeySize = m_bitSize, Mode = CipherMode.CBC }) + { + using (ICryptoTransform cryptoTransform = desCryptoServiceProvider.CreateDecryptor(m_key, m_iv)) + { + using (MemoryStream memoryStream = new MemoryStream()) + { + using (CryptoStream cryptoStream = new CryptoStream(memoryStream, cryptoTransform, + CryptoStreamMode.Write)) + { + cryptoStream.Write(msg.m_data, 0, msg.m_data.Length); + } + msg.m_data = memoryStream.ToArray(); + } + } + } + + } + catch + { + return false; + } + return true; + } + } +} \ No newline at end of file diff --git a/Lidgren.Network/Encryption/NetRC2Encryption.cs b/Lidgren.Network/Encryption/NetRC2Encryption.cs new file mode 100644 index 000000000..7151ca201 --- /dev/null +++ b/Lidgren.Network/Encryption/NetRC2Encryption.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace Lidgren.Network +{ + /// + /// RC2 encryption + /// + public class NetRC2Encryption : INetEncryption + { + private readonly byte[] m_key; + private readonly byte[] m_iv; + private readonly int m_bitSize; + private static readonly List m_keysizes; + private static readonly List m_blocksizes; + + static NetRC2Encryption() + { + + RC2CryptoServiceProvider rc2 = new RC2CryptoServiceProvider(); + List temp = new List(); + foreach (KeySizes keysize in rc2.LegalKeySizes) + { + for (int i = keysize.MinSize; i <= keysize.MaxSize; i += keysize.SkipSize) + { + if (!temp.Contains(i)) + temp.Add(i); + if (i == keysize.MaxSize) + break; + } + } + m_keysizes = temp; + temp = new List(); + foreach (KeySizes keysize in rc2.LegalBlockSizes) + { + for (int i = keysize.MinSize; i <= keysize.MaxSize; i += keysize.SkipSize) + { + + if (!temp.Contains(i)) + temp.Add(i); + if (i == keysize.MaxSize) + break; + } + } + m_blocksizes = temp; + } + + /// + /// NetRC2Encryption constructor + /// + public NetRC2Encryption(byte[] key, byte[] iv) + { + if (!m_keysizes.Contains(key.Length * 8)) + throw new NetException(string.Format("Not a valid key size. (Valid values are: {0})", NetUtility.MakeCommaDelimitedList(m_keysizes))); + + if (!m_blocksizes.Contains(iv.Length * 8)) + throw new NetException(string.Format("Not a valid iv size. (Valid values are: {0})", NetUtility.MakeCommaDelimitedList(m_blocksizes))); + + m_key = key; + m_iv = iv; + m_bitSize = m_key.Length * 8; + } + + /// + /// NetRC2Encryption constructor + /// + public NetRC2Encryption(string key, int bitsize) + { + if (!m_keysizes.Contains(bitsize)) + throw new NetException(string.Format("Not a valid key size. (Valid values are: {0})", NetUtility.MakeCommaDelimitedList(m_keysizes))); + + byte[] entropy = Encoding.UTF32.GetBytes(key); + // I know hardcoding salts is bad, but in this case I think it is acceptable. + HMACSHA512 hmacsha512 = new HMACSHA512(Convert.FromBase64String("i88NEiez3c50bHqr3YGasDc4p8jRrxJAaiRiqixpvp4XNAStP5YNoC2fXnWkURtkha6M8yY901Gj07IRVIRyGL==")); + hmacsha512.Initialize(); + for (int i = 0; i < 1000; i++) + { + entropy = hmacsha512.ComputeHash(entropy); + } + int keylen = bitsize / 8; + m_key = new byte[keylen]; + Buffer.BlockCopy(entropy, 0, m_key, 0, keylen); + m_iv = new byte[m_blocksizes[0] / 8]; + + Buffer.BlockCopy(entropy, entropy.Length - m_iv.Length - 1, m_iv, 0, m_iv.Length); + m_bitSize = bitsize; + } + + /// + /// NetRC2Encryption constructor + /// + /// + public NetRC2Encryption(string key) + : this(key, m_keysizes[0]) + { + } + + /// + /// Encrypt outgoing message + /// + public bool Encrypt(NetOutgoingMessage msg) + { + try + { + // nested usings are fun! + using (RC2CryptoServiceProvider rc2CryptoServiceProvider = new RC2CryptoServiceProvider { KeySize = m_bitSize, Mode = CipherMode.CBC }) + { + using (ICryptoTransform cryptoTransform = rc2CryptoServiceProvider.CreateEncryptor(m_key, m_iv)) + { + using (MemoryStream memoryStream = new MemoryStream()) + { + using (CryptoStream cryptoStream = new CryptoStream(memoryStream, cryptoTransform, + CryptoStreamMode.Write)) + { + cryptoStream.Write(msg.m_data, 0, msg.m_data.Length); + } + msg.m_data = memoryStream.ToArray(); + } + } + } + + } + catch + { + return false; + } + return true; + } + + /// + /// Decrypt incoming message + /// + public bool Decrypt(NetIncomingMessage msg) + { + try + { + // nested usings are fun! + using (RC2CryptoServiceProvider rc2CryptoServiceProvider = new RC2CryptoServiceProvider { KeySize = m_bitSize, Mode = CipherMode.CBC }) + { + using (ICryptoTransform cryptoTransform = rc2CryptoServiceProvider.CreateDecryptor(m_key, m_iv)) + { + using (MemoryStream memoryStream = new MemoryStream()) + { + using (CryptoStream cryptoStream = new CryptoStream(memoryStream, cryptoTransform, + CryptoStreamMode.Write)) + { + cryptoStream.Write(msg.m_data, 0, msg.m_data.Length); + } + msg.m_data = memoryStream.ToArray(); + } + } + } + + } + catch + { + return false; + } + return true; + } + } +} \ No newline at end of file diff --git a/Lidgren.Network/Encryption/NetTripleDESEncryption.cs b/Lidgren.Network/Encryption/NetTripleDESEncryption.cs new file mode 100644 index 000000000..5495e0721 --- /dev/null +++ b/Lidgren.Network/Encryption/NetTripleDESEncryption.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace Lidgren.Network +{ + /// + /// Triple DES encryption + /// + public class NetTripleDESEncryption : INetEncryption + { + private readonly byte[] m_key; + private readonly byte[] m_iv; + private readonly int m_bitSize; + private static readonly List m_keysizes; + private static readonly List m_blocksizes; + + static NetTripleDESEncryption() + { + + TripleDESCryptoServiceProvider tripleDES = new TripleDESCryptoServiceProvider(); + List temp = new List(); + foreach (KeySizes keysize in tripleDES.LegalKeySizes) + { + for (int i = keysize.MinSize; i <= keysize.MaxSize; i += keysize.SkipSize) + { + if (!temp.Contains(i)) + temp.Add(i); + if (i == keysize.MaxSize) + break; + } + } + m_keysizes = temp; + temp = new List(); + foreach (KeySizes keysize in tripleDES.LegalBlockSizes) + { + for (int i = keysize.MinSize; i <= keysize.MaxSize; i += keysize.SkipSize) + { + + if (!temp.Contains(i)) + temp.Add(i); + if (i == keysize.MaxSize) + break; + } + } + m_blocksizes = temp; + } + + /// + /// NetTriplsDESEncryption constructor + /// + public NetTripleDESEncryption(byte[] key, byte[] iv) + { + if (!m_keysizes.Contains(key.Length * 8)) + throw new NetException(string.Format("Not a valid key size. (Valid values are: {0})", NetUtility.MakeCommaDelimitedList(m_keysizes))); + + if (!m_blocksizes.Contains(iv.Length * 8)) + throw new NetException(string.Format("Not a valid iv size. (Valid values are: {0})", NetUtility.MakeCommaDelimitedList(m_blocksizes))); + + m_key = key; + m_iv = iv; + m_bitSize = m_key.Length * 8; + } + + /// + /// NetTriplsDESEncryption constructor + /// + public NetTripleDESEncryption(string key, int bitsize) + { + if (!m_keysizes.Contains(bitsize)) + throw new NetException(string.Format("Not a valid key size. (Valid values are: {0})", NetUtility.MakeCommaDelimitedList(m_keysizes))); + + byte[] entropy = Encoding.UTF32.GetBytes(key); + // I know hardcoding salts is bad, but in this case I think it is acceptable. + HMACSHA512 hmacsha512 = new HMACSHA512(Convert.FromBase64String("i88NEiez3c50bHqr3YGasDc4p8jRrxJAaiRiqixpvp4XNAStP5YNoC2fXnWkURtkha6M8yY901Gj07IRVIRyGL==")); + hmacsha512.Initialize(); + for (int i = 0; i < 1000; i++) + { + entropy = hmacsha512.ComputeHash(entropy); + } + int keylen = bitsize / 8; + m_key = new byte[keylen]; + Buffer.BlockCopy(entropy, 0, m_key, 0, keylen); + m_iv = new byte[m_blocksizes[0] / 8]; + + Buffer.BlockCopy(entropy, entropy.Length - m_iv.Length - 1, m_iv, 0, m_iv.Length); + m_bitSize = bitsize; + } + + /// + /// NetTriplsDESEncryption constructor + /// + public NetTripleDESEncryption(string key) + : this(key, m_keysizes[0]) + { + } + + /// + /// Encrypt outgoing message + /// + public bool Encrypt(NetOutgoingMessage msg) + { + try + { + // nested usings are fun! + using (TripleDESCryptoServiceProvider tripleDESCryptoServiceProvider = new TripleDESCryptoServiceProvider { KeySize = m_bitSize, Mode = CipherMode.CBC }) + { + using (ICryptoTransform cryptoTransform = tripleDESCryptoServiceProvider.CreateEncryptor(m_key, m_iv)) + { + using (MemoryStream memoryStream = new MemoryStream()) + { + using (CryptoStream cryptoStream = new CryptoStream(memoryStream, cryptoTransform, + CryptoStreamMode.Write)) + { + cryptoStream.Write(msg.m_data, 0, msg.m_data.Length); + } + msg.m_data = memoryStream.ToArray(); + } + } + } + + } + catch + { + return false; + } + return true; + } + + /// + /// Decrypt incoming message + /// + public bool Decrypt(NetIncomingMessage msg) + { + try + { + // nested usings are fun! + using (TripleDESCryptoServiceProvider tripleDESCryptoServiceProvider = new TripleDESCryptoServiceProvider { KeySize = m_bitSize, Mode = CipherMode.CBC }) + { + using (ICryptoTransform cryptoTransform = tripleDESCryptoServiceProvider.CreateDecryptor(m_key, m_iv)) + { + using (MemoryStream memoryStream = new MemoryStream()) + { + using (CryptoStream cryptoStream = new CryptoStream(memoryStream, cryptoTransform, + CryptoStreamMode.Write)) + { + cryptoStream.Write(msg.m_data, 0, msg.m_data.Length); + } + msg.m_data = memoryStream.ToArray(); + } + } + } + + } + catch + { + return false; + } + return true; + } + } +} \ No newline at end of file diff --git a/Lidgren.Network/Encryption/NetXorEncryption.cs b/Lidgren.Network/Encryption/NetXorEncryption.cs new file mode 100644 index 000000000..e9c3d5868 --- /dev/null +++ b/Lidgren.Network/Encryption/NetXorEncryption.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Lidgren.Network +{ + /// + /// Example class; not very good encryption + /// + public class NetXorEncryption : INetEncryption + { + private byte[] m_key; + + /// + /// NetXorEncryption constructor + /// + public NetXorEncryption(byte[] key) + { + m_key = key; + } + + /// + /// NetXorEncryption constructor + /// + public NetXorEncryption(string key) + { + m_key = Encoding.UTF8.GetBytes(key); + } + + /// + /// Encrypt an outgoing message + /// + public bool Encrypt(NetOutgoingMessage msg) + { + int numBytes = msg.LengthBytes; + for (int i = 0; i < numBytes; i++) + { + int offset = i % m_key.Length; + msg.m_data[i] = (byte)(msg.m_data[i] ^ m_key[offset]); + } + return true; + } + + /// + /// Decrypt an incoming message + /// + public bool Decrypt(NetIncomingMessage msg) + { + int numBytes = msg.LengthBytes; + for (int i = 0; i < numBytes; i++) + { + int offset = i % m_key.Length; + msg.m_data[i] = (byte)(msg.m_data[i] ^ m_key[offset]); + } + return true; + } + } +} diff --git a/Lidgren.Network/Encryption/NetXteaEncryption.cs b/Lidgren.Network/Encryption/NetXteaEncryption.cs new file mode 100644 index 000000000..258c7cf02 --- /dev/null +++ b/Lidgren.Network/Encryption/NetXteaEncryption.cs @@ -0,0 +1,146 @@ +/* Copyright (c) 2010 Michael Lidgren + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom +the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +using System; +using System.Security.Cryptography; +using System.Text; +using System.Security; + +namespace Lidgren.Network +{ + /// + /// Methods to encrypt and decrypt data using the XTEA algorithm + /// + public sealed class NetXtea : NetBlockEncryptionBase + { + private const int c_blockSize = 8; + private const int c_keySize = 16; + private const int c_delta = unchecked((int)0x9E3779B9); + + private readonly int m_numRounds; + private readonly uint[] m_sum0; + private readonly uint[] m_sum1; + + /// + /// Gets the block size for this cipher + /// + public override int BlockSize { get { return c_blockSize; } } + + /// + /// 16 byte key + /// + public NetXtea(byte[] key, int rounds) + { + if (key.Length < c_keySize) + throw new NetException("Key too short!"); + + m_numRounds = rounds; + m_sum0 = new uint[m_numRounds]; + m_sum1 = new uint[m_numRounds]; + uint[] tmp = new uint[8]; + + int num2; + int index = num2 = 0; + while (index < 4) + { + tmp[index] = BitConverter.ToUInt32(key, num2); + index++; + num2 += 4; + } + for (index = num2 = 0; index < 32; index++) + { + m_sum0[index] = ((uint)num2) + tmp[num2 & 3]; + num2 += -1640531527; + m_sum1[index] = ((uint)num2) + tmp[(num2 >> 11) & 3]; + } + } + + /// + /// 16 byte key + /// + public NetXtea(byte[] key) + : this(key, 32) + { + } + + /// + /// String to hash for key + /// + public NetXtea(string key) + : this(SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(key)), 32) + { + } + + /// + /// Encrypts a block of bytes + /// + protected override void EncryptBlock(byte[] source, int sourceOffset, byte[] destination) + { + uint v0 = BytesToUInt(source, sourceOffset); + uint v1 = BytesToUInt(source, sourceOffset + 4); + + for (int i = 0; i != m_numRounds; i++) + { + v0 += (((v1 << 4) ^ (v1 >> 5)) + v1) ^ m_sum0[i]; + v1 += (((v0 << 4) ^ (v0 >> 5)) + v0) ^ m_sum1[i]; + } + + UIntToBytes(v0, destination, 0); + UIntToBytes(v1, destination, 0 + 4); + + return; + } + + /// + /// Decrypts a block of bytes + /// + protected override void DecryptBlock(byte[] source, int sourceOffset, byte[] destination) + { + // Pack bytes into integers + uint v0 = BytesToUInt(source, sourceOffset); + uint v1 = BytesToUInt(source, sourceOffset + 4); + + for (int i = m_numRounds - 1; i >= 0; i--) + { + v1 -= (((v0 << 4) ^ (v0 >> 5)) + v0) ^ m_sum1[i]; + v0 -= (((v1 << 4) ^ (v1 >> 5)) + v1) ^ m_sum0[i]; + } + + UIntToBytes(v0, destination, 0); + UIntToBytes(v1, destination, 0 + 4); + + return; + } + + private static uint BytesToUInt(byte[] bytes, int offset) + { + uint retval = (uint)(bytes[offset] << 24); + retval |= (uint)(bytes[++offset] << 16); + retval |= (uint)(bytes[++offset] << 8); + return (retval | bytes[++offset]); + } + + private static void UIntToBytes(uint value, byte[] destination, int destinationOffset) + { + destination[destinationOffset++] = (byte)(value >> 24); + destination[destinationOffset++] = (byte)(value >> 16); + destination[destinationOffset++] = (byte)(value >> 8); + destination[destinationOffset++] = (byte)value; + } + } +} diff --git a/Lidgren.Network/Lidgren.Network.csproj b/Lidgren.Network/Lidgren.Network.csproj new file mode 100644 index 000000000..a854cb1ef --- /dev/null +++ b/Lidgren.Network/Lidgren.Network.csproj @@ -0,0 +1,165 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8} + Library + Properties + Lidgren.Network + Lidgren.Network + v3.5 + 512 + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + false + true + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + AllRules.ruleset + bin\Debug\Lidgren.Network.XML + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + AllRules.ruleset + + + true + + + Lidgren.snk + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + False + Microsoft .NET Framework 4 %28x86 and x64%29 + true + + + False + .NET Framework 3.5 SP1 Client Profile + false + + + False + .NET Framework 3.5 SP1 + false + + + False + Windows Installer 3.1 + true + + + + + + + + \ No newline at end of file diff --git a/Lidgren.Network/Lidgren.snk b/Lidgren.Network/Lidgren.snk new file mode 100644 index 0000000000000000000000000000000000000000..f057e2116f7eb7dc3163b06e99e0eccdbba56cae GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50096uGgqr*A}SK%YP`<*Ky3ZsEXy4SzXQRA zVZ9yCRWb;Fm4C%vCJAM{R|5=KzG0?T9+V}K4K!3PZF&);my47oCO!+nHf2u0dF88n z+LP77^;&g;8S$B`|D`K_N**wYRx4$tVjH|GD#qzg(aIcgwoQnf)l5CU1IgwXiJ_Z& ztdr;tnO!h?f14I5=(@`^g24~kY9fG(KIIXj_7I!JeuGZ|>bcqu%n|fU*X`ejdLQa2| zc6i?-!{LIO+P$!`KwzjY*F0nIC*fhMNNy4ManuFA?q0KXF@nS|Y4~wUU_N}HMncWd z#CQiAGm3~sW!m@rteg$+gw3y~kdFt?K@+~dXa^-AABXG0SDTphdZ5E+9_)5069t?Y z6}lb7#;=6iWw0kKAJW;ZTRFnvU^JP}Wi1@nW6ySk%WWgPq#fUL if7C~?^eDh0|L%Nu69!C#KvQsGHb7XTa$^H9x#vNc_Zoiy literal 0 HcmV?d00001 diff --git a/Lidgren.Network/NamespaceDoc.cs b/Lidgren.Network/NamespaceDoc.cs new file mode 100644 index 000000000..b7d6db0f8 --- /dev/null +++ b/Lidgren.Network/NamespaceDoc.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Lidgren.Network +{ + /// + /// Lidgren Network Library + /// + internal class NamespaceDoc + { + // + } +} diff --git a/Lidgren.Network/NetBigInteger.cs b/Lidgren.Network/NetBigInteger.cs new file mode 100644 index 000000000..3f5c2dbf5 --- /dev/null +++ b/Lidgren.Network/NetBigInteger.cs @@ -0,0 +1,2337 @@ +using System; +using System.Text; +using System.Collections; +using System.Diagnostics; +using System.Globalization; + +namespace Lidgren.Network +{ + /// + /// Big integer class based on BouncyCastle (http://www.bouncycastle.org) big integer code + /// + internal class NetBigInteger + { + private const long IMASK = 0xffffffffL; + private const ulong UIMASK = (ulong)IMASK; + + private static readonly int[] ZeroMagnitude = new int[0]; + private static readonly byte[] ZeroEncoding = new byte[0]; + + public static readonly NetBigInteger Zero = new NetBigInteger(0, ZeroMagnitude, false); + public static readonly NetBigInteger One = createUValueOf(1); + public static readonly NetBigInteger Two = createUValueOf(2); + public static readonly NetBigInteger Three = createUValueOf(3); + public static readonly NetBigInteger Ten = createUValueOf(10); + + private const int chunk2 = 1; + private static readonly NetBigInteger radix2 = ValueOf(2); + private static readonly NetBigInteger radix2E = radix2.Pow(chunk2); + + private const int chunk10 = 19; + private static readonly NetBigInteger radix10 = ValueOf(10); + private static readonly NetBigInteger radix10E = radix10.Pow(chunk10); + + private const int chunk16 = 16; + private static readonly NetBigInteger radix16 = ValueOf(16); + private static readonly NetBigInteger radix16E = radix16.Pow(chunk16); + + private const int BitsPerByte = 8; + private const int BitsPerInt = 32; + private const int BytesPerInt = 4; + + private int m_sign; // -1 means -ve; +1 means +ve; 0 means 0; + private int[] m_magnitude; // array of ints with [0] being the most significant + private int m_numBits = -1; // cache BitCount() value + private int m_numBitLength = -1; // cache calcBitLength() value + private long m_quote = -1L; // -m^(-1) mod b, b = 2^32 (see Montgomery mult.) + + private static int GetByteLength( + int nBits) + { + return (nBits + BitsPerByte - 1) / BitsPerByte; + } + + private NetBigInteger() + { + } + + private NetBigInteger( + int signum, + int[] mag, + bool checkMag) + { + if (checkMag) + { + int i = 0; + while (i < mag.Length && mag[i] == 0) + { + ++i; + } + + if (i == mag.Length) + { + // sign = 0; + m_magnitude = ZeroMagnitude; + } + else + { + m_sign = signum; + + if (i == 0) + { + m_magnitude = mag; + } + else + { + // strip leading 0 words + m_magnitude = new int[mag.Length - i]; + Array.Copy(mag, i, m_magnitude, 0, m_magnitude.Length); + } + } + } + else + { + m_sign = signum; + m_magnitude = mag; + } + } + + public NetBigInteger( + string value) + : this(value, 10) + { + } + + public NetBigInteger( + string str, + int radix) + { + if (str.Length == 0) + throw new FormatException("Zero length BigInteger"); + + NumberStyles style; + int chunk; + NetBigInteger r; + NetBigInteger rE; + + switch (radix) + { + case 2: + // Is there anyway to restrict to binary digits? + style = NumberStyles.Integer; + chunk = chunk2; + r = radix2; + rE = radix2E; + break; + case 10: + // This style seems to handle spaces and minus sign already (our processing redundant?) + style = NumberStyles.Integer; + chunk = chunk10; + r = radix10; + rE = radix10E; + break; + case 16: + // TODO Should this be HexNumber? + style = NumberStyles.AllowHexSpecifier; + chunk = chunk16; + r = radix16; + rE = radix16E; + break; + default: + throw new FormatException("Only bases 2, 10, or 16 allowed"); + } + + + int index = 0; + m_sign = 1; + + if (str[0] == '-') + { + if (str.Length == 1) + throw new FormatException("Zero length BigInteger"); + + m_sign = -1; + index = 1; + } + + // strip leading zeros from the string str + while (index < str.Length && Int32.Parse(str[index].ToString(), style) == 0) + { + index++; + } + + if (index >= str.Length) + { + // zero value - we're done + m_sign = 0; + m_magnitude = ZeroMagnitude; + return; + } + + ////// + // could we work out the max number of ints required to store + // str.Length digits in the given base, then allocate that + // storage in one hit?, then Generate the magnitude in one hit too? + ////// + + NetBigInteger b = Zero; + + + int next = index + chunk; + + if (next <= str.Length) + { + do + { + string s = str.Substring(index, chunk); + ulong i = ulong.Parse(s, style); + NetBigInteger bi = createUValueOf(i); + + switch (radix) + { + case 2: + if (i > 1) + throw new FormatException("Bad character in radix 2 string: " + s); + + b = b.ShiftLeft(1); + break; + case 16: + b = b.ShiftLeft(64); + break; + default: + b = b.Multiply(rE); + break; + } + + b = b.Add(bi); + + index = next; + next += chunk; + } + while (next <= str.Length); + } + + if (index < str.Length) + { + string s = str.Substring(index); + ulong i = ulong.Parse(s, style); + NetBigInteger bi = createUValueOf(i); + + if (b.m_sign > 0) + { + if (radix == 2) + { + // NB: Can't reach here since we are parsing one char at a time + Debug.Assert(false); + } + else if (radix == 16) + { + b = b.ShiftLeft(s.Length << 2); + } + else + { + b = b.Multiply(r.Pow(s.Length)); + } + + b = b.Add(bi); + } + else + { + b = bi; + } + } + + // Note: This is the previous (slower) algorithm + // while (index < value.Length) + // { + // char c = value[index]; + // string s = c.ToString(); + // int i = Int32.Parse(s, style); + // + // b = b.Multiply(r).Add(ValueOf(i)); + // index++; + // } + + m_magnitude = b.m_magnitude; + } + + public NetBigInteger( + byte[] bytes) + : this(bytes, 0, bytes.Length) + { + } + + public NetBigInteger( + byte[] bytes, + int offset, + int length) + { + if (length == 0) + throw new FormatException("Zero length BigInteger"); + if ((sbyte)bytes[offset] < 0) + { + m_sign = -1; + + int end = offset + length; + + int iBval; + // strip leading sign bytes + for (iBval = offset; iBval < end && ((sbyte)bytes[iBval] == -1); iBval++) + { + } + + if (iBval >= end) + { + m_magnitude = One.m_magnitude; + } + else + { + int numBytes = end - iBval; + byte[] inverse = new byte[numBytes]; + + int index = 0; + while (index < numBytes) + { + inverse[index++] = (byte)~bytes[iBval++]; + } + + Debug.Assert(iBval == end); + + while (inverse[--index] == byte.MaxValue) + { + inverse[index] = byte.MinValue; + } + + inverse[index]++; + + m_magnitude = MakeMagnitude(inverse, 0, inverse.Length); + } + } + else + { + // strip leading zero bytes and return magnitude bytes + m_magnitude = MakeMagnitude(bytes, offset, length); + m_sign = m_magnitude.Length > 0 ? 1 : 0; + } + } + + private static int[] MakeMagnitude( + byte[] bytes, + int offset, + int length) + { + int end = offset + length; + + // strip leading zeros + int firstSignificant; + for (firstSignificant = offset; firstSignificant < end + && bytes[firstSignificant] == 0; firstSignificant++) + { + } + + if (firstSignificant >= end) + { + return ZeroMagnitude; + } + + int nInts = (end - firstSignificant + 3) / BytesPerInt; + int bCount = (end - firstSignificant) % BytesPerInt; + if (bCount == 0) + { + bCount = BytesPerInt; + } + + if (nInts < 1) + { + return ZeroMagnitude; + } + + int[] mag = new int[nInts]; + + int v = 0; + int magnitudeIndex = 0; + for (int i = firstSignificant; i < end; ++i) + { + v <<= 8; + v |= bytes[i] & 0xff; + bCount--; + if (bCount <= 0) + { + mag[magnitudeIndex] = v; + magnitudeIndex++; + bCount = BytesPerInt; + v = 0; + } + } + + if (magnitudeIndex < mag.Length) + { + mag[magnitudeIndex] = v; + } + + return mag; + } + + public NetBigInteger( + int sign, + byte[] bytes) + : this(sign, bytes, 0, bytes.Length) + { + } + + public NetBigInteger( + int sign, + byte[] bytes, + int offset, + int length) + { + if (sign < -1 || sign > 1) + throw new FormatException("Invalid sign value"); + + if (sign == 0) + { + //sign = 0; + m_magnitude = ZeroMagnitude; + } + else + { + // copy bytes + m_magnitude = MakeMagnitude(bytes, offset, length); + m_sign = m_magnitude.Length < 1 ? 0 : sign; + } + } + + public NetBigInteger Abs() + { + return m_sign >= 0 ? this : Negate(); + } + + // return a = a + b - b preserved. + private static int[] AddMagnitudes( + int[] a, + int[] b) + { + int tI = a.Length - 1; + int vI = b.Length - 1; + long m = 0; + + while (vI >= 0) + { + m += ((long)(uint)a[tI] + (long)(uint)b[vI--]); + a[tI--] = (int)m; + m = (long)((ulong)m >> 32); + } + + if (m != 0) + { + while (tI >= 0 && ++a[tI--] == 0) + { + } + } + + return a; + } + + public NetBigInteger Add( + NetBigInteger value) + { + if (m_sign == 0) + return value; + + if (m_sign != value.m_sign) + { + if (value.m_sign == 0) + return this; + + if (value.m_sign < 0) + return Subtract(value.Negate()); + + return value.Subtract(Negate()); + } + + return AddToMagnitude(value.m_magnitude); + } + + private NetBigInteger AddToMagnitude( + int[] magToAdd) + { + int[] big, small; + if (m_magnitude.Length < magToAdd.Length) + { + big = magToAdd; + small = m_magnitude; + } + else + { + big = m_magnitude; + small = magToAdd; + } + + // Conservatively avoid over-allocation when no overflow possible + uint limit = uint.MaxValue; + if (big.Length == small.Length) + limit -= (uint)small[0]; + + bool possibleOverflow = (uint)big[0] >= limit; + + int[] bigCopy; + if (possibleOverflow) + { + bigCopy = new int[big.Length + 1]; + big.CopyTo(bigCopy, 1); + } + else + { + bigCopy = (int[])big.Clone(); + } + + bigCopy = AddMagnitudes(bigCopy, small); + + return new NetBigInteger(m_sign, bigCopy, possibleOverflow); + } + + public NetBigInteger And( + NetBigInteger value) + { + if (m_sign == 0 || value.m_sign == 0) + { + return Zero; + } + + int[] aMag = m_sign > 0 + ? m_magnitude + : Add(One).m_magnitude; + + int[] bMag = value.m_sign > 0 + ? value.m_magnitude + : value.Add(One).m_magnitude; + + bool resultNeg = m_sign < 0 && value.m_sign < 0; + int resultLength = System.Math.Max(aMag.Length, bMag.Length); + int[] resultMag = new int[resultLength]; + + int aStart = resultMag.Length - aMag.Length; + int bStart = resultMag.Length - bMag.Length; + + for (int i = 0; i < resultMag.Length; ++i) + { + int aWord = i >= aStart ? aMag[i - aStart] : 0; + int bWord = i >= bStart ? bMag[i - bStart] : 0; + + if (m_sign < 0) + { + aWord = ~aWord; + } + + if (value.m_sign < 0) + { + bWord = ~bWord; + } + + resultMag[i] = aWord & bWord; + + if (resultNeg) + { + resultMag[i] = ~resultMag[i]; + } + } + + NetBigInteger result = new NetBigInteger(1, resultMag, true); + + if (resultNeg) + { + result = result.Not(); + } + + return result; + } + + private int calcBitLength( + int indx, + int[] mag) + { + for (; ; ) + { + if (indx >= mag.Length) + return 0; + + if (mag[indx] != 0) + break; + + ++indx; + } + + // bit length for everything after the first int + int bitLength = 32 * ((mag.Length - indx) - 1); + + // and determine bitlength of first int + int firstMag = mag[indx]; + bitLength += BitLen(firstMag); + + // Check for negative powers of two + if (m_sign < 0 && ((firstMag & -firstMag) == firstMag)) + { + do + { + if (++indx >= mag.Length) + { + --bitLength; + break; + } + } + while (mag[indx] == 0); + } + + return bitLength; + } + + public int BitLength + { + get + { + if (m_numBitLength == -1) + { + m_numBitLength = m_sign == 0 + ? 0 + : calcBitLength(0, m_magnitude); + } + + return m_numBitLength; + } + } + + // + // BitLen(value) is the number of bits in value. + // + private static int BitLen( + int w) + { + // Binary search - decision tree (5 tests, rarely 6) + return (w < 1 << 15 ? (w < 1 << 7 + ? (w < 1 << 3 ? (w < 1 << 1 + ? (w < 1 << 0 ? (w < 0 ? 32 : 0) : 1) + : (w < 1 << 2 ? 2 : 3)) : (w < 1 << 5 + ? (w < 1 << 4 ? 4 : 5) + : (w < 1 << 6 ? 6 : 7))) + : (w < 1 << 11 + ? (w < 1 << 9 ? (w < 1 << 8 ? 8 : 9) : (w < 1 << 10 ? 10 : 11)) + : (w < 1 << 13 ? (w < 1 << 12 ? 12 : 13) : (w < 1 << 14 ? 14 : 15)))) : (w < 1 << 23 ? (w < 1 << 19 + ? (w < 1 << 17 ? (w < 1 << 16 ? 16 : 17) : (w < 1 << 18 ? 18 : 19)) + : (w < 1 << 21 ? (w < 1 << 20 ? 20 : 21) : (w < 1 << 22 ? 22 : 23))) : (w < 1 << 27 + ? (w < 1 << 25 ? (w < 1 << 24 ? 24 : 25) : (w < 1 << 26 ? 26 : 27)) + : (w < 1 << 29 ? (w < 1 << 28 ? 28 : 29) : (w < 1 << 30 ? 30 : 31))))); + } + + private bool QuickPow2Check() + { + return m_sign > 0 && m_numBits == 1; + } + + public int CompareTo( + object obj) + { + return CompareTo((NetBigInteger)obj); + } + + + // unsigned comparison on two arrays - note the arrays may + // start with leading zeros. + private static int CompareTo( + int xIndx, + int[] x, + int yIndx, + int[] y) + { + while (xIndx != x.Length && x[xIndx] == 0) + { + xIndx++; + } + + while (yIndx != y.Length && y[yIndx] == 0) + { + yIndx++; + } + + return CompareNoLeadingZeroes(xIndx, x, yIndx, y); + } + + private static int CompareNoLeadingZeroes( + int xIndx, + int[] x, + int yIndx, + int[] y) + { + int diff = (x.Length - y.Length) - (xIndx - yIndx); + + if (diff != 0) + { + return diff < 0 ? -1 : 1; + } + + // lengths of magnitudes the same, test the magnitude values + + while (xIndx < x.Length) + { + uint v1 = (uint)x[xIndx++]; + uint v2 = (uint)y[yIndx++]; + + if (v1 != v2) + return v1 < v2 ? -1 : 1; + } + + return 0; + } + + public int CompareTo( + NetBigInteger value) + { + return m_sign < value.m_sign ? -1 + : m_sign > value.m_sign ? 1 + : m_sign == 0 ? 0 + : m_sign * CompareNoLeadingZeroes(0, m_magnitude, 0, value.m_magnitude); + } + + // return z = x / y - done in place (z value preserved, x contains the remainder) + private int[] Divide( + int[] x, + int[] y) + { + int xStart = 0; + while (xStart < x.Length && x[xStart] == 0) + { + ++xStart; + } + + int yStart = 0; + while (yStart < y.Length && y[yStart] == 0) + { + ++yStart; + } + + Debug.Assert(yStart < y.Length); + + int xyCmp = CompareNoLeadingZeroes(xStart, x, yStart, y); + int[] count; + + if (xyCmp > 0) + { + int yBitLength = calcBitLength(yStart, y); + int xBitLength = calcBitLength(xStart, x); + int shift = xBitLength - yBitLength; + + int[] iCount; + int iCountStart = 0; + + int[] c; + int cStart = 0; + int cBitLength = yBitLength; + if (shift > 0) + { + // iCount = ShiftLeft(One.magnitude, shift); + iCount = new int[(shift >> 5) + 1]; + iCount[0] = 1 << (shift % 32); + + c = ShiftLeft(y, shift); + cBitLength += shift; + } + else + { + iCount = new int[] { 1 }; + + int len = y.Length - yStart; + c = new int[len]; + Array.Copy(y, yStart, c, 0, len); + } + + count = new int[iCount.Length]; + + for (; ; ) + { + if (cBitLength < xBitLength + || CompareNoLeadingZeroes(xStart, x, cStart, c) >= 0) + { + Subtract(xStart, x, cStart, c); + AddMagnitudes(count, iCount); + + while (x[xStart] == 0) + { + if (++xStart == x.Length) + return count; + } + + //xBitLength = calcBitLength(xStart, x); + xBitLength = 32 * (x.Length - xStart - 1) + BitLen(x[xStart]); + + if (xBitLength <= yBitLength) + { + if (xBitLength < yBitLength) + return count; + + xyCmp = CompareNoLeadingZeroes(xStart, x, yStart, y); + + if (xyCmp <= 0) + break; + } + } + + shift = cBitLength - xBitLength; + + // NB: The case where c[cStart] is 1-bit is harmless + if (shift == 1) + { + uint firstC = (uint)c[cStart] >> 1; + uint firstX = (uint)x[xStart]; + if (firstC > firstX) + ++shift; + } + + if (shift < 2) + { + c = ShiftRightOneInPlace(cStart, c); + --cBitLength; + iCount = ShiftRightOneInPlace(iCountStart, iCount); + } + else + { + c = ShiftRightInPlace(cStart, c, shift); + cBitLength -= shift; + iCount = ShiftRightInPlace(iCountStart, iCount, shift); + } + + //cStart = c.Length - ((cBitLength + 31) / 32); + while (c[cStart] == 0) + { + ++cStart; + } + + while (iCount[iCountStart] == 0) + { + ++iCountStart; + } + } + } + else + { + count = new int[1]; + } + + if (xyCmp == 0) + { + AddMagnitudes(count, One.m_magnitude); + Array.Clear(x, xStart, x.Length - xStart); + } + + return count; + } + + public NetBigInteger Divide( + NetBigInteger val) + { + if (val.m_sign == 0) + throw new ArithmeticException("Division by zero error"); + + if (m_sign == 0) + return Zero; + + if (val.QuickPow2Check()) // val is power of two + { + NetBigInteger result = Abs().ShiftRight(val.Abs().BitLength - 1); + return val.m_sign == m_sign ? result : result.Negate(); + } + + int[] mag = (int[])m_magnitude.Clone(); + + return new NetBigInteger(m_sign * val.m_sign, Divide(mag, val.m_magnitude), true); + } + + public NetBigInteger[] DivideAndRemainder( + NetBigInteger val) + { + if (val.m_sign == 0) + throw new ArithmeticException("Division by zero error"); + + NetBigInteger[] biggies = new NetBigInteger[2]; + + if (m_sign == 0) + { + biggies[0] = Zero; + biggies[1] = Zero; + } + else if (val.QuickPow2Check()) // val is power of two + { + int e = val.Abs().BitLength - 1; + NetBigInteger quotient = Abs().ShiftRight(e); + int[] remainder = LastNBits(e); + + biggies[0] = val.m_sign == m_sign ? quotient : quotient.Negate(); + biggies[1] = new NetBigInteger(m_sign, remainder, true); + } + else + { + int[] remainder = (int[])m_magnitude.Clone(); + int[] quotient = Divide(remainder, val.m_magnitude); + + biggies[0] = new NetBigInteger(m_sign * val.m_sign, quotient, true); + biggies[1] = new NetBigInteger(m_sign, remainder, true); + } + + return biggies; + } + + public override bool Equals( + object obj) + { + if (obj == this) + return true; + + NetBigInteger biggie = obj as NetBigInteger; + if (biggie == null) + return false; + + if (biggie.m_sign != m_sign || biggie.m_magnitude.Length != m_magnitude.Length) + return false; + + for (int i = 0; i < m_magnitude.Length; i++) + { + if (biggie.m_magnitude[i] != m_magnitude[i]) + { + return false; + } + } + + return true; + } + + public NetBigInteger Gcd( + NetBigInteger value) + { + if (value.m_sign == 0) + return Abs(); + + if (m_sign == 0) + return value.Abs(); + + NetBigInteger r; + NetBigInteger u = this; + NetBigInteger v = value; + + while (v.m_sign != 0) + { + r = u.Mod(v); + u = v; + v = r; + } + + return u; + } + + public override int GetHashCode() + { + int hc = m_magnitude.Length; + if (m_magnitude.Length > 0) + { + hc ^= m_magnitude[0]; + + if (m_magnitude.Length > 1) + { + hc ^= m_magnitude[m_magnitude.Length - 1]; + } + } + + return m_sign < 0 ? ~hc : hc; + } + + private NetBigInteger Inc() + { + if (m_sign == 0) + return One; + + if (m_sign < 0) + return new NetBigInteger(-1, doSubBigLil(m_magnitude, One.m_magnitude), true); + + return AddToMagnitude(One.m_magnitude); + } + + public int IntValue + { + get + { + return m_sign == 0 ? 0 + : m_sign > 0 ? m_magnitude[m_magnitude.Length - 1] + : -m_magnitude[m_magnitude.Length - 1]; + } + } + + public NetBigInteger Max( + NetBigInteger value) + { + return CompareTo(value) > 0 ? this : value; + } + + public NetBigInteger Min( + NetBigInteger value) + { + return CompareTo(value) < 0 ? this : value; + } + + public NetBigInteger Mod( + NetBigInteger m) + { + if (m.m_sign < 1) + throw new ArithmeticException("Modulus must be positive"); + + NetBigInteger biggie = Remainder(m); + + return (biggie.m_sign >= 0 ? biggie : biggie.Add(m)); + } + + public NetBigInteger ModInverse( + NetBigInteger m) + { + if (m.m_sign < 1) + throw new ArithmeticException("Modulus must be positive"); + + NetBigInteger x = new NetBigInteger(); + NetBigInteger gcd = ExtEuclid(this, m, x, null); + + if (!gcd.Equals(One)) + throw new ArithmeticException("Numbers not relatively prime."); + + if (x.m_sign < 0) + { + x.m_sign = 1; + //x = m.Subtract(x); + x.m_magnitude = doSubBigLil(m.m_magnitude, x.m_magnitude); + } + + return x; + } + + private static NetBigInteger ExtEuclid( + NetBigInteger a, + NetBigInteger b, + NetBigInteger u1Out, + NetBigInteger u2Out) + { + NetBigInteger u1 = One; + NetBigInteger u3 = a; + NetBigInteger v1 = Zero; + NetBigInteger v3 = b; + + while (v3.m_sign > 0) + { + NetBigInteger[] q = u3.DivideAndRemainder(v3); + + NetBigInteger tmp = v1.Multiply(q[0]); + NetBigInteger tn = u1.Subtract(tmp); + u1 = v1; + v1 = tn; + + u3 = v3; + v3 = q[1]; + } + + if (u1Out != null) + { + u1Out.m_sign = u1.m_sign; + u1Out.m_magnitude = u1.m_magnitude; + } + + if (u2Out != null) + { + NetBigInteger tmp = u1.Multiply(a); + tmp = u3.Subtract(tmp); + NetBigInteger res = tmp.Divide(b); + u2Out.m_sign = res.m_sign; + u2Out.m_magnitude = res.m_magnitude; + } + + return u3; + } + + private static void ZeroOut( + int[] x) + { + Array.Clear(x, 0, x.Length); + } + + public NetBigInteger ModPow( + NetBigInteger exponent, + NetBigInteger m) + { + if (m.m_sign < 1) + throw new ArithmeticException("Modulus must be positive"); + + if (m.Equals(One)) + return Zero; + + if (exponent.m_sign == 0) + return One; + + if (m_sign == 0) + return Zero; + + int[] zVal = null; + int[] yAccum = null; + int[] yVal; + + // Montgomery exponentiation is only possible if the modulus is odd, + // but AFAIK, this is always the case for crypto algo's + bool useMonty = ((m.m_magnitude[m.m_magnitude.Length - 1] & 1) == 1); + long mQ = 0; + if (useMonty) + { + mQ = m.GetMQuote(); + + // tmp = this * R mod m + NetBigInteger tmp = ShiftLeft(32 * m.m_magnitude.Length).Mod(m); + zVal = tmp.m_magnitude; + + useMonty = (zVal.Length <= m.m_magnitude.Length); + + if (useMonty) + { + yAccum = new int[m.m_magnitude.Length + 1]; + if (zVal.Length < m.m_magnitude.Length) + { + int[] longZ = new int[m.m_magnitude.Length]; + zVal.CopyTo(longZ, longZ.Length - zVal.Length); + zVal = longZ; + } + } + } + + if (!useMonty) + { + if (m_magnitude.Length <= m.m_magnitude.Length) + { + //zAccum = new int[m.magnitude.Length * 2]; + zVal = new int[m.m_magnitude.Length]; + m_magnitude.CopyTo(zVal, zVal.Length - m_magnitude.Length); + } + else + { + // + // in normal practice we'll never see .. + // + NetBigInteger tmp = Remainder(m); + + //zAccum = new int[m.magnitude.Length * 2]; + zVal = new int[m.m_magnitude.Length]; + tmp.m_magnitude.CopyTo(zVal, zVal.Length - tmp.m_magnitude.Length); + } + + yAccum = new int[m.m_magnitude.Length * 2]; + } + + yVal = new int[m.m_magnitude.Length]; + + // + // from LSW to MSW + // + for (int i = 0; i < exponent.m_magnitude.Length; i++) + { + int v = exponent.m_magnitude[i]; + int bits = 0; + + if (i == 0) + { + while (v > 0) + { + v <<= 1; + bits++; + } + + // + // first time in initialise y + // + zVal.CopyTo(yVal, 0); + + v <<= 1; + bits++; + } + + while (v != 0) + { + if (useMonty) + { + // Montgomery square algo doesn't exist, and a normal + // square followed by a Montgomery reduction proved to + // be almost as heavy as a Montgomery mulitply. + MultiplyMonty(yAccum, yVal, yVal, m.m_magnitude, mQ); + } + else + { + Square(yAccum, yVal); + Remainder(yAccum, m.m_magnitude); + Array.Copy(yAccum, yAccum.Length - yVal.Length, yVal, 0, yVal.Length); + ZeroOut(yAccum); + } + bits++; + + if (v < 0) + { + if (useMonty) + { + MultiplyMonty(yAccum, yVal, zVal, m.m_magnitude, mQ); + } + else + { + Multiply(yAccum, yVal, zVal); + Remainder(yAccum, m.m_magnitude); + Array.Copy(yAccum, yAccum.Length - yVal.Length, yVal, 0, + yVal.Length); + ZeroOut(yAccum); + } + } + + v <<= 1; + } + + while (bits < 32) + { + if (useMonty) + { + MultiplyMonty(yAccum, yVal, yVal, m.m_magnitude, mQ); + } + else + { + Square(yAccum, yVal); + Remainder(yAccum, m.m_magnitude); + Array.Copy(yAccum, yAccum.Length - yVal.Length, yVal, 0, yVal.Length); + ZeroOut(yAccum); + } + bits++; + } + } + + if (useMonty) + { + // Return y * R^(-1) mod m by doing y * 1 * R^(-1) mod m + ZeroOut(zVal); + zVal[zVal.Length - 1] = 1; + MultiplyMonty(yAccum, yVal, zVal, m.m_magnitude, mQ); + } + + NetBigInteger result = new NetBigInteger(1, yVal, true); + + return exponent.m_sign > 0 + ? result + : result.ModInverse(m); + } + + // return w with w = x * x - w is assumed to have enough space. + private static int[] Square( + int[] w, + int[] x) + { + // Note: this method allows w to be only (2 * x.Length - 1) words if result will fit + // if (w.Length != 2 * x.Length) + // throw new ArgumentException("no I don't think so..."); + + ulong u1, u2, c; + + int wBase = w.Length - 1; + + for (int i = x.Length - 1; i != 0; i--) + { + ulong v = (ulong)(uint)x[i]; + + u1 = v * v; + u2 = u1 >> 32; + u1 = (uint)u1; + + u1 += (ulong)(uint)w[wBase]; + + w[wBase] = (int)(uint)u1; + c = u2 + (u1 >> 32); + + for (int j = i - 1; j >= 0; j--) + { + --wBase; + u1 = v * (ulong)(uint)x[j]; + u2 = u1 >> 31; // multiply by 2! + u1 = (uint)(u1 << 1); // multiply by 2! + u1 += c + (ulong)(uint)w[wBase]; + + w[wBase] = (int)(uint)u1; + c = u2 + (u1 >> 32); + } + + c += (ulong)(uint)w[--wBase]; + w[wBase] = (int)(uint)c; + + if (--wBase >= 0) + { + w[wBase] = (int)(uint)(c >> 32); + } + else + { + Debug.Assert((uint)(c >> 32) == 0); + } + wBase += i; + } + + u1 = (ulong)(uint)x[0]; + u1 = u1 * u1; + u2 = u1 >> 32; + u1 = u1 & IMASK; + + u1 += (ulong)(uint)w[wBase]; + + w[wBase] = (int)(uint)u1; + if (--wBase >= 0) + { + w[wBase] = (int)(uint)(u2 + (u1 >> 32) + (ulong)(uint)w[wBase]); + } + else + { + Debug.Assert((uint)(u2 + (u1 >> 32)) == 0); + } + + return w; + } + + // return x with x = y * z - x is assumed to have enough space. + private static int[] Multiply( + int[] x, + int[] y, + int[] z) + { + int i = z.Length; + + if (i < 1) + return x; + + int xBase = x.Length - y.Length; + + for (; ; ) + { + long a = z[--i] & IMASK; + long val = 0; + + for (int j = y.Length - 1; j >= 0; j--) + { + val += a * (y[j] & IMASK) + (x[xBase + j] & IMASK); + + x[xBase + j] = (int)val; + + val = (long)((ulong)val >> 32); + } + + --xBase; + + if (i < 1) + { + if (xBase >= 0) + { + x[xBase] = (int)val; + } + else + { + Debug.Assert(val == 0); + } + break; + } + + x[xBase] = (int)val; + } + + return x; + } + + private static long FastExtEuclid( + long a, + long b, + long[] uOut) + { + long u1 = 1; + long u3 = a; + long v1 = 0; + long v3 = b; + + while (v3 > 0) + { + long q, tn; + + q = u3 / v3; + + tn = u1 - (v1 * q); + u1 = v1; + v1 = tn; + + tn = u3 - (v3 * q); + u3 = v3; + v3 = tn; + } + + uOut[0] = u1; + uOut[1] = (u3 - (u1 * a)) / b; + + return u3; + } + + private static long FastModInverse( + long v, + long m) + { + if (m < 1) + throw new ArithmeticException("Modulus must be positive"); + + long[] x = new long[2]; + long gcd = FastExtEuclid(v, m, x); + + if (gcd != 1) + throw new ArithmeticException("Numbers not relatively prime."); + + if (x[0] < 0) + { + x[0] += m; + } + + return x[0]; + } + + private long GetMQuote() + { + Debug.Assert(m_sign > 0); + + if (m_quote != -1) + { + return m_quote; // already calculated + } + + if (m_magnitude.Length == 0 || (m_magnitude[m_magnitude.Length - 1] & 1) == 0) + { + return -1; // not for even numbers + } + + long v = (((~m_magnitude[m_magnitude.Length - 1]) | 1) & 0xffffffffL); + m_quote = FastModInverse(v, 0x100000000L); + + return m_quote; + } + + private static void MultiplyMonty( + int[] a, + int[] x, + int[] y, + int[] m, + long mQuote) + // mQuote = -m^(-1) mod b + { + if (m.Length == 1) + { + x[0] = (int)MultiplyMontyNIsOne((uint)x[0], (uint)y[0], (uint)m[0], (ulong)mQuote); + return; + } + + int n = m.Length; + int nMinus1 = n - 1; + long y_0 = y[nMinus1] & IMASK; + + // 1. a = 0 (Notation: a = (a_{n} a_{n-1} ... a_{0})_{b} ) + Array.Clear(a, 0, n + 1); + + // 2. for i from 0 to (n - 1) do the following: + for (int i = n; i > 0; i--) + { + long x_i = x[i - 1] & IMASK; + + // 2.1 u = ((a[0] + (x[i] * y[0]) * mQuote) mod b + long u = ((((a[n] & IMASK) + ((x_i * y_0) & IMASK)) & IMASK) * mQuote) & IMASK; + + // 2.2 a = (a + x_i * y + u * m) / b + long prod1 = x_i * y_0; + long prod2 = u * (m[nMinus1] & IMASK); + long tmp = (a[n] & IMASK) + (prod1 & IMASK) + (prod2 & IMASK); + long carry = (long)((ulong)prod1 >> 32) + (long)((ulong)prod2 >> 32) + (long)((ulong)tmp >> 32); + for (int j = nMinus1; j > 0; j--) + { + prod1 = x_i * (y[j - 1] & IMASK); + prod2 = u * (m[j - 1] & IMASK); + tmp = (a[j] & IMASK) + (prod1 & IMASK) + (prod2 & IMASK) + (carry & IMASK); + carry = (long)((ulong)carry >> 32) + (long)((ulong)prod1 >> 32) + + (long)((ulong)prod2 >> 32) + (long)((ulong)tmp >> 32); + a[j + 1] = (int)tmp; // division by b + } + carry += (a[0] & IMASK); + a[1] = (int)carry; + a[0] = (int)((ulong)carry >> 32); // OJO!!!!! + } + + // 3. if x >= m the x = x - m + if (CompareTo(0, a, 0, m) >= 0) + { + Subtract(0, a, 0, m); + } + + // put the result in x + Array.Copy(a, 1, x, 0, n); + } + + private static uint MultiplyMontyNIsOne( + uint x, + uint y, + uint m, + ulong mQuote) + { + ulong um = m; + ulong prod1 = (ulong)x * (ulong)y; + ulong u = (prod1 * mQuote) & UIMASK; + ulong prod2 = u * um; + ulong tmp = (prod1 & UIMASK) + (prod2 & UIMASK); + ulong carry = (prod1 >> 32) + (prod2 >> 32) + (tmp >> 32); + + if (carry > um) + { + carry -= um; + } + + return (uint)(carry & UIMASK); + } + + public NetBigInteger Modulus( + NetBigInteger val) + { + return Mod(val); + } + + public NetBigInteger Multiply( + NetBigInteger val) + { + if (m_sign == 0 || val.m_sign == 0) + return Zero; + + if (val.QuickPow2Check()) // val is power of two + { + NetBigInteger result = ShiftLeft(val.Abs().BitLength - 1); + return val.m_sign > 0 ? result : result.Negate(); + } + + if (QuickPow2Check()) // this is power of two + { + NetBigInteger result = val.ShiftLeft(Abs().BitLength - 1); + return m_sign > 0 ? result : result.Negate(); + } + + int maxBitLength = BitLength + val.BitLength; + int resLength = (maxBitLength + BitsPerInt - 1) / BitsPerInt; + + int[] res = new int[resLength]; + + if (val == this) + { + Square(res, m_magnitude); + } + else + { + Multiply(res, m_magnitude, val.m_magnitude); + } + + return new NetBigInteger(m_sign * val.m_sign, res, true); + } + + public NetBigInteger Negate() + { + if (m_sign == 0) + return this; + + return new NetBigInteger(-m_sign, m_magnitude, false); + } + + public NetBigInteger Not() + { + return Inc().Negate(); + } + + public NetBigInteger Pow(int exp) + { + if (exp < 0) + { + throw new ArithmeticException("Negative exponent"); + } + + if (exp == 0) + { + return One; + } + + if (m_sign == 0 || Equals(One)) + { + return this; + } + + NetBigInteger y = One; + NetBigInteger z = this; + + for (; ; ) + { + if ((exp & 0x1) == 1) + { + y = y.Multiply(z); + } + exp >>= 1; + if (exp == 0) break; + z = z.Multiply(z); + } + + return y; + } + + private int Remainder( + int m) + { + Debug.Assert(m > 0); + + long acc = 0; + for (int pos = 0; pos < m_magnitude.Length; ++pos) + { + long posVal = (uint)m_magnitude[pos]; + acc = (acc << 32 | posVal) % m; + } + + return (int)acc; + } + + // return x = x % y - done in place (y value preserved) + private int[] Remainder( + int[] x, + int[] y) + { + int xStart = 0; + while (xStart < x.Length && x[xStart] == 0) + { + ++xStart; + } + + int yStart = 0; + while (yStart < y.Length && y[yStart] == 0) + { + ++yStart; + } + + Debug.Assert(yStart < y.Length); + + int xyCmp = CompareNoLeadingZeroes(xStart, x, yStart, y); + + if (xyCmp > 0) + { + int yBitLength = calcBitLength(yStart, y); + int xBitLength = calcBitLength(xStart, x); + int shift = xBitLength - yBitLength; + + int[] c; + int cStart = 0; + int cBitLength = yBitLength; + if (shift > 0) + { + c = ShiftLeft(y, shift); + cBitLength += shift; + Debug.Assert(c[0] != 0); + } + else + { + int len = y.Length - yStart; + c = new int[len]; + Array.Copy(y, yStart, c, 0, len); + } + + for (; ; ) + { + if (cBitLength < xBitLength + || CompareNoLeadingZeroes(xStart, x, cStart, c) >= 0) + { + Subtract(xStart, x, cStart, c); + + while (x[xStart] == 0) + { + if (++xStart == x.Length) + return x; + } + + //xBitLength = calcBitLength(xStart, x); + xBitLength = 32 * (x.Length - xStart - 1) + BitLen(x[xStart]); + + if (xBitLength <= yBitLength) + { + if (xBitLength < yBitLength) + return x; + + xyCmp = CompareNoLeadingZeroes(xStart, x, yStart, y); + + if (xyCmp <= 0) + break; + } + } + + shift = cBitLength - xBitLength; + + // NB: The case where c[cStart] is 1-bit is harmless + if (shift == 1) + { + uint firstC = (uint)c[cStart] >> 1; + uint firstX = (uint)x[xStart]; + if (firstC > firstX) + ++shift; + } + + if (shift < 2) + { + c = ShiftRightOneInPlace(cStart, c); + --cBitLength; + } + else + { + c = ShiftRightInPlace(cStart, c, shift); + cBitLength -= shift; + } + + //cStart = c.Length - ((cBitLength + 31) / 32); + while (c[cStart] == 0) + { + ++cStart; + } + } + } + + if (xyCmp == 0) + { + Array.Clear(x, xStart, x.Length - xStart); + } + + return x; + } + + public NetBigInteger Remainder( + NetBigInteger n) + { + if (n.m_sign == 0) + throw new ArithmeticException("Division by zero error"); + + if (m_sign == 0) + return Zero; + + // For small values, use fast remainder method + if (n.m_magnitude.Length == 1) + { + int val = n.m_magnitude[0]; + + if (val > 0) + { + if (val == 1) + return Zero; + + int rem = Remainder(val); + + return rem == 0 + ? Zero + : new NetBigInteger(m_sign, new int[] { rem }, false); + } + } + + if (CompareNoLeadingZeroes(0, m_magnitude, 0, n.m_magnitude) < 0) + return this; + + int[] result; + if (n.QuickPow2Check()) // n is power of two + { + result = LastNBits(n.Abs().BitLength - 1); + } + else + { + result = (int[])m_magnitude.Clone(); + result = Remainder(result, n.m_magnitude); + } + + return new NetBigInteger(m_sign, result, true); + } + + private int[] LastNBits( + int n) + { + if (n < 1) + return ZeroMagnitude; + + int numWords = (n + BitsPerInt - 1) / BitsPerInt; + numWords = System.Math.Min(numWords, m_magnitude.Length); + int[] result = new int[numWords]; + + Array.Copy(m_magnitude, m_magnitude.Length - numWords, result, 0, numWords); + + int hiBits = n % 32; + if (hiBits != 0) + { + result[0] &= ~(-1 << hiBits); + } + + return result; + } + + + // do a left shift - this returns a new array. + private static int[] ShiftLeft( + int[] mag, + int n) + { + int nInts = (int)((uint)n >> 5); + int nBits = n & 0x1f; + int magLen = mag.Length; + int[] newMag; + + if (nBits == 0) + { + newMag = new int[magLen + nInts]; + mag.CopyTo(newMag, 0); + } + else + { + int i = 0; + int nBits2 = 32 - nBits; + int highBits = (int)((uint)mag[0] >> nBits2); + + if (highBits != 0) + { + newMag = new int[magLen + nInts + 1]; + newMag[i++] = highBits; + } + else + { + newMag = new int[magLen + nInts]; + } + + int m = mag[0]; + for (int j = 0; j < magLen - 1; j++) + { + int next = mag[j + 1]; + + newMag[i++] = (m << nBits) | (int)((uint)next >> nBits2); + m = next; + } + + newMag[i] = mag[magLen - 1] << nBits; + } + + return newMag; + } + + public NetBigInteger ShiftLeft( + int n) + { + if (m_sign == 0 || m_magnitude.Length == 0) + return Zero; + + if (n == 0) + return this; + + if (n < 0) + return ShiftRight(-n); + + NetBigInteger result = new NetBigInteger(m_sign, ShiftLeft(m_magnitude, n), true); + + if (m_numBits != -1) + { + result.m_numBits = m_sign > 0 + ? m_numBits + : m_numBits + n; + } + + if (m_numBitLength != -1) + { + result.m_numBitLength = m_numBitLength + n; + } + + return result; + } + + // do a right shift - this does it in place. + private static int[] ShiftRightInPlace( + int start, + int[] mag, + int n) + { + int nInts = (int)((uint)n >> 5) + start; + int nBits = n & 0x1f; + int magEnd = mag.Length - 1; + + if (nInts != start) + { + int delta = (nInts - start); + + for (int i = magEnd; i >= nInts; i--) + { + mag[i] = mag[i - delta]; + } + for (int i = nInts - 1; i >= start; i--) + { + mag[i] = 0; + } + } + + if (nBits != 0) + { + int nBits2 = 32 - nBits; + int m = mag[magEnd]; + + for (int i = magEnd; i > nInts; --i) + { + int next = mag[i - 1]; + + mag[i] = (int)((uint)m >> nBits) | (next << nBits2); + m = next; + } + + mag[nInts] = (int)((uint)mag[nInts] >> nBits); + } + + return mag; + } + + // do a right shift by one - this does it in place. + private static int[] ShiftRightOneInPlace( + int start, + int[] mag) + { + int i = mag.Length; + int m = mag[i - 1]; + + while (--i > start) + { + int next = mag[i - 1]; + mag[i] = ((int)((uint)m >> 1)) | (next << 31); + m = next; + } + + mag[start] = (int)((uint)mag[start] >> 1); + + return mag; + } + + public NetBigInteger ShiftRight( + int n) + { + if (n == 0) + return this; + + if (n < 0) + return ShiftLeft(-n); + + if (n >= BitLength) + return (m_sign < 0 ? One.Negate() : Zero); + + // int[] res = (int[]) magnitude.Clone(); + // + // res = ShiftRightInPlace(0, res, n); + // + // return new BigInteger(sign, res, true); + + int resultLength = (BitLength - n + 31) >> 5; + int[] res = new int[resultLength]; + + int numInts = n >> 5; + int numBits = n & 31; + + if (numBits == 0) + { + Array.Copy(m_magnitude, 0, res, 0, res.Length); + } + else + { + int numBits2 = 32 - numBits; + + int magPos = m_magnitude.Length - 1 - numInts; + for (int i = resultLength - 1; i >= 0; --i) + { + res[i] = (int)((uint)m_magnitude[magPos--] >> numBits); + + if (magPos >= 0) + { + res[i] |= m_magnitude[magPos] << numBits2; + } + } + } + + Debug.Assert(res[0] != 0); + + return new NetBigInteger(m_sign, res, false); + } + + public int SignValue + { + get { return m_sign; } + } + + // returns x = x - y - we assume x is >= y + private static int[] Subtract( + int xStart, + int[] x, + int yStart, + int[] y) + { + Debug.Assert(yStart < y.Length); + Debug.Assert(x.Length - xStart >= y.Length - yStart); + + int iT = x.Length; + int iV = y.Length; + long m; + int borrow = 0; + + do + { + m = (x[--iT] & IMASK) - (y[--iV] & IMASK) + borrow; + x[iT] = (int)m; + + // borrow = (m < 0) ? -1 : 0; + borrow = (int)(m >> 63); + } + while (iV > yStart); + + if (borrow != 0) + { + while (--x[--iT] == -1) + { + } + } + + return x; + } + + public NetBigInteger Subtract( + NetBigInteger n) + { + if (n.m_sign == 0) + return this; + + if (m_sign == 0) + return n.Negate(); + + if (m_sign != n.m_sign) + return Add(n.Negate()); + + int compare = CompareNoLeadingZeroes(0, m_magnitude, 0, n.m_magnitude); + if (compare == 0) + return Zero; + + NetBigInteger bigun, lilun; + if (compare < 0) + { + bigun = n; + lilun = this; + } + else + { + bigun = this; + lilun = n; + } + + return new NetBigInteger(m_sign * compare, doSubBigLil(bigun.m_magnitude, lilun.m_magnitude), true); + } + + private static int[] doSubBigLil( + int[] bigMag, + int[] lilMag) + { + int[] res = (int[])bigMag.Clone(); + + return Subtract(0, res, 0, lilMag); + } + + public byte[] ToByteArray() + { + return ToByteArray(false); + } + + public byte[] ToByteArrayUnsigned() + { + return ToByteArray(true); + } + + private byte[] ToByteArray( + bool unsigned) + { + if (m_sign == 0) + return unsigned ? ZeroEncoding : new byte[1]; + + int nBits = (unsigned && m_sign > 0) + ? BitLength + : BitLength + 1; + + int nBytes = GetByteLength(nBits); + byte[] bytes = new byte[nBytes]; + + int magIndex = m_magnitude.Length; + int bytesIndex = bytes.Length; + + if (m_sign > 0) + { + while (magIndex > 1) + { + uint mag = (uint)m_magnitude[--magIndex]; + bytes[--bytesIndex] = (byte)mag; + bytes[--bytesIndex] = (byte)(mag >> 8); + bytes[--bytesIndex] = (byte)(mag >> 16); + bytes[--bytesIndex] = (byte)(mag >> 24); + } + + uint lastMag = (uint)m_magnitude[0]; + while (lastMag > byte.MaxValue) + { + bytes[--bytesIndex] = (byte)lastMag; + lastMag >>= 8; + } + + bytes[--bytesIndex] = (byte)lastMag; + } + else // sign < 0 + { + bool carry = true; + + while (magIndex > 1) + { + uint mag = ~((uint)m_magnitude[--magIndex]); + + if (carry) + { + carry = (++mag == uint.MinValue); + } + + bytes[--bytesIndex] = (byte)mag; + bytes[--bytesIndex] = (byte)(mag >> 8); + bytes[--bytesIndex] = (byte)(mag >> 16); + bytes[--bytesIndex] = (byte)(mag >> 24); + } + + uint lastMag = (uint)m_magnitude[0]; + + if (carry) + { + // Never wraps because magnitude[0] != 0 + --lastMag; + } + + while (lastMag > byte.MaxValue) + { + bytes[--bytesIndex] = (byte)~lastMag; + lastMag >>= 8; + } + + bytes[--bytesIndex] = (byte)~lastMag; + + if (bytesIndex > 0) + { + bytes[--bytesIndex] = byte.MaxValue; + } + } + + return bytes; + } + + public override string ToString() + { + return ToString(10); + } + + public string ToString( + int radix) + { + switch (radix) + { + case 2: + case 10: + case 16: + break; + default: + throw new FormatException("Only bases 2, 10, 16 are allowed"); + } + + // NB: Can only happen to internally managed instances + if (m_magnitude == null) + return "null"; + + if (m_sign == 0) + return "0"; + + Debug.Assert(m_magnitude.Length > 0); + + StringBuilder sb = new StringBuilder(); + + if (radix == 16) + { + sb.Append(m_magnitude[0].ToString("x")); + + for (int i = 1; i < m_magnitude.Length; i++) + { + sb.Append(m_magnitude[i].ToString("x8")); + } + } + else if (radix == 2) + { + sb.Append('1'); + + for (int i = BitLength - 2; i >= 0; --i) + { + sb.Append(TestBit(i) ? '1' : '0'); + } + } + else + { + // This is algorithm 1a from chapter 4.4 in Seminumerical Algorithms, slow but it works + Stack S = new Stack(); + NetBigInteger bs = ValueOf(radix); + + NetBigInteger u = Abs(); + NetBigInteger b; + + while (u.m_sign != 0) + { + b = u.Mod(bs); + if (b.m_sign == 0) + { + S.Push("0"); + } + else + { + // see how to interact with different bases + S.Push(b.m_magnitude[0].ToString("d")); + } + u = u.Divide(bs); + } + + // Then pop the stack + while (S.Count != 0) + { + sb.Append((string)S.Pop()); + } + } + + string s = sb.ToString(); + + Debug.Assert(s.Length > 0); + + // Strip leading zeros. (We know this number is not all zeroes though) + if (s[0] == '0') + { + int nonZeroPos = 0; + while (s[++nonZeroPos] == '0') { } + + s = s.Substring(nonZeroPos); + } + + if (m_sign == -1) + { + s = "-" + s; + } + + return s; + } + + private static NetBigInteger createUValueOf( + ulong value) + { + int msw = (int)(value >> 32); + int lsw = (int)value; + + if (msw != 0) + return new NetBigInteger(1, new int[] { msw, lsw }, false); + + if (lsw != 0) + { + NetBigInteger n = new NetBigInteger(1, new int[] { lsw }, false); + // Check for a power of two + if ((lsw & -lsw) == lsw) + { + n.m_numBits = 1; + } + return n; + } + + return Zero; + } + + private static NetBigInteger createValueOf( + long value) + { + if (value < 0) + { + if (value == long.MinValue) + return createValueOf(~value).Not(); + + return createValueOf(-value).Negate(); + } + + return createUValueOf((ulong)value); + } + + public static NetBigInteger ValueOf( + long value) + { + switch (value) + { + case 0: + return Zero; + case 1: + return One; + case 2: + return Two; + case 3: + return Three; + case 10: + return Ten; + } + + return createValueOf(value); + } + + public int GetLowestSetBit() + { + if (m_sign == 0) + return -1; + + int w = m_magnitude.Length; + + while (--w > 0) + { + if (m_magnitude[w] != 0) + break; + } + + int word = (int)m_magnitude[w]; + Debug.Assert(word != 0); + + int b = (word & 0x0000FFFF) == 0 + ? (word & 0x00FF0000) == 0 + ? 7 + : 15 + : (word & 0x000000FF) == 0 + ? 23 + : 31; + + while (b > 0) + { + if ((word << b) == int.MinValue) + break; + + b--; + } + + return ((m_magnitude.Length - w) * 32 - (b + 1)); + } + + public bool TestBit( + int n) + { + if (n < 0) + throw new ArithmeticException("Bit position must not be negative"); + + if (m_sign < 0) + return !Not().TestBit(n); + + int wordNum = n / 32; + if (wordNum >= m_magnitude.Length) + return false; + + int word = m_magnitude[m_magnitude.Length - 1 - wordNum]; + return ((word >> (n % 32)) & 1) > 0; + } + } +} \ No newline at end of file diff --git a/Lidgren.Network/NetBitVector.cs b/Lidgren.Network/NetBitVector.cs new file mode 100644 index 000000000..af4b4d743 --- /dev/null +++ b/Lidgren.Network/NetBitVector.cs @@ -0,0 +1,172 @@ +/* Copyright (c) 2010 Michael Lidgren + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom +the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ +using System; +using System.Text; + +namespace Lidgren.Network +{ + /// + /// Fixed size vector of booleans + /// + public sealed class NetBitVector + { + private readonly int m_capacity; + private readonly int[] m_data; + private int m_numBitsSet; + + /// + /// Gets the number of bits/booleans stored in this vector + /// + public int Capacity { get { return m_capacity; } } + + /// + /// NetBitVector constructor + /// + public NetBitVector(int bitsCapacity) + { + m_capacity = bitsCapacity; + m_data = new int[(bitsCapacity + 31) / 32]; + } + + /// + /// Returns true if all bits/booleans are set to zero/false + /// + public bool IsEmpty() + { + return (m_numBitsSet == 0); + } + + /// + /// Returns the number of bits/booleans set to one/true + /// + /// + public int Count() + { + return m_numBitsSet; + } + + /// + /// Shift all bits one step down, cycling the first bit to the top + /// + public void RotateDown() + { + int lenMinusOne = m_data.Length - 1; + + int firstBit = m_data[0] & 1; + for (int i = 0; i < lenMinusOne; i++) + m_data[i] = ((m_data[i] >> 1) & ~(1 << 31)) | m_data[i + 1] << 31; + + int lastIndex = m_capacity - 1 - (32 * lenMinusOne); + + // special handling of last int + int cur = m_data[lenMinusOne]; + cur = cur >> 1; + cur |= firstBit << lastIndex; + + m_data[lenMinusOne] = cur; + } + + /// + /// Gets the first (lowest) index set to true + /// + public int GetFirstSetIndex() + { + int idx = 0; + + int data = m_data[0]; + while (data == 0) + { + idx++; + data = m_data[idx]; + } + + int a = 0; + while (((data >> a) & 1) == 0) + a++; + + return (idx * 32) + a; + } + + /// + /// Gets the bit/bool at the specified index + /// + public bool Get(int bitIndex) + { + NetException.Assert(bitIndex >= 0 && bitIndex < m_capacity); + + return (m_data[bitIndex / 32] & (1 << (bitIndex % 32))) != 0; + } + + /// + /// Sets or clears the bit/bool at the specified index + /// + public void Set(int bitIndex, bool value) + { + NetException.Assert(bitIndex >= 0 && bitIndex < m_capacity); + + int idx = bitIndex / 32; + if (value) + { + if ((m_data[idx] & (1 << (bitIndex % 32))) == 0) + m_numBitsSet++; + m_data[idx] |= (1 << (bitIndex % 32)); + } + else + { + if ((m_data[idx] & (1 << (bitIndex % 32))) != 0) + m_numBitsSet--; + m_data[idx] &= (~(1 << (bitIndex % 32))); + } + } + + /// + /// Gets the bit/bool at the specified index + /// + [System.Runtime.CompilerServices.IndexerName("Bit")] + public bool this[int index] + { + get { return Get(index); } + set { Set(index, value); } + } + + /// + /// Sets all bits/booleans to zero/false + /// + public void Clear() + { + Array.Clear(m_data, 0, m_data.Length); + m_numBitsSet = 0; + NetException.Assert(this.IsEmpty()); + } + + /// + /// Returns a string that represents this object + /// + public override string ToString() + { + StringBuilder bdr = new StringBuilder(m_capacity + 2); + bdr.Append('['); + for (int i = 0; i < m_capacity; i++) + bdr.Append(Get(m_capacity - i - 1) ? '1' : '0'); + bdr.Append(']'); + return bdr.ToString(); + } + } +} diff --git a/Lidgren.Network/NetBitWriter.cs b/Lidgren.Network/NetBitWriter.cs new file mode 100644 index 000000000..e2c87f49d --- /dev/null +++ b/Lidgren.Network/NetBitWriter.cs @@ -0,0 +1,514 @@ +//#define UNSAFE +//#define BIGENDIAN +/* Copyright (c) 2010 Michael Lidgren + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom +the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ +using System; +using System.Collections.Generic; + +using System.Diagnostics; + +namespace Lidgren.Network +{ + /// + /// Helper class for NetBuffer to write/read bits + /// + public static class NetBitWriter + { + /// + /// Read 1-8 bits from a buffer into a byte + /// + public static byte ReadByte(byte[] fromBuffer, int numberOfBits, int readBitOffset) + { + NetException.Assert(((numberOfBits > 0) && (numberOfBits < 9)), "Read() can only read between 1 and 8 bits"); + + int bytePtr = readBitOffset >> 3; + int startReadAtIndex = readBitOffset - (bytePtr * 8); // (readBitOffset % 8); + + if (startReadAtIndex == 0 && numberOfBits == 8) + return fromBuffer[bytePtr]; + + // mask away unused bits lower than (right of) relevant bits in first byte + byte returnValue = (byte)(fromBuffer[bytePtr] >> startReadAtIndex); + + int numberOfBitsInSecondByte = numberOfBits - (8 - startReadAtIndex); + + if (numberOfBitsInSecondByte < 1) + { + // we don't need to read from the second byte, but we DO need + // to mask away unused bits higher than (left of) relevant bits + return (byte)(returnValue & (255 >> (8 - numberOfBits))); + } + + byte second = fromBuffer[bytePtr + 1]; + + // mask away unused bits higher than (left of) relevant bits in second byte + second &= (byte)(255 >> (8 - numberOfBitsInSecondByte)); + + return (byte)(returnValue | (byte)(second << (numberOfBits - numberOfBitsInSecondByte))); + } + + /// + /// Read several bytes from a buffer + /// + public static void ReadBytes(byte[] fromBuffer, int numberOfBytes, int readBitOffset, byte[] destination, int destinationByteOffset) + { + int readPtr = readBitOffset >> 3; + int startReadAtIndex = readBitOffset - (readPtr * 8); // (readBitOffset % 8); + + if (startReadAtIndex == 0) + { + Buffer.BlockCopy(fromBuffer, readPtr, destination, destinationByteOffset, numberOfBytes); + return; + } + + int secondPartLen = 8 - startReadAtIndex; + int secondMask = 255 >> secondPartLen; + + for (int i = 0; i < numberOfBytes; i++) + { + // mask away unused bits lower than (right of) relevant bits in byte + int b = fromBuffer[readPtr] >> startReadAtIndex; + + readPtr++; + + // mask away unused bits higher than (left of) relevant bits in second byte + int second = fromBuffer[readPtr] & secondMask; + + destination[destinationByteOffset++] = (byte)(b | (second << secondPartLen)); + } + + return; + } + + /// + /// Write 0-8 bits of data to buffer + /// + public static void WriteByte(byte source, int numberOfBits, byte[] destination, int destBitOffset) + { + if (numberOfBits == 0) + return; + + NetException.Assert(((numberOfBits >= 0) && (numberOfBits <= 8)), "Must write between 0 and 8 bits!"); + + // Mask out all the bits we dont want + source = (byte)(source & (0xFF >> (8 - numberOfBits))); + + int p = destBitOffset >> 3; + int bitsUsed = destBitOffset & 0x7; // mod 8 + int bitsFree = 8 - bitsUsed; + int bitsLeft = bitsFree - numberOfBits; + + // Fast path, everything fits in the first byte + if (bitsLeft >= 0) + { + int mask = (0xFF >> bitsFree) | (0xFF << (8 - bitsLeft)); + + destination[p] = (byte)( + // Mask out lower and upper bits + (destination[p] & mask) | + + // Insert new bits + (source << bitsUsed) + ); + + return; + } + + destination[p] = (byte)( + // Mask out upper bits + (destination[p] & (0xFF >> bitsFree)) | + + // Write the lower bits to the upper bits in the first byte + (source << bitsUsed) + ); + + p += 1; + + destination[p] = (byte)( + // Mask out lower bits + (destination[p] & (0xFF << (numberOfBits - bitsFree))) | + + // Write the upper bits to the lower bits of the second byte + (source >> bitsFree) + ); + } + + /// + /// Write several whole bytes + /// + public static void WriteBytes(byte[] source, int sourceByteOffset, int numberOfBytes, byte[] destination, int destBitOffset) + { + int dstBytePtr = destBitOffset >> 3; + int firstPartLen = (destBitOffset % 8); + + if (firstPartLen == 0) + { + Buffer.BlockCopy(source, sourceByteOffset, destination, dstBytePtr, numberOfBytes); + return; + } + + int lastPartLen = 8 - firstPartLen; + + for (int i = 0; i < numberOfBytes; i++) + { + byte src = source[sourceByteOffset + i]; + + // write last part of this byte + destination[dstBytePtr] &= (byte)(255 >> lastPartLen); // clear before writing + destination[dstBytePtr] |= (byte)(src << firstPartLen); // write first half + + dstBytePtr++; + + // write first part of next byte + destination[dstBytePtr] &= (byte)(255 << firstPartLen); // clear before writing + destination[dstBytePtr] |= (byte)(src >> lastPartLen); // write second half + } + + return; + } + + /// + /// Reads an unsigned 16 bit integer + /// + [CLSCompliant(false)] +#if UNSAFE + public static unsafe ushort ReadUInt16(byte[] fromBuffer, int numberOfBits, int readBitOffset) + { + Debug.Assert(((numberOfBits > 0) && (numberOfBits <= 16)), "ReadUInt16() can only read between 1 and 16 bits"); + + if (numberOfBits == 16 && ((readBitOffset % 8) == 0)) + { + fixed (byte* ptr = &(fromBuffer[readBitOffset / 8])) + { + return *(((ushort*)ptr)); + } + } +#else + public static ushort ReadUInt16(byte[] fromBuffer, int numberOfBits, int readBitOffset) + { + Debug.Assert(((numberOfBits > 0) && (numberOfBits <= 16)), "ReadUInt16() can only read between 1 and 16 bits"); +#endif + ushort returnValue; + if (numberOfBits <= 8) + { + returnValue = ReadByte(fromBuffer, numberOfBits, readBitOffset); + return returnValue; + } + returnValue = ReadByte(fromBuffer, 8, readBitOffset); + numberOfBits -= 8; + readBitOffset += 8; + + if (numberOfBits <= 8) + { + returnValue |= (ushort)(ReadByte(fromBuffer, numberOfBits, readBitOffset) << 8); + } + +#if BIGENDIAN + // reorder bytes + uint retVal = returnValue; + retVal = ((retVal & 0x0000ff00) >> 8) | ((retVal & 0x000000ff) << 8); + return (ushort)retVal; +#else + return returnValue; +#endif + } + + /// + /// Reads the specified number of bits into an UInt32 + /// + [CLSCompliant(false)] +#if UNSAFE + public static unsafe uint ReadUInt32(byte[] fromBuffer, int numberOfBits, int readBitOffset) + { + NetException.Assert(((numberOfBits > 0) && (numberOfBits <= 32)), "ReadUInt32() can only read between 1 and 32 bits"); + + if (numberOfBits == 32 && ((readBitOffset % 8) == 0)) + { + fixed (byte* ptr = &(fromBuffer[readBitOffset / 8])) + { + return *(((uint*)ptr)); + } + } +#else + + public static uint ReadUInt32(byte[] fromBuffer, int numberOfBits, int readBitOffset) + { + NetException.Assert(((numberOfBits > 0) && (numberOfBits <= 32)), "ReadUInt32() can only read between 1 and 32 bits"); +#endif + uint returnValue; + if (numberOfBits <= 8) + { + returnValue = ReadByte(fromBuffer, numberOfBits, readBitOffset); + return returnValue; + } + returnValue = ReadByte(fromBuffer, 8, readBitOffset); + numberOfBits -= 8; + readBitOffset += 8; + + if (numberOfBits <= 8) + { + returnValue |= (uint)(ReadByte(fromBuffer, numberOfBits, readBitOffset) << 8); + return returnValue; + } + returnValue |= (uint)(ReadByte(fromBuffer, 8, readBitOffset) << 8); + numberOfBits -= 8; + readBitOffset += 8; + + if (numberOfBits <= 8) + { + uint r = ReadByte(fromBuffer, numberOfBits, readBitOffset); + r <<= 16; + returnValue |= r; + return returnValue; + } + returnValue |= (uint)(ReadByte(fromBuffer, 8, readBitOffset) << 16); + numberOfBits -= 8; + readBitOffset += 8; + + returnValue |= (uint)(ReadByte(fromBuffer, numberOfBits, readBitOffset) << 24); + +#if BIGENDIAN + // reorder bytes + return + ((returnValue & 0xff000000) >> 24) | + ((returnValue & 0x00ff0000) >> 8) | + ((returnValue & 0x0000ff00) << 8) | + ((returnValue & 0x000000ff) << 24); +#else + return returnValue; +#endif + } + + //[CLSCompliant(false)] + //public static ulong ReadUInt64(byte[] fromBuffer, int numberOfBits, int readBitOffset) + + /// + /// Writes an unsigned 16 bit integer + /// + [CLSCompliant(false)] + public static void WriteUInt16(ushort source, int numberOfBits, byte[] destination, int destinationBitOffset) + { + if (numberOfBits == 0) + return; + + NetException.Assert((numberOfBits >= 0 && numberOfBits <= 16), "numberOfBits must be between 0 and 16"); +#if BIGENDIAN + // reorder bytes + uint intSource = source; + intSource = ((intSource & 0x0000ff00) >> 8) | ((intSource & 0x000000ff) << 8); + source = (ushort)intSource; +#endif + if (numberOfBits <= 8) + { + NetBitWriter.WriteByte((byte)source, numberOfBits, destination, destinationBitOffset); + return; + } + + NetBitWriter.WriteByte((byte)source, 8, destination, destinationBitOffset); + + numberOfBits -= 8; + if (numberOfBits > 0) + NetBitWriter.WriteByte((byte)(source >> 8), numberOfBits, destination, destinationBitOffset + 8); + } + + /// + /// Writes the specified number of bits into a byte array + /// + [CLSCompliant(false)] + public static int WriteUInt32(uint source, int numberOfBits, byte[] destination, int destinationBitOffset) + { +#if BIGENDIAN + // reorder bytes + source = ((source & 0xff000000) >> 24) | + ((source & 0x00ff0000) >> 8) | + ((source & 0x0000ff00) << 8) | + ((source & 0x000000ff) << 24); +#endif + + int returnValue = destinationBitOffset + numberOfBits; + if (numberOfBits <= 8) + { + NetBitWriter.WriteByte((byte)source, numberOfBits, destination, destinationBitOffset); + return returnValue; + } + NetBitWriter.WriteByte((byte)source, 8, destination, destinationBitOffset); + destinationBitOffset += 8; + numberOfBits -= 8; + + if (numberOfBits <= 8) + { + NetBitWriter.WriteByte((byte)(source >> 8), numberOfBits, destination, destinationBitOffset); + return returnValue; + } + NetBitWriter.WriteByte((byte)(source >> 8), 8, destination, destinationBitOffset); + destinationBitOffset += 8; + numberOfBits -= 8; + + if (numberOfBits <= 8) + { + NetBitWriter.WriteByte((byte)(source >> 16), numberOfBits, destination, destinationBitOffset); + return returnValue; + } + NetBitWriter.WriteByte((byte)(source >> 16), 8, destination, destinationBitOffset); + destinationBitOffset += 8; + numberOfBits -= 8; + + NetBitWriter.WriteByte((byte)(source >> 24), numberOfBits, destination, destinationBitOffset); + return returnValue; + } + + /// + /// Writes the specified number of bits into a byte array + /// + [CLSCompliant(false)] + public static int WriteUInt64(ulong source, int numberOfBits, byte[] destination, int destinationBitOffset) + { +#if BIGENDIAN + source = ((source & 0xff00000000000000L) >> 56) | + ((source & 0x00ff000000000000L) >> 40) | + ((source & 0x0000ff0000000000L) >> 24) | + ((source & 0x000000ff00000000L) >> 8) | + ((source & 0x00000000ff000000L) << 8) | + ((source & 0x0000000000ff0000L) << 24) | + ((source & 0x000000000000ff00L) << 40) | + ((source & 0x00000000000000ffL) << 56); +#endif + + int returnValue = destinationBitOffset + numberOfBits; + if (numberOfBits <= 8) + { + NetBitWriter.WriteByte((byte)source, numberOfBits, destination, destinationBitOffset); + return returnValue; + } + NetBitWriter.WriteByte((byte)source, 8, destination, destinationBitOffset); + destinationBitOffset += 8; + numberOfBits -= 8; + + if (numberOfBits <= 8) + { + NetBitWriter.WriteByte((byte)(source >> 8), numberOfBits, destination, destinationBitOffset); + return returnValue; + } + NetBitWriter.WriteByte((byte)(source >> 8), 8, destination, destinationBitOffset); + destinationBitOffset += 8; + numberOfBits -= 8; + + if (numberOfBits <= 8) + { + NetBitWriter.WriteByte((byte)(source >> 16), numberOfBits, destination, destinationBitOffset); + return returnValue; + } + NetBitWriter.WriteByte((byte)(source >> 16), 8, destination, destinationBitOffset); + destinationBitOffset += 8; + numberOfBits -= 8; + + if (numberOfBits <= 8) + { + NetBitWriter.WriteByte((byte)(source >> 24), numberOfBits, destination, destinationBitOffset); + return returnValue; + } + NetBitWriter.WriteByte((byte)(source >> 24), 8, destination, destinationBitOffset); + destinationBitOffset += 8; + numberOfBits -= 8; + + if (numberOfBits <= 8) + { + NetBitWriter.WriteByte((byte)(source >> 32), numberOfBits, destination, destinationBitOffset); + return returnValue; + } + NetBitWriter.WriteByte((byte)(source >> 32), 8, destination, destinationBitOffset); + destinationBitOffset += 8; + numberOfBits -= 8; + + if (numberOfBits <= 8) + { + NetBitWriter.WriteByte((byte)(source >> 40), numberOfBits, destination, destinationBitOffset); + return returnValue; + } + NetBitWriter.WriteByte((byte)(source >> 40), 8, destination, destinationBitOffset); + destinationBitOffset += 8; + numberOfBits -= 8; + + if (numberOfBits <= 8) + { + NetBitWriter.WriteByte((byte)(source >> 48), numberOfBits, destination, destinationBitOffset); + return returnValue; + } + NetBitWriter.WriteByte((byte)(source >> 48), 8, destination, destinationBitOffset); + destinationBitOffset += 8; + numberOfBits -= 8; + + if (numberOfBits <= 8) + { + NetBitWriter.WriteByte((byte)(source >> 56), numberOfBits, destination, destinationBitOffset); + return returnValue; + } + NetBitWriter.WriteByte((byte)(source >> 56), 8, destination, destinationBitOffset); + destinationBitOffset += 8; + numberOfBits -= 8; + + return returnValue; + } + + // + // Variable size + // + + /// + /// Write Base128 encoded variable sized unsigned integer + /// + /// number of bytes written + [CLSCompliant(false)] + public static int WriteVariableUInt32(byte[] intoBuffer, int offset, uint value) + { + int retval = 0; + uint num1 = (uint)value; + while (num1 >= 0x80) + { + intoBuffer[offset + retval] = (byte)(num1 | 0x80); + num1 = num1 >> 7; + retval++; + } + intoBuffer[offset + retval] = (byte)num1; + return retval + 1; + } + + /// + /// Reads a UInt32 written using WriteUnsignedVarInt(); will increment offset! + /// + [CLSCompliant(false)] + public static uint ReadVariableUInt32(byte[] buffer, ref int offset) + { + int num1 = 0; + int num2 = 0; + while (true) + { + if (num2 == 0x23) + throw new FormatException("Bad 7-bit encoded integer"); + + byte num3 = buffer[offset++]; + num1 |= (num3 & 0x7f) << (num2 & 0x1f); + num2 += 7; + if ((num3 & 0x80) == 0) + return (uint)num1; + } + } + } +} diff --git a/Lidgren.Network/NetBuffer.Peek.cs b/Lidgren.Network/NetBuffer.Peek.cs new file mode 100644 index 000000000..fa4adc927 --- /dev/null +++ b/Lidgren.Network/NetBuffer.Peek.cs @@ -0,0 +1,312 @@ +/* Copyright (c) 2010 Michael Lidgren + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom +the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ +using System; +using System.Diagnostics; +using System.Net; + +namespace Lidgren.Network +{ + public partial class NetBuffer + { + /// + /// Gets the internal data buffer + /// + public byte[] PeekDataBuffer() { return m_data; } + + // + // 1 bit + // + /// + /// Reads a 1-bit Boolean without advancing the read pointer + /// + public bool PeekBoolean() + { + NetException.Assert(m_bitLength - m_readPosition >= 1, c_readOverflowError); + byte retval = NetBitWriter.ReadByte(m_data, 1, m_readPosition); + return (retval > 0 ? true : false); + } + + // + // 8 bit + // + /// + /// Reads a Byte without advancing the read pointer + /// + public byte PeekByte() + { + NetException.Assert(m_bitLength - m_readPosition >= 8, c_readOverflowError); + byte retval = NetBitWriter.ReadByte(m_data, 8, m_readPosition); + return retval; + } + + /// + /// Reads an SByte without advancing the read pointer + /// + [CLSCompliant(false)] + public sbyte PeekSByte() + { + NetException.Assert(m_bitLength - m_readPosition >= 8, c_readOverflowError); + byte retval = NetBitWriter.ReadByte(m_data, 8, m_readPosition); + return (sbyte)retval; + } + + /// + /// Reads the specified number of bits into a Byte without advancing the read pointer + /// + public byte PeekByte(int numberOfBits) + { + byte retval = NetBitWriter.ReadByte(m_data, numberOfBits, m_readPosition); + return retval; + } + + /// + /// Reads the specified number of bytes without advancing the read pointer + /// + public byte[] PeekBytes(int numberOfBytes) + { + NetException.Assert(m_bitLength - m_readPosition >= (numberOfBytes * 8), c_readOverflowError); + + byte[] retval = new byte[numberOfBytes]; + NetBitWriter.ReadBytes(m_data, numberOfBytes, m_readPosition, retval, 0); + return retval; + } + + /// + /// Reads the specified number of bytes without advancing the read pointer + /// + public void PeekBytes(byte[] into, int offset, int numberOfBytes) + { + NetException.Assert(m_bitLength - m_readPosition >= (numberOfBytes * 8), c_readOverflowError); + NetException.Assert(offset + numberOfBytes <= into.Length); + + NetBitWriter.ReadBytes(m_data, numberOfBytes, m_readPosition, into, offset); + return; + } + + // + // 16 bit + // + /// + /// Reads an Int16 without advancing the read pointer + /// + public Int16 PeekInt16() + { + NetException.Assert(m_bitLength - m_readPosition >= 16, c_readOverflowError); + uint retval = NetBitWriter.ReadUInt16(m_data, 16, m_readPosition); + return (short)retval; + } + + /// + /// Reads a UInt16 without advancing the read pointer + /// + [CLSCompliant(false)] + public UInt16 PeekUInt16() + { + NetException.Assert(m_bitLength - m_readPosition >= 16, c_readOverflowError); + uint retval = NetBitWriter.ReadUInt16(m_data, 16, m_readPosition); + return (ushort)retval; + } + + // + // 32 bit + // + /// + /// Reads an Int32 without advancing the read pointer + /// + public Int32 PeekInt32() + { + NetException.Assert(m_bitLength - m_readPosition >= 32, c_readOverflowError); + uint retval = NetBitWriter.ReadUInt32(m_data, 32, m_readPosition); + return (Int32)retval; + } + + /// + /// Reads the specified number of bits into an Int32 without advancing the read pointer + /// + public Int32 PeekInt32(int numberOfBits) + { + NetException.Assert((numberOfBits > 0 && numberOfBits <= 32), "ReadInt() can only read between 1 and 32 bits"); + NetException.Assert(m_bitLength - m_readPosition >= numberOfBits, c_readOverflowError); + + uint retval = NetBitWriter.ReadUInt32(m_data, numberOfBits, m_readPosition); + + if (numberOfBits == 32) + return (int)retval; + + int signBit = 1 << (numberOfBits - 1); + if ((retval & signBit) == 0) + return (int)retval; // positive + + // negative + unchecked + { + uint mask = ((uint)-1) >> (33 - numberOfBits); + uint tmp = (retval & mask) + 1; + return -((int)tmp); + } + } + + /// + /// Reads a UInt32 without advancing the read pointer + /// + [CLSCompliant(false)] + public UInt32 PeekUInt32() + { + NetException.Assert(m_bitLength - m_readPosition >= 32, c_readOverflowError); + uint retval = NetBitWriter.ReadUInt32(m_data, 32, m_readPosition); + return retval; + } + + /// + /// Reads the specified number of bits into a UInt32 without advancing the read pointer + /// + [CLSCompliant(false)] + public UInt32 PeekUInt32(int numberOfBits) + { + NetException.Assert((numberOfBits > 0 && numberOfBits <= 32), "ReadUInt() can only read between 1 and 32 bits"); + //NetException.Assert(m_bitLength - m_readBitPtr >= numberOfBits, "tried to read past buffer size"); + + UInt32 retval = NetBitWriter.ReadUInt32(m_data, numberOfBits, m_readPosition); + return retval; + } + + // + // 64 bit + // + /// + /// Reads a UInt64 without advancing the read pointer + /// + [CLSCompliant(false)] + public UInt64 PeekUInt64() + { + NetException.Assert(m_bitLength - m_readPosition >= 64, c_readOverflowError); + + ulong low = NetBitWriter.ReadUInt32(m_data, 32, m_readPosition); + ulong high = NetBitWriter.ReadUInt32(m_data, 32, m_readPosition + 32); + + ulong retval = low + (high << 32); + + return retval; + } + + /// + /// Reads an Int64 without advancing the read pointer + /// + public Int64 PeekInt64() + { + NetException.Assert(m_bitLength - m_readPosition >= 64, c_readOverflowError); + unchecked + { + ulong retval = PeekUInt64(); + long longRetval = (long)retval; + return longRetval; + } + } + + /// + /// Reads the specified number of bits into an UInt64 without advancing the read pointer + /// + [CLSCompliant(false)] + public UInt64 PeekUInt64(int numberOfBits) + { + NetException.Assert((numberOfBits > 0 && numberOfBits <= 64), "ReadUInt() can only read between 1 and 64 bits"); + NetException.Assert(m_bitLength - m_readPosition >= numberOfBits, c_readOverflowError); + + ulong retval; + if (numberOfBits <= 32) + { + retval = (ulong)NetBitWriter.ReadUInt32(m_data, numberOfBits, m_readPosition); + } + else + { + retval = NetBitWriter.ReadUInt32(m_data, 32, m_readPosition); + retval |= NetBitWriter.ReadUInt32(m_data, numberOfBits - 32, m_readPosition) << 32; + } + return retval; + } + + /// + /// Reads the specified number of bits into an Int64 without advancing the read pointer + /// + public Int64 PeekInt64(int numberOfBits) + { + NetException.Assert(((numberOfBits > 0) && (numberOfBits < 65)), "ReadInt64(bits) can only read between 1 and 64 bits"); + return (long)PeekUInt64(numberOfBits); + } + + // + // Floating point + // + /// + /// Reads a 32-bit Single without advancing the read pointer + /// + public float PeekFloat() + { + return PeekSingle(); + } + + /// + /// Reads a 32-bit Single without advancing the read pointer + /// + public float PeekSingle() + { + NetException.Assert(m_bitLength - m_readPosition >= 32, c_readOverflowError); + + if ((m_readPosition & 7) == 0) // read directly + { + float retval = BitConverter.ToSingle(m_data, m_readPosition >> 3); + return retval; + } + + byte[] bytes = PeekBytes(4); + return BitConverter.ToSingle(bytes, 0); + } + + /// + /// Reads a 64-bit Double without advancing the read pointer + /// + public double PeekDouble() + { + NetException.Assert(m_bitLength - m_readPosition >= 64, c_readOverflowError); + + if ((m_readPosition & 7) == 0) // read directly + { + // read directly + double retval = BitConverter.ToDouble(m_data, m_readPosition >> 3); + return retval; + } + + byte[] bytes = PeekBytes(8); + return BitConverter.ToDouble(bytes, 0); + } + + /// + /// Reads a string without advancing the read pointer + /// + public string PeekString() + { + int wasReadPosition = m_readPosition; + string retval = ReadString(); + m_readPosition = wasReadPosition; + return retval; + } + } +} + diff --git a/Lidgren.Network/NetBuffer.Read.Reflection.cs b/Lidgren.Network/NetBuffer.Read.Reflection.cs new file mode 100644 index 000000000..ed149f8e6 --- /dev/null +++ b/Lidgren.Network/NetBuffer.Read.Reflection.cs @@ -0,0 +1,103 @@ +/* Copyright (c) 2010 Michael Lidgren + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom +the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +using System; +using System.Reflection; + +namespace Lidgren.Network +{ + public partial class NetBuffer + { + /// + /// Reads all public and private declared instance fields of the object in alphabetical order using reflection + /// + public void ReadAllFields(object target) + { + ReadAllFields(target, BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + } + + /// + /// Reads all fields with the specified binding of the object in alphabetical order using reflection + /// + public void ReadAllFields(object target, BindingFlags flags) + { + if (target == null) + return; + Type tp = target.GetType(); + + FieldInfo[] fields = tp.GetFields(flags); + NetUtility.SortMembersList(fields); + + foreach (FieldInfo fi in fields) + { + object value; + + // find read method + MethodInfo readMethod; + if (s_readMethods.TryGetValue(fi.FieldType, out readMethod)) + { + // read value + value = readMethod.Invoke(this, null); + + // set the value + fi.SetValue(target, value); + } + } + } + + /// + /// Reads all public and private declared instance fields of the object in alphabetical order using reflection + /// + public void ReadAllProperties(object target) + { + ReadAllProperties(target, BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + } + + /// + /// Reads all fields with the specified binding of the object in alphabetical order using reflection + /// + public void ReadAllProperties(object target, BindingFlags flags) + { + if (target == null) + throw new ArgumentNullException("target"); + + if (target == null) + return; + Type tp = target.GetType(); + + PropertyInfo[] fields = tp.GetProperties(flags); + NetUtility.SortMembersList(fields); + foreach (PropertyInfo fi in fields) + { + object value; + + // find read method + MethodInfo readMethod; + if (s_readMethods.TryGetValue(fi.PropertyType, out readMethod)) + { + // read value + value = readMethod.Invoke(this, null); + + // set the value + MethodInfo setMethod = fi.GetSetMethod((flags & BindingFlags.NonPublic) == BindingFlags.NonPublic); + setMethod.Invoke(target, new object[] { value }); + } + } + } + } +} diff --git a/Lidgren.Network/NetBuffer.Read.cs b/Lidgren.Network/NetBuffer.Read.cs new file mode 100644 index 000000000..fb9dee844 --- /dev/null +++ b/Lidgren.Network/NetBuffer.Read.cs @@ -0,0 +1,657 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Reflection; +using System.Net; + +namespace Lidgren.Network +{ + /// + /// Base class for NetIncomingMessage and NetOutgoingMessage + /// + public partial class NetBuffer + { + private const string c_readOverflowError = "Trying to read past the buffer size - likely caused by mismatching Write/Reads, different size or order."; + + /// + /// Reads a boolean value (stored as a single bit) written using Write(bool) + /// + public bool ReadBoolean() + { + NetException.Assert(m_bitLength - m_readPosition >= 1, c_readOverflowError); + byte retval = NetBitWriter.ReadByte(m_data, 1, m_readPosition); + m_readPosition += 1; + return (retval > 0 ? true : false); + } + + /// + /// Reads a byte + /// + public byte ReadByte() + { + NetException.Assert(m_bitLength - m_readPosition >= 8, c_readOverflowError); + byte retval = NetBitWriter.ReadByte(m_data, 8, m_readPosition); + m_readPosition += 8; + return retval; + } + + /// + /// Reads a byte and returns true or false for success + /// + public bool ReadByte(out byte result) + { + if (m_bitLength - m_readPosition < 8) + { + result = 0; + return false; + } + result = NetBitWriter.ReadByte(m_data, 8, m_readPosition); + m_readPosition += 8; + return true; + } + + /// + /// Reads a signed byte + /// + [CLSCompliant(false)] + public sbyte ReadSByte() + { + NetException.Assert(m_bitLength - m_readPosition >= 8, c_readOverflowError); + byte retval = NetBitWriter.ReadByte(m_data, 8, m_readPosition); + m_readPosition += 8; + return (sbyte)retval; + } + + /// + /// Reads 1 to 8 bits into a byte + /// + public byte ReadByte(int numberOfBits) + { + NetException.Assert(numberOfBits > 0 && numberOfBits <= 8, "ReadByte(bits) can only read between 1 and 8 bits"); + byte retval = NetBitWriter.ReadByte(m_data, numberOfBits, m_readPosition); + m_readPosition += numberOfBits; + return retval; + } + + /// + /// Reads the specified number of bytes + /// + public byte[] ReadBytes(int numberOfBytes) + { + NetException.Assert(m_bitLength - m_readPosition + 7 >= (numberOfBytes * 8), c_readOverflowError); + + byte[] retval = new byte[numberOfBytes]; + NetBitWriter.ReadBytes(m_data, numberOfBytes, m_readPosition, retval, 0); + m_readPosition += (8 * numberOfBytes); + return retval; + } + + /// + /// Reads the specified number of bytes and returns true for success + /// + public bool ReadBytes(int numberOfBytes, out byte[] result) + { + if (m_bitLength - m_readPosition + 7 < (numberOfBytes * 8)) + { + result = null; + return false; + } + + result = new byte[numberOfBytes]; + NetBitWriter.ReadBytes(m_data, numberOfBytes, m_readPosition, result, 0); + m_readPosition += (8 * numberOfBytes); + return true; + } + + /// + /// Reads the specified number of bytes into a preallocated array + /// + /// The destination array + /// The offset where to start writing in the destination array + /// The number of bytes to read + public void ReadBytes(byte[] into, int offset, int numberOfBytes) + { + NetException.Assert(m_bitLength - m_readPosition + 7 >= (numberOfBytes * 8), c_readOverflowError); + NetException.Assert(offset + numberOfBytes <= into.Length); + + NetBitWriter.ReadBytes(m_data, numberOfBytes, m_readPosition, into, offset); + m_readPosition += (8 * numberOfBytes); + return; + } + + /// + /// Reads the specified number of bits into a preallocated array + /// + /// The destination array + /// The offset where to start writing in the destination array + /// The number of bits to read + public void ReadBits(byte[] into, int offset, int numberOfBits) + { + NetException.Assert(m_bitLength - m_readPosition >= numberOfBits, c_readOverflowError); + NetException.Assert(offset + NetUtility.BytesToHoldBits(numberOfBits) <= into.Length); + + int numberOfWholeBytes = numberOfBits / 8; + int extraBits = numberOfBits - (numberOfWholeBytes * 8); + + NetBitWriter.ReadBytes(m_data, numberOfWholeBytes, m_readPosition, into, offset); + m_readPosition += (8 * numberOfWholeBytes); + + if (extraBits > 0) + into[offset + numberOfWholeBytes] = ReadByte(extraBits); + + return; + } + + /// + /// Reads a 16 bit signed integer written using Write(Int16) + /// + public Int16 ReadInt16() + { + NetException.Assert(m_bitLength - m_readPosition >= 16, c_readOverflowError); + uint retval = NetBitWriter.ReadUInt16(m_data, 16, m_readPosition); + m_readPosition += 16; + return (short)retval; + } + + /// + /// Reads a 16 bit unsigned integer written using Write(UInt16) + /// + [CLSCompliant(false)] + public UInt16 ReadUInt16() + { + NetException.Assert(m_bitLength - m_readPosition >= 16, c_readOverflowError); + uint retval = NetBitWriter.ReadUInt16(m_data, 16, m_readPosition); + m_readPosition += 16; + return (ushort)retval; + } + + /// + /// Reads a 32 bit signed integer written using Write(Int32) + /// + public Int32 ReadInt32() + { + NetException.Assert(m_bitLength - m_readPosition >= 32, c_readOverflowError); + uint retval = NetBitWriter.ReadUInt32(m_data, 32, m_readPosition); + m_readPosition += 32; + return (Int32)retval; + } + + /// + /// Reads a 32 bit signed integer written using Write(Int32) + /// + [CLSCompliant(false)] + public bool ReadInt32(out Int32 result) + { + if (m_bitLength - m_readPosition < 32) + { + result = 0; + return false; + } + + result = (Int32)NetBitWriter.ReadUInt32(m_data, 32, m_readPosition); + m_readPosition += 32; + return true; + } + + /// + /// Reads a signed integer stored in 1 to 32 bits, written using Write(Int32, Int32) + /// + public Int32 ReadInt32(int numberOfBits) + { + NetException.Assert(numberOfBits > 0 && numberOfBits <= 32, "ReadInt32(bits) can only read between 1 and 32 bits"); + NetException.Assert(m_bitLength - m_readPosition >= numberOfBits, c_readOverflowError); + + uint retval = NetBitWriter.ReadUInt32(m_data, numberOfBits, m_readPosition); + m_readPosition += numberOfBits; + + if (numberOfBits == 32) + return (int)retval; + + int signBit = 1 << (numberOfBits - 1); + if ((retval & signBit) == 0) + return (int)retval; // positive + + // negative + unchecked + { + uint mask = ((uint)-1) >> (33 - numberOfBits); + uint tmp = (retval & mask) + 1; + return -((int)tmp); + } + } + + /// + /// Reads an 32 bit unsigned integer written using Write(UInt32) + /// + [CLSCompliant(false)] + public UInt32 ReadUInt32() + { + NetException.Assert(m_bitLength - m_readPosition >= 32, c_readOverflowError); + uint retval = NetBitWriter.ReadUInt32(m_data, 32, m_readPosition); + m_readPosition += 32; + return retval; + } + + /// + /// Reads an 32 bit unsigned integer written using Write(UInt32) and returns true for success + /// + [CLSCompliant(false)] + public bool ReadUInt32(out UInt32 result) + { + if (m_bitLength - m_readPosition < 32) + { + result = 0; + return false; + } + result = NetBitWriter.ReadUInt32(m_data, 32, m_readPosition); + m_readPosition += 32; + return true; + } + + /// + /// Reads an unsigned integer stored in 1 to 32 bits, written using Write(UInt32, Int32) + /// + [CLSCompliant(false)] + public UInt32 ReadUInt32(int numberOfBits) + { + NetException.Assert(numberOfBits > 0 && numberOfBits <= 32, "ReadUInt32(bits) can only read between 1 and 32 bits"); + //NetException.Assert(m_bitLength - m_readBitPtr >= numberOfBits, "tried to read past buffer size"); + + UInt32 retval = NetBitWriter.ReadUInt32(m_data, numberOfBits, m_readPosition); + m_readPosition += numberOfBits; + return retval; + } + + /// + /// Reads a 64 bit unsigned integer written using Write(UInt64) + /// + [CLSCompliant(false)] + public UInt64 ReadUInt64() + { + NetException.Assert(m_bitLength - m_readPosition >= 64, c_readOverflowError); + + ulong low = NetBitWriter.ReadUInt32(m_data, 32, m_readPosition); + m_readPosition += 32; + ulong high = NetBitWriter.ReadUInt32(m_data, 32, m_readPosition); + + ulong retval = low + (high << 32); + + m_readPosition += 32; + return retval; + } + + /// + /// Reads a 64 bit signed integer written using Write(Int64) + /// + public Int64 ReadInt64() + { + NetException.Assert(m_bitLength - m_readPosition >= 64, c_readOverflowError); + unchecked + { + ulong retval = ReadUInt64(); + long longRetval = (long)retval; + return longRetval; + } + } + + /// + /// Reads an unsigned integer stored in 1 to 64 bits, written using Write(UInt64, Int32) + /// + [CLSCompliant(false)] + public UInt64 ReadUInt64(int numberOfBits) + { + NetException.Assert(numberOfBits > 0 && numberOfBits <= 64, "ReadUInt64(bits) can only read between 1 and 64 bits"); + NetException.Assert(m_bitLength - m_readPosition >= numberOfBits, c_readOverflowError); + + ulong retval; + if (numberOfBits <= 32) + { + retval = (ulong)NetBitWriter.ReadUInt32(m_data, numberOfBits, m_readPosition); + } + else + { + retval = NetBitWriter.ReadUInt32(m_data, 32, m_readPosition); + retval |= NetBitWriter.ReadUInt32(m_data, numberOfBits - 32, m_readPosition) << 32; + } + m_readPosition += numberOfBits; + return retval; + } + + /// + /// Reads a signed integer stored in 1 to 64 bits, written using Write(Int64, Int32) + /// + public Int64 ReadInt64(int numberOfBits) + { + NetException.Assert(((numberOfBits > 0) && (numberOfBits <= 64)), "ReadInt64(bits) can only read between 1 and 64 bits"); + return (long)ReadUInt64(numberOfBits); + } + + /// + /// Reads a 32 bit floating point value written using Write(Single) + /// + public float ReadFloat() + { + return ReadSingle(); + } + + /// + /// Reads a 32 bit floating point value written using Write(Single) + /// + public float ReadSingle() + { + NetException.Assert(m_bitLength - m_readPosition >= 32, c_readOverflowError); + + if ((m_readPosition & 7) == 0) // read directly + { + float retval = BitConverter.ToSingle(m_data, m_readPosition >> 3); + m_readPosition += 32; + return retval; + } + + byte[] bytes = ReadBytes(4); + return BitConverter.ToSingle(bytes, 0); + } + + /// + /// Reads a 32 bit floating point value written using Write(Single) + /// + public bool ReadSingle(out float result) + { + if (m_bitLength - m_readPosition < 32) + { + result = 0.0f; + return false; + } + + if ((m_readPosition & 7) == 0) // read directly + { + result = BitConverter.ToSingle(m_data, m_readPosition >> 3); + m_readPosition += 32; + return true; + } + + byte[] bytes = ReadBytes(4); + result = BitConverter.ToSingle(bytes, 0); + return true; + } + + /// + /// Reads a 64 bit floating point value written using Write(Double) + /// + public double ReadDouble() + { + NetException.Assert(m_bitLength - m_readPosition >= 64, c_readOverflowError); + + if ((m_readPosition & 7) == 0) // read directly + { + // read directly + double retval = BitConverter.ToDouble(m_data, m_readPosition >> 3); + m_readPosition += 64; + return retval; + } + + byte[] bytes = ReadBytes(8); + return BitConverter.ToDouble(bytes, 0); + } + + // + // Variable bit count + // + + /// + /// Reads a variable sized UInt32 written using WriteVariableUInt32() + /// + [CLSCompliant(false)] + public uint ReadVariableUInt32() + { + int num1 = 0; + int num2 = 0; + while (true) + { + byte num3 = this.ReadByte(); + num1 |= (num3 & 0x7f) << num2; + num2 += 7; + if ((num3 & 0x80) == 0) + return (uint)num1; + } + } + + /// + /// Reads a variable sized UInt32 written using WriteVariableUInt32() and returns true for success + /// + [CLSCompliant(false)] + public bool ReadVariableUInt32(out uint result) + { + int num1 = 0; + int num2 = 0; + while (true) + { + byte num3; + if (ReadByte(out num3) == false) + { + result = 0; + return false; + } + num1 |= (num3 & 0x7f) << num2; + num2 += 7; + if ((num3 & 0x80) == 0) + { + result = (uint)num1; + return true; + } + } + } + + /// + /// Reads a variable sized Int32 written using WriteVariableInt32() + /// + public int ReadVariableInt32() + { + uint n = ReadVariableUInt32(); + return (int)(n >> 1) ^ -(int)(n & 1); // decode zigzag + } + + /// + /// Reads a variable sized Int64 written using WriteVariableInt64() + /// + public Int64 ReadVariableInt64() + { + UInt64 n = ReadVariableUInt64(); + return (Int64)(n >> 1) ^ -(long)(n & 1); // decode zigzag + } + + /// + /// Reads a variable sized UInt32 written using WriteVariableInt64() + /// + [CLSCompliant(false)] + public UInt64 ReadVariableUInt64() + { + UInt64 num1 = 0; + int num2 = 0; + while (true) + { + //if (num2 == 0x23) + // throw new FormatException("Bad 7-bit encoded integer"); + + byte num3 = this.ReadByte(); + num1 |= ((UInt64)num3 & 0x7f) << num2; + num2 += 7; + if ((num3 & 0x80) == 0) + return num1; + } + } + + /// + /// Reads a 32 bit floating point value written using WriteSignedSingle() + /// + /// The number of bits used when writing the value + /// A floating point value larger or equal to -1 and smaller or equal to 1 + public float ReadSignedSingle(int numberOfBits) + { + uint encodedVal = ReadUInt32(numberOfBits); + int maxVal = (1 << numberOfBits) - 1; + return ((float)(encodedVal + 1) / (float)(maxVal + 1) - 0.5f) * 2.0f; + } + + /// + /// Reads a 32 bit floating point value written using WriteUnitSingle() + /// + /// The number of bits used when writing the value + /// A floating point value larger or equal to 0 and smaller or equal to 1 + public float ReadUnitSingle(int numberOfBits) + { + uint encodedVal = ReadUInt32(numberOfBits); + int maxVal = (1 << numberOfBits) - 1; + return (float)(encodedVal + 1) / (float)(maxVal + 1); + } + + /// + /// Reads a 32 bit floating point value written using WriteRangedSingle() + /// + /// The minimum value used when writing the value + /// The maximum value used when writing the value + /// The number of bits used when writing the value + /// A floating point value larger or equal to MIN and smaller or equal to MAX + public float ReadRangedSingle(float min, float max, int numberOfBits) + { + float range = max - min; + int maxVal = (1 << numberOfBits) - 1; + float encodedVal = (float)ReadUInt32(numberOfBits); + float unit = encodedVal / (float)maxVal; + return min + (unit * range); + } + + /// + /// Reads a 32 bit integer value written using WriteRangedInteger() + /// + /// The minimum value used when writing the value + /// The maximum value used when writing the value + /// A signed integer value larger or equal to MIN and smaller or equal to MAX + public int ReadRangedInteger(int min, int max) + { + uint range = (uint)(max - min); + int numBits = NetUtility.BitsToHoldUInt(range); + + uint rvalue = ReadUInt32(numBits); + return (int)(min + rvalue); + } + + /// + /// Reads a string written using Write(string) + /// + public string ReadString() + { + int byteLen = (int)ReadVariableUInt32(); + + if (byteLen == 0) + return String.Empty; + + NetException.Assert(m_bitLength - m_readPosition >= (byteLen * 8), c_readOverflowError); + + if ((m_readPosition & 7) == 0) + { + // read directly + string retval = System.Text.Encoding.UTF8.GetString(m_data, m_readPosition >> 3, byteLen); + m_readPosition += (8 * byteLen); + return retval; + } + + byte[] bytes = ReadBytes(byteLen); + return System.Text.Encoding.UTF8.GetString(bytes, 0, bytes.Length); + } + + /// + /// Reads a string written using Write(string) and returns true for success + /// + public bool ReadString(out string result) + { + uint byteLen; + if (ReadVariableUInt32(out byteLen) == false) + { + result = String.Empty; + return false; + } + + if (byteLen == 0) + { + result = String.Empty; + return true; + } + + if (m_bitLength - m_readPosition < (byteLen * 8)) + { + result = String.Empty; + return false; + } + + if ((m_readPosition & 7) == 0) + { + // read directly + result = System.Text.Encoding.UTF8.GetString(m_data, m_readPosition >> 3, (int)byteLen); + m_readPosition += (8 * (int)byteLen); + return true; + } + + byte[] bytes; + if (ReadBytes((int)byteLen, out bytes) == false) + { + result = String.Empty; + return false; + } + + result = System.Text.Encoding.UTF8.GetString(bytes, 0, bytes.Length); + return true; + } + + /// + /// Reads a value, in local time comparable to NetTime.Now, written using WriteTime() for the connection supplied + /// + public double ReadTime(NetConnection connection, bool highPrecision) + { + double remoteTime = (highPrecision ? ReadDouble() : (double)ReadSingle()); + + if (connection == null) + throw new NetException("Cannot call ReadTime() on message without a connected sender (ie. unconnected messages)"); + + // lets bypass NetConnection.GetLocalTime for speed + return remoteTime - connection.m_remoteTimeOffset; + } + + /// + /// Reads a stored IPv4 endpoint description + /// + public IPEndPoint ReadIPEndPoint() + { + byte len = ReadByte(); + byte[] addressBytes = ReadBytes(len); + int port = (int)ReadUInt16(); + + IPAddress address = new IPAddress(addressBytes); + return new IPEndPoint(address, port); + } + + /// + /// Pads data with enough bits to reach a full byte. Decreases cpu usage for subsequent byte writes. + /// + public void SkipPadBits() + { + m_readPosition = ((m_readPosition + 7) >> 3) * 8; + } + + /// + /// Pads data with enough bits to reach a full byte. Decreases cpu usage for subsequent byte writes. + /// + public void ReadPadBits() + { + m_readPosition = ((m_readPosition + 7) >> 3) * 8; + } + + /// + /// Pads data with the specified number of bits. + /// + public void SkipPadBits(int numberOfBits) + { + m_readPosition += numberOfBits; + } + } +} diff --git a/Lidgren.Network/NetBuffer.Write.Reflection.cs b/Lidgren.Network/NetBuffer.Write.Reflection.cs new file mode 100644 index 000000000..99782b7f7 --- /dev/null +++ b/Lidgren.Network/NetBuffer.Write.Reflection.cs @@ -0,0 +1,91 @@ +/* Copyright (c) 2010 Michael Lidgren + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom +the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +using System; +using System.Reflection; + +namespace Lidgren.Network +{ + public partial class NetBuffer + { + /// + /// Writes all public and private declared instance fields of the object in alphabetical order using reflection + /// + public void WriteAllFields(object ob) + { + WriteAllFields(ob, BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + } + + /// + /// Writes all fields with specified binding in alphabetical order using reflection + /// + public void WriteAllFields(object ob, BindingFlags flags) + { + if (ob == null) + return; + Type tp = ob.GetType(); + + FieldInfo[] fields = tp.GetFields(flags); + NetUtility.SortMembersList(fields); + + foreach (FieldInfo fi in fields) + { + object value = fi.GetValue(ob); + + // find the appropriate Write method + MethodInfo writeMethod; + if (s_writeMethods.TryGetValue(fi.FieldType, out writeMethod)) + writeMethod.Invoke(this, new object[] { value }); + else + throw new NetException("Failed to find write method for type " + fi.FieldType); + } + } + + /// + /// Writes all public and private declared instance properties of the object in alphabetical order using reflection + /// + public void WriteAllProperties(object ob) + { + WriteAllProperties(ob, BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + } + + /// + /// Writes all properties with specified binding in alphabetical order using reflection + /// + public void WriteAllProperties(object ob, BindingFlags flags) + { + if (ob == null) + return; + Type tp = ob.GetType(); + + PropertyInfo[] fields = tp.GetProperties(flags); + NetUtility.SortMembersList(fields); + + foreach (PropertyInfo fi in fields) + { + MethodInfo getMethod = fi.GetGetMethod((flags & BindingFlags.NonPublic) == BindingFlags.NonPublic); + object value = getMethod.Invoke(ob, null); + + // find the appropriate Write method + MethodInfo writeMethod; + if (s_writeMethods.TryGetValue(fi.PropertyType, out writeMethod)) + writeMethod.Invoke(this, new object[] { value }); + } + } + } +} \ No newline at end of file diff --git a/Lidgren.Network/NetBuffer.Write.cs b/Lidgren.Network/NetBuffer.Write.cs new file mode 100644 index 000000000..1b881681b --- /dev/null +++ b/Lidgren.Network/NetBuffer.Write.cs @@ -0,0 +1,619 @@ +//#define UNSAFE +//#define BIGENDIAN +/* Copyright (c) 2010 Michael Lidgren + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom +the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +using System; +using System.Collections.Generic; +using System.Net; +using System.Reflection; +using System.Text; +using System.Runtime.InteropServices; + +namespace Lidgren.Network +{ + /// + /// Utility struct for writing Singles + /// + [StructLayout(LayoutKind.Explicit)] + public struct SingleUIntUnion + { + /// + /// Value as a 32 bit float + /// + [FieldOffset(0)] + public float SingleValue; + + /// + /// Value as an unsigned 32 bit integer + /// + [FieldOffset(0)] + [CLSCompliant(false)] + public uint UIntValue; + } + + public partial class NetBuffer + { + /// + /// Ensures the buffer can hold this number of bits + /// + public void EnsureBufferSize(int numberOfBits) + { + int byteLen = ((numberOfBits + 7) >> 3); + if (m_data == null) + { + m_data = new byte[byteLen + c_overAllocateAmount]; + return; + } + if (m_data.Length < byteLen) + Array.Resize(ref m_data, byteLen + c_overAllocateAmount); + return; + } + + /// + /// Ensures the buffer can hold this number of bits + /// + internal void InternalEnsureBufferSize(int numberOfBits) + { + int byteLen = ((numberOfBits + 7) >> 3); + if (m_data == null) + { + m_data = new byte[byteLen]; + return; + } + if (m_data.Length < byteLen) + Array.Resize(ref m_data, byteLen); + return; + } + + /// + /// Writes a boolean value using 1 bit + /// + public void Write(bool value) + { + EnsureBufferSize(m_bitLength + 1); + NetBitWriter.WriteByte((value ? (byte)1 : (byte)0), 1, m_data, m_bitLength); + m_bitLength += 1; + } + + /// + /// Write a byte + /// + public void Write(byte source) + { + EnsureBufferSize(m_bitLength + 8); + NetBitWriter.WriteByte(source, 8, m_data, m_bitLength); + m_bitLength += 8; + } + + /// + /// Writes a signed byte + /// + [CLSCompliant(false)] + public void Write(sbyte source) + { + EnsureBufferSize(m_bitLength + 8); + NetBitWriter.WriteByte((byte)source, 8, m_data, m_bitLength); + m_bitLength += 8; + } + + /// + /// Writes 1 to 8 bits of a byte + /// + public void Write(byte source, int numberOfBits) + { + NetException.Assert((numberOfBits > 0 && numberOfBits <= 8), "Write(byte, numberOfBits) can only write between 1 and 8 bits"); + EnsureBufferSize(m_bitLength + numberOfBits); + NetBitWriter.WriteByte(source, numberOfBits, m_data, m_bitLength); + m_bitLength += numberOfBits; + } + + /// + /// Writes all bytes in an array + /// + public void Write(byte[] source) + { + if (source == null) + throw new ArgumentNullException("source"); + int bits = source.Length * 8; + EnsureBufferSize(m_bitLength + bits); + NetBitWriter.WriteBytes(source, 0, source.Length, m_data, m_bitLength); + m_bitLength += bits; + } + + /// + /// Writes the specified number of bytes from an array + /// + public void Write(byte[] source, int offsetInBytes, int numberOfBytes) + { + if (source == null) + throw new ArgumentNullException("source"); + int bits = numberOfBytes * 8; + EnsureBufferSize(m_bitLength + bits); + NetBitWriter.WriteBytes(source, offsetInBytes, numberOfBytes, m_data, m_bitLength); + m_bitLength += bits; + } + + /// + /// Writes an unsigned 16 bit integer + /// + /// + [CLSCompliant(false)] + public void Write(UInt16 source) + { + EnsureBufferSize(m_bitLength + 16); + NetBitWriter.WriteUInt16(source, 16, m_data, m_bitLength); + m_bitLength += 16; + } + + /// + /// Writes an unsigned integer using 1 to 16 bits + /// + [CLSCompliant(false)] + public void Write(UInt16 source, int numberOfBits) + { + NetException.Assert((numberOfBits > 0 && numberOfBits <= 16), "Write(ushort, numberOfBits) can only write between 1 and 16 bits"); + EnsureBufferSize(m_bitLength + numberOfBits); + NetBitWriter.WriteUInt16(source, numberOfBits, m_data, m_bitLength); + m_bitLength += numberOfBits; + } + + /// + /// Writes a signed 16 bit integer + /// + public void Write(Int16 source) + { + EnsureBufferSize(m_bitLength + 16); + NetBitWriter.WriteUInt16((ushort)source, 16, m_data, m_bitLength); + m_bitLength += 16; + } + +#if UNSAFE + /// + /// Writes a 32 bit signed integer + /// + public unsafe void Write(Int32 source) + { + EnsureBufferSize(m_bitLength + 32); + + // can write fast? + if (m_bitLength % 8 == 0) + { + fixed (byte* numRef = &Data[m_bitLength / 8]) + { + *((int*)numRef) = source; + } + } + else + { + NetBitWriter.WriteUInt32((UInt32)source, 32, Data, m_bitLength); + } + m_bitLength += 32; + } +#else + /// + /// Writes a 32 bit signed integer + /// + public void Write(Int32 source) + { + EnsureBufferSize(m_bitLength + 32); + NetBitWriter.WriteUInt32((UInt32)source, 32, m_data, m_bitLength); + m_bitLength += 32; + } +#endif + +#if UNSAFE + /// + /// Writes a 32 bit unsigned integer + /// + public unsafe void Write(UInt32 source) + { + EnsureBufferSize(m_bitLength + 32); + + // can write fast? + if (m_bitLength % 8 == 0) + { + fixed (byte* numRef = &Data[m_bitLength / 8]) + { + *((uint*)numRef) = source; + } + } + else + { + NetBitWriter.WriteUInt32(source, 32, Data, m_bitLength); + } + + m_bitLength += 32; + } +#else + /// + /// Writes a 32 bit unsigned integer + /// + [CLSCompliant(false)] + public void Write(UInt32 source) + { + EnsureBufferSize(m_bitLength + 32); + NetBitWriter.WriteUInt32(source, 32, m_data, m_bitLength); + m_bitLength += 32; + } +#endif + + /// + /// Writes a 32 bit signed integer + /// + [CLSCompliant(false)] + public void Write(UInt32 source, int numberOfBits) + { + NetException.Assert((numberOfBits > 0 && numberOfBits <= 32), "Write(uint, numberOfBits) can only write between 1 and 32 bits"); + EnsureBufferSize(m_bitLength + numberOfBits); + NetBitWriter.WriteUInt32(source, numberOfBits, m_data, m_bitLength); + m_bitLength += numberOfBits; + } + + /// + /// Writes a signed integer using 1 to 32 bits + /// + public void Write(Int32 source, int numberOfBits) + { + NetException.Assert((numberOfBits > 0 && numberOfBits <= 32), "Write(int, numberOfBits) can only write between 1 and 32 bits"); + EnsureBufferSize(m_bitLength + numberOfBits); + + if (numberOfBits != 32) + { + // make first bit sign + int signBit = 1 << (numberOfBits - 1); + if (source < 0) + source = (-source - 1) | signBit; + else + source &= (~signBit); + } + + NetBitWriter.WriteUInt32((uint)source, numberOfBits, m_data, m_bitLength); + + m_bitLength += numberOfBits; + } + + /// + /// Writes a 64 bit unsigned integer + /// + [CLSCompliant(false)] + public void Write(UInt64 source) + { + EnsureBufferSize(m_bitLength + 64); + NetBitWriter.WriteUInt64(source, 64, m_data, m_bitLength); + m_bitLength += 64; + } + + /// + /// Writes an unsigned integer using 1 to 64 bits + /// + [CLSCompliant(false)] + public void Write(UInt64 source, int numberOfBits) + { + EnsureBufferSize(m_bitLength + numberOfBits); + NetBitWriter.WriteUInt64(source, numberOfBits, m_data, m_bitLength); + m_bitLength += numberOfBits; + } + + /// + /// Writes a 64 bit signed integer + /// + public void Write(Int64 source) + { + EnsureBufferSize(m_bitLength + 64); + ulong usource = (ulong)source; + NetBitWriter.WriteUInt64(usource, 64, m_data, m_bitLength); + m_bitLength += 64; + } + + /// + /// Writes a signed integer using 1 to 64 bits + /// + public void Write(Int64 source, int numberOfBits) + { + EnsureBufferSize(m_bitLength + numberOfBits); + ulong usource = (ulong)source; + NetBitWriter.WriteUInt64(usource, numberOfBits, m_data, m_bitLength); + m_bitLength += numberOfBits; + } + + // + // Floating point + // +#if UNSAFE + /// + /// Writes a 32 bit floating point value + /// + public unsafe void Write(float source) + { + uint val = *((uint*)&source); +#if BIGENDIAN + val = NetUtility.SwapByteOrder(val); +#endif + Write(val); + } +#else + /// + /// Writes a 32 bit floating point value + /// + public void Write(float source) + { + // Use union to avoid BitConverter.GetBytes() which allocates memory on the heap + SingleUIntUnion su; + su.UIntValue = 0; // must initialize every member of the union to avoid warning + su.SingleValue = source; + +#if BIGENDIAN + // swap byte order + su.UIntValue = NetUtility.SwapByteOrder(su.UIntValue); +#endif + Write(su.UIntValue); + } +#endif + +#if UNSAFE + /// + /// Writes a 64 bit floating point value + /// + public unsafe void Write(double source) + { + ulong val = *((ulong*)&source); +#if BIGENDIAN + val = NetUtility.SwapByteOrder(val); +#endif + Write(val); + } +#else + /// + /// Writes a 64 bit floating point value + /// + public void Write(double source) + { + byte[] val = BitConverter.GetBytes(source); +#if BIGENDIAN + // 0 1 2 3 4 5 6 7 + + // swap byte order + byte tmp = val[7]; + val[7] = val[0]; + val[0] = tmp; + + tmp = val[6]; + val[6] = val[1]; + val[1] = tmp; + + tmp = val[5]; + val[5] = val[2]; + val[2] = tmp; + + tmp = val[4]; + val[4] = val[3]; + val[3] = tmp; +#endif + Write(val); + } +#endif + + // + // Variable bits + // + + /// + /// Write Base128 encoded variable sized unsigned integer of up to 32 bits + /// + /// number of bytes written + [CLSCompliant(false)] + public int WriteVariableUInt32(uint value) + { + int retval = 1; + uint num1 = (uint)value; + while (num1 >= 0x80) + { + this.Write((byte)(num1 | 0x80)); + num1 = num1 >> 7; + retval++; + } + this.Write((byte)num1); + return retval; + } + + /// + /// Write Base128 encoded variable sized signed integer of up to 32 bits + /// + /// number of bytes written + public int WriteVariableInt32(int value) + { + uint zigzag = (uint)(value << 1) ^ (uint)(value >> 31); + return WriteVariableUInt32(zigzag); + } + + /// + /// Write Base128 encoded variable sized signed integer of up to 64 bits + /// + /// number of bytes written + public int WriteVariableInt64(Int64 value) + { + ulong zigzag = (ulong)(value << 1) ^ (ulong)(value >> 63); + return WriteVariableUInt64(zigzag); + } + + /// + /// Write Base128 encoded variable sized unsigned integer of up to 64 bits + /// + /// number of bytes written + [CLSCompliant(false)] + public int WriteVariableUInt64(UInt64 value) + { + int retval = 1; + UInt64 num1 = (UInt64)value; + while (num1 >= 0x80) + { + this.Write((byte)(num1 | 0x80)); + num1 = num1 >> 7; + retval++; + } + this.Write((byte)num1); + return retval; + } + + /// + /// Compress (lossy) a float in the range -1..1 using numberOfBits bits + /// + public void WriteSignedSingle(float value, int numberOfBits) + { + NetException.Assert(((value >= -1.0) && (value <= 1.0)), " WriteSignedSingle() must be passed a float in the range -1 to 1; val is " + value); + + float unit = (value + 1.0f) * 0.5f; + int maxVal = (1 << numberOfBits) - 1; + uint writeVal = (uint)(unit * (float)maxVal); + + Write(writeVal, numberOfBits); + } + + /// + /// Compress (lossy) a float in the range 0..1 using numberOfBits bits + /// + public void WriteUnitSingle(float value, int numberOfBits) + { + NetException.Assert(((value >= 0.0) && (value <= 1.0)), " WriteUnitSingle() must be passed a float in the range 0 to 1; val is " + value); + + int maxValue = (1 << numberOfBits) - 1; + uint writeVal = (uint)(value * (float)maxValue); + + Write(writeVal, numberOfBits); + } + + /// + /// Compress a float within a specified range using a certain number of bits + /// + public void WriteRangedSingle(float value, float min, float max, int numberOfBits) + { + NetException.Assert(((value >= min) && (value <= max)), " WriteRangedSingle() must be passed a float in the range MIN to MAX; val is " + value); + + float range = max - min; + float unit = ((value - min) / range); + int maxVal = (1 << numberOfBits) - 1; + Write((UInt32)((float)maxVal * unit), numberOfBits); + } + + /// + /// Writes an integer with the least amount of bits need for the specified range + /// Returns number of bits written + /// + public int WriteRangedInteger(int min, int max, int value) + { + NetException.Assert(value >= min && value <= max, "Value not within min/max range!"); + + uint range = (uint)(max - min); + int numBits = NetUtility.BitsToHoldUInt(range); + + uint rvalue = (uint)(value - min); + Write(rvalue, numBits); + + return numBits; + } + + /// + /// Write a string + /// + public void Write(string source) + { + if (string.IsNullOrEmpty(source)) + { + EnsureBufferSize(m_bitLength + 8); + WriteVariableUInt32(0); + return; + } + + byte[] bytes = Encoding.UTF8.GetBytes(source); + EnsureBufferSize(m_bitLength + 8 + (bytes.Length * 8)); + WriteVariableUInt32((uint)bytes.Length); + Write(bytes); + } + + /// + /// Writes an endpoint description + /// + public void Write(IPEndPoint endPoint) + { + byte[] bytes = endPoint.Address.GetAddressBytes(); + Write((byte)bytes.Length); + Write(bytes); + Write((ushort)endPoint.Port); + } + + /// + /// Writes the current local time to a message; readable (and convertable to local time) by the remote host using ReadTime() + /// + public void WriteTime(bool highPrecision) + { + double localTime = NetTime.Now; + if (highPrecision) + Write(localTime); + else + Write((float)localTime); + } + + /// + /// Writes a local timestamp to a message; readable (and convertable to local time) by the remote host using ReadTime() + /// + public void WriteTime(double localTime, bool highPrecision) + { + if (highPrecision) + Write(localTime); + else + Write((float)localTime); + } + + /// + /// Pads data with enough bits to reach a full byte. Decreases cpu usage for subsequent byte writes. + /// + public void WritePadBits() + { + m_bitLength = ((m_bitLength + 7) >> 3) * 8; + EnsureBufferSize(m_bitLength); + } + + /// + /// Pads data with the specified number of bits. + /// + public void WritePadBits(int numberOfBits) + { + m_bitLength += numberOfBits; + EnsureBufferSize(m_bitLength); + } + + /// + /// Append all the bits of message to this message + /// + public void Write(NetBuffer buffer) + { + EnsureBufferSize(m_bitLength + (buffer.LengthBytes * 8)); + + Write(buffer.m_data, 0, buffer.LengthBytes); + + // did we write excessive bits? + int bitsInLastByte = (buffer.m_bitLength % 8); + if (bitsInLastByte != 0) + { + int excessBits = 8 - bitsInLastByte; + m_bitLength -= excessBits; + } + } + } +} diff --git a/Lidgren.Network/NetBuffer.cs b/Lidgren.Network/NetBuffer.cs new file mode 100644 index 000000000..34c783fde --- /dev/null +++ b/Lidgren.Network/NetBuffer.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Lidgren.Network +{ + public partial class NetBuffer + { + /// + /// Number of bytes to overallocate for each message to avoid resizing + /// + protected const int c_overAllocateAmount = 4; + + private static readonly Dictionary s_readMethods; + private static readonly Dictionary s_writeMethods; + + internal byte[] m_data; + internal int m_bitLength; + internal int m_readPosition; + + /// + /// Gets or sets the internal data buffer + /// + public byte[] Data + { + get { return m_data; } + set { m_data = value; } + } + + /// + /// Gets or sets the length of the used portion of the buffer in bytes + /// + public int LengthBytes + { + get { return ((m_bitLength + 7) >> 3); } + set + { + m_bitLength = value * 8; + InternalEnsureBufferSize(m_bitLength); + } + } + + /// + /// Gets or sets the length of the used portion of the buffer in bits + /// + public int LengthBits + { + get { return m_bitLength; } + set + { + m_bitLength = value; + InternalEnsureBufferSize(m_bitLength); + } + } + + /// + /// Gets or sets the read position in the buffer, in bits (not bytes) + /// + public long Position + { + get { return (long)m_readPosition; } + set { m_readPosition = (int)value; } + } + + /// + /// Gets the position in the buffer in bytes; note that the bits of the first returned byte may already have been read - check the Position property to make sure. + /// + public int PositionInBytes + { + get { return (int)(m_readPosition / 8); } + } + + static NetBuffer() + { + s_readMethods = new Dictionary(); + MethodInfo[] methods = typeof(NetIncomingMessage).GetMethods(BindingFlags.Instance | BindingFlags.Public); + foreach (MethodInfo mi in methods) + { + if (mi.GetParameters().Length == 0 && mi.Name.StartsWith("Read", StringComparison.InvariantCulture) && mi.Name.Substring(4) == mi.ReturnType.Name) + { + s_readMethods[mi.ReturnType] = mi; + } + } + + s_writeMethods = new Dictionary(); + methods = typeof(NetOutgoingMessage).GetMethods(BindingFlags.Instance | BindingFlags.Public); + foreach (MethodInfo mi in methods) + { + if (mi.Name.Equals("Write", StringComparison.InvariantCulture)) + { + ParameterInfo[] pis = mi.GetParameters(); + if (pis.Length == 1) + s_writeMethods[pis[0].ParameterType] = mi; + } + } + } + } +} diff --git a/Lidgren.Network/NetClient.cs b/Lidgren.Network/NetClient.cs new file mode 100644 index 000000000..588de4929 --- /dev/null +++ b/Lidgren.Network/NetClient.cs @@ -0,0 +1,171 @@ +/* Copyright (c) 2010 Michael Lidgren + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom +the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +using System; +using System.Net; + +namespace Lidgren.Network +{ + /// + /// Specialized version of NetPeer used for a "client" connection. It does not accept any incoming connections and maintains a ServerConnection property + /// + public class NetClient : NetPeer + { + /// + /// Gets the connection to the server, if any + /// + public NetConnection ServerConnection + { + get + { + NetConnection retval = null; + if (m_connections.Count > 0) + { + try + { + retval = m_connections[0]; + } + catch + { + // preempted! + return null; + } + } + return retval; + } + } + + /// + /// Gets the connection status of the server connection (or NetConnectionStatus.Disconnected if no connection) + /// + public NetConnectionStatus ConnectionStatus + { + get + { + var conn = ServerConnection; + if (conn == null) + return NetConnectionStatus.Disconnected; + return conn.Status; + } + } + + /// + /// NetClient constructor + /// + /// + public NetClient(NetPeerConfiguration config) + : base(config) + { + config.AcceptIncomingConnections = false; + } + + /// + /// Connect to a remote server + /// + /// The remote endpoint to connect to + /// The hail message to pass + /// server connection, or null if already connected + public override NetConnection Connect(IPEndPoint remoteEndPoint, NetOutgoingMessage hailMessage) + { + lock (m_connections) + { + if (m_connections.Count > 0) + { + LogWarning("Connect attempt failed; Already connected"); + return null; + } + } + + lock (m_handshakes) + { + if (m_handshakes.Count > 0) + { + LogWarning("Connect attempt failed; Handshake already in progress"); + return null; + } + } + + return base.Connect(remoteEndPoint, hailMessage); + } + + /// + /// Disconnect from server + /// + /// reason for disconnect + public void Disconnect(string byeMessage) + { + NetConnection serverConnection = ServerConnection; + if (serverConnection == null) + { + lock (m_handshakes) + { + if (m_handshakes.Count > 0) + { + LogVerbose("Aborting connection attempt"); + foreach(var hs in m_handshakes) + hs.Value.Disconnect(byeMessage); + return; + } + } + + LogWarning("Disconnect requested when not connected!"); + return; + } + serverConnection.Disconnect(byeMessage); + } + + /// + /// Sends message to server + /// + public NetSendResult SendMessage(NetOutgoingMessage msg, NetDeliveryMethod method) + { + NetConnection serverConnection = ServerConnection; + if (serverConnection == null) + { + LogWarning("Cannot send message, no server connection!"); + return NetSendResult.FailedNotConnected; + } + + return serverConnection.SendMessage(msg, method, 0); + } + + /// + /// Sends message to server + /// + public NetSendResult SendMessage(NetOutgoingMessage msg, NetDeliveryMethod method, int sequenceChannel) + { + NetConnection serverConnection = ServerConnection; + if (serverConnection == null) + { + LogWarning("Cannot send message, no server connection!"); + return NetSendResult.FailedNotConnected; + } + + return serverConnection.SendMessage(msg, method, sequenceChannel); + } + + /// + /// Returns a string that represents this object + /// + public override string ToString() + { + return "[NetClient " + ServerConnection + "]"; + } + + } +} diff --git a/Lidgren.Network/NetConnection.Handshake.cs b/Lidgren.Network/NetConnection.Handshake.cs new file mode 100644 index 000000000..d643c3b9f --- /dev/null +++ b/Lidgren.Network/NetConnection.Handshake.cs @@ -0,0 +1,479 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Lidgren.Network +{ + public partial class NetConnection + { + internal bool m_connectRequested; + internal bool m_disconnectRequested; + internal bool m_connectionInitiator; + internal string m_disconnectMessage; + internal NetIncomingMessage m_remoteHailMessage; + internal float m_lastHandshakeSendTime; + internal int m_handshakeAttempts; + + /// + /// The message that the remote part specified via Connect() or Approve() - can be null. + /// + public NetIncomingMessage RemoteHailMessage { get { return m_remoteHailMessage; } } + + // heartbeat called when connection still is in m_handshakes of NetPeer + internal void UnconnectedHeartbeat(float now) + { + m_peer.VerifyNetworkThread(); + + if (m_disconnectRequested) + ExecuteDisconnect(m_disconnectMessage, true); + + if (m_connectRequested) + { + switch (m_status) + { + case NetConnectionStatus.Connected: + case NetConnectionStatus.RespondedConnect: + // reconnect + ExecuteDisconnect("Reconnecting", true); + break; + + case NetConnectionStatus.InitiatedConnect: + // send another connect attempt + SendConnect(now); + break; + + case NetConnectionStatus.Disconnected: + throw new NetException("This connection is Disconnected; spent. A new one should have been created"); + + case NetConnectionStatus.Disconnecting: + // let disconnect finish first + break; + + case NetConnectionStatus.None: + default: + SendConnect(now); + break; + } + return; + } + + if (now - m_lastHandshakeSendTime > m_peerConfiguration.m_resendHandshakeInterval) + { + if (m_handshakeAttempts >= m_peerConfiguration.m_maximumHandshakeAttempts) + { + // failed to connect + ExecuteDisconnect("Failed to establish connection - no response from remote host", true); + return; + } + + // resend handshake + switch (m_status) + { + case NetConnectionStatus.InitiatedConnect: + SendConnect(now); + break; + case NetConnectionStatus.RespondedConnect: + SendConnectResponse(now, true); + break; + case NetConnectionStatus.None: + case NetConnectionStatus.ReceivedInitiation: + m_peer.LogWarning("Time to resend handshake, but status is " + m_status); + break; + case NetConnectionStatus.RespondedAwaitingApproval: + // awaiting approval + m_lastHandshakeSendTime = now; // postpone handshake resend + break; + default: + m_peer.LogWarning("Time to resend handshake, but status is " + m_status); + break; + } + } + } + + internal void ExecuteDisconnect(string reason, bool sendByeMessage) + { + m_peer.VerifyNetworkThread(); + + //m_peer.LogDebug("Executing disconnect"); + + // clear send queues + for (int i = 0; i < m_sendChannels.Length; i++) + { + NetSenderChannelBase channel = m_sendChannels[i]; + if (channel != null) + channel.Reset(); + } + + if (sendByeMessage) + SendDisconnect(reason, true); + + SetStatus(NetConnectionStatus.Disconnected, reason); + + // in case we're still in handshake + lock (m_peer.m_handshakes) + m_peer.m_handshakes.Remove(m_remoteEndPoint); + + m_disconnectRequested = false; + m_connectRequested = false; + m_handshakeAttempts = 0; + } + + internal void SendConnect(float now) + { + m_peer.VerifyNetworkThread(); + + int preAllocate = 13 + m_peerConfiguration.AppIdentifier.Length; + preAllocate += (m_localHailMessage == null ? 0 : m_localHailMessage.LengthBytes); + + NetOutgoingMessage om = m_peer.CreateMessage(preAllocate); + om.m_messageType = NetMessageType.Connect; + om.Write(m_peerConfiguration.AppIdentifier); + om.Write(m_peer.m_uniqueIdentifier); + om.Write(now); + + WriteLocalHail(om); + + m_peer.SendLibrary(om, m_remoteEndPoint); + + m_connectRequested = false; + m_lastHandshakeSendTime = now; + m_handshakeAttempts++; + + if (m_handshakeAttempts > 1) + m_peer.LogDebug("Resending Connect..."); + SetStatus(NetConnectionStatus.InitiatedConnect, "Locally requested connect"); + } + + internal void SendConnectResponse(float now, bool onLibraryThread) + { + if (onLibraryThread) + m_peer.VerifyNetworkThread(); + + NetOutgoingMessage om = m_peer.CreateMessage(m_peerConfiguration.AppIdentifier.Length + 13 + (m_localHailMessage == null ? 0 : m_localHailMessage.LengthBytes)); + om.m_messageType = NetMessageType.ConnectResponse; + om.Write(m_peerConfiguration.AppIdentifier); + om.Write(m_peer.m_uniqueIdentifier); + om.Write(now); + + WriteLocalHail(om); + + if (onLibraryThread) + m_peer.SendLibrary(om, m_remoteEndPoint); + else + m_peer.m_unsentUnconnectedMessages.Enqueue(new NetTuple(m_remoteEndPoint, om)); + + m_lastHandshakeSendTime = now; + m_handshakeAttempts++; + + if (m_handshakeAttempts > 1) + m_peer.LogDebug("Resending ConnectResponse..."); + + SetStatus(NetConnectionStatus.RespondedConnect, "Remotely requested connect"); + } + + internal void SendDisconnect(string reason, bool onLibraryThread) + { + if (onLibraryThread) + m_peer.VerifyNetworkThread(); + + NetOutgoingMessage om = m_peer.CreateMessage(reason); + om.m_messageType = NetMessageType.Disconnect; + if (onLibraryThread) + m_peer.SendLibrary(om, m_remoteEndPoint); + else + m_peer.m_unsentUnconnectedMessages.Enqueue(new NetTuple(m_remoteEndPoint, om)); + } + + private void WriteLocalHail(NetOutgoingMessage om) + { + if (m_localHailMessage != null) + { + byte[] hi = m_localHailMessage.Data; + if (hi != null && hi.Length >= m_localHailMessage.LengthBytes) + { + if (om.LengthBytes + m_localHailMessage.LengthBytes > m_peerConfiguration.m_maximumTransmissionUnit - 10) + throw new NetException("Hail message too large; can maximally be " + (m_peerConfiguration.m_maximumTransmissionUnit - 10 - om.LengthBytes)); + om.Write(m_localHailMessage.Data, 0, m_localHailMessage.LengthBytes); + } + } + } + + internal void SendConnectionEstablished() + { + NetOutgoingMessage om = m_peer.CreateMessage(4); + om.m_messageType = NetMessageType.ConnectionEstablished; + om.Write((float)NetTime.Now); + m_peer.SendLibrary(om, m_remoteEndPoint); + + m_handshakeAttempts = 0; + + InitializePing(); + if (m_status != NetConnectionStatus.Connected) + SetStatus(NetConnectionStatus.Connected, "Connected to " + NetUtility.ToHexString(m_remoteUniqueIdentifier)); + } + + /// + /// Approves this connection; sending a connection response to the remote host + /// + public void Approve() + { + if (m_status != NetConnectionStatus.RespondedAwaitingApproval) + { + m_peer.LogWarning("Approve() called in wrong status; expected RespondedAwaitingApproval; got " + m_status); + return; + } + + m_localHailMessage = null; + m_handshakeAttempts = 0; + SendConnectResponse((float)NetTime.Now, false); + } + + /// + /// Approves this connection; sending a connection response to the remote host + /// + /// The local hail message that will be set as RemoteHailMessage on the remote host + public void Approve(NetOutgoingMessage localHail) + { + if (m_status != NetConnectionStatus.RespondedAwaitingApproval) + { + m_peer.LogWarning("Approve() called in wrong status; expected RespondedAwaitingApproval; got " + m_status); + return; + } + + m_localHailMessage = localHail; + m_handshakeAttempts = 0; + SendConnectResponse((float)NetTime.Now, false); + } + + /// + /// Denies this connection; disconnecting it + /// + public void Deny() + { + Deny(string.Empty); + } + + /// + /// Denies this connection; disconnecting it + /// + /// The stated reason for the disconnect, readable as a string in the StatusChanged message on the remote host + public void Deny(string reason) + { + // send disconnect; remove from handshakes + SendDisconnect(reason, false); + + // remove from handshakes + m_peer.m_handshakes.Remove(m_remoteEndPoint); // TODO: make this more thread safe? we're on user thread + } + + internal void ReceivedHandshake(double now, NetMessageType tp, int ptr, int payloadLength) + { + m_peer.VerifyNetworkThread(); + + byte[] hail; + switch (tp) + { + case NetMessageType.Connect: + if (m_status == NetConnectionStatus.ReceivedInitiation) + { + // Whee! Server full has already been checked + bool ok = ValidateHandshakeData(ptr, payloadLength, out hail); + if (ok) + { + if (hail != null) + { + m_remoteHailMessage = m_peer.CreateIncomingMessage(NetIncomingMessageType.Data, hail); + m_remoteHailMessage.LengthBits = (hail.Length * 8); + } + else + { + m_remoteHailMessage = null; + } + + if (m_peerConfiguration.IsMessageTypeEnabled(NetIncomingMessageType.ConnectionApproval)) + { + // ok, let's not add connection just yet + NetIncomingMessage appMsg = m_peer.CreateIncomingMessage(NetIncomingMessageType.ConnectionApproval, (m_remoteHailMessage == null ? 0 : m_remoteHailMessage.LengthBytes)); + appMsg.m_receiveTime = now; + appMsg.m_senderConnection = this; + appMsg.m_senderEndPoint = this.m_remoteEndPoint; + if (m_remoteHailMessage != null) + appMsg.Write(m_remoteHailMessage.m_data, 0, m_remoteHailMessage.LengthBytes); + SetStatus(NetConnectionStatus.RespondedAwaitingApproval, "Awaiting approval"); + m_peer.ReleaseMessage(appMsg); + return; + } + + SendConnectResponse((float)now, true); + } + return; + } + if (m_status == NetConnectionStatus.RespondedAwaitingApproval) + { + m_peer.LogWarning("Ignoring multiple Connect() most likely due to a delayed Approval"); + return; + } + if (m_status == NetConnectionStatus.RespondedConnect) + { + // our ConnectResponse must have been lost + SendConnectResponse((float)now, true); + return; + } + m_peer.LogDebug("Unhandled Connect: " + tp + ", status is " + m_status + " length: " + payloadLength); + break; + case NetMessageType.ConnectResponse: + switch (m_status) + { + case NetConnectionStatus.InitiatedConnect: + // awesome + bool ok = ValidateHandshakeData(ptr, payloadLength, out hail); + if (ok) + { + if (hail != null) + { + m_remoteHailMessage = m_peer.CreateIncomingMessage(NetIncomingMessageType.Data, hail); + m_remoteHailMessage.LengthBits = (hail.Length * 8); + } + else + { + m_remoteHailMessage = null; + } + + m_peer.AcceptConnection(this); + SendConnectionEstablished(); + return; + } + break; + case NetConnectionStatus.RespondedConnect: + // hello, wtf? + break; + case NetConnectionStatus.Disconnecting: + case NetConnectionStatus.Disconnected: + case NetConnectionStatus.ReceivedInitiation: + case NetConnectionStatus.None: + // wtf? anyway, bye! + break; + case NetConnectionStatus.Connected: + // my ConnectionEstablished must have been lost, send another one + SendConnectionEstablished(); + return; + } + break; + case NetMessageType.ConnectionEstablished: + switch (m_status) + { + case NetConnectionStatus.Connected: + // ok... + break; + case NetConnectionStatus.Disconnected: + case NetConnectionStatus.Disconnecting: + case NetConnectionStatus.None: + // too bad, almost made it + break; + case NetConnectionStatus.ReceivedInitiation: + // uh, a little premature... ignore + break; + case NetConnectionStatus.InitiatedConnect: + // weird, should have been RespondedConnect... + break; + case NetConnectionStatus.RespondedConnect: + // awesome + + NetIncomingMessage msg = m_peer.SetupReadHelperMessage(ptr, payloadLength); + InitializeRemoteTimeOffset(msg.ReadSingle()); + + m_peer.AcceptConnection(this); + InitializePing(); + SetStatus(NetConnectionStatus.Connected, "Connected to " + NetUtility.ToHexString(m_remoteUniqueIdentifier)); + return; + } + break; + + case NetMessageType.Disconnect: + // ouch + string reason = "Ouch"; + try + { + NetIncomingMessage inc = m_peer.SetupReadHelperMessage(ptr, payloadLength); + reason = inc.ReadString(); + } + catch + { + } + ExecuteDisconnect(reason, false); + break; + + case NetMessageType.Discovery: + m_peer.HandleIncomingDiscoveryRequest(now, m_remoteEndPoint, ptr, payloadLength); + return; + + case NetMessageType.DiscoveryResponse: + m_peer.HandleIncomingDiscoveryResponse(now, m_remoteEndPoint, ptr, payloadLength); + return; + + case NetMessageType.Ping: + // silently ignore + return; + + default: + m_peer.LogDebug("Unhandled type during handshake: " + tp + " length: " + payloadLength); + break; + } + } + + private bool ValidateHandshakeData(int ptr, int payloadLength, out byte[] hail) + { + hail = null; + + // create temporary incoming message + NetIncomingMessage msg = m_peer.SetupReadHelperMessage(ptr, payloadLength); + try + { + string remoteAppIdentifier = msg.ReadString(); + long remoteUniqueIdentifier = msg.ReadInt64(); + InitializeRemoteTimeOffset(msg.ReadSingle()); + + int remainingBytes = payloadLength - (msg.PositionInBytes - ptr); + if (remainingBytes > 0) + hail = msg.ReadBytes(remainingBytes); + + if (remoteAppIdentifier != m_peer.m_configuration.AppIdentifier) + { + // wrong app identifier + ExecuteDisconnect("Wrong application identifier!", true); + return false; + } + + m_remoteUniqueIdentifier = remoteUniqueIdentifier; + } + catch(Exception ex) + { + // whatever; we failed + ExecuteDisconnect("Handshake data validation failed", true); + m_peer.LogWarning("ReadRemoteHandshakeData failed: " + ex.Message); + return false; + } + return true; + } + + /// + /// Disconnect from the remote peer + /// + /// the message to send with the disconnect message + public void Disconnect(string byeMessage) + { + // user or library thread + if (m_status == NetConnectionStatus.None || m_status == NetConnectionStatus.Disconnected) + return; + + m_peer.LogVerbose("Disconnect requested for " + this); + m_disconnectMessage = byeMessage; + + if (m_status != NetConnectionStatus.Disconnected && m_status != NetConnectionStatus.None) + SetStatus(NetConnectionStatus.Disconnecting, byeMessage); + + m_handshakeAttempts = 0; + m_disconnectRequested = true; + } + } +} diff --git a/Lidgren.Network/NetConnection.Latency.cs b/Lidgren.Network/NetConnection.Latency.cs new file mode 100644 index 000000000..2d4882d33 --- /dev/null +++ b/Lidgren.Network/NetConnection.Latency.cs @@ -0,0 +1,147 @@ +using System; + +namespace Lidgren.Network +{ + public partial class NetConnection + { + private float m_sentPingTime; + private int m_sentPingNumber; + private float m_averageRoundtripTime; + private float m_timeoutDeadline = float.MaxValue; + + // local time value + m_remoteTimeOffset = remote time value + internal double m_remoteTimeOffset; + + /// + /// Gets the current average roundtrip time in seconds + /// + public float AverageRoundtripTime { get { return m_averageRoundtripTime; } } + + /// + /// Time offset between this peer and the remote peer + /// + public float RemoteTimeOffset { get { return (float)m_remoteTimeOffset; } } + + // this might happen more than once + internal void InitializeRemoteTimeOffset(float remoteSendTime) + { + m_remoteTimeOffset = (remoteSendTime + (m_averageRoundtripTime / 2.0)) - NetTime.Now; + } + + /// + /// Gets local time value comparable to NetTime.Now from a remote value + /// + public double GetLocalTime(double remoteTimestamp) + { + return remoteTimestamp - m_remoteTimeOffset; + } + + /// + /// Gets the remote time value for a local time value produced by NetTime.Now + /// + public double GetRemoteTime(double localTimestamp) + { + return localTimestamp + m_remoteTimeOffset; + } + + internal void InitializePing() + { + float now = (float)NetTime.Now; + + // randomize ping sent time (0.25 - 1.0 x ping interval) + m_sentPingTime = now; + m_sentPingTime -= (m_peerConfiguration.PingInterval * 0.25f); // delay ping for a little while + m_sentPingTime -= (NetRandom.Instance.NextSingle() * (m_peerConfiguration.PingInterval * 0.75f)); + m_timeoutDeadline = now + (m_peerConfiguration.m_connectionTimeout * 2.0f); // initially allow a little more time + + // make it better, quick :-) + SendPing(); + } + + internal void SendPing() + { + m_peer.VerifyNetworkThread(); + + m_sentPingNumber++; + + m_sentPingTime = (float)NetTime.Now; + NetOutgoingMessage om = m_peer.CreateMessage(1); + om.Write((byte)m_sentPingNumber); // truncating to 0-255 + om.m_messageType = NetMessageType.Ping; + + int len = om.Encode(m_peer.m_sendBuffer, 0, 0); + bool connectionReset; + m_peer.SendPacket(len, m_remoteEndPoint, 1, out connectionReset); + + m_statistics.PacketSent(len, 1); + } + + internal void SendPong(int pingNumber) + { + m_peer.VerifyNetworkThread(); + + NetOutgoingMessage om = m_peer.CreateMessage(5); + om.Write((byte)pingNumber); + om.Write((float)NetTime.Now); // we should update this value to reflect the exact point in time the packet is SENT + om.m_messageType = NetMessageType.Pong; + + int len = om.Encode(m_peer.m_sendBuffer, 0, 0); + bool connectionReset; + + m_peer.SendPacket(len, m_remoteEndPoint, 1, out connectionReset); + + m_statistics.PacketSent(len, 1); + } + + internal void ReceivedPong(float now, int pongNumber, float remoteSendTime) + { + if ((byte)pongNumber != (byte)m_sentPingNumber) + { + m_peer.LogVerbose("Ping/Pong mismatch; dropped message?"); + return; + } + + m_timeoutDeadline = now + m_peerConfiguration.m_connectionTimeout; + + float rtt = now - m_sentPingTime; + NetException.Assert(rtt >= 0); + + double diff = (remoteSendTime + (rtt / 2.0)) - now; + + if (m_averageRoundtripTime < 0) + { + m_remoteTimeOffset = diff; + m_averageRoundtripTime = rtt; + m_peer.LogDebug("Initiated average roundtrip time to " + NetTime.ToReadable(m_averageRoundtripTime) + " Remote time is: " + (now + diff)); + } + else + { + m_averageRoundtripTime = (m_averageRoundtripTime * 0.7f) + (float)(rtt * 0.3f); + + m_remoteTimeOffset = ((m_remoteTimeOffset * (double)(m_sentPingNumber - 1)) + diff) / (double)m_sentPingNumber; + m_peer.LogVerbose("Updated average roundtrip time to " + NetTime.ToReadable(m_averageRoundtripTime) + ", remote time to " + (now + m_remoteTimeOffset) + " (ie. diff " + m_remoteTimeOffset + ")"); + } + + // update resend delay for all channels + float resendDelay = GetResendDelay(); + foreach (var chan in m_sendChannels) + { + var rchan = chan as NetReliableSenderChannel; + if (rchan != null) + rchan.m_resendDelay = resendDelay; + } + + // m_peer.LogVerbose("Timeout deadline pushed to " + m_timeoutDeadline); + + // notify the application that average rtt changed + if (m_peer.m_configuration.IsMessageTypeEnabled(NetIncomingMessageType.ConnectionLatencyUpdated)) + { + NetIncomingMessage update = m_peer.CreateIncomingMessage(NetIncomingMessageType.ConnectionLatencyUpdated, 4); + update.m_senderConnection = this; + update.m_senderEndPoint = this.m_remoteEndPoint; + update.Write(rtt); + m_peer.ReleaseMessage(update); + } + } + } +} diff --git a/Lidgren.Network/NetConnection.MTU.cs b/Lidgren.Network/NetConnection.MTU.cs new file mode 100644 index 000000000..7f48dffa4 --- /dev/null +++ b/Lidgren.Network/NetConnection.MTU.cs @@ -0,0 +1,175 @@ +using System; + +namespace Lidgren.Network +{ + public partial class NetConnection + { + private enum ExpandMTUStatus + { + None, + InProgress, + Finished + } + + private const int c_protocolMaxMTU = (int)((((float)ushort.MaxValue / 8.0f) - 1.0f)); + + private ExpandMTUStatus m_expandMTUStatus; + + private int m_largestSuccessfulMTU; + private int m_smallestFailedMTU; + + private int m_lastSentMTUAttemptSize; + private double m_lastSentMTUAttemptTime; + private int m_mtuAttemptFails; + + internal int m_currentMTU; + + internal void InitExpandMTU(double now) + { + m_lastSentMTUAttemptTime = now + m_peerConfiguration.m_expandMTUFrequency + 1.5f + m_averageRoundtripTime; // wait a tiny bit before starting to expand mtu + m_largestSuccessfulMTU = 512; + m_smallestFailedMTU = -1; + m_currentMTU = m_peerConfiguration.MaximumTransmissionUnit; + } + + private void MTUExpansionHeartbeat(double now) + { + if (m_expandMTUStatus == ExpandMTUStatus.Finished) + return; + + if (m_expandMTUStatus == ExpandMTUStatus.None) + { + if (m_peerConfiguration.m_autoExpandMTU == false) + { + FinalizeMTU(m_currentMTU); + return; + } + + // begin expansion + ExpandMTU(now); + return; + } + + if (now > m_lastSentMTUAttemptTime + m_peerConfiguration.ExpandMTUFrequency) + { + m_mtuAttemptFails++; + if (m_mtuAttemptFails == 3) + { + FinalizeMTU(m_currentMTU); + return; + } + + // timed out; ie. failed + m_smallestFailedMTU = m_lastSentMTUAttemptSize; + ExpandMTU(now); + } + } + + private void ExpandMTU(double now) + { + int tryMTU; + + // we've nevered encountered failure + if (m_smallestFailedMTU == -1) + { + // we've never encountered failure; expand by 25% each time + tryMTU = (int)((float)m_currentMTU * 1.25f); + //m_peer.LogDebug("Trying MTU " + tryMTU); + } + else + { + // we HAVE encountered failure; so try in between + tryMTU = (int)(((float)m_smallestFailedMTU + (float)m_largestSuccessfulMTU) / 2.0f); + //m_peer.LogDebug("Trying MTU " + m_smallestFailedMTU + " <-> " + m_largestSuccessfulMTU + " = " + tryMTU); + } + + if (tryMTU > c_protocolMaxMTU) + tryMTU = c_protocolMaxMTU; + + if (tryMTU == m_largestSuccessfulMTU) + { + //m_peer.LogDebug("Found optimal MTU - exiting"); + FinalizeMTU(m_largestSuccessfulMTU); + return; + } + + SendExpandMTU(now, tryMTU); + } + + private void SendExpandMTU(double now, int size) + { + NetOutgoingMessage om = m_peer.CreateMessage(size); + byte[] tmp = new byte[size]; + om.Write(tmp); + om.m_messageType = NetMessageType.ExpandMTURequest; + int len = om.Encode(m_peer.m_sendBuffer, 0, 0); + + bool ok = m_peer.SendMTUPacket(len, m_remoteEndPoint); + if (ok == false) + { + //m_peer.LogDebug("Send MTU failed for size " + size); + + // failure + if (m_smallestFailedMTU == -1 || size < m_smallestFailedMTU) + { + m_smallestFailedMTU = size; + m_mtuAttemptFails++; + if (m_mtuAttemptFails >= m_peerConfiguration.ExpandMTUFailAttempts) + { + FinalizeMTU(m_largestSuccessfulMTU); + return; + } + } + ExpandMTU(now); + return; + } + + m_lastSentMTUAttemptSize = size; + m_lastSentMTUAttemptTime = now; + + m_statistics.PacketSent(len, 1); + } + + private void FinalizeMTU(int size) + { + if (m_expandMTUStatus == ExpandMTUStatus.Finished) + return; + m_expandMTUStatus = ExpandMTUStatus.Finished; + m_currentMTU = size; + if (m_currentMTU != m_peerConfiguration.m_maximumTransmissionUnit) + m_peer.LogDebug("Expanded Maximum Transmission Unit to: " + m_currentMTU + " bytes"); + return; + } + + private void SendMTUSuccess(int size) + { + NetOutgoingMessage om = m_peer.CreateMessage(4); + om.Write(size); + om.m_messageType = NetMessageType.ExpandMTUSuccess; + int len = om.Encode(m_peer.m_sendBuffer, 0, 0); + bool connectionReset; + m_peer.SendPacket(len, m_remoteEndPoint, 1, out connectionReset); + + // m_peer.LogDebug("Received MTU expand request for " + size + " bytes"); + + m_statistics.PacketSent(len, 1); + } + + private void HandleExpandMTUSuccess(double now, int size) + { + if (size > m_largestSuccessfulMTU) + m_largestSuccessfulMTU = size; + + if (size < m_currentMTU) + { + //m_peer.LogDebug("Received low MTU expand success (size " + size + "); current mtu is " + m_currentMTU); + return; + } + + //m_peer.LogDebug("Expanding MTU to " + size); + m_currentMTU = size; + + ExpandMTU(now); + } + } +} diff --git a/Lidgren.Network/NetConnection.cs b/Lidgren.Network/NetConnection.cs new file mode 100644 index 000000000..ac0d8ce4c --- /dev/null +++ b/Lidgren.Network/NetConnection.cs @@ -0,0 +1,512 @@ +using System; +using System.Net; +using System.Threading; +using System.Diagnostics; + +namespace Lidgren.Network +{ + /// + /// Represents a connection to a remote peer + /// + [DebuggerDisplay("RemoteUniqueIdentifier={RemoteUniqueIdentifier} RemoteEndPoint={remoteEndPoint}")] + public partial class NetConnection + { + private const int m_infrequentEventsSkipFrames = 8; // number of heartbeats to skip checking for infrequent events (ping, timeout etc) + private const int m_messageCoalesceFrames = 3; // number of heartbeats to wait for more incoming messages before sending packet + + internal NetPeer m_peer; + internal NetPeerConfiguration m_peerConfiguration; + internal NetConnectionStatus m_status; + internal NetConnectionStatus m_visibleStatus; + internal IPEndPoint m_remoteEndPoint; + internal NetSenderChannelBase[] m_sendChannels; + internal NetReceiverChannelBase[] m_receiveChannels; + internal NetOutgoingMessage m_localHailMessage; + internal long m_remoteUniqueIdentifier; + internal NetQueue> m_queuedOutgoingAcks; + internal NetQueue> m_queuedIncomingAcks; + private int m_sendBufferWritePtr; + private int m_sendBufferNumMessages; + private object m_tag; + internal NetConnectionStatistics m_statistics; + + /// + /// Gets or sets the application defined object containing data about the connection + /// + public object Tag + { + get { return m_tag; } + set { m_tag = value; } + } + + /// + /// Gets the peer which holds this connection + /// + public NetPeer Peer { get { return m_peer; } } + + /// + /// Gets the current status of the connection (synced to the last status message read) + /// + public NetConnectionStatus Status { get { return m_visibleStatus; } } + + /// + /// Gets various statistics for this connection + /// + public NetConnectionStatistics Statistics { get { return m_statistics; } } + + /// + /// Gets the remote endpoint for the connection + /// + public IPEndPoint RemoteEndPoint { get { return m_remoteEndPoint; } } + + /// + /// Gets the unique identifier of the remote NetPeer for this connection + /// + public long RemoteUniqueIdentifier { get { return m_remoteUniqueIdentifier; } } + + /// + /// Gets the local hail message that was sent as part of the handshake + /// + public NetOutgoingMessage LocalHailMessage { get { return m_localHailMessage; } } + + // gets the time before automatically resending an unacked message + internal float GetResendDelay() + { + float avgRtt = m_averageRoundtripTime; + if (avgRtt <= 0) + avgRtt = 0.1f; // "default" resend is based on 100 ms roundtrip time + return 0.02f + (avgRtt * 2.0f); // 20 ms + double rtt + } + + internal NetConnection(NetPeer peer, IPEndPoint remoteEndPoint) + { + m_peer = peer; + m_peerConfiguration = m_peer.Configuration; + m_status = NetConnectionStatus.None; + m_visibleStatus = NetConnectionStatus.None; + m_remoteEndPoint = remoteEndPoint; + m_sendChannels = new NetSenderChannelBase[NetConstants.NumTotalChannels]; + m_receiveChannels = new NetReceiverChannelBase[NetConstants.NumTotalChannels]; + m_queuedOutgoingAcks = new NetQueue>(4); + m_queuedIncomingAcks = new NetQueue>(4); + m_statistics = new NetConnectionStatistics(this); + m_averageRoundtripTime = -1.0f; + m_currentMTU = m_peerConfiguration.MaximumTransmissionUnit; + } + + /// + /// Change the internal endpoint to this new one. Used when, during handshake, a switch in port is detected (due to NAT) + /// + internal void MutateEndPoint(IPEndPoint endPoint) + { + m_remoteEndPoint = endPoint; + } + + internal void SetStatus(NetConnectionStatus status, string reason) + { + // user or library thread + + if (status == m_status) + return; + + m_status = status; + if (reason == null) + reason = string.Empty; + + if (m_status == NetConnectionStatus.Connected) + { + m_timeoutDeadline = (float)NetTime.Now + m_peerConfiguration.m_connectionTimeout; + m_peer.LogVerbose("Timeout deadline initialized to " + m_timeoutDeadline); + } + + if (m_peerConfiguration.IsMessageTypeEnabled(NetIncomingMessageType.StatusChanged)) + { + NetIncomingMessage info = m_peer.CreateIncomingMessage(NetIncomingMessageType.StatusChanged, 4 + reason.Length + (reason.Length > 126 ? 2 : 1)); + info.m_senderConnection = this; + info.m_senderEndPoint = m_remoteEndPoint; + info.Write((byte)m_status); + info.Write(reason); + m_peer.ReleaseMessage(info); + } + else + { + // app dont want those messages, update visible status immediately + m_visibleStatus = m_status; + } + } + + internal void Heartbeat(float now, uint frameCounter) + { + m_peer.VerifyNetworkThread(); + + NetException.Assert(m_status != NetConnectionStatus.InitiatedConnect && m_status != NetConnectionStatus.RespondedConnect); + + if ((frameCounter % m_infrequentEventsSkipFrames) == 0) + { + if (now > m_timeoutDeadline) + { + // + // connection timed out + // + m_peer.LogVerbose("Connection timed out at " + now + " deadline was " + m_timeoutDeadline); + ExecuteDisconnect("Connection timed out", true); + } + + // send ping? + if (m_status == NetConnectionStatus.Connected) + { + if (now > m_sentPingTime + m_peer.m_configuration.m_pingInterval) + SendPing(); + + // handle expand mtu + MTUExpansionHeartbeat(now); + } + + if (m_disconnectRequested) + { + ExecuteDisconnect(m_disconnectMessage, true); + return; + } + } + + bool connectionReset; // TODO: handle connection reset + + // + // Note: at this point m_sendBufferWritePtr and m_sendBufferNumMessages may be non-null; resends may already be queued up + // + + byte[] sendBuffer = m_peer.m_sendBuffer; + int mtu = m_currentMTU; + + if ((frameCounter % m_messageCoalesceFrames) == 0) // coalesce a few frames + { + // + // send ack messages + // + while (m_queuedOutgoingAcks.Count > 0) + { + int acks = (mtu - (m_sendBufferWritePtr + 5)) / 3; // 3 bytes per actual ack + if (acks > m_queuedOutgoingAcks.Count) + acks = m_queuedOutgoingAcks.Count; + + NetException.Assert(acks > 0); + + m_sendBufferNumMessages++; + + // write acks header + sendBuffer[m_sendBufferWritePtr++] = (byte)NetMessageType.Acknowledge; + sendBuffer[m_sendBufferWritePtr++] = 0; // no sequence number + sendBuffer[m_sendBufferWritePtr++] = 0; // no sequence number + int len = (acks * 3) * 8; // bits + sendBuffer[m_sendBufferWritePtr++] = (byte)len; + sendBuffer[m_sendBufferWritePtr++] = (byte)(len >> 8); + + // write acks + for (int i = 0; i < acks; i++) + { + NetTuple tuple; + m_queuedOutgoingAcks.TryDequeue(out tuple); + + //m_peer.LogVerbose("Sending ack for " + tuple.Item1 + "#" + tuple.Item2); + + sendBuffer[m_sendBufferWritePtr++] = (byte)tuple.Item1; + sendBuffer[m_sendBufferWritePtr++] = (byte)tuple.Item2; + sendBuffer[m_sendBufferWritePtr++] = (byte)(tuple.Item2 >> 8); + } + + if (m_queuedOutgoingAcks.Count > 0) + { + // send packet and go for another round of acks + NetException.Assert(m_sendBufferWritePtr > 0 && m_sendBufferNumMessages > 0); + m_peer.SendPacket(m_sendBufferWritePtr, m_remoteEndPoint, m_sendBufferNumMessages, out connectionReset); + m_statistics.PacketSent(m_sendBufferWritePtr, 1); + m_sendBufferWritePtr = 0; + m_sendBufferNumMessages = 0; + } + } + + // + // Parse incoming acks (may trigger resends) + // + NetTuple incAck; + while (m_queuedIncomingAcks.TryDequeue(out incAck)) + { + //m_peer.LogVerbose("Received ack for " + acktp + "#" + seqNr); + NetSenderChannelBase chan = m_sendChannels[(int)incAck.Item1 - 1]; + if (chan == null) + chan = CreateSenderChannel(incAck.Item1); + chan.ReceiveAcknowledge(now, incAck.Item2); + } + } + + // + // send queued messages + // + if (m_peer.m_executeFlushSendQueue) + { + for (int i = m_sendChannels.Length - 1; i >= 0; i--) // Reverse order so reliable messages are sent first + { + var channel = m_sendChannels[i]; + NetException.Assert(m_sendBufferWritePtr < 1 || m_sendBufferNumMessages > 0); + if (channel != null) + channel.SendQueuedMessages(now); + NetException.Assert(m_sendBufferWritePtr < 1 || m_sendBufferNumMessages > 0); + } + } + + // + // Put on wire data has been written to send buffer but not yet sent + // + if (m_sendBufferWritePtr > 0) + { + m_peer.VerifyNetworkThread(); + NetException.Assert(m_sendBufferWritePtr > 0 && m_sendBufferNumMessages > 0); + m_peer.SendPacket(m_sendBufferWritePtr, m_remoteEndPoint, m_sendBufferNumMessages, out connectionReset); + m_statistics.PacketSent(m_sendBufferWritePtr, m_sendBufferNumMessages); + m_sendBufferWritePtr = 0; + m_sendBufferNumMessages = 0; + } + } + + // Queue an item for immediate sending on the wire + // This method is called from the ISenderChannels + internal void QueueSendMessage(NetOutgoingMessage om, int seqNr) + { + m_peer.VerifyNetworkThread(); + + int sz = om.GetEncodedSize(); + if (sz > m_currentMTU) + m_peer.LogWarning("Message larger than MTU! Fragmentation must have failed!"); + + if (m_sendBufferWritePtr + sz > m_currentMTU) + { + bool connReset; // TODO: handle connection reset + NetException.Assert(m_sendBufferWritePtr > 0 && m_sendBufferNumMessages > 0); // or else the message should have been fragmented earlier + m_peer.SendPacket(m_sendBufferWritePtr, m_remoteEndPoint, m_sendBufferNumMessages, out connReset); + m_statistics.PacketSent(m_sendBufferWritePtr, m_sendBufferNumMessages); + m_sendBufferWritePtr = 0; + m_sendBufferNumMessages = 0; + } + + m_sendBufferWritePtr = om.Encode(m_peer.m_sendBuffer, m_sendBufferWritePtr, seqNr); + m_sendBufferNumMessages++; + + NetException.Assert(m_sendBufferWritePtr > 0, "Encoded zero size message?"); + NetException.Assert(m_sendBufferNumMessages > 0); + } + + /// + /// Send a message to this remote connection + /// + /// The message to send + /// How to deliver the message + /// Sequence channel within the delivery method + public NetSendResult SendMessage(NetOutgoingMessage msg, NetDeliveryMethod method, int sequenceChannel) + { + return m_peer.SendMessage(msg, this, method, sequenceChannel); + } + + // called by SendMessage() and NetPeer.SendMessage; ie. may be user thread + internal NetSendResult EnqueueMessage(NetOutgoingMessage msg, NetDeliveryMethod method, int sequenceChannel) + { + if (m_status != NetConnectionStatus.Connected) + return NetSendResult.FailedNotConnected; + + NetMessageType tp = (NetMessageType)((int)method + sequenceChannel); + msg.m_messageType = tp; + + // TODO: do we need to make this more thread safe? + int channelSlot = (int)method - 1 + sequenceChannel; + NetSenderChannelBase chan = m_sendChannels[channelSlot]; + if (chan == null) + chan = CreateSenderChannel(tp); + + if (msg.GetEncodedSize() > m_currentMTU) + throw new NetException("Message too large! Fragmentation failure?"); + + var retval = chan.Enqueue(msg); + if (retval == NetSendResult.Sent && m_peerConfiguration.m_autoFlushSendQueue == false) + retval = NetSendResult.Queued; // queued since we're not autoflushing + return retval; + } + + // may be on user thread + private NetSenderChannelBase CreateSenderChannel(NetMessageType tp) + { + NetSenderChannelBase chan; + lock (m_sendChannels) + { + NetDeliveryMethod method = NetUtility.GetDeliveryMethod(tp); + int sequenceChannel = (int)tp - (int)method; + + int channelSlot = (int)method - 1 + sequenceChannel; + if (m_sendChannels[channelSlot] != null) + { + // we were pre-empted by another call to this method + chan = m_sendChannels[channelSlot]; + } + else + { + + switch (method) + { + case NetDeliveryMethod.Unreliable: + case NetDeliveryMethod.UnreliableSequenced: + chan = new NetUnreliableSenderChannel(this, NetUtility.GetWindowSize(method)); + break; + case NetDeliveryMethod.ReliableOrdered: + chan = new NetReliableSenderChannel(this, NetUtility.GetWindowSize(method)); + break; + case NetDeliveryMethod.ReliableSequenced: + case NetDeliveryMethod.ReliableUnordered: + default: + chan = new NetReliableSenderChannel(this, NetUtility.GetWindowSize(method)); + break; + } + m_sendChannels[channelSlot] = chan; + } + } + + return chan; + } + + // received a library message while Connected + internal void ReceivedLibraryMessage(NetMessageType tp, int ptr, int payloadLength) + { + m_peer.VerifyNetworkThread(); + + float now = (float)NetTime.Now; + + switch (tp) + { + case NetMessageType.Disconnect: + NetIncomingMessage msg = m_peer.SetupReadHelperMessage(ptr, payloadLength); + ExecuteDisconnect(msg.ReadString(), false); + break; + case NetMessageType.Acknowledge: + for (int i = 0; i < payloadLength; i+=3) + { + NetMessageType acktp = (NetMessageType)m_peer.m_receiveBuffer[ptr++]; // netmessagetype + int seqNr = m_peer.m_receiveBuffer[ptr++]; + seqNr |= (m_peer.m_receiveBuffer[ptr++] << 8); + + // need to enqueue this and handle it in the netconnection heartbeat; so be able to send resends together with normal sends + m_queuedIncomingAcks.Enqueue(new NetTuple(acktp, seqNr)); + } + break; + case NetMessageType.Ping: + int pingNr = m_peer.m_receiveBuffer[ptr++]; + SendPong(pingNr); + break; + case NetMessageType.Pong: + NetIncomingMessage pmsg = m_peer.SetupReadHelperMessage(ptr, payloadLength); + int pongNr = pmsg.ReadByte(); + float remoteSendTime = pmsg.ReadSingle(); + ReceivedPong(now, pongNr, remoteSendTime); + break; + case NetMessageType.ExpandMTURequest: + SendMTUSuccess(payloadLength); + break; + case NetMessageType.ExpandMTUSuccess: + NetIncomingMessage emsg = m_peer.SetupReadHelperMessage(ptr, payloadLength); + int size = emsg.ReadInt32(); + HandleExpandMTUSuccess(now, size); + break; + case NetMessageType.NatIntroduction: + // Unusual situation where server is actually already known, but got a nat introduction - oh well, lets handle it as usual + m_peer.HandleNatIntroduction(ptr); + break; + default: + m_peer.LogWarning("Connection received unhandled library message: " + tp); + break; + } + } + + internal void ReceivedMessage(NetIncomingMessage msg) + { + m_peer.VerifyNetworkThread(); + + NetMessageType tp = msg.m_receivedMessageType; + + int channelSlot = (int)tp - 1; + NetReceiverChannelBase chan = m_receiveChannels[channelSlot]; + if (chan == null) + chan = CreateReceiverChannel(tp); + + chan.ReceiveMessage(msg); + } + + private NetReceiverChannelBase CreateReceiverChannel(NetMessageType tp) + { + m_peer.VerifyNetworkThread(); + + // create receiver channel + NetReceiverChannelBase chan; + NetDeliveryMethod method = NetUtility.GetDeliveryMethod(tp); + switch (method) + { + case NetDeliveryMethod.Unreliable: + chan = new NetUnreliableUnorderedReceiver(this); + break; + case NetDeliveryMethod.ReliableOrdered: + chan = new NetReliableOrderedReceiver(this, NetConstants.ReliableOrderedWindowSize); + break; + case NetDeliveryMethod.UnreliableSequenced: + chan = new NetUnreliableSequencedReceiver(this); + break; + case NetDeliveryMethod.ReliableUnordered: + chan = new NetReliableUnorderedReceiver(this, NetConstants.ReliableOrderedWindowSize); + break; + case NetDeliveryMethod.ReliableSequenced: + chan = new NetReliableSequencedReceiver(this, NetConstants.ReliableSequencedWindowSize); + break; + default: + throw new NetException("Unhandled NetDeliveryMethod!"); + } + + int channelSlot = (int)tp - 1; + NetException.Assert(m_receiveChannels[channelSlot] == null); + m_receiveChannels[channelSlot] = chan; + + return chan; + } + + internal void QueueAck(NetMessageType tp, int sequenceNumber) + { + m_queuedOutgoingAcks.Enqueue(new NetTuple(tp, sequenceNumber)); + } + + /// + /// Zero windowSize indicates that the channel is not yet instantiated (used) + /// Negative freeWindowSlots means this amount of messages are currently queued but delayed due to closed window + /// + public void GetSendQueueInfo(NetDeliveryMethod method, int sequenceChannel, out int windowSize, out int freeWindowSlots) + { + int channelSlot = (int)method - 1 + sequenceChannel; + var chan = m_sendChannels[channelSlot]; + if (chan == null) + { + windowSize = NetUtility.GetWindowSize(method); + freeWindowSlots = windowSize; + return; + } + + windowSize = chan.WindowSize; + freeWindowSlots = chan.GetAllowedSends() - chan.m_queuedSends.Count; + return; + } + + internal void Shutdown(string reason) + { + ExecuteDisconnect(reason, true); + } + + /// + /// Returns a string that represents this object + /// + public override string ToString() + { + return "[NetConnection to " + m_remoteEndPoint + "]"; + } + } +} diff --git a/Lidgren.Network/NetConnectionStatistics.cs b/Lidgren.Network/NetConnectionStatistics.cs new file mode 100644 index 000000000..33ec49426 --- /dev/null +++ b/Lidgren.Network/NetConnectionStatistics.cs @@ -0,0 +1,204 @@ +/* Copyright (c) 2010 Michael Lidgren + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom +the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +// Uncomment the line below to get statistics in RELEASE builds +//#define USE_RELEASE_STATISTICS + +using System; +using System.Collections.Generic; +using System.Text; +using System.Diagnostics; + +namespace Lidgren.Network +{ + internal enum MessageResendReason + { + Delay, + HoleInSequence + } + + /// + /// Statistics for a NetConnection instance + /// + public sealed class NetConnectionStatistics + { + private readonly NetConnection m_connection; + + internal int m_sentPackets; + internal int m_receivedPackets; + + internal int m_sentMessages; + internal int m_receivedMessages; + + internal int m_sentBytes; + internal int m_receivedBytes; + + internal int m_resentMessagesDueToDelay; + internal int m_resentMessagesDueToHole; + + internal NetConnectionStatistics(NetConnection conn) + { + m_connection = conn; + Reset(); + } + + internal void Reset() + { + m_sentPackets = 0; + m_receivedPackets = 0; + m_sentBytes = 0; + m_receivedBytes = 0; + } + + /// + /// Gets the number of sent packets for this connection + /// + public int SentPackets { get { return m_sentPackets; } } + + /// + /// Gets the number of received packets for this connection + /// + public int ReceivedPackets { get { return m_receivedPackets; } } + + /// + /// Gets the number of sent bytes for this connection + /// + public int SentBytes { get { return m_sentBytes; } } + + /// + /// Gets the number of received bytes for this connection + /// + public int ReceivedBytes { get { return m_receivedBytes; } } + + /// + /// Gets the number of resent reliable messages for this connection + /// + public int ResentMessages { get { return m_resentMessagesDueToHole + m_resentMessagesDueToDelay; } } + + // public double LastSendRespondedTo { get { return m_connection.m_lastSendRespondedTo; } } + +#if USE_RELEASE_STATISTICS + internal void PacketSent(int numBytes, int numMessages) + { + NetException.Assert(numBytes > 0 && numMessages > 0); + m_sentPackets++; + m_sentBytes += numBytes; + m_sentMessages += numMessages; + } +#else + [Conditional("DEBUG")] + internal void PacketSent(int numBytes, int numMessages) + { + NetException.Assert(numBytes > 0 && numMessages > 0); + m_sentPackets++; + m_sentBytes += numBytes; + m_sentMessages += numMessages; + } +#endif + +#if USE_RELEASE_STATISTICS + internal void PacketReceived(int numBytes, int numMessages) + { + NetException.Assert(numBytes > 0 && numMessages > 0); + m_receivedPackets++; + m_receivedBytes += numBytes; + m_receivedMessages += numMessages; + } +#else + [Conditional("DEBUG")] + internal void PacketReceived(int numBytes, int numMessages) + { + NetException.Assert(numBytes > 0 && numMessages > 0); + m_receivedPackets++; + m_receivedBytes += numBytes; + m_receivedMessages += numMessages; + } +#endif + +#if USE_RELEASE_STATISTICS + internal void MessageResent(MessageResendReason reason) + { + if (reason == MessageResendReason.Delay) + m_resentMessagesDueToDelay++; + else + m_resentMessagesDueToHole++; + } +#else + [Conditional("DEBUG")] + internal void MessageResent(MessageResendReason reason) + { + if (reason == MessageResendReason.Delay) + m_resentMessagesDueToDelay++; + else + m_resentMessagesDueToHole++; + } +#endif + + /// + /// Returns a string that represents this object + /// + public override string ToString() + { + StringBuilder bdr = new StringBuilder(); + //bdr.AppendLine("Average roundtrip time: " + NetTime.ToReadable(m_connection.m_averageRoundtripTime)); + bdr.AppendLine("Sent " + m_sentBytes + " bytes in " + m_sentMessages + " messages in " + m_sentPackets + " packets"); + bdr.AppendLine("Received " + m_receivedBytes + " bytes in " + m_receivedMessages + " messages in " + m_receivedPackets + " packets"); + + if (m_resentMessagesDueToDelay > 0) + bdr.AppendLine("Resent messages (delay): " + m_resentMessagesDueToDelay); + if (m_resentMessagesDueToDelay > 0) + bdr.AppendLine("Resent messages (holes): " + m_resentMessagesDueToHole); + + int numUnsent = 0; + int numStored = 0; + foreach (NetSenderChannelBase sendChan in m_connection.m_sendChannels) + { + if (sendChan == null) + continue; + numUnsent += sendChan.m_queuedSends.Count; + + var relSendChan = sendChan as NetReliableSenderChannel; + if (relSendChan != null) + { + for (int i = 0; i < relSendChan.m_storedMessages.Length; i++) + if (relSendChan.m_storedMessages[i].Message != null) + numStored++; + } + } + + int numWithheld = 0; + foreach (NetReceiverChannelBase recChan in m_connection.m_receiveChannels) + { + var relRecChan = recChan as NetReliableOrderedReceiver; + if (relRecChan != null) + { + for (int i = 0; i < relRecChan.m_withheldMessages.Length; i++) + if (relRecChan.m_withheldMessages[i] != null) + numWithheld++; + } + } + + bdr.AppendLine("Unsent messages: " + numUnsent); + bdr.AppendLine("Stored messages: " + numStored); + bdr.AppendLine("Withheld messages: " + numWithheld); + + return bdr.ToString(); + } + } +} \ No newline at end of file diff --git a/Lidgren.Network/NetConnectionStatus.cs b/Lidgren.Network/NetConnectionStatus.cs new file mode 100644 index 000000000..15c1937cf --- /dev/null +++ b/Lidgren.Network/NetConnectionStatus.cs @@ -0,0 +1,68 @@ +/* Copyright (c) 2010 Michael Lidgren + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom +the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +using System; + +namespace Lidgren.Network +{ + /// + /// Status for a NetConnection instance + /// + public enum NetConnectionStatus + { + /// + /// No connection, or attempt, in place + /// + None, + + /// + /// Connect has been sent; waiting for ConnectResponse + /// + InitiatedConnect, + + /// + /// Connect was received, but ConnectResponse hasn't been sent yet + /// + ReceivedInitiation, + + /// + /// Connect was received and ApprovalMessage released to the application; awaiting Approve() or Deny() + /// + RespondedAwaitingApproval, // We got Connect, released ApprovalMessage + + /// + /// Connect was received and ConnectResponse has been sent; waiting for ConnectionEstablished + /// + RespondedConnect, // we got Connect, sent ConnectResponse + + /// + /// Connected + /// + Connected, // we received ConnectResponse (if initiator) or ConnectionEstablished (if passive) + + /// + /// In the process of disconnecting + /// + Disconnecting, + + /// + /// Disconnected + /// + Disconnected + } +} diff --git a/Lidgren.Network/NetConstants.cs b/Lidgren.Network/NetConstants.cs new file mode 100644 index 000000000..a5a0c37e8 --- /dev/null +++ b/Lidgren.Network/NetConstants.cs @@ -0,0 +1,57 @@ +/* Copyright (c) 2010 Michael Lidgren + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom +the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +using System; + +namespace Lidgren.Network +{ + /// + /// All the constants used when compiling the library + /// + internal static class NetConstants + { + internal const int NumTotalChannels = 99; + + internal const int NetChannelsPerDeliveryMethod = 32; + + internal const int NumSequenceNumbers = 1024; + + internal const int HeaderByteSize = 5; + + internal const int UnreliableWindowSize = 128; + internal const int ReliableOrderedWindowSize = 64; + internal const int ReliableSequencedWindowSize = 64; + internal const int DefaultWindowSize = 64; + + internal const int MaxFragmentationGroups = ushort.MaxValue - 1; + + internal const int UnfragmentedMessageHeaderSize = 5; + + /// + /// Number of channels which needs a sequence number to work + /// + internal const int NumSequencedChannels = ((int)NetMessageType.UserReliableOrdered1 + NetConstants.NetChannelsPerDeliveryMethod) - (int)NetMessageType.UserSequenced1; + + /// + /// Number of reliable channels + /// + internal const int NumReliableChannels = ((int)NetMessageType.UserReliableOrdered1 + NetConstants.NetChannelsPerDeliveryMethod) - (int)NetMessageType.UserReliableUnordered; + + internal const string ConnResetMessage = "Connection was reset by remote host"; + } +} diff --git a/Lidgren.Network/NetDeliveryMethod.cs b/Lidgren.Network/NetDeliveryMethod.cs new file mode 100644 index 000000000..d04266d48 --- /dev/null +++ b/Lidgren.Network/NetDeliveryMethod.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Lidgren.Network +{ + /// + /// How the library deals with resends and handling of late messages + /// + public enum NetDeliveryMethod : byte + { + // + // Actually a publicly visible subset of NetMessageType + // + + /// + /// Indicates an error + /// + Unknown = 0, + + /// + /// Unreliable, unordered delivery + /// + Unreliable = 1, + + /// + /// Unreliable delivery, but automatically dropping late messages + /// + UnreliableSequenced = 2, + + /// + /// Reliable delivery, but unordered + /// + ReliableUnordered = 34, + + /// + /// Reliable delivery, except for late messages which are dropped + /// + ReliableSequenced = 35, + + /// + /// Reliable, ordered delivery + /// + ReliableOrdered = 67, + } +} diff --git a/Lidgren.Network/NetException.cs b/Lidgren.Network/NetException.cs new file mode 100644 index 000000000..4a7725863 --- /dev/null +++ b/Lidgren.Network/NetException.cs @@ -0,0 +1,83 @@ +/* Copyright (c) 2010 Michael Lidgren + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom +the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +using System; +using System.Diagnostics; +using System.Runtime.Serialization; + +namespace Lidgren.Network +{ + /// + /// Exception thrown in the Lidgren Network Library + /// + [Serializable] + public sealed class NetException : Exception + { + /// + /// NetException constructor + /// + public NetException() + : base() + { + } + + /// + /// NetException constructor + /// + public NetException(string message) + : base(message) + { + } + + /// + /// NetException constructor + /// + public NetException(string message, Exception inner) + : base(message, inner) + { + } + + /// + /// NetException constructor + /// + private NetException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + /// + /// Throws an exception, in DEBUG only, if first parameter is false + /// + [Conditional("DEBUG")] + public static void Assert(bool isOk, string message) + { + if (!isOk) + throw new NetException(message); + } + + /// + /// Throws an exception, in DEBUG only, if first parameter is false + /// + [Conditional("DEBUG")] + public static void Assert(bool isOk) + { + if (!isOk) + throw new NetException(); + } + } +} diff --git a/Lidgren.Network/NetFragmentationHelper.cs b/Lidgren.Network/NetFragmentationHelper.cs new file mode 100644 index 000000000..85efe4d13 --- /dev/null +++ b/Lidgren.Network/NetFragmentationHelper.cs @@ -0,0 +1,175 @@ +using System; + +namespace Lidgren.Network +{ + internal static class NetFragmentationHelper + { + internal static int WriteHeader( + byte[] destination, + int ptr, + int group, + int totalBits, + int chunkByteSize, + int chunkNumber) + { + uint num1 = (uint)group; + while (num1 >= 0x80) + { + destination[ptr++] = (byte)(num1 | 0x80); + num1 = num1 >> 7; + } + destination[ptr++] = (byte)num1; + + // write variable length fragment total bits + uint num2 = (uint)totalBits; + while (num2 >= 0x80) + { + destination[ptr++] = (byte)(num2 | 0x80); + num2 = num2 >> 7; + } + destination[ptr++] = (byte)num2; + + // write variable length fragment chunk size + uint num3 = (uint)chunkByteSize; + while (num3 >= 0x80) + { + destination[ptr++] = (byte)(num3 | 0x80); + num3 = num3 >> 7; + } + destination[ptr++] = (byte)num3; + + // write variable length fragment chunk number + uint num4 = (uint)chunkNumber; + while (num4 >= 0x80) + { + destination[ptr++] = (byte)(num4 | 0x80); + num4 = num4 >> 7; + } + destination[ptr++] = (byte)num4; + + return ptr; + } + + internal static int ReadHeader(byte[] buffer, int ptr, out int group, out int totalBits, out int chunkByteSize, out int chunkNumber) + { + int num1 = 0; + int num2 = 0; + while (true) + { + byte num3 = buffer[ptr++]; + num1 |= (num3 & 0x7f) << (num2 & 0x1f); + num2 += 7; + if ((num3 & 0x80) == 0) + { + group = num1; + break; + } + } + + num1 = 0; + num2 = 0; + while (true) + { + byte num3 = buffer[ptr++]; + num1 |= (num3 & 0x7f) << (num2 & 0x1f); + num2 += 7; + if ((num3 & 0x80) == 0) + { + totalBits = num1; + break; + } + } + + num1 = 0; + num2 = 0; + while (true) + { + byte num3 = buffer[ptr++]; + num1 |= (num3 & 0x7f) << (num2 & 0x1f); + num2 += 7; + if ((num3 & 0x80) == 0) + { + chunkByteSize = num1; + break; + } + } + + num1 = 0; + num2 = 0; + while (true) + { + byte num3 = buffer[ptr++]; + num1 |= (num3 & 0x7f) << (num2 & 0x1f); + num2 += 7; + if ((num3 & 0x80) == 0) + { + chunkNumber = num1; + break; + } + } + + return ptr; + } + + internal static int GetFragmentationHeaderSize(int groupId, int totalBytes, int chunkByteSize, int numChunks) + { + int len = 4; + + // write variable length fragment group id + uint num1 = (uint)groupId; + while (num1 >= 0x80) + { + len++; + num1 = num1 >> 7; + } + + // write variable length fragment total bits + uint num2 = (uint)(totalBytes * 8); + while (num2 >= 0x80) + { + len++; + num2 = num2 >> 7; + } + + // write variable length fragment chunk byte size + uint num3 = (uint)chunkByteSize; + while (num3 >= 0x80) + { + len++; + num3 = num3 >> 7; + } + + // write variable length fragment chunk number + uint num4 = (uint)numChunks; + while (num4 >= 0x80) + { + len++; + num4 = num4 >> 7; + } + + return len; + } + + internal static int GetBestChunkSize(int group, int totalBytes, int mtu) + { + int tryChunkSize = mtu - NetConstants.HeaderByteSize - 4; // naive approximation + int est = GetFragmentationHeaderSize(group, totalBytes, tryChunkSize, totalBytes / tryChunkSize); + tryChunkSize = mtu - NetConstants.HeaderByteSize - est; // slightly less naive approximation + + int headerSize = 0; + do + { + tryChunkSize--; // keep reducing chunk size until it fits within MTU including header + + int numChunks = totalBytes / tryChunkSize; + if (numChunks * tryChunkSize < totalBytes) + numChunks++; + + headerSize = GetFragmentationHeaderSize(group, totalBytes, tryChunkSize, numChunks); // 4+ bytes + + } while (tryChunkSize + headerSize + NetConstants.HeaderByteSize + 1 >= mtu); + + return tryChunkSize; + } + } +} diff --git a/Lidgren.Network/NetFragmentationInfo.cs b/Lidgren.Network/NetFragmentationInfo.cs new file mode 100644 index 000000000..2c8b70eda --- /dev/null +++ b/Lidgren.Network/NetFragmentationInfo.cs @@ -0,0 +1,12 @@ +using System; + +namespace Lidgren.Network +{ + public sealed class NetFragmentationInfo + { + public int TotalFragmentCount; + public bool[] Received; + public int TotalReceived; + public int FragmentSize; + } +} diff --git a/Lidgren.Network/NetIncomingMessage.cs b/Lidgren.Network/NetIncomingMessage.cs new file mode 100644 index 000000000..2b1f87a9f --- /dev/null +++ b/Lidgren.Network/NetIncomingMessage.cs @@ -0,0 +1,115 @@ +/* Copyright (c) 2010 Michael Lidgren + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom +the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +using System; +using System.Net; +using System.Diagnostics; + +namespace Lidgren.Network +{ + /// + /// Incoming message either sent from a remote peer or generated within the library + /// + [DebuggerDisplay("Type={MessageType} LengthBits={LengthBits}")] + public sealed class NetIncomingMessage : NetBuffer + { + internal NetIncomingMessageType m_incomingMessageType; + internal IPEndPoint m_senderEndPoint; + internal NetConnection m_senderConnection; + internal int m_sequenceNumber; + internal NetMessageType m_receivedMessageType; + internal bool m_isFragment; + internal double m_receiveTime; + + /// + /// Gets the type of this incoming message + /// + public NetIncomingMessageType MessageType { get { return m_incomingMessageType; } } + + /// + /// Gets the delivery method this message was sent with (if user data) + /// + public NetDeliveryMethod DeliveryMethod { get { return NetUtility.GetDeliveryMethod(m_receivedMessageType); } } + + /// + /// Gets the sequence channel this message was sent with (if user data) + /// + public int SequenceChannel { get { return (int)m_receivedMessageType - (int)NetUtility.GetDeliveryMethod(m_receivedMessageType); } } + + /// + /// IPEndPoint of sender, if any + /// + public IPEndPoint SenderEndPoint { get { return m_senderEndPoint; } } + + /// + /// NetConnection of sender, if any + /// + public NetConnection SenderConnection { get { return m_senderConnection; } } + + /// + /// What local time the message was received from the network + /// + public double ReceiveTime { get { return m_receiveTime; } } + + internal NetIncomingMessage() + { + } + + internal NetIncomingMessage(NetIncomingMessageType tp) + { + m_incomingMessageType = tp; + } + + internal void Reset() + { + m_incomingMessageType = NetIncomingMessageType.Error; + m_readPosition = 0; + m_receivedMessageType = NetMessageType.LibraryError; + m_senderConnection = null; + m_bitLength = 0; + m_isFragment = false; + } + + /// + /// Decrypt a message + /// + /// The encryption algorithm used to encrypt the message + /// true on success + public bool Decrypt(INetEncryption encryption) + { + return encryption.Decrypt(this); + } + + /// + /// Reads a value, in local time comparable to NetTime.Now, written using WriteTime() + /// Must have a connected sender + /// + public double ReadTime(bool highPrecision) + { + return ReadTime(m_senderConnection, highPrecision); + } + + /// + /// Returns a string that represents this object + /// + public override string ToString() + { + return "[NetIncomingMessage #" + m_sequenceNumber + " " + this.LengthBytes + " bytes]"; + } + } +} diff --git a/Lidgren.Network/NetIncomingMessageType.cs b/Lidgren.Network/NetIncomingMessageType.cs new file mode 100644 index 000000000..73f4ae75a --- /dev/null +++ b/Lidgren.Network/NetIncomingMessageType.cs @@ -0,0 +1,105 @@ +/* Copyright (c) 2010 Michael Lidgren + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom +the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Lidgren.Network +{ + /// + /// The type of a NetIncomingMessage + /// + [SuppressMessage("Microsoft.Design", "CA1027:MarkEnumsWithFlags")] + public enum NetIncomingMessageType + { + // + // library note: values are power-of-two, but they are not flags - it's a convenience for NetPeerConfiguration.DisabledMessageTypes + // + + /// + /// Error; this value should never appear + /// + Error = 0, + + /// + /// Status for a connection changed + /// + StatusChanged = 1 << 0, // Data (string) + + /// + /// Data sent using SendUnconnectedMessage + /// + UnconnectedData = 1 << 1, // Data Based on data received + + /// + /// Connection approval is needed + /// + ConnectionApproval = 1 << 2, // Data + + /// + /// Application data + /// + Data = 1 << 3, // Data Based on data received + + /// + /// Receipt of delivery + /// + Receipt = 1 << 4, // Data + + /// + /// Discovery request for a response + /// + DiscoveryRequest = 1 << 5, // (no data) + + /// + /// Discovery response to a request + /// + DiscoveryResponse = 1 << 6, // Data + + /// + /// Verbose debug message + /// + VerboseDebugMessage = 1 << 7, // Data (string) + + /// + /// Debug message + /// + DebugMessage = 1 << 8, // Data (string) + + /// + /// Warning message + /// + WarningMessage = 1 << 9, // Data (string) + + /// + /// Error message + /// + ErrorMessage = 1 << 10, // Data (string) + + /// + /// NAT introduction was successful + /// + NatIntroductionSuccess = 1 << 11, // Data (as passed to master server) + + /// + /// A roundtrip was measured and NetConnection.AverageRoundtripTime was updated + /// + ConnectionLatencyUpdated = 1 << 12, // Seconds as a Single + } +} diff --git a/Lidgren.Network/NetMessageType.cs b/Lidgren.Network/NetMessageType.cs new file mode 100644 index 000000000..ef56eee0a --- /dev/null +++ b/Lidgren.Network/NetMessageType.cs @@ -0,0 +1,175 @@ +/* Copyright (c) 2010 Michael Lidgren + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom +the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +using System; + +namespace Lidgren.Network +{ + internal enum NetMessageType : byte + { + Unconnected = 0, + + UserUnreliable = 1, + + UserSequenced1 = 2, + UserSequenced2 = 3, + UserSequenced3 = 4, + UserSequenced4 = 5, + UserSequenced5 = 6, + UserSequenced6 = 7, + UserSequenced7 = 8, + UserSequenced8 = 9, + UserSequenced9 = 10, + UserSequenced10 = 11, + UserSequenced11 = 12, + UserSequenced12 = 13, + UserSequenced13 = 14, + UserSequenced14 = 15, + UserSequenced15 = 16, + UserSequenced16 = 17, + UserSequenced17 = 18, + UserSequenced18 = 19, + UserSequenced19 = 20, + UserSequenced20 = 21, + UserSequenced21 = 22, + UserSequenced22 = 23, + UserSequenced23 = 24, + UserSequenced24 = 25, + UserSequenced25 = 26, + UserSequenced26 = 27, + UserSequenced27 = 28, + UserSequenced28 = 29, + UserSequenced29 = 30, + UserSequenced30 = 31, + UserSequenced31 = 32, + UserSequenced32 = 33, + + UserReliableUnordered = 34, + + UserReliableSequenced1 = 35, + UserReliableSequenced2 = 36, + UserReliableSequenced3 = 37, + UserReliableSequenced4 = 38, + UserReliableSequenced5 = 39, + UserReliableSequenced6 = 40, + UserReliableSequenced7 = 41, + UserReliableSequenced8 = 42, + UserReliableSequenced9 = 43, + UserReliableSequenced10 = 44, + UserReliableSequenced11 = 45, + UserReliableSequenced12 = 46, + UserReliableSequenced13 = 47, + UserReliableSequenced14 = 48, + UserReliableSequenced15 = 49, + UserReliableSequenced16 = 50, + UserReliableSequenced17 = 51, + UserReliableSequenced18 = 52, + UserReliableSequenced19 = 53, + UserReliableSequenced20 = 54, + UserReliableSequenced21 = 55, + UserReliableSequenced22 = 56, + UserReliableSequenced23 = 57, + UserReliableSequenced24 = 58, + UserReliableSequenced25 = 59, + UserReliableSequenced26 = 60, + UserReliableSequenced27 = 61, + UserReliableSequenced28 = 62, + UserReliableSequenced29 = 63, + UserReliableSequenced30 = 64, + UserReliableSequenced31 = 65, + UserReliableSequenced32 = 66, + + UserReliableOrdered1 = 67, + UserReliableOrdered2 = 68, + UserReliableOrdered3 = 69, + UserReliableOrdered4 = 70, + UserReliableOrdered5 = 71, + UserReliableOrdered6 = 72, + UserReliableOrdered7 = 73, + UserReliableOrdered8 = 74, + UserReliableOrdered9 = 75, + UserReliableOrdered10 = 76, + UserReliableOrdered11 = 77, + UserReliableOrdered12 = 78, + UserReliableOrdered13 = 79, + UserReliableOrdered14 = 80, + UserReliableOrdered15 = 81, + UserReliableOrdered16 = 82, + UserReliableOrdered17 = 83, + UserReliableOrdered18 = 84, + UserReliableOrdered19 = 85, + UserReliableOrdered20 = 86, + UserReliableOrdered21 = 87, + UserReliableOrdered22 = 88, + UserReliableOrdered23 = 89, + UserReliableOrdered24 = 90, + UserReliableOrdered25 = 91, + UserReliableOrdered26 = 92, + UserReliableOrdered27 = 93, + UserReliableOrdered28 = 94, + UserReliableOrdered29 = 95, + UserReliableOrdered30 = 96, + UserReliableOrdered31 = 97, + UserReliableOrdered32 = 98, + + Unused1 = 99, + Unused2 = 100, + Unused3 = 101, + Unused4 = 102, + Unused5 = 103, + Unused6 = 104, + Unused7 = 105, + Unused8 = 106, + Unused9 = 107, + Unused10 = 108, + Unused11 = 109, + Unused12 = 110, + Unused13 = 111, + Unused14 = 112, + Unused15 = 113, + Unused16 = 114, + Unused17 = 115, + Unused18 = 116, + Unused19 = 117, + Unused20 = 118, + Unused21 = 119, + Unused22 = 120, + Unused23 = 121, + Unused24 = 122, + Unused25 = 123, + Unused26 = 124, + Unused27 = 125, + Unused28 = 126, + Unused29 = 127, + + LibraryError = 128, + Ping = 129, // used for RTT calculation + Pong = 130, // used for RTT calculation + Connect = 131, + ConnectResponse = 132, + ConnectionEstablished = 133, + Acknowledge = 134, + Disconnect = 135, + Discovery = 136, + DiscoveryResponse = 137, + NatPunchMessage = 138, // send between peers + NatIntroduction = 139, // send to master server + ExpandMTURequest = 140, + ExpandMTUSuccess = 141, + } +} \ No newline at end of file diff --git a/Lidgren.Network/NetNatIntroduction.cs b/Lidgren.Network/NetNatIntroduction.cs new file mode 100644 index 000000000..d68d7e2b1 --- /dev/null +++ b/Lidgren.Network/NetNatIntroduction.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Net; + +namespace Lidgren.Network +{ + public partial class NetPeer + { + /// + /// Send NetIntroduction to hostExternal and clientExternal; introducing client to host + /// + public void Introduce( + IPEndPoint hostInternal, + IPEndPoint hostExternal, + IPEndPoint clientInternal, + IPEndPoint clientExternal, + string token) + { + // send message to client + NetOutgoingMessage msg = CreateMessage(10 + token.Length + 1); + msg.m_messageType = NetMessageType.NatIntroduction; + msg.Write((byte)0); + msg.Write(hostInternal); + msg.Write(hostExternal); + msg.Write(token); + m_unsentUnconnectedMessages.Enqueue(new NetTuple(clientExternal, msg)); + + // send message to host + msg = CreateMessage(10 + token.Length + 1); + msg.m_messageType = NetMessageType.NatIntroduction; + msg.Write((byte)1); + msg.Write(clientInternal); + msg.Write(clientExternal); + msg.Write(token); + m_unsentUnconnectedMessages.Enqueue(new NetTuple(hostExternal, msg)); + } + + /// + /// Called when host/client receives a NatIntroduction message from a master server + /// + internal void HandleNatIntroduction(int ptr) + { + VerifyNetworkThread(); + + // read intro + NetIncomingMessage tmp = SetupReadHelperMessage(ptr, 1000); // never mind length + + byte hostByte = tmp.ReadByte(); + IPEndPoint remoteInternal = tmp.ReadIPEndPoint(); + IPEndPoint remoteExternal = tmp.ReadIPEndPoint(); + string token = tmp.ReadString(); + bool isHost = (hostByte != 0); + + LogDebug("NAT introduction received; we are designated " + (isHost ? "host" : "client")); + + NetOutgoingMessage punch; + + if (!isHost && m_configuration.IsMessageTypeEnabled(NetIncomingMessageType.NatIntroductionSuccess) == false) + return; // no need to punch - we're not listening for nat intros! + + // send internal punch + punch = CreateMessage(1); + punch.m_messageType = NetMessageType.NatPunchMessage; + punch.Write(hostByte); + punch.Write(token); + m_unsentUnconnectedMessages.Enqueue(new NetTuple(remoteInternal, punch)); + + // send external punch + punch = CreateMessage(1); + punch.m_messageType = NetMessageType.NatPunchMessage; + punch.Write(hostByte); + punch.Write(token); + m_unsentUnconnectedMessages.Enqueue(new NetTuple(remoteExternal, punch)); + } + + /// + /// Called when receiving a NatPunchMessage from a remote endpoint + /// + private void HandleNatPunch(int ptr, IPEndPoint senderEndPoint) + { + NetIncomingMessage tmp = SetupReadHelperMessage(ptr, 1000); // never mind length + + byte fromHostByte = tmp.ReadByte(); + if (fromHostByte == 0) + { + // it's from client + LogDebug("NAT punch received from " + senderEndPoint + " we're host, so we ignore this"); + return; // don't alert hosts about nat punch successes; only clients + } + string token = tmp.ReadString(); + + LogDebug("NAT punch received from " + senderEndPoint + " we're client, so we've succeeded - token is " + token); + + // + // Release punch success to client; enabling him to Connect() to msg.SenderIPEndPoint if token is ok + // + NetIncomingMessage punchSuccess = CreateIncomingMessage(NetIncomingMessageType.NatIntroductionSuccess, 10); + punchSuccess.m_senderEndPoint = senderEndPoint; + punchSuccess.Write(token); + ReleaseMessage(punchSuccess); + + // send a return punch just for good measure + var punch = CreateMessage(1); + punch.m_messageType = NetMessageType.NatPunchMessage; + punch.Write((byte)0); + punch.Write(token); + m_unsentUnconnectedMessages.Enqueue(new NetTuple(senderEndPoint, punch)); + } + } +} diff --git a/Lidgren.Network/NetOutgoingMessage.cs b/Lidgren.Network/NetOutgoingMessage.cs new file mode 100644 index 000000000..da065019d --- /dev/null +++ b/Lidgren.Network/NetOutgoingMessage.cs @@ -0,0 +1,135 @@ +/* Copyright (c) 2010 Michael Lidgren + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom +the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ +using System; +using System.Diagnostics; + +namespace Lidgren.Network +{ + /// + /// Outgoing message used to send data to remote peer(s) + /// + [DebuggerDisplay("LengthBits={LengthBits}")] + public sealed class NetOutgoingMessage : NetBuffer + { + internal NetMessageType m_messageType; + internal bool m_isSent; + internal int m_recyclingCount; + + internal int m_fragmentGroup; // which group of fragments ths belongs to + internal int m_fragmentGroupTotalBits; // total number of bits in this group + internal int m_fragmentChunkByteSize; // size, in bytes, of every chunk but the last one + internal int m_fragmentChunkNumber; // which number chunk this is, starting with 0 + + internal NetOutgoingMessage() + { + } + + internal void Reset() + { + m_messageType = NetMessageType.LibraryError; + m_bitLength = 0; + m_isSent = false; + m_recyclingCount = 0; + m_fragmentGroup = 0; + } + + internal int Encode(byte[] intoBuffer, int ptr, int sequenceNumber) + { + // 8 bits - NetMessageType + // 1 bit - Fragment? + // 15 bits - Sequence number + // 16 bits - Payload length in bits + + intoBuffer[ptr++] = (byte)m_messageType; + + byte low = (byte)((sequenceNumber << 1) | (m_fragmentGroup == 0 ? 0 : 1)); + intoBuffer[ptr++] = low; + intoBuffer[ptr++] = (byte)(sequenceNumber >> 7); + + if (m_fragmentGroup == 0) + { + intoBuffer[ptr++] = (byte)m_bitLength; + intoBuffer[ptr++] = (byte)(m_bitLength >> 8); + + int byteLen = NetUtility.BytesToHoldBits(m_bitLength); + if (byteLen > 0) + { + Buffer.BlockCopy(m_data, 0, intoBuffer, ptr, byteLen); + ptr += byteLen; + } + } + else + { + int wasPtr = ptr; + intoBuffer[ptr++] = (byte)m_bitLength; + intoBuffer[ptr++] = (byte)(m_bitLength >> 8); + + // + // write fragmentation header + // + ptr = NetFragmentationHelper.WriteHeader(intoBuffer, ptr, m_fragmentGroup, m_fragmentGroupTotalBits, m_fragmentChunkByteSize, m_fragmentChunkNumber); + int hdrLen = ptr - wasPtr - 2; + + // update length + int realBitLength = m_bitLength + (hdrLen * 8); + intoBuffer[wasPtr] = (byte)realBitLength; + intoBuffer[wasPtr + 1] = (byte)(realBitLength >> 8); + + int byteLen = NetUtility.BytesToHoldBits(m_bitLength); + if (byteLen > 0) + { + Buffer.BlockCopy(m_data, (int)(m_fragmentChunkNumber * m_fragmentChunkByteSize), intoBuffer, ptr, byteLen); + ptr += byteLen; + } + } + + NetException.Assert(ptr > 0); + return ptr; + } + + internal int GetEncodedSize() + { + int retval = NetConstants.UnfragmentedMessageHeaderSize; // regular headers + if (m_fragmentGroup != 0) + retval += NetFragmentationHelper.GetFragmentationHeaderSize(m_fragmentGroup, m_fragmentGroupTotalBits / 8, m_fragmentChunkByteSize, m_fragmentChunkNumber); + retval += this.LengthBytes; + return retval; + } + + /// + /// Encrypt this message using the provided algorithm; no more writing can be done before sending it or the message will be corrupt! + /// + public bool Encrypt(INetEncryption encryption) + { + return encryption.Encrypt(this); + } + + /// + /// Returns a string that represents this object + /// + public override string ToString() + { + if (m_isSent) + return "[NetOutgoingMessage " + m_messageType + " " + this.LengthBytes + " bytes]"; + + return "[NetOutgoingMessage " + this.LengthBytes + " bytes]"; + } + } +} diff --git a/Lidgren.Network/NetPeer.Discovery.cs b/Lidgren.Network/NetPeer.Discovery.cs new file mode 100644 index 000000000..c00c8265f --- /dev/null +++ b/Lidgren.Network/NetPeer.Discovery.cs @@ -0,0 +1,60 @@ +using System; +using System.Net; + +namespace Lidgren.Network +{ + public partial class NetPeer + { + /// + /// Emit a discovery signal to all hosts on your subnet + /// + public void DiscoverLocalPeers(int serverPort) + { + NetOutgoingMessage om = CreateMessage(0); + om.m_messageType = NetMessageType.Discovery; + m_unsentUnconnectedMessages.Enqueue(new NetTuple(new IPEndPoint(IPAddress.Broadcast, serverPort), om)); + } + + /// + /// Emit a discovery signal to a single known host + /// + public bool DiscoverKnownPeer(string host, int serverPort) + { + IPAddress address = NetUtility.Resolve(host); + if (address == null) + return false; + DiscoverKnownPeer(new IPEndPoint(address, serverPort)); + return true; + } + + /// + /// Emit a discovery signal to a single known host + /// + public void DiscoverKnownPeer(IPEndPoint endPoint) + { + NetOutgoingMessage om = CreateMessage(0); + om.m_messageType = NetMessageType.Discovery; + m_unsentUnconnectedMessages.Enqueue(new NetTuple(endPoint, om)); + } + + /// + /// Send a discovery response message + /// + public void SendDiscoveryResponse(NetOutgoingMessage msg, IPEndPoint recipient) + { + if (recipient == null) + throw new ArgumentNullException("recipient"); + + if (msg == null) + msg = CreateMessage(0); + else if (msg.m_isSent) + throw new NetException("Message has already been sent!"); + + if (msg.LengthBytes >= m_configuration.MaximumTransmissionUnit) + throw new NetException("Cannot send discovery message larger than MTU (currently " + m_configuration.MaximumTransmissionUnit + " bytes)"); + + msg.m_messageType = NetMessageType.DiscoveryResponse; + m_unsentUnconnectedMessages.Enqueue(new NetTuple(recipient, msg)); + } + } +} diff --git a/Lidgren.Network/NetPeer.Fragmentation.cs b/Lidgren.Network/NetPeer.Fragmentation.cs new file mode 100644 index 000000000..bff9a5c03 --- /dev/null +++ b/Lidgren.Network/NetPeer.Fragmentation.cs @@ -0,0 +1,161 @@ +using System; +using System.Threading; +using System.Collections.Generic; + +namespace Lidgren.Network +{ + internal class ReceivedFragmentGroup + { + public float LastReceived; + public byte[] Data; + public NetBitVector ReceivedChunks; + } + + public partial class NetPeer + { + private int m_lastUsedFragmentGroup; + + private Dictionary> m_receivedFragmentGroups; + + // on user thread + private void SendFragmentedMessage(NetOutgoingMessage msg, IList recipients, NetDeliveryMethod method, int sequenceChannel) + { + // Note: this group id is PER SENDING/NetPeer; ie. same id is sent to all recipients; + // this should be ok however; as long as recipients differentiate between same id but different sender + int group = Interlocked.Increment(ref m_lastUsedFragmentGroup); + if (group >= NetConstants.MaxFragmentationGroups) + { + // @TODO: not thread safe; but in practice probably not an issue + m_lastUsedFragmentGroup = 1; + group = 1; + } + msg.m_fragmentGroup = group; + + // do not send msg; but set fragmentgroup in case user tries to recycle it immediately + + // create fragmentation specifics + int totalBytes = msg.LengthBytes; + + // determine minimum mtu for all recipients + int mtu = GetMTU(recipients); + int bytesPerChunk = NetFragmentationHelper.GetBestChunkSize(group, totalBytes, mtu); + + int numChunks = totalBytes / bytesPerChunk; + if (numChunks * bytesPerChunk < totalBytes) + numChunks++; + + int bitsPerChunk = bytesPerChunk * 8; + int bitsLeft = msg.LengthBits; + for (int i = 0; i < numChunks; i++) + { + NetOutgoingMessage chunk = CreateMessage(mtu); + + chunk.m_bitLength = (bitsLeft > bitsPerChunk ? bitsPerChunk : bitsLeft); + chunk.m_data = msg.m_data; + chunk.m_fragmentGroup = group; + chunk.m_fragmentGroupTotalBits = totalBytes * 8; + chunk.m_fragmentChunkByteSize = bytesPerChunk; + chunk.m_fragmentChunkNumber = i; + + NetException.Assert(chunk.m_bitLength != 0); + NetException.Assert(chunk.GetEncodedSize() < mtu); + + Interlocked.Add(ref chunk.m_recyclingCount, recipients.Count); + + foreach (NetConnection recipient in recipients) + recipient.EnqueueMessage(chunk, method, sequenceChannel); + + bitsLeft -= bitsPerChunk; + } + + return; + } + + private void HandleReleasedFragment(NetIncomingMessage im) + { + VerifyNetworkThread(); + + // + // read fragmentation header and combine fragments + // + int group; + int totalBits; + int chunkByteSize; + int chunkNumber; + int ptr = NetFragmentationHelper.ReadHeader( + im.m_data, 0, + out group, + out totalBits, + out chunkByteSize, + out chunkNumber + ); + + NetException.Assert(im.LengthBytes > ptr); + + NetException.Assert(group > 0); + NetException.Assert(totalBits > 0); + NetException.Assert(chunkByteSize > 0); + + int totalBytes = NetUtility.BytesToHoldBits((int)totalBits); + int totalNumChunks = totalBytes / chunkByteSize; + if (totalNumChunks * chunkByteSize < totalBytes) + totalNumChunks++; + + NetException.Assert(chunkNumber < totalNumChunks); + + if (chunkNumber >= totalNumChunks) + { + LogWarning("Index out of bounds for chunk " + chunkNumber + " (total chunks " + totalNumChunks + ")"); + return; + } + + Dictionary groups; + if (!m_receivedFragmentGroups.TryGetValue(im.SenderConnection, out groups)) + { + groups = new Dictionary(); + m_receivedFragmentGroups[im.SenderConnection] = groups; + } + + ReceivedFragmentGroup info; + if (!groups.TryGetValue(group, out info)) + { + info = new ReceivedFragmentGroup(); + info.Data = new byte[totalBytes]; + info.ReceivedChunks = new NetBitVector(totalNumChunks); + groups[group] = info; + } + + info.ReceivedChunks[chunkNumber] = true; + info.LastReceived = (float)NetTime.Now; + + // copy to data + int offset = (chunkNumber * chunkByteSize); + Buffer.BlockCopy(im.m_data, ptr, info.Data, offset, im.LengthBytes - ptr); + + int cnt = info.ReceivedChunks.Count(); + //LogVerbose("Found fragment #" + chunkNumber + " in group " + group + " offset " + offset + " of total bits " + totalBits + " (total chunks done " + cnt + ")"); + + LogVerbose("Received fragment " + chunkNumber + " of " + totalNumChunks + " (" + cnt + " chunks received)"); + + if (info.ReceivedChunks.Count() == totalNumChunks) + { + // Done! Transform this incoming message + im.m_data = info.Data; + im.m_bitLength = (int)totalBits; + im.m_isFragment = false; + + LogVerbose("Fragment group #" + group + " fully received in " + totalNumChunks + " chunks (" + totalBits + " bits)"); + groups.Remove(group); + + ReleaseMessage(im); + } + else + { + // data has been copied; recycle this incoming message + Recycle(im); + } + + return; + } + } +} diff --git a/Lidgren.Network/NetPeer.Internal.cs b/Lidgren.Network/NetPeer.Internal.cs new file mode 100644 index 000000000..eeb93e869 --- /dev/null +++ b/Lidgren.Network/NetPeer.Internal.cs @@ -0,0 +1,691 @@ +#if !__ANDROID__ && !IOS +#define IS_MAC_AVAILABLE +#endif + +using System; +using System.Net; +using System.Threading; +using System.Diagnostics; +using System.Security.Cryptography; +using System.Net.Sockets; +using System.Collections.Generic; + +namespace Lidgren.Network +{ + public partial class NetPeer + { + private NetPeerStatus m_status; + private Thread m_networkThread; + private Socket m_socket; + internal byte[] m_sendBuffer; + internal byte[] m_receiveBuffer; + internal NetIncomingMessage m_readHelperMessage; + private EndPoint m_senderRemote; + private object m_initializeLock = new object(); + private uint m_frameCounter; + private double m_lastHeartbeat; + private NetUPnP m_upnp; + + internal readonly NetPeerConfiguration m_configuration; + private readonly NetQueue m_releasedIncomingMessages; + internal readonly NetQueue> m_unsentUnconnectedMessages; + + internal Dictionary m_handshakes; + + internal readonly NetPeerStatistics m_statistics; + internal long m_uniqueIdentifier; + internal bool m_executeFlushSendQueue; + + private AutoResetEvent m_messageReceivedEvent; + private List> m_receiveCallbacks; + + /// + /// Gets the socket, if Start() has been called + /// + public Socket Socket { get { return m_socket; } } + + /// + /// Call this to register a callback for when a new message arrives + /// + public void RegisterReceivedCallback(SendOrPostCallback callback) + { + if (SynchronizationContext.Current == null) + throw new NetException("Need a SynchronizationContext to register callback on correct thread!"); + if (m_receiveCallbacks == null) + m_receiveCallbacks = new List>(); + m_receiveCallbacks.Add(new NetTuple(SynchronizationContext.Current, callback)); + } + + /// + /// Call this to unregister a callback, but remember to do it in the same synchronization context! + /// + public void UnregisterReceivedCallback(SendOrPostCallback callback) + { + if (m_receiveCallbacks == null) + return; + m_receiveCallbacks.Remove(new NetTuple(SynchronizationContext.Current, callback)); + if (m_receiveCallbacks.Count < 1) + m_receiveCallbacks = null; + } + + internal void ReleaseMessage(NetIncomingMessage msg) + { + NetException.Assert(msg.m_incomingMessageType != NetIncomingMessageType.Error); + + if (msg.m_isFragment) + { + HandleReleasedFragment(msg); + return; + } + + m_releasedIncomingMessages.Enqueue(msg); + + if (m_messageReceivedEvent != null) + m_messageReceivedEvent.Set(); + + if (m_receiveCallbacks != null) + { + foreach (var tuple in m_receiveCallbacks) + tuple.Item1.Post(tuple.Item2, this); + } + } + + private void InitializeNetwork() + { + lock (m_initializeLock) + { + m_configuration.Lock(); + + if (m_status == NetPeerStatus.Running) + return; + + if (m_configuration.m_enableUPnP) + m_upnp = new NetUPnP(this); + + InitializePools(); + + m_releasedIncomingMessages.Clear(); + m_unsentUnconnectedMessages.Clear(); + m_handshakes.Clear(); + + // bind to socket + IPEndPoint iep = null; + + iep = new IPEndPoint(m_configuration.LocalAddress, m_configuration.Port); + EndPoint ep = (EndPoint)iep; + + m_socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + m_socket.ReceiveBufferSize = m_configuration.ReceiveBufferSize; + m_socket.SendBufferSize = m_configuration.SendBufferSize; + m_socket.Blocking = false; + m_socket.Bind(ep); + + try + { + const uint IOC_IN = 0x80000000; + const uint IOC_VENDOR = 0x18000000; + uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12; + m_socket.IOControl((int)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null); + } + catch + { + // ignore; SIO_UDP_CONNRESET not supported on this platform + } + + IPEndPoint boundEp = m_socket.LocalEndPoint as IPEndPoint; + LogDebug("Socket bound to " + boundEp + ": " + m_socket.IsBound); + m_listenPort = boundEp.Port; + + m_receiveBuffer = new byte[m_configuration.ReceiveBufferSize]; + m_sendBuffer = new byte[m_configuration.SendBufferSize]; + m_readHelperMessage = new NetIncomingMessage(NetIncomingMessageType.Error); + m_readHelperMessage.m_data = m_receiveBuffer; + + byte[] macBytes = new byte[8]; + NetRandom.Instance.NextBytes(macBytes); + +#if IS_MAC_AVAILABLE + try + { + System.Net.NetworkInformation.PhysicalAddress pa = NetUtility.GetMacAddress(); + if (pa != null) + { + macBytes = pa.GetAddressBytes(); + LogVerbose("Mac address is " + NetUtility.ToHexString(macBytes)); + } + else + { + LogWarning("Failed to get Mac address"); + } + } + catch (NotSupportedException) + { + // not supported; lets just keep the random bytes set above + } +#endif + byte[] epBytes = BitConverter.GetBytes(boundEp.GetHashCode()); + byte[] combined = new byte[epBytes.Length + macBytes.Length]; + Array.Copy(epBytes, 0, combined, 0, epBytes.Length); + Array.Copy(macBytes, 0, combined, epBytes.Length, macBytes.Length); + m_uniqueIdentifier = BitConverter.ToInt64(SHA1.Create().ComputeHash(combined), 0); + + m_status = NetPeerStatus.Running; + } + } + + private void NetworkLoop() + { + VerifyNetworkThread(); + + LogDebug("Network thread started"); + + // + // Network loop + // + do + { + try + { + Heartbeat(); + } + catch (Exception ex) + { + LogWarning(ex.ToString()); + } + } while (m_status == NetPeerStatus.Running); + + // + // perform shutdown + // + ExecutePeerShutdown(); + } + + private void ExecutePeerShutdown() + { + VerifyNetworkThread(); + + LogDebug("Shutting down..."); + + // disconnect and make one final heartbeat + var list = new List(m_handshakes.Count + m_connections.Count); + lock (m_connections) + { + foreach (var conn in m_connections) + if (conn != null) + list.Add(conn); + + lock (m_handshakes) + { + foreach (var hs in m_handshakes.Values) + if (hs != null) + list.Add(hs); + + // shut down connections + foreach (NetConnection conn in list) + conn.Shutdown(m_shutdownReason); + } + } + + FlushDelayedPackets(); + + // one final heartbeat, will send stuff and do disconnect + Heartbeat(); + + Thread.Sleep(10); + + lock (m_initializeLock) + { + try + { + if (m_socket != null) + { + try + { + m_socket.Shutdown(SocketShutdown.Receive); + } + catch { } + m_socket.Close(2); // 2 seconds timeout + } + if (m_messageReceivedEvent != null) + { + m_messageReceivedEvent.Set(); + m_messageReceivedEvent.Close(); + m_messageReceivedEvent = null; + } + } + finally + { + m_socket = null; + m_status = NetPeerStatus.NotRunning; + LogDebug("Shutdown complete"); + } + + m_receiveBuffer = null; + m_sendBuffer = null; + m_unsentUnconnectedMessages.Clear(); + m_connections.Clear(); + m_connectionLookup.Clear(); + m_handshakes.Clear(); + } + + return; + } + + private void Heartbeat() + { + VerifyNetworkThread(); + + double dnow = NetTime.Now; + float now = (float)dnow; + + double delta = dnow - m_lastHeartbeat; + + int maxCHBpS = 1250 - m_connections.Count; + if (maxCHBpS < 250) + maxCHBpS = 250; + if (delta > (1.0 / (double)maxCHBpS) || delta < 0.0) // max connection heartbeats/second max + { + m_frameCounter++; + m_lastHeartbeat = dnow; + + // do handshake heartbeats + if ((m_frameCounter % 3) == 0) + { + foreach (var kvp in m_handshakes) + { + NetConnection conn = kvp.Value as NetConnection; +#if DEBUG + // sanity check + if (kvp.Key != kvp.Key) + LogWarning("Sanity fail! Connection in handshake list under wrong key!"); +#endif + conn.UnconnectedHeartbeat(now); + if (conn.m_status == NetConnectionStatus.Connected || conn.m_status == NetConnectionStatus.Disconnected) + { +#if DEBUG + // sanity check + if (conn.m_status == NetConnectionStatus.Disconnected && m_handshakes.ContainsKey(conn.RemoteEndPoint)) + { + LogWarning("Sanity fail! Handshakes list contained disconnected connection!"); + m_handshakes.Remove(conn.RemoteEndPoint); + } +#endif + break; // collection has been modified + } + } + } + +#if DEBUG + SendDelayedPackets(); +#endif + + // update m_executeFlushSendQueue + if (m_configuration.m_autoFlushSendQueue) + m_executeFlushSendQueue = true; + + // do connection heartbeats + lock (m_connections) + { + foreach (NetConnection conn in m_connections) + { + conn.Heartbeat(now, m_frameCounter); + if (conn.m_status == NetConnectionStatus.Disconnected) + { + // + // remove connection + // + m_connections.Remove(conn); + m_connectionLookup.Remove(conn.RemoteEndPoint); + break; // can't continue iteration here + } + } + } + m_executeFlushSendQueue = false; + + // send unsent unconnected messages + NetTuple unsent; + while (m_unsentUnconnectedMessages.TryDequeue(out unsent)) + { + NetOutgoingMessage om = unsent.Item2; + + bool connReset; + int len = om.Encode(m_sendBuffer, 0, 0); + SendPacket(len, unsent.Item1, 1, out connReset); + + Interlocked.Decrement(ref om.m_recyclingCount); + if (om.m_recyclingCount <= 0) + Recycle(om); + } + } + + // + // read from socket + // + if (m_socket == null) + return; + + if (!m_socket.Poll(1000, SelectMode.SelectRead)) // wait up to 1 ms for data to arrive + return; + + //if (m_socket == null || m_socket.Available < 1) + // return; + + // update now + dnow = NetTime.Now; + now = (float)dnow; + + do + { + int bytesReceived = 0; + try + { + bytesReceived = m_socket.ReceiveFrom(m_receiveBuffer, 0, m_receiveBuffer.Length, SocketFlags.None, ref m_senderRemote); + } + catch (SocketException sx) + { + if (sx.SocketErrorCode == SocketError.ConnectionReset) + { + // connection reset by peer, aka connection forcibly closed aka "ICMP port unreachable" + // we should shut down the connection; but m_senderRemote seemingly cannot be trusted, so which connection should we shut down?! + // So, what to do? + LogWarning("ConnectionReset"); + return; + } + + LogWarning(sx.ToString()); + return; + } + + if (bytesReceived < NetConstants.HeaderByteSize) + return; + + //LogVerbose("Received " + bytesReceived + " bytes"); + + IPEndPoint ipsender = (IPEndPoint)m_senderRemote; + + if (m_upnp != null && now < m_upnp.m_discoveryResponseDeadline) + { + // is this an UPnP response? + try + { + string resp = System.Text.Encoding.ASCII.GetString(m_receiveBuffer, 0, bytesReceived); + if (resp.Contains("upnp:rootdevice") || resp.Contains("UPnP/1.0")) + { + resp = resp.Substring(resp.ToLower().IndexOf("location:") + 9); + resp = resp.Substring(0, resp.IndexOf("\r")).Trim(); + m_upnp.ExtractServiceUrl(resp); + return; + } + } + catch { } + } + + NetConnection sender = null; + m_connectionLookup.TryGetValue(ipsender, out sender); + + // + // parse packet into messages + // + int numMessages = 0; + int ptr = 0; + while ((bytesReceived - ptr) >= NetConstants.HeaderByteSize) + { + // decode header + // 8 bits - NetMessageType + // 1 bit - Fragment? + // 15 bits - Sequence number + // 16 bits - Payload length in bits + + numMessages++; + + NetMessageType tp = (NetMessageType)m_receiveBuffer[ptr++]; + + byte low = m_receiveBuffer[ptr++]; + byte high = m_receiveBuffer[ptr++]; + + bool isFragment = ((low & 1) == 1); + ushort sequenceNumber = (ushort)((low >> 1) | (((int)high) << 7)); + + ushort payloadBitLength = (ushort)(m_receiveBuffer[ptr++] | (m_receiveBuffer[ptr++] << 8)); + int payloadByteLength = NetUtility.BytesToHoldBits(payloadBitLength); + + if (bytesReceived - ptr < payloadByteLength) + { + LogWarning("Malformed packet; stated payload length " + payloadByteLength + ", remaining bytes " + (bytesReceived - ptr)); + return; + } + + try + { + NetException.Assert(tp < NetMessageType.Unused1 || tp > NetMessageType.Unused29); + + if (tp >= NetMessageType.LibraryError) + { + if (sender != null) + sender.ReceivedLibraryMessage(tp, ptr, payloadByteLength); + else + ReceivedUnconnectedLibraryMessage(dnow, ipsender, tp, ptr, payloadByteLength); + } + else + { + if (sender == null && !m_configuration.IsMessageTypeEnabled(NetIncomingMessageType.UnconnectedData)) + return; // dropping unconnected message since it's not enabled + + NetIncomingMessage msg = CreateIncomingMessage(NetIncomingMessageType.Data, payloadByteLength); + msg.m_isFragment = isFragment; + msg.m_receiveTime = dnow; + msg.m_sequenceNumber = sequenceNumber; + msg.m_receivedMessageType = tp; + msg.m_senderConnection = sender; + msg.m_senderEndPoint = ipsender; + msg.m_bitLength = payloadBitLength; + Buffer.BlockCopy(m_receiveBuffer, ptr, msg.m_data, 0, payloadByteLength); + if (sender != null) + { + if (tp == NetMessageType.Unconnected) + { + // We're connected; but we can still send unconnected messages to this peer + msg.m_incomingMessageType = NetIncomingMessageType.UnconnectedData; + ReleaseMessage(msg); + } + else + { + // connected application (non-library) message + sender.ReceivedMessage(msg); + } + } + else + { + // at this point we know the message type is enabled + // unconnected application (non-library) message + msg.m_incomingMessageType = NetIncomingMessageType.UnconnectedData; + ReleaseMessage(msg); + } + } + } + catch (Exception ex) + { + LogError("Packet parsing error: " + ex.Message + " from " + ipsender); + } + ptr += payloadByteLength; + } + + m_statistics.PacketReceived(bytesReceived, numMessages); + if (sender != null) + sender.m_statistics.PacketReceived(bytesReceived, numMessages); + + } while (m_socket.Available > 0); + } + + /// + /// If NetPeerConfiguration.AutoFlushSendQueue() is false; you need to call this to send all messages queued using SendMessage() + /// + public void FlushSendQueue() + { + m_executeFlushSendQueue = true; + } + + internal void HandleIncomingDiscoveryRequest(double now, IPEndPoint senderEndPoint, int ptr, int payloadByteLength) + { + if (m_configuration.IsMessageTypeEnabled(NetIncomingMessageType.DiscoveryRequest)) + { + NetIncomingMessage dm = CreateIncomingMessage(NetIncomingMessageType.DiscoveryRequest, payloadByteLength); + if (payloadByteLength > 0) + Buffer.BlockCopy(m_receiveBuffer, ptr, dm.m_data, 0, payloadByteLength); + dm.m_receiveTime = now; + dm.m_bitLength = payloadByteLength * 8; + dm.m_senderEndPoint = senderEndPoint; + ReleaseMessage(dm); + } + } + + internal void HandleIncomingDiscoveryResponse(double now, IPEndPoint senderEndPoint, int ptr, int payloadByteLength) + { + if (m_configuration.IsMessageTypeEnabled(NetIncomingMessageType.DiscoveryResponse)) + { + NetIncomingMessage dr = CreateIncomingMessage(NetIncomingMessageType.DiscoveryResponse, payloadByteLength); + if (payloadByteLength > 0) + Buffer.BlockCopy(m_receiveBuffer, ptr, dr.m_data, 0, payloadByteLength); + dr.m_receiveTime = now; + dr.m_bitLength = payloadByteLength * 8; + dr.m_senderEndPoint = senderEndPoint; + ReleaseMessage(dr); + } + } + + private void ReceivedUnconnectedLibraryMessage(double now, IPEndPoint senderEndPoint, NetMessageType tp, int ptr, int payloadByteLength) + { + NetConnection shake; + if (m_handshakes.TryGetValue(senderEndPoint, out shake)) + { + shake.ReceivedHandshake(now, tp, ptr, payloadByteLength); + return; + } + + // + // Library message from a completely unknown sender; lets just accept Connect + // + switch (tp) + { + case NetMessageType.Discovery: + HandleIncomingDiscoveryRequest(now, senderEndPoint, ptr, payloadByteLength); + return; + case NetMessageType.DiscoveryResponse: + HandleIncomingDiscoveryResponse(now, senderEndPoint, ptr, payloadByteLength); + return; + case NetMessageType.NatIntroduction: + HandleNatIntroduction(ptr); + return; + case NetMessageType.NatPunchMessage: + HandleNatPunch(ptr, senderEndPoint); + return; + case NetMessageType.ConnectResponse: + + lock (m_handshakes) + { + foreach (var hs in m_handshakes) + { + if (hs.Key.Address.Equals(senderEndPoint.Address)) + { + if (hs.Value.m_connectionInitiator) + { + // + // We are currently trying to connection to XX.XX.XX.XX:Y + // ... but we just received a ConnectResponse from XX.XX.XX.XX:Z + // Lets just assume the router decided to use this port instead + // + var hsconn = hs.Value; + m_connectionLookup.Remove(hs.Key); + m_handshakes.Remove(hs.Key); + + LogDebug("Detected host port change; rerouting connection to " + senderEndPoint); + hsconn.MutateEndPoint(senderEndPoint); + + m_connectionLookup.Add(senderEndPoint, hsconn); + m_handshakes.Add(senderEndPoint, hsconn); + + hsconn.ReceivedHandshake(now, tp, ptr, payloadByteLength); + return; + } + } + } + } + + LogWarning("Received unhandled library message " + tp + " from " + senderEndPoint); + return; + case NetMessageType.Connect: + if (m_configuration.AcceptIncomingConnections == false) + { + LogWarning("Received Connect, but we're not accepting incoming connections!"); + return; + } + // handle connect + // It's someone wanting to shake hands with us! + + int reservedSlots = m_handshakes.Count + m_connections.Count; + if (reservedSlots >= m_configuration.m_maximumConnections) + { + // server full + NetOutgoingMessage full = CreateMessage("Server full"); + full.m_messageType = NetMessageType.Disconnect; + SendLibrary(full, senderEndPoint); + return; + } + + // Ok, start handshake! + NetConnection conn = new NetConnection(this, senderEndPoint); + conn.m_status = NetConnectionStatus.ReceivedInitiation; + m_handshakes.Add(senderEndPoint, conn); + conn.ReceivedHandshake(now, tp, ptr, payloadByteLength); + return; + + case NetMessageType.Disconnect: + // this is probably ok + LogVerbose("Received Disconnect from unconnected source: " + senderEndPoint); + return; + default: + LogWarning("Received unhandled library message " + tp + " from " + senderEndPoint); + return; + } + } + + internal void AcceptConnection(NetConnection conn) + { + // LogDebug("Accepted connection " + conn); + conn.InitExpandMTU(NetTime.Now); + + if (m_handshakes.Remove(conn.m_remoteEndPoint) == false) + LogWarning("AcceptConnection called but m_handshakes did not contain it!"); + + lock (m_connections) + { + if (m_connections.Contains(conn)) + { + LogWarning("AcceptConnection called but m_connection already contains it!"); + } + else + { + m_connections.Add(conn); + m_connectionLookup.Add(conn.m_remoteEndPoint, conn); + } + } + } + + [Conditional("DEBUG")] + internal void VerifyNetworkThread() + { + Thread ct = Thread.CurrentThread; + if (Thread.CurrentThread != m_networkThread) + throw new NetException("Executing on wrong thread! Should be library system thread (is " + ct.Name + " mId " + ct.ManagedThreadId + ")"); + } + + internal NetIncomingMessage SetupReadHelperMessage(int ptr, int payloadLength) + { + VerifyNetworkThread(); + + m_readHelperMessage.m_bitLength = (ptr + payloadLength) * 8; + m_readHelperMessage.m_readPosition = (ptr * 8); + return m_readHelperMessage; + } + } +} \ No newline at end of file diff --git a/Lidgren.Network/NetPeer.LatencySimulation.cs b/Lidgren.Network/NetPeer.LatencySimulation.cs new file mode 100644 index 000000000..a43021f52 --- /dev/null +++ b/Lidgren.Network/NetPeer.LatencySimulation.cs @@ -0,0 +1,304 @@ +/* Copyright (c) 2010 Michael Lidgren + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom +the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ +//#define USE_RELEASE_STATISTICS + +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Diagnostics; + +namespace Lidgren.Network +{ + public partial class NetPeer + { + +#if DEBUG + private readonly List m_delayedPackets = new List(); + + private class DelayedPacket + { + public byte[] Data; + public double DelayedUntil; + public IPEndPoint Target; + } + + internal void SendPacket(int numBytes, IPEndPoint target, int numMessages, out bool connectionReset) + { + connectionReset = false; + + // simulate loss + float loss = m_configuration.m_loss; + if (loss > 0.0f) + { + if ((float)NetRandom.Instance.NextDouble() < loss) + { + LogVerbose("Sending packet " + numBytes + " bytes - SIMULATED LOST!"); + return; // packet "lost" + } + } + + m_statistics.PacketSent(numBytes, numMessages); + + // simulate latency + float m = m_configuration.m_minimumOneWayLatency; + float r = m_configuration.m_randomOneWayLatency; + if (m == 0.0f && r == 0.0f) + { + // no latency simulation + // LogVerbose("Sending packet " + numBytes + " bytes"); + bool wasSent = ActuallySendPacket(m_sendBuffer, numBytes, target, out connectionReset); + // TODO: handle wasSent == false? + return; + } + + int num = 1; + if (m_configuration.m_duplicates > 0.0f && NetRandom.Instance.NextSingle() < m_configuration.m_duplicates) + num++; + + float delay = 0; + for (int i = 0; i < num; i++) + { + delay = m_configuration.m_minimumOneWayLatency + (NetRandom.Instance.NextSingle() * m_configuration.m_randomOneWayLatency); + + // Enqueue delayed packet + DelayedPacket p = new DelayedPacket(); + p.Target = target; + p.Data = new byte[numBytes]; + Buffer.BlockCopy(m_sendBuffer, 0, p.Data, 0, numBytes); + p.DelayedUntil = NetTime.Now + delay; + + m_delayedPackets.Add(p); + } + + // LogVerbose("Sending packet " + numBytes + " bytes - delayed " + NetTime.ToReadable(delay)); + } + + private void SendDelayedPackets() + { + if (m_delayedPackets.Count <= 0) + return; + + double now = NetTime.Now; + + bool connectionReset; + + RestartDelaySending: + foreach (DelayedPacket p in m_delayedPackets) + { + if (now > p.DelayedUntil) + { + ActuallySendPacket(p.Data, p.Data.Length, p.Target, out connectionReset); + m_delayedPackets.Remove(p); + goto RestartDelaySending; + } + } + } + + private void FlushDelayedPackets() + { + try + { + bool connectionReset; + foreach (DelayedPacket p in m_delayedPackets) + ActuallySendPacket(p.Data, p.Data.Length, p.Target, out connectionReset); + m_delayedPackets.Clear(); + } + catch { } + } + + internal bool ActuallySendPacket(byte[] data, int numBytes, IPEndPoint target, out bool connectionReset) + { + connectionReset = false; + try + { + // TODO: refactor this check outta here + if (target.Address == IPAddress.Broadcast) + { + // Some networks do not allow + // a global broadcast so we use the BroadcastAddress from the configuration + // this can be resolved to a local broadcast addresss e.g 192.168.x.255 + target.Address = m_configuration.BroadcastAddress; + m_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true); + } + + int bytesSent = m_socket.SendTo(data, 0, numBytes, SocketFlags.None, target); + if (numBytes != bytesSent) + LogWarning("Failed to send the full " + numBytes + "; only " + bytesSent + " bytes sent in packet!"); + + // LogDebug("Sent " + numBytes + " bytes"); + } + catch (SocketException sx) + { + if (sx.SocketErrorCode == SocketError.WouldBlock) + { + // send buffer full? + LogWarning("Socket threw exception; would block - send buffer full? Increase in NetPeerConfiguration"); + return false; + } + if (sx.SocketErrorCode == SocketError.ConnectionReset) + { + // connection reset by peer, aka connection forcibly closed aka "ICMP port unreachable" + connectionReset = true; + return false; + } + LogError("Failed to send packet: " + sx); + } + catch (Exception ex) + { + LogError("Failed to send packet: " + ex); + } + finally + { + if (target.Address == IPAddress.Broadcast) + m_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, false); + } + return true; + } + + internal bool SendMTUPacket(int numBytes, IPEndPoint target) + { + try + { + m_socket.DontFragment = true; + int bytesSent = m_socket.SendTo(m_sendBuffer, 0, numBytes, SocketFlags.None, target); + if (numBytes != bytesSent) + LogWarning("Failed to send the full " + numBytes + "; only " + bytesSent + " bytes sent in packet!"); + + m_statistics.PacketSent(numBytes, 1); + } + catch (SocketException sx) + { + if (sx.SocketErrorCode == SocketError.MessageSize) + return false; + if (sx.SocketErrorCode == SocketError.WouldBlock) + { + // send buffer full? + LogWarning("Socket threw exception; would block - send buffer full? Increase in NetPeerConfiguration"); + return true; + } + if (sx.SocketErrorCode == SocketError.ConnectionReset) + return true; + LogError("Failed to send packet: (" + sx.SocketErrorCode + ") " + sx); + } + catch (Exception ex) + { + LogError("Failed to send packet: " + ex); + } + finally + { + m_socket.DontFragment = false; + } + return true; + } +#else + internal bool SendMTUPacket(int numBytes, IPEndPoint target) + { + try + { + m_socket.DontFragment = true; + int bytesSent = m_socket.SendTo(m_sendBuffer, 0, numBytes, SocketFlags.None, target); + if (numBytes != bytesSent) + LogWarning("Failed to send the full " + numBytes + "; only " + bytesSent + " bytes sent in packet!"); + } + catch (SocketException sx) + { + if (sx.SocketErrorCode == SocketError.MessageSize) + return false; + if (sx.SocketErrorCode == SocketError.WouldBlock) + { + // send buffer full? + LogWarning("Socket threw exception; would block - send buffer full? Increase in NetPeerConfiguration"); + return true; + } + if (sx.SocketErrorCode == SocketError.ConnectionReset) + return true; + LogError("Failed to send packet: (" + sx.SocketErrorCode + ") " + sx); + } + catch (Exception ex) + { + LogError("Failed to send packet: " + ex); + } + finally + { + m_socket.DontFragment = false; + } + return true; + } + + // + // Release - just send the packet straight away + // + internal void SendPacket(int numBytes, IPEndPoint target, int numMessages, out bool connectionReset) + { +#if USE_RELEASE_STATISTICS + m_statistics.PacketSent(numBytes, numMessages); +#endif + connectionReset = false; + try + { + // TODO: refactor this check outta here + if (target.Address == IPAddress.Broadcast) + m_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true); + + int bytesSent = m_socket.SendTo(m_sendBuffer, 0, numBytes, SocketFlags.None, target); + if (numBytes != bytesSent) + LogWarning("Failed to send the full " + numBytes + "; only " + bytesSent + " bytes sent in packet!"); + } + catch (SocketException sx) + { + if (sx.SocketErrorCode == SocketError.WouldBlock) + { + // send buffer full? + LogWarning("Socket threw exception; would block - send buffer full? Increase in NetPeerConfiguration"); + return; + } + if (sx.SocketErrorCode == SocketError.ConnectionReset) + { + // connection reset by peer, aka connection forcibly closed aka "ICMP port unreachable" + connectionReset = true; + return; + } + LogError("Failed to send packet: " + sx); + } + catch (Exception ex) + { + LogError("Failed to send packet: " + ex); + } + finally + { + if (target.Address == IPAddress.Broadcast) + m_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, false); + } + return; + } + + private void FlushDelayedPackets() + { + } + + private void SendCallBack(IAsyncResult res) + { + NetException.Assert(res.IsCompleted == true); + m_socket.EndSendTo(res); + } +#endif + } +} \ No newline at end of file diff --git a/Lidgren.Network/NetPeer.Logging.cs b/Lidgren.Network/NetPeer.Logging.cs new file mode 100644 index 000000000..9b1975cd3 --- /dev/null +++ b/Lidgren.Network/NetPeer.Logging.cs @@ -0,0 +1,71 @@ +/* Copyright (c) 2010 Michael Lidgren + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom +the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +using System.Diagnostics; + +namespace Lidgren.Network +{ + public partial class NetPeer + { + [Conditional("DEBUG")] + internal void LogVerbose(string message) + { +#if __ANDROID__ + Android.Util.Log.WriteLine(Android.Util.LogPriority.Verbose, "", message); +#endif + Debug.WriteLine(message); + + if (m_configuration.IsMessageTypeEnabled(NetIncomingMessageType.VerboseDebugMessage)) + ReleaseMessage(CreateIncomingMessage(NetIncomingMessageType.VerboseDebugMessage, message)); + } + + [Conditional("DEBUG")] + internal void LogDebug(string message) + { +#if __ANDROID__ + Android.Util.Log.WriteLine(Android.Util.LogPriority.Debug, "", message); +#endif + Debug.WriteLine(message); + + if (m_configuration.IsMessageTypeEnabled(NetIncomingMessageType.DebugMessage)) + ReleaseMessage(CreateIncomingMessage(NetIncomingMessageType.DebugMessage, message)); + } + + internal void LogWarning(string message) + { +#if __ANDROID__ + Android.Util.Log.WriteLine(Android.Util.LogPriority.Warn, "", message); +#endif + Debug.WriteLine(message); + + if (m_configuration.IsMessageTypeEnabled(NetIncomingMessageType.WarningMessage)) + ReleaseMessage(CreateIncomingMessage(NetIncomingMessageType.WarningMessage, message)); + } + + internal void LogError(string message) + { +#if __ANDROID__ + Android.Util.Log.WriteLine(Android.Util.LogPriority.Error, "", message); +#endif + Debug.WriteLine(message); + + if (m_configuration.IsMessageTypeEnabled(NetIncomingMessageType.ErrorMessage)) + ReleaseMessage(CreateIncomingMessage(NetIncomingMessageType.ErrorMessage, message)); + } + } +} diff --git a/Lidgren.Network/NetPeer.MessagePools.cs b/Lidgren.Network/NetPeer.MessagePools.cs new file mode 100644 index 000000000..a104d6638 --- /dev/null +++ b/Lidgren.Network/NetPeer.MessagePools.cs @@ -0,0 +1,228 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Lidgren.Network +{ + public partial class NetPeer + { + private List m_storagePool; // sorted smallest to largest + private NetQueue m_outgoingMessagesPool; + private NetQueue m_incomingMessagesPool; + + internal int m_storagePoolBytes; + + private void InitializePools() + { + if (m_configuration.UseMessageRecycling) + { + m_storagePool = new List(16); + m_outgoingMessagesPool = new NetQueue(4); + m_incomingMessagesPool = new NetQueue(4); + } + else + { + m_storagePool = null; + m_outgoingMessagesPool = null; + m_incomingMessagesPool = null; + } + } + + internal byte[] GetStorage(int minimumCapacityInBytes) + { + if (m_storagePool == null) + return new byte[minimumCapacityInBytes]; + + lock (m_storagePool) + { + for (int i = 0; i < m_storagePool.Count; i++) + { + byte[] retval = m_storagePool[i]; + if (retval != null && retval.Length >= minimumCapacityInBytes) + { + m_storagePool[i] = null; + m_storagePoolBytes -= retval.Length; + return retval; + } + } + } + m_statistics.m_bytesAllocated += minimumCapacityInBytes; + return new byte[minimumCapacityInBytes]; + } + + internal void Recycle(byte[] storage) + { + if (m_storagePool == null) + return; + + lock (m_storagePool) + { + m_storagePoolBytes += storage.Length; + int cnt = m_storagePool.Count; + for (int i = 0; i < cnt; i++) + { + if (m_storagePool[i] == null) + { + m_storagePool[i] = storage; + return; + } + } + m_storagePool.Add(storage); + } + } + + /// + /// Creates a new message for sending + /// + public NetOutgoingMessage CreateMessage() + { + return CreateMessage(m_configuration.m_defaultOutgoingMessageCapacity); + } + + /// + /// Creates a new message for sending and writes the provided string to it + /// + public NetOutgoingMessage CreateMessage(string content) + { + byte[] bytes = Encoding.UTF8.GetBytes(content); + NetOutgoingMessage om = CreateMessage(2 + bytes.Length); + om.WriteVariableUInt32((uint)bytes.Length); + om.Write(bytes); + return om; + } + + /// + /// Creates a new message for sending + /// + /// initial capacity in bytes + public NetOutgoingMessage CreateMessage(int initialCapacity) + { + NetOutgoingMessage retval; + if (m_outgoingMessagesPool == null || !m_outgoingMessagesPool.TryDequeue(out retval)) + retval = new NetOutgoingMessage(); + + byte[] storage = GetStorage(initialCapacity); + retval.m_data = storage; + + return retval; + } + + internal NetIncomingMessage CreateIncomingMessage(NetIncomingMessageType tp, byte[] useStorageData) + { + NetIncomingMessage retval; + if (m_incomingMessagesPool == null || !m_incomingMessagesPool.TryDequeue(out retval)) + retval = new NetIncomingMessage(tp); + else + retval.m_incomingMessageType = tp; + retval.m_data = useStorageData; + return retval; + } + + internal NetIncomingMessage CreateIncomingMessage(NetIncomingMessageType tp, int minimumByteSize) + { + NetIncomingMessage retval; + if (m_incomingMessagesPool == null || !m_incomingMessagesPool.TryDequeue(out retval)) + retval = new NetIncomingMessage(tp); + else + retval.m_incomingMessageType = tp; + retval.m_data = GetStorage(minimumByteSize); + return retval; + } + + /// + /// Recycles a NetIncomingMessage instance for reuse; taking pressure off the garbage collector + /// + public void Recycle(NetIncomingMessage msg) + { + if (m_incomingMessagesPool == null) + return; +#if DEBUG + if (m_incomingMessagesPool.Contains(msg)) + throw new NetException("Recyling already recycled message! Thread race?"); +#endif + byte[] storage = msg.m_data; + msg.m_data = null; + Recycle(storage); + msg.Reset(); + m_incomingMessagesPool.Enqueue(msg); + } + + /// + /// Recycles a list of NetIncomingMessage instances for reuse; taking pressure off the garbage collector + /// + public void Recycle(IEnumerable toRecycle) + { + if (m_incomingMessagesPool == null) + return; + + // first recycle the storage of each message + if (m_storagePool != null) + { + lock (m_storagePool) + { + foreach (var msg in toRecycle) + { + var storage = msg.m_data; + msg.m_data = null; + m_storagePoolBytes += storage.Length; + int cnt = m_storagePool.Count; + for (int i = 0; i < cnt; i++) + { + if (m_storagePool[i] == null) + { + m_storagePool[i] = storage; + return; + } + } + msg.Reset(); + m_storagePool.Add(storage); + } + } + } + + // then recycle the message objects + m_incomingMessagesPool.Enqueue(toRecycle); + } + + internal void Recycle(NetOutgoingMessage msg) + { + if (m_outgoingMessagesPool == null) + return; +#if DEBUG + if (m_outgoingMessagesPool.Contains(msg)) + throw new NetException("Recyling already recycled message! Thread race?"); +#endif + + byte[] storage = msg.m_data; + msg.m_data = null; + + // message fragments cannot be recycled + // TODO: find a way to recycle large message after all fragments has been acknowledged; or? possibly better just to garbage collect them + if (msg.m_fragmentGroup == 0) + Recycle(storage); + + msg.Reset(); + m_outgoingMessagesPool.Enqueue(msg); + } + + /// + /// Creates an incoming message with the required capacity for releasing to the application + /// + internal NetIncomingMessage CreateIncomingMessage(NetIncomingMessageType tp, string text) + { + NetIncomingMessage retval; + if (string.IsNullOrEmpty(text)) + { + retval = CreateIncomingMessage(tp, 1); + retval.Write(string.Empty); + return retval; + } + + int numBytes = System.Text.Encoding.UTF8.GetByteCount(text); + retval = CreateIncomingMessage(tp, numBytes + (numBytes > 127 ? 2 : 1)); + retval.Write(text); + + return retval; + } + } +} diff --git a/Lidgren.Network/NetPeer.Send.cs b/Lidgren.Network/NetPeer.Send.cs new file mode 100644 index 000000000..9cfe4ca82 --- /dev/null +++ b/Lidgren.Network/NetPeer.Send.cs @@ -0,0 +1,227 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Net; + +namespace Lidgren.Network +{ + public partial class NetPeer + { + /// + /// Send a message to a specific connection + /// + /// The message to send + /// The recipient connection + /// How to deliver the message + public NetSendResult SendMessage(NetOutgoingMessage msg, NetConnection recipient, NetDeliveryMethod method) + { + return SendMessage(msg, recipient, method, 0); + } + + /// + /// Send a message to a specific connection + /// + /// The message to send + /// The recipient connection + /// How to deliver the message + /// Sequence channel within the delivery method + public NetSendResult SendMessage(NetOutgoingMessage msg, NetConnection recipient, NetDeliveryMethod method, int sequenceChannel) + { + if (msg == null) + throw new ArgumentNullException("msg"); + if (recipient == null) + throw new ArgumentNullException("recipient"); + if (sequenceChannel >= NetConstants.NetChannelsPerDeliveryMethod) + throw new ArgumentOutOfRangeException("sequenceChannel"); + + NetException.Assert( + ((method != NetDeliveryMethod.Unreliable && method != NetDeliveryMethod.ReliableUnordered) || + ((method == NetDeliveryMethod.Unreliable || method == NetDeliveryMethod.ReliableUnordered) && sequenceChannel == 0)), + "Delivery method " + method + " cannot use sequence channels other than 0!" + ); + + NetException.Assert(method != NetDeliveryMethod.Unknown, "Bad delivery method!"); + + if (msg.m_isSent) + throw new NetException("This message has already been sent! Use NetPeer.SendMessage() to send to multiple recipients efficiently"); + msg.m_isSent = true; + + int len = NetConstants.UnfragmentedMessageHeaderSize + msg.LengthBytes; // headers + length, faster than calling msg.GetEncodedSize + if (len <= recipient.m_currentMTU) + { + Interlocked.Increment(ref msg.m_recyclingCount); + return recipient.EnqueueMessage(msg, method, sequenceChannel); + } + else + { + // message must be fragmented! + if (recipient.m_status != NetConnectionStatus.Connected) + return NetSendResult.FailedNotConnected; + SendFragmentedMessage(msg, new NetConnection[] { recipient }, method, sequenceChannel); + return NetSendResult.Queued; // could be different for each connection; Queued is "most true" + } + } + + internal static int GetMTU(IList recipients) + { + int count = recipients.Count; + NetException.Assert(count > 0); + + int mtu = int.MaxValue; + for(int i=0;i + /// Send a message to a list of connections + /// + /// The message to send + /// The list of recipients to send to + /// How to deliver the message + /// Sequence channel within the delivery method + public void SendMessage(NetOutgoingMessage msg, List recipients, NetDeliveryMethod method, int sequenceChannel) + { + if (msg == null) + throw new ArgumentNullException("msg"); + if (recipients == null) + throw new ArgumentNullException("recipients"); + if (recipients.Count < 1) + throw new NetException("recipients must contain at least one item"); + if (method == NetDeliveryMethod.Unreliable || method == NetDeliveryMethod.ReliableUnordered) + NetException.Assert(sequenceChannel == 0, "Delivery method " + method + " cannot use sequence channels other than 0!"); + if (msg.m_isSent) + throw new NetException("This message has already been sent! Use NetPeer.SendMessage() to send to multiple recipients efficiently"); + + int mtu = GetMTU(recipients); + + msg.m_isSent = true; + + int len = msg.GetEncodedSize(); + if (len <= mtu) + { + Interlocked.Add(ref msg.m_recyclingCount, recipients.Count); + foreach (NetConnection conn in recipients) + { + if (conn == null) + { + Interlocked.Decrement(ref msg.m_recyclingCount); + continue; + } + NetSendResult res = conn.EnqueueMessage(msg, method, sequenceChannel); + if (res != NetSendResult.Queued && res != NetSendResult.Sent) + Interlocked.Decrement(ref msg.m_recyclingCount); + } + } + else + { + // message must be fragmented! + SendFragmentedMessage(msg, recipients, method, sequenceChannel); + } + + return; + } + + /// + /// Send a message to an unconnected host + /// + public void SendUnconnectedMessage(NetOutgoingMessage msg, string host, int port) + { + if (msg == null) + throw new ArgumentNullException("msg"); + if (host == null) + throw new ArgumentNullException("host"); + if (msg.m_isSent) + throw new NetException("This message has already been sent! Use NetPeer.SendMessage() to send to multiple recipients efficiently"); + if (msg.LengthBytes > m_configuration.MaximumTransmissionUnit) + throw new NetException("Unconnected messages too long! Must be shorter than NetConfiguration.MaximumTransmissionUnit (currently " + m_configuration.MaximumTransmissionUnit + ")"); + + IPAddress adr = NetUtility.Resolve(host); + if (adr == null) + throw new NetException("Failed to resolve " + host); + + msg.m_messageType = NetMessageType.Unconnected; + msg.m_isSent = true; + + Interlocked.Increment(ref msg.m_recyclingCount); + m_unsentUnconnectedMessages.Enqueue(new NetTuple(new IPEndPoint(adr, port), msg)); + } + + /// + /// Send a message to an unconnected host + /// + public void SendUnconnectedMessage(NetOutgoingMessage msg, IPEndPoint recipient) + { + if (msg == null) + throw new ArgumentNullException("msg"); + if (recipient == null) + throw new ArgumentNullException("recipient"); + if (msg.m_isSent) + throw new NetException("This message has already been sent! Use NetPeer.SendMessage() to send to multiple recipients efficiently"); + if (msg.LengthBytes > m_configuration.MaximumTransmissionUnit) + throw new NetException("Unconnected messages too long! Must be shorter than NetConfiguration.MaximumTransmissionUnit (currently " + m_configuration.MaximumTransmissionUnit + ")"); + + msg.m_messageType = NetMessageType.Unconnected; + msg.m_isSent = true; + + Interlocked.Increment(ref msg.m_recyclingCount); + m_unsentUnconnectedMessages.Enqueue(new NetTuple(recipient, msg)); + } + + /// + /// Send a message to an unconnected host + /// + public void SendUnconnectedMessage(NetOutgoingMessage msg, IList recipients) + { + if (msg == null) + throw new ArgumentNullException("msg"); + if (recipients == null) + throw new ArgumentNullException("recipients"); + if (recipients.Count < 1) + throw new NetException("recipients must contain at least one item"); + if (msg.m_isSent) + throw new NetException("This message has already been sent! Use NetPeer.SendMessage() to send to multiple recipients efficiently"); + if (msg.LengthBytes > m_configuration.MaximumTransmissionUnit) + throw new NetException("Unconnected messages too long! Must be shorter than NetConfiguration.MaximumTransmissionUnit (currently " + m_configuration.MaximumTransmissionUnit + ")"); + + msg.m_messageType = NetMessageType.Unconnected; + msg.m_isSent = true; + + Interlocked.Add(ref msg.m_recyclingCount, recipients.Count); + foreach(IPEndPoint ep in recipients) + m_unsentUnconnectedMessages.Enqueue(new NetTuple(ep, msg)); + } + + /// + /// Send a message to this exact same netpeer (loopback) + /// + public void SendUnconnectedToSelf(NetOutgoingMessage msg) + { + if (msg == null) + throw new ArgumentNullException("msg"); + if (msg.m_isSent) + throw new NetException("This message has already been sent! Use NetPeer.SendMessage() to send to multiple recipients efficiently"); + + msg.m_messageType = NetMessageType.Unconnected; + msg.m_isSent = true; + + if (m_configuration.IsMessageTypeEnabled(NetIncomingMessageType.UnconnectedData) == false) + return; // dropping unconnected message since it's not enabled for receiving + + NetIncomingMessage om = CreateIncomingMessage(NetIncomingMessageType.UnconnectedData, msg.LengthBytes); + om.Write(msg); + om.m_isFragment = false; + om.m_receiveTime = NetTime.Now; + om.m_senderConnection = null; + om.m_senderEndPoint = m_socket.LocalEndPoint as IPEndPoint; + NetException.Assert(om.m_bitLength == msg.LengthBits); + + ReleaseMessage(om); + } + } +} diff --git a/Lidgren.Network/NetPeer.cs b/Lidgren.Network/NetPeer.cs new file mode 100644 index 000000000..4293f92f8 --- /dev/null +++ b/Lidgren.Network/NetPeer.cs @@ -0,0 +1,337 @@ +using System; +using System.Threading; +using System.Collections.Generic; +using System.Net; + +namespace Lidgren.Network +{ + /// + /// Represents a local peer capable of holding zero, one or more connections to remote peers + /// + public partial class NetPeer + { + private static int s_initializedPeersCount; + + private int m_listenPort; + private object m_tag; + + internal readonly List m_connections; + private readonly Dictionary m_connectionLookup; + + private string m_shutdownReason; + + /// + /// Gets the NetPeerStatus of the NetPeer + /// + public NetPeerStatus Status { get { return m_status; } } + + /// + /// Signalling event which can be waited on to determine when a message is queued for reading. + /// Note that there is no guarantee that after the event is signaled the blocked thread will + /// find the message in the queue. Other user created threads could be preempted and dequeue + /// the message before the waiting thread wakes up. + /// + public AutoResetEvent MessageReceivedEvent + { + get + { + if (m_messageReceivedEvent == null) + m_messageReceivedEvent = new AutoResetEvent(false); + return m_messageReceivedEvent; + } + } + + /// + /// Gets a unique identifier for this NetPeer based on Mac address and ip/port. Note! Not available until Start() has been called! + /// + public long UniqueIdentifier { get { return m_uniqueIdentifier; } } + + /// + /// Gets the port number this NetPeer is listening and sending on, if Start() has been called + /// + public int Port { get { return m_listenPort; } } + + /// + /// Returns an UPnP object if enabled in the NetPeerConfiguration + /// + public NetUPnP UPnP { get { return m_upnp; } } + + /// + /// Gets or sets the application defined object containing data about the peer + /// + public object Tag + { + get { return m_tag; } + set { m_tag = value; } + } + + /// + /// Gets a copy of the list of connections + /// + public List Connections + { + get + { + lock (m_connections) + return new List(m_connections); + } + } + + /// + /// Gets the number of active connections + /// + public int ConnectionsCount + { + get { return m_connections.Count; } + } + + /// + /// Statistics on this NetPeer since it was initialized + /// + public NetPeerStatistics Statistics + { + get { return m_statistics; } + } + + /// + /// Gets the configuration used to instanciate this NetPeer + /// + public NetPeerConfiguration Configuration { get { return m_configuration; } } + + /// + /// NetPeer constructor + /// + public NetPeer(NetPeerConfiguration config) + { + m_configuration = config; + m_statistics = new NetPeerStatistics(this); + m_releasedIncomingMessages = new NetQueue(4); + m_unsentUnconnectedMessages = new NetQueue>(2); + m_connections = new List(); + m_connectionLookup = new Dictionary(); + m_handshakes = new Dictionary(); + m_senderRemote = (EndPoint)new IPEndPoint(IPAddress.Any, 0); + m_status = NetPeerStatus.NotRunning; + m_receivedFragmentGroups = new Dictionary>(); + } + + /// + /// Binds to socket and spawns the networking thread + /// + public void Start() + { + if (m_status != NetPeerStatus.NotRunning) + { + // already running! Just ignore... + LogWarning("Start() called on already running NetPeer - ignoring."); + return; + } + + m_status = NetPeerStatus.Starting; + + // fix network thread name + if (m_configuration.NetworkThreadName == "Lidgren network thread") + { + int pc = Interlocked.Increment(ref s_initializedPeersCount); + m_configuration.NetworkThreadName = "Lidgren network thread " + pc.ToString(); + } + + InitializeNetwork(); + + // start network thread + m_networkThread = new Thread(new ThreadStart(NetworkLoop)); + m_networkThread.Name = m_configuration.NetworkThreadName; + m_networkThread.IsBackground = true; + m_networkThread.Start(); + + // send upnp discovery + if (m_upnp != null) + m_upnp.Discover(this); + + // allow some time for network thread to start up in case they call Connect() or UPnP calls immediately + Thread.Sleep(50); + } + + /// + /// Get the connection, if any, for a certain remote endpoint + /// + public NetConnection GetConnection(IPEndPoint ep) + { + NetConnection retval; + + // this should not pose a threading problem, m_connectionLookup is never added to concurrently + // and TryGetValue will not throw an exception on fail, only yield null, which is acceptable + m_connectionLookup.TryGetValue(ep, out retval); + + return retval; + } + + /// + /// Read a pending message from any connection, blocking up to maxMillis if needed + /// + public NetIncomingMessage WaitMessage(int maxMillis) + { + var msg = ReadMessage(); + if (msg != null) + return msg; // no need to wait; we already have a message to deliver + if (m_messageReceivedEvent != null) + m_messageReceivedEvent.WaitOne(maxMillis); + return ReadMessage(); + } + + /// + /// Read a pending message from any connection, if any + /// + public NetIncomingMessage ReadMessage() + { + NetIncomingMessage retval; + if (m_releasedIncomingMessages.TryDequeue(out retval)) + { + if (retval.MessageType == NetIncomingMessageType.StatusChanged) + { + NetConnectionStatus status = (NetConnectionStatus)retval.PeekByte(); + retval.SenderConnection.m_visibleStatus = status; + } + } + return retval; + } + + /// + /// Read a pending message from any connection, if any + /// + public int ReadMessages(IList addTo) + { + int added = m_releasedIncomingMessages.TryDrain(addTo); + if (added > 0) + { + for (int i = 0; i < added; i++) + { + var index = addTo.Count - added + i; + var nim = addTo[index]; + if (nim.MessageType == NetIncomingMessageType.StatusChanged) + { + NetConnectionStatus status = (NetConnectionStatus)nim.PeekByte(); + nim.SenderConnection.m_visibleStatus = status; + } + } + } + return added; + } + + // send message immediately + internal void SendLibrary(NetOutgoingMessage msg, IPEndPoint recipient) + { + VerifyNetworkThread(); + NetException.Assert(msg.m_isSent == false); + + bool connReset; + int len = msg.Encode(m_sendBuffer, 0, 0); + SendPacket(len, recipient, 1, out connReset); + } + + /// + /// Create a connection to a remote endpoint + /// + public NetConnection Connect(string host, int port) + { + return Connect(new IPEndPoint(NetUtility.Resolve(host), port), null); + } + + /// + /// Create a connection to a remote endpoint + /// + public NetConnection Connect(string host, int port, NetOutgoingMessage hailMessage) + { + return Connect(new IPEndPoint(NetUtility.Resolve(host), port), hailMessage); + } + + /// + /// Create a connection to a remote endpoint + /// + public NetConnection Connect(IPEndPoint remoteEndPoint) + { + return Connect(remoteEndPoint, null); + } + + /// + /// Create a connection to a remote endpoint + /// + public virtual NetConnection Connect(IPEndPoint remoteEndPoint, NetOutgoingMessage hailMessage) + { + if (remoteEndPoint == null) + throw new ArgumentNullException("remoteEndPoint"); + + lock (m_connections) + { + if (m_status == NetPeerStatus.NotRunning) + throw new NetException("Must call Start() first"); + + if (m_connectionLookup.ContainsKey(remoteEndPoint)) + throw new NetException("Already connected to that endpoint!"); + + NetConnection hs; + if (m_handshakes.TryGetValue(remoteEndPoint, out hs)) + { + // already trying to connect to that endpoint; make another try + switch (hs.m_status) + { + case NetConnectionStatus.InitiatedConnect: + // send another connect + hs.m_connectRequested = true; + break; + case NetConnectionStatus.RespondedConnect: + // send another response + hs.SendConnectResponse((float)NetTime.Now, false); + break; + default: + // weird + LogWarning("Weird situation; Connect() already in progress to remote endpoint; but hs status is " + hs.m_status); + break; + } + return hs; + } + + NetConnection conn = new NetConnection(this, remoteEndPoint); + conn.m_status = NetConnectionStatus.InitiatedConnect; + conn.m_localHailMessage = hailMessage; + + // handle on network thread + conn.m_connectRequested = true; + conn.m_connectionInitiator = true; + + m_handshakes.Add(remoteEndPoint, conn); + + return conn; + } + } + + /// + /// Send raw bytes; only used for debugging + /// +#if DEBUG + public void RawSend(byte[] arr, int offset, int length, IPEndPoint destination) +#else + internal void RawSend(byte[] arr, int offset, int length, IPEndPoint destination) +#endif + { + // wrong thread - this miiiight crash with network thread... but what's a boy to do. + Array.Copy(arr, offset, m_sendBuffer, 0, length); + bool unused; + SendPacket(length, destination, 1, out unused); + } + + /// + /// Disconnects all active connections and closes the socket + /// + public void Shutdown(string bye) + { + // called on user thread + if (m_socket == null) + return; // already shut down + + LogDebug("Shutdown requested"); + m_shutdownReason = bye; + m_status = NetPeerStatus.ShutdownRequested; + } + } +} diff --git a/Lidgren.Network/NetPeerConfiguration.cs b/Lidgren.Network/NetPeerConfiguration.cs new file mode 100644 index 000000000..fb0cab121 --- /dev/null +++ b/Lidgren.Network/NetPeerConfiguration.cs @@ -0,0 +1,471 @@ +/* Copyright (c) 2010 Michael Lidgren + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom +the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ +using System; +using System.Net; + +namespace Lidgren.Network +{ + /// + /// Partly immutable after NetPeer has been initialized + /// + public sealed class NetPeerConfiguration + { + private const string c_isLockedMessage = "You may not modify the NetPeerConfiguration after it has been used to initialize a NetPeer"; + + private bool m_isLocked; + private readonly string m_appIdentifier; + private string m_networkThreadName; + private IPAddress m_localAddress; + private IPAddress m_broadcastAddress; + internal bool m_acceptIncomingConnections; + internal int m_maximumConnections; + internal int m_defaultOutgoingMessageCapacity; + internal float m_pingInterval; + internal bool m_useMessageRecycling; + internal float m_connectionTimeout; + internal bool m_enableUPnP; + internal bool m_autoFlushSendQueue; + + internal NetIncomingMessageType m_disabledTypes; + internal int m_port; + internal int m_receiveBufferSize; + internal int m_sendBufferSize; + internal float m_resendHandshakeInterval; + internal int m_maximumHandshakeAttempts; + + // bad network simulation + internal float m_loss; + internal float m_duplicates; + internal float m_minimumOneWayLatency; + internal float m_randomOneWayLatency; + + // MTU + internal int m_maximumTransmissionUnit; + internal bool m_autoExpandMTU; + internal float m_expandMTUFrequency; + internal int m_expandMTUFailAttempts; + + /// + /// NetPeerConfiguration constructor + /// + public NetPeerConfiguration(string appIdentifier) + { + if (string.IsNullOrEmpty(appIdentifier)) + throw new NetException("App identifier must be at least one character long"); + m_appIdentifier = appIdentifier.ToString(System.Globalization.CultureInfo.InvariantCulture); + + // + // default values + // + m_disabledTypes = NetIncomingMessageType.ConnectionApproval | NetIncomingMessageType.UnconnectedData | NetIncomingMessageType.VerboseDebugMessage | NetIncomingMessageType.ConnectionLatencyUpdated; + m_networkThreadName = "Lidgren network thread"; + m_localAddress = IPAddress.Any; + m_broadcastAddress = IPAddress.Broadcast; + var ip = NetUtility.GetBroadcastAddress(); + if (ip != null) + { + m_broadcastAddress = ip; + } + m_port = 0; + m_receiveBufferSize = 131071; + m_sendBufferSize = 131071; + m_acceptIncomingConnections = false; + m_maximumConnections = 32; + m_defaultOutgoingMessageCapacity = 16; + m_pingInterval = 4.0f; + m_connectionTimeout = 25.0f; + m_useMessageRecycling = true; + m_resendHandshakeInterval = 3.0f; + m_maximumHandshakeAttempts = 5; + m_autoFlushSendQueue = true; + + // Maximum transmission unit + // Ethernet can take 1500 bytes of payload, so lets stay below that. + // The aim is for a max full packet to be 1440 bytes (30 x 48 bytes, lower than 1468) + // -20 bytes IP header + // -8 bytes UDP header + // -4 bytes to be on the safe side and align to 8-byte boundary + // Total 1408 bytes + // Note that lidgren headers (5 bytes) are not included here; since it's part of the "mtu payload" + m_maximumTransmissionUnit = 1408; + m_autoExpandMTU = false; + m_expandMTUFrequency = 2.0f; + m_expandMTUFailAttempts = 5; + + m_loss = 0.0f; + m_minimumOneWayLatency = 0.0f; + m_randomOneWayLatency = 0.0f; + m_duplicates = 0.0f; + + m_isLocked = false; + } + + internal void Lock() + { + m_isLocked = true; + } + + /// + /// Gets the identifier of this application; the library can only connect to matching app identifier peers + /// + public string AppIdentifier + { + get { return m_appIdentifier; } + } + + /// + /// Enables receiving of the specified type of message + /// + public void EnableMessageType(NetIncomingMessageType type) + { + m_disabledTypes &= (~type); + } + + /// + /// Disables receiving of the specified type of message + /// + public void DisableMessageType(NetIncomingMessageType type) + { + m_disabledTypes |= type; + } + + /// + /// Enables or disables receiving of the specified type of message + /// + public void SetMessageTypeEnabled(NetIncomingMessageType type, bool enabled) + { + if (enabled) + m_disabledTypes &= (~type); + else + m_disabledTypes |= type; + } + + /// + /// Gets if receiving of the specified type of message is enabled + /// + public bool IsMessageTypeEnabled(NetIncomingMessageType type) + { + return !((m_disabledTypes & type) == type); + } + + /// + /// Gets or sets the name of the library network thread. Cannot be changed once NetPeer is initialized. + /// + public string NetworkThreadName + { + get { return m_networkThreadName; } + set + { + if (m_isLocked) + throw new NetException("NetworkThreadName may not be set after the NetPeer which uses the configuration has been started"); + m_networkThreadName = value; + } + } + + /// + /// Gets or sets the maximum amount of connections this peer can hold. Cannot be changed once NetPeer is initialized. + /// + public int MaximumConnections + { + get { return m_maximumConnections; } + set + { + if (m_isLocked) + throw new NetException(c_isLockedMessage); + m_maximumConnections = value; + } + } + + /// + /// Gets or sets the maximum amount of bytes to send in a single packet, excluding ip, udp and lidgren headers. Cannot be changed once NetPeer is initialized. + /// + public int MaximumTransmissionUnit + { + get { return m_maximumTransmissionUnit; } + set + { + if (m_isLocked) + throw new NetException(c_isLockedMessage); + if (value < 1 || value >= ((ushort.MaxValue + 1) / 8)) + throw new NetException("MaximumTransmissionUnit must be between 1 and " + (((ushort.MaxValue + 1) / 8) - 1) + " bytes"); + m_maximumTransmissionUnit = value; + } + } + + /// + /// Gets or sets the default capacity in bytes when NetPeer.CreateMessage() is called without argument + /// + public int DefaultOutgoingMessageCapacity + { + get { return m_defaultOutgoingMessageCapacity; } + set { m_defaultOutgoingMessageCapacity = value; } + } + + /// + /// Gets or sets the time between latency calculating pings + /// + public float PingInterval + { + get { return m_pingInterval; } + set { m_pingInterval = value; } + } + + /// + /// Gets or sets if the library should recycling messages to avoid excessive garbage collection. Cannot be changed once NetPeer is initialized. + /// + public bool UseMessageRecycling + { + get { return m_useMessageRecycling; } + set + { + if (m_isLocked) + throw new NetException(c_isLockedMessage); + m_useMessageRecycling = value; + } + } + + /// + /// Gets or sets the number of seconds timeout will be postponed on a successful ping/pong + /// + public float ConnectionTimeout + { + get { return m_connectionTimeout; } + set + { + if (value < m_pingInterval) + throw new NetException("Connection timeout cannot be lower than ping interval!"); + m_connectionTimeout = value; + } + } + + /// + /// Enables UPnP support; enabling port forwarding and getting external ip + /// + public bool EnableUPnP + { + get { return m_enableUPnP; } + set + { + if (m_isLocked) + throw new NetException(c_isLockedMessage); + m_enableUPnP = value; + } + } + + /// + /// Enables or disables automatic flushing of the send queue. If disabled, you must manully call NetPeer.FlushSendQueue() to flush sent messages to network. + /// + public bool AutoFlushSendQueue + { + get { return m_autoFlushSendQueue; } + set { m_autoFlushSendQueue = value; } + } + + /// + /// Gets or sets the local ip address to bind to. Defaults to IPAddress.Any. Cannot be changed once NetPeer is initialized. + /// + public IPAddress LocalAddress + { + get { return m_localAddress; } + set + { + if (m_isLocked) + throw new NetException(c_isLockedMessage); + m_localAddress = value; + } + } + + /// + /// Gets or sets the local broadcast address to use when broadcasting + /// + public IPAddress BroadcastAddress + { + get { return m_broadcastAddress; } + set + { + if (m_isLocked) + throw new NetException(c_isLockedMessage); + m_broadcastAddress = value; + } + } + + /// + /// Gets or sets the local port to bind to. Defaults to 0. Cannot be changed once NetPeer is initialized. + /// + public int Port + { + get { return m_port; } + set + { + if (m_isLocked) + throw new NetException(c_isLockedMessage); + m_port = value; + } + } + + /// + /// Gets or sets the size in bytes of the receiving buffer. Defaults to 131071 bytes. Cannot be changed once NetPeer is initialized. + /// + public int ReceiveBufferSize + { + get { return m_receiveBufferSize; } + set + { + if (m_isLocked) + throw new NetException(c_isLockedMessage); + m_receiveBufferSize = value; + } + } + + /// + /// Gets or sets the size in bytes of the sending buffer. Defaults to 131071 bytes. Cannot be changed once NetPeer is initialized. + /// + public int SendBufferSize + { + get { return m_sendBufferSize; } + set + { + if (m_isLocked) + throw new NetException(c_isLockedMessage); + m_sendBufferSize = value; + } + } + + /// + /// Gets or sets if the NetPeer should accept incoming connections. This is automatically set to true in NetServer and false in NetClient. + /// + public bool AcceptIncomingConnections + { + get { return m_acceptIncomingConnections; } + set { m_acceptIncomingConnections = value; } + } + + /// + /// Gets or sets the number of seconds between handshake attempts + /// + public float ResendHandshakeInterval + { + get { return m_resendHandshakeInterval; } + set { m_resendHandshakeInterval = value; } + } + + /// + /// Gets or sets the maximum number of handshake attempts before failing to connect + /// + public int MaximumHandshakeAttempts + { + get { return m_maximumHandshakeAttempts; } + set + { + if (value < 1) + throw new NetException("MaximumHandshakeAttempts must be at least 1"); + m_maximumHandshakeAttempts = value; + } + } + + /// + /// Gets or sets if the NetPeer should send large messages to try to expand the maximum transmission unit size + /// + public bool AutoExpandMTU + { + get { return m_autoExpandMTU; } + set + { + if (m_isLocked) + throw new NetException(c_isLockedMessage); + m_autoExpandMTU = value; + } + } + + /// + /// Gets or sets how often to send large messages to expand MTU if AutoExpandMTU is enabled + /// + public float ExpandMTUFrequency + { + get { return m_expandMTUFrequency; } + set { m_expandMTUFrequency = value; } + } + + /// + /// Gets or sets the number of failed expand mtu attempts to perform before setting final MTU + /// + public int ExpandMTUFailAttempts + { + get { return m_expandMTUFailAttempts; } + set { m_expandMTUFailAttempts = value; } + } + +#if DEBUG + /// + /// Gets or sets the simulated amount of sent packets lost from 0.0f to 1.0f + /// + public float SimulatedLoss + { + get { return m_loss; } + set { m_loss = value; } + } + + /// + /// Gets or sets the minimum simulated amount of one way latency for sent packets in seconds + /// + public float SimulatedMinimumLatency + { + get { return m_minimumOneWayLatency; } + set { m_minimumOneWayLatency = value; } + } + + /// + /// Gets or sets the simulated added random amount of one way latency for sent packets in seconds + /// + public float SimulatedRandomLatency + { + get { return m_randomOneWayLatency; } + set { m_randomOneWayLatency = value; } + } + + /// + /// Gets the average simulated one way latency in seconds + /// + public float SimulatedAverageLatency + { + get { return m_minimumOneWayLatency + (m_randomOneWayLatency * 0.5f); } + } + + /// + /// Gets or sets the simulated amount of duplicated packets from 0.0f to 1.0f + /// + public float SimulatedDuplicatesChance + { + get { return m_duplicates; } + set { m_duplicates = value; } + } +#endif + + /// + /// Creates a memberwise shallow clone of this configuration + /// + public NetPeerConfiguration Clone() + { + NetPeerConfiguration retval = this.MemberwiseClone() as NetPeerConfiguration; + retval.m_isLocked = false; + return retval; + } + } +} diff --git a/Lidgren.Network/NetPeerStatistics.cs b/Lidgren.Network/NetPeerStatistics.cs new file mode 100644 index 000000000..14790c388 --- /dev/null +++ b/Lidgren.Network/NetPeerStatistics.cs @@ -0,0 +1,161 @@ +/* Copyright (c) 2010 Michael Lidgren + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom +the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + +// Uncomment the line below to get statistics in RELEASE builds +//#define USE_RELEASE_STATISTICS + +using System; +using System.Text; +using System.Diagnostics; + +namespace Lidgren.Network +{ + /// + /// Statistics for a NetPeer instance + /// + public sealed class NetPeerStatistics + { + private readonly NetPeer m_peer; + + internal int m_sentPackets; + internal int m_receivedPackets; + + internal int m_sentMessages; + internal int m_receivedMessages; + + internal int m_sentBytes; + internal int m_receivedBytes; + + internal long m_bytesAllocated; + + internal NetPeerStatistics(NetPeer peer) + { + m_peer = peer; + Reset(); + } + + internal void Reset() + { + m_sentPackets = 0; + m_receivedPackets = 0; + + m_sentMessages = 0; + m_receivedMessages = 0; + + m_sentBytes = 0; + m_receivedBytes = 0; + + m_bytesAllocated = 0; + } + + /// + /// Gets the number of sent packets since the NetPeer was initialized + /// + public int SentPackets { get { return m_sentPackets; } } + + /// + /// Gets the number of received packets since the NetPeer was initialized + /// + public int ReceivedPackets { get { return m_receivedPackets; } } + + /// + /// Gets the number of sent messages since the NetPeer was initialized + /// + public int SentMessages { get { return m_sentMessages; } } + + /// + /// Gets the number of received messages since the NetPeer was initialized + /// + public int ReceivedMessages { get { return m_receivedMessages; } } + + /// + /// Gets the number of sent bytes since the NetPeer was initialized + /// + public int SentBytes { get { return m_sentBytes; } } + + /// + /// Gets the number of received bytes since the NetPeer was initialized + /// + public int ReceivedBytes { get { return m_receivedBytes; } } + + /// + /// Gets the number of bytes allocated (and possibly garbage collected) for message storage + /// + public long StorageBytesAllocated { get { return m_bytesAllocated; } } + + /// + /// Gets the number of bytes in the recycled pool + /// + public int BytesInRecyclePool { get { return m_peer.m_storagePoolBytes; } } + +#if USE_RELEASE_STATISTICS + internal void PacketSent(int numBytes, int numMessages) + { + m_sentPackets++; + m_sentBytes += numBytes; + m_sentMessages += numMessages; + } +#else + [Conditional("DEBUG")] + internal void PacketSent(int numBytes, int numMessages) + { + m_sentPackets++; + m_sentBytes += numBytes; + m_sentMessages += numMessages; + } +#endif + +#if USE_RELEASE_STATISTICS + internal void PacketReceived(int numBytes, int numMessages) + { + m_receivedPackets++; + m_receivedBytes += numBytes; + m_receivedMessages += numMessages; + } +#else + [Conditional("DEBUG")] + internal void PacketReceived(int numBytes, int numMessages) + { + m_receivedPackets++; + m_receivedBytes += numBytes; + m_receivedMessages += numMessages; + } +#endif + + /// + /// Returns a string that represents this object + /// + public override string ToString() + { + StringBuilder bdr = new StringBuilder(); + bdr.AppendLine(m_peer.ConnectionsCount.ToString() + " connections"); +#if DEBUG || USE_RELEASE_STATISTICS + bdr.AppendLine("Sent " + m_sentBytes + " bytes in " + m_sentMessages + " messages in " + m_sentPackets + " packets"); + bdr.AppendLine("Received " + m_receivedBytes + " bytes in " + m_receivedMessages + " messages in " + m_receivedPackets + " packets"); +#else + bdr.AppendLine("Sent (n/a) bytes in (n/a) messages in (n/a) packets"); + bdr.AppendLine("Received (n/a) bytes in (n/a) messages in (n/a) packets"); +#endif + bdr.AppendLine("Storage allocated " + m_bytesAllocated + " bytes"); + bdr.AppendLine("Recycled pool " + m_peer.m_storagePoolBytes + " bytes"); + return bdr.ToString(); + } + } +} diff --git a/Lidgren.Network/NetPeerStatus.cs b/Lidgren.Network/NetPeerStatus.cs new file mode 100644 index 000000000..3be8a1d79 --- /dev/null +++ b/Lidgren.Network/NetPeerStatus.cs @@ -0,0 +1,49 @@ +/* Copyright (c) 2010 Michael Lidgren + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom +the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ +using System; + +namespace Lidgren.Network +{ + /// + /// Status for a NetPeer instance + /// + public enum NetPeerStatus + { + /// + /// NetPeer is not running; socket is not bound + /// + NotRunning = 0, + + /// + /// NetPeer is in the process of starting up + /// + Starting = 1, + + /// + /// NetPeer is bound to socket and listening for packets + /// + Running = 2, + + /// + /// Shutdown has been requested and will be executed shortly + /// + ShutdownRequested = 3, + } +} diff --git a/Lidgren.Network/NetQueue.cs b/Lidgren.Network/NetQueue.cs new file mode 100644 index 000000000..ecf4fec39 --- /dev/null +++ b/Lidgren.Network/NetQueue.cs @@ -0,0 +1,326 @@ +/* Copyright (c) 2010 Michael Lidgren + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom +the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ +using System; +using System.Diagnostics; +using System.Collections.Generic; +using System.Threading; + +namespace Lidgren.Network +{ + /// + /// Thread safe (blocking) expanding queue with TryDequeue() and EnqueueFirst() + /// + [DebuggerDisplay("Count={Count} Capacity={Capacity}")] + public sealed class NetQueue + { + // Example: + // m_capacity = 8 + // m_size = 6 + // m_head = 4 + // + // [0] item + // [1] item (tail = ((head + size - 1) % capacity) + // [2] + // [3] + // [4] item (head) + // [5] item + // [6] item + // [7] item + // + private T[] m_items; + private readonly ReaderWriterLockSlim m_lock = new ReaderWriterLockSlim(); + private int m_size; + private int m_head; + + /// + /// Gets the number of items in the queue + /// + public int Count { get { return m_size; } } + + /// + /// Gets the current capacity for the queue + /// + public int Capacity { get { return m_items.Length; } } + + /// + /// NetQueue constructor + /// + public NetQueue(int initialCapacity) + { + m_items = new T[initialCapacity]; + } + + /// + /// Adds an item last/tail of the queue + /// + public void Enqueue(T item) + { + m_lock.EnterWriteLock(); + try + { + if (m_size == m_items.Length) + SetCapacity(m_items.Length + 8); + + int slot = (m_head + m_size) % m_items.Length; + m_items[slot] = item; + m_size++; + } + finally + { + m_lock.ExitWriteLock(); + } + } + + /// + /// Adds an item last/tail of the queue + /// + public void Enqueue(IEnumerable items) + { + m_lock.EnterWriteLock(); + try + { + foreach (var item in items) + { + if (m_size == m_items.Length) + SetCapacity(m_items.Length + 8); // @TODO move this out of loop + + int slot = (m_head + m_size) % m_items.Length; + m_items[slot] = item; + m_size++; + } + } + finally + { + m_lock.ExitWriteLock(); + } + } + + /// + /// Places an item first, at the head of the queue + /// + public void EnqueueFirst(T item) + { + m_lock.EnterWriteLock(); + try + { + if (m_size >= m_items.Length) + SetCapacity(m_items.Length + 8); + + m_head--; + if (m_head < 0) + m_head = m_items.Length - 1; + m_items[m_head] = item; + m_size++; + } + finally + { + m_lock.ExitWriteLock(); + } + } + + // must be called from within a write locked m_lock! + private void SetCapacity(int newCapacity) + { + if (m_size == 0) + { + if (m_size == 0) + { + m_items = new T[newCapacity]; + m_head = 0; + return; + } + } + + T[] newItems = new T[newCapacity]; + + if (m_head + m_size - 1 < m_items.Length) + { + Array.Copy(m_items, m_head, newItems, 0, m_size); + } + else + { + Array.Copy(m_items, m_head, newItems, 0, m_items.Length - m_head); + Array.Copy(m_items, 0, newItems, m_items.Length - m_head, (m_size - (m_items.Length - m_head))); + } + + m_items = newItems; + m_head = 0; + + } + + /// + /// Gets an item from the head of the queue, or returns default(T) if empty + /// + public bool TryDequeue(out T item) + { + if (m_size == 0) + { + item = default(T); + return false; + } + + m_lock.EnterWriteLock(); + try + { + if (m_size == 0) + { + item = default(T); + return false; + } + + item = m_items[m_head]; + m_items[m_head] = default(T); + + m_head = (m_head + 1) % m_items.Length; + m_size--; + + return true; + } + finally + { + m_lock.ExitWriteLock(); + } + } + + /// + /// Gets an item from the head of the queue, or returns default(T) if empty + /// + public int TryDrain(IList addTo) + { + if (m_size == 0) + return 0; + + m_lock.EnterWriteLock(); + try + { + int added = m_size; + while (m_size > 0) + { + var item = m_items[m_head]; + addTo.Add(item); + + m_items[m_head] = default(T); + m_head = (m_head + 1) % m_items.Length; + m_size--; + } + return added; + } + finally + { + m_lock.ExitWriteLock(); + } + } + + /// + /// Returns default(T) if queue is empty + /// + public T TryPeek(int offset) + { + if (m_size == 0) + return default(T); + + m_lock.EnterReadLock(); + try + { + if (m_size == 0) + return default(T); + return m_items[(m_head + offset) % m_items.Length]; + } + finally + { + m_lock.ExitReadLock(); + } + } + + /// + /// Determines whether an item is in the queue + /// + public bool Contains(T item) + { + m_lock.EnterReadLock(); + try + { + int ptr = m_head; + for (int i = 0; i < m_size; i++) + { + if (m_items[ptr] == null) + { + if (item == null) + return true; + } + else + { + if (m_items[ptr].Equals(item)) + return true; + } + ptr = (ptr + 1) % m_items.Length; + } + return false; + } + finally + { + m_lock.ExitReadLock(); + } + } + + /// + /// Copies the queue items to a new array + /// + public T[] ToArray() + { + m_lock.EnterReadLock(); + try + { + T[] retval = new T[m_size]; + int ptr = m_head; + for (int i = 0; i < m_size; i++) + { + retval[i] = m_items[ptr++]; + if (ptr >= m_items.Length) + ptr = 0; + } + return retval; + } + finally + { + m_lock.ExitReadLock(); + } + } + + /// + /// Removes all objects from the queue + /// + public void Clear() + { + m_lock.EnterWriteLock(); + try + { + for (int i = 0; i < m_items.Length; i++) + m_items[i] = default(T); + m_head = 0; + m_size = 0; + } + finally + { + m_lock.ExitWriteLock(); + } + } + } +} diff --git a/Lidgren.Network/NetRandom.cs b/Lidgren.Network/NetRandom.cs new file mode 100644 index 000000000..e3536284a --- /dev/null +++ b/Lidgren.Network/NetRandom.cs @@ -0,0 +1,373 @@ +using System; + +namespace Lidgren.Network +{ + /// + /// A fast random number generator for .NET + /// Colin Green, January 2005 + /// + /// September 4th 2005 + /// Added NextBytesUnsafe() - commented out by default. + /// Fixed bug in Reinitialise() - y,z and w variables were not being reset. + /// + /// Key points: + /// 1) Based on a simple and fast xor-shift pseudo random number generator (RNG) specified in: + /// Marsaglia, George. (2003). Xorshift RNGs. + /// http://www.jstatsoft.org/v08/i14/xorshift.pdf + /// + /// This particular implementation of xorshift has a period of 2^128-1. See the above paper to see + /// how this can be easily extened if you need a longer period. At the time of writing I could find no + /// information on the period of System.Random for comparison. + /// + /// 2) Faster than System.Random. Up to 8x faster, depending on which methods are called. + /// + /// 3) Direct replacement for System.Random. This class implements all of the methods that System.Random + /// does plus some additional methods. The like named methods are functionally equivalent. + /// + /// 4) Allows fast re-initialisation with a seed, unlike System.Random which accepts a seed at construction + /// time which then executes a relatively expensive initialisation routine. This provides a vast speed improvement + /// if you need to reset the pseudo-random number sequence many times, e.g. if you want to re-generate the same + /// sequence many times. An alternative might be to cache random numbers in an array, but that approach is limited + /// by memory capacity and the fact that you may also want a large number of different sequences cached. Each sequence + /// can each be represented by a single seed value (int) when using FastRandom. + /// + /// Notes. + /// A further performance improvement can be obtained by declaring local variables as static, thus avoiding + /// re-allocation of variables on each call. However care should be taken if multiple instances of + /// FastRandom are in use or if being used in a multi-threaded environment. + public class NetRandom + { + /// + /// Gets a global NetRandom instance + /// + public static readonly NetRandom Instance = new NetRandom(); + + // The +1 ensures NextDouble doesn't generate 1.0 + const double REAL_UNIT_INT = 1.0 / ((double)int.MaxValue + 1.0); + const double REAL_UNIT_UINT = 1.0 / ((double)uint.MaxValue + 1.0); + const uint Y = 842502087, Z = 3579807591, W = 273326509; + + private static int s_extraSeed = 42; + + uint x, y, z, w; + + #region Constructors + + /// + /// Initialises a new instance using time dependent seed. + /// + public NetRandom() + { + // Initialise using the system tick count. + Reinitialise(GetSeed(this)); + } + + /// + /// Initialises a new instance using an int value as seed. + /// This constructor signature is provided to maintain compatibility with + /// System.Random + /// + public NetRandom(int seed) + { + Reinitialise(seed); + } + + /// + /// Create a semi-random seed based on an object + /// + public static int GetSeed(object forObject) + { + // mix some semi-random properties + int seed = (int)Environment.TickCount; + seed ^= forObject.GetHashCode(); + //seed ^= (int)(Stopwatch.GetTimestamp()); + //seed ^= (int)(Environment.WorkingSet); // will return 0 on mono + + int extraSeed = System.Threading.Interlocked.Increment(ref s_extraSeed); + + return seed + extraSeed; + } + + #endregion + + #region Public Methods [Reinitialisation] + + /// + /// Reinitialises using an int value as a seed. + /// + /// + public void Reinitialise(int seed) + { + // The only stipulation stated for the xorshift RNG is that at least one of + // the seeds x,y,z,w is non-zero. We fulfill that requirement by only allowing + // resetting of the x seed + x = (uint)seed; + y = Y; + z = Z; + w = W; + } + + #endregion + + #region Public Methods [System.Random functionally equivalent methods] + + /// + /// Generates a random int over the range 0 to int.MaxValue-1. + /// MaxValue is not generated in order to remain functionally equivalent to System.Random.Next(). + /// This does slightly eat into some of the performance gain over System.Random, but not much. + /// For better performance see: + /// + /// Call NextInt() for an int over the range 0 to int.MaxValue. + /// + /// Call NextUInt() and cast the result to an int to generate an int over the full Int32 value range + /// including negative values. + /// + /// + public int Next() + { + uint t = (x ^ (x << 11)); + x = y; y = z; z = w; + w = (w ^ (w >> 19)) ^ (t ^ (t >> 8)); + + // Handle the special case where the value int.MaxValue is generated. This is outside of + // the range of permitted values, so we therefore call Next() to try again. + uint rtn = w & 0x7FFFFFFF; + if (rtn == 0x7FFFFFFF) + return Next(); + return (int)rtn; + } + + /// + /// Generates a random int over the range 0 to upperBound-1, and not including upperBound. + /// + /// + /// + public int Next(int upperBound) + { + if (upperBound < 0) + throw new ArgumentOutOfRangeException("upperBound", upperBound, "upperBound must be >=0"); + + uint t = (x ^ (x << 11)); + x = y; y = z; z = w; + + // The explicit int cast before the first multiplication gives better performance. + // See comments in NextDouble. + return (int)((REAL_UNIT_INT * (int)(0x7FFFFFFF & (w = (w ^ (w >> 19)) ^ (t ^ (t >> 8))))) * upperBound); + } + + /// + /// Generates a random int over the range lowerBound to upperBound-1, and not including upperBound. + /// upperBound must be >= lowerBound. lowerBound may be negative. + /// + /// + /// + /// + public int Next(int lowerBound, int upperBound) + { + if (lowerBound > upperBound) + throw new ArgumentOutOfRangeException("upperBound", upperBound, "upperBound must be >=lowerBound"); + + uint t = (x ^ (x << 11)); + x = y; y = z; z = w; + + // The explicit int cast before the first multiplication gives better performance. + // See comments in NextDouble. + int range = upperBound - lowerBound; + if (range < 0) + { // If range is <0 then an overflow has occured and must resort to using long integer arithmetic instead (slower). + // We also must use all 32 bits of precision, instead of the normal 31, which again is slower. + return lowerBound + (int)((REAL_UNIT_UINT * (double)(w = (w ^ (w >> 19)) ^ (t ^ (t >> 8)))) * (double)((long)upperBound - (long)lowerBound)); + } + + // 31 bits of precision will suffice if range<=int.MaxValue. This allows us to cast to an int and gain + // a little more performance. + return lowerBound + (int)((REAL_UNIT_INT * (double)(int)(0x7FFFFFFF & (w = (w ^ (w >> 19)) ^ (t ^ (t >> 8))))) * (double)range); + } + + /// + /// Generates a random double. Values returned are from 0.0 up to but not including 1.0. + /// + /// + public double NextDouble() + { + uint t = (x ^ (x << 11)); + x = y; y = z; z = w; + + // Here we can gain a 2x speed improvement by generating a value that can be cast to + // an int instead of the more easily available uint. If we then explicitly cast to an + // int the compiler will then cast the int to a double to perform the multiplication, + // this final cast is a lot faster than casting from a uint to a double. The extra cast + // to an int is very fast (the allocated bits remain the same) and so the overall effect + // of the extra cast is a significant performance improvement. + // + // Also note that the loss of one bit of precision is equivalent to what occurs within + // System.Random. + return (REAL_UNIT_INT * (int)(0x7FFFFFFF & (w = (w ^ (w >> 19)) ^ (t ^ (t >> 8))))); + } + + /// + /// Generates a random single. Values returned are from 0.0 up to but not including 1.0. + /// + public float NextSingle() + { + return (float)NextDouble(); + } + + /// + /// Fills the provided byte array with random bytes. + /// This method is functionally equivalent to System.Random.NextBytes(). + /// + /// + public void NextBytes(byte[] buffer) + { + // Fill up the bulk of the buffer in chunks of 4 bytes at a time. + uint x = this.x, y = this.y, z = this.z, w = this.w; + int i = 0; + uint t; + for (int bound = buffer.Length - 3; i < bound; ) + { + // Generate 4 bytes. + // Increased performance is achieved by generating 4 random bytes per loop. + // Also note that no mask needs to be applied to zero out the higher order bytes before + // casting because the cast ignores thos bytes. Thanks to Stefan Troschütz for pointing this out. + t = (x ^ (x << 11)); + x = y; y = z; z = w; + w = (w ^ (w >> 19)) ^ (t ^ (t >> 8)); + + buffer[i++] = (byte)w; + buffer[i++] = (byte)(w >> 8); + buffer[i++] = (byte)(w >> 16); + buffer[i++] = (byte)(w >> 24); + } + + // Fill up any remaining bytes in the buffer. + if (i < buffer.Length) + { + // Generate 4 bytes. + t = (x ^ (x << 11)); + x = y; y = z; z = w; + w = (w ^ (w >> 19)) ^ (t ^ (t >> 8)); + + buffer[i++] = (byte)w; + if (i < buffer.Length) + { + buffer[i++] = (byte)(w >> 8); + if (i < buffer.Length) + { + buffer[i++] = (byte)(w >> 16); + if (i < buffer.Length) + { + buffer[i] = (byte)(w >> 24); + } + } + } + } + this.x = x; this.y = y; this.z = z; this.w = w; + } + + + // /// + // /// A version of NextBytes that uses a pointer to set 4 bytes of the byte buffer in one operation + // /// thus providing a nice speedup. The loop is also partially unrolled to allow out-of-order-execution, + // /// this results in about a x2 speedup on an AMD Athlon. Thus performance may vary wildly on different CPUs + // /// depending on the number of execution units available. + // /// + // /// Another significant speedup is obtained by setting the 4 bytes by indexing pDWord (e.g. pDWord[i++]=w) + // /// instead of adjusting it dereferencing it (e.g. *pDWord++=w). + // /// + // /// Note that this routine requires the unsafe compilation flag to be specified and so is commented out by default. + // /// + // /// + // public unsafe void NextBytesUnsafe(byte[] buffer) + // { + // if(buffer.Length % 8 != 0) + // throw new ArgumentException("Buffer length must be divisible by 8", "buffer"); + // + // uint x=this.x, y=this.y, z=this.z, w=this.w; + // + // fixed(byte* pByte0 = buffer) + // { + // uint* pDWord = (uint*)pByte0; + // for(int i=0, len=buffer.Length>>2; i < len; i+=2) + // { + // uint t=(x^(x<<11)); + // x=y; y=z; z=w; + // pDWord[i] = w = (w^(w>>19))^(t^(t>>8)); + // + // t=(x^(x<<11)); + // x=y; y=z; z=w; + // pDWord[i+1] = w = (w^(w>>19))^(t^(t>>8)); + // } + // } + // + // this.x=x; this.y=y; this.z=z; this.w=w; + // } + + #endregion + + #region Public Methods [Methods not present on System.Random] + + /// + /// Generates a uint. Values returned are over the full range of a uint, + /// uint.MinValue to uint.MaxValue, inclusive. + /// + /// This is the fastest method for generating a single random number because the underlying + /// random number generator algorithm generates 32 random bits that can be cast directly to + /// a uint. + /// + [CLSCompliant(false)] + public uint NextUInt() + { + uint t = (x ^ (x << 11)); + x = y; y = z; z = w; + return (w = (w ^ (w >> 19)) ^ (t ^ (t >> 8))); + } + + /// + /// Generates a random int over the range 0 to int.MaxValue, inclusive. + /// This method differs from Next() only in that the range is 0 to int.MaxValue + /// and not 0 to int.MaxValue-1. + /// + /// The slight difference in range means this method is slightly faster than Next() + /// but is not functionally equivalent to System.Random.Next(). + /// + /// + public int NextInt() + { + uint t = (x ^ (x << 11)); + x = y; y = z; z = w; + return (int)(0x7FFFFFFF & (w = (w ^ (w >> 19)) ^ (t ^ (t >> 8)))); + } + + + // Buffer 32 bits in bitBuffer, return 1 at a time, keep track of how many have been returned + // with bitBufferIdx. + uint bitBuffer; + uint bitMask = 1; + + /// + /// Generates a single random bit. + /// This method's performance is improved by generating 32 bits in one operation and storing them + /// ready for future calls. + /// + /// + public bool NextBool() + { + if (bitMask == 1) + { + // Generate 32 more bits. + uint t = (x ^ (x << 11)); + x = y; y = z; z = w; + bitBuffer = w = (w ^ (w >> 19)) ^ (t ^ (t >> 8)); + + // Reset the bitMask that tells us which bit to read next. + bitMask = 0x80000000; + return (bitBuffer & bitMask) == 0; + } + + return (bitBuffer & (bitMask >>= 1)) == 0; + } + + #endregion + } +} diff --git a/Lidgren.Network/NetReceiverChannelBase.cs b/Lidgren.Network/NetReceiverChannelBase.cs new file mode 100644 index 000000000..e3f634e30 --- /dev/null +++ b/Lidgren.Network/NetReceiverChannelBase.cs @@ -0,0 +1,18 @@ +using System; + +namespace Lidgren.Network +{ + internal abstract class NetReceiverChannelBase + { + internal NetPeer m_peer; + internal NetConnection m_connection; + + public NetReceiverChannelBase(NetConnection connection) + { + m_connection = connection; + m_peer = connection.m_peer; + } + + internal abstract void ReceiveMessage(NetIncomingMessage msg); + } +} diff --git a/Lidgren.Network/NetReliableOrderedReceiver.cs b/Lidgren.Network/NetReliableOrderedReceiver.cs new file mode 100644 index 000000000..29914488f --- /dev/null +++ b/Lidgren.Network/NetReliableOrderedReceiver.cs @@ -0,0 +1,87 @@ +using System; + +namespace Lidgren.Network +{ + internal sealed class NetReliableOrderedReceiver : NetReceiverChannelBase + { + private int m_windowStart; + private int m_windowSize; + private NetBitVector m_earlyReceived; + internal NetIncomingMessage[] m_withheldMessages; + + public NetReliableOrderedReceiver(NetConnection connection, int windowSize) + : base(connection) + { + m_windowSize = windowSize; + m_withheldMessages = new NetIncomingMessage[windowSize]; + m_earlyReceived = new NetBitVector(windowSize); + } + + private void AdvanceWindow() + { + m_earlyReceived.Set(m_windowStart % m_windowSize, false); + m_windowStart = (m_windowStart + 1) % NetConstants.NumSequenceNumbers; + } + + internal override void ReceiveMessage(NetIncomingMessage message) + { + int relate = NetUtility.RelativeSequenceNumber(message.m_sequenceNumber, m_windowStart); + + // ack no matter what + m_connection.QueueAck(message.m_receivedMessageType, message.m_sequenceNumber); + + if (relate == 0) + { + // Log("Received message #" + message.SequenceNumber + " right on time"); + + // + // excellent, right on time + // + //m_peer.LogVerbose("Received RIGHT-ON-TIME " + message); + + AdvanceWindow(); + m_peer.ReleaseMessage(message); + + // release withheld messages + int nextSeqNr = (message.m_sequenceNumber + 1) % NetConstants.NumSequenceNumbers; + + while (m_earlyReceived[nextSeqNr % m_windowSize]) + { + message = m_withheldMessages[nextSeqNr % m_windowSize]; + NetException.Assert(message != null); + + // remove it from withheld messages + m_withheldMessages[nextSeqNr % m_windowSize] = null; + + m_peer.LogVerbose("Releasing withheld message #" + message); + + m_peer.ReleaseMessage(message); + + AdvanceWindow(); + nextSeqNr++; + } + + return; + } + + if (relate < 0) + { + m_peer.LogVerbose("Received message #" + message.m_sequenceNumber + " DROPPING DUPLICATE"); + // duplicate + return; + } + + // relate > 0 = early message + if (relate > m_windowSize) + { + // too early message! + m_peer.LogDebug("Received " + message + " TOO EARLY! Expected " + m_windowStart); + return; + } + + m_earlyReceived.Set(message.m_sequenceNumber % m_windowSize, true); + m_peer.LogVerbose("Received " + message + " WITHHOLDING, waiting for " + m_windowStart); + m_withheldMessages[message.m_sequenceNumber % m_windowSize] = message; + } + } +} diff --git a/Lidgren.Network/NetReliableSenderChannel.cs b/Lidgren.Network/NetReliableSenderChannel.cs new file mode 100644 index 000000000..c46f3ba14 --- /dev/null +++ b/Lidgren.Network/NetReliableSenderChannel.cs @@ -0,0 +1,261 @@ +using System; +using System.Threading; + +namespace Lidgren.Network +{ + /// + /// Sender part of Selective repeat ARQ for a particular NetChannel + /// + internal sealed class NetReliableSenderChannel : NetSenderChannelBase + { + private NetConnection m_connection; + private int m_windowStart; + private int m_windowSize; + private int m_sendStart; + + private NetBitVector m_receivedAcks; + internal NetStoredReliableMessage[] m_storedMessages; + + internal float m_resendDelay; + + internal override int WindowSize { get { return m_windowSize; } } + + internal NetReliableSenderChannel(NetConnection connection, int windowSize) + { + m_connection = connection; + m_windowSize = windowSize; + m_windowStart = 0; + m_sendStart = 0; + m_receivedAcks = new NetBitVector(NetConstants.NumSequenceNumbers); + m_storedMessages = new NetStoredReliableMessage[m_windowSize]; + m_queuedSends = new NetQueue(8); + m_resendDelay = m_connection.GetResendDelay(); + } + + internal override int GetAllowedSends() + { + int retval = m_windowSize - ((m_sendStart + NetConstants.NumSequenceNumbers) - m_windowStart) % NetConstants.NumSequenceNumbers; + NetException.Assert(retval >= 0 && retval <= m_windowSize); + return retval; + } + + internal override void Reset() + { + m_receivedAcks.Clear(); + for (int i = 0; i < m_storedMessages.Length; i++) + m_storedMessages[i].Reset(); + m_queuedSends.Clear(); + m_windowStart = 0; + m_sendStart = 0; + } + + internal override NetSendResult Enqueue(NetOutgoingMessage message) + { + m_queuedSends.Enqueue(message); + + int queueLen = m_queuedSends.Count; + int left = m_windowSize - ((m_sendStart + NetConstants.NumSequenceNumbers) - m_windowStart) % NetConstants.NumSequenceNumbers; + if (queueLen <= left) + return NetSendResult.Sent; + return NetSendResult.Queued; + } + + // call this regularely + internal override void SendQueuedMessages(float now) + { + // + // resends + // + for (int i = 0; i < m_storedMessages.Length; i++) + { + NetOutgoingMessage om = m_storedMessages[i].Message; + if (om == null) + continue; + + float t = m_storedMessages[i].LastSent; + if (t > 0 && (now - t) > m_resendDelay) + { + // deduce sequence number + /* + int startSlot = m_windowStart % m_windowSize; + int seqNr = m_windowStart; + while (startSlot != i) + { + startSlot--; + if (startSlot < 0) + startSlot = m_windowSize - 1; + seqNr--; + } + */ + + //m_connection.m_peer.LogVerbose("Resending due to delay #" + m_storedMessages[i].SequenceNumber + " " + om.ToString()); + m_connection.m_statistics.MessageResent(MessageResendReason.Delay); + + m_connection.QueueSendMessage(om, m_storedMessages[i].SequenceNumber); + + m_storedMessages[i].LastSent = now; + m_storedMessages[i].NumSent++; + } + } + + int num = GetAllowedSends(); + if (num < 1) + return; + + // queued sends + while (m_queuedSends.Count > 0 && num > 0) + { + NetOutgoingMessage om; + if (m_queuedSends.TryDequeue(out om)) + ExecuteSend(now, om); + num--; + NetException.Assert(num == GetAllowedSends()); + } + } + + private void ExecuteSend(float now, NetOutgoingMessage message) + { + int seqNr = m_sendStart; + m_sendStart = (m_sendStart + 1) % NetConstants.NumSequenceNumbers; + + m_connection.QueueSendMessage(message, seqNr); + + int storeIndex = seqNr % m_windowSize; + NetException.Assert(m_storedMessages[storeIndex].Message == null); + + m_storedMessages[storeIndex].NumSent++; + m_storedMessages[storeIndex].Message = message; + m_storedMessages[storeIndex].LastSent = now; + m_storedMessages[storeIndex].SequenceNumber = seqNr; + + return; + } + + private void DestoreMessage(int storeIndex) + { + NetOutgoingMessage storedMessage = m_storedMessages[storeIndex].Message; +#if DEBUG + if (storedMessage == null) + throw new NetException("m_storedMessages[" + storeIndex + "].Message is null; sent " + m_storedMessages[storeIndex].NumSent + " times, last time " + (NetTime.Now - m_storedMessages[storeIndex].LastSent) + " seconds ago"); +#else + if (storedMessage != null) + { +#endif + Interlocked.Decrement(ref storedMessage.m_recyclingCount); + if (storedMessage.m_recyclingCount <= 0) + m_connection.m_peer.Recycle(storedMessage); + +#if !DEBUG + } +#endif + m_storedMessages[storeIndex] = new NetStoredReliableMessage(); + } + + // remoteWindowStart is remote expected sequence number; everything below this has arrived properly + // seqNr is the actual nr received + internal override void ReceiveAcknowledge(float now, int seqNr) + { + // late (dupe), on time or early ack? + int relate = NetUtility.RelativeSequenceNumber(seqNr, m_windowStart); + + if (relate < 0) + { + //m_connection.m_peer.LogDebug("Received late/dupe ack for #" + seqNr); + return; // late/duplicate ack + } + + if (relate == 0) + { + //m_connection.m_peer.LogDebug("Received right-on-time ack for #" + seqNr); + + // ack arrived right on time + NetException.Assert(seqNr == m_windowStart); + + m_receivedAcks[m_windowStart] = false; + DestoreMessage(m_windowStart % m_windowSize); + m_windowStart = (m_windowStart + 1) % NetConstants.NumSequenceNumbers; + + // advance window if we already have early acks + while (m_receivedAcks.Get(m_windowStart)) + { + //m_connection.m_peer.LogDebug("Using early ack for #" + m_windowStart + "..."); + m_receivedAcks[m_windowStart] = false; + DestoreMessage(m_windowStart % m_windowSize); + + NetException.Assert(m_storedMessages[m_windowStart % m_windowSize].Message == null); // should already be destored + m_windowStart = (m_windowStart + 1) % NetConstants.NumSequenceNumbers; + //m_connection.m_peer.LogDebug("Advancing window to #" + m_windowStart); + } + + return; + } + + // + // early ack... (if it has been sent!) + // + // If it has been sent either the m_windowStart message was lost + // ... or the ack for that message was lost + // + + //m_connection.m_peer.LogDebug("Received early ack for #" + seqNr); + + int sendRelate = NetUtility.RelativeSequenceNumber(seqNr, m_sendStart); + if (sendRelate <= 0) + { + // yes, we've sent this message - it's an early (but valid) ack + if (m_receivedAcks[seqNr]) + { + // we've already destored/been acked for this message + } + else + { + m_receivedAcks[seqNr] = true; + } + } + else if (sendRelate > 0) + { + // uh... we haven't sent this message yet? Weird, dupe or error... + NetException.Assert(false, "Got ack for message not yet sent?"); + return; + } + + // Ok, lets resend all missing acks + int rnr = seqNr; + do + { + rnr--; + if (rnr < 0) + rnr = NetConstants.NumSequenceNumbers - 1; + + if (m_receivedAcks[rnr]) + { + // m_connection.m_peer.LogDebug("Not resending #" + rnr + " (since we got ack)"); + } + else + { + int slot = rnr % m_windowSize; + NetException.Assert(m_storedMessages[slot].Message != null); + if (m_storedMessages[slot].NumSent == 1) + { + // just sent once; resend immediately since we found gap in ack sequence + NetOutgoingMessage rmsg = m_storedMessages[slot].Message; + //m_connection.m_peer.LogVerbose("Resending #" + rnr + " (" + rmsg + ")"); + + if (now - m_storedMessages[slot].LastSent < (m_resendDelay * 0.35f)) + { + // already resent recently + } + else + { + m_storedMessages[slot].LastSent = now; + m_storedMessages[slot].NumSent++; + m_connection.m_statistics.MessageResent(MessageResendReason.HoleInSequence); + m_connection.QueueSendMessage(rmsg, rnr); + } + } + } + + } while (rnr != m_windowStart); + } + } +} diff --git a/Lidgren.Network/NetReliableSequencedReceiver.cs b/Lidgren.Network/NetReliableSequencedReceiver.cs new file mode 100644 index 000000000..0068dfb15 --- /dev/null +++ b/Lidgren.Network/NetReliableSequencedReceiver.cs @@ -0,0 +1,63 @@ +using System; + +namespace Lidgren.Network +{ + internal sealed class NetReliableSequencedReceiver : NetReceiverChannelBase + { + private int m_windowStart; + private int m_windowSize; + + public NetReliableSequencedReceiver(NetConnection connection, int windowSize) + : base(connection) + { + m_windowSize = windowSize; + } + + private void AdvanceWindow() + { + m_windowStart = (m_windowStart + 1) % NetConstants.NumSequenceNumbers; + } + + internal override void ReceiveMessage(NetIncomingMessage message) + { + int nr = message.m_sequenceNumber; + + int relate = NetUtility.RelativeSequenceNumber(nr, m_windowStart); + + // ack no matter what + m_connection.QueueAck(message.m_receivedMessageType, nr); + + if (relate == 0) + { + // Log("Received message #" + message.SequenceNumber + " right on time"); + + // + // excellent, right on time + // + + AdvanceWindow(); + m_peer.ReleaseMessage(message); + return; + } + + if (relate < 0) + { + m_peer.LogVerbose("Received message #" + message.m_sequenceNumber + " DROPPING LATE or DUPE"); + return; + } + + // relate > 0 = early message + if (relate > m_windowSize) + { + // too early message! + m_peer.LogDebug("Received " + message + " TOO EARLY! Expected " + m_windowStart); + return; + } + + // ok + m_windowStart = (m_windowStart + relate) % NetConstants.NumSequenceNumbers; + m_peer.ReleaseMessage(message); + return; + } + } +} diff --git a/Lidgren.Network/NetReliableUnorderedReceiver.cs b/Lidgren.Network/NetReliableUnorderedReceiver.cs new file mode 100644 index 000000000..364e6d8a2 --- /dev/null +++ b/Lidgren.Network/NetReliableUnorderedReceiver.cs @@ -0,0 +1,87 @@ +using System; + +namespace Lidgren.Network +{ + internal sealed class NetReliableUnorderedReceiver : NetReceiverChannelBase + { + private int m_windowStart; + private int m_windowSize; + private NetBitVector m_earlyReceived; + + public NetReliableUnorderedReceiver(NetConnection connection, int windowSize) + : base(connection) + { + m_windowSize = windowSize; + m_earlyReceived = new NetBitVector(windowSize); + } + + private void AdvanceWindow() + { + m_earlyReceived.Set(m_windowStart % m_windowSize, false); + m_windowStart = (m_windowStart + 1) % NetConstants.NumSequenceNumbers; + } + + internal override void ReceiveMessage(NetIncomingMessage message) + { + int relate = NetUtility.RelativeSequenceNumber(message.m_sequenceNumber, m_windowStart); + + // ack no matter what + m_connection.QueueAck(message.m_receivedMessageType, message.m_sequenceNumber); + + if (relate == 0) + { + // Log("Received message #" + message.SequenceNumber + " right on time"); + + // + // excellent, right on time + // + //m_peer.LogVerbose("Received RIGHT-ON-TIME " + message); + + AdvanceWindow(); + m_peer.ReleaseMessage(message); + + // release withheld messages + int nextSeqNr = (message.m_sequenceNumber + 1) % NetConstants.NumSequenceNumbers; + + while (m_earlyReceived[nextSeqNr % m_windowSize]) + { + //message = m_withheldMessages[nextSeqNr % m_windowSize]; + //NetException.Assert(message != null); + + // remove it from withheld messages + //m_withheldMessages[nextSeqNr % m_windowSize] = null; + + //m_peer.LogVerbose("Releasing withheld message #" + message); + + //m_peer.ReleaseMessage(message); + + AdvanceWindow(); + nextSeqNr++; + } + + return; + } + + if (relate < 0) + { + // duplicate + m_peer.LogVerbose("Received message #" + message.m_sequenceNumber + " DROPPING DUPLICATE"); + return; + } + + // relate > 0 = early message + if (relate > m_windowSize) + { + // too early message! + m_peer.LogDebug("Received " + message + " TOO EARLY! Expected " + m_windowStart); + return; + } + + m_earlyReceived.Set(message.m_sequenceNumber % m_windowSize, true); + //m_peer.LogVerbose("Received " + message + " WITHHOLDING, waiting for " + m_windowStart); + //m_withheldMessages[message.m_sequenceNumber % m_windowSize] = message; + + m_peer.ReleaseMessage(message); + } + } +} diff --git a/Lidgren.Network/NetSRP.cs b/Lidgren.Network/NetSRP.cs new file mode 100644 index 000000000..1243c6a77 --- /dev/null +++ b/Lidgren.Network/NetSRP.cs @@ -0,0 +1,204 @@ +#define USE_SHA256 + +using System; +using System.Security.Cryptography; +using System.Text; + +namespace Lidgren.Network +{ + /// + /// Helper methods for implementing SRP authentication + /// + public static class NetSRP + { + private static readonly NetBigInteger N = new NetBigInteger("0115b8b692e0e045692cf280b436735c77a5a9e8a9e7ed56c965f87db5b2a2ece3", 16); + private static readonly NetBigInteger g = NetBigInteger.Two; + private static readonly NetBigInteger k = ComputeMultiplier(); + + private static HashAlgorithm GetHashAlgorithm() + { +#if USE_SHA256 + // this does not seem to work as of yet + return SHA256.Create(); +#else + return SHA1.Create(); +#endif + } + + /// + /// Compute multiplier (k) + /// + private static NetBigInteger ComputeMultiplier() + { + string one = NetUtility.ToHexString(N.ToByteArrayUnsigned()); + string two = NetUtility.ToHexString(g.ToByteArrayUnsigned()); + + string ccstr = one + two.PadLeft(one.Length, '0'); + byte[] cc = NetUtility.ToByteArray(ccstr); + + var sha = GetHashAlgorithm(); + var ccHashed = sha.ComputeHash(cc); + + return new NetBigInteger(NetUtility.ToHexString(ccHashed), 16); + } + + /// + /// Create 16 bytes of random salt + /// + public static byte[] CreateRandomSalt() + { + byte[] retval = new byte[16]; + NetRandom.Instance.NextBytes(retval); + return retval; + } + + /// + /// Create 32 bytes of random ephemeral value + /// + public static byte[] CreateRandomEphemeral() + { + byte[] retval = new byte[32]; + NetRandom.Instance.NextBytes(retval); + return retval; + } + + /// + /// Computer private key (x) + /// + public static byte[] ComputePrivateKey(string username, string password, byte[] salt) + { + var sha = GetHashAlgorithm(); + + byte[] tmp = Encoding.UTF8.GetBytes(username + ":" + password); + byte[] innerHash = sha.ComputeHash(tmp); + + byte[] total = new byte[innerHash.Length + salt.Length]; + Buffer.BlockCopy(salt, 0, total, 0, salt.Length); + Buffer.BlockCopy(innerHash, 0, total, salt.Length, innerHash.Length); + + // x ie. H(salt || H(username || ":" || password)) + return new NetBigInteger(NetUtility.ToHexString(sha.ComputeHash(total)), 16).ToByteArrayUnsigned(); + } + + /// + /// Creates a verifier that the server can later use to authenticate users later on (v) + /// + public static byte[] ComputeServerVerifier(byte[] privateKey) + { + NetBigInteger x = new NetBigInteger(NetUtility.ToHexString(privateKey), 16); + + // Verifier (v) = g^x (mod N) + var serverVerifier = g.ModPow(x, N); + + return serverVerifier.ToByteArrayUnsigned(); + } + + /// + /// SHA hash data + /// + public static byte[] Hash(byte[] data) + { + var sha = GetHashAlgorithm(); + return sha.ComputeHash(data); + } + + /// + /// Compute client public ephemeral value (A) + /// + public static byte[] ComputeClientEphemeral(byte[] clientPrivateEphemeral) // a + { + // A= g^a (mod N) + NetBigInteger a = new NetBigInteger(NetUtility.ToHexString(clientPrivateEphemeral), 16); + NetBigInteger retval = g.ModPow(a, N); + + return retval.ToByteArrayUnsigned(); + } + + /// + /// Compute server ephemeral value (B) + /// + public static byte[] ComputeServerEphemeral(byte[] serverPrivateEphemeral, byte[] verifier) // b + { + var b = new NetBigInteger(NetUtility.ToHexString(serverPrivateEphemeral), 16); + var v = new NetBigInteger(NetUtility.ToHexString(verifier), 16); + + // B = kv + g^b (mod N) + var bb = g.ModPow(b, N); + var kv = v.Multiply(k); + var B = (kv.Add(bb)).Mod(N); + + return B.ToByteArrayUnsigned(); + } + + /// + /// Compute intermediate value (u) + /// + public static byte[] ComputeU(byte[] clientPublicEphemeral, byte[] serverPublicEphemeral) + { + // u = SHA-1(A || B) + string one = NetUtility.ToHexString(clientPublicEphemeral); + string two = NetUtility.ToHexString(serverPublicEphemeral); + + int len = 66; // Math.Max(one.Length, two.Length); + string ccstr = one.PadLeft(len, '0') + two.PadLeft(len, '0'); + + byte[] cc = NetUtility.ToByteArray(ccstr); + + var sha = GetHashAlgorithm(); + var ccHashed = sha.ComputeHash(cc); + + return new NetBigInteger(NetUtility.ToHexString(ccHashed), 16).ToByteArrayUnsigned(); + } + + /// + /// Computes the server session value + /// + public static byte[] ComputeServerSessionValue(byte[] clientPublicEphemeral, byte[] verifier, byte[] udata, byte[] serverPrivateEphemeral) + { + // S = (Av^u) ^ b (mod N) + var A = new NetBigInteger(NetUtility.ToHexString(clientPublicEphemeral), 16); + var v = new NetBigInteger(NetUtility.ToHexString(verifier), 16); + var u = new NetBigInteger(NetUtility.ToHexString(udata), 16); + var b = new NetBigInteger(NetUtility.ToHexString(serverPrivateEphemeral), 16); + + NetBigInteger retval = v.ModPow(u, N).Multiply(A).Mod(N).ModPow(b, N).Mod(N); + + return retval.ToByteArrayUnsigned(); + } + + /// + /// Computes the client session value + /// + public static byte[] ComputeClientSessionValue(byte[] serverPublicEphemeral, byte[] xdata, byte[] udata, byte[] clientPrivateEphemeral) + { + // (B - kg^x) ^ (a + ux) (mod N) + var B = new NetBigInteger(NetUtility.ToHexString(serverPublicEphemeral), 16); + var x = new NetBigInteger(NetUtility.ToHexString(xdata), 16); + var u = new NetBigInteger(NetUtility.ToHexString(udata), 16); + var a = new NetBigInteger(NetUtility.ToHexString(clientPrivateEphemeral), 16); + + var bx = g.ModPow(x, N); + var btmp = B.Add(N.Multiply(k)).Subtract(bx.Multiply(k)).Mod(N); + return btmp.ModPow(x.Multiply(u).Add(a), N).ToByteArrayUnsigned(); + } + + /// + /// Create XTEA symmetrical encryption object from sessionValue + /// + public static NetXtea CreateEncryption(byte[] sessionValue) + { + var sha = GetHashAlgorithm(); + var hash = sha.ComputeHash(sessionValue); + + var key = new byte[16]; + for(int i=0;i<16;i++) + { + key[i] = hash[i]; + for (int j = 1; j < hash.Length / 16; j++) + key[i] ^= hash[i + (j * 16)]; + } + + return new NetXtea(key); + } + } +} diff --git a/Lidgren.Network/NetSendResult.cs b/Lidgren.Network/NetSendResult.cs new file mode 100644 index 000000000..9ceb5bab4 --- /dev/null +++ b/Lidgren.Network/NetSendResult.cs @@ -0,0 +1,30 @@ +using System; + +namespace Lidgren.Network +{ + /// + /// Result of a SendMessage call + /// + public enum NetSendResult + { + /// + /// Message failed to enqueue because there is no connection + /// + FailedNotConnected = 0, + + /// + /// Message was immediately sent + /// + Sent = 1, + + /// + /// Message was queued for delivery + /// + Queued = 2, + + /// + /// Message was dropped immediately since too many message were queued + /// + Dropped = 3 + } +} diff --git a/Lidgren.Network/NetSenderChannelBase.cs b/Lidgren.Network/NetSenderChannelBase.cs new file mode 100644 index 000000000..e9562b6b2 --- /dev/null +++ b/Lidgren.Network/NetSenderChannelBase.cs @@ -0,0 +1,19 @@ +using System; + +namespace Lidgren.Network +{ + internal abstract class NetSenderChannelBase + { + // access this directly to queue things in this channel + internal NetQueue m_queuedSends; + + internal abstract int WindowSize { get; } + + internal abstract int GetAllowedSends(); + + internal abstract NetSendResult Enqueue(NetOutgoingMessage message); + internal abstract void SendQueuedMessages(float now); + internal abstract void Reset(); + internal abstract void ReceiveAcknowledge(float now, int sequenceNumber); + } +} diff --git a/Lidgren.Network/NetServer.cs b/Lidgren.Network/NetServer.cs new file mode 100644 index 000000000..22e2effdc --- /dev/null +++ b/Lidgren.Network/NetServer.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; + +namespace Lidgren.Network +{ + /// + /// Specialized version of NetPeer used for "server" peers + /// + public class NetServer : NetPeer + { + /// + /// NetServer constructor + /// + public NetServer(NetPeerConfiguration config) + : base(config) + { + config.AcceptIncomingConnections = true; + } + + /// + /// Send a message to all connections + /// + /// The message to send + /// How to deliver the message + public void SendToAll(NetOutgoingMessage msg, NetDeliveryMethod method) + { + var all = this.Connections; + if (all.Count <= 0) + return; + + SendMessage(msg, all, method, 0); + } + + /// + /// Send a message to all connections except one + /// + /// The message to send + /// How to deliver the message + /// Don't send to this particular connection + /// Which sequence channel to use for the message + public void SendToAll(NetOutgoingMessage msg, NetConnection except, NetDeliveryMethod method, int sequenceChannel) + { + var all = this.Connections; + if (all.Count <= 0) + return; + + if (except == null) + { + SendMessage(msg, all, method, sequenceChannel); + return; + } + + List recipients = new List(all.Count - 1); + foreach (var conn in all) + if (conn != except) + recipients.Add(conn); + + if (recipients.Count > 0) + SendMessage(msg, recipients, method, sequenceChannel); + } + + /// + /// Returns a string that represents this object + /// + public override string ToString() + { + return "[NetServer " + ConnectionsCount + " connections]"; + } + } +} diff --git a/Lidgren.Network/NetStoredReliableMessage.cs b/Lidgren.Network/NetStoredReliableMessage.cs new file mode 100644 index 000000000..bb2b2fcfe --- /dev/null +++ b/Lidgren.Network/NetStoredReliableMessage.cs @@ -0,0 +1,19 @@ +using System; + +namespace Lidgren.Network +{ + internal struct NetStoredReliableMessage + { + public int NumSent; + public float LastSent; + public NetOutgoingMessage Message; + public int SequenceNumber; + + public void Reset() + { + NumSent = 0; + LastSent = 0; + Message = null; + } + } +} diff --git a/Lidgren.Network/NetTime.cs b/Lidgren.Network/NetTime.cs new file mode 100644 index 000000000..e8d27ba33 --- /dev/null +++ b/Lidgren.Network/NetTime.cs @@ -0,0 +1,61 @@ +/* Copyright (c) 2010 Michael Lidgren + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom +the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ +#define IS_STOPWATCH_AVAILABLE + +using System; +using System.Collections.Generic; +using System.Text; +using System.Diagnostics; + +namespace Lidgren.Network +{ + /// + /// Time service + /// + public static class NetTime + { +#if IS_STOPWATCH_AVAILABLE + private static readonly long s_timeInitialized = Stopwatch.GetTimestamp(); + private static readonly double s_dInvFreq = 1.0 / (double)Stopwatch.Frequency; + + /// + /// Get number of seconds since the application started + /// + public static double Now { get { return (double)(Stopwatch.GetTimestamp() - s_timeInitialized) * s_dInvFreq; } } +#else + private static readonly uint s_timeInitialized = (uint)Environment.TickCount; + + /// + /// Get number of seconds since the application started + /// + public static double Now { get { return (double)((uint)Environment.TickCount - s_timeInitialized) / 1000.0; } } +#endif + + /// + /// Given seconds it will output a human friendly readable string (milliseconds if less than 60 seconds) + /// + public static string ToReadable(double seconds) + { + if (seconds > 60) + return TimeSpan.FromSeconds(seconds).ToString(); + return (seconds * 1000.0).ToString("N2") + " ms"; + } + } +} \ No newline at end of file diff --git a/Lidgren.Network/NetTuple.cs b/Lidgren.Network/NetTuple.cs new file mode 100644 index 000000000..fe3c276cf --- /dev/null +++ b/Lidgren.Network/NetTuple.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Lidgren.Network +{ + // replace with BCL 4.0 Tuple<> when appropriate + internal struct NetTuple + { + public A Item1; + public B Item2; + + public NetTuple(A item1, B item2) + { + Item1 = item1; + Item2 = item2; + } + } +} diff --git a/Lidgren.Network/NetUPnP.cs b/Lidgren.Network/NetUPnP.cs new file mode 100644 index 000000000..8ac09e34d --- /dev/null +++ b/Lidgren.Network/NetUPnP.cs @@ -0,0 +1,266 @@ +using System; +using System.IO; +using System.Xml; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace Lidgren.Network +{ + /// + /// Status of the UPnP capabilities + /// + public enum UPnPStatus + { + /// + /// Still discovering UPnP capabilities + /// + Discovering, + + /// + /// UPnP is not available + /// + NotAvailable, + + /// + /// UPnP is available and ready to use + /// + Available + } + + /// + /// UPnP support class + /// + public class NetUPnP + { + private const int c_discoveryTimeOutMillis = 1000; + + private string m_serviceUrl; + private string m_serviceName = ""; + private NetPeer m_peer; + private ManualResetEvent m_discoveryComplete = new ManualResetEvent(false); + + internal float m_discoveryResponseDeadline; + + private UPnPStatus m_status; + + /// + /// Status of the UPnP capabilities of this NetPeer + /// + public UPnPStatus Status { get { return m_status; } } + + /// + /// NetUPnP constructor + /// + public NetUPnP(NetPeer peer) + { + m_peer = peer; + m_discoveryResponseDeadline = float.MinValue; + } + + internal void Discover(NetPeer peer) + { + string str = +"M-SEARCH * HTTP/1.1\r\n" + +"HOST: 239.255.255.250:1900\r\n" + +"ST:upnp:rootdevice\r\n" + +"MAN:\"ssdp:discover\"\r\n" + +"MX:3\r\n\r\n"; + + m_status = UPnPStatus.Discovering; + + byte[] arr = System.Text.Encoding.UTF8.GetBytes(str); + + m_peer.LogDebug("Attempting UPnP discovery"); + peer.Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true); + peer.RawSend(arr, 0, arr.Length, new IPEndPoint(IPAddress.Broadcast, 1900)); + peer.Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, false); + + // allow some extra time for router to respond + // System.Threading.Thread.Sleep(50); + + m_discoveryResponseDeadline = (float)NetTime.Now + 6.0f; // arbitrarily chosen number, router gets 6 seconds to respond + m_status = UPnPStatus.Discovering; + } + + internal void ExtractServiceUrl(string resp) + { +#if !DEBUG + try + { +#endif + XmlDocument desc = new XmlDocument(); + desc.Load(WebRequest.Create(resp).GetResponse().GetResponseStream()); + XmlNamespaceManager nsMgr = new XmlNamespaceManager(desc.NameTable); + nsMgr.AddNamespace("tns", "urn:schemas-upnp-org:device-1-0"); + XmlNode typen = desc.SelectSingleNode("//tns:device/tns:deviceType/text()", nsMgr); + if (!typen.Value.Contains("InternetGatewayDevice")) + return; + + m_serviceName = "WANIPConnection"; + XmlNode node = desc.SelectSingleNode("//tns:service[tns:serviceType=\"urn:schemas-upnp-org:service:" + m_serviceName + ":1\"]/tns:controlURL/text()", nsMgr); + if (node == null) + { + //try another service name + m_serviceName = "WANPPPConnection"; + node = desc.SelectSingleNode("//tns:service[tns:serviceType=\"urn:schemas-upnp-org:service:" + m_serviceName + ":1\"]/tns:controlURL/text()", nsMgr); + if (node == null) + return; + } + + m_serviceUrl = CombineUrls(resp, node.Value); + m_peer.LogDebug("UPnP service ready"); + m_status = UPnPStatus.Available; + m_discoveryComplete.Set(); +#if !DEBUG + } + catch + { + m_peer.LogVerbose("Exception ignored trying to parse UPnP XML response"); + return; + } +#endif + } + + private static string CombineUrls(string gatewayURL, string subURL) + { + // Is Control URL an absolute URL? + if ((subURL.Contains("http:")) || (subURL.Contains("."))) + return subURL; + + gatewayURL = gatewayURL.Replace("http://", ""); // strip any protocol + int n = gatewayURL.IndexOf("/"); + if (n != -1) + gatewayURL = gatewayURL.Substring(0, n); // Use first portion of URL + return "http://" + gatewayURL + subURL; + } + + private bool CheckAvailability() + { + switch (m_status) + { + case UPnPStatus.NotAvailable: + return false; + case UPnPStatus.Available: + return true; + case UPnPStatus.Discovering: + if (m_discoveryComplete.WaitOne(c_discoveryTimeOutMillis)) + return true; + if (NetTime.Now > m_discoveryResponseDeadline) + m_status = UPnPStatus.NotAvailable; + return false; + } + return false; + } + + /// + /// Add a forwarding rule to the router using UPnP + /// + public bool ForwardPort(int port, string description) + { + if (!CheckAvailability()) + return false; + + IPAddress mask; + var client = NetUtility.GetMyAddress(out mask); + if (client == null) + return false; + + try + { + XmlDocument xdoc = SOAPRequest(m_serviceUrl, + "" + + "" + + "" + port.ToString() + "" + + "" + ProtocolType.Udp.ToString().ToUpper() + "" + + "" + port.ToString() + "" + + "" + client.ToString() + "" + + "1" + + "" + description + "" + + "0" + + "", + "AddPortMapping"); + + m_peer.LogDebug("Sent UPnP port forward request"); + System.Threading.Thread.Sleep(50); + } + catch (Exception ex) + { + m_peer.LogWarning("UPnP port forward failed: " + ex.Message); + return false; + } + return true; + } + + /// + /// Delete a forwarding rule from the router using UPnP + /// + public bool DeleteForwardingRule(int port) + { + if (!CheckAvailability()) + return false; + + try + { + XmlDocument xdoc = SOAPRequest(m_serviceUrl, + "" + + "" + + "" + + "" + port + "" + + "" + ProtocolType.Udp.ToString().ToUpper() + "" + + "", "DeletePortMapping"); + return true; + } + catch (Exception ex) + { + m_peer.LogWarning("UPnP delete forwarding rule failed: " + ex.Message); + return false; + } + } + + /// + /// Retrieve the extern ip using UPnP + /// + public IPAddress GetExternalIP() + { + if (!CheckAvailability()) + return null; + try + { + XmlDocument xdoc = SOAPRequest(m_serviceUrl, "" + + "", "GetExternalIPAddress"); + XmlNamespaceManager nsMgr = new XmlNamespaceManager(xdoc.NameTable); + nsMgr.AddNamespace("tns", "urn:schemas-upnp-org:device-1-0"); + string IP = xdoc.SelectSingleNode("//NewExternalIPAddress/text()", nsMgr).Value; + return IPAddress.Parse(IP); + } + catch (Exception ex) + { + m_peer.LogWarning("Failed to get external IP: " + ex.Message); + return null; + } + } + + private XmlDocument SOAPRequest(string url, string soap, string function) + { + string req = "" + + "" + + "" + + soap + + "" + + ""; + WebRequest r = HttpWebRequest.Create(url); + r.Method = "POST"; + byte[] b = System.Text.Encoding.UTF8.GetBytes(req); + r.Headers.Add("SOAPACTION", "\"urn:schemas-upnp-org:service:" + m_serviceName + ":1#" + function + "\""); + r.ContentType = "text/xml; charset=\"utf-8\""; + r.ContentLength = b.Length; + r.GetRequestStream().Write(b, 0, b.Length); + XmlDocument resp = new XmlDocument(); + WebResponse wres = r.GetResponse(); + Stream ress = wres.GetResponseStream(); + resp.Load(ress); + return resp; + } + } +} \ No newline at end of file diff --git a/Lidgren.Network/NetUnreliableSenderChannel.cs b/Lidgren.Network/NetUnreliableSenderChannel.cs new file mode 100644 index 000000000..4c1b9db0f --- /dev/null +++ b/Lidgren.Network/NetUnreliableSenderChannel.cs @@ -0,0 +1,125 @@ +using System; +using System.Threading; + +namespace Lidgren.Network +{ + /// + /// Sender part of Selective repeat ARQ for a particular NetChannel + /// + internal sealed class NetUnreliableSenderChannel : NetSenderChannelBase + { + private NetConnection m_connection; + private int m_windowStart; + private int m_windowSize; + private int m_sendStart; + + private NetBitVector m_receivedAcks; + + internal override int WindowSize { get { return m_windowSize; } } + + internal NetUnreliableSenderChannel(NetConnection connection, int windowSize) + { + m_connection = connection; + m_windowSize = windowSize; + m_windowStart = 0; + m_sendStart = 0; + m_receivedAcks = new NetBitVector(NetConstants.NumSequenceNumbers); + m_queuedSends = new NetQueue(8); + } + + internal override int GetAllowedSends() + { + int retval = m_windowSize - ((m_sendStart + NetConstants.NumSequenceNumbers) - m_windowStart) % m_windowSize; + NetException.Assert(retval >= 0 && retval <= m_windowSize); + return retval; + } + + internal override void Reset() + { + m_receivedAcks.Clear(); + m_queuedSends.Clear(); + m_windowStart = 0; + m_sendStart = 0; + } + + internal override NetSendResult Enqueue(NetOutgoingMessage message) + { + int queueLen = m_queuedSends.Count + 1; + int left = m_windowSize - ((m_sendStart + NetConstants.NumSequenceNumbers) - m_windowStart) % NetConstants.NumSequenceNumbers; + if (queueLen > left) + return NetSendResult.Dropped; + + m_queuedSends.Enqueue(message); + return NetSendResult.Sent; + } + + // call this regularely + internal override void SendQueuedMessages(float now) + { + int num = GetAllowedSends(); + if (num < 1) + return; + + // queued sends + while (m_queuedSends.Count > 0 && num > 0) + { + NetOutgoingMessage om; + if (m_queuedSends.TryDequeue(out om)) + ExecuteSend(om); + num--; + } + } + + private void ExecuteSend(NetOutgoingMessage message) + { + m_connection.m_peer.VerifyNetworkThread(); + + int seqNr = m_sendStart; + m_sendStart = (m_sendStart + 1) % NetConstants.NumSequenceNumbers; + + m_connection.QueueSendMessage(message, seqNr); + + Interlocked.Decrement(ref message.m_recyclingCount); + if (message.m_recyclingCount <= 0) + m_connection.m_peer.Recycle(message); + + return; + } + + // remoteWindowStart is remote expected sequence number; everything below this has arrived properly + // seqNr is the actual nr received + internal override void ReceiveAcknowledge(float now, int seqNr) + { + // late (dupe), on time or early ack? + int relate = NetUtility.RelativeSequenceNumber(seqNr, m_windowStart); + + if (relate < 0) + { + //m_connection.m_peer.LogDebug("Received late/dupe ack for #" + seqNr); + return; // late/duplicate ack + } + + if (relate == 0) + { + //m_connection.m_peer.LogDebug("Received right-on-time ack for #" + seqNr); + + // ack arrived right on time + NetException.Assert(seqNr == m_windowStart); + + m_receivedAcks[m_windowStart] = false; + m_windowStart = (m_windowStart + 1) % NetConstants.NumSequenceNumbers; + + return; + } + + // Advance window to this position + m_receivedAcks[seqNr] = true; + + while (m_windowStart != seqNr) + { + m_receivedAcks[m_windowStart] = false; + m_windowStart = (m_windowStart + 1) % NetConstants.NumSequenceNumbers; + } + } + } +} diff --git a/Lidgren.Network/NetUnreliableSequencedReceiver.cs b/Lidgren.Network/NetUnreliableSequencedReceiver.cs new file mode 100644 index 000000000..e3a582b38 --- /dev/null +++ b/Lidgren.Network/NetUnreliableSequencedReceiver.cs @@ -0,0 +1,29 @@ +using System; + +namespace Lidgren.Network +{ + internal sealed class NetUnreliableSequencedReceiver : NetReceiverChannelBase + { + private int m_lastReceivedSequenceNumber; + + public NetUnreliableSequencedReceiver(NetConnection connection) + : base(connection) + { + } + + internal override void ReceiveMessage(NetIncomingMessage msg) + { + int nr = msg.m_sequenceNumber; + + // ack no matter what + m_connection.QueueAck(msg.m_receivedMessageType, nr); + + int relate = NetUtility.RelativeSequenceNumber(nr, m_lastReceivedSequenceNumber); + if (relate < 0) + return; // drop if late + + m_lastReceivedSequenceNumber = nr; + m_peer.ReleaseMessage(msg); + } + } +} diff --git a/Lidgren.Network/NetUnreliableUnorderedReceiver.cs b/Lidgren.Network/NetUnreliableUnorderedReceiver.cs new file mode 100644 index 000000000..b61bb1dd4 --- /dev/null +++ b/Lidgren.Network/NetUnreliableUnorderedReceiver.cs @@ -0,0 +1,20 @@ +using System; + +namespace Lidgren.Network +{ + internal sealed class NetUnreliableUnorderedReceiver : NetReceiverChannelBase + { + public NetUnreliableUnorderedReceiver(NetConnection connection) + : base(connection) + { + } + + internal override void ReceiveMessage(NetIncomingMessage msg) + { + // ack no matter what + m_connection.QueueAck(msg.m_receivedMessageType, msg.m_sequenceNumber); + + m_peer.ReleaseMessage(msg); + } + } +} diff --git a/Lidgren.Network/NetUtility.cs b/Lidgren.Network/NetUtility.cs new file mode 100644 index 000000000..43794789b --- /dev/null +++ b/Lidgren.Network/NetUtility.cs @@ -0,0 +1,592 @@ +/* Copyright (c) 2010 Michael Lidgren + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom +the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +#if !__ANDROID__ && !IOS +#define IS_FULL_NET_AVAILABLE +#endif + +using System; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Text; +using System.Text.RegularExpressions; +using System.Collections.Generic; + +namespace Lidgren.Network +{ + /// + /// Utility methods + /// + public static class NetUtility + { + /// + /// Resolve endpoint callback + /// + public delegate void ResolveEndPointCallback(IPEndPoint endPoint); + + /// + /// Resolve address callback + /// + public delegate void ResolveAddressCallback(IPAddress adr); + + /// + /// Get IPv4 endpoint from notation (xxx.xxx.xxx.xxx) or hostname and port number (asynchronous version) + /// + public static void ResolveAsync(string ipOrHost, int port, ResolveEndPointCallback callback) + { + ResolveAsync(ipOrHost, delegate(IPAddress adr) + { + if (adr == null) + { + callback(null); + } + else + { + callback(new IPEndPoint(adr, port)); + } + }); + } + + /// + /// Get IPv4 endpoint from notation (xxx.xxx.xxx.xxx) or hostname and port number + /// + public static IPEndPoint Resolve(string ipOrHost, int port) + { + IPAddress adr = Resolve(ipOrHost); + return new IPEndPoint(adr, port); + } + + /// + /// Get IPv4 address from notation (xxx.xxx.xxx.xxx) or hostname (asynchronous version) + /// + public static void ResolveAsync(string ipOrHost, ResolveAddressCallback callback) + { + if (string.IsNullOrEmpty(ipOrHost)) + throw new ArgumentException("Supplied string must not be empty", "ipOrHost"); + + ipOrHost = ipOrHost.Trim(); + + IPAddress ipAddress = null; + if (IPAddress.TryParse(ipOrHost, out ipAddress)) + { + if (ipAddress.AddressFamily == AddressFamily.InterNetwork) + { + callback(ipAddress); + return; + } + throw new ArgumentException("This method will not currently resolve other than ipv4 addresses"); + } + + // ok must be a host name + IPHostEntry entry; + try + { + Dns.BeginGetHostEntry(ipOrHost, delegate(IAsyncResult result) + { + try + { + entry = Dns.EndGetHostEntry(result); + } + catch (SocketException ex) + { + if (ex.SocketErrorCode == SocketError.HostNotFound) + { + //LogWrite(string.Format(CultureInfo.InvariantCulture, "Failed to resolve host '{0}'.", ipOrHost)); + callback(null); + return; + } + else + { + throw; + } + } + + if (entry == null) + { + callback(null); + return; + } + + // check each entry for a valid IP address + foreach (IPAddress ipCurrent in entry.AddressList) + { + if (ipCurrent.AddressFamily == AddressFamily.InterNetwork) + { + callback(ipCurrent); + return; + } + } + + callback(null); + }, null); + } + catch (SocketException ex) + { + if (ex.SocketErrorCode == SocketError.HostNotFound) + { + //LogWrite(string.Format(CultureInfo.InvariantCulture, "Failed to resolve host '{0}'.", ipOrHost)); + callback(null); + } + else + { + throw; + } + } + } + + /// + /// Get IPv4 address from notation (xxx.xxx.xxx.xxx) or hostname + /// + public static IPAddress Resolve(string ipOrHost) + { + if (string.IsNullOrEmpty(ipOrHost)) + throw new ArgumentException("Supplied string must not be empty", "ipOrHost"); + + ipOrHost = ipOrHost.Trim(); + + IPAddress ipAddress = null; + if (IPAddress.TryParse(ipOrHost, out ipAddress)) + { + if (ipAddress.AddressFamily == AddressFamily.InterNetwork) + return ipAddress; + throw new ArgumentException("This method will not currently resolve other than ipv4 addresses"); + } + + // ok must be a host name + try + { + var addresses = Dns.GetHostAddresses(ipOrHost); + if (addresses == null) + return null; + foreach (var address in addresses) + { + if (address.AddressFamily == AddressFamily.InterNetwork) + return address; + } + return null; + } + catch (SocketException ex) + { + if (ex.SocketErrorCode == SocketError.HostNotFound) + { + //LogWrite(string.Format(CultureInfo.InvariantCulture, "Failed to resolve host '{0}'.", ipOrHost)); + return null; + } + else + { + throw; + } + } + } + +#if IS_FULL_NET_AVAILABLE + + private static NetworkInterface GetNetworkInterface() + { + IPGlobalProperties computerProperties = IPGlobalProperties.GetIPGlobalProperties(); + if (computerProperties == null) + return null; + + NetworkInterface[] nics = NetworkInterface.GetAllNetworkInterfaces(); + if (nics == null || nics.Length < 1) + return null; + + NetworkInterface best = null; + foreach (NetworkInterface adapter in nics) + { + if (adapter.NetworkInterfaceType == NetworkInterfaceType.Loopback || adapter.NetworkInterfaceType == NetworkInterfaceType.Unknown) + continue; + if (!adapter.Supports(NetworkInterfaceComponent.IPv4)) + continue; + if (best == null) + best = adapter; + if (adapter.OperationalStatus != OperationalStatus.Up) + continue; + + // make sure this adapter has any ipv4 addresses + IPInterfaceProperties properties = adapter.GetIPProperties(); + foreach (UnicastIPAddressInformation unicastAddress in properties.UnicastAddresses) + { + if (unicastAddress != null && unicastAddress.Address != null && unicastAddress.Address.AddressFamily == AddressFamily.InterNetwork) + { + // Yes it does, return this network interface. + return adapter; + } + } + } + return best; + } + + /// + /// Returns the physical (MAC) address for the first usable network interface + /// + public static PhysicalAddress GetMacAddress() + { + NetworkInterface ni = GetNetworkInterface(); + if (ni == null) + return null; + return ni.GetPhysicalAddress(); + } +#endif + + /// + /// Create a hex string from an Int64 value + /// + public static string ToHexString(long data) + { + return ToHexString(BitConverter.GetBytes(data)); + } + + /// + /// Create a hex string from an array of bytes + /// + public static string ToHexString(byte[] data) + { + char[] c = new char[data.Length * 2]; + byte b; + for (int i = 0; i < data.Length; ++i) + { + b = ((byte)(data[i] >> 4)); + c[i * 2] = (char)(b > 9 ? b + 0x37 : b + 0x30); + b = ((byte)(data[i] & 0xF)); + c[i * 2 + 1] = (char)(b > 9 ? b + 0x37 : b + 0x30); + } + return new string(c); + } + + /// + /// Gets the local broadcast address + /// + public static IPAddress GetBroadcastAddress() + { +#if __ANDROID__ + try{ + Android.Net.Wifi.WifiManager wifi = (Android.Net.Wifi.WifiManager)Android.App.Application.Context.GetSystemService(Android.App.Activity.WifiService); + if (wifi.IsWifiEnabled) + { + var dhcp = wifi.DhcpInfo; + + int broadcast = (dhcp.IpAddress & dhcp.Netmask) | ~dhcp.Netmask; + byte[] quads = new byte[4]; + for (int k = 0; k < 4; k++) + { + quads[k] = (byte) ((broadcast >> k * 8) & 0xFF); + } + return new IPAddress(quads); + } + } + catch // Catch Access Denied Errors + { + return IPAddress.Broadcast; + } +#endif +#if IS_FULL_NET_AVAILABLE + try + { + NetworkInterface ni = GetNetworkInterface(); + if (ni == null) + { + return null; + } + + IPInterfaceProperties properties = ni.GetIPProperties(); + foreach (UnicastIPAddressInformation unicastAddress in properties.UnicastAddresses) + { + if (unicastAddress != null && unicastAddress.Address != null && unicastAddress.Address.AddressFamily == AddressFamily.InterNetwork) + { + var mask = unicastAddress.IPv4Mask; + byte[] ipAdressBytes = unicastAddress.Address.GetAddressBytes(); + byte[] subnetMaskBytes = mask.GetAddressBytes(); + + if (ipAdressBytes.Length != subnetMaskBytes.Length) + throw new ArgumentException("Lengths of IP address and subnet mask do not match."); + + byte[] broadcastAddress = new byte[ipAdressBytes.Length]; + for (int i = 0; i < broadcastAddress.Length; i++) + { + broadcastAddress[i] = (byte)(ipAdressBytes[i] | (subnetMaskBytes[i] ^ 255)); + } + return new IPAddress(broadcastAddress); + } + } + } + catch // Catch any errors + { + return IPAddress.Broadcast; + } +#endif + return IPAddress.Broadcast; + } + + /// + /// Gets my local IPv4 address (not necessarily external) and subnet mask + /// + public static IPAddress GetMyAddress(out IPAddress mask) + { + mask = null; +#if __ANDROID__ + try + { + Android.Net.Wifi.WifiManager wifi = (Android.Net.Wifi.WifiManager)Android.App.Application.Context.GetSystemService(Android.App.Activity.WifiService); + if (!wifi.IsWifiEnabled) return null; + var dhcp = wifi.DhcpInfo; + + int addr = dhcp.IpAddress; + byte[] quads = new byte[4]; + for (int k = 0; k < 4; k++) + { + quads[k] = (byte) ((addr >> k * 8) & 0xFF); + } + return new IPAddress(quads); + } + catch // Catch Access Denied errors + { + return null; + } + +#endif +#if IS_FULL_NET_AVAILABLE + NetworkInterface ni = GetNetworkInterface(); + if (ni == null) + { + mask = null; + return null; + } + + IPInterfaceProperties properties = ni.GetIPProperties(); + foreach (UnicastIPAddressInformation unicastAddress in properties.UnicastAddresses) + { + if (unicastAddress != null && unicastAddress.Address != null && unicastAddress.Address.AddressFamily == AddressFamily.InterNetwork) + { + mask = unicastAddress.IPv4Mask; + return unicastAddress.Address; + } + } +#endif + return null; + } + + /// + /// Returns true if the IPEndPoint supplied is on the same subnet as this host + /// + public static bool IsLocal(IPEndPoint endPoint) + { + if (endPoint == null) + return false; + return IsLocal(endPoint.Address); + } + + /// + /// Returns true if the IPAddress supplied is on the same subnet as this host + /// + public static bool IsLocal(IPAddress remote) + { + IPAddress mask; + IPAddress local = GetMyAddress(out mask); + + if (mask == null) + return false; + + uint maskBits = BitConverter.ToUInt32(mask.GetAddressBytes(), 0); + uint remoteBits = BitConverter.ToUInt32(remote.GetAddressBytes(), 0); + uint localBits = BitConverter.ToUInt32(local.GetAddressBytes(), 0); + + // compare network portions + return ((remoteBits & maskBits) == (localBits & maskBits)); + } + + /// + /// Returns how many bits are necessary to hold a certain number + /// + [CLSCompliant(false)] + public static int BitsToHoldUInt(uint value) + { + int bits = 1; + while ((value >>= 1) != 0) + bits++; + return bits; + } + + /// + /// Returns how many bytes are required to hold a certain number of bits + /// + public static int BytesToHoldBits(int numBits) + { + return (numBits + 7) / 8; + } + + internal static UInt32 SwapByteOrder(UInt32 value) + { + return + ((value & 0xff000000) >> 24) | + ((value & 0x00ff0000) >> 8) | + ((value & 0x0000ff00) << 8) | + ((value & 0x000000ff) << 24); + } + + internal static UInt64 SwapByteOrder(UInt64 value) + { + return + ((value & 0xff00000000000000L) >> 56) | + ((value & 0x00ff000000000000L) >> 40) | + ((value & 0x0000ff0000000000L) >> 24) | + ((value & 0x000000ff00000000L) >> 8) | + ((value & 0x00000000ff000000L) << 8) | + ((value & 0x0000000000ff0000L) << 24) | + ((value & 0x000000000000ff00L) << 40) | + ((value & 0x00000000000000ffL) << 56); + } + + internal static bool CompareElements(byte[] one, byte[] two) + { + if (one.Length != two.Length) + return false; + for (int i = 0; i < one.Length; i++) + if (one[i] != two[i]) + return false; + return true; + } + + /// + /// Convert a hexadecimal string to a byte array + /// + public static byte[] ToByteArray(String hexString) + { + byte[] retval = new byte[hexString.Length / 2]; + for (int i = 0; i < hexString.Length; i += 2) + retval[i / 2] = Convert.ToByte(hexString.Substring(i, 2), 16); + return retval; + } + + /// + /// Converts a number of bytes to a shorter, more readable string representation + /// + public static string ToHumanReadable(long bytes) + { + if (bytes < 4000) // 1-4 kb is printed in bytes + return bytes + " bytes"; + if (bytes < 1000 * 1000) // 4-999 kb is printed in kb + return Math.Round(((double)bytes / 1000.0), 2) + " kilobytes"; + return Math.Round(((double)bytes / (1000.0 * 1000.0)), 2) + " megabytes"; // else megabytes + } + + internal static int RelativeSequenceNumber(int nr, int expected) + { + return (nr - expected + NetConstants.NumSequenceNumbers + (NetConstants.NumSequenceNumbers / 2)) % NetConstants.NumSequenceNumbers - (NetConstants.NumSequenceNumbers / 2); + + // old impl: + //int retval = ((nr + NetConstants.NumSequenceNumbers) - expected) % NetConstants.NumSequenceNumbers; + //if (retval > (NetConstants.NumSequenceNumbers / 2)) + // retval -= NetConstants.NumSequenceNumbers; + //return retval; + } + + /// + /// Gets the window size used internally in the library for a certain delivery method + /// + public static int GetWindowSize(NetDeliveryMethod method) + { + switch (method) + { + case NetDeliveryMethod.Unknown: + return 0; + + case NetDeliveryMethod.Unreliable: + case NetDeliveryMethod.UnreliableSequenced: + return NetConstants.UnreliableWindowSize; + + case NetDeliveryMethod.ReliableOrdered: + return NetConstants.ReliableOrderedWindowSize; + + case NetDeliveryMethod.ReliableSequenced: + case NetDeliveryMethod.ReliableUnordered: + default: + return NetConstants.DefaultWindowSize; + } + } + + // shell sort + internal static void SortMembersList(System.Reflection.MemberInfo[] list) + { + int h; + int j; + System.Reflection.MemberInfo tmp; + + h = 1; + while (h * 3 + 1 <= list.Length) + h = 3 * h + 1; + + while (h > 0) + { + for (int i = h - 1; i < list.Length; i++) + { + tmp = list[i]; + j = i; + while (true) + { + if (j >= h) + { + if (string.Compare(list[j - h].Name, tmp.Name, StringComparison.InvariantCulture) > 0) + { + list[j] = list[j - h]; + j -= h; + } + else + break; + } + else + break; + } + + list[j] = tmp; + } + h /= 3; + } + } + + internal static NetDeliveryMethod GetDeliveryMethod(NetMessageType mtp) + { + if (mtp >= NetMessageType.UserReliableOrdered1) + return NetDeliveryMethod.ReliableOrdered; + else if (mtp >= NetMessageType.UserReliableSequenced1) + return NetDeliveryMethod.ReliableSequenced; + else if (mtp >= NetMessageType.UserReliableUnordered) + return NetDeliveryMethod.ReliableUnordered; + else if (mtp >= NetMessageType.UserSequenced1) + return NetDeliveryMethod.UnreliableSequenced; + return NetDeliveryMethod.Unreliable; + } + + /// + /// Creates a comma delimited string from a lite of items + /// + public static string MakeCommaDelimitedList(IList list) + { + var cnt = list.Count; + StringBuilder bdr = new StringBuilder(cnt * 5); // educated guess + for(int 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 P)!*gBzK_i1;qLJbAUZ6* literal 0 HcmV?d00001 diff --git a/Subsurface/Content/Map/locationTypes.xml b/Subsurface/Content/Map/locationTypes.xml index 5224c0cdd..93450568d 100644 --- a/Subsurface/Content/Map/locationTypes.xml +++ b/Subsurface/Content/Map/locationTypes.xml @@ -1,7 +1,7 @@  + symbol="Content/Map/beaconSymbol.png"> + symbol="Content/Map/citySymbol.png"> + symbol="Content/Map/militarySymbol.png"> + symbol="Content/Map/researchSymbol.png"> @@ -67,7 +67,7 @@ startcolor="0.5, 0.0, 0.0" startalpha="1.0" colorchange="0.0, 0.0, 0.0, -0.25" lifetime="5.0" - inwater="true" + drawtarget="water" velocitychange="0.0, 0.0"> @@ -79,7 +79,7 @@ startcolor="1.0, 1.0, 1.0" startalpha="1.0" colorchange="-0.4, -0.4, -0.4, -0.3" lifetime="5.0" - inwater="false" + drawtarget="air" deleteonhit="true" velocitychange="0.0, 0.5"> @@ -91,7 +91,7 @@ startcolor="1.0, 1.0, 1.0" startalpha="1.0" colorchange="0.0, 0.0, 0.0, -4.0" lifetime="0.25" - inwater="false" + drawtarget="both" velocitychange="0.0, 0.0"> @@ -102,7 +102,7 @@ startcolor="1.0, 1.0, 1.0" startalpha="1.0" colorchange="0.0, 0.0, 0.0, -4.0" lifetime="0.25" - inwater="false" + drawtarget="both" velocitychange="0.0, 0.0"> diff --git a/Subsurface/Content/Sounds/sounds.xml b/Subsurface/Content/Sounds/sounds.xml index 77d6fbc01..e1c7eb5fd 100644 --- a/Subsurface/Content/Sounds/sounds.xml +++ b/Subsurface/Content/Sounds/sounds.xml @@ -29,7 +29,7 @@ - - - + + + \ No newline at end of file diff --git a/Subsurface/Content/UI/style.xml b/Subsurface/Content/UI/style.xml index 5fb091a28..e96349746 100644 --- a/Subsurface/Content/UI/style.xml +++ b/Subsurface/Content/UI/style.xml @@ -11,8 +11,8 @@ outlinecolor="0.5, 0.57, 0.6, 1.0"> - - + + - - + + - + - + - + - - + + \ No newline at end of file diff --git a/Subsurface/Content/effects_linux.mgfx b/Subsurface/Content/effects_linux.mgfx new file mode 100644 index 0000000000000000000000000000000000000000..39132c2f9b8cb5457ed5be8b7d0622b60f57da49 GIT binary patch literal 3699 zcmb_fU2oeq6s4p%9}W!UCkV*Hk~9@0S+)lZ1&St3`>+B5I;;b-BGY6!(O^r4BrCFu z0{gvt+Dkb2+@v+ZW6;LJ1FnOr&DAx!sIE0ys$GEEj-aY`&n46W6lO zP1_oso6qUnw%pMv;Q3IgEjJvw;mVC6G>##hC=QW9V5>ark4=c9LYx-FRUvK*GE^bM z7NjF5UJEi(M;QTxCpPmJ%Xyf(mk7%+3*8o)5@g2)`@l#k{BuP5OG)nm?Y_m2s zEXjp!-py~XQvdEEnI|c(Z;?b(bKytv$?2f~Y0y6a7O8EOYgk20wuhh3@Uy^Pn5uck0}B_(iv&6>ff54q7swD>$%w<22S*nfaUVg|c^hs=N!eA`K+L$Q zXGC6|E#oaeZTo0m6j_5Q_v*4iptdz`e_?nYDL9{P*iLP zMa2dtRrq+&;PL@RkXLwDGqDnMBg-p792FwVD?(fqBFig6hAKpsSA=*fM3z^Cj8q89 zs~pi-g^XK}i3*tj#Lx3I`hAs$mE@udHq(_*Nxe?8D2D>u=|R`wN#%xE63D9%^-ggH zP>P!u1E<>=JMdsJ!x=Srphjz|7TAkN7~%B1aQF$caC`|5>K8RbdCvS_az{@%;(2R3CZJD%oF+NWQo!YbI#V&?!{9HTXZVF` zBgZM&xx(BTmca2SM%_^e949@pwva|K{@0|jRMd+jCgG%^3U;l9z;V)8WjazXaGdmb z8|lqsq^m6^GCgUKMr&gOO&X-dMiV$rdeR`R;yCF^gEYs%_P|>~E7LaL9!XUeVP_P) zWW!~LpG*r^so4hYuO!r>r$x5nr3Y#Rx5K64tl?c(3n&(P4=1&;M9?pX7L0$O`R(uF z>UFVrQd*#t{r^iLtD4Ol9V_7UVu;H?tEVPcPtqsGpnNr7rI%5b`*9FL(S*W@k7n$` z{S~gE@VUdf8gHXte(;O%Lp06rYcFCP((bn*l-IoO*NlDI59*22V;Xz=CDa4FLNfx!-T;Ek!xRI0=Py~Wen|V? b-}}=rC0g&=&oZ0PEFZpJEc12Sg4y{BH{KpO literal 0 HcmV?d00001 diff --git a/Subsurface/Content/randomevents.xml b/Subsurface/Content/randomevents.xml index 1f5a88d84..03aada535 100644 --- a/Subsurface/Content/randomevents.xml +++ b/Subsurface/Content/randomevents.xml @@ -1,24 +1,21 @@  \ No newline at end of file diff --git a/Subsurface/Source/Characters/AI/AIController.cs b/Subsurface/Source/Characters/AI/AIController.cs index 53aed1da8..e0ebca03f 100644 --- a/Subsurface/Source/Characters/AI/AIController.cs +++ b/Subsurface/Source/Characters/AI/AIController.cs @@ -34,6 +34,7 @@ namespace Subsurface public AiState State { get { return state; } + set { state = value; } } public AIController (Character c) diff --git a/Subsurface/Source/Characters/AI/EnemyAIController.cs b/Subsurface/Source/Characters/AI/EnemyAIController.cs index 4eda0150f..497335955 100644 --- a/Subsurface/Source/Characters/AI/EnemyAIController.cs +++ b/Subsurface/Source/Characters/AI/EnemyAIController.cs @@ -469,8 +469,8 @@ namespace Subsurface public override void FillNetworkData(NetOutgoingMessage message) { message.Write((byte)state); - - bool wallAttack = (wallAttackPos!=Vector2.Zero && state == AiState.Attack); + + bool wallAttack = (wallAttackPos != Vector2.Zero && state == AiState.Attack); message.Write(wallAttack); diff --git a/Subsurface/Source/Characters/Character.cs b/Subsurface/Source/Characters/Character.cs index 00093844e..714e6260d 100644 --- a/Subsurface/Source/Characters/Character.cs +++ b/Subsurface/Source/Characters/Character.cs @@ -46,9 +46,11 @@ namespace Subsurface get { return Properties; } } - protected Key selectKeyHit; - protected Key actionKeyHit, actionKeyDown; - protected Key secondaryKeyHit, secondaryKeyDown; + protected Key[] keys; + + //protected Key selectKeyHit; + //protected Key actionKeyHit, actionKeyDown; + //protected Key secondaryKeyHit, secondaryKeyDown; private Item selectedConstruction; private Item[] selectedItems; @@ -123,10 +125,10 @@ namespace Subsurface get { return cursorPosition; } } - public AITarget AiTarget - { - get { return aiTarget; } - } + //public AITarget AiTarget + //{ + // get { return aiTarget; } + //} public float SoundRange { @@ -244,31 +246,6 @@ namespace Subsurface get { return closestItem; } } - public Key SelectKeyHit - { - get { return selectKeyHit; } - } - - public Key ActionKeyHit - { - get { return actionKeyHit; } - } - - public Key ActionKeyDown - { - get { return actionKeyDown; } - } - - public Key SecondaryKeyHit - { - get { return secondaryKeyHit; } - } - - public Key SecondaryKeyDown - { - get { return secondaryKeyDown; } - } - public AIController AIController { get { return aiController; } @@ -311,11 +288,19 @@ namespace Subsurface public Character(string file, Vector2 position, CharacterInfo characterInfo = null, bool isNetworkPlayer = false) { - selectKeyHit = new Key(false); - actionKeyDown = new Key(true); - actionKeyHit = new Key(false); - secondaryKeyHit = new Key(false); - secondaryKeyDown = new Key(true); + keys = new Key[Enum.GetNames(typeof(InputType)).Length]; + keys[(int)InputType.Select] = new Key(false); + keys[(int)InputType.ActionHeld] = new Key(true); + keys[(int)InputType.ActionHit] = new Key(false); + keys[(int)InputType.SecondaryHit] = new Key(false); + keys[(int)InputType.SecondaryHeld] = new Key(true); + + keys[(int)InputType.Left] = new Key(true); + keys[(int)InputType.Right] = new Key(true); + keys[(int)InputType.Up] = new Key(true); + keys[(int)InputType.Down] = new Key(true); + + keys[(int)InputType.Run] = new Key(true); selectedItems = new Item[2]; @@ -426,6 +411,19 @@ namespace Subsurface } } + public bool GetInputState(InputType inputType) + { + return keys[(int)inputType].State; + } + + public void ClearInputs() + { + foreach (Key key in keys) + { + key.State = false; + } + } + public override string ToString() { return (info != null && !string.IsNullOrWhiteSpace(info.Name)) ? info.Name : SpeciesName; @@ -477,6 +475,42 @@ namespace Subsurface { if (isDead) return; + Vector2 targetMovement = Vector2.Zero; + if (GetInputState(InputType.Left)) targetMovement.X -= 1.0f; + if (GetInputState(InputType.Right)) targetMovement.X += 1.0f; + if (GetInputState(InputType.Up)) targetMovement.Y += 1.0f; + if (GetInputState(InputType.Down)) targetMovement.Y -= 1.0f; + + //the vertical component is only used for falling through platforms and climbing ladders when not in water, + //so the movement can't be normalized or the character would walk slower when pressing down/up + if (AnimController.InWater) + { + float length = targetMovement.Length(); + if (length > 0.0f) targetMovement = targetMovement / length; + } + + if (Math.Sign(targetMovement.X) == Math.Sign(AnimController.Dir) && GetInputState(InputType.Run)) + targetMovement *= 3.0f; + + AnimController.TargetMovement = targetMovement; + AnimController.IsStanding = true; + + if (AnimController.onGround && + !AnimController.InWater && + AnimController.Anim != AnimController.Animation.UsingConstruction) + { + Limb head = AnimController.GetLimb(LimbType.Head); + + if (cursorPosition.X < head.Position.X - 10.0f) + { + AnimController.TargetDir = Direction.Left; + } + else if (cursorPosition.X > head.Position.X + 10.0f) + { + AnimController.TargetDir = Direction.Right; + } + } + //find the closest item if selectkey has been hit, or if the character is being //controlled by the player (in order to highlight it) if (controlled == this) @@ -487,7 +521,7 @@ namespace Subsurface if (closestItem != null) { closestItem.IsHighlighted = true; - if (selectKeyHit.State && closestItem.Pick(this, forcePick)) + if (GetInputState(InputType.Select) && closestItem.Pick(this, forcePick)) { new NetworkEvent(NetworkEventType.PickItem, ID, true, closestItem.ID); } @@ -497,7 +531,7 @@ namespace Subsurface if (closestCharacter != selectedCharacter) selectedCharacter = null; if (closestCharacter!=null) { - if (selectKeyHit.State) selectedCharacter = (selectedCharacter==null) ? closestCharacter : null; + if (GetInputState(InputType.Select)) selectedCharacter = (selectedCharacter == null) ? closestCharacter : null; } } @@ -506,23 +540,22 @@ namespace Subsurface if (selectedItems[i] == null) continue; if (i == 1 && selectedItems[0] == selectedItems[1]) continue; - if (actionKeyDown.State) selectedItems[i].Use(deltaTime, this); - if (secondaryKeyDown.State && selectedItems[i] != null) selectedItems[i].SecondaryUse(deltaTime, this); + if (GetInputState(InputType.ActionHeld)) selectedItems[i].Use(deltaTime, this); + if (GetInputState(InputType.SecondaryHeld) && selectedItems[i] != null) selectedItems[i].SecondaryUse(deltaTime, this); } if (selectedConstruction != null) { - if (actionKeyDown.State) selectedConstruction.Use(deltaTime, this); - if (secondaryKeyDown.State) selectedConstruction.SecondaryUse(deltaTime, this); + if (GetInputState(InputType.ActionHeld)) selectedConstruction.Use(deltaTime, this); + if (GetInputState(InputType.SecondaryHeld)) selectedConstruction.SecondaryUse(deltaTime, this); } if (IsNetworkPlayer) { - selectKeyHit.Reset(); - actionKeyHit.Reset(); - actionKeyDown.Reset(); - secondaryKeyHit.Reset(); - secondaryKeyDown.Reset(); + foreach (Key key in keys) + { + key.Reset(); + } } } @@ -574,44 +607,29 @@ namespace Subsurface Lights.LightManager.ViewPos = ConvertUnits.ToDisplayUnits(head.SimPosition); - Vector2 targetMovement = Vector2.Zero; - if (!DisableControls) { - if (PlayerInput.KeyDown(Keys.W)) targetMovement.Y += 1.0f; - if (PlayerInput.KeyDown(Keys.S)) targetMovement.Y -= 1.0f; - if (PlayerInput.KeyDown(Keys.A)) targetMovement.X -= 1.0f; - if (PlayerInput.KeyDown(Keys.D)) targetMovement.X += 1.0f; + keys[(int)InputType.Left].SetState(PlayerInput.KeyDown(Keys.A)); + keys[(int)InputType.Right].SetState(PlayerInput.KeyDown(Keys.D)); + keys[(int)InputType.Up].SetState(PlayerInput.KeyDown(Keys.W)); + keys[(int)InputType.Down].SetState(PlayerInput.KeyDown(Keys.S)); - //the vertical component is only used for falling through platforms and climbing ladders when not in water, - //so the movement can't be normalized or the character would walk slower when pressing down/up - if (AnimController.InWater) - { - float length = targetMovement.Length(); - if (length > 0.0f) targetMovement = targetMovement / length; - } + keys[(int)InputType.Select].SetState(PlayerInput.KeyHit(Keys.E)); + keys[(int)InputType.ActionHit].SetState(PlayerInput.LeftButtonClicked()); + keys[(int)InputType.ActionHeld].SetState(PlayerInput.GetMouseState.LeftButton == ButtonState.Pressed); + keys[(int)InputType.SecondaryHit].SetState(PlayerInput.RightButtonClicked()); + keys[(int)InputType.SecondaryHeld].SetState(PlayerInput.GetMouseState.RightButton == ButtonState.Pressed); - if (Keyboard.GetState().IsKeyDown(Keys.LeftShift) && Math.Sign(targetMovement.X) == Math.Sign(AnimController.Dir)) - targetMovement *= 3.0f; - - selectKeyHit.SetState(PlayerInput.KeyHit(Keys.E)); - actionKeyHit.SetState(PlayerInput.LeftButtonClicked()); - actionKeyDown.SetState(PlayerInput.GetMouseState.LeftButton == ButtonState.Pressed); - secondaryKeyHit.SetState(PlayerInput.RightButtonClicked()); - secondaryKeyDown.SetState(PlayerInput.GetMouseState.RightButton == ButtonState.Pressed); + keys[(int)InputType.Run].SetState(PlayerInput.KeyDown(Keys.LeftShift)); } else { - selectKeyHit.SetState(false); - actionKeyHit.SetState(false); - actionKeyDown.SetState(false); - secondaryKeyHit.SetState(false); - secondaryKeyDown.SetState(false); + foreach (Key key in keys) + { + key.SetState(false); + } } - AnimController.TargetMovement = targetMovement; - AnimController.IsStanding = true; - if (moveCam) { cam.TargetPos = ConvertUnits.ToDisplayUnits(AnimController.limbs[0].SimPosition); @@ -634,20 +652,6 @@ namespace Subsurface } } - if (AnimController.onGround && - !AnimController.InWater && - AnimController.Anim != AnimController.Animation.UsingConstruction) - { - if (mouseSimPos.X < head.SimPosition.X-1.0f) - { - AnimController.TargetDir = Direction.Left; - } - else if (mouseSimPos.X > head.SimPosition.X + 1.0f) - { - AnimController.TargetDir = Direction.Right; - } - } - DisableControls = false; } @@ -810,7 +814,7 @@ namespace Subsurface Color color = Color.Orange; - if (closestCharacter != null && closestCharacter.isDead) + if (closestCharacter != null && closestCharacter.isDead && closestCharacter.isHumanoid) { Vector2 startPos = Position + (closestCharacter.Position - Position) * 0.7f; startPos = cam.WorldToScreen(startPos); @@ -840,10 +844,10 @@ namespace Subsurface textPos.Y += 50.0f; foreach (ColoredText coloredText in closestItem.GetHUDTexts(Controlled)) { - textPos.X = startPos.X - GUI.Font.MeasureString(coloredText.text).X / 2; + textPos.X = startPos.X - GUI.Font.MeasureString(coloredText.Text).X / 2; - spriteBatch.DrawString(GUI.Font, coloredText.text, textPos, Color.Black); - spriteBatch.DrawString(GUI.Font, coloredText.text, textPos + new Vector2(1, -1), coloredText.color); + spriteBatch.DrawString(GUI.Font, coloredText.Text, textPos, Color.Black); + spriteBatch.DrawString(GUI.Font, coloredText.Text, textPos + new Vector2(1, -1), coloredText.Color); textPos.Y += 25; } @@ -919,13 +923,7 @@ namespace Subsurface Limb torso= AnimController.GetLimb(LimbType.Torso); if (torso == null) torso = AnimController.GetLimb(LimbType.Head); - Vector2 centerOfMass = Vector2.Zero; - foreach (Limb limb in AnimController.limbs) - { - centerOfMass += limb.Mass * limb.SimPosition; - } - - centerOfMass /= AnimController.Mass; + Vector2 centerOfMass = AnimController.GetCenterOfMass(); health = 0.0f; @@ -1022,34 +1020,44 @@ namespace Subsurface } else if (type == NetworkEventType.KillCharacter) { - return; - } - else if (type== NetworkEventType.NotMoving) - { - return; + return; } + var hasInputs = (GetInputState(InputType.Left) || + GetInputState(InputType.Right) || + GetInputState(InputType.Up) || + GetInputState(InputType.Down) || + GetInputState(InputType.ActionHeld) || + GetInputState(InputType.SecondaryHeld)); - //if (type == Networking.NetworkEventType.KeyHit) - //{ - // message.Write(selectKeyHit.Dequeue); - message.Write(actionKeyDown.Dequeue); - message.Write(secondaryKeyDown.Dequeue); - //} + message.Write(hasInputs || LargeUpdateTimer <= 0); message.Write((float)NetTime.Now); - // Write byte = move direction - message.WriteRangedSingle(MathHelper.Clamp(AnimController.TargetMovement.X, -10.0f, 10.0f), -10.0f, 10.0f, 8); - message.WriteRangedSingle(MathHelper.Clamp(AnimController.TargetMovement.Y, -10.0f, 10.0f), -10.0f, 10.0f, 8); + message.Write(keys[(int)InputType.ActionHeld].Dequeue); + message.Write(keys[(int)InputType.SecondaryHeld].Dequeue); + + message.Write(keys[(int)InputType.Left].Dequeue); + message.Write(keys[(int)InputType.Right].Dequeue); - message.Write(AnimController.TargetDir==Direction.Right); + message.Write(keys[(int)InputType.Up].Dequeue); + message.Write(keys[(int)InputType.Down].Dequeue); + + message.Write(keys[(int)InputType.Run].Dequeue); + + // Write byte = move direction + //message.WriteRangedSingle(MathHelper.Clamp(AnimController.TargetMovement.X, -10.0f, 10.0f), -10.0f, 10.0f, 8); + //message.WriteRangedSingle(MathHelper.Clamp(AnimController.TargetMovement.Y, -10.0f, 10.0f), -10.0f, 10.0f, 8); if (aiController==null) { message.Write(cursorPosition.X); message.Write(cursorPosition.Y); } + else + { + message.Write(AnimController.TargetDir == Direction.Right); + } message.Write(LargeUpdateTimer <= 0); @@ -1070,7 +1078,7 @@ namespace Subsurface } message.WriteRangedSingle(MathHelper.Clamp(AnimController.StunTimer,0.0f,60.0f), 0.0f, 60.0f, 8); - message.Write((byte)health); + message.Write((byte)((health/maxHealth)*255.0f)); if (aiController != null) aiController.FillNetworkData(message); @@ -1122,40 +1130,39 @@ namespace Subsurface } return; } - else if (type == NetworkEventType.NotMoving) - { - AnimController.TargetMovement = Vector2.Zero; - actionKeyDown.State = false; - secondaryKeyDown.State = false; - return; - } bool actionKeyState = false; bool secondaryKeyState = false; - float sendingTime = 0.0f; - Vector2 targetMovement = Vector2.Zero; + float sendingTime = 0.0f; + //Vector2 targetMovement = Vector2.Zero; bool targetDir = false; Vector2 cursorPos = Vector2.Zero; + bool leftKeyState = false, rightKeyState = false; + bool upKeyState = false, downKeyState = false; + + bool runState = false; + try { + bool hasInputs = message.ReadBoolean(); + if (!hasInputs) + { + ClearInputs(); + return; + } + + sendingTime = message.ReadFloat(); + actionKeyState = message.ReadBoolean(); secondaryKeyState = message.ReadBoolean(); - sendingTime = message.ReadFloat(); + leftKeyState = message.ReadBoolean(); + rightKeyState = message.ReadBoolean(); + upKeyState = message.ReadBoolean(); + downKeyState = message.ReadBoolean(); - targetMovement = new Vector2(message.ReadRangedSingle(-10.0f, 10.0f, 8), message.ReadRangedSingle(-10.0f, 10.0f, 8)); - targetMovement.X = MathUtils.Round(targetMovement.X, 0.1f); - targetMovement.Y = MathUtils.Round(targetMovement.Y, 0.1f); - - targetDir = message.ReadBoolean(); - - if (aiController==null) - { - cursorPos = new Vector2( - message.ReadFloat(), - message.ReadFloat()); - } + runState = message.ReadBoolean(); } catch @@ -1165,22 +1172,52 @@ namespace Subsurface AnimController.IsStanding = true; - actionKeyDown.State = actionKeyState; - secondaryKeyDown.State = secondaryKeyState; + keys[(int)InputType.ActionHeld].State = actionKeyState; + keys[(int)InputType.SecondaryHeld].State = secondaryKeyState; if (sendingTime <= LastNetworkUpdate) return; - - cursorPosition = cursorPos; - AnimController.TargetMovement= targetMovement; - AnimController.TargetDir = (targetDir) ? Direction.Right : Direction.Left; + keys[(int)InputType.Left].State = leftKeyState; + keys[(int)InputType.Right].State = rightKeyState; + + keys[(int)InputType.Up].State = upKeyState; + keys[(int)InputType.Down].State = downKeyState; + + keys[(int)InputType.Run].State = runState; + + try + { + if (aiController == null) + { + cursorPos = new Vector2( + message.ReadFloat(), + message.ReadFloat()); + } + else + { + targetDir = message.ReadBoolean(); + } + } + catch + { + return; + } + if (aiController == null) + { + cursorPosition = cursorPos; + } + else + { + AnimController.TargetDir = (targetDir) ? Direction.Right : Direction.Left; + } + if (message.ReadBoolean()) { foreach (Limb limb in AnimController.limbs) { Vector2 pos = Vector2.Zero, vel = Vector2.Zero; - float rotation = 0.0f, angularVel = 0.0f; + float rotation = 0.0f; try { @@ -1213,7 +1250,7 @@ namespace Subsurface try { newStunTimer = message.ReadRangedSingle(0.0f, 60.0f, 8); - newHealth = message.ReadByte(); + newHealth = (message.ReadByte() / 255.0f) * maxHealth; } catch { return; } diff --git a/Subsurface/Source/Characters/FishAnimController.cs b/Subsurface/Source/Characters/FishAnimController.cs index efc9059d1..789852c76 100644 --- a/Subsurface/Source/Characters/FishAnimController.cs +++ b/Subsurface/Source/Characters/FishAnimController.cs @@ -368,7 +368,7 @@ namespace Subsurface } } - float midX = (leftX + rightX) / 2.0f; + float midX = GetCenterOfMass().X; foreach (Limb l in limbs) { diff --git a/Subsurface/Source/Characters/HumanoidAnimController.cs b/Subsurface/Source/Characters/HumanoidAnimController.cs index 93f618a1c..3941c4aa3 100644 --- a/Subsurface/Source/Characters/HumanoidAnimController.cs +++ b/Subsurface/Source/Characters/HumanoidAnimController.cs @@ -9,6 +9,8 @@ namespace Subsurface { class HumanoidAnimController : AnimController { + private bool aiming; + public HumanoidAnimController(Character character, XElement element) : base(character, element) { @@ -144,6 +146,7 @@ namespace Subsurface limb.Disabled = false; } + aiming = false; } void UpdateStanding() @@ -398,7 +401,7 @@ namespace Subsurface rotation = MathHelper.ToDegrees(rotation); if (rotation < 0.0f) rotation += 360; - if (!character.IsNetworkPlayer) + if (!character.IsNetworkPlayer && !aiming) { if (rotation > 20 && rotation < 170) TargetDir = Direction.Left; @@ -413,12 +416,17 @@ namespace Subsurface //if trying to head to the opposite direction, apply torque //to the torso to flip the ragdoll around - if (Math.Sign(TargetMovement.X) != Dir && TargetMovement.X != 0.0f) - { - float torque = torso.Mass * 10.0f; - torque *= (rotation > 90 && rotation < 270) ? -Dir : Dir; + //if (Math.Sign(TargetMovement.X) != Dir && TargetMovement.X != 0.0f) + //{ + // float torque = torso.Mass * 10.0f; + // torque *= (rotation > 90 && rotation < 270) ? -Dir : Dir; - torso.body.ApplyTorque(torque); + // torso.body.ApplyTorque(torque); + //} + + if (targetSpeed > 0.1f && !aiming) + { + torso.body.SmoothRotate(MathUtils.VectorToAngle(TargetMovement)-MathHelper.PiOver2); } movement = MathUtils.SmoothStep(movement, TargetMovement, 0.3f); @@ -578,7 +586,7 @@ namespace Subsurface handPos = new Vector2( ladderSimPos.X, - head.SimPosition.Y + 0.5f + movement.Y * 0.1f - ladderSimPos.Y); + head.SimPosition.Y + 0.0f + movement.Y * 0.1f - ladderSimPos.Y); MoveLimb(leftHand, new Vector2(handPos.X, @@ -595,7 +603,7 @@ namespace Subsurface footPos = new Vector2( handPos.X - Dir*0.05f, - head.SimPosition.Y - stepHeight * 2.7f - ladderSimPos.Y); + head.SimPosition.Y - stepHeight * 2.7f - ladderSimPos.Y - 0.7f); //if (movement.Y < 0) footPos.Y += 0.05f; @@ -679,10 +687,10 @@ namespace Subsurface Limb rightHand = GetLimb(LimbType.RightHand); Limb rightArm = GetLimb(LimbType.RightArm); - Vector2 itemPos = character.SecondaryKeyDown.State ? aimPos : holdPos; + Vector2 itemPos = character.GetInputState(InputType.SecondaryHeld) ? aimPos : holdPos; float itemAngle; - if (character.SecondaryKeyDown.State && itemPos != Vector2.Zero) + if (character.GetInputState(InputType.SecondaryHeld) && itemPos != Vector2.Zero) { Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); @@ -697,9 +705,11 @@ namespace Subsurface if (TargetMovement == Vector2.Zero && inWater) { - torso.body.SmoothRotate(0.2f * Dir); + torso.body.AngularVelocity -= torso.body.AngularVelocity * 0.1f; torso.body.ApplyForce(torso.body.LinearVelocity * -0.5f); } + + aiming = true; } else { diff --git a/Subsurface/Source/Characters/Ragdoll.cs b/Subsurface/Source/Characters/Ragdoll.cs index 3187ed605..f8a22301e 100644 --- a/Subsurface/Source/Characters/Ragdoll.cs +++ b/Subsurface/Source/Characters/Ragdoll.cs @@ -8,6 +8,7 @@ using FarseerPhysics.Dynamics.Contacts; using FarseerPhysics.Dynamics.Joints; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using Subsurface.Networking; namespace Subsurface { @@ -39,7 +40,7 @@ namespace Subsurface //a movement vector that overrides targetmovement if trying to steer //a character to the position sent by server in multiplayer mode - public Vector2 correctionMovement; + private Vector2 correctionMovement; protected float floorY; protected float surfaceY; @@ -73,11 +74,7 @@ namespace Subsurface } set { - if (float.IsNaN(value.X) || float.IsNaN(value.Y)) - { - targetMovement = Vector2.Zero; - return; - } + if (!MathUtils.IsValid(value)) return; targetMovement.X = MathHelper.Clamp(value.X, -3.0f, 3.0f); targetMovement.Y = MathHelper.Clamp(value.Y, -3.0f, 3.0f); } @@ -408,10 +405,24 @@ namespace Subsurface new Vector2( -limbs[i].pullJoint.LocalAnchorA.X, limbs[i].pullJoint.LocalAnchorA.Y); - } - + } } + public Vector2 GetCenterOfMass() + { + Vector2 centerOfMass = Vector2.Zero; + foreach (Limb limb in limbs) + { + centerOfMass += limb.Mass * limb.SimPosition; + } + + centerOfMass /= Mass; + + return centerOfMass; + } + + + /// /// /// @@ -567,10 +578,10 @@ namespace Subsurface if (refLimb.body.TargetPosition == Vector2.Zero) return; //if the limb is further away than resetdistance, all limbs are immediately snapped to their targetpositions - float resetDistance = 1.5f; + float resetDistance = NetConfig.ResetRagdollDistance; - //if the limb is closer than alloweddistance, limb positions aren't updated - float allowedDistance = 0.1f; + //if the limb is closer than alloweddistance, just ignore the difference + float allowedDistance = NetConfig.AllowedRagdollDistance; float dist = Vector2.Distance(limbs[0].body.Position, refLimb.body.TargetPosition); bool resetAll = (dist > resetDistance && character.LargeUpdateTimer == 1); @@ -596,8 +607,7 @@ namespace Subsurface } else { - correctionMovement = Vector2.Normalize(newMovement) * ((targetMovement == Vector2.Zero) ? 1.0f : targetMovement.Length()); - + correctionMovement = Vector2.Normalize(newMovement) * Math.Min(1.0f + dist, 3.0f); } } @@ -627,13 +637,13 @@ namespace Subsurface foreach (MapEntity e in MapEntity.mapEntityList) { Gap gap = e as Gap; - if (gap == null || gap.FlowTargetHull!=currentHull ||gap.FlowForce == Vector2.Zero) continue; + if (gap == null || gap.FlowTargetHull != currentHull || gap.FlowForce == Vector2.Zero) continue; Vector2 gapPos = gap.SimPosition; float dist = Vector2.Distance(limbPos, gapPos); - force += Vector2.Normalize(gap.FlowForce)*(Math.Max(gap.FlowForce.Length() - dist, 0.0f)/1000.0f); + force += Vector2.Normalize(gap.FlowForce) * (Math.Max(gap.FlowForce.Length() - dist, 0.0f) / 500.0f); } if (force.Length() > 20.0f) return force; diff --git a/Subsurface/Source/ContentPackage.cs b/Subsurface/Source/ContentPackage.cs index 9a31a1bf3..d44fd3576 100644 --- a/Subsurface/Source/ContentPackage.cs +++ b/Subsurface/Source/ContentPackage.cs @@ -57,13 +57,14 @@ namespace Subsurface { XDocument doc = ToolBox.TryLoadXml(filePath); + Path = filePath; + if (doc==null) { DebugConsole.ThrowError("Couldn't load content package ''"+filePath+"''!"); return; } - Path = filePath; name = ToolBox.GetAttributeString(doc.Root, "name", ""); @@ -81,7 +82,7 @@ namespace Subsurface public static ContentPackage CreatePackage(string name) { - ContentPackage newPackage = new ContentPackage(); + ContentPackage newPackage = new ContentPackage("Content/Data/"+name); newPackage.name = name; newPackage.Path = Folder + name; list.Add(newPackage); @@ -150,8 +151,6 @@ namespace Subsurface } } - - //string str = sb.ToString(); byte[] bytes = new byte[hashes.Count()*16]; for (int i = 0; i < hashes.Count; i++ ) diff --git a/Subsurface/Source/CoroutineManager.cs b/Subsurface/Source/CoroutineManager.cs index ec47072c0..9b08261bf 100644 --- a/Subsurface/Source/CoroutineManager.cs +++ b/Subsurface/Source/CoroutineManager.cs @@ -6,7 +6,7 @@ using System.Text; namespace Subsurface { - enum Status + enum CoroutineStatus { Running, Success, Failure } @@ -24,6 +24,13 @@ namespace Subsurface Coroutines.Add(func.GetEnumerator()); } + public static void StopCoroutine(string name) + { + IEnumerator coroutine = Coroutines.FirstOrDefault(c => c.ToString().Contains(name)); + + if (coroutine != null) Coroutines.Remove(coroutine); + } + // Updating just means stepping through all the coroutines public static void Update(float deltaTime) { @@ -39,12 +46,12 @@ namespace Subsurface } else { - switch ((Status)Coroutines[i].Current) + switch ((CoroutineStatus)Coroutines[i].Current) { - case Status.Success: + case CoroutineStatus.Success: Coroutines.RemoveAt(i); continue; - case Status.Failure: + case CoroutineStatus.Failure: DebugConsole.ThrowError("Coroutine ''" + Coroutines[i]+ "'' has failed"); break; } diff --git a/Subsurface/Source/DebugConsole.cs b/Subsurface/Source/DebugConsole.cs index edf99ab91..3b5477908 100644 --- a/Subsurface/Source/DebugConsole.cs +++ b/Subsurface/Source/DebugConsole.cs @@ -10,13 +10,17 @@ namespace Subsurface { struct ColoredText { - public string text; - public Color color; + public string Text; + public Color Color; + + public readonly string Time; public ColoredText(string text, Color color) { - this.text = text; - this.color = color; + this.Text = text; + this.Color = color; + + Time = DateTime.Now.ToString(); } } @@ -96,7 +100,7 @@ namespace Subsurface if (selectedIndex < 0) selectedIndex = messageCount - 1; selectedIndex = selectedIndex % messageCount; - textBox.Text = messages[selectedIndex].text; + textBox.Text = messages[selectedIndex].Text; } public static void Draw(SpriteBatch spriteBatch) @@ -127,7 +131,7 @@ namespace Subsurface Vector2 messagePos = new Vector2(x + margin * 2, y + height - 70 - messages.Count()*20); foreach (ColoredText message in messages) { - spriteBatch.DrawString(GUI.Font, message.text, messagePos, message.color); + spriteBatch.DrawString(GUI.Font, message.Text, messagePos, message.Color); messagePos.Y += 20; } @@ -227,6 +231,16 @@ namespace Subsurface it.Condition = 100.0f; } break; + case "fixhull": + case "fixwalls": + foreach (Structure w in Structure.wallList) + { + for (int i = 0 ; i < w.SectionCount; i++) + { + w.AddDamage(i, -100000.0f); + } + } + break; case "shake": Game1.GameScreen.Cam.Shake = 10.0f; break; diff --git a/Subsurface/Source/EventInput/EventInput.cs b/Subsurface/Source/EventInput/EventInput.cs index 402be3fd4..62bb51bf7 100644 --- a/Subsurface/Source/EventInput/EventInput.cs +++ b/Subsurface/Source/EventInput/EventInput.cs @@ -157,16 +157,19 @@ namespace EventInput throw new InvalidOperationException("TextInput.Initialize can only be called once!"); hookProcDelegate = HookProc; - prevWndProc = (IntPtr)SetWindowLong(window.Handle, GWL_WNDPROC, - (int)Marshal.GetFunctionPointerForDelegate(hookProcDelegate)); +#if WINDOWS + prevWndProc = (IntPtr)SetWindowLong(window.Handle, GWL_WNDPROC, + (int)Marshal.GetFunctionPointerForDelegate(hookProcDelegate)); + + hIMC = ImmGetContext(window.Handle); +#endif - hIMC = ImmGetContext(window.Handle); initialized = true; } static IntPtr HookProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { - IntPtr returnCode = CallWindowProc(prevWndProc, hWnd, msg, wParam, lParam); + IntPtr returnCode = CallWindowProc(prevWndProc, hWnd, msg, wParam, lParam); switch (msg) { @@ -200,7 +203,10 @@ namespace EventInput break; } + + return returnCode; + } } diff --git a/Subsurface/Source/Events/MonsterEvent.cs b/Subsurface/Source/Events/MonsterEvent.cs index 0b2981616..906a6eb2e 100644 --- a/Subsurface/Source/Events/MonsterEvent.cs +++ b/Subsurface/Source/Events/MonsterEvent.cs @@ -18,10 +18,10 @@ namespace Subsurface characterFile = ToolBox.GetAttributeString(element, "characterfile", ""); minAmount = ToolBox.GetAttributeInt(element, "minamount", 1); - maxAmount = Math.Max(ToolBox.GetAttributeInt(element, "maxamount", 1),minAmount); + maxAmount = Math.Max(ToolBox.GetAttributeInt(element, "maxamount", 1), minAmount); } - protected override void Start() + private void SpawnMonsters() { WayPoint randomWayPoint = WayPoint.GetRandom(SpawnType.Enemy); @@ -40,22 +40,26 @@ namespace Subsurface public override void Update(float deltaTime) { - base.Update(deltaTime); + if (monsters == null) SpawnMonsters(); - if (!isStarted) return; - - if (!isFinished) + //base.Update(deltaTime); + + //if (!isStarted) return; + + if (isFinished) return; + + bool monstersDead = true; + for (int i = 0; i < monsters.Length; i++) { - bool monstersDead = true; - for (int i = 0; i < monsters.Length; i++) - { - if (monsters[i].IsDead) continue; + if (monsters[i].IsDead) continue; + + if (!isStarted && monsters[i].SimPosition != Vector2.Zero && monsters[i].SimPosition.Length() < 20.0) isStarted = true; - monstersDead = false; - break; - } - if (monstersDead) Finished(); + monstersDead = false; + break; } + if (monstersDead) Finished(); + } } } diff --git a/Subsurface/Source/Events/Quests/MonsterQuest.cs b/Subsurface/Source/Events/Quests/MonsterQuest.cs index 37f049dc1..f88110803 100644 --- a/Subsurface/Source/Events/Quests/MonsterQuest.cs +++ b/Subsurface/Source/Events/Quests/MonsterQuest.cs @@ -32,7 +32,7 @@ namespace Subsurface public override void Start(Level level) { - Vector2 position = level.PositionsOfInterest[Rand.Int(level.PositionsOfInterest.Count)]; + Vector2 position = level.PositionsOfInterest[Rand.Int(level.PositionsOfInterest.Count, false)]; monster = new Character(monsterFile, ConvertUnits.ToSimUnits(position+level.Position)); } diff --git a/Subsurface/Source/Events/Quests/Quest.cs b/Subsurface/Source/Events/Quests/Quest.cs index bf8b27fc7..d877e35e9 100644 --- a/Subsurface/Source/Events/Quests/Quest.cs +++ b/Subsurface/Source/Events/Quests/Quest.cs @@ -104,7 +104,7 @@ namespace Subsurface try { - t = Type.GetType("Subsurface." + type + ", Subsurface", true, true); + t = Type.GetType("Subsurface." + type, true, true); if (t == null) { DebugConsole.ThrowError("Error in " + configFile + "! Could not find a quest class of the type ''" + type + "''."); diff --git a/Subsurface/Source/Events/ScriptedEvent.cs b/Subsurface/Source/Events/ScriptedEvent.cs index 18575fc93..a216ce276 100644 --- a/Subsurface/Source/Events/ScriptedEvent.cs +++ b/Subsurface/Source/Events/ScriptedEvent.cs @@ -137,7 +137,7 @@ namespace Subsurface try { - t = Type.GetType("Subsurface." + type + ", Subsurface", true, true); + t = Type.GetType("Subsurface." + type, true, true); if (t == null) { DebugConsole.ThrowError("Error in " + configFile + "! Could not find an event class of the type ''" + type + "''."); diff --git a/Subsurface/Source/Events/ScriptedTask.cs b/Subsurface/Source/Events/ScriptedTask.cs index 996b2ab5c..2fb257261 100644 --- a/Subsurface/Source/Events/ScriptedTask.cs +++ b/Subsurface/Source/Events/ScriptedTask.cs @@ -6,6 +6,11 @@ private bool prevStarted; + public override bool IsStarted + { + get { return scriptedEvent.IsStarted; } + } + public ScriptedTask(ScriptedEvent scriptedEvent) : base(scriptedEvent.Difficulty, scriptedEvent.Name) { diff --git a/Subsurface/Source/Events/Task.cs b/Subsurface/Source/Events/Task.cs index 004245bdd..a20c89432 100644 --- a/Subsurface/Source/Events/Task.cs +++ b/Subsurface/Source/Events/Task.cs @@ -34,6 +34,11 @@ namespace Subsurface get { return isFinished; } } + public virtual bool IsStarted + { + get { return true; } + } + public Task(float priority, string name) { if (Game1.GameSession==null || Game1.GameSession.taskManager == null) return; diff --git a/Subsurface/Source/Events/TaskManager.cs b/Subsurface/Source/Events/TaskManager.cs index 6474414ac..6600683d1 100644 --- a/Subsurface/Source/Events/TaskManager.cs +++ b/Subsurface/Source/Events/TaskManager.cs @@ -11,7 +11,7 @@ namespace Subsurface private List tasks; - private GUIListBox taskListBox; + //private GUIListBox taskListBox; public List Tasks { @@ -34,7 +34,7 @@ namespace Subsurface { tasks = new List(); - taskListBox = new GUIListBox(new Rectangle(Game1.GraphicsWidth - 250, 50, 250, 500), Color.Transparent); + //taskListBox = new GUIListBox(new Rectangle(Game1.GraphicsWidth - 250, 50, 250, 500), Color.Transparent); //taskListBox.ScrollBarEnabled = false; //taskListBox.Padding = GUI.style.smallPadding; } @@ -50,24 +50,24 @@ namespace Subsurface { CreateScriptedEvents(level); - taskListBox.ClearChildren(); + //taskListBox.ClearChildren(); } public void EndShift() { - taskListBox.ClearChildren(); + //taskListBox.ClearChildren(); tasks.Clear(); } private void CreateScriptedEvents(Level level) { - Random rand = new Random(level.Seed.GetHashCode()); + Random rand = new Random(ToolBox.StringToInt(level.Seed)); float totalDifficulty = level.Difficulty; int tries = 0; - do + while (tries < 5) { ScriptedEvent scriptedEvent = ScriptedEvent.LoadRandom(rand); if (scriptedEvent==null || scriptedEvent.Difficulty > totalDifficulty) @@ -79,36 +79,36 @@ namespace Subsurface AddTask(new ScriptedTask(scriptedEvent)); totalDifficulty -= scriptedEvent.Difficulty; tries = 0; - } while (tries < 5); + } } public void TaskStarted(Task task) { - Color color = Color.Red; - int width = 250; - if (task.Priority < 30.0f) - { - width = 200; - color = Color.Yellow; - } - else if (task.Priority < 60) - { - width = 220; - color = Color.Orange; - } + //Color color = Color.Red; + //int width = 250; + //if (task.Priority < 30.0f) + //{ + // width = 200; + // color = Color.Yellow; + //} + //else if (task.Priority < 60) + //{ + // width = 220; + // color = Color.Orange; + //} - GUIFrame frame = new GUIFrame(new Rectangle(0,0,width,40), Color.Transparent, Alignment.Right, null, taskListBox); - frame.UserData = task; - frame.Padding = new Vector4(0.0f, 5.0f, 5.0f, 5.0f); + //GUIFrame frame = new GUIFrame(new Rectangle(0,0,width,40), Color.Transparent, Alignment.Right, null, taskListBox); + //frame.UserData = task; + //frame.Padding = new Vector4(0.0f, 5.0f, 5.0f, 5.0f); - GUIFrame colorFrame = new GUIFrame(new Rectangle(0, 0, 0, 0), color * 0.5f, Alignment.Right, null, frame); - GUITextBlock textBlock = new GUITextBlock(new Rectangle(5, 5, 0, 20), task.Name, Color.Transparent, Color.Black, Alignment.Right, null, colorFrame); + //GUIFrame colorFrame = new GUIFrame(new Rectangle(0, 0, 0, 0), color * 0.5f, Alignment.Right, null, frame); + //GUITextBlock textBlock = new GUITextBlock(new Rectangle(5, 5, 0, 20), task.Name, Color.Transparent, Color.Black, Alignment.Right, null, colorFrame); //textBlock.Padding = new Vector4(10.0f, 10.0f, 0.0f, 0.0f); //colorFrame.AddChild(textBlock); - taskListBox.children.Sort((x, y) => ((Task)y.UserData).Priority.CompareTo(((Task)x.UserData).Priority)); + //taskListBox.children.Sort((x, y) => ((Task)y.UserData).Priority.CompareTo(((Task)x.UserData).Priority)); } public void TaskFinished(Task task) @@ -121,23 +121,22 @@ namespace Subsurface public void Update(float deltaTime) { Task removeTask = null; - GUIComponent removeComponent = null; foreach (Task task in tasks) { if (task.IsFinished) { - foreach (GUIComponent comp in taskListBox.children) - { - if (comp.UserData as Task != task) continue; - comp.Rect = new Rectangle(comp.Rect.X, comp.Rect.Y, comp.Rect.Width, comp.Rect.Height - 1); - comp.children[0].ClearChildren(); - if (comp.Rect.Height < 1) - { - removeComponent = comp; + //foreach (GUIComponent comp in taskListBox.children) + //{ + // if (comp.UserData as Task != task) continue; + // comp.Rect = new Rectangle(comp.Rect.X, comp.Rect.Y, comp.Rect.Width, comp.Rect.Height - 1); + // comp.children[0].ClearChildren(); + // if (comp.Rect.Height < 1) + // { + // removeComponent = comp; removeTask = task; - } - break; - } + // } + // break; + //} } else @@ -146,18 +145,14 @@ namespace Subsurface } } - if (removeComponent!= null) + if (removeTask!= null) { - taskListBox.RemoveChild(removeComponent); + //taskListBox.RemoveChild(removeComponent); tasks.Remove(removeTask); } //endShiftButton.Enabled = finished || Game1.server!=null; } - public void Draw(SpriteBatch spriteBatch) - { - taskListBox.Draw(spriteBatch); - } } } diff --git a/Subsurface/Source/GUI/GUIMessage.cs b/Subsurface/Source/GUI/GUIMessage.cs index 532390eaf..6d3274988 100644 --- a/Subsurface/Source/GUI/GUIMessage.cs +++ b/Subsurface/Source/GUI/GUIMessage.cs @@ -18,12 +18,12 @@ namespace Subsurface public string Text { - get { return coloredText.text; } + get { return coloredText.Text; } } public Color Color { - get { return coloredText.color; } + get { return coloredText.Color; } } public Vector2 Pos diff --git a/Subsurface/Source/GUI/GUIMessageBox.cs b/Subsurface/Source/GUI/GUIMessageBox.cs index c56cda83f..d4d9dd0ed 100644 --- a/Subsurface/Source/GUI/GUIMessageBox.cs +++ b/Subsurface/Source/GUI/GUIMessageBox.cs @@ -15,6 +15,11 @@ namespace Subsurface //GUIFrame frame; public GUIButton[] Buttons; + public static GUIMessageBox VisibleBox + { + get { return MessageBoxes.Count==0 ? null : MessageBoxes.Peek(); } + } + public string Text { get { return (children[1] as GUITextBlock).Text; } @@ -63,8 +68,8 @@ namespace Subsurface public bool Close(GUIButton button, object obj) { if (parent != null) parent.RemoveChild(this); + if (MessageBoxes.Contains(this)) MessageBoxes.Dequeue(); - MessageBoxes.Dequeue(); return true; } } diff --git a/Subsurface/Source/Game1.cs b/Subsurface/Source/Game1.cs index e6562c43a..9ffae0f04 100644 --- a/Subsurface/Source/Game1.cs +++ b/Subsurface/Source/Game1.cs @@ -174,42 +174,42 @@ namespace Subsurface Hull.renderer = new WaterRenderer(GraphicsDevice); loadState = 1.0f; - yield return Status.Running; + yield return CoroutineStatus.Running; GUI.LoadContent(GraphicsDevice); loadState = 2.0f; - yield return Status.Running; + yield return CoroutineStatus.Running; MapEntityPrefab.Init(); loadState = 10.0f; - yield return Status.Running; + yield return CoroutineStatus.Running; JobPrefab.LoadAll(SelectedPackage.GetFilesOfType(ContentType.Jobs)); loadState = 15.0f; - yield return Status.Running; + yield return CoroutineStatus.Running; StructurePrefab.LoadAll(SelectedPackage.GetFilesOfType(ContentType.Structure)); loadState = 25.0f; - yield return Status.Running; + yield return CoroutineStatus.Running; ItemPrefab.LoadAll(SelectedPackage.GetFilesOfType(ContentType.Item)); loadState = 40.0f; - yield return Status.Running; + yield return CoroutineStatus.Running; Debug.WriteLine("sounds"); CoroutineManager.StartCoroutine(AmbientSoundManager.Init()); loadState = 70.0f; - yield return Status.Running; + yield return CoroutineStatus.Running; GameModePreset.Init(); Submarine.Preload("Content/SavedMaps"); loadState = 80.0f; - yield return Status.Running; + yield return CoroutineStatus.Running; GameScreen = new GameScreen(Graphics.GraphicsDevice); loadState = 90.0f; - yield return Status.Running; + yield return CoroutineStatus.Running; MainMenuScreen = new MainMenuScreen(this); LobbyScreen = new LobbyScreen(); @@ -220,21 +220,21 @@ namespace Subsurface EditMapScreen = new EditMapScreen(); EditCharacterScreen = new EditCharacterScreen(); - yield return Status.Running; + yield return CoroutineStatus.Running; ParticleManager = new ParticleManager("Content/Particles/ParticlePrefabs.xml", Cam); - yield return Status.Running; + yield return CoroutineStatus.Running; GUIComponent.Init(Window); DebugConsole.Init(Window); - yield return Status.Running; + yield return CoroutineStatus.Running; LocationType.Init("Content/Map/locationTypes.xml"); MainMenuScreen.Select(); - yield return Status.Running; + yield return CoroutineStatus.Running; loadState = 100.0f; - yield return Status.Success; + yield return CoroutineStatus.Success; } /// diff --git a/Subsurface/Source/GameSession/GameModes/QuestMode.cs b/Subsurface/Source/GameSession/GameModes/QuestMode.cs index cb2dc4592..66257d1a5 100644 --- a/Subsurface/Source/GameSession/GameModes/QuestMode.cs +++ b/Subsurface/Source/GameSession/GameModes/QuestMode.cs @@ -23,7 +23,7 @@ namespace Subsurface { Location[] locations = new Location[2]; - Random rand = new Random(Game1.NetLobbyScreen.LevelSeed.GetHashCode()); + Random rand = new Random(ToolBox.StringToInt(Game1.NetLobbyScreen.LevelSeed)); for (int i = 0; i < 2; i++) { diff --git a/Subsurface/Source/GameSession/GameModes/TutorialMode.cs b/Subsurface/Source/GameSession/GameModes/TutorialMode.cs index dd82ff8d9..67dec322a 100644 --- a/Subsurface/Source/GameSession/GameModes/TutorialMode.cs +++ b/Subsurface/Source/GameSession/GameModes/TutorialMode.cs @@ -69,7 +69,17 @@ namespace Subsurface { base.Update(deltaTime); + if (Character.Controlled!=null && Character.Controlled.IsDead) + { + Character.Controlled = null; + + CoroutineManager.StopCoroutine("TutorialMode.UpdateState"); + infoBox = null; + CoroutineManager.StartCoroutine(Dead()); + } + CrewManager.Update(deltaTime); + if (infoBox!=null) infoBox.Update(deltaTime); } @@ -90,7 +100,7 @@ namespace Subsurface while (!tutorialDoor.IsOpen) { - yield return Status.Running; + yield return CoroutineStatus.Running; } yield return new WaitForSeconds(2.0f); @@ -103,14 +113,14 @@ namespace Subsurface while (Vector2.Distance(Character.Controlled.Position, reactor.Item.Position)>200.0f) { - yield return Status.Running; + yield return CoroutineStatus.Running; } infoBox = CreateInfoFrame("Select the reactor by walking next to it and pressing E."); while (Character.Controlled.SelectedConstruction != reactor.Item) { - yield return Status.Running; + yield return CoroutineStatus.Running; } yield return new WaitForSeconds(0.5f); @@ -118,7 +128,7 @@ namespace Subsurface while (reactor.FissionRate <= 0.0f) { - yield return Status.Running; + yield return CoroutineStatus.Running; } yield return new WaitForSeconds(0.5f); @@ -128,7 +138,7 @@ namespace Subsurface while (Math.Abs(reactor.ShutDownTemp-5000.0f) > 400.0f) { - yield return Status.Running; + yield return CoroutineStatus.Running; } yield return new WaitForSeconds(0.5f); @@ -138,7 +148,7 @@ namespace Subsurface while (!reactor.AutoTemp) { - yield return Status.Running; + yield return CoroutineStatus.Running; } yield return new WaitForSeconds(0.5f); @@ -150,14 +160,14 @@ namespace Subsurface while (Vector2.Distance(Character.Controlled.Position, steering.Item.Position) > 150.0f) { - yield return Status.Running; + yield return CoroutineStatus.Running; } infoBox = CreateInfoFrame("Select the navigation terminal by walking next to it and pressing E."); while (Character.Controlled.SelectedConstruction != steering.Item) { - yield return Status.Running; + yield return CoroutineStatus.Running; } yield return new WaitForSeconds(0.5f); @@ -168,7 +178,7 @@ namespace Subsurface while (Character.Controlled.SelectedConstruction == steering.Item) { - yield return Status.Running; + yield return CoroutineStatus.Running; } yield return new WaitForSeconds(1.0f); @@ -178,7 +188,7 @@ namespace Subsurface while (Character.Controlled.SelectedConstruction != steering.Item || Character.Controlled.SelectedItems.FirstOrDefault(i => i != null && i.Name == "Screwdriver") == null) { - yield return Status.Running; + yield return CoroutineStatus.Running; } @@ -188,7 +198,7 @@ namespace Subsurface while (!HasItem("Wire")) { - yield return Status.Running; + yield return CoroutineStatus.Running; } infoBox = CreateInfoFrame("Head back to the navigation terminal to fix the wiring."); @@ -199,7 +209,7 @@ namespace Subsurface Character.Controlled.SelectedConstruction != steering.Item) || Character.Controlled.SelectedItems.FirstOrDefault(i => i != null && i.Name == "Screwdriver") == null) { - yield return Status.Running; + yield return CoroutineStatus.Running; } if (Character.Controlled.SelectedItems.FirstOrDefault(i => i != null && i.GetComponent()!=null) == null) @@ -208,7 +218,7 @@ namespace Subsurface while (Character.Controlled.SelectedItems.FirstOrDefault(i => i != null && i.GetComponent() != null) == null) { - yield return Status.Running; + yield return CoroutineStatus.Running; } } @@ -219,7 +229,7 @@ namespace Subsurface while (steeringConnection.Wires.FirstOrDefault(w => w != null) == null) { - yield return Status.Running; + yield return CoroutineStatus.Running; } @@ -228,7 +238,7 @@ namespace Subsurface while (Character.Controlled.SelectedConstruction!=null) { - yield return Status.Running; + yield return CoroutineStatus.Running; } yield return new WaitForSeconds(2.0f); @@ -239,7 +249,7 @@ namespace Subsurface while (radar.Voltage<0.1f) { - yield return Status.Running; + yield return CoroutineStatus.Running; } infoBox = CreateInfoFrame("Great! Now we should be able to get moving."); @@ -247,23 +257,23 @@ namespace Subsurface while (Character.Controlled.SelectedConstruction != steering.Item) { - yield return Status.Running; + yield return CoroutineStatus.Running; } infoBox = CreateInfoFrame("You can take a look at the area around the sub by pressing ''Activate Radar''."); while (!radar.IsActive) { - yield return Status.Running; + yield return CoroutineStatus.Running; } yield return new WaitForSeconds(0.5f); infoBox = CreateInfoFrame("The white box in the middle is the submarine, and the white lines outside it are the walls of an underwater cavern. " + "Try moving the submarine by clicking somewhere inside the rectangle and draggind the pointer to the direction you want to go to."); - while (steering.CurrTargetVelocity == Vector2.Zero && steering.CurrTargetVelocity.Length() < 40.0f) + while (steering.CurrTargetVelocity == Vector2.Zero && steering.CurrTargetVelocity.Length() < 50.0f) { - yield return Status.Running; + yield return CoroutineStatus.Running; } yield return new WaitForSeconds(4.0f); @@ -276,12 +286,14 @@ namespace Subsurface while (Submarine.Loaded.Position.Y > 31000.0f) { - yield return Status.Running; + yield return CoroutineStatus.Running; } - var moloch = new Character("Content/Characters/Moloch/moloch.xml", steering.Item.SimPosition + Vector2.UnitX * 15.0f); + var moloch = new Character("Content/Characters/Moloch/moloch.xml", steering.Item.SimPosition + Vector2.UnitX * 25.0f); moloch.PlaySound(AIController.AiState.Attack); + yield return new WaitForSeconds(1.0f); + infoBox = CreateInfoFrame("Uh-oh... Something enormous just appeared on the radar."); List windows = new List(); @@ -295,13 +307,15 @@ namespace Subsurface bool broken = false; do { + Submarine.Loaded.Speed = Vector2.Zero; + moloch.AIController.SelectTarget(steering.Item.CurrentHull.AiTarget); Vector2 steeringDir = windows[0].Position - moloch.Position; if (steeringDir != Vector2.Zero) steeringDir = Vector2.Normalize(steeringDir); foreach (Limb limb in moloch.AnimController.limbs) { - limb.body.LinearVelocity = new Vector2(limb.LinearVelocity.X, limb.LinearVelocity.Y + steeringDir.Y*0.01f); + limb.body.LinearVelocity = new Vector2(limb.LinearVelocity.X, limb.LinearVelocity.Y + steeringDir.Y*10.0f); } moloch.AIController.Steering = steeringDir; @@ -310,7 +324,7 @@ namespace Subsurface { for (int i = 0; i < window.SectionCount; i++) { - if (!window.SectionHasHole(i)) continue; + if (!window.SectionIsLeaking(i)) continue; broken = true; break; } @@ -336,17 +350,17 @@ namespace Subsurface Door commandDoor2 = Item.itemList.Find(i => i.HasTag("commanddoor2")).GetComponent(); Door commandDoor3 = Item.itemList.Find(i => i.HasTag("commanddoor3")).GetComponent(); - while (commandDoor1.IsOpen && (commandDoor2.IsOpen || commandDoor3.IsOpen)) + while (commandDoor1.IsOpen || (commandDoor2.IsOpen || commandDoor3.IsOpen)) { - yield return Status.Running; + yield return CoroutineStatus.Running; } - infoBox = CreateInfoFrame("Great! You should find yourself an diving mask or a diving suit, in case the creature causes more damage. "+ + infoBox = CreateInfoFrame("You should find yourself an diving mask or a diving suit, in case the creature causes more damage. "+ "There are some in the room next to the airlock."); while (!HasItem("Diving Mask") && !HasItem("Diving Suit")) { - yield return Status.Running; + yield return CoroutineStatus.Running; } if (HasItem("Diving Mask")) @@ -364,7 +378,7 @@ namespace Subsurface while (!HasItem("Oxygen Tank")) { - yield return Status.Running; + yield return CoroutineStatus.Running; } yield return new WaitForSeconds(5.0f); @@ -393,23 +407,153 @@ namespace Subsurface var loader = Item.itemList.Find(i => i.Name == "Railgun Loader").GetComponent(); - while (Math.Abs(Character.Controlled.Position.Y - loader.Item.Position.Y)>50) + while (Math.Abs(Character.Controlled.Position.Y - loader.Item.Position.Y)>80) { - yield return Status.Running; + yield return CoroutineStatus.Running; } infoBox = CreateInfoFrame("Grab one of the shells. You can load it by selecting the railgun loader and dragging the shell to. " - +"one of the free slots."); + +"one of the free slots. You need two hands to carry a shell, so make sure you don't have anything else in either hand."); - while (loader.Item.ContainedItems.FirstOrDefault(i => i != null) != null) + while (loader.Item.ContainedItems.FirstOrDefault(i => i != null && i.Name == "Railgun Shell") == null) { - capacitor1.Charge += 1.0f; - capacitor2.Charge += 1.0f; - yield return Status.Running; + moloch.Health = 50.0f; + + capacitor1.Charge += 5.0f; + capacitor2.Charge += 5.0f; + yield return CoroutineStatus.Running; } + infoBox = CreateInfoFrame("Now we're ready to shoot! Select the railgun controller."); - yield return Status.Success; + while (Character.Controlled.SelectedConstruction == null || Character.Controlled.SelectedConstruction.Name != "Railgun Controller") + { + yield return CoroutineStatus.Running; + } + + infoBox = CreateInfoFrame("Use the right mouse button to aim and left to shoot."); + + while (!moloch.IsDead) + { + yield return CoroutineStatus.Running; + } + + infoBox = CreateInfoFrame("The creature has died. Now you should fix the damages in the control room: "+ + "Grab a welding tool from the closet in the railgun room."); + + while (!HasItem("Welding Tool")) + { + yield return CoroutineStatus.Running; + } + + infoBox = CreateInfoFrame("The welding tool requires fuel to work. Grab a welding fuel tank and attach it to the tool "+ + "by dragging it into the same slot."); + + do + { + var weldingTool = Character.Controlled.Inventory.items.FirstOrDefault(i => i != null && i.Name == "Welding Tool"); + if (weldingTool != null && + weldingTool.ContainedItems.FirstOrDefault(contained => contained != null && contained.Name == "Welding Fuel Tank") != null) break; + + yield return CoroutineStatus.Running; + } while (true); + + + infoBox = CreateInfoFrame("You can aim with the tool using the right mouse button and weld using the left button. "+ + "Head to the command room to fix the leaks there."); + + do + { + broken = false; + foreach (Structure window in windows) + { + for (int i = 0; i < window.SectionCount; i++) + { + if (!window.SectionIsLeaking(i)) continue; + broken = true; + break; + } + if (broken) break; + } + + yield return new WaitForSeconds(1.0f); + } while (broken); + + infoBox = CreateInfoFrame("Great! However, there's still quite a bit of water inside the sub. It should be pumped out " + +"using the pump in the room at the bottom of the submarine."); + + Pump pump = Item.itemList.Find(i => i.HasTag("tutorialpump")).GetComponent(); + + while (Vector2.Distance(Character.Controlled.Position, pump.Item.Position) > 100.0f) + { + yield return CoroutineStatus.Running; + } + + infoBox = CreateInfoFrame("The two pumps inside the ballast tanks " + +"are connected straight to the navigation terminal and can't be manually controlled unless you mess with their wiring, "+ + "so you should only use the pump in the middle room to pump out the water. Select it, turn it on and adjust the pumping speed "+ + "to start pumping water out."); + + while (pump.Item.CurrentHull.Volume>1000.0f) + { + yield return CoroutineStatus.Running; + } + + infoBox = CreateInfoFrame("That was all there is to this tutorial! Now you should be able to handle "+ + "most of the basic tasks on board the submarine."); + + yield return new WaitForSeconds(4.0f); + + float endPreviewLength = 10.0f; + + DateTime endTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, (int)(1000.0f * endPreviewLength)); + float secondsLeft = endPreviewLength; + + Character.Controlled = null; + + do + { + secondsLeft = (float)(endTime - DateTime.Now).TotalSeconds; + + float camAngle = (float)((DateTime.Now - endTime).TotalSeconds / endPreviewLength) * MathHelper.TwoPi; + Vector2 offset = (new Vector2( + (float)Math.Cos(camAngle) * (Submarine.Borders.Width / 2.0f), + (float)Math.Sin(camAngle) * (Submarine.Borders.Height / 2.0f))); + + Game1.GameScreen.Cam.TargetPos = offset * 0.8f; + //Game1.GameScreen.Cam.MoveCamera((float)deltaTime); + + yield return CoroutineStatus.Running; + } while (secondsLeft > 0.0f); + + Submarine.Unload(); + Game1.MainMenuScreen.Select(); + + yield return CoroutineStatus.Success; + } + + private IEnumerable Dead() + { + yield return new WaitForSeconds(3.0f); + + var messageBox = new GUIMessageBox("You have died", "Do you want to try again?", new string[] { "Yes", "No" }); + + messageBox.Buttons[0].OnClicked += Restart; + messageBox.Buttons[0].OnClicked += messageBox.Close; + + messageBox.Buttons[1].UserData = MainMenuScreen.Tabs.Main; + messageBox.Buttons[1].OnClicked = Game1.MainMenuScreen.SelectTab; + messageBox.Buttons[1].OnClicked += messageBox.Close; + + + yield return CoroutineStatus.Success; + } + + private bool Restart(GUIButton button, object obj) + { + TutorialMode.Start(); + + return true; } private bool HasItem(string itemName) @@ -425,6 +569,10 @@ namespace Subsurface { do { + enemy.Health = 50.0f; + + enemy.AIController.State = AIController.AiState.None; + Vector2 targetPos = Character.Controlled.Position + new Vector2(0.0f, 3000.0f); Vector2 steering = targetPos - enemy.Position; @@ -432,10 +580,10 @@ namespace Subsurface enemy.AIController.Steering = steering*2.0f; - yield return Status.Running; + yield return CoroutineStatus.Running; } while (capacitors.FirstOrDefault(c => c.Charge > 0.4f) == null); - yield return Status.Success; + yield return CoroutineStatus.Success; } public override void Draw(SpriteBatch spriteBatch) diff --git a/Subsurface/Source/GameSession/GameSession.cs b/Subsurface/Source/GameSession/GameSession.cs index 8ac5a91a9..692d9f72d 100644 --- a/Subsurface/Source/GameSession/GameSession.cs +++ b/Subsurface/Source/GameSession/GameSession.cs @@ -175,8 +175,7 @@ namespace Subsurface public void Draw(SpriteBatch spriteBatch) { guiRoot.Draw(spriteBatch); - taskManager.Draw(spriteBatch); - + if (gameMode != null) gameMode.Draw(spriteBatch); } diff --git a/Subsurface/Source/GameSettings.cs b/Subsurface/Source/GameSettings.cs index a1831609a..bfe7f1ee4 100644 --- a/Subsurface/Source/GameSettings.cs +++ b/Subsurface/Source/GameSettings.cs @@ -45,38 +45,41 @@ namespace Subsurface public void Load(string filePath) { XDocument doc = ToolBox.TryLoadXml(filePath); - try + + if (doc == null) { - XElement graphicsMode = doc.Root.Element("graphicsmode"); - GraphicsWidth = int.Parse(graphicsMode.Attribute("width").Value); - GraphicsHeight = int.Parse(graphicsMode.Attribute("height").Value); - - FullScreenEnabled = graphicsMode.Attribute("fullscreen").Value == "true"; + DebugConsole.ThrowError("No config file found"); - MasterServerUrl = ToolBox.GetAttributeString(doc.Root, "masterserverurl", ""); - - foreach (XElement subElement in doc.Root.Elements()) - { - switch (subElement.Name.ToString().ToLower()) - { - case "contentpackage": - string path = ToolBox.GetAttributeString(subElement, "path", ""); - SelectedContentPackage = ContentPackage.list.Find(cp => cp.Path == path); - - if (SelectedContentPackage == null) SelectedContentPackage = new ContentPackage(path); - break; - } - } - } - catch - { GraphicsWidth = 1024; - GraphicsHeight = 768; + GraphicsHeight = 678; + + MasterServerUrl = ""; + + SelectedContentPackage = new ContentPackage(""); + + return; } + XElement graphicsMode = doc.Root.Element("graphicsmode"); + GraphicsWidth = int.Parse(graphicsMode.Attribute("width").Value); + GraphicsHeight = int.Parse(graphicsMode.Attribute("height").Value); + + FullScreenEnabled = graphicsMode.Attribute("fullscreen").Value == "true"; + MasterServerUrl = ToolBox.GetAttributeString(doc.Root, "masterserverurl", ""); + foreach (XElement subElement in doc.Root.Elements()) + { + switch (subElement.Name.ToString().ToLower()) + { + case "contentpackage": + string path = ToolBox.GetAttributeString(subElement, "path", ""); + SelectedContentPackage = ContentPackage.list.Find(cp => cp.Path == path); + if (SelectedContentPackage == null) SelectedContentPackage = new ContentPackage(path); + break; + } + } } public void Save(string filePath) diff --git a/Subsurface/Source/Items/Components/Holdable/RangedWeapon.cs b/Subsurface/Source/Items/Components/Holdable/RangedWeapon.cs index 05cbb0559..7055f4f63 100644 --- a/Subsurface/Source/Items/Components/Holdable/RangedWeapon.cs +++ b/Subsurface/Source/Items/Components/Holdable/RangedWeapon.cs @@ -53,7 +53,7 @@ namespace Subsurface.Items.Components public override bool Use(float deltaTime, Character character = null) { if (character == null) return false; - if (!character.SecondaryKeyDown.State || reload > 0.0f) return false; + if (!character.GetInputState(InputType.SecondaryHeld) || reload > 0.0f) return false; isActive = true; reload = 1.0f; diff --git a/Subsurface/Source/Items/Components/Holdable/RepairTool.cs b/Subsurface/Source/Items/Components/Holdable/RepairTool.cs index 7063d955e..6cbbc4c68 100644 --- a/Subsurface/Source/Items/Components/Holdable/RepairTool.cs +++ b/Subsurface/Source/Items/Components/Holdable/RepairTool.cs @@ -102,7 +102,7 @@ namespace Subsurface.Items.Components public override bool Use(float deltaTime, Character character = null) { if (character == null) return false; - if (!character.SecondaryKeyDown.State) return false; + if (!character.GetInputState(InputType.SecondaryHeld)) return false; if (DoesUseFail(character)) return false; @@ -126,9 +126,6 @@ namespace Subsurface.Items.Components if (targetBody == null || targetBody.UserData == null) return true; - //ApplyStatusEffects(ActionType.OnUse, 1.0f, character); - - Structure targetStructure; Limb targetLimb; Item targetItem; @@ -143,10 +140,19 @@ namespace Subsurface.Items.Components targetStructure.AddDamage(sectionIndex, -structureFixAmount); + //if the next section is small enough, apply the effect to it as well + //(to make it easier to fix a small "left-over" section) + int nextSectionLength = targetStructure.SectionLength(sectionIndex + 1); + if (nextSectionLength > 0 && nextSectionLength < Structure.wallSectionSize * 0.3f) + { + targetStructure.HighLightSection(sectionIndex + 1); + targetStructure.AddDamage(sectionIndex + 1, -structureFixAmount); + } + } else if ((targetLimb = (targetBody.UserData as Limb)) != null) { - if (character.SecondaryKeyDown.State) + if (character.GetInputState(InputType.SecondaryHeld)) { targetLimb.character.Health += limbFixAmount; //isActive = true; diff --git a/Subsurface/Source/Items/Components/Holdable/Throwable.cs b/Subsurface/Source/Items/Components/Holdable/Throwable.cs index e2e08be40..62a817c7b 100644 --- a/Subsurface/Source/Items/Components/Holdable/Throwable.cs +++ b/Subsurface/Source/Items/Components/Holdable/Throwable.cs @@ -28,7 +28,7 @@ namespace Subsurface.Items.Components public override bool Use(float deltaTime, Character character = null) { if (character == null) return false; - if (!character.SecondaryKeyDown.State || throwing) return false; + if (!character.GetInputState(InputType.SecondaryHeld) || throwing) return false; throwing = true; @@ -60,7 +60,7 @@ namespace Subsurface.Items.Components if (!item.body.Enabled) return; if (!picker.HasSelectedItem(item)) isActive = false; - if (!picker.SecondaryKeyDown.State && !throwing) throwPos = 0.0f; + if (!picker.GetInputState(InputType.SecondaryHeld) && !throwing) throwPos = 0.0f; ApplyStatusEffects(ActionType.OnActive, deltaTime, picker); diff --git a/Subsurface/Source/Items/Components/ItemComponent.cs b/Subsurface/Source/Items/Components/ItemComponent.cs index 5bd69c663..e65b0a8ba 100644 --- a/Subsurface/Source/Items/Components/ItemComponent.cs +++ b/Subsurface/Source/Items/Components/ItemComponent.cs @@ -223,7 +223,7 @@ namespace Subsurface.Items.Components case "sound": string filePath = ToolBox.GetAttributeString(subElement, "file", ""); if (filePath=="") continue; - if (!filePath.Contains("\\")) filePath = Path.GetDirectoryName(item.Prefab.ConfigFile)+"\\"+filePath; + if (!filePath.Contains("/")) filePath = Path.GetDirectoryName(item.Prefab.ConfigFile)+"/"+filePath; ActionType type; @@ -594,7 +594,7 @@ namespace Subsurface.Items.Components try { // Get the type of a specified class. - t = Type.GetType("Subsurface.Items.Components." + type + ", Subsurface", false, true); + t = Type.GetType("Subsurface.Items.Components." + type + "", false, true); if (t == null) { if (errorMessages) DebugConsole.ThrowError("Could not find the component ''" + type + "'' (" + file + ")"); diff --git a/Subsurface/Source/Items/Components/Machines/MiniMap.cs b/Subsurface/Source/Items/Components/Machines/MiniMap.cs index 48c25d061..03798047e 100644 --- a/Subsurface/Source/Items/Components/Machines/MiniMap.cs +++ b/Subsurface/Source/Items/Components/Machines/MiniMap.cs @@ -67,17 +67,17 @@ namespace Subsurface.Items.Components GUI.DrawRectangle(spriteBatch, hullRect, Color.White); } - foreach (Character c in Character.CharacterList) - { - if (c.AnimController.CurrentHull!=null) continue; + //foreach (Character c in Character.CharacterList) + //{ + // if (c.AnimController.CurrentHull!=null) continue; - Rectangle characterRect = new Rectangle( - miniMap.X + (int)((c.Position.X - Submarine.Borders.X) * size), - miniMap.Y - (int)((c.Position.Y - Submarine.Borders.Y) * size), - 5, 5); + // Rectangle characterRect = new Rectangle( + // miniMap.X + (int)((c.Position.X - Submarine.Borders.X) * size), + // miniMap.Y - (int)((c.Position.Y - Submarine.Borders.Y) * size), + // 5, 5); - GUI.DrawRectangle(spriteBatch, characterRect, Color.White, true); - } + // GUI.DrawRectangle(spriteBatch, characterRect, Color.White, true); + //} } } diff --git a/Subsurface/Source/Items/Components/Machines/Pump.cs b/Subsurface/Source/Items/Components/Machines/Pump.cs index c320f0944..f2f7e8471 100644 --- a/Subsurface/Source/Items/Components/Machines/Pump.cs +++ b/Subsurface/Source/Items/Components/Machines/Pump.cs @@ -17,7 +17,7 @@ namespace Subsurface.Items.Components Hull hull1, hull2; [HasDefaultValue(0.0f, true)] - private float FlowPercentage + public float FlowPercentage { get { return flowPercentage; } set @@ -116,21 +116,18 @@ namespace Subsurface.Items.Components item.NewComponentEvent(this, true); } - spriteBatch.DrawString(GUI.Font, "Flow percentage: " + (int)flowPercentage + " %", new Vector2(x + 20, y + 80), Color.White); - - if (GUI.DrawButton(spriteBatch, new Rectangle(x + 200, y + 70, 40, 40), "+", false)) - { - FlowPercentage += 10.0f; - item.NewComponentEvent(this, true); - } - if (GUI.DrawButton(spriteBatch, new Rectangle(x + 250, y + 70, 40, 40), "-", false)) + spriteBatch.DrawString(GUI.Font, "Pumping speed: " + (int)flowPercentage + " %", new Vector2(x + 20, y + 80), Color.White); + + if (GUI.DrawButton(spriteBatch, new Rectangle(x + 200, y + 70, 40, 40), "OUT", false)) { FlowPercentage -= 10.0f; item.NewComponentEvent(this, true); } - - - + if (GUI.DrawButton(spriteBatch, new Rectangle(x + 250, y + 70, 40, 40), "IN", false)) + { + FlowPercentage += 10.0f; + item.NewComponentEvent(this, true); + } } public override void ReceiveSignal(string signal, Connection connection, Item sender, float power=0.0f) diff --git a/Subsurface/Source/Items/Components/Machines/Steering.cs b/Subsurface/Source/Items/Components/Machines/Steering.cs index 5bb97e7c8..f7eba534c 100644 --- a/Subsurface/Source/Items/Components/Machines/Steering.cs +++ b/Subsurface/Source/Items/Components/Machines/Steering.cs @@ -10,7 +10,7 @@ using System.Xml.Linq; namespace Subsurface.Items.Components { - class Steering : ItemComponent + class Steering : Powered { private Vector2 currVelocity; private Vector2 targetVelocity; @@ -67,7 +67,9 @@ namespace Subsurface.Items.Components public override void Update(float deltaTime, Camera cam) { - base.Update(deltaTime, cam); + //base.Update(deltaTime, cam); + + //if (voltage < minVoltage) return; if (autoPilot) { @@ -112,6 +114,8 @@ namespace Subsurface.Items.Components public override void DrawHUD(SpriteBatch spriteBatch, Character character) { + //if (voltage < minVoltage) return; + int width = GuiFrame.Rect.Width, height = GuiFrame.Rect.Height; int x = GuiFrame.Rect.X; int y = GuiFrame.Rect.Y; diff --git a/Subsurface/Source/Items/Components/Turret.cs b/Subsurface/Source/Items/Components/Turret.cs index 7a899881d..b25d9dca1 100644 --- a/Subsurface/Source/Items/Components/Turret.cs +++ b/Subsurface/Source/Items/Components/Turret.cs @@ -75,7 +75,7 @@ namespace Subsurface.Items.Components { isActive = true; - barrelSprite = new Sprite(Path.GetDirectoryName(item.Prefab.ConfigFile) + "\\" +element.Attribute("barrelsprite").Value, + barrelSprite = new Sprite(Path.GetDirectoryName(item.Prefab.ConfigFile) + "/" +element.Attribute("barrelsprite").Value, ToolBox.GetAttributeVector2(element, "origin", Vector2.Zero)); } @@ -99,23 +99,8 @@ namespace Subsurface.Items.Components } rotation = MathUtils.CurveAngle(rotation, targetRotation, 0.05f); - - } - //public override void SecondaryUse(float deltaTime, Character character = null) - //{ - // if (character == null) return; - - // Vector2 centerPos = new Vector2(item.Rect.X + barrelPos.X, item.Rect.Y - barrelPos.Y); - - // if (character == Character.Controlled && cam!=null) - // { - // Lights.LightManager.ViewPos = centerPos; - // cam.TargetPos = new Vector2(item.Rect.X + barrelPos.X, item.Rect.Y - barrelPos.Y); - // } - //} - public override bool Use(float deltaTime, Character character = null) { if (reload > 0.0f) return false; @@ -174,8 +159,6 @@ namespace Subsurface.Items.Components projectile.body.Enabled = true; projectile.SetTransform(ConvertUnits.ToSimUnits(new Vector2(item.Rect.X + barrelPos.X, item.Rect.Y - barrelPos.Y)), -rotation); - //if (useSounds.Count() > 0) useSounds[Game1.localRandom.Next(useSounds.Count())].Play(1.0f, 800.0f, item.body.FarseerBody); - projectileComponent.Use(deltaTime); item.RemoveContained(projectile); diff --git a/Subsurface/Source/Items/Components/Wearable.cs b/Subsurface/Source/Items/Components/Wearable.cs index 57bcb7224..ed50c116a 100644 --- a/Subsurface/Source/Items/Components/Wearable.cs +++ b/Subsurface/Source/Items/Components/Wearable.cs @@ -50,7 +50,7 @@ namespace Subsurface.Items.Components } string spritePath = subElement.Attribute("texture").Value; - spritePath = Path.GetDirectoryName( item.Prefab.ConfigFile)+"\\"+spritePath; + spritePath = Path.GetDirectoryName( item.Prefab.ConfigFile)+"/"+spritePath; var sprite = new Sprite(subElement, "", spritePath); wearableSprite[i] = new WearableSprite(sprite, ToolBox.GetAttributeBool(subElement, "hidelimb", false)); diff --git a/Subsurface/Source/Items/Item.cs b/Subsurface/Source/Items/Item.cs index d8cbe19d6..416d98842 100644 --- a/Subsurface/Source/Items/Item.cs +++ b/Subsurface/Source/Items/Item.cs @@ -126,10 +126,10 @@ namespace Subsurface } } - public AITarget AiTarget - { - get { return aiTarget; } - } + //public override AITarget AiTarget + //{ + // get { return aiTarget; } + //} public bool Updated { diff --git a/Subsurface/Source/Map/Explosion.cs b/Subsurface/Source/Map/Explosion.cs index 569751750..7f53ac4a6 100644 --- a/Subsurface/Source/Map/Explosion.cs +++ b/Subsurface/Source/Map/Explosion.cs @@ -129,12 +129,12 @@ namespace Subsurface currBrightness -= 0.1f; - yield return Status.Running; + yield return CoroutineStatus.Running; } light.Remove(); - yield return Status.Success; + yield return CoroutineStatus.Success; } } } diff --git a/Subsurface/Source/Map/Gap.cs b/Subsurface/Source/Map/Gap.cs index ad8dc5541..6b2fb9120 100644 --- a/Subsurface/Source/Map/Gap.cs +++ b/Subsurface/Source/Map/Gap.cs @@ -249,12 +249,20 @@ namespace Subsurface { pos.Y = ConvertUnits.ToSimUnits(MathHelper.Clamp(lowerSurface, rect.Y-rect.Height, rect.Y)); - Game1.ParticleManager.CreateParticle("watersplash", + var particle = Game1.ParticleManager.CreateParticle("watersplash", new Vector2(pos.X, pos.Y - Rand.Range(0.0f, 0.1f)), - new Vector2(flowForce.X * Rand.Range(0.005f, 0.007f), flowForce.Y * Rand.Range(0.005f, 0.007f))); + new Vector2( + MathHelper.Clamp(flowForce.X, -5000.0f, 5000.0f) * Rand.Range(0.005f, 0.007f), + flowForce.Y * Rand.Range(0.005f, 0.007f))); + if (particle!=null) + { + particle.Size = particle.Size * Math.Abs(flowForce.X / 1000.0f); + } + pos.Y = ConvertUnits.ToSimUnits(Rand.Range(lowerSurface, rect.Y - rect.Height)); - Game1.ParticleManager.CreateParticle("bubbles", pos, flowForce / 200.0f); + + Game1.ParticleManager.CreateParticle("bubbles", pos, flowForce / 200.0f); } else { @@ -312,7 +320,7 @@ namespace Subsurface flowTargetHull = hull1; //make sure not to move more than what the room contains - delta = Math.Min((hull2.Pressure - hull1.Pressure) * sizeModifier, Math.Min(hull2.Volume, hull2.FullVolume)); + delta = Math.Min((hull2.Pressure - hull1.Pressure) * 5.0f * sizeModifier, Math.Min(hull2.Volume, hull2.FullVolume)); //make sure not to place more water to the target room than it can hold delta = Math.Min(delta, hull1.FullVolume + Hull.MaxCompress - (hull1.Volume)); @@ -331,7 +339,7 @@ namespace Subsurface flowTargetHull = hull2; //make sure not to move more than what the room contains - delta = Math.Min((hull1.Pressure - hull2.Pressure) * sizeModifier, Math.Min(hull1.Volume, hull1.FullVolume)); + delta = Math.Min((hull1.Pressure - hull2.Pressure) * 5.0f * sizeModifier, Math.Min(hull1.Volume, hull1.FullVolume)); //make sure not to place more water to the target room than it can hold delta = Math.Min(delta, hull2.FullVolume + Hull.MaxCompress - (hull2.Volume)); @@ -405,7 +413,7 @@ namespace Subsurface flowTargetHull = hull2; //make sure the amount of water moved isn't more than what the room contains - float delta = Math.Min(hull1.Volume, deltaTime * 10000f * sizeModifier); + float delta = Math.Min(hull1.Volume, deltaTime * 25000f * sizeModifier); //make sure not to place more water to the target room than it can hold delta = Math.Min(delta, (hull2.FullVolume + Math.Max(hull1.Volume - hull1.FullVolume, 0.0f)) - hull2.Volume + Hull.MaxCompress / 4.0f); diff --git a/Subsurface/Source/Map/Levels/Level.cs b/Subsurface/Source/Map/Levels/Level.cs index 4ce5ff929..c77d70087 100644 --- a/Subsurface/Source/Map/Levels/Level.cs +++ b/Subsurface/Source/Map/Levels/Level.cs @@ -122,8 +122,8 @@ namespace Subsurface public static Level CreateRandom(LocationConnection locationConnection) { - int seed = locationConnection.Locations[0].GetHashCode() | locationConnection.Locations[1].GetHashCode(); - return new Level(seed.ToString(), locationConnection.Difficulty, 100000, 40000, 2000); + string seed = locationConnection.Locations[0].Name + locationConnection.Locations[1].Name; + return new Level(seed, locationConnection.Difficulty, 100000, 40000, 2000); } public static Level CreateRandom(string seed = "") @@ -153,7 +153,7 @@ namespace Subsurface bodies = new List(); - Random rand = new Random(seed.GetHashCode()); + Random rand = new Random(ToolBox.StringToInt(seed)); float siteVariance = siteInterval * 0.8f; for (int x = siteInterval / 2; x < borders.Width; x += siteInterval) diff --git a/Subsurface/Source/Map/Lights/ConvexHull.cs b/Subsurface/Source/Map/Lights/ConvexHull.cs index 364b82ad3..5a8491391 100644 --- a/Subsurface/Source/Map/Lights/ConvexHull.cs +++ b/Subsurface/Source/Map/Lights/ConvexHull.cs @@ -119,7 +119,7 @@ namespace Subsurface.Lights shadowEffect.TextureEnabled = true; //shadowEffect.VertexColorEnabled = true; shadowEffect.LightingEnabled = false; - shadowEffect.Texture = Game1.TextureLoader.FromFile("Content/lights/penumbra.png"); + shadowEffect.Texture = Game1.TextureLoader.FromFile("Content/Lights/penumbra.png"); } //compute facing of each edge, using N*L diff --git a/Subsurface/Source/Map/Map.cs b/Subsurface/Source/Map/Map.cs index 953badbd7..dfc0a72a4 100644 --- a/Subsurface/Source/Map/Map.cs +++ b/Subsurface/Source/Map/Map.cs @@ -72,7 +72,7 @@ namespace Subsurface if (iceCraters == null) iceCraters = Game1.TextureLoader.FromFile("Content/Map/iceCraters.png"); if (iceCrack == null) iceCrack = Game1.TextureLoader.FromFile("Content/Map/iceCrack.png"); - Rand.SetSyncedSeed(this.seed.GetHashCode()); + Rand.SetSyncedSeed(ToolBox.StringToInt(this.seed)); GenerateLocations(); @@ -126,7 +126,7 @@ namespace Subsurface newLocations[i] = Location.CreateRandom(position); locations.Add(newLocations[i]); } - int seed = (newLocations[0].GetHashCode() | newLocations[1].GetHashCode()); + //int seed = (newLocations[0].GetHashCode() | newLocations[1].GetHashCode()); connections.Add(new LocationConnection(newLocations[0], newLocations[1])); } diff --git a/Subsurface/Source/Map/Md5Hash.cs b/Subsurface/Source/Map/Md5Hash.cs index d13aa078c..c91579662 100644 --- a/Subsurface/Source/Map/Md5Hash.cs +++ b/Subsurface/Source/Map/Md5Hash.cs @@ -59,6 +59,11 @@ namespace Subsurface shortHash = GetShortHash(hash); } + public override string ToString() + { + return hash; + } + private string CalculateHash(FileStream stream) { MD5 md5 = MD5.Create(); diff --git a/Subsurface/Source/Map/Structure.cs b/Subsurface/Source/Map/Structure.cs index 2b4defea3..ba386dd30 100644 --- a/Subsurface/Source/Map/Structure.cs +++ b/Subsurface/Source/Map/Structure.cs @@ -38,7 +38,7 @@ namespace Subsurface class Structure : MapEntity, IDamageable { - static int wallSectionSize = 100; + public static int wallSectionSize = 100; public static List wallList = new List(); ConvexHull convexHull; @@ -97,12 +97,7 @@ namespace Subsurface { get { return prefab.MaxHealth; } } - - public AITarget AiTarget - { - get { return null;} - } - + public override void Move(Vector2 amount) { base.Move(amount); @@ -368,6 +363,20 @@ namespace Subsurface return (sections[sectionIndex].damage>=prefab.MaxHealth); } + public bool SectionIsLeaking(int sectionIndex) + { + if (sectionIndex < 0 || sectionIndex >= sections.Length) return false; + + return (sections[sectionIndex].damage >= prefab.MaxHealth*0.5f); + } + + public int SectionLength(int sectionIndex) + { + if (sectionIndex < 0 || sectionIndex >= sections.Length) return 0; + + return (isHorizontal ? sections[sectionIndex].rect.Width : sections[sectionIndex].rect.Height); + } + public void AddDamage(int sectionIndex, float damage) { if (!prefab.HasBody || prefab.IsPlatform) return; @@ -428,7 +437,7 @@ namespace Subsurface if (!prefab.HasBody) return; if (damage != sections[sectionIndex].damage) - new NetworkEvent(ID, false); + new NetworkEvent(NetworkEventType.UpdateEntity, ID, false, sectionIndex); if (damage < prefab.MaxHealth*0.5f) { @@ -615,19 +624,40 @@ namespace Subsurface public override void FillNetworkData(NetworkEventType type, NetOutgoingMessage message, object data) { - for (int i = 0; i < sections.Length; i++ ) + int sectionIndex = 0; + byte byteIndex = 0; + + try { - message.Write(sections[i].damage); + sectionIndex = (int)data; + byteIndex = (byte)sectionIndex; } + catch + { + return; + } + + message.Write(byteIndex); + message.Write(sections[sectionIndex].damage); } public override void ReadNetworkData(NetworkEventType type, NetIncomingMessage message) { - for (int i = 0; i < sections.Length; i++) + int sectionIndex = 0; + float damage = 0.0f; + + try { - float damage = message.ReadFloat(); - if (damage != sections[i].damage) SetDamage(i, damage); + sectionIndex = message.ReadByte(); + damage = message.ReadFloat(); } + catch + { + return; + } + + SetDamage(sectionIndex, damage); + } } diff --git a/Subsurface/Source/Map/Submarine.cs b/Subsurface/Source/Map/Submarine.cs index 3f02177da..596ada878 100644 --- a/Subsurface/Source/Map/Submarine.cs +++ b/Subsurface/Source/Map/Submarine.cs @@ -76,7 +76,7 @@ namespace Subsurface private set; } - public Md5Hash Hash + public Md5Hash MD5Hash { get { @@ -115,6 +115,11 @@ namespace Subsurface public Vector2 Speed { get { return speed; } + set + { + if (!MathUtils.IsValid(value)) return; + speed = value; + } } public string FilePath @@ -773,7 +778,7 @@ namespace Subsurface try { // Get the type of a specified class. - t = Type.GetType("Subsurface." + typeName + ", Subsurface", true, true); + t = Type.GetType("Subsurface." + typeName, true, true); if (t == null) { DebugConsole.ThrowError("Error in " + filePath + "! Could not find a entity of the type ''" + typeName + "''."); diff --git a/Subsurface/Source/Map/WaterRenderer.cs b/Subsurface/Source/Map/WaterRenderer.cs index 37ca73c25..6d401e21e 100644 --- a/Subsurface/Source/Map/WaterRenderer.cs +++ b/Subsurface/Source/Map/WaterRenderer.cs @@ -56,8 +56,13 @@ namespace Subsurface //vertexBuffer.SetData(vertices); //effect = Game1.game.Content.Load("effects"); +#if WINDOWS + byte[] bytecode = File.ReadAllBytes("Content/effects.mgfx"); +#endif +#if LINUX + byte[] bytecode = File.ReadAllBytes("Content/effects_linux.mgfx"); +#endif - byte[] bytecode = File.ReadAllBytes("Content/effects.mgfx"); effect = new Effect(graphicsDevice, bytecode); //Texture2D waterBumpMap = Game1.textureLoader.FromFile("Content/waterbump.jpg"); diff --git a/Subsurface/Source/Networking/GameClient.cs b/Subsurface/Source/Networking/GameClient.cs index 0a13ddd5f..c4bb169d5 100644 --- a/Subsurface/Source/Networking/GameClient.cs +++ b/Subsurface/Source/Networking/GameClient.cs @@ -10,9 +10,6 @@ namespace Subsurface.Networking { private NetClient client; - private Character myCharacter; - private CharacterInfo characterInfo; - private GUIMessageBox reconnectBox; private bool connected; @@ -23,17 +20,6 @@ namespace Subsurface.Networking private string serverIP; - public Character Character - { - get { return myCharacter; } - set { myCharacter = value; } - } - - public CharacterInfo CharacterInfo - { - get { return characterInfo; } - } - public int ID { get { return myID; } @@ -56,7 +42,7 @@ namespace Subsurface.Networking if (address.Length==1) { serverIP = hostIP; - Port = DefaultPort; + Port = NetConfig.DefaultPort; } else { @@ -65,7 +51,7 @@ namespace Subsurface.Networking if (!int.TryParse(address[1], out Port)) { DebugConsole.ThrowError("Invalid port: "+address[1]+"!"); - Port = DefaultPort; + Port = NetConfig.DefaultPort; } } @@ -151,10 +137,10 @@ namespace Subsurface.Networking DateTime timeOut = DateTime.Now + new TimeSpan(0,0,15); - // Loop untill we are approved + // Loop until we are approved while (!CanStart) { - yield return Status.Running; + yield return CoroutineStatus.Running; if (DateTime.Now > timeOut) break; @@ -171,7 +157,7 @@ namespace Subsurface.Networking if (packetType == (byte)PacketTypes.LoggedIn) { myID = inc.ReadInt32(); - if (inc.ReadBoolean()) + if (inc.ReadBoolean() && Screen.Selected != Game1.GameScreen) { new GUIMessageBox("Please wait", "A round is already running. You will have to wait for a new round to start."); } @@ -234,11 +220,11 @@ namespace Subsurface.Networking } else { - if (Screen.Selected == Game1.MainMenuScreen) Game1.NetLobbyScreen.Select(); + if (Screen.Selected != Game1.GameScreen) Game1.NetLobbyScreen.Select(); connected = true; } - yield return Status.Success; + yield return CoroutineStatus.Success; } public override void Update(float deltaTime) @@ -274,17 +260,7 @@ namespace Subsurface.Networking } else if (gameStarted) { - Vector2 charMovement = myCharacter.AnimController.TargetMovement; - if ((charMovement==Vector2.Zero || charMovement.Length()<0.001f) && - !myCharacter.ActionKeyDown.State && !myCharacter.SecondaryKeyDown.State) - { - new NetworkEvent(NetworkEventType.NotMoving, myCharacter.ID, true); - - } - else - { - new NetworkEvent(myCharacter.ID, true); - } + new NetworkEvent(myCharacter.ID, true); } } @@ -335,7 +311,7 @@ namespace Subsurface.Networking if (this.Character != null) Character.Remove(); int seed = inc.ReadInt32(); - Rand.SetSyncedSeed(seed); + string levelSeed = inc.ReadString(); @@ -347,7 +323,7 @@ namespace Subsurface.Networking double durationMinutes = inc.ReadDouble(); TimeSpan duration = new TimeSpan(0,(int)durationMinutes,0); - + Rand.SetSyncedSeed(seed); //int gameModeIndex = inc.ReadInt32(); Game1.GameSession = new GameSession(Submarine.Loaded, "", Game1.NetLobbyScreen.SelectedMode); Game1.GameSession.StartShift(duration, levelSeed); @@ -456,7 +432,7 @@ namespace Subsurface.Networking //Game1.GameScreen.Cam.MoveCamera((float)deltaTime); messageBox.Text = endMessage + "\nReturning to lobby in " + (int)secondsLeft + " s"; - yield return Status.Running; + yield return CoroutineStatus.Running; } while (secondsLeft > 0.0f); messageBox.Text = endMessage; @@ -469,7 +445,7 @@ namespace Subsurface.Networking myCharacter = null; - yield return Status.Success; + yield return CoroutineStatus.Success; } diff --git a/Subsurface/Source/Networking/GameServer.cs b/Subsurface/Source/Networking/GameServer.cs index 6b6d0b572..07c16777c 100644 --- a/Subsurface/Source/Networking/GameServer.cs +++ b/Subsurface/Source/Networking/GameServer.cs @@ -13,10 +13,12 @@ namespace Subsurface.Networking public List connectedClients = new List(); + bool started; + private NetServer server; private NetPeerConfiguration config; - private TimeSpan SparseUpdateInterval = new TimeSpan(0, 0, 0, 1); + private TimeSpan SparseUpdateInterval = new TimeSpan(0, 0, 0, 3); private DateTime sparseUpdateTimer; private TimeSpan refreshMasterInterval = new TimeSpan(0, 0, 40); @@ -36,7 +38,6 @@ namespace Subsurface.Networking endRoundButton.OnClicked = EndButtonHit; this.name = name; - this.password = password; config = new NetPeerConfiguration("subsurface"); @@ -45,7 +46,6 @@ namespace Subsurface.Networking config.SimulatedLoss = 0.2f; config.SimulatedMinimumLatency = 0.3f; #endif - config.Port = port; Port = port; @@ -56,25 +56,52 @@ namespace Subsurface.Networking config.MaximumConnections = maxPlayers; - config.DisableMessageType(NetIncomingMessageType.DebugMessage | NetIncomingMessageType.WarningMessage | NetIncomingMessageType.Receipt - | NetIncomingMessageType.ErrorMessage | NetIncomingMessageType.Error); + config.DisableMessageType(NetIncomingMessageType.DebugMessage | + NetIncomingMessageType.WarningMessage | NetIncomingMessageType.Receipt | + NetIncomingMessageType.ErrorMessage | NetIncomingMessageType.Error); config.EnableMessageType(NetIncomingMessageType.ConnectionApproval); - + + CoroutineManager.StartCoroutine(StartServer(isPublic)); + } + + private IEnumerable StartServer(bool isPublic) + { try { - server = new NetServer(config); + server = new NetServer(config); server.Start(); - - if (attemptUPnP) - { - server.UPnP.ForwardPort(port, "subsurface"); - } } - catch (Exception e) { - DebugConsole.ThrowError("Couldn't start the server", e); + DebugConsole.ThrowError("Couldn't start the server", e); + } + + + if (config.EnableUPnP) + { + server.UPnP.ForwardPort(config.Port, "subsurface"); + + GUIMessageBox upnpBox = new GUIMessageBox("Please wait...", "Attempting UPnP port forwarding", new string[] {"Cancel"} ); + upnpBox.Buttons[0].OnClicked = upnpBox.Close; + + DateTime upnpTimeout = DateTime.Now + new TimeSpan(0,0,5); + while (server.UPnP.Status == UPnPStatus.Discovering + && GUIMessageBox.VisibleBox == upnpBox)// && upnpTimeout>DateTime.Now) + { + yield return null; + } + + upnpBox.Close(null,null); + + if (server.UPnP.Status == UPnPStatus.NotAvailable) + { + new GUIMessageBox("Error", "UPnP not available"); + } + else if (server.UPnP.Status == UPnPStatus.Discovering) + { + new GUIMessageBox("Error", "UPnP discovery timed out"); + } } if (isPublic) @@ -86,11 +113,15 @@ namespace Subsurface.Networking updateInterval = new TimeSpan(0, 0, 0, 0, 30); DebugConsole.NewMessage("Server started", Color.Green); + + Game1.NetLobbyScreen.Select(); + started = true; + yield return CoroutineStatus.Success; } private void RegisterToMasterServer() { - var client = new RestClient(NetworkMember.MasterServerUrl); + var client = new RestClient(NetConfig.MasterServerUrl); var request = new RestRequest("masterserver.php", Method.GET); request.AddParameter("action", "addserver"); @@ -120,7 +151,7 @@ namespace Subsurface.Networking private IEnumerable RefreshMaster() { - var client = new RestClient(NetworkMember.MasterServerUrl); + var client = new RestClient(NetConfig.MasterServerUrl); var request = new RestRequest("masterserver.php", Method.GET); request.AddParameter("action", "refreshserver"); @@ -146,10 +177,10 @@ namespace Subsurface.Networking } System.Diagnostics.Debug.WriteLine("took "+sw.ElapsedMilliseconds+" ms"); - yield return Status.Running; + yield return CoroutineStatus.Running; } - yield return Status.Success; + yield return CoroutineStatus.Success; } private void MasterServerCallBack(IRestResponse response) @@ -173,6 +204,8 @@ namespace Subsurface.Networking public override void Update(float deltaTime) { + if (!started) return; + base.Update(deltaTime); if (gameStarted) inGameHUD.Update((float)Physics.step); @@ -298,7 +331,7 @@ namespace Subsurface.Networking } int id = 1; - while (connectedClients.Find(c=>c.ID==id)!=null) + while (connectedClients.Find(c => c.ID==id)!=null) { id++; } @@ -458,26 +491,17 @@ namespace Subsurface.Networking List recipients = new List(); - if (!networkEvent.IsImportant) - { Entity e = Entity.FindEntityByID(networkEvent.ID); - foreach (Client c in connectedClients) - { - if (c.character==null) continue; - if (Vector2.Distance(e.SimPosition, c.character.SimPosition) > 2000.0f) continue; - recipients.Add(c.Connection); - } - } - else - { foreach (Client c in connectedClients) { if (c.character == null) continue; + //if (networkEvent.Type == NetworkEventType.UpdateEntity && + // Vector2.Distance(e.SimPosition, c.character.SimPosition) > NetConfig.UpdateEntityDistance) continue; recipients.Add(c.Connection); } - } + if (recipients.Count == 0) return; @@ -510,13 +534,14 @@ namespace Subsurface.Networking return false; } - int seed = DateTime.Now.Millisecond; - Rand.SetSyncedSeed(seed); + AssignJobs(); //selectedMap.Load(); + int seed = DateTime.Now.Millisecond; + Rand.SetSyncedSeed(seed); Game1.GameSession = new GameSession(selectedMap, "", Game1.NetLobbyScreen.SelectedMode); Game1.GameSession.StartShift(Game1.NetLobbyScreen.GameDuration, Game1.NetLobbyScreen.LevelSeed); //EventManager.SelectEvent(Game1.netLobbyScreen.SelectedEvent); @@ -570,7 +595,7 @@ namespace Subsurface.Networking msg.Write(Game1.NetLobbyScreen.LevelSeed); msg.Write(Game1.NetLobbyScreen.SelectedMap.Name); - msg.Write(Game1.NetLobbyScreen.SelectedMap.Hash.Hash); + msg.Write(Game1.NetLobbyScreen.SelectedMap.MD5Hash.Hash); msg.Write(Game1.NetLobbyScreen.GameDuration.TotalMinutes); @@ -651,7 +676,7 @@ namespace Subsurface.Networking Game1.GameScreen.Cam.TargetPos = offset * 0.8f; //Game1.GameScreen.Cam.MoveCamera((float)deltaTime); - yield return Status.Running; + yield return CoroutineStatus.Running; } while (secondsLeft > 0.0f); Submarine.Unload(); @@ -660,7 +685,7 @@ namespace Subsurface.Networking DebugConsole.ThrowError(endMessage); - yield return Status.Success; + yield return CoroutineStatus.Success; } @@ -674,7 +699,7 @@ namespace Subsurface.Networking { if (client == null) return; - if (gameStarted && client.character != null) client.character.Kill(true); + if (gameStarted && client.character != null) client.character.ClearInputs(); if (msg == "") msg = client.name + " has left the server"; if (targetmsg == "") targetmsg = "You have left the server"; @@ -742,7 +767,7 @@ namespace Subsurface.Networking GUI.DrawRectangle(spriteBatch, new Rectangle(x,y,width,height), Color.Black*0.7f, true); spriteBatch.DrawString(GUI.Font, "Network statistics:", new Vector2(x+10, y+10), Color.White); - + spriteBatch.DrawString(GUI.SmallFont, "Connections: "+server.ConnectionsCount, new Vector2(x + 10, y + 30), Color.White); spriteBatch.DrawString(GUI.SmallFont, "Received bytes: " + server.Statistics.ReceivedBytes, new Vector2(x + 10, y + 45), Color.White); spriteBatch.DrawString(GUI.SmallFont, "Received packets: " + server.Statistics.ReceivedPackets, new Vector2(x + 10, y + 60), Color.White); diff --git a/Subsurface/Source/Networking/NetConfig.cs b/Subsurface/Source/Networking/NetConfig.cs new file mode 100644 index 000000000..f2cd6231f --- /dev/null +++ b/Subsurface/Source/Networking/NetConfig.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Subsurface.Networking +{ + static class NetConfig + { + public const int DefaultPort = 14242; + + //UpdateEntity networkevents aren't sent to clients if they're further than this from the entity + public const float UpdateEntityDistance = 2500.0f; + + public static string MasterServerUrl = Game1.Config.MasterServerUrl; + + //if a ragdoll is further than this from the correct position, teleport it there + //(in sim units) + public const float ResetRagdollDistance = 2.0f; + + //if the ragdoll is closer than this, don't try to correct its position + public const float AllowedRagdollDistance = 0.1f; + } +} diff --git a/Subsurface/Source/Networking/NetworkEvent.cs b/Subsurface/Source/Networking/NetworkEvent.cs index 37e69e849..59a4b670e 100644 --- a/Subsurface/Source/Networking/NetworkEvent.cs +++ b/Subsurface/Source/Networking/NetworkEvent.cs @@ -11,8 +11,7 @@ namespace Subsurface.Networking DropItem = 3, InventoryUpdate = 4, PickItem = 5, - UpdateProperty = 6, - NotMoving = 7 + UpdateProperty = 6 } class NetworkEvent @@ -46,6 +45,11 @@ namespace Subsurface.Networking get { return isImportant[(int)eventType]; } } + public NetworkEventType Type + { + get { return eventType; } + } + public NetworkEvent(int id, bool isClient) : this(NetworkEventType.UpdateEntity, id, isClient) { @@ -64,9 +68,9 @@ namespace Subsurface.Networking eventType = type; - foreach (NetworkEvent e in events) + if (!isImportant[(int)type]) { - if (!isImportant[(int)type] && e.id == id && e.eventType == type) return; + if (events.Find(e => e.id == id && e.eventType == type) != null) return; } this.id = id; diff --git a/Subsurface/Source/Networking/NetworkMember.cs b/Subsurface/Source/Networking/NetworkMember.cs index 7e7f4c7b6..9bdf0a3de 100644 --- a/Subsurface/Source/Networking/NetworkMember.cs +++ b/Subsurface/Source/Networking/NetworkMember.cs @@ -31,9 +31,6 @@ namespace Subsurface.Networking class NetworkMember { - public const int DefaultPort = 14242; - - public static string MasterServerUrl = Game1.Config.MasterServerUrl; protected static Color[] messageColor = { Color.White, Color.Red, Color.LightBlue, Color.LightGreen }; @@ -54,6 +51,21 @@ namespace Subsurface.Networking protected bool gameStarted; + protected Character myCharacter; + protected CharacterInfo characterInfo; + + public Character Character + { + get { return myCharacter; } + set { myCharacter = value; } + } + + public CharacterInfo CharacterInfo + { + get { return characterInfo; } + set { characterInfo = value; } + } + public string Name { get { return name; } diff --git a/Subsurface/Source/Particles/Particle.cs b/Subsurface/Source/Particles/Particle.cs index 6fa547c85..a6210f135 100644 --- a/Subsurface/Source/Particles/Particle.cs +++ b/Subsurface/Source/Particles/Particle.cs @@ -32,9 +32,9 @@ namespace Subsurface.Particles private float checkCollisionTimer; - public bool InWater + public ParticlePrefab.DrawTargetType DrawTarget { - get { return prefab.inWater; } + get { return prefab.DrawTarget; } } public Vector2 yLimits diff --git a/Subsurface/Source/Particles/ParticleManager.cs b/Subsurface/Source/Particles/ParticleManager.cs index 967b18409..857b4322c 100644 --- a/Subsurface/Source/Particles/ParticleManager.cs +++ b/Subsurface/Source/Particles/ParticleManager.cs @@ -95,14 +95,15 @@ namespace Subsurface.Particles public void Draw(SpriteBatch spriteBatch, bool inWater) { + ParticlePrefab.DrawTargetType drawTarget = inWater ? ParticlePrefab.DrawTargetType.Water : ParticlePrefab.DrawTargetType.Air; + for (int i = 0; i < particleCount; i++) { - if (particles[i].InWater != inWater) continue; + if (!particles[i].DrawTarget.HasFlag(drawTarget)) continue; particles[i].Draw(spriteBatch); } } - } } diff --git a/Subsurface/Source/Particles/ParticlePrefab.cs b/Subsurface/Source/Particles/ParticlePrefab.cs index a88028034..9f2b9a1e1 100644 --- a/Subsurface/Source/Particles/ParticlePrefab.cs +++ b/Subsurface/Source/Particles/ParticlePrefab.cs @@ -5,6 +5,8 @@ namespace Subsurface.Particles { class ParticlePrefab { + public enum DrawTargetType { Air = 1, Water = 2, Both = 3 } + public readonly string name; public readonly Sprite sprite; @@ -27,7 +29,7 @@ namespace Subsurface.Particles public readonly Vector2 velocityChange; - public readonly bool inWater; + public readonly DrawTargetType DrawTarget; public readonly bool rotateToDirection; @@ -67,8 +69,20 @@ namespace Subsurface.Particles rotateToDirection = ToolBox.GetAttributeBool(element, "rotatetodirection", false); - inWater = ToolBox.GetAttributeBool(element, "inwater", false); - + switch (ToolBox.GetAttributeString(element, "drawtarget", "air").ToLower()) + { + case "air": + default: + DrawTarget = DrawTargetType.Air; + break; + case "water": + DrawTarget = DrawTargetType.Water; + break; + case "both": + DrawTarget = DrawTargetType.Both; + break; + + } } } } diff --git a/Subsurface/Source/Physics/PhysicsBody.cs b/Subsurface/Source/Physics/PhysicsBody.cs index 562397974..42c2d4773 100644 --- a/Subsurface/Source/Physics/PhysicsBody.cs +++ b/Subsurface/Source/Physics/PhysicsBody.cs @@ -49,7 +49,7 @@ namespace Subsurface get { return targetPosition; } set { - if (float.IsNaN(value.X) || float.IsNaN(value.Y)) return; + if (!MathUtils.IsValid(value)) return; targetPosition.X = MathHelper.Clamp(value.X, -10000.0f, 10000.0f); targetPosition.Y = MathHelper.Clamp(value.Y, -10000.0f, 10000.0f); } @@ -60,7 +60,7 @@ namespace Subsurface get { return targetVelocity; } set { - if (float.IsNaN(value.X) || float.IsNaN(value.Y)) return; + if (!MathUtils.IsValid(value)) return; targetVelocity.X = MathHelper.Clamp(value.X, -100.0f, 100.0f); targetVelocity.Y = MathHelper.Clamp(value.Y, -100.0f, 100.0f); } @@ -71,7 +71,7 @@ namespace Subsurface get { return targetRotation; } set { - if (float.IsNaN(value) || float.IsInfinity(value)) return; + if (!MathUtils.IsValid(value)) return; targetRotation = value; } } @@ -81,7 +81,7 @@ namespace Subsurface get { return targetAngularVelocity; } set { - if (float.IsNaN(value) || float.IsInfinity(value)) return; + if (!MathUtils.IsValid(value)) return; targetAngularVelocity = value; } } diff --git a/Subsurface/Source/PlayerInput.cs b/Subsurface/Source/PlayerInput.cs index 01a19573b..a6427ac94 100644 --- a/Subsurface/Source/PlayerInput.cs +++ b/Subsurface/Source/PlayerInput.cs @@ -3,10 +3,25 @@ using Microsoft.Xna.Framework.Input; namespace Subsurface { + + public enum InputType + { + Select, + ActionHit, ActionHeld, + SecondaryHit, SecondaryHeld, + Left, Right, Up, Down, + Run + } + class Key { private bool state, stateQueue; private bool canBeHeld; + + public bool CanBeHeld + { + get { return canBeHeld; } + } public Key(bool canBeHeld) { diff --git a/Subsurface/Source/Program.cs b/Subsurface/Source/Program.cs index 4fefd14f5..0c8c357b2 100644 --- a/Subsurface/Source/Program.cs +++ b/Subsurface/Source/Program.cs @@ -1,6 +1,12 @@ #region Using Statements using System; +using System.IO; +using System.Reflection; +using System.Text; + +using System.Management; +using System.Windows.Forms; #endregion @@ -19,7 +25,73 @@ namespace Subsurface static void Main() { using (var game = new Game1()) - game.Run(); + { +#if !DEBUG + try + { +#endif + + game.Run(); +#if !DEBUG + } + catch (Exception e) + { + CrashDump(game, "crashreport.txt", e); + } +#endif + } + } + + static void CrashDump(Game1 game, string filePath, Exception exception) + { + StreamWriter sw = new StreamWriter(filePath); + + StringBuilder sb = new StringBuilder(); + sb.AppendLine("Subsurface crash report (generated on " + DateTime.Now + ")"); + sb.AppendLine("\n"); + sb.AppendLine("Subsurface seems to have crashed. Sorry for the inconvenience! "); + sb.AppendLine("If you'd like to help fix the bug that caused the crash, please send this file to the developers on the Undertow Games forums."); + sb.AppendLine("\n"); + sb.AppendLine("Subsurface version " + Game1.Version); + sb.AppendLine("Selected content package: "+Game1.SelectedPackage.Name); + sb.AppendLine("Level seed: "+ ((Level.Loaded == null) ? "no level loaded" : Level.Loaded.Seed)); + sb.AppendLine("Loaded submarine: " + ((Submarine.Loaded == null) ? "none" : Submarine.Loaded.Name +" ("+Submarine.Loaded.MD5Hash+")")); + sb.AppendLine("Selected screen: " + Screen.Selected.ToString()); + + if (Game1.Server != null) + { + sb.AppendLine("Server (" +(Game1.Server.GameStarted ? "Round had started" : "Round hand't been started")); + } + else if (Game1.Client != null) + { + sb.AppendLine("Server (" +(Game1.Client.GameStarted ? "Round had started" : "Round hand't been started")); + } + + sb.AppendLine("\n"); + sb.AppendLine("System info:"); + sb.AppendLine(" Operating system: " + System.Environment.OSVersion + (System.Environment.Is64BitOperatingSystem ? " 64 bit" : " x86")); + sb.AppendLine(" Graphics device handle: " + game.GraphicsDevice.Handle.ToString()); + sb.AppendLine("\n"); + sb.AppendLine("Exception: "+exception.Message); + sb.AppendLine("Target site: " +exception.TargetSite.ToString()); + sb.AppendLine("Stack trace: "); + sb.AppendLine(exception.StackTrace); + sb.AppendLine("\n"); + + sb.AppendLine("Last debug messages:"); + for (int i = DebugConsole.messages.Count - 1; i > 0 && i > DebugConsole.messages.Count - 10; i-- ) + { + sb.AppendLine(" "+DebugConsole.messages[i].Time+" - "+DebugConsole.messages[i].Text); + } + + + sw.WriteLine(sb.ToString()); + + MessageBox.Show( "A crash report (''crashreport.txt'') was saved in the root folder of the game."+ + " If you'd like to help fix this bug, please make a bug report on the Undertow Games forum with the report attached.", + "Oops! Subsurface just crashed.", MessageBoxButtons.OK, MessageBoxIcon.Error); + + sw.Close(); } } #endif diff --git a/Subsurface/Source/Screens/MainMenu.cs b/Subsurface/Source/Screens/MainMenu.cs index 107da690a..ef53f9a54 100644 --- a/Subsurface/Source/Screens/MainMenu.cs +++ b/Subsurface/Source/Screens/MainMenu.cs @@ -118,7 +118,7 @@ namespace Subsurface new GUITextBlock(new Rectangle(0, 100, 0, 30), "Server port:", GUI.style, Alignment.TopLeft, Alignment.Left, menuTabs[(int)Tabs.HostServer]); portBox = new GUITextBox(new Rectangle(160, 100, 200, 30), null, null, Alignment.TopLeft, Alignment.Left, GUI.style, menuTabs[(int)Tabs.HostServer]); - portBox.Text = NetworkMember.DefaultPort.ToString(); + portBox.Text = NetConfig.DefaultPort.ToString(); portBox.ToolTip = "Server port"; new GUITextBlock(new Rectangle(0, 150, 100, 30), "Max players:", GUI.style, Alignment.TopLeft, Alignment.Left, menuTabs[(int)Tabs.HostServer]); @@ -210,7 +210,7 @@ namespace Subsurface int port; if (!int.TryParse(portBox.Text, out port) || port < 0 || port > 65535) { - portBox.Text = NetworkMember.DefaultPort.ToString(); + portBox.Text = NetConfig.DefaultPort.ToString(); portBox.Flash(); return false; @@ -219,7 +219,7 @@ namespace Subsurface Game1.NetworkMember = new GameServer(name, port, isPublicBox.Selected, passwordBox.Text, useUpnpBox.Selected, int.Parse(maxPlayersBox.Text)); Game1.NetLobbyScreen.IsServer = true; - Game1.NetLobbyScreen.Select(); + //Game1.NetLobbyScreen.Select(); return true; } @@ -320,7 +320,7 @@ namespace Subsurface } private void RemoveSaveFrame() - { + { GUIComponent prevFrame = null; foreach (GUIComponent child in menuTabs[(int)Tabs.LoadGame].children) { diff --git a/Subsurface/Source/Screens/NetLobbyScreen.cs b/Subsurface/Source/Screens/NetLobbyScreen.cs index dbcf83bde..4a2d9ac97 100644 --- a/Subsurface/Source/Screens/NetLobbyScreen.cs +++ b/Subsurface/Source/Screens/NetLobbyScreen.cs @@ -279,23 +279,44 @@ namespace Subsurface durationBar.OnMoved = Game1.Server.UpdateNetLobby; if (subList.CountChildren > 0) subList.Select(-1); - if (GameModePreset.list.Count > 0) modeList.Select(0); + if (GameModePreset.list.Count > 0) modeList.Select(0); + + var playYourself = new GUITickBox(new Rectangle(0, -20, 20, 20), "Play yourself", Alignment.TopLeft, playerFrame); + playYourself.OnSelected = TogglePlayYourself; } - else if (playerFrame.children.Count==0) - { + else + { + UpdatePlayerFrame(Game1.Client.CharacterInfo); + } + + + + base.Select(); + } + + private void UpdatePlayerFrame(CharacterInfo characterInfo) + { + if (playerFrame.children.Count <= 1) + { playerFrame.ClearChildren(); + if (IsServer && Game1.Server != null) + { + var playYourself = new GUITickBox(new Rectangle(0, -20, 200, 30), "Play yourself", Alignment.TopLeft, playerFrame); + playYourself.OnSelected = TogglePlayYourself; + } + new GUITextBlock(new Rectangle(60, 0, 200, 30), "Name: ", GUI.style, playerFrame); GUITextBox playerName = new GUITextBox(new Rectangle(60, 30, 0, 20), Alignment.TopLeft, GUI.style, playerFrame); - playerName.Text = Game1.Client.CharacterInfo.Name; + playerName.Text = characterInfo.Name; playerName.OnEnter += ChangeCharacterName; new GUITextBlock(new Rectangle(0, 70, 200, 30), "Gender: ", GUI.style, playerFrame); - GUIButton maleButton = new GUIButton(new Rectangle(0, 100, 70, 20), "Male", - Alignment.TopLeft, GUI.style,playerFrame); + GUIButton maleButton = new GUIButton(new Rectangle(0, 100, 70, 20), "Male", + Alignment.TopLeft, GUI.style, playerFrame); maleButton.UserData = Gender.Male; maleButton.OnClicked += SwitchGender; @@ -313,7 +334,7 @@ namespace Subsurface int i = 1; foreach (JobPrefab job in JobPrefab.List) { - GUITextBlock jobText = new GUITextBlock(new Rectangle(0,0,0,20), i+". "+job.Name, GUI.style, Alignment.Left, Alignment.Right, jobList); + GUITextBlock jobText = new GUITextBlock(new Rectangle(0, 0, 0, 20), i + ". " + job.Name, GUI.style, Alignment.Left, Alignment.Right, jobList); jobText.UserData = job; GUIButton upButton = new GUIButton(new Rectangle(0, 0, 15, 15), "u", GUI.style, jobText); @@ -329,10 +350,29 @@ namespace Subsurface //UpdatePreviewPlayer(Game1.Client.CharacterInfo); - UpdatePreviewPlayer(Game1.Client.CharacterInfo); + UpdatePreviewPlayer(characterInfo); } + } - base.Select(); + private bool TogglePlayYourself(object obj) + { + GUITickBox tickBox = obj as GUITickBox; + if (tickBox.Selected) + { + Game1.Server.CharacterInfo = new CharacterInfo(Character.HumanConfigFile, Game1.Server.Name); + UpdatePlayerFrame(Game1.Server.CharacterInfo); + } + else + { + playerFrame.ClearChildren(); + + if (IsServer && Game1.Server != null) + { + var playYourself = new GUITickBox(new Rectangle(0, -20, 200, 30), "Play yourself", Alignment.TopLeft, playerFrame); + playYourself.OnSelected = TogglePlayYourself; + } + } + return false; } private bool SelectMap(object obj) @@ -590,10 +630,10 @@ namespace Subsurface } else { - if (map.Hash.Hash != md5Hash) + if (map.MD5Hash.Hash != md5Hash) { DebugConsole.ThrowError("Your version of the map file ''" + map.Name + "'' doesn't match the server's version!"); - DebugConsole.ThrowError("Your file: " + map.Name + "(MD5 hash : " + map.Hash.Hash + ")"); + DebugConsole.ThrowError("Your file: " + map.Name + "(MD5 hash : " + map.MD5Hash.Hash + ")"); DebugConsole.ThrowError("Server's file: " + mapName + "(MD5 hash : " + md5Hash + ")"); return false; } @@ -618,7 +658,7 @@ namespace Subsurface else { msg.Write(Path.GetFileName(selectedMap.Name)); - msg.Write(selectedMap.Hash.Hash); + msg.Write(selectedMap.MD5Hash.Hash); } msg.Write(ServerName); diff --git a/Subsurface/Source/Screens/ServerListScreen.cs b/Subsurface/Source/Screens/ServerListScreen.cs index 00dd70f84..b7cd51b58 100644 --- a/Subsurface/Source/Screens/ServerListScreen.cs +++ b/Subsurface/Source/Screens/ServerListScreen.cs @@ -130,7 +130,7 @@ namespace Subsurface refreshDisableTimer = DateTime.Now + AllowedRefreshInterval; - yield return Status.Success; + yield return CoroutineStatus.Success; } private void UpdateServerList(string masterServerData) @@ -197,14 +197,14 @@ namespace Subsurface RestClient client = null; try { - client = new RestClient(NetworkMember.MasterServerUrl); + client = new RestClient(NetConfig.MasterServerUrl); } catch (Exception e) { DebugConsole.ThrowError("Error while connecting to master server", e); } - if (client == null) yield return Status.Success; + if (client == null) yield return CoroutineStatus.Success; var request = new RestRequest("masterserver.php", Method.GET); @@ -231,10 +231,10 @@ namespace Subsurface restRequestHandle.Abort(); DebugConsole.ThrowError("Couldn't connect to master server (request timed out)"); } - yield return Status.Running; + yield return CoroutineStatus.Running; } - yield return Status.Success; + yield return CoroutineStatus.Success; } @@ -296,7 +296,7 @@ namespace Subsurface while (GUIMessageBox.MessageBoxes.Contains(msgBox)) { okButton.Enabled = !string.IsNullOrWhiteSpace(passwordBox.Text); - yield return Status.Running; + yield return CoroutineStatus.Running; } selectedPassword = passwordBox.Text; @@ -305,9 +305,7 @@ namespace Subsurface Game1.NetworkMember = new GameClient(clientNameBox.Text); Game1.Client.ConnectToServer(ip, selectedPassword); - Game1.NetLobbyScreen.Select(); - - yield return Status.Success; + yield return CoroutineStatus.Success; } public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) diff --git a/Subsurface/Source/Sounds/AmbientSoundManager.cs b/Subsurface/Source/Sounds/AmbientSoundManager.cs index 72ef26434..14a0e0725 100644 --- a/Subsurface/Source/Sounds/AmbientSoundManager.cs +++ b/Subsurface/Source/Sounds/AmbientSoundManager.cs @@ -75,22 +75,22 @@ namespace Subsurface startDrone.Play(); - yield return Status.Running; + yield return CoroutineStatus.Running; waterAmbience = Sound.Load("Content/Sounds/Water/WaterAmbience.ogg"); - yield return Status.Running; + yield return CoroutineStatus.Running; flowSounds[0] = Sound.Load("Content/Sounds/Water/FlowSmall.ogg"); - yield return Status.Running; + yield return CoroutineStatus.Running; flowSounds[1] = Sound.Load("Content/Sounds/Water/FlowMedium.ogg"); - yield return Status.Running; + yield return CoroutineStatus.Running; flowSounds[2] = Sound.Load("Content/Sounds/Water/FlowLarge.ogg"); - yield return Status.Running; + yield return CoroutineStatus.Running; - XDocument doc = ToolBox.TryLoadXml("Content/Sounds/Sounds.xml"); - if (doc == null) yield return Status.Failure; + XDocument doc = ToolBox.TryLoadXml("Content/Sounds/sounds.xml"); + if (doc == null) yield return CoroutineStatus.Failure; - yield return Status.Running; + yield return CoroutineStatus.Running; var xMusic = doc.Root.Elements("music").ToList(); @@ -100,13 +100,13 @@ namespace Subsurface int i = 0; foreach (XElement element in xMusic) { - string file = ToolBox.GetAttributeString(element, "file", "").ToLower(); + string file = ToolBox.GetAttributeString(element, "file", ""); string type = ToolBox.GetAttributeString(element, "type", "").ToLower(); Vector2 priority = ToolBox.GetAttributeVector2(element, "priorityrange", new Vector2(0.0f, 100.0f)); musicClips[i] = new BackgroundMusic(file, type, priority); - yield return Status.Running; + yield return CoroutineStatus.Running; i++; } @@ -120,7 +120,7 @@ namespace Subsurface int i = 0; foreach (XElement element in xDamageSounds) { - yield return Status.Running; + yield return CoroutineStatus.Running; Sound sound = Sound.Load(ToolBox.GetAttributeString(element, "file", "")); if (sound == null) continue; @@ -144,7 +144,7 @@ namespace Subsurface } } - yield return Status.Success; + yield return CoroutineStatus.Success; } @@ -189,6 +189,7 @@ namespace Subsurface { foreach (Task task in Game1.GameSession.taskManager.Tasks) { + if (!task.IsStarted) continue; if (criticalTask == null || task.Priority > criticalTask.Priority) { criticalTask = task; diff --git a/Subsurface/Source/Sounds/SoundManager.cs b/Subsurface/Source/Sounds/SoundManager.cs index 345e7009d..7df7098ee 100644 --- a/Subsurface/Source/Sounds/SoundManager.cs +++ b/Subsurface/Source/Sounds/SoundManager.cs @@ -1,9 +1,7 @@ using System.Collections.Generic; using System.Diagnostics; using Microsoft.Xna.Framework; -#if WINDOWS using OpenTK.Audio.OpenAL; -#endif using OpenTK.Audio; using System; diff --git a/Subsurface/Source/Sprite.cs b/Subsurface/Source/Sprite.cs index 5bce899f1..482ffe367 100644 --- a/Subsurface/Source/Sprite.cs +++ b/Subsurface/Source/Sprite.cs @@ -77,7 +77,7 @@ namespace Subsurface if (!string.IsNullOrEmpty(path)) { - if (!path.EndsWith("\\")) path += "\\"; + if (!path.EndsWith("/")) path += "/"; } file = path + file; diff --git a/Subsurface/Source/Utils/TextureLoader.cs b/Subsurface/Source/Utils/TextureLoader.cs index d224204e2..0533f07e4 100644 --- a/Subsurface/Source/Utils/TextureLoader.cs +++ b/Subsurface/Source/Utils/TextureLoader.cs @@ -51,7 +51,7 @@ namespace Subsurface #endif #if LINUX using (Stream fileStream = File.OpenRead(path)) - return Texture2D.FromFile(_graphicsDevice, fileStream);// .FromStream(fileStream, preMultiplyAlpha); + return Texture2D.FromStream(_graphicsDevice, fileStream);// .FromStream(fileStream, preMultiplyAlpha); #endif } diff --git a/Subsurface/Source/Utils/ToolBox.cs b/Subsurface/Source/Utils/ToolBox.cs index b37a172bc..27d303c20 100644 --- a/Subsurface/Source/Utils/ToolBox.cs +++ b/Subsurface/Source/Utils/ToolBox.cs @@ -270,6 +270,15 @@ namespace Subsurface .Select(s => s[Rand.Int(s.Length)]) .ToArray()); } + + public static int StringToInt(string str) + { + str = str.Substring(0, Math.Min(str.Length, 32)); + + byte[] asciiBytes = Encoding.ASCII.GetBytes(str); + + return BitConverter.ToInt32(asciiBytes, 0); + } public static string WrapText(string text, float lineLength, SpriteFont font) { diff --git a/Subsurface/Subsurface.csproj b/Subsurface/Subsurface.csproj index 616b23517..973ad2ef1 100644 --- a/Subsurface/Subsurface.csproj +++ b/Subsurface/Subsurface.csproj @@ -82,6 +82,7 @@ + @@ -218,10 +219,6 @@ - - False - .\Lidgren.Network.dll - $(MSBuildProgramFiles32)\MonoGame\v3.0\Assemblies\Windows\MonoGame.Framework.dll @@ -238,6 +235,7 @@ + @@ -943,6 +941,10 @@ {0aad36e3-51a5-4a07-ab60-5c8a66bd38b7} Farseer Physics MonoGame + + {49ba1c69-6104-41ac-a5d8-b54fa9f696e8} + Lidgren.Network + {1e6bf44d-6e31-40cc-8321-3d5958c983e7} Subsurface_content diff --git a/Subsurface_Solution.sln b/Subsurface_Solution.sln index 6a2e3b42f..5255b3473 100644 --- a/Subsurface_Solution.sln +++ b/Subsurface_Solution.sln @@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Farseer Physics MonoGame", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Launcher", "Launcher\Launcher.csproj", "{24420B91-6CD9-4DE3-9ADD-2F2C7E1FB6BB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lidgren.Network", "Lidgren.Network\Lidgren.Network.csproj", "{49BA1C69-6104-41AC-A5D8-B54FA9F696E8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Android|Any CPU = Android|Any CPU @@ -251,6 +253,51 @@ Global {24420B91-6CD9-4DE3-9ADD-2F2C7E1FB6BB}.Windows8|Mixed Platforms.ActiveCfg = Release|Any CPU {24420B91-6CD9-4DE3-9ADD-2F2C7E1FB6BB}.Windows8|Mixed Platforms.Build.0 = Release|Any CPU {24420B91-6CD9-4DE3-9ADD-2F2C7E1FB6BB}.Windows8|x86.ActiveCfg = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Android|Any CPU.ActiveCfg = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Android|Any CPU.Build.0 = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Android|Mixed Platforms.ActiveCfg = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Android|Mixed Platforms.Build.0 = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Android|x86.ActiveCfg = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Debug|x86.ActiveCfg = Debug|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.iOS|Any CPU.ActiveCfg = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.iOS|Any CPU.Build.0 = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.iOS|Mixed Platforms.ActiveCfg = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.iOS|Mixed Platforms.Build.0 = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.iOS|x86.ActiveCfg = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Linux|Any CPU.ActiveCfg = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Linux|Any CPU.Build.0 = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Linux|Mixed Platforms.ActiveCfg = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Linux|Mixed Platforms.Build.0 = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Linux|x86.ActiveCfg = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.OSX|Any CPU.ActiveCfg = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.OSX|Any CPU.Build.0 = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.OSX|Mixed Platforms.ActiveCfg = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.OSX|Mixed Platforms.Build.0 = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.OSX|x86.ActiveCfg = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.PSM|Any CPU.ActiveCfg = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.PSM|Any CPU.Build.0 = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.PSM|Mixed Platforms.ActiveCfg = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.PSM|Mixed Platforms.Build.0 = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.PSM|x86.ActiveCfg = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Release|Any CPU.Build.0 = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Release|x86.ActiveCfg = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Windows|Any CPU.ActiveCfg = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Windows|Any CPU.Build.0 = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Windows|Mixed Platforms.ActiveCfg = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Windows|Mixed Platforms.Build.0 = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Windows|x86.ActiveCfg = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Windows8|Any CPU.ActiveCfg = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Windows8|Any CPU.Build.0 = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Windows8|Mixed Platforms.ActiveCfg = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Windows8|Mixed Platforms.Build.0 = Release|Any CPU + {49BA1C69-6104-41AC-A5D8-B54FA9F696E8}.Windows8|x86.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Subsurface_Solution.v12.suo b/Subsurface_Solution.v12.suo index 07e54b4b4a5d6890b6e26cb526c17d4915664aa1..9f1e149588e327b8ec41834a1c2ede01a366d9e7 100644 GIT binary patch delta 44778 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 Q- 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 Z);=5+o%nRtf}*a$3VpO05P*#R|39@uv&R4c delta 23952 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