if(!isObject(RSTerrain_Grass)) datablock StaticShapeData(RSTerrain_Grass) {
	shapeFile = "Add-Ons/Server_Runescape/shapes/terrainChunks/grass.dts";
};
if(!isObject(RSTerrain_GrassStonePath_Cross)) datablock StaticShapeData(RSTerrain_GrassStonePath_Cross) {
	shapeFile = "Add-Ons/Server_Runescape/shapes/terrainChunks/grassStonePath_Cross.dts";
};
if(!isObject(RSTerrain_GrassStonePath_T)) datablock StaticShapeData(RSTerrain_GrassStonePath_T) {
	shapeFile = "Add-Ons/Server_Runescape/shapes/terrainChunks/grassStonePath_T.dts";
};
if(!isObject(RSTerrain_GrassStonePath_Straight)) datablock StaticShapeData(RSTerrain_GrassStonePath_Straight) {
	shapeFile = "Add-Ons/Server_Runescape/shapes/terrainChunks/grassStonePath_Straight.dts";
};
if(!isObject(RSTerrain_GrassStonePath_Corner)) datablock StaticShapeData(RSTerrain_GrassStonePath_Corner) {
	shapeFile = "Add-Ons/Server_Runescape/shapes/terrainChunks/grassStonePath_Corner.dts";
};
if(!isObject(RSTerrain_GrassStonePath_toWaterbed)) datablock StaticShapeData(RSTerrain_GrassStonePath_toWaterbed) {
	shapeFile = "Add-Ons/Server_Runescape/shapes/terrainChunks/grassStonePath_toWaterbed.dts";
};
// Water
if(!isObject(RSTerrain_grassToWaterbed)) datablock StaticShapeData(RSTerrain_grassToWaterbed) {
	shapeFile = "Add-Ons/Server_Runescape/shapes/terrainChunks/grassToWaterbed.dts";
};
if(!isObject(RSTerrain_grassToWaterbed_Corner)) datablock StaticShapeData(RSTerrain_grassToWaterbed_Corner) {
	shapeFile = "Add-Ons/Server_Runescape/shapes/terrainChunks/grassToWaterbed_Corner.dts";
};
if(!isObject(RSTerrain_grassToWaterbed_CornerInverted)) datablock StaticShapeData(RSTerrain_grassToWaterbed_CornerInverted) {
	shapeFile = "Add-Ons/Server_Runescape/shapes/terrainChunks/grassToWaterbed_InvertedCorner.dts";
};
if(!isObject(RSTerrain_riverStraight)) datablock StaticShapeData(RSTerrain_riverStraight) {
	shapeFile = "Add-Ons/Server_Runescape/shapes/terrainChunks/river_straight.dts";
};
if(!isObject(RSTerrain_riverCorner)) datablock StaticShapeData(RSTerrain_riverCorner) {
	shapeFile = "Add-Ons/Server_Runescape/shapes/terrainChunks/river_corner.dts";
};
if(!isObject(RSTerrain_dirtToOcean)) datablock StaticShapeData(RSTerrain_dirtToOcean) {
	shapeFile = "Add-Ons/Server_Runescape/shapes/terrainChunks/dirtToOcean.dts";
};
if(!isObject(RSTerrain_oceanCorner)) datablock StaticShapeData(RSTerrain_oceanCorner) {
	shapeFile = "Add-Ons/Server_Runescape/shapes/terrainChunks/ocean_corner.dts";
};
if(!isObject(RSTerrain_ocean)) datablock StaticShapeData(RSTerrain_ocean) {
	shapeFile = "Add-Ons/Server_Runescape/shapes/terrainChunks/ocean.dts";
};
if(!isObject(RSTerrain_oceanCornerInverted)) datablock StaticShapeData(RSTerrain_oceanCornerInverted) {
	shapeFile = "Add-Ons/Server_Runescape/shapes/terrainChunks/ocean_cornerInverted.dts";
};

function RS_getMaxChunkSize() {
	return 10 SPC 10;
}

function ShapeBase::getEulerRotation(%this) {
	return eulerDegFromAxisAngle(getWords(%this.getTransform(), 3, 6));
}

function ShapeBase::setTileDirection(%this, %dir) {
	%dir[2] = "0 0 1 3.1416";  // 0 0 180
	%dir[0] = "1 0 0 0";       // 0 0 0
	%dir[3] = "0 0 -1 1.5708"; // 0 0 90
	%dir[1] = "0 0 1 1.5708";  // 0 0 -90
	%this.setTransform(getWords(%this.getTransform(), 0, 2) SPC %dir[mClamp(%dir, 0, 3)]);
}

function ShapeBase::getTileDirection(%this) {
	%rot = getWord(eulerDegFromAxisAngle(getWords(%this.getTransform(), 3, 6)), 2);
	
	switch(mFloatLength(%rot, 0)) {
		case 180: return 2;
		case 0:   return 0;
		case 90:  return 3;
		case -90: return 1;
		default:  return -1;
	}
}

function ShapeBase::addRotation(%this, %eul) {
	%rot = eulerDegFromAxisAngle(getWords(%this.getTransform(), 3, 6));
	
	switch(%this.getTileDirection()) {
		case 0: %rot = vectorSub(%oRot, -getWord(%eul, 0) SPC getWord(%eul, 1) SPC getWord(%eul, 2));
		case 1: %rot = vectorAdd(%oRot, getWord(%eul, 1) SPC getWord(%eul, 0) SPC getWord(%eul, 2));
		case 2: %rot = vectorSub(%oRot, getWord(%eul, 0) SPC getWord(%eul, 1) SPC getWord(%eul, 2));
		case 3: %rot = vectorSub(%oRot, getWord(%eul, 1) SPC getWord(%eul, 0) SPC getWord(%eul, 2));
		case -1: return;
	}
	
	%this.setTransform(rotateTransform(%this.getTransform(), %rot));
}

if(!isObject(RSTerrainGroup)) new SimGroup(RSTerrainGroup);

function RS_debugTerrain_Checkerboard() {
	for(%i=0;%i<RSTerrainGroup.getCount();%i++) {
		%g = RSTerrainGroup.getObject(%i);
		
		for(%j=0;%j<%g.getCount();%j++) {
			%obj = %g.getObject(%j);
			
			%obj.setNodeColor("ALL", ((%val ^= 1) ? "1 0 0 1" : "1 0 1 1"));
			%obj.setShapeName("(" @ %itr++ @ "): " @ %obj.attributeCRC);
		}
	}
}

function rotateTransform(%transform, %deg) {
	%newTransform = matrixCreateFromEuler(mDegToRad(getWord(%deg, 0)) SPC mDegToRad(getWord(%deg, 1)) SPC mDegToRad(getWord(%deg, 2)));
	%transform    = MatrixMultiply(%transform, %newTransform);
	return %transform;
}

