Skip to content

Commit 4df32de

Browse files
committed
Modified NetworkStateListener to keep a thread-safe (see caveats
of BufferedHashSet) set of active IDs. This can be used by the client-specific parts of the server to quickly determine what the player can/can't see.
1 parent 52b27ca commit 4df32de

File tree

4 files changed

+276
-16
lines changed

4 files changed

+276
-16
lines changed

release-notes.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ Version 1.3.1 (unreleased)
33
* Fixed zone ID calculation for non-uniform grids. See PR #2.
44
* Modified ZoneManager to automatically send "no-change" updates for
55
objects it is managing but didn't receive updates for. See PR #5
6+
* Added a thread-safe BufferedHashSet for creating "one writer, many readers"
7+
fast thread safe hash sets.
8+
* Added a double-buffered thread safe active IDs set to the NetworkStateListener.
69

710

811
Version 1.3.0 (latest)

src/main/java/com/simsilica/ethereal/NetworkStateListener.java

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,19 @@
3636

3737
package com.simsilica.ethereal;
3838

39+
import java.io.IOException;
40+
import java.util.*;
41+
import java.util.concurrent.ConcurrentLinkedQueue;
42+
43+
import org.slf4j.Logger;
44+
import org.slf4j.LoggerFactory;
45+
3946
import com.jme3.network.HostedConnection;
47+
4048
import com.simsilica.mathd.Quatd;
4149
import com.simsilica.mathd.Vec3i;
4250
import com.simsilica.mathd.Vec3d;
51+
4352
import com.simsilica.ethereal.net.ClientStateMessage;
4453
import com.simsilica.ethereal.net.SentState;
4554
import com.simsilica.ethereal.net.StateWriter;
@@ -48,14 +57,7 @@
4857
import com.simsilica.ethereal.zone.StateListener;
4958
import com.simsilica.ethereal.zone.ZoneGrid;
5059
import com.simsilica.ethereal.zone.ZoneKey;
51-
import java.io.IOException;
52-
import java.util.ArrayList;
53-
import java.util.Iterator;
54-
import java.util.List;
55-
import java.util.Objects;
56-
import java.util.concurrent.ConcurrentLinkedQueue;
57-
import org.slf4j.Logger;
58-
import org.slf4j.LoggerFactory;
60+
import com.simsilica.util.BufferedHashSet;
5961

6062

6163
/**
@@ -76,6 +78,8 @@ public class NetworkStateListener implements StateListener {
7678
private IdIndex idIndex;
7779
private SharedObjectSpace space;
7880

81+
private BufferedHashSet<Long> activeIds = new BufferedHashSet<>();
82+
7983
/**
8084
* Keeps track of who 'we' are for proper central zone setting
8185
*/
@@ -132,6 +136,13 @@ public Long getSelf() {
132136
return self;
133137
}
134138

139+
/**
140+
* Returns the set of all active IDs at this point in time.
141+
*/
142+
public Set<Long> getActiveIds() {
143+
return activeIds.getSnapshot();
144+
}
145+
135146
public ConnectionStats getConnectionStats() {
136147
return stats;
137148
}
@@ -245,7 +256,7 @@ public void endFrame( long time ) {
245256
if( log.isTraceEnabled() ) {
246257
log.trace(self + ":endFrame(" + time + ") selfPosition:" + selfPosition);
247258
}
248-
259+
249260
// See if we've gotten any ACKs to add to our ACK header
250261
ClientStateMessage ackedMsg;
251262
while( (ackedMsg = acked.poll()) != null ) {
@@ -329,6 +340,10 @@ public void endFrame( long time ) {
329340
}
330341
it.remove();
331342
idIndex.retireId(so.getNetworkId());
343+
344+
activeIds.remove(so.getEntityId());
345+
} else {
346+
activeIds.add(so.getEntityId());
332347
}
333348

334349
}
@@ -345,7 +360,9 @@ public void endFrame( long time ) {
345360
if( zoneIndex.setCenter(selfPosition, entered, exited) ) {
346361
zonesChanged = true;
347362
}
348-
}
363+
}
364+
365+
activeIds.commit();
349366
}
350367

351368

@@ -373,8 +390,7 @@ public void stateChanged( StateBlock b ) {
373390
+ " removals:" + b.getRemovals());
374391
}
375392

