mirror of
https://github.com/sbrl/Nibriboard.git
synced 2018-01-10 21:33:49 +00:00
296 lines
9.2 KiB
JavaScript
296 lines
9.2 KiB
JavaScript
"use strict";
|
|
|
|
import ChunkReference from './ChunkReference';
|
|
import Chunk from './Chunk';
|
|
import Rectangle from './Utilities/Rectangle';
|
|
import Vector from './Utilities/Vector';
|
|
|
|
class ChunkCache
|
|
{
|
|
constructor(inBoardWindow)
|
|
{
|
|
this.boardWindow = inBoardWindow;
|
|
this.cache = new Map();
|
|
|
|
// Whether to highlight rendered chunks. Useful for debugging purposes.
|
|
this.showRenderedChunks = false;
|
|
|
|
this.boardWindow.rippleLink.on("ChunkUpdate", this.handleChunkUpdate.bind(this));
|
|
|
|
/** A few presetsymbols for various non-chunk entries in the cache. */
|
|
this.cacheTypes = {
|
|
/** An empty chunk @type {Symbol} */
|
|
empty: Symbol("empty-chunk"),
|
|
/** A chunk that's been requested from the server, but hasn't arrived yet. @type {Symbol} */
|
|
requested: Symbol("requested-chunk")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetches the chunk with the specified chunk reference.
|
|
* @param {ChunkReference} chunkRef The chunk reference of the chunk to fetch.
|
|
* @param {bool} quiet Whether to be quiet if the chunk wasn't found.
|
|
* @return {Chunk|null} The requested chunk, or null if it isn't present in the cache.
|
|
*/
|
|
fetchChunk(chunkRef, quiet)
|
|
{
|
|
// Return null if we don't currently have that chunk in storage
|
|
if(!this.cache.has(chunkRef.toString()))
|
|
return null;
|
|
|
|
let requestedChunk = this.cache.get(chunkRef.toString());
|
|
if(!this.isChunk(requestedChunk)) {
|
|
if(!quiet)
|
|
console.warn(`Attempt to access a chunk at ${chunkRef} that's not loaded yet.`);
|
|
return null;
|
|
}
|
|
return requestedChunk;
|
|
}
|
|
|
|
/**
|
|
* Walk the currently cached chunks to find all the line fragments for the
|
|
* specified line id, starting at the specified chunk reference.
|
|
* @param {ChunkReference} startingChunkRef The reference of hte chunk we should start walking at.
|
|
* @param {string} lineId The unique id of the first line fragment we should include in the list.
|
|
* @return {object[]} A list of line fragments found.
|
|
*/
|
|
fetchLineFragments(startingChunkRef, lineUniqueId)
|
|
{
|
|
let lineFragments = [];
|
|
let currentChunk = this.fetchChunk(startingChunkRef);
|
|
let nextUniqueId = lineUniqueId;
|
|
|
|
while(currentChunk instanceof Chunk) // No need to search empty chunks
|
|
{
|
|
let nextLineFragment = currentChunk.getLineByUniqueId(nextUniqueId);
|
|
if(nextLineFragment == null)
|
|
break;
|
|
|
|
lineFragments.push(nextLineFragment);
|
|
|
|
if(nextLineFragment.ContinuesIn == null)
|
|
break;
|
|
|
|
currentChunk = this.fetchChunk(nextLineFragment.ContinuesIn);
|
|
nextUniqueId = nextLineFragment.ContinuesWithId;
|
|
}
|
|
|
|
return lineFragments;
|
|
}
|
|
|
|
/**
|
|
* Deletes all the stray chunk requests we've filed in the chunk cache.
|
|
* @return {[type]} [description]
|
|
*/
|
|
clearRequestedChunks()
|
|
{
|
|
for(let [chunkRefStr, chunk] of this.cache.entries()) {
|
|
if(chunk == this.cacheTypes.requested)
|
|
this.cache.delete(chunkRefStr);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds the given chunk to the chunk cache.
|
|
* @param {Chunk} chunkData The chunk to add to the cache.
|
|
* @returns {bool} Whether we actually updated an existing chunk in the
|
|
* cache instead of adding a new one.
|
|
*/
|
|
add(chunkData, override = false)
|
|
{
|
|
var hasChunk = this.cache.has(chunkData.chunkRef.toString()) &&
|
|
this.cache.get(chunkData.chunkRef.toString()) instanceof Chunk;
|
|
|
|
if(!override && hasChunk)
|
|
throw new Error("Error: We already have a chunk at that location stored.");
|
|
|
|
this.cache.set(chunkData.chunkRef.toString(), chunkData);
|
|
|
|
return !hasChunk;
|
|
}
|
|
|
|
/**
|
|
* Prunes the cache of all the chunks that haven't been seen on (or near)
|
|
* the screen for at least the specified number of milliseconds.
|
|
* @param {number} msSinceSeen The minimum number of milliseconds since a chunk as been seen for it to be unloaded.
|
|
* @return {number} The number of chunks unloaded.
|
|
*/
|
|
prune(msSinceSeen)
|
|
{
|
|
let now = +new Date(), chunksPruned = 0;
|
|
for(let [cRefStr, chunk] of this.cache) {
|
|
// Skip over symbols and other such oddities
|
|
// future: handle these
|
|
if(!(chunk instanceof Chunk))
|
|
continue;
|
|
|
|
if(typeof chunk.lastSeen == "undefined") debugger;
|
|
|
|
if(now - chunk.lastSeen.getTime() >= msSinceSeen) {
|
|
this.cache.delete(cRefStr);
|
|
chunksPruned++;
|
|
}
|
|
}
|
|
return chunksPruned;
|
|
}
|
|
|
|
/**
|
|
* Works out whether _thing_ is a chunk or not.
|
|
* @param {object} thing The thing to analyze.
|
|
* @return {bool} Whether _thing_ is a chunk or not.
|
|
*/
|
|
isChunk(thing) {
|
|
if(thing instanceof Chunk)
|
|
return true;
|
|
if(thing === this.cacheTypes.empty)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Updates the chunk cache ready for the next frame.
|
|
* @param {number} dt The amount of time, in milliseconds, since that last frame was rendered.
|
|
* @param {viewport_object} visibleArea The area that's currently visible on-screen.
|
|
*/
|
|
update(dt, visibleArea)
|
|
{
|
|
let chunkSize = this.boardWindow.gridSize;
|
|
let chunkArea = ChunkCache.CalculateChunkArea(visibleArea, chunkSize);
|
|
|
|
// Collect a list of missing chunks
|
|
let missingChunks = [];
|
|
for(let cx = chunkArea.x; cx <= chunkArea.x + chunkArea.width; cx += chunkSize)
|
|
{
|
|
for(let cy = chunkArea.y; cy <= chunkArea.y + chunkArea.height; cy += chunkSize)
|
|
{
|
|
let cChunk = new ChunkReference(
|
|
this.boardWindow.currentPlaneName,
|
|
cx / chunkSize, cy / chunkSize
|
|
);
|
|
let chunk = this.cache.get(cChunk.toString());
|
|
if(!this.isChunk(chunk) && chunk != this.cacheTypes.requested) {
|
|
console.info(`Requesting ${cChunk}`);
|
|
missingChunks.push(cChunk);
|
|
this.cache.set(cChunk.toString(), this.cacheTypes.requested);
|
|
}
|
|
else if(chunk instanceof Chunk){
|
|
// It's a real (non-empty), so update it
|
|
chunk.update(dt);
|
|
}
|
|
}
|
|
}
|
|
|
|
if(missingChunks.length > 0) {
|
|
// Make sure that the server knows our current viewport
|
|
this.boardWindow.sendViewport();
|
|
// Asynchronously request the missing chunks from the server
|
|
this.boardWindow.rippleLink.send({
|
|
"Event": "ChunkUpdateRequest",
|
|
"ForgottenChunks": missingChunks
|
|
});
|
|
}
|
|
|
|
// Prune chunks from the cache that haven't been accessed in 60
|
|
// seconds or more
|
|
let prunedChunkCount = this.prune(60 * 1000);
|
|
if(prunedChunkCount > 0)
|
|
console.debug(`Pruned ${prunedChunkCount} from the local chunk cache, leaving ${this.cache.size} entries total remaining.`);
|
|
}
|
|
|
|
/**
|
|
* Renders the specified area to the given canvas with the given context.
|
|
* @param {Rectangle} visibleArea The area to render.
|
|
* @param {number} chunkSize The size of the chunks on the current plane.
|
|
* @param {HTMLCanvasElement} canvas The canvas to draw on.
|
|
* @param {CanvasRenderingContext2D} context The rendering context to
|
|
* use to draw on the canvas.
|
|
*/
|
|
renderVisible(visibleArea, canvas, context)
|
|
{
|
|
context.save();
|
|
let chunkSize = this.boardWindow.gridSize;
|
|
let chunkArea = ChunkCache.CalculateChunkArea(visibleArea, chunkSize);
|
|
|
|
for(let cx = chunkArea.x; cx <= chunkArea.x + chunkArea.width; cx += chunkSize)
|
|
{
|
|
for(let cy = chunkArea.y; cy <= chunkArea.y + chunkArea.height; cy += chunkSize)
|
|
{
|
|
let cChunk = new ChunkReference(
|
|
this.boardWindow.currentPlaneName,
|
|
// Translate from plane space to chunk space when creating
|
|
// a _chunk_ reference
|
|
cx / chunkSize, cy / chunkSize
|
|
);
|
|
|
|
let chunk = this.fetchChunk(cChunk, true);
|
|
if(chunk != null)
|
|
chunk.render(canvas, context, this, chunkArea);
|
|
|
|
if(this.showRenderedChunks) {
|
|
context.beginPath();
|
|
context.fillStyle = `hsla(270.5, 79.6%, 55.9%, ${typeof chunk != "undefined" ? 0.3 : 0.1})`;
|
|
context.fillRect(cx, cy, chunkSize, chunkSize);
|
|
}
|
|
}
|
|
}
|
|
|
|
context.restore();
|
|
}
|
|
|
|
handleChunkUpdate(message)
|
|
{
|
|
for (let chunkData of message.Chunks) {
|
|
let newChunkRef = new ChunkReference(
|
|
this.boardWindow.currentPlaneName,
|
|
chunkData.Location.X,
|
|
chunkData.Location.Y
|
|
);
|
|
|
|
console.info(`Chunk Update @ ${newChunkRef}`);
|
|
|
|
let newChunk = new Chunk(newChunkRef, chunkData.Size);
|
|
let newLines = chunkData.lines.map((line) => {
|
|
line.Points = line.Points.map((raw) => new Vector(raw.X, raw.Y));
|
|
if(line.ContinuesIn != null) {
|
|
line.ContinuesIn = new ChunkReference(
|
|
this.boardWindow.currentPlaneName,
|
|
line.ContinuesIn.X, line.ContinuesIn.Y
|
|
);
|
|
}
|
|
if(line.ContinuesFrom != null) {
|
|
line.ContinuesFrom = new ChunkReference(
|
|
this.boardWindow.currentPlaneName,
|
|
line.ContinuesFrom.X, line.ContinuesFrom.Y
|
|
);
|
|
}
|
|
line.ContainingChunk = new ChunkReference(
|
|
this.boardWindow.currentPlaneName,
|
|
line.ContainingChunk.X,
|
|
line.ContainingChunk.Y
|
|
);
|
|
return line;
|
|
});
|
|
newChunk.lines = newChunk.lines.concat(newLines);
|
|
|
|
this.add(newChunk, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculates the area of the chunks that cover a specified box.
|
|
* @param {Rectangle} visibleArea The box to calculate the covering chunks for.
|
|
* @param {number} chunkSize The size of the chunks that cover the box.
|
|
* @return {Rectangle} The area of the chunks that cover the box.
|
|
*/
|
|
ChunkCache.CalculateChunkArea = function(visibleArea, chunkSize) {
|
|
return new Rectangle(
|
|
Math.floor(visibleArea.x / chunkSize) * chunkSize,
|
|
Math.floor(visibleArea.y / chunkSize) * chunkSize,
|
|
(Math.ceil((Math.abs(visibleArea.x) + (visibleArea.width)) / chunkSize) * chunkSize),
|
|
(Math.ceil((Math.abs(visibleArea.y) + (visibleArea.height)) / chunkSize) * chunkSize)
|
|
);
|
|
}
|
|
|
|
export default ChunkCache;
|