function RS_TerrainChunk::setHighlighted(%this, %val) {
	if(%this.hilighted == %val) return;
	
	%this.hilighted = (%val = mClamp(%val, 0, 1));
	
	for(%i=0;%i<%this.getCount();%i++) {
		%obj = %this.getObject(%i);
		
		%obj.setNodeColor("ALL", (%val ? "1 1 1 0.3" : "1 1 1 1"));
	}
}

function RS_TerrainChunk::setGhostedToClient(%this, %client, %val) {
	if(!isObject(%client)) return;
	if(%val == %this.isGhostedToClient(%client)) return;
	
	%this.ghostStatus[%client] = (%val = mClamp(%val, 0, 1));
	
	for(%i=0;%i<%this.getCount();%i++) {
		%obj = %this.getObject(%i);
		
		if(%val) %obj.scopeToClient(%client);
		else %obj.clearScopeToClient(%client);
	}
	
	for(%i=0;%i<%this.objectCount;%i++) {
		%obj = %this.object[%i];
		
		if(!isObject(%obj)) {
			// Cleanup
			for(%j=%i + 1;%j<%this.objectCount + 2;%j++) %this.object[%j - 1] = %this.object[%j];
			
			%this.objectCount--;
			%i--;
			continue;
		}
		
		if(!%obj.ghostInit) {
			%obj.setNetFlag(6, 1);
			%obj.ghostInit = 1;
		}
		
		if(%val) %obj.scopeToClient(%client);
		else %obj.clearScopeToClient(%client);
	}
}

function RS_TerrainChunk::isGhostedToClient(%this, %client) {
	if(!isObject(%client)) return;
	
	return (%this.ghostStatus[%client] $= "" ? 1 : %this.ghostStatus[%client]);
}

function RS_clearTerrain() {
	for(%i=0;%i<RSTerrainGroup.getCount();%i++) if(isFunction((%g=RSTerrainGroup.getObject(%i)).getClassName(), "deleteAll")) %g.deleteAll();
	
	RSTerrainGroup.deleteAll();
	
	deleteVariables("$CACHE::Runescape::Server::TerrainChunks::obj*");
}

function RS_DumpTerrain() {
	%sizeW = getWord($CACHE::Runescape::Server::TerrainChunks::MapSize, 0);
	%sizeH = getWord($CACHE::Runescape::Server::TerrainChunks::MapSize, 1);
	
	echo(%sizeW @ "x" @ %sizeH);
	
	for(%y=0;%y<%sizeH;%y++) {
		%builtStr = "";
		
		for(%x=0;%x<%sizeW;%x++) {
			%data = $CACHE::Runescape::Server::TerrainChunks::obj[%x, %y];
			%eStr = getSubStr("00" @ %data.tileChr, strLen("00" @ %data.tileChr) - 2, 2);
			
			%builtStr = trim(%builtStr SPC %eStr);
		}
		echo(%builtStr);
	}
}

