public void ServerAdvance() { for (int d = 0; d < NentDatas.Length; d++) { ReArrayIdPool <SnapHistory <TSnap, TStatic> > data = NentDatas[d]; for (int i = 0; i < data.Count; i++) { SnapHistory <TSnap, TStatic> h = data.Values[i]; if (h.PrevFlag != SnapHistory <TSnap, TStatic> .FlagGold) { continue; // if the prevflag is not gold, } // the entity does not exist at this timestamp // so we don't bother simulating it yet // now we advance the snapshot forward to the current point in // time, and then save it h.Shots[h.CurrentIndex] = h.Shots[h.PrevIndex]; Advancer.AdvanceLogic(AdvancerConfig, h, ref h.Shots[h.CurrentIndex], NetSnapper.TickMSTarget); Nents.ServerSaveSimIntoCurrent(h); } } }
public void ReqTooManyTest() { ReArrayIdPool <TestPoolable> pool = new ReArrayIdPool <TestPoolable>(10, 1000, () => { return(new TestPoolable()); }, (obj) => { obj.Clear(); }); List <int> ids = new List <int>(); for (int i = 0; i < 15; i++) { ids.Add(pool.RequestId()); } for (int i = 0; i < 15; i++) { Assert.IsFalse(ids[i] == -1); } for (int i = 0; i < pool.Count; i++) { pool.Values[i].X += i; } foreach (int id in ids) { if (id != -1) { Assert.AreEqual(id, pool.Values[pool.IdsToIndices[id]].X); } } }
private bool UnpackDelta(ReArrayIdPool <SnapHistory <TSnap, TStatic> > data, int innerid, byte[] blob, int blobstart, int blobcount, ushort timestamp, ushort basisTimestamp) { SnapHistory <TSnap, TStatic> sh = data.Values[data.IdsToIndices[innerid]]; int index = sh.FindIndex(timestamp); if (index < 0 || index >= sh.Shots.Length) { return(false); // out of bounds } int basisIndex = sh.FindIndex(basisTimestamp); if (basisIndex < 0 || basisIndex >= sh.Shots.Length) { return(false); // out of bounds } Packer.UnpackDelta(ref sh.Shots[index], sh.Shots[basisIndex], blob, blobstart, blobcount); sh.Timestamps[index] = timestamp; sh.Flags[index] = SnapHistory <TSnap, TStatic> .FlagGold; // ask the server to resimulate from this timestamp // to ensure our silver guesses get updated NetSnapper.RequestResimulate(timestamp); return(true); }
private bool UnpackDelta(ReArrayIdPool <SnapHistory <TSnap, TStatic> > data, int innerid, byte[] blob, int blobstart, int blobcount, ushort timestamp, ushort basisTimestamp) { SnapHistory <TSnap, TStatic> sh = data.Values[data.IdsToIndices[innerid]]; int index = sh.FindIndex(timestamp); if (index < 0 || index >= sh.Shots.Length) { return(false); // out of bounds } int basisIndex = sh.FindIndex(basisTimestamp); if (basisIndex < 0 || basisIndex >= sh.Shots.Length) { return(false); // out of bounds } Packer.UnpackDelta(ref sh.Shots[index], sh.Shots[basisIndex], blob, blobstart, blobcount); sh.Timestamps[index] = timestamp; sh.Flags[index] = SnapHistory <TSnap, TStatic> .FlagGold; // handle flag rollover Rollover(sh, index, timestamp); return(true); }
private bool Deghost(ReArrayIdPool <SnapHistory <TSnap, TStatic> > data, int innerid, ushort timestamp) { SnapHistory <TSnap, TStatic> sh = data.Values[data.IdsToIndices[innerid]]; int index = sh.FindIndex(timestamp); if (index < 0 || index >= sh.Shots.Length) { return(false); // out of bounds } sh.Flags[index] = SnapHistory <TSnap, TStatic> .FlagDeghosted; // scroll forward and deghost subsequent timestamps! // what to do if deghost and ghost arrive out of order? // simple: by only overwriting SILVER flags, we ensure that deghosting // will not overwrite fresh / more recent data int nindex = index + 1; ushort nextTime = timestamp; while (true) { if (nindex == sh.Shots.Length) { nindex = 0; } if (nextTime == ushort.MaxValue) { nextTime = 0; } else { nextTime++; } // we can only rollover onto silver flags if (sh.Flags[nindex] != SnapHistory <TSnap, TStatic> .FlagSilver && sh.Flags[nindex] != SnapHistory <TSnap, TStatic> .FlagEmpty) { break; } // can only rollover onto subsequent timestamps if (sh.Timestamps[nindex] != nextTime) { break; } // now we're ready to roll sh.Flags[nindex] = SnapHistory <TSnap, TStatic> .FlagDeghosted; nindex++; } return(true); }
public void OrderedReqAndRemoveTest() { ReArrayIdPool <TestPoolable> pool = new ReArrayIdPool <TestPoolable>(10, 1000, () => { return(new TestPoolable()); }, (obj) => { obj.Clear(); }, ordered: true); List <int> ids = new List <int>(); for (int i = 0; i < 10; i++) { ids.Add(pool.RequestId()); } foreach (int id in ids) { Assert.IsFalse(id == -1); } for (int i = 0; i < pool.Count; i++) { pool.Values[i].X += i; } foreach (int id in ids) { Assert.AreEqual(id, pool.Values[pool.IdsToIndices[id]].X); } for (int i = 5; i < 8; i++) { pool.ReturnId(ids[i]); // return 5, 6, and 7 } Assert.IsTrue(pool.Count == 7); // down from 10 Assert.IsTrue(pool.Values[pool.IdsToIndices[ids[9]]].X == 9); // verify that accessing id9 still works Assert.IsTrue(pool.IndicesToIds[pool.Count - 1] == ids[ids.Count - 1]); // verify order was preserved // request 3 new structs, to fill back up to 10 List <int> newids = new List <int>(); for (int i = 0; i < 3; i++) { newids.Add(pool.RequestId()); } foreach (int id in newids) { Assert.IsFalse(id == -1); } Assert.IsTrue(pool.Count == 10); // back to 10 foreach (int id in newids) { Assert.AreEqual(0, pool.Values[pool.IdsToIndices[id]].X); // verify that returned structs were cleared } }
public void ServerAdvance() { // handle inputs /*for (int i = 0; i < Server.PlayerInfos.Count; i++) * { * byte pid = Server.PlayerInfos.Values[i].PlayerId; * if (pid == 0) * continue; // server doesn't have inputs * * InputChecker(pid); * }*/ for (int d = 0; d < NentDatas.Length; d++) { ReArrayIdPool <SnapHistory <NentBasic2d, NentStaticBasic2d> > data = NentDatas[d]; for (int i = 0; i < data.Count; i++) { SnapHistory <NentBasic2d, NentStaticBasic2d> h = data.Values[i]; if (h.PrevFlag != SnapHistory <NentBasic2d, NentStaticBasic2d> .FlagGold) { continue; // if the prevflag is not gold, } // the entity does not exist at this timestamp // so we don't bother simulating it yet // now we advance the snapshot forward to the current point in // time, and then save it if (h.StaticData.Id2 == NENT_PLAYEROBJ) { // store the player entities so we know which entity belongs // to which player PlayerEntityIds[h.StaticData.Id1] = (byte)h.EntityId; // if we have no inputs, do regular advancement h.Shots[h.CurrentIndex] = h.Shots[h.PrevIndex]; if (!InputChecker(h, h.StaticData.Id1)) { AdvanceLogic(h, NetSnapper.TickMSTarget); } } else { // if this were client, we'd check the flag of the next. // as it is, just create the next from the current h.Shots[h.CurrentIndex] = h.Shots[h.PrevIndex]; AdvanceLogic(h, NetSnapper.TickMSTarget); } Nents.ServerSaveSimIntoCurrent(h); } } }
public VirtualSocket(ISocket[] targets, IPEndPoint[] addresses, int maxQueueSize, IPEndPoint fromPoint) { Targets = targets; Addresses = addresses; FromPoint = fromPoint; Pool = new ReArrayIdPool <Receipt>(10, maxQueueSize, PoolCreate, (obj) => { obj.Clear(); }); WritingReceipt = Pool.Request(); }
// LEAVING THIS DISCUSSION HERE FOR POSTERITY // ultimately we decided: // this can't be generalized neatly into extrap functions // instead, the developer / consumer of netsnapper must // define a simulator class that handles these questions // so how should the server handle advancement? // I was thinking that it should just supply an Action which // can then loop over entities and create new snapshots however // you desire // but are we getting into a mess with client extrapolate (prediction) // being a different process than the server's engine calculations? // how do we keep the central logic condensed there? // I guess a bigger question is: are extrapolation methods sufficient for // proper prediction? I guess so if they're written sufficiently // (e.g. if you don't include collision detection it might predict runnning // through walls) // but maybe the real way to make things orderly is to require a engine-run // method from both client and server, and let the consumer game decide // what is done, with the understanding that on the client side // missing timestamps will be filled in with prediction // game can then structure this in a hybrid approach where the same // engine methods are called on both sides // and we'd have to remove the rollover extrapolation and replace it with // something more sophisticated here, right? that's tricky // and we need to account for, requesting frames that are between snapshots right? // are those just pure-extrapolated or do we need a system that can, // given a starting snapshot, calculate everything forward a given amount // on the server side, this system could be run and then new snapshots are generated // based on the results // on the client side, this system is run on advancement to generate new predictive // snapshots, and on frame to generate in-between frames // right? // so what we really need is some kind of advancement scaffold, // which has reusable memory for all the snapshots that must be loaded // and serves as a sandbox that prediction/calculation can be run in // and then the snapshots in there can be either rendered, stored // clientside as new silver-flags, or stored & networked by the server // as new gold-flags // Cleanup public void Removed() { // Called when this snapper is removed // salt the earth FirstData.Clear(); SecondData.Clear(); FirstData = null; SecondData = null; FirstEntityIdToInnerId = null; SecondEntityIdToInnerId = null; }
public Snapper(int firstWindowLength = 64, int secondWindowLength = 64) { Packer = new TPacker(); FirstWindowLength = firstWindowLength; SecondWindowLength = secondWindowLength; // setup entity data FirstData = new ReArrayIdPool <SnapHistory <TSnap, TStatic> >(4, byte.MaxValue + 1, () => { return(new SnapHistory <TSnap, TStatic>(firstWindowLength, true)); }, (s) => { for (int i = 0; i < s.Shots.Length; i++) { Packer.Clear(ref s.Shots[i]); } s.Clear(); }); SecondData = new ReArrayIdPool <SnapHistory <TSnap, TStatic> >(4, ushort.MaxValue + 1, () => { return(new SnapHistory <TSnap, TStatic>(secondWindowLength, false)); }, (s) => { for (int i = 0; i < s.Shots.Length; i++) { Packer.Clear(ref s.Shots[i]); } s.Clear(); }); // setup entityid to innerid maps FirstEntityIdToInnerId = new int[8]; for (int i = 0; i < FirstEntityIdToInnerId.Length; i++) { FirstEntityIdToInnerId[i] = -1; } SecondEntityIdToInnerId = new int[8]; for (int i = 0; i < SecondEntityIdToInnerId.Length; i++) { SecondEntityIdToInnerId[i] = -1; } }
public Receipt(ReArrayIdPool <Receipt> pool) { Data = new byte[UdpClientExtensions.MaxUdpSize]; Length = 0; EndPoint = (IPEndPoint)UdpClientExtensions.anyV4Endpoint; PoolId = -1; Pool = pool; PlayerId = 0; TargetPlayerId = 0; MessageId = 0; IsImmediate = false; Processed = false; CanBeReleased = false; }
private bool UnpackFull(ReArrayIdPool <SnapHistory <TSnap, TStatic> > data, int innerid, byte[] blob, int blobstart, int blobcount, ushort timestamp) { SnapHistory <TSnap, TStatic> sh = data.Values[data.IdsToIndices[innerid]]; int index = sh.FindIndex(timestamp); if (index < 0) { return(false); // out of bounds-- too far in future or past } Packer.UnpackFull(ref sh.Shots[index], ref sh.StaticData, blob, blobstart, blobcount); sh.Timestamps[index] = timestamp; sh.Flags[index] = SnapHistory <TSnap, TStatic> .FlagGold; // handle flag rollover Rollover(sh, index, timestamp); return(true); }
public void ServerAdvance() { for (int d = 0; d < NentDatas.Length; d++) { ReArrayIdPool<SnapHistory<TSnap, TStatic>> data = NentDatas[d]; for (int i = 0; i < data.Count; i++) { SnapHistory<TSnap, TStatic> h = data.Values[i]; if (h.PrevFlag != SnapHistory<TSnap, TStatic>.FlagGold) continue; // if the prevflag is not gold, // the entity does not exist at this timestamp // so we don't bother simulating it yet // now we advance the snapshot forward to the current point in // time, and then save it byte ownerPid = Advancer.GetInputPlayer(h.Shots[h.CurrentIndex], h.StaticData); if (ownerPid != 0) { // if we have no inputs, do regular advancement h.Shots[h.CurrentIndex] = h.Shots[h.PrevIndex]; if (!InputChecker(h, ref h.Shots[h.CurrentIndex], ownerPid, NetSnapper.TickMSTarget)) { Advancer.AdvanceLogic(AdvancerConfig, h, ref h.Shots[h.CurrentIndex], NetSnapper.TickMSTarget); } } else { // if this were client, we'd check the flag of the next. // as it is, just create the next from the current h.Shots[h.CurrentIndex] = h.Shots[h.PrevIndex]; Advancer.AdvanceLogic(AdvancerConfig, h, ref h.Shots[h.CurrentIndex], NetSnapper.TickMSTarget); } Nents.ServerSaveSimIntoCurrent(h); } } }
private bool UnpackFull(ReArrayIdPool <SnapHistory <TSnap, TStatic> > data, int innerid, byte[] blob, int blobstart, int blobcount, ushort timestamp) { SnapHistory <TSnap, TStatic> sh = data.Values[data.IdsToIndices[innerid]]; int index = sh.FindIndex(timestamp); if (index < 0) { return(false); // out of bounds-- too far in future or past } Packer.UnpackFull(ref sh.Shots[index], ref sh.StaticData, blob, blobstart, blobcount); sh.Timestamps[index] = timestamp; sh.Flags[index] = SnapHistory <TSnap, TStatic> .FlagGold; // ask the server to resimulate from this timestamp // to ensure our silver guesses get updated NetSnapper.RequestResimulate(timestamp); return(true); }
private int Ghost(ReArrayIdPool <SnapHistory <TSnap, TStatic> > data, ushort entityid, byte[] blob, int blobstart, int blobcount, ushort timestamp) { SnapHistory <TSnap, TStatic> sh = data.Request(); sh.EntityId = entityid; sh.LeadingIndex = 0; sh.Timestamps[0] = CurrentTime; // setup the rest of the future timestamps ushort itime = sh.Timestamps[0]; for (int i = 1; i <= sh.Shots.Length / 2; i++) { if (itime == ushort.MaxValue) { itime = 0; } else { itime++; } sh.Timestamps[i] = itime; } // and the past itime = sh.Timestamps[0]; for (int i = 0; i < sh.Shots.Length / 2; i++) { if (itime == 0) { itime = ushort.MaxValue; } else { itime--; } sh.Timestamps[sh.Shots.Length - 1 - i] = itime; } int expIndex = sh.FindIndex(timestamp); if (expIndex == -1) { // in this case, we need to just abandon the snapshot we // received. This can obviously create issues since we'll // need a full snapshot again. // however, we still unpack it in full, so the static data // can be constructed Packer.UnpackFull(ref sh.Shots[0], ref sh.StaticData, blob, blobstart, blobcount); sh.Flags[0] = SnapHistory <TSnap, TStatic> .FlagEmpty; } else { // if the snapshot we received has a space in the current window, // so just unpack into there Packer.UnpackFull(ref sh.Shots[expIndex], ref sh.StaticData, blob, blobstart, blobcount); sh.Timestamps[expIndex] = timestamp; sh.Flags[expIndex] = SnapHistory <TSnap, TStatic> .FlagGold; } // return the innerid return(sh.PoolId); }
// predicts forward a whole tick // this returns true if all impulse entities have conf snapshots // indicating that we can turn up the impulseTimestamp public bool ClientPredictTick(ushort confTimestamp) { ushort prevConfTimestamp = confTimestamp; if (prevConfTimestamp == 0) prevConfTimestamp = ushort.MaxValue; else prevConfTimestamp--; bool noMissingImpulse = true; for (int d = 0; d < NentDatas.Length; d++) { ReArrayIdPool<SnapHistory<TSnap, TStatic>> data = NentDatas[d]; for (int i = 0; i < data.Count; i++) { SnapHistory<TSnap, TStatic> h = data.Values[i]; if (h.PrevFlag == SnapHistory<TSnap, TStatic>.FlagEmpty || h.PrevFlag == SnapHistory<TSnap, TStatic>.FlagDeghosted) continue; // if the prev flag is empty, this entity does // not exist yet // if the entity has impulse, we may need to predict // if the current timestamp is after the impulse timestamp if (h.HasImpulse && ((NetSnapper.SimulateTimestamp >= h.ImpulseTimestamp && !(NetSnapper.SimulateTimestamp >= ushort.MaxValue - 500 && h.ImpulseTimestamp <= 500)) || (NetSnapper.SimulateTimestamp <= 500 && h.ImpulseTimestamp >= ushort.MaxValue - 500))) { // cases: // 0. if SimulateTimestamp is before h.ImpulseTimestamp, don't // predict, do normal interp // 1. if this entity has a gold snapshot in SimulateTimestamp + I, // then don't predict; use the server result // 2. if not, advance a predicted result // case 0 is accounted for by the if statement above // case 1 int confIndex = h.FindIndex(confTimestamp); if (confIndex != -1 && h.Flags[confIndex] == SnapHistory<TSnap, TStatic>.FlagGold && h.ImpulseTimestamp == NetSnapper.SimulateTimestamp) { // in this case, our job is easy, we have a server confirmed snap // so we just use that h.ImpulseShot = h.Shots[confIndex]; //NetSnapper.Server.NetLogger.Log(NetSnapper.SimulateTimestamp + " / " + confTimestamp + " F"); } else { // case 2 // in this case, we must predict based on the current ImpulseShot // because we got here, we are missing a conf for this impulse // so we can't move up the NetSnapper's ImpulseTimestamp noMissingImpulse = false; if (h.ImpulseTimestamp == NetSnapper.SimulateTimestamp) { // special consideration: if this is our first impulse, // we must populate ImpulseShot // if we have a confirmed shot for the previous timestamp, // we should use that // if we don't, this must be our first prediction, so we // can simply use the previous snapshot int prevConfIndex = h.FindIndex(prevConfTimestamp); if (prevConfIndex != -1 && h.Flags[prevConfIndex] == SnapHistory<TSnap, TStatic>.FlagGold) { h.ImpulseShot = h.Shots[prevConfIndex]; //NetSnapper.Server.NetLogger.Log(h.ImpulseTimestamp + " / " + prevConfTimestamp + " C"); } else { h.ImpulseShot = h.Shots[h.PrevIndex]; //NetSnapper.Server.NetLogger.Log(h.ImpulseTimestamp + " / " + prevConfTimestamp + " P"); } } // first, check if we have an input // we could only have an input if we are the owner of this object byte inputPid = Advancer.GetInputPlayer(h.ImpulseShot, h.StaticData); if (inputPid == Server.OurPlayerId) { // now check if we have any inputs // if so, process them if (!InputChecker(h, ref h.ImpulseShot, inputPid, NetSnapper.TickMSTarget)) { // if not, do a normal advance Advancer.AdvanceLogic(AdvancerConfig, h, ref h.ImpulseShot, NetSnapper.TickMSTarget); } } else { // if we couldn't have an input, just do a normal advance Advancer.AdvanceLogic(AdvancerConfig, h, ref h.ImpulseShot, NetSnapper.TickMSTarget); } } } // even if we did impulse above, we still want to interp // because we may need to create silver snapshots to use for // blending or impulse later // if we have a gold from the server, we don't need to do anything // because the current shot is already good if (h.CurrentFlag == SnapHistory<TSnap, TStatic>.FlagGold) continue; // (you might notice from the following, that we resimulate silver // flags [client guesses] each tick; this way they continue to update // their guesses if new server info arrives) // if not, we need to create a silver for the current shot // we know the previous is gold or silver already, since we checked // for empty or ghost way back above // so we know the prev is good for interp, but is the next? // if the next is also good for interp, we'll just interp h.Shots[h.CurrentIndex] = h.Shots[h.PrevIndex]; if (h.NextFlag == SnapHistory<TSnap, TStatic>.FlagGold || h.NextFlag == SnapHistory<TSnap, TStatic>.FlagSilver) { Advancer.InterpTickLogic(h); } else { // otherwise, do extrapolation Advancer.AdvanceLogic(AdvancerConfig, h, ref h.Shots[h.CurrentIndex], NetSnapper.TickMSTarget); } // save our guess snapshot as a new silver Nents.ClientSaveSimIntoCurrent(h); } } return noMissingImpulse; }
// Advance Methods public void ClientAdvance(int times, float tickms) { for (int t = 0; t < times; t++) { for (int d = 0; d < NentDatas.Length; d++) { ReArrayIdPool <SnapHistory <NentBasic2d, NentStaticBasic2d> > data = NentDatas[d]; for (int i = 0; i < data.Count; i++) { SnapHistory <NentBasic2d, NentStaticBasic2d> h = data.Values[i]; if (h.PrevFlag == SnapHistory <NentBasic2d, NentStaticBasic2d> .FlagEmpty || h.PrevFlag == SnapHistory <NentBasic2d, NentStaticBasic2d> .FlagDeghosted) { continue; // if the prev flag is empty, this entity does } // not exist yet // check to see if this is one of our player objects, // if so, we resimulate it according to our inputs bool skipAdvancement = false; if (h.StaticData.Id2 == NENT_PLAYEROBJ) { // store the player entities so we know which entity belongs // to which player PlayerEntityIds[h.StaticData.Id1] = (byte)h.EntityId; // we'll only have inputs if it is our player object // since we don't receive inputs for other players if (h.StaticData.Id1 == Server.OurPlayerId) { // if we have no inputs, skip advancement will be false // and we'll do regular simulation on this object (see below) h.Shots[h.CurrentIndex] = h.Shots[h.PrevIndex]; skipAdvancement = InputChecker(h, (byte)h.EntityId); } } if (!skipAdvancement) { if (h.CurrentFlag == SnapHistory <NentBasic2d, NentStaticBasic2d> .FlagGold) { continue; // if we already have a gold from the server, } // there is no purpose in resimulating. // we know the prev snapshot must be gold or silver, or we // wouldn't be here right now. So now we just need to see // what the next snapshot is. // if we have a gold snapshot ahead of us, we can just // interpolate between Prev and Next, which is less costly. // if not, we need to Advance from Prev to Current // check Next if (h.NextFlag != SnapHistory <NentBasic2d, NentStaticBasic2d> .FlagGold) { // we don't have gold ahead of us, so we must // simulate the current snapshot from previous h.Shots[h.CurrentIndex] = h.Shots[h.PrevIndex]; AdvanceLogic(h, NetSnapper.TickMSTarget); } else { // we can interpolate since the next flag is gold InterpolateSnapLogic(h); } } // if we have gold we would have already `continue;`d by now // so it's safe to save as silver here Nents.ClientSaveSimIntoCurrent(h); } } // load next NetSnapper.AdvanceSimulateTimestamp(); } // now advance to TickMS for (int d = 0; d < NentDatas.Length; d++) { ReArrayIdPool <SnapHistory <NentBasic2d, NentStaticBasic2d> > data = NentDatas[d]; for (int i = 0; i < data.Count; i++) { SnapHistory <NentBasic2d, NentStaticBasic2d> h = data.Values[i]; if (h.PrevFlag == SnapHistory <NentBasic2d, NentStaticBasic2d> .FlagEmpty || h.PrevFlag == SnapHistory <NentBasic2d, NentStaticBasic2d> .FlagDeghosted) { continue; // if the prev flag is empty, this entity does } // not exist yet if (h.CurrentFlag == SnapHistory <NentBasic2d, NentStaticBasic2d> .FlagGold) { // we have a gold flag, so we can interpolate InterpolateLogic(h, tickms); } else { // otherwise we must simulate AdvanceLogic(h, tickms); } } } // now Current is populated with the latest snapshots // adjusted forward to meet the current tickMS // this can be rendered by the client }
private int Ghost(ReArrayIdPool <SnapHistory <TSnap, TStatic> > data, ushort entityid, byte[] blob, int blobstart, int blobcount, ushort timestamp) { SnapHistory <TSnap, TStatic> sh = data.Request(); sh.EntityId = entityid; Packer.UnpackFull(ref sh.Shots[0], ref sh.StaticData, blob, blobstart, blobcount); sh.Timestamps[0] = timestamp; sh.Flags[0] = SnapHistory <TSnap, TStatic> .FlagGold; sh.LeadingIndex = 0; // now, figure how close we are to current time // under normal conditions, we should be receiving snapshots // very close to the CurrentTime int expIndex = sh.FindIndex(CurrentTime); // however, if the expIndex is -1, it means the snapshot // we just received is outside the current window // (because CurrentTime relative to it is outside the window) if (expIndex == -1) { // in this case, we need to just abandon the snapshot we // received. This can obviously create issues since we'll // need a full snapshot again. sh.Timestamps[0] = CurrentTime; sh.Flags[0] = SnapHistory <TSnap, TStatic> .FlagEmpty; } else { // in this case, we're within the window, so let's // just set the leading index properly. sh.LeadingIndex = expIndex; sh.Timestamps[sh.LeadingIndex] = CurrentTime; } // setup the rest of the future timestamps ushort itime = sh.Timestamps[0]; for (int i = 1; i <= sh.Shots.Length / 2; i++) { if (itime == ushort.MaxValue) { itime = 0; } else { itime++; } sh.Timestamps[i] = itime; } // and the past itime = sh.Timestamps[0]; for (int i = 0; i < sh.Shots.Length / 2; i++) { if (itime == 0) { itime = ushort.MaxValue; } else { itime--; } sh.Timestamps[sh.Shots.Length - 1 - i] = itime; } // return the innerid return(sh.PoolId); }
/// <summary> /// Tries to open a UdpSocket, starting with port, and if port is already /// in use, incrementing and retrying until it hits maxPort. /// </summary> /// <param name="port"></param> /// <param name="maxPort"></param> /// <param name="maxQueueSize"></param> /// <param name="netLogger"></param> public UdpSocket(int port, int maxPort, int maxQueueSize, NetLogger netLogger) { // check if port is available IPEndPoint[] actives = System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties().GetActiveUdpListeners(); while (true) { bool found = false; for (int i = 0; i < actives.Length; i++) { if (actives[i].Port == port) { port++; found = true; break; } } if (port > maxPort) { throw new Exception("Tried to open UDP socket, but first available port '" + port + "' exceeded maximum port '" + maxPort + "'"); } if (!found) { break; } } Port = port; NetLogger = netLogger; UdpClient = new UdpClient(port); DirectFromPoint = new IPEndPoint(IPAddress.Loopback, port); // important note: // the pool here is ordered, which means returning an item is less efficient // but it guarantees that the last receipt, which is the empty one being // written to, stays at the end of the pool. Pool = new ReArrayIdPool <Receipt>(10, maxQueueSize, PoolCreate, (obj) => { obj.Clear(); }); WritingReceipt = Pool.Request(); CancellationTokenSource = new CancellationTokenSource(); CancellationToken ctoken = CancellationTokenSource.Token; Task.Run(async() => { if (NetLogger.On) { NetLogger.Log("Opening [UDPv6] listener on port " + port + "..."); } while (!ctoken.IsCancellationRequested) { try { Receipt rec = await ReceiveAsync(); // the receipt, by nature of its existence, goes into the queue. } catch (Exception e) { if (NetLogger.On) { NetLogger.Error("UdpClient listener encountered exception on " + port, e); } } } if (NetLogger.On) { NetLogger.Log("Closing listener on port " + port + "..."); } try { UdpClient.Close(); UdpClient.Dispose(); } catch { } }); }
// predicts forward a set number of ms public void ClientPredictMS(float delta) { for (int d = 0; d < NentDatas.Length; d++) { ReArrayIdPool<SnapHistory<TSnap, TStatic>> data = NentDatas[d]; for (int i = 0; i < data.Count; i++) { SnapHistory<TSnap, TStatic> h = data.Values[i]; // if the impulse time has moved up, move up the entity's to match if (h.HasImpulse && ((NetSnapper.ClientImpulseTime >= 500 && h.ImpulseTimestamp < NetSnapper.ClientImpulseTime && h.ImpulseTimestamp >= NetSnapper.ClientImpulseTime - 500) || (NetSnapper.ClientImpulseTime < 500 && h.ImpulseTimestamp < NetSnapper.ClientImpulseTime) || (NetSnapper.ClientImpulseTime < 500 && h.ImpulseTimestamp >= ushort.MaxValue - 500))) h.ImpulseTimestamp = NetSnapper.ClientImpulseTime; if (h.PrevFlag == SnapHistory<TSnap, TStatic>.FlagEmpty || h.PrevFlag == SnapHistory<TSnap, TStatic>.FlagDeghosted) continue; // if the prev flag is empty, this entity does // not exist yet // if the entity has impulse, we may need to predict // if the current timestamp is after the impulse timestamp if (h.HasImpulse && ((NetSnapper.SimulateTimestamp >= h.ImpulseTimestamp && !(NetSnapper.SimulateTimestamp >= ushort.MaxValue - 500 && h.ImpulseTimestamp <= 500)) || (NetSnapper.SimulateTimestamp <= 500 && h.ImpulseTimestamp >= ushort.MaxValue - 500))) { // first, check if we have an input // we could only have an input if we are the owner of this object byte inputPid = Advancer.GetInputPlayer(h.ImpulseShot, h.StaticData); if (inputPid == Server.OurPlayerId) { // now check if we have any inputs // if so, process them if (!InputChecker(h, ref h.ImpulseShot, inputPid, delta)) { // if not, do a normal advance Advancer.AdvanceLogic(AdvancerConfig, h, ref h.ImpulseShot, delta); } } else { // if we couldn't have an input, just do a normal advance Advancer.AdvanceLogic(AdvancerConfig, h, ref h.ImpulseShot, delta); } // now check if we need to blend if (!h.PermanentImpulse && ((NetSnapper.SimulateTimestamp >= h.BlendTimestamp && !(NetSnapper.SimulateTimestamp >= ushort.MaxValue - 500 && h.BlendTimestamp <= 500)) || (NetSnapper.SimulateTimestamp <= 500 && h.BlendTimestamp >= ushort.MaxValue - 500))) { // we're in the Blend Zone // find the snapshot that we would be displaying, if this were not // an impulse. TSnap blendTarget = h.Shots[h.CurrentIndex]; if (h.NextFlag == SnapHistory<TSnap, TStatic>.FlagGold || h.NextFlag == SnapHistory<TSnap, TStatic>.FlagSilver) { Advancer.InterpMSLogic(h, ref blendTarget, delta, NetSnapper.TickMSTarget); } else { Advancer.AdvanceLogic(AdvancerConfig, h, ref blendTarget, delta); } // determine the blend factor int blendTicks = NetSnapper.SimulateTimestamp; if (NetSnapper.SimulateTimestamp <= 500 && h.BlendTimestamp >= ushort.MaxValue - 500) { blendTicks = (ushort.MaxValue - h.BlendTimestamp) + 1 + NetSnapper.SimulateTimestamp; } else { blendTicks -= h.BlendTimestamp; } h.ActiveBlendFactor = blendTicks * h.BlendFactor + h.BlendFactor * (delta/NetSnapper.TickMSTarget); if (h.ActiveBlendFactor > 1) { h.ActiveBlendFactor = 1; // special consideration: // if the impulse time equals the simulate time (in other words, // this is our first timestamp of the predictive window), and // our blend factor is already over 1, then we are already // back to being in-sync with the server, so we can remove impulse // from this entity. if (NetSnapper.ClientImpulseTime == NetSnapper.SimulateTimestamp) { h.HasImpulse = false; } } // now blend the shots together Advancer.BlendLogic(h, ref h.ImpulseShot, blendTarget, h.ActiveBlendFactor); } // finally, continue so we don't hit the interp logic below continue; } // if it's not impulse, we need to interp or extrap as normal // so we know the prev is good for interp, but is the next? // if the next is also good for interp, we'll just interp h.ImpulseShot = h.Shots[h.CurrentIndex]; if (h.NextFlag == SnapHistory<TSnap, TStatic>.FlagGold || h.NextFlag == SnapHistory<TSnap, TStatic>.FlagSilver) { Advancer.InterpMSLogic(h, ref h.ImpulseShot, delta, NetSnapper.TickMSTarget); } else { // otherwise, do extrapolation Advancer.AdvanceLogic(AdvancerConfig, h, ref h.ImpulseShot, delta); } // note: we don't save this, it is just for rendering } } }