From 4b51c5c4c018dafb936785c21a8aa0296aecba48 Mon Sep 17 00:00:00 2001 From: UnknownShadow200 Date: Sat, 19 Sep 2015 11:03:18 +1000 Subject: [PATCH] Add support for loading .dat files, mark single-player support as done. --- ClassicalSharp/ClassicalSharp.csproj | 4 +- ClassicalSharp/Map/IMapFile.cs | 26 ++++ ClassicalSharp/Map/MapDat.cs | 183 ++++++++++++++++++++++++ ClassicalSharp/Map/MapFcm3.cs | 21 ++- ClassicalSharp/Map/MapLvl.cs | 142 ------------------ ClassicalSharp/Singleplayer/Commands.cs | 25 +++- readme.md | 5 +- 7 files changed, 250 insertions(+), 156 deletions(-) create mode 100644 ClassicalSharp/Map/IMapFile.cs create mode 100644 ClassicalSharp/Map/MapDat.cs delete mode 100644 ClassicalSharp/Map/MapLvl.cs diff --git a/ClassicalSharp/ClassicalSharp.csproj b/ClassicalSharp/ClassicalSharp.csproj index 2dae2ef75..da638e75b 100644 --- a/ClassicalSharp/ClassicalSharp.csproj +++ b/ClassicalSharp/ClassicalSharp.csproj @@ -45,7 +45,6 @@ TRACE; obj\ Project - wwwf null 127.0.0.1 25566 AnyCPU @@ -115,9 +114,10 @@ + + - diff --git a/ClassicalSharp/Map/IMapFile.cs b/ClassicalSharp/Map/IMapFile.cs new file mode 100644 index 000000000..da70fedfb --- /dev/null +++ b/ClassicalSharp/Map/IMapFile.cs @@ -0,0 +1,26 @@ +using System; +using System.IO; + +namespace ClassicalSharp { + + public abstract class IMapFile { + + public virtual bool SupportsLoading { + get { return false; } + } + + public virtual bool SupportsSaving { + get { return false; } + } + + public virtual byte[] Load( Stream stream, Game game, out int width, out int height, out int length ) { + width = 0; + height = 0; + length = 0; + return null; + } + + public virtual void Save( Stream stream, Game game ) { + } + } +} diff --git a/ClassicalSharp/Map/MapDat.cs b/ClassicalSharp/Map/MapDat.cs new file mode 100644 index 000000000..6864929ee --- /dev/null +++ b/ClassicalSharp/Map/MapDat.cs @@ -0,0 +1,183 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Net; +using System.Text; +using OpenTK; + +namespace ClassicalSharp { + + public sealed class MapDat : IMapFile { + + const byte TC_NULL = 0x70, TC_REFERENCE = 0x71; + const byte TC_CLASSDESC = 0x72, TC_OBJECT = 0x73; + const byte TC_STRING = 0x74, TC_ARRAY = 0x75; + const byte TC_ENDBLOCKDATA = 0x78; + + public override bool SupportsLoading { + get { return true; } + } + + BinaryReader reader; + public override byte[] Load( Stream stream, Game game, out int width, out int height, out int length ) { + byte[] map = null; + width = 0; + height = 0; + length = 0; + LocalPlayer p = game.LocalPlayer; + p.SpawnPoint = Vector3.Zero; + + using( GZipStream gs = new GZipStream( stream, CompressionMode.Decompress ) ) { + reader = new BinaryReader( gs ); + ClassDescription obj = ReadData(); + for( int i = 0; i < obj.Fields.Length; i++ ) { + FieldDescription field = obj.Fields[i]; + if( field.FieldName == "width" ) + width = (int)field.Value; + else if( field.FieldName == "height" ) + length = (int)field.Value; + else if( field.FieldName == "depth" ) + height = (int)field.Value; + else if( field.FieldName == "blocks" ) + map = (byte[])field.Value; + else if( field.FieldName == "xSpawn" ) + p.SpawnPoint.X = (int)field.Value; + else if( field.FieldName == "ySpawn" ) + p.SpawnPoint.Y = (int)field.Value; + else if( field.FieldName == "zSpawn" ) + p.SpawnPoint.Z = (int)field.Value; + } + } + return map; + } + + ClassDescription ReadData() { + if( ReadInt32() != 0x271BB788 || reader.ReadByte() != 0x02 ) { + throw new InvalidDataException( "Unexpected constant in .lvl file" ); + } + + // Java serialization constants + if( (ushort)ReadInt16() != 0xACED || ReadInt16() != 0x0005 ) { + throw new InvalidDataException( "Unexpected java serialisation constant(s)." ); + } + + byte typeCode = reader.ReadByte(); + if( typeCode != TC_OBJECT ) ParseError( TC_OBJECT, typeCode ); + ClassDescription desc = ReadClassDescription(); + ReadClassData( desc.Fields, desc.Flags ); + return desc; + } + + void ReadClassData( FieldDescription[] fields, byte flags ) { + for( int i = 0; i < fields.Length; i++ ) { + switch( fields[i].Type ) { + case FieldType.Byte: + fields[i].Value = reader.ReadByte(); break; + case FieldType.Float: + fields[i].Value = ReadInt32(); break; // wrong, but we don't support it anyways + case FieldType.Integer: + fields[i].Value = ReadInt32(); break; + case FieldType.Long: + fields[i].Value = ReadInt64(); break; + case FieldType.Boolean: + fields[i].Value = reader.ReadByte() != 0; break; + case FieldType.Object: + if( fields[i].FieldName == "blockMap" ) { + byte typeCode = reader.ReadByte(); + // Skip all blockMap data with awful hacks + if( typeCode == TC_OBJECT ) { + reader.ReadBytes( 315 ); + int count = ReadInt32(); + + byte[] temp = new byte[17]; + Stream stream = reader.BaseStream; + for( int j = 0; j < count; j++ ) { + stream.Read( temp, 0, temp.Length ); + } + reader.ReadBytes( 152 ); + } + } break; + case FieldType.Array: + ReadArray( ref fields[i] ); break; + } + } + } + + void ReadArray( ref FieldDescription field ) { + byte typeCode = reader.ReadByte(); + if( typeCode == TC_NULL ) return; + if( typeCode != TC_ARRAY ) ParseError( TC_ARRAY, typeCode ); + + ClassDescription desc = ReadClassDescription(); + field.Value = reader.ReadBytes( ReadInt32() ); + } + + ClassDescription ReadClassDescription() { + ClassDescription desc = default( ClassDescription ); + byte typeCode = reader.ReadByte(); + if( typeCode == TC_NULL ) return desc; + if( typeCode != TC_CLASSDESC ) ParseError( TC_CLASSDESC, typeCode ); + + desc.ClassName = ReadString(); + reader.ReadUInt64(); // serial version UID + reader.ReadByte(); // flags + FieldDescription[] fields = new FieldDescription[ReadInt16()]; + for( int i = 0; i < fields.Length; i++ ) { + fields[i] = ReadFieldDescription(); + } + desc.Fields = fields; + + typeCode = reader.ReadByte(); + if( typeCode != TC_ENDBLOCKDATA ) ParseError( TC_ENDBLOCKDATA, typeCode ); + ReadClassDescription(); // super class description + return desc; + } + + FieldDescription ReadFieldDescription() { + FieldDescription desc = default( FieldDescription ); + byte fieldType = reader.ReadByte(); + desc.Type = (FieldType)fieldType; + desc.FieldName = ReadString(); + + if( desc.Type == FieldType.Array || desc.Type == FieldType.Object ) { + byte typeCode = reader.ReadByte(); + if( typeCode == TC_STRING ) + desc.ClassName1 = ReadString(); + else if( typeCode == TC_REFERENCE ) + reader.ReadInt32(); // handle + else + ParseError( TC_STRING, typeCode ); + } + return desc; + } + + void ParseError( int expected, int typeCode ) { + throw new InvalidDataException( + String.Format( "Expected {0}, got {1}", expected.ToString( "X2" ), typeCode.ToString( "X2" ) ) ); + } + + struct ClassDescription { + public string ClassName; + public byte Flags; + public FieldDescription[] Fields; + public object Value; + } + + struct FieldDescription { + public FieldType Type; + public string FieldName; + public string ClassName1; + public object Value; + } + + enum FieldType { + Byte = 'B', Float = 'F', Integer = 'I', Long = 'J', + Boolean = 'Z', Array = '[', Object = 'L', + } + + long ReadInt64() { return IPAddress.HostToNetworkOrder( reader.ReadInt64() ); } + int ReadInt32() { return IPAddress.HostToNetworkOrder( reader.ReadInt32() ); } + short ReadInt16() { return IPAddress.HostToNetworkOrder( reader.ReadInt16() ); } + string ReadString() { return Encoding.ASCII.GetString( reader.ReadBytes( ReadInt16() ) ); } + } +} \ No newline at end of file diff --git a/ClassicalSharp/Map/MapFcm3.cs b/ClassicalSharp/Map/MapFcm3.cs index 143cd00d5..34fc2677e 100644 --- a/ClassicalSharp/Map/MapFcm3.cs +++ b/ClassicalSharp/Map/MapFcm3.cs @@ -6,11 +6,20 @@ using System.Text; namespace ClassicalSharp { - public sealed class MapFcm3 { + public sealed class MapFcm3 : IMapFile { + const uint Identifier = 0x0FC2AF40; const byte Revision = 13; + + public override bool SupportsLoading { + get { return true; } + } + + public override bool SupportsSaving { + get { return true; } + } - public static byte[] Load( Stream stream, Game game, out int width, out int height, out int length ) { + public override byte[] Load( Stream stream, Game game, out int width, out int height, out int length ) { BinaryReader reader = new BinaryReader( stream ); if( reader.ReadInt32() != Identifier || reader.ReadByte() != Revision ) { throw new InvalidDataException( "Unexpected constant in .fcm file" ); @@ -20,9 +29,9 @@ namespace ClassicalSharp { height = reader.ReadInt16(); length = reader.ReadInt16(); - int posX = reader.ReadInt32(); - int posY = reader.ReadInt32(); - int posZ = reader.ReadInt32(); + game.LocalPlayer.SpawnPoint.X = reader.ReadInt32() / 32f; + game.LocalPlayer.SpawnPoint.Y = reader.ReadInt32() / 32f; + game.LocalPlayer.SpawnPoint.Z = reader.ReadInt32() / 32f; byte yaw = reader.ReadByte(); byte pitch = reader.ReadByte(); @@ -71,7 +80,7 @@ namespace ClassicalSharp { } } - public static void Save( Stream stream, Game game ) { + public override void Save( Stream stream, Game game ) { BinaryWriter writer = new BinaryWriter( stream ); LocalPlayer p = game.LocalPlayer; Map map = game.Map; diff --git a/ClassicalSharp/Map/MapLvl.cs b/ClassicalSharp/Map/MapLvl.cs deleted file mode 100644 index 33a42cbe8..000000000 --- a/ClassicalSharp/Map/MapLvl.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.IO; -using System.IO.Compression; -using System.Text; - -namespace ClassicalSharp { - - public sealed class MapLvl { - const uint Identifier = 0x88B71B27; - const byte Revision = 2; - const ushort streamMagic = 0xEDAC; - const ushort streamVersion = 0x0500; - - public byte[] Load( Stream stream, Game game, out int width, out int height, out int length ) { - using( GZipStream decompressed = new GZipStream( stream, CompressionMode.Decompress ) ) { - BinaryReader reader = new BinaryReader( decompressed ); - if( reader.ReadUInt32() != Identifier || reader.ReadByte() != Revision ) { - throw new InvalidDataException( "Unexpected constant in .lvl file" ); - } - - // Java serialization constants - if( reader.ReadUInt16() != streamMagic || reader.ReadUInt16() != streamVersion ) { - throw new InvalidDataException( "Unexpected java serialisation constant(s)." ); - } - - int typeCode = 0; - while( ( typeCode = decompressed.ReadByte() ) != -1 ) { - HandleTypeCode( typeCode, reader ); - } - } - width = 0; - length = 0; - height = 0; - return null; - } - - void HandleTypeCode( int typeCode, BinaryReader reader ) { - switch( typeCode ) { - - case 0x42: - ReadPrimField( reader, FieldType.Byte ); break; - case 0x43: - ReadPrimField( reader, FieldType.Char ); break; - case 0x44: - ReadPrimField( reader, FieldType.Double ); break; - case 0x46: - ReadPrimField( reader, FieldType.Float ); break; - case 0x49: - ReadPrimField( reader, FieldType.Integer ); break; - case 0x4A: - ReadPrimField( reader, FieldType.Long ); break; - case 0x53: - ReadPrimField( reader, FieldType.Short ); break; - case 0x5A: - ReadPrimField( reader, FieldType.Boolean ); break; - - case 0x4C: - ReadPrimField( reader, FieldType.Object ); break; - case 0x5B: - ReadPrimField( reader, FieldType.Array ); break; - - case 0x70: // TC_NULL - break; - - case 0x71: // TC_REFERENCE - reader.ReadInt32(); // handle - break; - - case 0x72: // TC_CLASSDESC - { - string className = ReadString( reader ); - reader.ReadUInt64(); // serial identifier - reader.ReadByte(); // various flags - fields = new Field[(reader.ReadByte() << 8) | reader.ReadByte()]; - } break; - - case 0x73: // TC_OBJECT - break; - - case 0x74: // TC_STRING - string value = ReadString( reader ); - break; - - case 0x75: // TC_ARRAY - break; - - case 0x76: // TC_CLASS - break; - - case 0x77: // TC_BLOCKDATA - break; - - case 0x78: // TC_ENDBLOCKDATA - break; - - case 0x79: // TC_RESET - break; - - case 0x7A: // TC_BLOCKDATALONG - break; - - case 0x7B: // TC_EXCEPTION - break; - - case 0x7C: // TC_LONGSTRING - break; - - case 0x7D: // TC_PROXYCLASSDESC - break; - - case 0x7E: // TC_ENUM - break; - - } - } - - enum FieldType { - Byte, Char, Double, Float, Integer, Long, - Short, Boolean, Object, Array, - } - - struct Field { - public string Name; - public FieldType Type; - } - - static string ReadString( BinaryReader reader ) { - int len = ( reader.ReadByte() << 8 ) | reader.ReadByte(); - byte[] data = reader.ReadBytes( len ); - return Encoding.ASCII.GetString( data ); - } - - Field[] fields; - int fieldIndex; - void ReadPrimField( BinaryReader reader, FieldType type ) { - Field field; - field.Name = ReadString( reader ); - field.Type = type; - fields[fieldIndex++] = field; - } - } -} \ No newline at end of file diff --git a/ClassicalSharp/Singleplayer/Commands.cs b/ClassicalSharp/Singleplayer/Commands.cs index 243981aba..4ae1b096a 100644 --- a/ClassicalSharp/Singleplayer/Commands.cs +++ b/ClassicalSharp/Singleplayer/Commands.cs @@ -48,7 +48,9 @@ namespace ClassicalSharp.Singleplayer { Name = "LoadMap"; Help = new [] { "&a/client loadmap [filename]", - "&bfilename: &eLoads a fcm map from the specified filename.", + "&bfilename: &eLoads a map from the specified filename.", + "&eSupported formats are .fcm (fCraft map) and ", + "&e.dat (Original classic map or WoM saved map)", }; } @@ -56,17 +58,31 @@ namespace ClassicalSharp.Singleplayer { string path = reader.NextAll(); if( String.IsNullOrEmpty( path ) ) return; + IMapFile mapFile; + if( path.EndsWith( ".dat" ) ) { + mapFile = new MapDat(); + } else if( path.EndsWith( ".fcm" ) ) { + mapFile = new MapFcm3(); + } else { + game.AddChat( "&e/client loadmap: Map format of file \"" + path + "\" not supported" ); + return; + } + try { using( FileStream fs = new FileStream( path, FileMode.Open, FileAccess.Read, FileShare.Read ) ) { int width, height, length; game.Map.Reset(); - byte[] blocks = MapFcm3.Load( fs, game, out width, out height, out length ); + byte[] blocks = mapFile.Load( fs, game, out width, out height, out length ); game.Map.UseRawMap( blocks, width, height, length ); game.RaiseOnNewMapLoaded(); + + LocalPlayer p = game.LocalPlayer; + LocationUpdate update = LocationUpdate.MakePos( p.SpawnPoint, false ); + p.SetLocation( update, false ); } } catch( FileNotFoundException ) { - game.AddChat( "&e/client load: Couldn't find file \"" + path + "\"" ); + game.AddChat( "&e/client loadmap: Couldn't find file \"" + path + "\"" ); } } } @@ -86,7 +102,8 @@ namespace ClassicalSharp.Singleplayer { if( String.IsNullOrEmpty( path ) ) return; using( FileStream fs = new FileStream( path, FileMode.CreateNew, FileAccess.Write ) ) { - MapFcm3.Save( fs, game ); + MapFcm3 map = new MapFcm3(); + map.Save( fs, game ); } } } diff --git a/readme.md b/readme.md index ac9762bc3..2e5551c59 100644 --- a/readme.md +++ b/readme.md @@ -7,10 +7,10 @@ You can get the latest binaries [here](https://github.com/UnknownShadow200/Class * Works with both minecraft.net and classicube.net accounts. * Lightweight, minimal memory usage compared to the standard client. * Works with effectively all graphics cards that support OpenGL. +* Provides single-player support. (only flatgrass generator, but can load .dat and .fcm maps) It does not: * Work with 'modern/premium' Minecraft servers. -* Provide single-player support. (But I am working on adding it) #### Requirements * Windows: .NET framework 2.0 or Mono. (Vista and later have .NET framework 2.0 built in) @@ -27,7 +27,8 @@ The simple way to use ClassicalSharp is to use the launcher application. You can Note that the first time you run the launcher, a dialog box will pop up saying: *"Some required resources weren't found. Would you like to download them now?"* Just click OK. (This is necessary because I cannot legally redistribute the assets of Minecraft Classic with the application) -*Alternatively, you can pass command line arguments directly to the client. These are expected to be in the form: ` `, where skin server is optional.* +*Alternatively, you can pass command line arguments to run in multiplayer directly to the client. +These are expected to be in the form: ` `, where skin server is optional.* #### Key combinations * Press escape (after joining a world) to switch to the pause menu.