function RS_generateTerrain(%sizeW, %sizeH, %str, %origin, %group, %offset, %totalSize) {
	if(%offset $= "") %offset = "0";
	%origin   = (%origin $= "" ? "0 0 6" : %origin);
	%group    = (!isObject(%group) ? RSTerrainGroup : %group);
	%tileW    = 14;
	%tileH    = 14;
	%tempWOut = -1;
	%oOrigin  = getWord(%origin, 0) + (getWord(%offset, 0) * %tileW) SPC getWord(%origin, 1) + (getWord(%offset, 1) * %tileH);
	
	if($RS::Debug) {
		echo("$RS::Debug |RS_generateTerrain() CALLED:");
		echo("$RS::Debug |  > Size   = (" @ %sizeW @ ", " @ %sizeH @ ")");
		echo("$RS::Debug |  > Str    = " @ %str);
		echo("$RS::Debug |  > Origin = " @ %origin);
		echo("$RS::Debug |  > Group  = " @ %group);
		echo("$RS::Debug |  > Offset = " @ %offset);
	}
	
	// Tiles (plus rotation)
	// First TAB is scalar information. Format:
	// <scalar> <orientation when moving object back into place> <can be scaled along the X axis> <can be scaled along the Y axis>
	// <hasPathNode> <hasTerrainNode>
	// Water

	%t_["16"] = "0 1 0 0 0 1 0 0" TAB RSTerrain_riverStraight.getId() TAB "0 0 0";

	%t_["17"] = "0 1 0 1 1 0 0 0" TAB RSTerrain_riverStraight.getId() TAB "0 0 90";

	%t_["18"] = "1 1 1 0 0 0 0 0" TAB RSTerrain_riverCorner.getId() TAB "0 0 -90";

	%t_["19"] = "1 1 1 0 0 0 0 0" TAB RSTerrain_riverCorner.getId() TAB "0 0 180";

	%t_["20"] = "1 1 1 0 0 0 0 0" TAB RSTerrain_riverCorner.getId() TAB "0 0 90";

	%t_["21"] = "1 1 1 0 0 0 0 0" TAB RSTerrain_riverCorner.getId() TAB "0 0 0";

	

	%t_["22"] = "0 1 0 1 1 0 0 0" TAB RSTerrain_dirtToOcean.getId() TAB "0 0 90";

	%t_["23"] = "0 1 0 1 1 0 0 0" TAB RSTerrain_dirtToOcean.getId() TAB "0 0 -90";

	%t_["24"] = "0 1 0 0 0 0 0 0" TAB RSTerrain_dirtToOcean.getId() TAB "0 0 0";

	%t_["25"] = "0 1 0 0 0 1 0 0" TAB RSTerrain_dirtToOcean.getId() TAB "0 0 180";

	%t_["26"] = "1 1 1 0 0 0 0 0" TAB RSTerrain_oceanCornerInverted.getId() TAB "0 0 90";

	%t_["27"] = "1 1 1 0 0 0 0 0" TAB RSTerrain_oceanCornerInverted.getId() TAB "0 0 180";

	%t_["28"] = "1 1 1 0 0 0 0 0" TAB RSTerrain_oceanCornerInverted.getId() TAB "0 0 0";

	%t_["29"] = "1 1 1 0 0 0 0 0" TAB RSTerrain_oceanCornerInverted.getId() TAB "0 0 -90";

	%t_["30"] = "1 0 0 0 1 1 0 0" TAB RSTerrain_ocean.getId() TAB "0 0 0";

	

	// Grass

	%t_["32"] = "0 1 0 0 0 0 1 1" TAB RSTerrain_GrassStonePath_Straight.getId() TAB "0 0 0";  // ^ v

	%t_["33"] = "0 1 0 1 1 0 1 1" TAB RSTerrain_GrassStonePath_Straight.getId() TAB "0 0 90"; // < >

	%t_["34"] = "1 1 1 0 0 0 1 1" TAB RSTerrain_GrassStonePath_Corner.getId() TAB "0 0 -90";

	%t_["35"] = "1 1 1 0 0 0 1 1" TAB RSTerrain_GrassStonePath_Corner.getId() TAB "0 0 180";

	%t_["36"] = "1 1 1 0 0 0 1 1" TAB RSTerrain_GrassStonePath_Corner.getId() TAB "0 0 90";

	%t_["37"] = "1 1 1 0 0 0 1 1" TAB RSTerrain_GrassStonePath_Corner.getId() TAB "0 0 0";

	%t_["38"] = "1 1 1 0 0 0 1 1" TAB RSTerrain_GrassStonePath_T.getId() TAB "0 0 -90";

	%t_["39"] = "1 1 1 0 0 0 1 1" TAB RSTerrain_GrassStonePath_T.getId() TAB "0 0 90";

	%t_["40"] = "1 1 1 0 0 0 1 1" TAB RSTerrain_GrassStonePath_T.getId() TAB "0 0 180";

	%t_["41"] = "1 1 1 0 0 0 1 1" TAB RSTerrain_GrassStonePath_T.getId() TAB "0 0 0";

	%t_["42"] = "1 1 1 0 0 0 1 1" TAB RSTerrain_GrassStonePath_Cross.getId() TAB "0 0 0";

	%t_["43"] = "1 0 0 0 1 1 0 1" TAB RSTerrain_Grass.getId() TAB "0 0 0";

	%t_["44"] = "1 0 0 0 1 0 0 1" TAB RSTerrain_grassToWaterbed.getId() TAB "0 0 180";

	%t_["45"] = "1 0 0 0 1 0 0 1" TAB RSTerrain_grassToWaterbed.getId() TAB "0 0 0";

	%t_["46"] = "1 0 0 1 0 0 0 1" TAB RSTerrain_grassToWaterbed.getId() TAB "0 0 -90";

	%t_["47"] = "1 0 0 1 0 0 0 1" TAB RSTerrain_grassToWaterbed.getId() TAB "0 0 90";

	%t_["48"] = "1 1 1 0 0 0 0 0" TAB RSTerrain_oceanCorner.getId() TAB "0 0 -90";

	%t_["49"] = "1 1 1 0 0 0 0 0" TAB RSTerrain_oceanCorner.getId() TAB "0 0 0";

	%t_["50"] = "1 1 1 0 0 0 0 0" TAB RSTerrain_oceanCorner.getId() TAB "0 0 180";

	%t_["51"] = "1 1 1 0 0 0 0 0" TAB RSTerrain_oceanCorner.getId() TAB "0 0 90";

	%t_["54"] = "1 1 1 0 0 0 1 1" TAB RSTerrain_GrassStonePath_toWaterbed.getId() TAB "0 0 90";

	%t_["55"] = "1 1 1 0 0 0 1 1" TAB RSTerrain_GrassStonePath_toWaterbed.getId() TAB "0 0 0";

	%t_["56"] = "1 1 1 0 0 0 1 1" TAB RSTerrain_GrassStonePath_toWaterbed.getId() TAB "0 0 -90";

	%t_["57"] = "1 1 1 0 0 0 1 1" TAB RSTerrain_GrassStonePath_toWaterbed.getId() TAB "0 0 180";

	%t_["62"] = "1 1 1 0 0 0 0 1" TAB RSTerrain_grassToWaterbed_Corner.getId() TAB "0 0 180";

	%t_["63"] = "1 1 1 0 0 0 0 1" TAB RSTerrain_grassToWaterbed_Corner.getId() TAB "0 0 90";

	%t_["64"] = "1 1 1 0 0 0 0 1" TAB RSTerrain_grassToWaterbed_Corner.getId() TAB "0 0 0";

	%t_["65"] = "1 1 1 0 0 0 0 1" TAB RSTerrain_grassToWaterbed_Corner.getId() TAB "0 0 -90";

	%t_["66"] = "1 1 1 0 0 0 0 1" TAB RSTerrain_grassToWaterbed_CornerInverted.getId() TAB "0 0 0";

	%t_["67"] = "1 1 1 0 0 0 0 1" TAB RSTerrain_grassToWaterbed_CornerInverted.getId() TAB "0 0 90";

	%t_["68"] = "1 1 1 0 0 0 0 1" TAB RSTerrain_grassToWaterbed_CornerInverted.getId() TAB "0 0 -90";

	%t_["69"] = "1 1 1 0 0 0 0 1" TAB RSTerrain_grassToWaterbed_CornerInverted.getId() TAB "0 0 180";
	
	// Colors:   TERRAIN COLOR         PATH COLOR
	%_c["0"]  = "000 171 000 255" TAB "182 182 182 255"; // Grass with stone path
	%_c["1"]  = "000 171 000 255" TAB "155 118 083 255"; // Grass with dirt path
	%_c["2"]  = "000 120 000 255" TAB "182 182 182 255"; // Grass with stone path
	%_c["3"]  = "000 120 000 255" TAB "155 118 083 255"; // Grass with dirt path
	
	for(%i=0;%i<=getWordCount(%str);%i++) {
		if(%debugSafe++ >= 200) { echo("Broke terrain generation loop."); break; }
		%chr = strReplace(getWord(%str, %i), "_", " "); // Tile type
		%clr = getWord(%chr, 1);
		%hig = getWord(%chr, 2);
		%chr = getWord(%chr, 0);
		%x   = getWord(%offset, 0) + (%i % %sizeW);
		%y   = getWord(%offset, 1) + mFloor(%i / %sizeW);
		
		%dat = %t_[%chr];
		%sca = getField(%dat, 0);    // Can be stretched (optimization)
		%dbO = getField(%dat, 1);    // Datablock object
		%rot = getField(%dat, 2);    // Rotation
		
		// Additional info
		%ori = getWord(%sca, 3);     // Scalar Orientation
		%xSc = getWord(%sca, 4);     // Can this object be scaled along the X axis?
		%ySc = getWord(%sca, 5);     // Can this object be scaled along the Y axis?
		%n_p = getWord(%sca, 6);     // Does this object have a path node?
		%n_t = getWord(%sca, 7);     // Does this object have a terrain node?
		
		// Finish variables
		%sca = getWords(%sca, 0, 2); // Isolate scalar
		%doS = 0;                    // Attempt scale?
		
		// Apply the color for this tile
		%terrain_color = ((%d = getField(%_c[%clr], 0)) !$= "" ? ($CACHE::Runescape::Server::ChunkLoader::LastTerrainColor = getColorF(%d)) : $CACHE::Runescape::Server::ChunkLoader::LastTerrainColor);
		%path_color    = ((%d = getField(%_c[%clr], 1)) !$= "" ? ($CACHE::Runescape::Server::ChunkLoader::LastPathColor = getColorF(%d)) : $CACHE::Runescape::Server::ChunkLoader::LastPathColor);
		switch(%hig) {
			case -1: %heightOffset = $CACHE::Runescape::Server::ChunkLoader::LastHeightOffset; %heightChange = 0;
			case 0:  %heightOffset = (%heightChange = 1);
			case 1:  %heightOffset = ($CACHE::Runescape::Server::ChunkLoader::LastHeightOffset -= 1); %heightChange = -1;
			case 2:  %heightOffset = ($CACHE::Runescape::Server::ChunkLoader::LastHeightOffset -= 2); %heightChange = -2;
			case 3:  %heightOffset = ($CACHE::Runescape::Server::ChunkLoader::LastHeightOffset += 1); %heightChange = 1;
			case 4:  %heightOffset = ($CACHE::Runescape::Server::ChunkLoader::LastHeightOffset += 2); %heightChange = 2;
		}
		
		// Build the attribute CRC
		%crc_0 = getWord(strReplace(getWord(%str, %i + 1), "_", " "), 2); // Height of the tile to the right
		%crc_1 = getWord(strReplace(getWord(%str, %i - 1), "_", " "), 2); // Height of the tile to the left
		%crc_2 = getWord(strReplace(getWord(%str, %i - %sizeW), "_", " "), 2); // Height of the tile to the top
		%crc_3 = getWord(strReplace(getWord(%str, %i + %sizeW), "_", " "), 2); // Height of the tile to the bottom
		
		// Sanity check
		%crc_0 = (%crc_0 $= "" ? -1 : %crc_0);
		%crc_1 = (%crc_1 $= "" ? -1 : %crc_1);
		%crc_2 = (%crc_2 $= "" ? -1 : %crc_2);
		%crc_3 = (%crc_3 $= "" ? -1 : %crc_3);
		
		%crc = getStringCRC(%heightOffset SPC %clr SPC %heightChange SPC %crc_0 SPC %crc_1 SPC %crc_2 SPC %crc_3);
		
		if(!isObject(%dbO) && %i != getWordCount(%str)) { %dbO = 0; continue; }
		
		// Try to get a close last object to stretch for optimization
		if(!$Pref::Server::Runescape::DisableTerrainOptimization && isObject(%dbO)) {
			if(%xSc && isObject(%lastObj) && %lastObj.getDataBlock().getId() == %dbO.getId() && %y == getWord(%lastObj.gridPos, 1) && %lastObj.attributeCRC $= %crc) %doS = 1;
			else if(%i < %tempWOut) %doS = 3;
			else if(%ySc && %y != getWord(%offset, 1) && isObject(%tO = $CACHE::Runescape::Server::TerrainChunks::obj[%x, %y - 1]) && %tO.getDataBlock().getId() == %dbO.getId() && %tO.attributeCRC $= %crc) { //%x == getWord(%tO.gridPos, 0)) {
				// Check if we're the same width
				%ourW = 1;
				%tO_W = getWord(%tO.gridSize, 0);
				
				for(%j=%i + 1;%j<getWordCount(%str);%j++) { if(getWord(%offset, 1) + mFloor(%j / %sizeW) == %y && getField(%t_[getWord(%str, %j)], 1) == %dbO) { %ourW++; } else { break; } }
				
				if(%ourW == %tO_W) {
					%nLastObj = %tO;
					%doS      = 2;
					%tempWOut = %i + %tO_W;
				}
			}
		}
		
		if(%scaleLastObject && (%doS != 1 || %x == getWord(%offset, 0) || !isObject(%dbO))) {
			// getWord(<extent>, 0) is correct since the Z scale is always 1; we're working on a 2D plane here
			
			%lastObjOri = getWord(getField(%lastObj.tileData, 0), 3);
			%oPosition  = %lastObj.getPosition();
			%originalX  = getWord(%oPosition, 0) + (getWord(%lastObj.getExtent(0), 0) / 2);
			%originalY  = getWord(%oPosition, 1) - (getWord(%lastObj.getExtent(1), 0) / 2);
			
			%lastObj.setScale(%scalar);
			
			%newX    = getWord(%oPosition, 0) + (getWord(%lastObj.getExtent(0), 0) / 2);
			%newY    = getWord(%oPosition, 1) - (getWord(%lastObj.getExtent(1), 0) / 2);
			%offsetX = %newX - %originalX;
			%offsetY = %newY - %originalY;
			
			%newTransform = vectorSub(%oPosition, (%lastObjOri ? -%offsetY SPC -%offsetX : %offsetX SPC %offsetY) SPC "0") SPC getWords(%lastObj.getTransform(), 3, 6);
			
			%lastObj.setTransform(%newTransform);
			
			%xSc = getWord(%lastObj.getExtent(0), 0);
			%ySc = getWord(%lastObj.getExtent(1), 1);
			
			// Reset scale info
			%scaleLastObject = 0;
			%scalar          = "";
			
			if($RS::Debug) %lastObj.setShapeName(setWord(removeWord(%lastObj.getShapeName(), 5), 4, "<" @ getWord(%lastObj.gridSize, 0) @ ", " @ getWord(%lastObj.gridSize, 0) @ ">"));
		}
		
		if(!isObject(%dbO)) break;
		
		if($RS::Debug == 2) {
			%line = drawSphere(rotateTransform(vectorAdd(%origin, -(%tileW * (%x - getWord(%totalSize, 0))) SPC %tileH * %y SPC "2") SPC "0 0 0 0", %rot));
			%line.setNodeColor("ALL", "1 0 0 1");
			$DBGSPHERE[-1 + $DBGSPHERECOUNT++] = %line;
			%line.setShapeName(%chr @ ": " @ %doS);
		}
		
		if(%doS == 1) {
			// Stretch the last tile by %sca
			
			$CACHE::Runescape::Server::TerrainChunks::obj[%x, %y] = %lastObj;
			%scaleLastObject  = 1;
			%scalar           = (%scalar $= "" ? vectorAdd("1 1 1", %sca) : vectorAdd(%scalar, %sca));
			%lastObj.scaled   = 1;
			%lastObj.gridSize = getWord(%lastObj.gridSize, 0) + 1 SPC getWord(%lastObj.gridSize, 1);
		} else if(%doS == 2) {
			// Stretch the last tile by %sca on Y axis
			
			for(%j=getWord(%lastObj.gridPos, 0);%j<getWord(%lastObj.gridPos, 0) + getWord(%lastObj.gridSize, 0);%j++)
				$CACHE::Runescape::Server::TerrainChunks::obj[%j, %y] = %lastObj;
			%lastObj          = %nLastObj;
			%nLastObj         = "";
			%scaleLastObject  = 1;
			if(%xSc) %sca     = getWord(%sca, 1) SPC getWord(%sca, 0) SPC getWord(%sca, 2);
			%scalar           = vectorAdd(%lastObj.getScale(), %sca);
			%lastObj.gridSize = getWord(%lastObj.gridSize, 0) SPC getWord(%lastObj.gridSize, 1) + 1;
			%lastObj.scaled   = 1;
		} else if(%doS == 3) {
			continue;
		} else {
			%obj = new StaticShape() {
				height       = (%heightChange < 0 ? (%heightOffset - %heightChange) : %heightOffset);
				attributeCRC = %crc;
				color        = %clr;
				dataBlock    = %dbO;
				tileData     = %dat;
				tileChr      = %chr;
				gridPos      = %x SPC %y;
				gridSize     = "1 1";
				scalar       = %sca;
				originalRot  = %rot;
			};
			
			// Apply colors
			if(%n_p) %obj.setNodeColor("Path", %path_color);
			if(%n_t) %obj.setNodeColor("Terrain", %terrain_color);
			
			%obj.setNetFlag(6, 1);
			%obj.scopeToClient(findLocalClient());
			
			if($RS::Debug) %obj.setShapeName("\"" @ %chr @ "\" - (" @ %x @ ", " @ %y @ ") <" @ getWord(%obj.gridSize, 0) @ ", " @ getWord(%obj.gridSize, 0) @ ">");
			
			$CACHE::Runescape::Server::TerrainChunks::obj[%x, %y] = %obj;
			
			%group.add(%obj);
			
			// Calculate height
			
			%transform = rotateTransform(vectorAdd(%origin, -(%tileW * (%x - getWord(%totalSize, 0))) SPC %tileH * %y SPC %height) SPC "0 0 0 0", %rot);
			%obj.setTransform(%transform);
			
			%lastObj = %obj;
		}
	}
}

