3838import java .io .*;
3939import java .nio .file .Files ;
4040import java .nio .file .Path ;
41- import java .util .ArrayList ;
42- import java .util .Collection ;
43- import java .util .Collections ;
44- import java .util .List ;
41+ import java .util .*;
4542
4643public class LinearRegion implements Region {
4744
4845 public static final String FILE_SUFFIX = ".linear" ;
4946
47+ private static final List <Byte > SUPPORTED_VERSIONS = Arrays .asList ((byte ) 1 , (byte ) 2 );
5048 private static final long SUPERBLOCK = -4323716122432332390L ;
51- private static final byte VERSION = 1 ;
5249 private static final int HEADER_SIZE = 32 ;
5350 private static final int FOOTER_SIZE = 8 ;
5451
@@ -80,26 +77,28 @@ public Chunk loadChunk(int chunkX, int chunkZ, boolean ignoreMissingLightData) t
8077
8178 long superBlock = rawDataStream .readLong ();
8279 if (superBlock != SUPERBLOCK )
83- throw new RuntimeException ("Superblock invalid : " + superBlock + " file " + regionFile );
80+ throw new RuntimeException ("Invalid superblock : " + superBlock + " file " + regionFile );
8481
8582 byte version = rawDataStream .readByte ();
86- if (version != VERSION )
87- throw new RuntimeException ("Version invalid : " + version + " file " + regionFile );
83+ if (! SUPPORTED_VERSIONS . contains ( version ) )
84+ throw new RuntimeException ("Invalid version : " + version + " file " + regionFile );
8885
89- rawDataStream .skipBytes (11 ); // newestTimestamp + compression level + chunk count
86+ // Skip newestTimestamp (Long) + Compression level (Byte) + Chunk count (Short): Unused.
87+ rawDataStream .skipBytes (11 );
9088
9189 int dataCount = rawDataStream .readInt ();
9290 if (fileLength != HEADER_SIZE + dataCount + FOOTER_SIZE )
93- throw new RuntimeException ("File length invalid " + this .regionFile + " " + fileLength + " " + (HEADER_SIZE + dataCount + FOOTER_SIZE ));
91+ throw new RuntimeException ("Invalid file length: " + this .regionFile + " " + fileLength + " " + (HEADER_SIZE + dataCount + FOOTER_SIZE ));
9492
95- rawDataStream .skipBytes (8 ); // Data Hash
93+ // Skip data hash (Long): Unused.
94+ rawDataStream .skipBytes (8 );
9695
9796 byte [] rawCompressed = new byte [dataCount ];
9897 rawDataStream .readFully (rawCompressed , 0 , dataCount );
9998
10099 superBlock = rawDataStream .readLong ();
101100 if (superBlock != SUPERBLOCK )
102- throw new RuntimeException ("Footer superblock invalid " + this .regionFile );
101+ throw new RuntimeException ("Invalid footer superblock: " + this .regionFile );
103102
104103 try (DataInputStream dis = new DataInputStream (new ZstdInputStream (new ByteArrayInputStream (rawCompressed )))) {
105104 int x = chunkX - (regionPos .getX () << 5 );
@@ -108,8 +107,8 @@ public Chunk loadChunk(int chunkX, int chunkZ, boolean ignoreMissingLightData) t
108107 int skip = 0 ;
109108
110109 for (int i = 0 ; i < pos ; i ++) {
111- skip += dis .readInt (); // size of the chunk (bytes) to skip
112- dis .skipBytes (4 ); // skip 0 (will be timestamps)
110+ skip += dis .readInt (); // Size of the chunk (bytes) to skip
111+ dis .skipBytes (4 ); // Skip timestamps
113112 }
114113
115114 int size = dis .readInt ();
@@ -136,8 +135,9 @@ public Chunk loadChunk(int chunkX, int chunkZ, boolean ignoreMissingLightData) t
136135 public Collection <Vector2i > listChunks (long modifiedSince ) {
137136 if (Files .notExists (regionFile )) return Collections .emptyList ();
138137
138+ long fileLength ;
139139 try {
140- long fileLength = Files .size (regionFile );
140+ fileLength = Files .size (regionFile );
141141 if (fileLength == 0 ) return Collections .emptyList ();
142142 } catch (IOException ex ) {
143143 Logger .global .logWarning ("Failed to read file-size for file: " + regionFile );
@@ -149,21 +149,51 @@ public Collection<Vector2i> listChunks(long modifiedSince) {
149149 DataInputStream rawDataStream = new DataInputStream (inputStream )) {
150150
151151 long superBlock = rawDataStream .readLong ();
152- if (superBlock != SUPERBLOCK ) throw new RuntimeException ("Superblock invalid: " + superBlock + " file " + regionFile );
152+ if (superBlock != SUPERBLOCK )
153+ throw new RuntimeException ("Invalid superblock: " + superBlock + " file " + regionFile );
153154
154155 byte version = rawDataStream .readByte ();
155- if (version != VERSION ) throw new RuntimeException ("Version invalid: " + version + " file " + regionFile );
156+ if (!SUPPORTED_VERSIONS .contains (version ))
157+ throw new RuntimeException ("Invalid version: " + version + " file " + regionFile );
156158
157- long newestTimestamp = rawDataStream . readLong ( );
159+ int date = ( int ) ( modifiedSince / 1000 );
158160
159161 // If whole region is the same - skip.
160- if (newestTimestamp < modifiedSince / 1000 ) return Collections .emptyList ();
162+ long newestTimestamp = rawDataStream .readLong ();
163+ if (newestTimestamp < date ) return Collections .emptyList ();
164+
165+ // Linear v1 files store whole region timestamp, not chunk timestamp. We need to render the whole region file.
166+ if (version == 1 ) {
167+ for (int i = 0 ; i < 1024 ; i ++)
168+ chunks .add (new Vector2i ((regionPos .getX () << 5 ) + (i & 31 ), (regionPos .getY () << 5 ) + (i >> 5 )));
169+ return chunks ;
170+ }
171+ // Linear v2: Chunk timestamps are here!
172+ // Skip Compression level (Byte) + Chunk count (Short): Unused.
173+ rawDataStream .skipBytes (3 );
174+
175+ int dataCount = rawDataStream .readInt ();
176+ if (fileLength != HEADER_SIZE + dataCount + FOOTER_SIZE )
177+ throw new RuntimeException ("Invalid file length: " + this .regionFile + " " + fileLength + " " + (HEADER_SIZE + dataCount + FOOTER_SIZE ));
178+
179+ // Skip data hash (Long): Unused.
180+ rawDataStream .skipBytes (8 );
161181
162- // Linear files store whole region timestamp, not chunk timestamp. We need to render the while region file.
163- // TODO: Add per-chunk timestamps when .linear add support for per-chunk timestamps (soon)
164- for (int i = 0 ; i < 1024 ; i ++)
165- chunks .add (new Vector2i ((regionPos .getX () << 5 ) + (i & 31 ), (regionPos .getY () << 5 ) + (i >> 5 )));
166- return chunks ;
182+ byte [] rawCompressed = new byte [dataCount ];
183+ rawDataStream .readFully (rawCompressed , 0 , dataCount );
184+
185+ superBlock = rawDataStream .readLong ();
186+ if (superBlock != SUPERBLOCK )
187+ throw new RuntimeException ("Invalid footer SuperBlock: " + this .regionFile );
188+
189+ try (DataInputStream dis = new DataInputStream (new ZstdInputStream (new ByteArrayInputStream (rawCompressed )))) {
190+ for (int i = 0 ; i < 1024 ; i ++) {
191+ dis .skipBytes (4 ); // Skip size of the chunk
192+ int timestamp = dis .readInt ();
193+ if (timestamp >= date ) // Timestamps
194+ chunks .add (new Vector2i ((regionPos .getX () << 5 ) + (i & 31 ), (regionPos .getY () << 5 ) + (i >> 5 )));
195+ }
196+ }
167197 } catch (RuntimeException | IOException ex ) {
168198 Logger .global .logWarning ("Failed to read .linear file: " + regionFile + " (" + ex + ")" );
169199 }
0 commit comments