376-
if( b.getUpdates() != null ) {
377-
393+
if( b.getUpdates() != null ) {
378394
for( StateEntry e : b.getUpdates() ) {
379395

380396
Vec3d pos = e.getPosition();
@@ -397,6 +413,7 @@ public void stateChanged( StateBlock b ) {
397413
log.trace(self + ":No updates");
398414
}
399415
}
416+
400417
if( b.getRemovals() != null ) {
401418
for( Long e : b.getRemovals() )
402419
{
@@ -414,7 +431,6 @@ public void stateChanged( StateBlock b ) {
414431
}
415432
so.markRemoved(time);
416433
}
417-
}
418-
434+
}
419435
}
420436
}

src/main/java/com/simsilica/ethereal/zone/StateCollector.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ protected void publishFrame( StateFrame frame ) {
227227
*/
228228
protected void collect() {
229229
log.trace("collect()");
230-
230+
231231
// Purge any pending removals
232232
StateListener remove;
233233
while( (remove = removed.poll()) != null ) {
@@ -255,7 +255,7 @@ protected void collect() {
255255
for( StateListener l : listeners ) {
256256
l.endFrameBlock();
257257
}
258-
258+
259259
// end = System.nanoTime();
260260
// System.out.println( "State published in:" + ((end - start)/1000000.0) + " ms" );
261261
log.trace("end collect()");
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
/*
2+
* $Id$
3+
*
4+
* Copyright (c) 2018, Simsilica, LLC
5+
* All rights reserved.
6+
*
7+
* Redistribution and use in source and binary forms, with or without
8+
* modification, are permitted provided that the following conditions
9+
* are met:
10+
*
11+
* 1. Redistributions of source code must retain the above copyright
12+
* notice, this list of conditions and the following disclaimer.
13+
*
14+
* 2. Redistributions in binary form must reproduce the above copyright
15+
* notice, this list of conditions and the following disclaimer in
16+
* the documentation and/or other materials provided with the
17+
* distribution.
18+
*
19+
* 3. Neither the name of the copyright holder nor the names of its
20+
* contributors may be used to endorse or promote products derived
21+
* from this software without specific prior written permission.
22+
*
23+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
24+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
25+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
26+
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
27+
* COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
28+
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
29+
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
31+
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
32+
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
33+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
34+
* OF THE POSSIBILITY OF SUCH DAMAGE.
35+
*/
36+
37+
package com.simsilica.util;
38+
39+
import java.util.*;
40+
41+
//import com.google.common.collect.Iterators;
42+
43+
/**
44+
* A HashSet implementation that buffers modifications until a commit
45+
* is performeed. The buffering can be done from one thread and the
46+
* reads from another. So reads are safe from any thread but writes
47+
* must be done from only one thread or properly synchronized externally.
48+
* This is useful for efficiently buffering the output of one process
49+
* for another.
50+
*
51+
* Note: this doubles the storage requirements over a typical HashSet
52+
* is it internally keeps two sets, one for the writer and one for all
53+
* readers.
54+
*
55+
* Also note: the semantics of the way reads and writes are decoupled
56+
* mean that it is possible for add(element) to return false (indicating
57+
* that the set already has the value) while contains(element) will also
58+
* return false (because the element hasn't been committed yet). The
59+
* writing thread should use getTransaction() if it wants to check
60+
* transactional data integrity for some reason.
61+
*
62+
* @author Paul Speed
63+
*/
64+
public class BufferedHashSet<E> implements Set<E> {
65+
66+
private HashSet<E> buffer = new HashSet<>();
67+
private volatile HashSet<E> delegate = new HashSet<>();
68+
private Thread writer = null;
69+
70+
public BufferedHashSet() {
71+
}
72+
73+
private boolean checkThread() {
74+
if( writer == null ) {
75+
writer = Thread.currentThread();
76+
return true;
77+
}
78+
return false;
79+
}
80+
81+
private String badThreadMessage() {
82+
return "Non-write thread:" + Thread.currentThread() + " accessing as writer:" + writer;
83+
}
84+
85+
/**
86+
* Returns the transactional buffer. Note: access to this
87+
* is only thread safe from the writing thread.
88+
*/
89+
public Set<E> getTransaction() {
90+
assert checkThread() : badThreadMessage();
91+
return buffer;
92+
}
93+
94+
/**
95+
* Called from the writing thread to apply the buffer changes
96+
* to the readable view.
97+
*/
98+
public void commit() {
99+
assert checkThread() : badThreadMessage();
100+
delegate = buffer;
101+
buffer = (HashSet<E>)delegate.clone();
102+
}
103+
104+
/**
105+
* Returns a snapshot of the readable view of this HashSet at the
106+
* time this method is called. Subsequent commits will not affect
107+
* this view.
108+
*/
109+
public Set<E> getSnapshot() {
110+
return Collections.unmodifiableSet(delegate);
111+
}
112+
113+
@Override
114+
public int size() {
115+
return delegate.size();
116+
}
117+
118+
@Override
119+
public boolean isEmpty() {
120+
return delegate.isEmpty();
121+
}
122+
123+
@Override
124+
public boolean contains( Object o ) {
125+
return delegate.contains(o);
126+
}
127+
128+
@Override
129+
public Iterator<E> iterator() {
130+
//return Iterators.unmodifiableIterator(delegate.iterator());
131+
return delegate.iterator();
132+
}
133+
134+
@Override
135+
public Object[] toArray() {
136+
return delegate.toArray();
137+
}
138+
139+
@Override
140+
public <T> T[] toArray( T[] a ) {
141+
return delegate.toArray(a);
142+
}
143+
144+
/**
145+
* Updates the write buffer to reflect the addition of the specified element.
146+
* This won't show up to the read methods until a commit() is performed.
147+
* Callers wishing to see data in the buffer must use the Set returned
148+
* from getTransaction().
149+
*/
150+
@Override
151+
public boolean add( E e ) {
152+
assert checkThread() : badThreadMessage();
153+
return buffer.add(e);
154+
}
155+
156+
/**
157+
* Updates the write buffer to reflect the removal of the specified element.
158+
* This won't show up to the read methods until a commit() is performed.
159+
* Callers wishing to see data in the buffer must use the Set returned
160+
* from getTransaction().
161+
*/
162+
@Override
163+
public boolean remove( Object o ) {
164+
assert checkThread() : badThreadMessage();
165+
return buffer.remove(o);
166+
}
167+
168+
@Override
169+
public boolean containsAll( Collection<?> c ) {
170+
return delegate.containsAll(c);
171+
}
172+
173+
/**
174+
* Updates the write buffer to reflect the removal of the specified elements.
175+
* This won't show up to the read methods until a commit() is performed.
176+
* Callers wishing to see data in the buffer must use the Set returned
177+
* from getTransaction().
178+
*/
179+
@Override
180+
public boolean addAll( Collection<? extends E> c ) {
181+
return buffer.addAll(c);
182+
}
183+
184+
/**
185+
* Updates the write buffer to reflect the retention of the specified elements.
186+
* This won't show up to the read methods until a commit() is performed.
187+
* Callers wishing to see data in the buffer must use the Set returned
188+
* from getTransaction().
189+
*/
190+
@Override
191+
public boolean retainAll( Collection<?> c ) {
192+
assert checkThread() : badThreadMessage();
193+
return buffer.retainAll(c);
194+
}
195+
196+
/**
197+
* Updates the write buffer to reflect the removal of the specified elements.
198+
* This won't show up to the read methods until a commit() is performed.
199+
* Callers wishing to see data in the buffer must use the Set returned
200+
* from getTransaction().
201+
*/
202+
@Override
203+
public boolean removeAll( Collection<?> c ) {
204+
assert checkThread() : badThreadMessage();
205+
return buffer.removeAll(c);
206+
}
207+
208+
/**
209+
* Clears the write buffer.
210+
* This won't show up to the read methods until a commit() is performed.
211+
* Callers wishing to see data in the buffer must use the Set returned
212+
* from getTransaction().
213+
*/
214+
@Override
215+
public void clear() {
216+
assert checkThread() : badThreadMessage();
217+
buffer.clear();
218+
}
219+
220+
@Override
221+
public boolean equals( Object o ) {
222+
if( o == this ) {
223+
return true;
224+
}
225+
if( o == null || o.getClass() != getClass() ) {
226+
return false;
227+
}
228+
BufferedHashSet other = (BufferedHashSet)o;
229+
return Objects.equals(delegate, other.delegate);
230+
}
231+
232+
@Override
233+
public int hashCode() {
234+
return delegate.hashCode();
235+
}
236+
237+
@Override
238+
public String toString() {
239+
return delegate.toString();
240+
}
241+
}

0 commit comments

Comments
 (0)