// $a=%hit;$b=$a.getTransform();
// $a.setTransform(rotateTransform($b, "0 0 0"))

function RS_loadChunk(%chunkFile, %offset) {
	%origin = "-0.205 -0.2953 6";
	//%offset = "-0.205 -0.2953 6";
	
	%chunkFile = "Add-Ons/Server_Runescape/mapChunks/" @ fileBase(%chunkFile) @ ".tmx";
	
	if(!isFile(%chunkFile)) {
		error("ERROR: RS_loadChunk() - Chunk file \"" @ %chunkFile @ "\" does not exist.");
		return 0;
	}
	
	// =======================================================
	// REMOVE THIS WHEN DONE DEBUGGING
	// =======================================================
	clearDebugSpheres();
	RS_clearTerrain();
	deleteVariables("$CACHE::Runescape::Server::TerrainChunks::Outline*");
	%p=findlocalclient().player;
	%p.settransform(vectoradd(%p.gettransform(), "0 0 1") SPC getWords(%p.gettransform(), 3, 6));
	// =======================================================
	// REMOVE THIS WHEN DONE DEBUGGING
	// =======================================================
	
	// Init variables
	%group          = RSTerrainGroup;
	%maxChunkWidth  = getWord(RS_getMaxChunkSize(), 0);
	%maxChunkHeight = getWord(RS_getMaxChunkSize(), 1);
	%origin         = getWord(%origin, 0) + (14 * (getWord(%offset, 0) * %maxChunkWidth)) SPC getWord(%origin, 1) + (14 * (getWord(%offset, 1) * %maxChunkHeight)) SPC "6";
	%chunks         = -1;
	%iterator       = -1;
	%tileW          = 14;
	%tileH          = 14;
	
	// Default colors
	$CACHE::Runescape::Server::ChunkLoader::LastTerrainColor = "0 0.67 0 1";
	$CACHE::Runescape::Server::ChunkLoader::LastPathColor    = "0.713 0.713 0.713 1";
	$CACHE::Runescape::Server::ChunkLoader::LastHeightOffset = "0";
	
	%SO = new FileObject(%chunkFile);
	%SO.openForRead(%chunkFile);
	
	while(!%SO.isEOF()) {
		%line = trim(%SO.readLine());
		if(%line $= "") continue;
		%tagN = xmlParser_getElementName(%line);
		%clos = xmlParser_isClosingElement(%line);
		
		switch$(%tagN) {
			case "map":
				if(%clos) continue;
				%mapSizeX = xmlParser_getElementTag(%line, "width");
				%mapSizeY = xmlParser_getElementTag(%line, "height");
			case "layer":
				if(%clos) continue;
				
				%lln          = xmlParser_getElementTag(%line, "name");
				%mapDataCount = "";
			case "data":
				while(!%SO.isEOF()) {
					%line = trim(%SO.readLine());
					if(%line $= "") continue;
					%tagN = xmlParser_getElementName(%line);
					%clos = xmlParser_isClosingElement(%line);
					
					if(%tagN $= "data" && %clos) break;
					
					%line = trim(strReplace(%line, ",", " "));
					for(%i=0;%i<getWordCount(%line);%i++)
						%_[(%lln $= "colors" ? "colorData" : (%lln $= "depth" ? "depthData" : "mapData")) @ (-1 + %mapDataCount++)] = getWord(%line, %i);
				}
			case "object":
				%lastOffsetObject = xmlParser_getElementTag(%line, "x");
				%lastOffsetObject = %lastOffsetObject SPC xmlParser_getElementTag(%line, "y");
				
				%objWidth  = xmlParser_getElementTag(%line, "width");
				%objHeight = xmlParser_getElementTag(%line, "height");
				
				if(%objWidth !$= "" && %objHeight !$= "") {
					%polyPoints[-1 + %polyPointCount++] = "NEWOBJECT" SPC %lastOffsetObject;
					
					%topLeft     = %lastOffsetObject;
					%topRight    = setWord(%topLeft, 0, getWord(%topLeft, 0) + %objWidth);
					%bottomLeft  = setWord(%topLeft, 1, getWord(%topLeft, 1) + %objHeight);
					%bottomRight = setWord(%topRight, 1, getWord(%topRight, 1) + %objHeight);
					
					%polyPoints[-1 + %polyPointCount++] = %topLeft SPC %topRight;
					%polyPoints[-1 + %polyPointCount++] = %topLeft SPC %bottomLeft;
					%polyPoints[-1 + %polyPointCount++] = %bottomLeft SPC %bottomRight;
					%polyPoints[-1 + %polyPointCount++] = %topRight SPC %bottomRight;
				}
			case "polyline":
				%lines                              = xmlParser_getElementTag(%line, "points");
				%polyPoints[-1 + %polyPointCount++] = "NEWOBJECT" SPC %lastOffsetObject;
				
				for(%i=0;%i<getWordCount(%lines);%i += 2) {
					%polyPoint0 = strReplace(getWord(%lines, %i), ",", " ");
					%polyPoint0 = getWords(vectorAdd(%lastOffsetObject, %polyPoint0), 0, 1);
					
					%polyPoint1 = strReplace(getWord(%lines, %i + 1), ",", " ");
					%polyPoint1 = getWords(vectorAdd(%lastOffsetObject, %polyPoint1), 0, 1);
					
					%polyPoints[-1 + %polyPointCount++] = %polyPoint0 SPC %polyPoint1;
				}
		}
	}
	
	%SO.close();
	%SO.delete();
	
	// Set variables
	$CACHE::Runescape::Server::TerrainChunks::MapSize = %mapSizeX SPC %mapSizeY;
	
	if(%mapSizeX $= "" || %mapSizeY $= "") {
		error("ERROR: RS_loadChunk() - File \"" @ %chunkFile @ "\"; Could not find map size");
		return 0;
	}
	
	for(%i=0;%i<=%mapSizeX * %mapSizeY;%i++) {
		%x            = %i % %mapSizeX;
		%y            = mFloor(%i / %mapSizeX);
		
		// Ready the data
		%tile[%x, %y] =   %_mapData[%i] - 1
			    SPC %_colorData[%i] - 1
			    SPC ((%n = %_depthData[%i]) == 0 ? -1 : %n - 12);
	}
	
	for(%i=0;%i<=%mapSizeX * %mapSizeY;%i++) {
		%iterator++;
		
		if(%dbg++ >= 5000) { echo("broke loop in terrain.cs Server_Runescape, RS_loadChunk();"); break; }
		
		%x   = %i % %mapSizeX;
		%y   = mFloor(%i / %mapSizeX);
		%i_x = %iterator % %mapSizeX;
		%i_y = mFloor(%iterator / %mapSizeX);
		
		if(%i == %mapSizeX * %mapSizeY) {
			%chunkSizeW = %mapSizeX - %maxChunkWidth;
			%chunkSizeH = mFloor(getWordCount(%chunk) / %mapSizeX);
			if(%chunk $= "" || %chunkSizeW == 0 || %chunkSizeH == 0) break;
			
			%chunks++;
			%offset = (%chunks * %maxChunkWidth) % %mapSizeX SPC %maxChunkHeight * mFloor((%chunks * %maxChunkWidth) / %mapSizeX);
			
			%newGroup  = new ScriptGroup("RS_TerrainChunk") {
				chunkFile = %chunkFile;
				offset    = %offset;
				size      = %chunkSizeW SPC %chunkSizeH;
			};
			
			%group.add(%newGroup);
			RS_generateTerrain(%chunkSizeW, %chunkSizeH, %chunk, %origin, %newGroup, %offset, %mapSizeX SPC %mapSizeY);
			break;
		}
		
		%relX  = ((%chunks + 1) * %maxChunkWidth) % %mapSizeX + mFloor(%iterator % %maxChunkWidth);
		%relY  = (mFloor(((%chunks + 1) * %maxChunkWidth) / %mapSizeX) * %maxChunkHeight) + mFloor((%iterator % (%maxChunkWidth * %maxChunkHeight)) / %maxChunkWidth);
		
		if(%relX >= %mapSizeX || %relY >= %mapSizeY) { %i--; continue; }
		
		%chunk              = trim(%chunk SPC strReplace(%tile[%relX, %relY], " ", "_"));
		%tile[%relX, %relY] = -1;
		
		if(getWordCount(%chunk) == %maxChunkWidth * %maxChunkHeight) {
			%boundsUpperLeft  = vectorAdd(vectorScale(%offset, %tileW), %tileW / 2 SPC -(%tileH / 2));
			%boundsLowerRight = vectorAdd(%boundsUpperLeft, %tileW * %maxChunkWidth SPC %tileH * %maxChunkHeight);
			
			%chunks++;
			%offset = (%chunks * %maxChunkWidth) % %mapSizeX SPC %maxChunkHeight * mFloor((%chunks * %maxChunkWidth) / %mapSizeX);
			
			%newGroup = new ScriptGroup("RS_TerrainChunk") {
				chunkFile = %chunkFile;
				bounds    = %boundsUpperLeft SPC %boundsLowerRight;
				offset    = %offset;
				size      = %maxChunkWidth SPC %maxChunkHeight;
			};
			
			%group.add(%newGroup);
			
			RS_generateTerrain(%maxChunkWidth, %maxChunkHeight, %chunk, %origin, %newGroup, %offset, %mapSizeX SPC %mapSizeY);
			%chunk = "";
		}
	}

	for(%y=0;%y<%mapSizeY;%y++) {
		%missed  = 0;
		%oMissed = 0;
		%missTbl = "";
		
		for(%x=0;%x<%mapSizeX;%x++) {
			%idx = %tile[%x, %y] - 1;
			
			%missTbl = trim(%missTbl SPC %idx);
			
			if(%idx == -2) continue;
			
			%missed  = 1;
			%oMissed = 1;
		}
		
		if(!%missed) continue;
		
		RS_generateTerrain(getWordCount(%missTbl), 1, %missTbl, %origin, %group, "0" SPC %y, %mapSizeX SPC %mapSizeY);
	}
	
	for(%y=0;%y<%mapSizeY;%y++) {
		for(%x=0;%x<%mapSizeX;%x++) {
			%obj          = $CACHE::Runescape::Server::TerrainChunks::obj[%x, %y];
			
			if(!isObject(%obj)) continue;
			
			%rot          = getWords(%obj.getTransform(), 3, 6);
			%heightOffset = %obj.height;
			
			%obj.setTransform(vectorAdd(getWords(%obj.getPosition(), 0, 1) SPC getWord(%origin, 2), "0 0" SPC brickToMetric(%heightOffset)) SPC %rot);
		}
	}
	
	// Find objects within chunks
	for(%i=0;%i<%group.getCount();%i++) %group.getObject(%i).findObjects();
	
	// Calculate height rotation
	for(%y=0;%y<%mapSizeY;%y++) {
		for(%x=0;%x<%mapSizeX;%x++) {
			%obj          = $CACHE::Runescape::Server::TerrainChunks::obj[%x, %y];
			
			if(!isObject(%obj)) continue;
			if(%got[%obj]) continue;
			
			%got[%obj]    = 1;
			%rot          = getWords(%obj.getTransform(), 3, 6);
			%heightOffset = %obj.height;
			%pos          = %obj.getPosition();
			
			if((isObject(%o = $CACHE::Runescape::Server::TerrainChunks::obj[%x, %y + 1]) && %o.height > %heightOffset)) {
				// Connect to top object
				%centerBottom0 = getWords(vectorAdd(%obj.getPosition(), "0" SPC getWord(%obj.getExtent(1), 0) / 2), 0, 1) SPC getWord(%obj.getPosition(), 2);
				%centerBottom1 = getWords(vectorSub(%obj.getPosition(), "0" SPC getWord(%obj.getExtent(1), 0) / 2), 0, 1) SPC getWord(%o.getPosition(), 2);
				
				%fVector       = vectorSub(%centerBottom1, %centerBottom0);
				%middlePoint   = vectorSub(%centerBottom1, vectorScale(%fVector, 0.5));
				%fVector       = vectorSub(%centerBottom1, %middlePoint);
				%pos           = %middlePoint;
				%dirVec        = "1 0 0";
				
				// Rotation
				%axis   = VectorNormalize(VectorCross(%dirVec, %fVector));
				%mag    = mACos(VectorDot(%dirVec, %fVector)) * -1;
				%newRot = %axis SPC %mag;
				%euler  = eulerDegFromAxisAngle(%newRot);
				%obj.addRotation(getWord(%euler, 0));
				
				%obj.setTransform(%pos SPC getWords(%obj.getTransform(), 3, 6));
			} else if((isObject(%o = $CACHE::Runescape::Server::TerrainChunks::obj[%x, %y - 1]) && %o.height > %heightOffset)) {
				%centerBottom0 = getWords(vectorSub(%obj.getPosition(), "0" SPC getWord(%obj.getExtent(1), 0) / 2), 0, 1) SPC getWord(%o.getPosition(), 2);
				%centerBottom1 = getWords(vectorAdd(%obj.getPosition(), "0" SPC getWord(%obj.getExtent(1), 0) / 2), 0, 1) SPC getWord(%obj.getPosition(), 2);
				
				%fVector       = vectorSub(%centerBottom1, %centerBottom0);
				%middlePoint   = vectorSub(%centerBottom1, vectorScale(%fVector, 0.5));
				%fVector       = vectorSub(%centerBottom1, %middlePoint);
				%pos           = %middlePoint;
				%dirVec        = "1 0 0";
				
				// Rotation
				%axis   = VectorNormalize(VectorCross(%dirVec, %fVector));
				%mag    = mACos(VectorDot(%dirVec, %fVector)) * -1;
				%newRot = %axis SPC %mag;
				%euler  = eulerDegFromAxisAngle(%newRot);
				%obj.addRotation(-getWord(%euler, 0));
				
				%obj.setTransform(%pos SPC getWords(%obj.getTransform(), 3, 6));
			} else if(isObject(%o = $CACHE::Runescape::Server::TerrainChunks::obj[%x - 1, %y]) && %o.height > %heightOffset) {
				%obj.setShapeName("2");
				%centerBottom0 = getWords(vectorAdd(%obj.getPosition(), getWord(%obj.getExtent(0), 0) / 2), 0, 1) SPC getWord(%o.getPosition(), 2);
				%centerBottom1 = vectorSub(%obj.getPosition(), getWord(%obj.getExtent(0), 0) / 2);
				
				%fVector       = vectorSub(%centerBottom1, %centerBottom0);
				%middlePoint   = vectorSub(%centerBottom1, vectorScale(%fVector, 0.5));
				%fVector       = vectorSub(%centerBottom1, %middlePoint);
				%pos           = %middlePoint;
				%dirVec        = "0 1 0";
				
				// Rotation
				%axis   = VectorNormalize(VectorCross(%dirVec, %fVector));
				%mag    = mACos(VectorDot(%dirVec, %fVector)) * -1;
				%newRot = %axis SPC %mag;
				%euler  = eulerDegFromAxisAngle(%newRot);
				%obj.addRotation("0" SPC getWord(%euler, 1));
				
				%obj.setTransform(%pos SPC getWords(%obj.getTransform(), 3, 6));
			} else if(isObject(%o = $CACHE::Runescape::Server::TerrainChunks::obj[%x - 1, %y]) && %o.height < %heightOffset) {
				%obj.setShapeName("1");
				%centerBottom0 = vectorSub(%obj.getPosition(), -getWord(%obj.getExtent(0), 0) / 2);
				%centerBottom1 = getWords(vectorAdd(%o.getPosition(), getWord(%o.getExtent(0), 0) / 2), 0, 1) SPC getWord(%o.getPosition(), 2);
				
				%fVector       = vectorSub(%centerBottom1, %centerBottom0);
				%middlePoint   = vectorSub(%centerBottom1, vectorScale(%fVector, 0.5));
				%fVector       = vectorSub(%centerBottom1, %middlePoint);
				%pos           = %middlePoint;
				%dirVec        = "0 1 0";
				
				// Rotation
				%axis   = VectorNormalize(VectorCross(%dirVec, %fVector));
				%mag    = mACos(VectorDot(%dirVec, %fVector)) * -1;
				%newRot = %axis SPC %mag;
				%euler  = eulerDegFromAxisAngle(%newRot);
				%o.addRotation("0" SPC getWord(%euler, 1));
				
				%o.setTransform(getWords(%o.getTransform(), 0, 1) SPC getWord(%pos, 2) SPC getWords(%o.getTransform(), 3, 6));
			} else if((isObject(%o = $CACHE::Runescape::Server::TerrainChunks::obj[%x - 1, %y + 1]) && %o.height > %heightOffset)) {
				%obj.setShapeName("I AM OBJ :: (obj=" @ %obj.getId() @ ") => (o=" @ %o.getId() @ ")");
				%o.setShapeName("I AM O   :: (obj=" @ %obj.getId() @ ") => (o=" @ %o.getId() @ ")");	
				continue;
				%line = drawLine(%centerBottom0, %centerBottom1);
				%line.setNodeColor("ALL", "1 0 0 1");
				$DBGSPHERE[-1 + $DBGSPHERECOUNT++] = %line;
				%line = drawSphere(%centerBottom0);
				%line.setNodeColor("ALL", "1 0 1 1");
				$DBGSPHERE[-1 + $DBGSPHERECOUNT++] = %line;
			}
		}
	}
	
	if(%oMissed) warn("WARNING: RS_LoadChunk() - Map chunk \"" @ %chunkFile @ "\" is not symmetrical.");
	
	// Make chunk grid variables
	%itr   = 0;
	%lastY = 0;
	for(%i=0;%i<RSTerrainGroup.getCount();%i++) {
		%object = RSTerrainGroup.getObject(%i);
		
		if(%lastY != getWord(%object.offset, 1)) {
			// new Y
			%itr = 0;
		} else {
			%lastO.chunkRight = %object;
			%object.chunkLeft = %lastO;
		}
		
		%YObj[%itr].chunkBottom = %object;
		%object.chunkTop = %YObj[%itr];
		%YObj[%itr] = %object;
		%itr++;
		%lastO = %object;
		%lastX = getWord(%object.offset, 0);
		%lastY = getWord(%object.offset, 1);
	}
	
	// 2D X = Y+
	// 2D Y = X+
	
	%origin = vectorAdd(%origin, (%mapSizeY * %tileH) + (%tileH / 2) SPC -(%tileW / 2) SPC "0");
	
	// Calculate outline map (for minimaps)
	for(%i=0;%i<%polyPointCount;%i++) {
		%polyPoint  = %polyPoints[%i];
		
		if(firstWord(%polyPoint) $= "NEWOBJECT") {
			%polyPoint      = restWords(%polyPoint);
			%lastPolyOrigin = -getWord(%polyPoint, 0) SPC getWord(%polyPoint, 1);
			%lastPolyEnd = "";
			continue;
		}
		
		%polyPoint0 = -getWord(%polyPoint, 0) SPC getWord(%polyPoint, 1);
		%polyPoint1 = -getWord(%polyPoint, 2) SPC getWord(%polyPoint, 3);
		
		//%line = drawLine(%polyPoint0, %polyPoint1);
		//%line.setNodeColor("ALL", "1 1 1 1");
		//$DBGSPHERE[-1 + $DBGSPHERECOUNT++] = %line;
		
		%chunkX    = mFloor(mFloor(-getWords(%polyPoint0, 0) / 16) / %maxChunkWidth);
		%chunkY    = mFloor(mFloor(getWords(%polyPoint0, 1) / 16) / %maxChunkHeight);
		%chunkIdx0 = mFloor(%chunkY * (%mapSizeX / %maxChunkWidth)) + %chunkX;
		
		%chunkX    = mFloor(mFloor(-getWords(%polyPoint1, 0) / 16) / %maxChunkWidth);
		%chunkY    = mFloor(mFloor(getWords(%polyPoint1, 1) / 16) / %maxChunkHeight);
		%chunkIdx1 = mFloor(%chunkY * (%mapSizeX / %maxChunkWidth)) + %chunkX;
		
		%chunk0 = RSTerrainGroup.getObject(%chunkIdx0);
		%chunk1 = RSTerrainGroup.getObject(%chunkIdx1);
		
		%polyPoint0 = vectorAdd(%origin, (getWord(%polyPoint0, 0) / (%mapSizeX * 16)) * (%mapSizeX * %tileW) SPC (getWord(%polyPoint0, 1) / (%mapSizeY * 16)) * (%mapSizeY * %tileH) SPC "0.4");
		
		if(%polyPoint1 $= %lastPolyOrigin) {
			if(%lastPolyEnd !$= "") {
				%line = drawLine(%lastPolyEnd, %polyPoint0);
				$DBGSPHERE[-1 + $DBGSPHERECOUNT++] = %line;
			}
			continue;
		}
		
		%polyPoint1 = vectorAdd(%origin, (getWord(%polyPoint1, 0) / (%mapSizeX * 16)) * (%mapSizeX * %tileW) SPC (getWord(%polyPoint1, 1) / (%mapSizeY * 16)) * (%mapSizeY * %tileH) SPC "0.4");
		
		if(%lastPolyEnd !$= "") {
			if(isObject(%chunk0)) %chunk0.outline[-1 + %chunk0.outlineCount++] = %lastPolyEnd SPC %polyPoint0;
			if(isObject(%chunk1) && %chunk0 != %chunk1) %chunk1.outline[-1 + %chunk1.outlineCount++] = %lastPolyEnd SPC %polyPoint0;
		}
		
		if(isObject(%chunk0)) %chunk0.outline[-1 + %chunk0.outlineCount++] = %polyPoint0 SPC %polyPoint1;
		if(isObject(%chunk1) && %chunk0 != %chunk1) %chunk1.outline[-1 + %chunk1.outlineCount++] = %polyPoint0 SPC %polyPoint1;

		%lastPolyEnd = %polyPoint1;
		
		// ==============================================
		// debug draw
		// ==============================================
		
		continue;
		
		if(%lastPolyEnd !$= "") {
			%line = drawLine(%lastPolyEnd, %polyPoint0);
			$DBGSPHERE[-1 + $DBGSPHERECOUNT++] = %line;
		}
		
		%line = drawLine(%polyPoint0, %polyPoint1);
		$DBGSPHERE[-1 + $DBGSPHERECOUNT++] = %line;
		
		%lastPolyEnd = %polyPoint1;
	}
	
	return 1;
}

