Add background generating of singleplayer maps, show progress of generation.

This commit is contained in:
UnknownShadow200 2015-12-10 20:28:38 +11:00
parent f733c4b4e4
commit ffafd8023b
9 changed files with 228 additions and 192 deletions

View file

@ -32,8 +32,8 @@ namespace ClassicalSharp {
public override void Init() {
graphicsApi.Fog = false;
titleWidget = TextWidget.Create( game, 0, 30, serverName, Anchor.Centre, Anchor.LeftOrTop, font );
messageWidget = TextWidget.Create( game, 0, 60, serverMotd, Anchor.Centre, Anchor.LeftOrTop, font );
SetTitle( serverName );
SetMessage( serverMotd );
progX = game.Width / 2f - progWidth / 2f;
Size size = new Size( progWidth, progHeight );
@ -46,6 +46,22 @@ namespace ClassicalSharp {
}
game.Events.MapLoading += MapLoading;
}
public void SetTitle( string title ) {
if( titleWidget != null )
titleWidget.Dispose();
titleWidget = TextWidget.Create( game, 0, 30, title, Anchor.Centre, Anchor.LeftOrTop, font );
}
public void SetMessage( string message ) {
if( messageWidget != null )
messageWidget.Dispose();
messageWidget = TextWidget.Create( game, 0, 60, message, Anchor.Centre, Anchor.LeftOrTop, font );
}
public void SetProgress( float progress ) {
this.progress = (int)(progress * 100);
}
void MapLoading( object sender, MapLoadingEventArgs e ) {
progress = e.Progress;

View file

@ -142,6 +142,8 @@
<Compile Include="Entities\LocationUpdate.cs" />
<Compile Include="Entities\NetPlayer.cs" />
<Compile Include="Game\Game.Properties.cs" />
<Compile Include="Generator\FlatGrassGenerator.cs" />
<Compile Include="Generator\IMapGenerator.cs" />
<Compile Include="Generator\Noise.cs" />
<Compile Include="Generator\NotchyGenerator.cs" />
<Compile Include="Generator\NotchyGenerator.Utils.cs" />

View file

@ -0,0 +1,41 @@
using System;
using OpenTK;
namespace ClassicalSharp.Generator {
public unsafe sealed class FlatGrassGenerator : IMapGenerator {
public override string GeneratorName {
get { return "Flatgrass generator"; }
}
int width, length;
public override byte[] Generate( int width, int height, int length, int seed ) {
byte[] map = new byte[width * height * length];
this.width = width;
this.length = length;
fixed( byte* ptr = map ) {
CurrentState = "Setting dirt blocks";
MapSet( ptr, 0, height / 2 - 2, (byte)Block.Dirt );
CurrentState = "Setting grass blocks";
MapSet( ptr, height / 2 - 1, height / 2 - 1, (byte)Block.Grass );
}
return map;
}
unsafe void MapSet( byte* ptr, int yStart, int yEnd, byte block ) {
int startIndex = yStart * length * width;
int endIndex = (yEnd * length + (length - 1)) * width + (width - 1);
int count = (endIndex - startIndex) + 1, offset = 0;
while( offset < count ) {
int bytes = Math.Min( count - offset, width * length );
MemUtils.memset( (IntPtr)ptr, block, startIndex + offset, bytes );
offset += bytes;
CurrentProgress = (float)offset / count;
}
}
}
}

View file

@ -0,0 +1,40 @@
using System;
using System.Threading;
using ClassicalSharp.Singleplayer;
namespace ClassicalSharp.Generator {
public abstract class IMapGenerator {
public abstract string GeneratorName { get; }
public abstract byte[] Generate( int width, int height, int length, int seed );
public string CurrentState;
public float CurrentProgress;
public bool Done = false;
public int Width, Height, Length;
public void GenerateAsync( Game game, int width, int height, int length, int seed ) {
Width = width; Height = height; Length = length;
Thread thread = new Thread(
() => {
SinglePlayerServer server = (SinglePlayerServer)game.Network;
try {
server.generatedMap = Generate( width, height, length, seed );
} catch( Exception ex ) {
ErrorHandler.LogError( "IMapGenerator.RunAsync", ex );
}
Done = true;
}
);
thread.IsBackground = true;
thread.Name = "IMapGenerator.RunAsync";
thread.Start();
}
}
}

View file

@ -8,7 +8,7 @@ using System.Collections.Generic;
namespace ClassicalSharp.Generator {
// TODO: figure out how noise functions differ, probably based on rnd.
public sealed partial class NotchyGenerator {
public sealed partial class NotchyGenerator : IMapGenerator {
int width, height, length;
int waterLevel, oneY;
@ -16,14 +16,18 @@ namespace ClassicalSharp.Generator {
short[] heightmap;
Random rnd;
public byte[] GenerateMap( int width, int height, int length ) {
public override string GeneratorName {
get { return "Vanilla classic generator"; }
}
public override byte[] Generate( int width, int height, int length, int seed ) {
this.width = width;
this.height = height;
this.length = length;
oneY = width * length;
waterLevel = height / 2;
blocks = new byte[width * height * length];
rnd = new Random( 0x5553200 ); // 0x5553201 is flatter.
rnd = new Random( seed );
CreateHeightmap();
CreateStrata();
@ -51,50 +55,57 @@ namespace ClassicalSharp.Generator {
Noise n3 = new OctaveNoise( 6, rnd );
int index = 0;
short[] hMap = new short[width * length];
CurrentState = "Building heightmap";
for( int z = 0; z < length; z++ )
for( int x = 0; x < width; x++ )
{
double hLow = n1.Compute( x * 1.3f, z * 1.3f ) / 6 - 4;
double hHigh = n2.Compute( x * 1.3f, z * 1.3f ) / 5 + 6;
double height = n3.Compute( x, z ) > 0 ? hLow : Math.Max( hLow, hHigh );
height *= 0.5;
if( height < 0 ) height *= 0.8f;
hMap[index++] = (short)(height + waterLevel);
for( int z = 0; z < length; z++ ) {
CurrentProgress = (float)z / length;
for( int x = 0; x < width; x++ ) {
double hLow = n1.Compute( x * 1.3f, z * 1.3f ) / 6 - 4;
double hHigh = n2.Compute( x * 1.3f, z * 1.3f ) / 5 + 6;
double height = n3.Compute( x, z ) > 0 ? hLow : Math.Max( hLow, hHigh );
height *= 0.5;
if( height < 0 ) height *= 0.8f;
hMap[index++] = (short)(height + waterLevel);
}
}
heightmap = hMap;
}
void CreateStrata() {
Noise n = new OctaveNoise( 8, rnd );
//Noise n = new ImprovedNoise( rnd );
CurrentState = "Creating strata";
int hMapIndex = 0;
for( int z = 0; z < length; z++ )
for( int x = 0; x < width; x++ )
{
int dirtThickness = (int)(n.Compute( x, z ) / 24 - 4);
int dirtHeight = heightmap[hMapIndex++];
for( int z = 0; z < length; z++ ) {
CurrentProgress = (float)z / length;
for( int x = 0; x < width; x++ ) {
int dirtThickness = (int)(n.Compute( x, z ) / 24 - 4);
int dirtHeight = heightmap[hMapIndex++];
int stoneHeight = dirtHeight + dirtThickness;
int mapIndex = z * width + x;
blocks[mapIndex] = (byte)Block.Lava;
mapIndex += oneY;
for( int y = 1; y < height; y++ ) {
byte type = 0;
if( y <= stoneHeight ) type = (byte)Block.Stone;
else if( y <= dirtHeight ) type = (byte)Block.Dirt;
blocks[mapIndex] = type;
int stoneHeight = dirtHeight + dirtThickness;
int mapIndex = z * width + x;
blocks[mapIndex] = (byte)Block.Lava;
mapIndex += oneY;
for( int y = 1; y < height; y++ ) {
byte type = 0;
if( y <= stoneHeight ) type = (byte)Block.Stone;
else if( y <= dirtHeight ) type = (byte)Block.Dirt;
blocks[mapIndex] = type;
mapIndex += oneY;
}
}
}
}
void CarveCaves() {
int cavesCount = blocks.Length / 8192;
CurrentState = "Carving caves";
for( int i = 0; i < cavesCount; i++ ) {
CurrentProgress = (float)i / cavesCount;
double caveX = rnd.Next( width );
double caveY = rnd.Next( height );
double caveZ = rnd.Next( length );
@ -128,7 +139,10 @@ namespace ClassicalSharp.Generator {
void CarveOreVeins( float abundance, byte block ) {
int numVeins = (int)(blocks.Length * abundance / 16384);
CurrentState = "Carving " + (Block)block;
for( int i = 0; i < numVeins; i++ ) {
CurrentProgress = (float)i / numVeins;
double veinX = rnd.Next( width );
double veinY = rnd.Next( height );
double veinZ = rnd.Next( length );
@ -157,7 +171,10 @@ namespace ClassicalSharp.Generator {
int waterY = waterLevel - 1;
int index1 = (waterY * length + 0) * width + 0;
int index2 = (waterY * length + (length - 1)) * width + 0;
CurrentState = "Flooding edge water";
for( int x = 0; x < width; x++ ) {
CurrentProgress = 0 + ((float)x / width) * 0.5f;
FloodFill( index1, (byte)Block.Water );
FloodFill( index2, (byte)Block.Water );
index1++; index2++;
@ -166,6 +183,7 @@ namespace ClassicalSharp.Generator {
index1 = (waterY * length + 0) * width + 0;
index2 = (waterY * length + 0) * width + (width - 1);
for( int z = 0; z < length; z++ ) {
CurrentProgress = 0.5f + ((float)z / length) * 0.5f;
FloodFill( index1, (byte)Block.Water );
FloodFill( index2, (byte)Block.Water );
index1 += width; index2 += width;
@ -174,7 +192,10 @@ namespace ClassicalSharp.Generator {
void FloodFillWater() {
int numSources = width * length / 800;
CurrentState = "Flooding water";
for( int i = 0; i < numSources; i++ ) {
CurrentProgress = (float)i / numSources;
int x = rnd.Next( width ), z = rnd.Next( length );
int y = waterLevel - rnd.Next( 1, 3 );
FloodFill( (y * length + z) * width + x, (byte)Block.Water );
@ -183,7 +204,10 @@ namespace ClassicalSharp.Generator {
void FloodFillLava() {
int numSources = width * length / 20000;
CurrentState = "Flooding lava";
for( int i = 0; i < numSources; i++ ) {
CurrentProgress = (float)i / numSources;
int x = rnd.Next( width ), z = rnd.Next( length );
int y = (int)((waterLevel - 3) * rnd.NextDouble() * rnd.NextDouble());
FloodFill( (y * length + z) * width + x, (byte)Block.Lava );
@ -192,31 +216,35 @@ namespace ClassicalSharp.Generator {
void CreateSurfaceLayer() {
Noise n1 = new OctaveNoise( 8, rnd ), n2 = new OctaveNoise( 8, rnd );
CurrentState = "Creating surface";
// TODO: update heightmap
int hMapIndex = 0;
for( int z = 0; z < length; z++ )
for( int x = 0; x < width; x++ )
{
bool sand = n1.Compute( x, z ) > 8;
bool gravel = n2.Compute( x, z ) > 12;
int y = heightmap[hMapIndex++];
int index = (y * length + z) * width + x;
byte blockAbove = y >= (height - 1) ? (byte)0 : blocks[index + oneY];
if( blockAbove == (byte)Block.Water && gravel ) {
blocks[index] = (byte)Block.Gravel;
} else if( blockAbove == 0 ) {
blocks[index] = (y <= waterLevel && sand) ?
(byte)Block.Sand : (byte)Block.Grass;
for( int z = 0; z < length; z++ ) {
CurrentProgress = (float)z / length;
for( int x = 0; x < width; x++ ) {
bool sand = n1.Compute( x, z ) > 8;
bool gravel = n2.Compute( x, z ) > 12;
int y = heightmap[hMapIndex++];
int index = (y * length + z) * width + x;
byte blockAbove = y >= (height - 1) ? (byte)0 : blocks[index + oneY];
if( blockAbove == (byte)Block.Water && gravel ) {
blocks[index] = (byte)Block.Gravel;
} else if( blockAbove == 0 ) {
blocks[index] = (y <= waterLevel && sand) ?
(byte)Block.Sand : (byte)Block.Grass;
}
}
}
}
void PlantFlowers() {
int numPatches = width * length / 3000;
CurrentState = "Planting flowers";
for( int i = 0; i < numPatches; i++ ) {
CurrentProgress = (float)i / numPatches;
byte type = (byte)((byte)Block.Dandelion + rnd.Next( 0, 2 ) );
int patchX = rnd.Next( width ), patchZ = rnd.Next( length );
for( int j = 0; j < 10; j++ ) {
@ -238,7 +266,10 @@ namespace ClassicalSharp.Generator {
void PlantMushrooms() {
int numPatches = width * length / 2000;
CurrentState = "Planting mushrooms";
for( int i = 0; i < numPatches; i++ ) {
CurrentProgress = (float)i / numPatches;
byte type = (byte)((byte)Block.BrownMushroom + rnd.Next( 0, 2 ) );
int patchX = rnd.Next( width );
int patchY = rnd.Next( height );
@ -265,7 +296,10 @@ namespace ClassicalSharp.Generator {
void PlantTrees() {
int numPatches = width * length / 4000;
CurrentState = "Planting tress";
for( int i = 0; i < numPatches; i++ ) {
CurrentProgress = (float)i / numPatches;
int patchX = rnd.Next( width ), patchZ = rnd.Next( length );
for( int j = 0; j < 20; j++ ) {

View file

@ -113,11 +113,14 @@ namespace ClassicalSharp.Singleplayer {
#region General
void TickRandomBlocks() {
int xMax = width - 1, yMax = height - 1, zMax = length - 1;
for( int y = 0; y < height; y += 16 ) {
for( int z = 0; z < length; z += 16 ) {
for( int x = 0; x < width; x += 16 ) {
int lo = (y * length + z) * width + x;
int hi = ((y + 15) * length + (z + 15)) * width + (x + 15 );
int hi = (Math.Min( yMax, y + 15 ) * length + Math.Min( zMax, z + 15 ))
* width + Math.Min( xMax, x + 15 );
HandleBlock( rnd.Next( lo, hi ) );
HandleBlock( rnd.Next( lo, hi ) );
HandleBlock( rnd.Next( lo, hi ) );

View file

@ -1,6 +1,7 @@
//#define TEST_VANILLA
using System;
using System.Net;
using ClassicalSharp.Generator;
using OpenTK;
using OpenTK.Input;
@ -47,7 +48,7 @@ namespace ClassicalSharp.Singleplayer {
}
public override void SendSetBlock( int x, int y, int z, bool place, byte block ) {
if( place )
if( place )
physics.OnBlockPlaced( x, y, z, block );
}
@ -58,11 +59,29 @@ namespace ClassicalSharp.Singleplayer {
physics.Dispose();
}
string lastState;
public override void Tick( double delta ) {
if( Disconnected ) return;
physics.Tick();
if( generator == null )
return;
if( generator.Done ) {
EndGeneration();
} else {
string state = generator.CurrentState;
float progress = generator.CurrentProgress;
LoadingMapScreen screen = ((LoadingMapScreen)game.GetActiveScreen);
screen.SetProgress( progress );
if( state != lastState ) {
lastState = state;
screen.SetMessage( state );
}
}
}
IMapGenerator generator;
internal void NewMap() {
ServerName = "Single player";
ServerMotd = "Generating a map..";
@ -72,26 +91,36 @@ namespace ClassicalSharp.Singleplayer {
game.SetNewScreen( new LoadingMapScreen( game, ServerName, ServerMotd ) );
}
internal unsafe void MakeMap( int width, int height, int length ) {
#if TEST_VANILLA
byte[] map = new ClassicalSharp.Generator.NotchyGenerator()
.GenerateMap( width, height, length );
#else
byte[] map = new byte[width * height * length];
fixed( byte* ptr = map ) {
MapSet( width, length, ptr, 0, height / 2 - 2, (byte)Block.Dirt );
MapSet( width, length, ptr, height / 2 - 1, height / 2 - 1, (byte)Block.Grass );
}
#endif
game.Map.SetData( map, width, height, length );
game.Events.RaiseOnNewMapLoaded();
void EndGeneration() {
game.SetNewScreen( null );
ResetPlayerPosition();
if( generatedMap == null ) {
game.Chat.Add( "&cFailed to generate the map." );
} else {
IMapGenerator gen = generator;
game.Map.SetData( generatedMap, generator.Width,
generator.Height, generator.Length );
generatedMap = null;
game.Events.RaiseOnNewMapLoaded();
ResetPlayerPosition();
}
generator = null;
game.Chat.Add( "&ePlaying single player", CpeMessage.Status1 );
GC.Collect();
}
public byte[] generatedMap;
internal void MakeMap( int width, int height, int length ) {
#if TEST_VANILLA
generator = new NotchyGenerator();
generator.GenerateAsync( game, width, height, length, 0x5553200 );
#else
generator = new FlatGrassGenerator();
generator.GenerateAsync( game, width, height, length, 0 );
#endif
}
unsafe void MapSet( int width, int length, byte* ptr, int yStart, int yEnd, byte block ) {
int startIndex = yStart * length * width;
int endIndex = ( yEnd * length + (length - 1) ) * width + (width - 1);

View file

@ -91,7 +91,6 @@
<Compile Include="WebService\IWebTask.cs" />
<Compile Include="WebService\ServerListEntry.cs" />
<Compile Include="WebService\UpdateCheckTask.cs" />
<Compile Include="WebService\WebUtility.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ClassicalSharp\ClassicalSharp.csproj">

View file

@ -1,128 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
namespace Launcher2 {
public static class WebUtility {
static Dictionary<string, char> _lookupTable = new Dictionary<string, char>( 253, StringComparer.Ordinal ) {
{ "quot", '\x0022' }, { "amp", '\x0026' }, { "apos", '\x0027' }, { "lt", '\x003C' },
{ "gt", '\x003E' }, { "nbsp", '\x00A0' }, { "iexcl", '\x00A1' }, { "cent", '\x00A2' },
{ "pound", '\x00A3' }, { "curren", '\x00A4' }, { "yen", '\x00A5' }, { "brvbar", '\x00A6' },
{ "sect", '\x00A7' }, { "uml", '\x00A8' }, { "copy", '\x00A9' }, { "ordf", '\x00AA' },
{ "laquo", '\x00AB' }, { "not", '\x00AC' }, { "shy", '\x00AD' }, { "reg", '\x00AE' },
{ "macr", '\x00AF' }, { "deg", '\x00B0' }, { "plusmn", '\x00B1' }, { "sup2", '\x00B2' },
{ "sup3", '\x00B3' }, { "acute", '\x00B4' }, { "micro", '\x00B5' }, { "para", '\x00B6' },
{ "middot", '\x00B7' }, { "cedil", '\x00B8' }, { "sup1", '\x00B9' }, { "ordm", '\x00BA' },
{ "raquo", '\x00BB' }, { "frac14", '\x00BC' }, { "frac12", '\x00BD' }, { "frac34", '\x00BE' },
{ "iquest", '\x00BF' }, { "Agrave", '\x00C0' }, { "Aacute", '\x00C1' }, { "Acirc", '\x00C2' },
{ "Atilde", '\x00C3' }, { "Auml", '\x00C4' }, { "Aring", '\x00C5' }, { "AElig", '\x00C6' },
{ "Ccedil", '\x00C7' }, { "Egrave", '\x00C8' }, { "Eacute", '\x00C9' }, { "Ecirc", '\x00CA' },
{ "Euml", '\x00CB' }, { "Igrave", '\x00CC' }, { "Iacute", '\x00CD' }, { "Icirc", '\x00CE' },
{ "Iuml", '\x00CF' }, { "ETH", '\x00D0' }, { "Ntilde", '\x00D1' }, { "Ograve", '\x00D2' },
{ "Oacute", '\x00D3' }, { "Ocirc", '\x00D4' }, { "Otilde", '\x00D5' }, { "Ouml", '\x00D6' },
{ "times", '\x00D7' }, { "Oslash", '\x00D8' }, { "Ugrave", '\x00D9' }, { "Uacute", '\x00DA' },
{ "Ucirc", '\x00DB' }, { "Uuml", '\x00DC' }, { "Yacute", '\x00DD' }, { "THORN", '\x00DE' },
{ "szlig", '\x00DF' }, { "agrave", '\x00E0' }, { "aacute", '\x00E1' }, { "acirc", '\x00E2' },
{ "atilde", '\x00E3' }, { "auml", '\x00E4' }, { "aring", '\x00E5' }, { "aelig", '\x00E6' },
{ "ccedil", '\x00E7' }, { "egrave", '\x00E8' }, { "eacute", '\x00E9' }, { "ecirc", '\x00EA' },
{ "euml", '\x00EB' }, { "igrave", '\x00EC' }, { "iacute", '\x00ED' }, { "icirc", '\x00EE' },
{ "iuml", '\x00EF' }, { "eth", '\x00F0' }, { "ntilde", '\x00F1' }, { "ograve", '\x00F2' },
{ "oacute", '\x00F3' }, { "ocirc", '\x00F4' }, { "otilde", '\x00F5' }, { "ouml", '\x00F6' },
{ "divide", '\x00F7' }, { "oslash", '\x00F8' }, { "ugrave", '\x00F9' }, { "uacute", '\x00FA' },
{ "ucirc", '\x00FB' }, { "uuml", '\x00FC' }, { "yacute", '\x00FD' }, { "thorn", '\x00FE' },
{ "yuml", '\x00FF' }, { "OElig", '\x0152' }, { "oelig", '\x0153' }, { "Scaron", '\x0160' },
{ "scaron", '\x0161' }, { "Yuml", '\x0178' }, { "fnof", '\x0192' }, { "circ", '\x02C6' },
{ "tilde", '\x02DC' }, { "Alpha", '\x0391' }, { "Beta", '\x0392' }, { "Gamma", '\x0393' },
{ "Delta", '\x0394' }, { "Epsilon", '\x0395' }, { "Zeta", '\x0396' }, { "Eta", '\x0397' },
{ "Theta", '\x0398' }, { "Iota", '\x0399' }, { "Kappa", '\x039A' }, { "Lambda", '\x039B' },
{ "Mu", '\x039C' }, { "Nu", '\x039D' }, { "Xi", '\x039E' }, { "Omicron", '\x039F' },
{ "Pi", '\x03A0' }, { "Rho", '\x03A1' }, { "Sigma", '\x03A3' }, { "Tau", '\x03A4' },
{ "Upsilon", '\x03A5' }, { "Phi", '\x03A6' }, { "Chi", '\x03A7' }, { "Psi", '\x03A8' },
{ "Omega", '\x03A9' }, { "alpha", '\x03B1' }, { "beta", '\x03B2' }, { "gamma", '\x03B3' },
{ "delta", '\x03B4' }, { "epsilon", '\x03B5' }, { "zeta", '\x03B6' }, { "eta", '\x03B7' },
{ "theta", '\x03B8' }, { "iota", '\x03B9' }, { "kappa", '\x03BA' }, { "lambda", '\x03BB' },
{ "mu", '\x03BC' }, { "nu", '\x03BD' }, { "xi", '\x03BE' }, { "omicron", '\x03BF' },
{ "pi", '\x03C0' }, { "rho", '\x03C1' }, { "sigmaf", '\x03C2' }, { "sigma", '\x03C3' },
{ "tau", '\x03C4' }, { "upsilon", '\x03C5' }, { "phi", '\x03C6' }, { "chi", '\x03C7' },
{ "psi", '\x03C8' }, { "omega", '\x03C9' }, { "thetasym", '\x03D1' }, { "upsih", '\x03D2' },
{ "piv", '\x03D6' }, { "ensp", '\x2002' }, { "emsp", '\x2003' }, { "thinsp", '\x2009' },
{ "zwnj", '\x200C' }, { "zwj", '\x200D' }, { "lrm", '\x200E' }, { "rlm", '\x200F' },
{ "ndash", '\x2013' }, { "mdash", '\x2014' }, { "lsquo", '\x2018' }, { "rsquo", '\x2019' },
{ "sbquo", '\x201A' }, { "ldquo", '\x201C' }, { "rdquo", '\x201D' }, { "bdquo", '\x201E' },
{ "dagger", '\x2020' }, { "Dagger", '\x2021' }, { "bull", '\x2022' }, { "hellip", '\x2026' },
{ "permil", '\x2030' }, { "prime", '\x2032' }, { "Prime", '\x2033' }, { "lsaquo", '\x2039' },
{ "rsaquo", '\x203A' }, { "oline", '\x203E' }, { "frasl", '\x2044' }, { "euro", '\x20AC' },
{ "image", '\x2111' }, { "weierp", '\x2118' }, { "real", '\x211C' }, { "trade", '\x2122' },
{ "alefsym", '\x2135' }, { "larr", '\x2190' }, { "uarr", '\x2191' }, { "rarr", '\x2192' },
{ "darr", '\x2193' }, { "harr", '\x2194' }, { "crarr", '\x21B5' }, { "lArr", '\x21D0' },
{ "uArr", '\x21D1' }, { "rArr", '\x21D2' }, { "dArr", '\x21D3' }, { "hArr", '\x21D4' },
{ "forall", '\x2200' }, { "part", '\x2202' }, { "exist", '\x2203' }, { "empty", '\x2205' },
{ "nabla", '\x2207' }, { "isin", '\x2208' }, { "notin", '\x2209' }, { "ni", '\x220B' },
{ "prod", '\x220F' }, { "sum", '\x2211' }, { "minus", '\x2212' }, { "lowast", '\x2217' },
{ "radic", '\x221A' }, { "prop", '\x221D' }, { "infin", '\x221E' }, { "ang", '\x2220' },
{ "and", '\x2227' }, { "or", '\x2228' }, { "cap", '\x2229' }, { "cup", '\x222A' },
{ "int", '\x222B' }, { "there4", '\x2234' }, { "sim", '\x223C' }, { "cong", '\x2245' },
{ "asymp", '\x2248' }, { "ne", '\x2260' }, { "equiv", '\x2261' }, { "le", '\x2264' },
{ "ge", '\x2265' }, { "sub", '\x2282' }, { "sup", '\x2283' }, { "nsub", '\x2284' },
{ "sube", '\x2286' }, { "supe", '\x2287' }, { "oplus", '\x2295' }, { "otimes", '\x2297' },
{ "perp", '\x22A5' }, { "sdot", '\x22C5' }, { "lceil", '\x2308' }, { "rceil", '\x2309' },
{ "lfloor", '\x230A' }, { "rfloor", '\x230B' }, { "lang", '\x2329' }, { "rang", '\x232A' },
{ "loz", '\x25CA' }, { "spades", '\x2660' }, { "clubs", '\x2663' }, { "hearts", '\x2665' },
{ "diams", '\x2666' },
};
public static string HtmlDecode( string value ) {
value = value.Replace( "hellip;", "\x2026" ); // minecraft.net doesn't escape this at the end properly.
if( String.IsNullOrEmpty( value ) || value.IndexOf( '&' ) < 0 ) {
return value;
}
StringBuilder sb = new StringBuilder();
WebUtility.HtmlDecode( value, sb );
return sb.ToString();
}
static void HtmlDecode( string value, StringBuilder output ) {
for( int i = 0; i < value.Length; i++ ) {
char token = value[i];
if( token != '&' ) {
output.Append( token );
continue;
}
int entityEnd = value.IndexOf( ';', i + 1 );
if( entityEnd <= 0 ) {
output.Append( token );
continue;
}
string entity = value.Substring( i + 1, entityEnd - i - 1 );
if( entity.Length > 1 && entity[0] == '#' ) {
ushort encodedNumber;
if( entity[1] == 'x' || entity[1] == 'X' ) {
ushort.TryParse( entity.Substring( 2 ), NumberStyles.AllowHexSpecifier, NumberFormatInfo.InvariantInfo, out encodedNumber );
} else {
ushort.TryParse( entity.Substring( 1 ), NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out encodedNumber );
}
if( encodedNumber != 0 ) {
output.Append( (char)encodedNumber );
i = entityEnd;
}
} else {
i = entityEnd;
char decodedEntity;
if( _lookupTable.TryGetValue( entity, out decodedEntity ) ) {
output.Append( decodedEntity );
} else { // Invalid token.
output.Append( '&' );
output.Append( entity );
output.Append( ';' );
}
}
}
}
}
}