function GameConnection::doChunkRenderLogic(%this, %dontHandleGhosting, %loop) {
	if(!isObject(%pl = %this.player)) {
		cancel(%this.chunkLogicSchedule);
		%this.chunkLogicSchedule = %this.schedule(500, doChunkRenderLogic, %dontHandleGhosting, 1);
		return;
	}
	
	if(!%loop) {
		for(%i=0;%i<RSTerrainGroup.getCount();%i++) RSTerrainGroup.getObject(%i).setGhostedToClient(%this, 0);
	}
	
	%activeChunkObject = %pl.getClosestChunkGroup();
	
	if(!isObject(%activeChunkObject)) {
		cancel(%this.chunkLogicSchedule);
		%this.chunkLogicSchedule = %this.schedule(500, doChunkRenderLogic, %dontHandleGhosting, 1);
		return;
	}
	
	if(isObject((%laco = %this.lastActiveChunkObject)) && %activeChunkObject.getId() != %laco.getId()) {
		%this.schedule(3, transmitMiniMap);
		
		for(%i=0;%i<%laco.objectCount;%i++) {
			if(%laco.object[%i] !$= %pl) continue;
			
			// clean our player object out of the chunk's object table
			for(%j=%i + 1;%j<%laco.objectCount + 3;%j++) %laco.object[%j - 1] = %laco.object[%j];
			
			%laco.objectCount--;
			break;
		}
		
		%activeChunkObject.object[-1 + %activeChunkObject.objectCount++] = %pl;
		
		if(!%dontHandleGhosting) {
			%laco.setGhostedToClient(%this, 0);
			if(isObject(%o = %laco.chunkLeft)   && %o != %activeChunkObject) %o.setGhostedToClient(%this, 0);
			if(isObject(%o = %laco.chunkTop)    && %o != %activeChunkObject) %o.setGhostedToClient(%this, 0);
			if(isObject(%o = %laco.chunkBottom) && %o != %activeChunkObject) %o.setGhostedToClient(%this, 0);
			if(isObject(%o = %laco.chunkRight)  && %o != %activeChunkObject) %o.setGhostedToClient(%this, 0);
			if(isObject(%o = %laco.chunkTop.chunkLeft)  && %o != %activeChunkObject) %o.setGhostedToClient(%this, 0);
			if(isObject(%o = %laco.chunkTop.chunkRight)  && %o != %activeChunkObject) %o.setGhostedToClient(%this, 0);
		}
	}
	
	%this.lastActiveChunkObject = %activeChunkObject;
	
	if(!%dontHandleGhosting) {
		%activeChunkObject.setGhostedToClient(%this, 1);
		if(isObject(%o = %activeChunkObject.chunkLeft))   %o.setGhostedToClient(%this, 1);
		if(isObject(%o = %activeChunkObject.chunkTop))    %o.setGhostedToClient(%this, 1);
		if(isObject(%o = %activeChunkObject.chunkBottom)) %o.setGhostedToClient(%this, 1);
		if(isObject(%o = %activeChunkObject.chunkRight))  %o.setGhostedToClient(%this, 1);
	}
	
	cancel(%this.chunkLogicSchedule);
	%this.chunkLogicSchedule = %this.schedule(500, doChunkRenderLogic, %dontHandleGhosting, 1);
}