├── LICENSE ├── README.md └── src ├── .gitignore ├── MuConsoleTestClient.csproj ├── MuConsoleTestClient.sln ├── Program.cs ├── TestClient.cs └── stylecop.json /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 sven-n 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenMU Console Test Client # 2 | 3 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 4 | 5 | I wrote this tool to demonstrate how easy it is to write network based tools with the nuget package ```MUnique.OpenMU.Network.Packets``` of the [OpenMU Project](https://github.com/MUnique/OpenMU/). 6 | 7 | It encapsulates most of the packet creation and parsing for you, so you don't have to mess around with bits and bytes yourself. 8 | 9 | Additionally, it's written for the best network performance you can get on .NET. It makes heavy use of ```Span``` and ref structs for packets. The package [MUnique.OpenMU.Network](https://github.com/MUnique/OpenMU/tree/master/src/Network) makes use of ```System.IO.Pipelines```. 10 | 11 | ## Licensing ## 12 | This project is released under the MIT license (see LICENSE file). 13 | 14 | Feel free to use this demo project as a base for your own projects. 15 | 16 | ## Contributions ## 17 | It's not going to be actively maintained or extended. Consider it done for the purpose it serves. 18 | 19 | It's also not perfect code - I wrote this in a matter of minutes. 20 | 21 | ## How to use ## 22 | It supports a start parameter which takes the IP Address and a Port. The default is "127.0.0.1:55901", if nothing is specified. 23 | 24 | It will connect to the specified address using the default network encryption of a MU Online Season 6 Episode 3 Client (English, GMO). 25 | 26 | The application will then ask for the username, password and character name. 27 | It wont do further actions after it entered the game with the character. 28 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | **/content/bundles/*.* 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # Benchmark Results 47 | BenchmarkDotNet.Artifacts/ 48 | 49 | # .NET Core 50 | project.lock.json 51 | project.fragment.lock.json 52 | artifacts/ 53 | **/Properties/launchSettings.json 54 | 55 | *_i.c 56 | *_p.c 57 | *_i.h 58 | *.ilk 59 | *.meta 60 | *.obj 61 | *.pch 62 | *.pdb 63 | *.pgc 64 | *.pgd 65 | *.rsp 66 | *.sbr 67 | *.tlb 68 | *.tli 69 | *.tlh 70 | *.tmp 71 | *.tmp_proj 72 | *.log 73 | *.vspscc 74 | *.vssscc 75 | .builds 76 | *.pidb 77 | *.svclog 78 | *.scc 79 | 80 | # Chutzpah Test files 81 | _Chutzpah* 82 | 83 | # Visual C++ cache files 84 | ipch/ 85 | *.aps 86 | *.ncb 87 | *.opendb 88 | *.opensdf 89 | *.sdf 90 | *.cachefile 91 | *.VC.db 92 | *.VC.VC.opendb 93 | 94 | # Visual Studio profiler 95 | *.psess 96 | *.vsp 97 | *.vspx 98 | *.sap 99 | 100 | # TFS 2012 Local Workspace 101 | $tf/ 102 | 103 | # Guidance Automation Toolkit 104 | *.gpState 105 | 106 | # ReSharper is a .NET coding add-in 107 | _ReSharper*/ 108 | *.[Rr]e[Ss]harper 109 | *.DotSettings.user 110 | 111 | # JustCode is a .NET coding add-in 112 | .JustCode 113 | 114 | # TeamCity is a build add-in 115 | _TeamCity* 116 | 117 | # DotCover is a Code Coverage Tool 118 | *.dotCover 119 | 120 | # Visual Studio code coverage results 121 | *.coverage 122 | *.coveragexml 123 | 124 | # NCrunch 125 | _NCrunch_* 126 | .*crunch*.local.xml 127 | nCrunchTemp_* 128 | 129 | # MightyMoose 130 | *.mm.* 131 | AutoTest.Net/ 132 | 133 | # Web workbench (sass) 134 | .sass-cache/ 135 | 136 | # Installshield output folder 137 | [Ee]xpress/ 138 | 139 | # DocProject is a documentation generator add-in 140 | DocProject/buildhelp/ 141 | DocProject/Help/*.HxT 142 | DocProject/Help/*.HxC 143 | DocProject/Help/*.hhc 144 | DocProject/Help/*.hhk 145 | DocProject/Help/*.hhp 146 | DocProject/Help/Html2 147 | DocProject/Help/html 148 | 149 | # Click-Once directory 150 | publish/ 151 | 152 | # Publish Web Output 153 | *.[Pp]ublish.xml 154 | *.azurePubxml 155 | # TODO: Comment the next line if you want to checkin your web deploy settings 156 | # but database connection strings (with potential passwords) will be unencrypted 157 | *.pubxml 158 | *.publishproj 159 | 160 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 161 | # checkin your Azure Web App publish settings, but sensitive information contained 162 | # in these scripts will be unencrypted 163 | PublishScripts/ 164 | 165 | # NuGet Packages 166 | *.nupkg 167 | # The packages folder can be ignored because of Package Restore 168 | **/packages/* 169 | # except build/, which is used as an MSBuild target. 170 | !**/packages/build/ 171 | # Uncomment if necessary however generally it will be regenerated when needed 172 | #!**/packages/repositories.config 173 | # NuGet v3's project.json files produces more ignorable files 174 | *.nuget.props 175 | *.nuget.targets 176 | 177 | # Microsoft Azure Build Output 178 | csx/ 179 | *.build.csdef 180 | 181 | # Microsoft Azure Emulator 182 | ecf/ 183 | rcf/ 184 | 185 | # Windows Store app package directories and files 186 | AppPackages/ 187 | BundleArtifacts/ 188 | Package.StoreAssociation.xml 189 | _pkginfo.txt 190 | *.appx 191 | 192 | # Visual Studio cache files 193 | # files ending in .cache can be ignored 194 | *.[Cc]ache 195 | # but keep track of directories ending in .cache 196 | !*.[Cc]ache/ 197 | 198 | # Others 199 | ClientBin/ 200 | ~$* 201 | *~ 202 | *.dbmdl 203 | *.dbproj.schemaview 204 | *.jfm 205 | *.pfx 206 | *.publishsettings 207 | orleans.codegen.cs 208 | 209 | # Since there are multiple workflows, uncomment next line to ignore bower_components 210 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 211 | #bower_components/ 212 | 213 | # RIA/Silverlight projects 214 | Generated_Code/ 215 | 216 | # Backup & report files from converting an old project file 217 | # to a newer Visual Studio version. Backup files are not needed, 218 | # because we have git ;-) 219 | _UpgradeReport_Files/ 220 | Backup*/ 221 | UpgradeLog*.XML 222 | UpgradeLog*.htm 223 | 224 | # SQL Server files 225 | *.mdf 226 | *.ldf 227 | *.ndf 228 | 229 | # Business Intelligence projects 230 | *.rdl.data 231 | *.bim.layout 232 | *.bim_*.settings 233 | 234 | # Microsoft Fakes 235 | FakesAssemblies/ 236 | 237 | # GhostDoc plugin setting file 238 | *.GhostDoc.xml 239 | 240 | # Node.js Tools for Visual Studio 241 | .ntvs_analysis.dat 242 | node_modules/ 243 | 244 | # Typescript v1 declaration files 245 | typings/ 246 | 247 | # Visual Studio 6 build log 248 | *.plg 249 | 250 | # Visual Studio 6 workspace options file 251 | *.opt 252 | 253 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 254 | *.vbw 255 | 256 | # Visual Studio LightSwitch build output 257 | **/*.HTMLClient/GeneratedArtifacts 258 | **/*.DesktopClient/GeneratedArtifacts 259 | **/*.DesktopClient/ModelManifest.xml 260 | **/*.Server/GeneratedArtifacts 261 | **/*.Server/ModelManifest.xml 262 | _Pvt_Extensions 263 | 264 | # Paket dependency manager 265 | .paket/paket.exe 266 | paket-files/ 267 | 268 | # FAKE - F# Make 269 | .fake/ 270 | 271 | # JetBrains Rider 272 | .idea/ 273 | *.sln.iml 274 | 275 | # CodeRush 276 | .cr/ 277 | 278 | # Python Tools for Visual Studio (PTVS) 279 | __pycache__/ 280 | *.pyc 281 | 282 | # Cake - Uncomment if you are using it 283 | # tools/** 284 | # !tools/packages.config 285 | 286 | # Tabs Studio 287 | *.tss 288 | 289 | # Telerik's JustMock configuration file 290 | *.jmconfig 291 | 292 | # BizTalk build output 293 | *.btp.cs 294 | *.btm.cs 295 | *.odx.cs 296 | *.xsd.cs 297 | 298 | # SubVersion 299 | .svn/ 300 | 301 | # Pandoc html result files 302 | doc/*.htm 303 | 304 | # text files; for documentation etc. use markdown 305 | *.txt 306 | 307 | # bundles 308 | src/AdminPanel/wwwroot/content/css/app.css 309 | src/AdminPanel/wwwroot/content/css/app.min.css 310 | src/AdminPanel/wwwroot/content/js/app.js 311 | src/AdminPanel/wwwroot/content/js/app.js.map 312 | -------------------------------------------------------------------------------- /src/MuConsoleTestClient.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/MuConsoleTestClient.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29728.190 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MuConsoleTestClient", "MuConsoleTestClient.csproj", "{02DFEBC7-4380-4AAE-AB59-0564412F025E}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {02DFEBC7-4380-4AAE-AB59-0564412F025E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {02DFEBC7-4380-4AAE-AB59-0564412F025E}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {02DFEBC7-4380-4AAE-AB59-0564412F025E}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {02DFEBC7-4380-4AAE-AB59-0564412F025E}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {FC9B4EB9-D4C9-46B5-87BC-5F252E7A4396} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /src/Program.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Licensed under the MIT License. See LICENSE file in the project root for full license information. 3 | // 4 | 5 | namespace MuConsoleTestClient 6 | { 7 | using System; 8 | using System.Net; 9 | using System.Threading.Tasks; 10 | using MUnique.OpenMU.Network; 11 | using MUnique.OpenMU.Network.SimpleModulus; 12 | using MUnique.OpenMU.Network.Xor; 13 | using Pipelines.Sockets.Unofficial; 14 | 15 | /// 16 | /// The program with the main entry point. 17 | /// 18 | internal class Program 19 | { 20 | /// 21 | /// Defines the entry point of the application. 22 | /// 23 | /// The arguments. The target address can be specified as an argument in the format [IP]:[Port]. 24 | internal static async Task Main(string[] args) 25 | { 26 | var address = args.Length > 0 ? args[0] : "127.0.0.1:55901"; 27 | var socketConnection = await SocketConnection.ConnectAsync(IPEndPoint.Parse(address)); 28 | var encryptor = new PipelinedXor32Encryptor(new PipelinedSimpleModulusEncryptor(socketConnection.Output, PipelinedSimpleModulusEncryptor.DefaultClientKey).Writer); 29 | var decryptor = new PipelinedSimpleModulusDecryptor(socketConnection.Input, PipelinedSimpleModulusDecryptor.DefaultClientKey); 30 | var connection = new Connection(socketConnection, decryptor, encryptor); 31 | _ = new TestClient(connection); 32 | 33 | await connection.BeginReceive(); 34 | 35 | Console.WriteLine("Press any key to continue"); 36 | Console.ReadKey(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/TestClient.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Licensed under the MIT License. See LICENSE file in the project root for full license information. 3 | // 4 | 5 | namespace MuConsoleTestClient 6 | { 7 | using System; 8 | using System.Buffers; 9 | using System.Collections.Generic; 10 | using System.Text; 11 | using MUnique.OpenMU.Network; 12 | using MUnique.OpenMU.Network.Packets; 13 | using MUnique.OpenMU.Network.Packets.ClientToServer; 14 | using MUnique.OpenMU.Network.Packets.ServerToClient; 15 | using MUnique.OpenMU.Network.Xor; 16 | 17 | /// 18 | /// A basic test client which logs into the server and selects a character. 19 | /// 20 | public class TestClient 21 | { 22 | private delegate void HandlePacket(Span packet); 23 | 24 | private readonly IDictionary packetHandlers = new Dictionary(); 25 | private readonly IDictionary characterHandlers = new Dictionary(); 26 | private readonly IConnection connection; 27 | 28 | /// 29 | /// You need a for the username and password of the login packet. 30 | /// 31 | private readonly Xor3Encryptor xor3Encryptor = new Xor3Encryptor(0); 32 | 33 | /// 34 | /// The player identifier. 35 | /// 36 | private ushort playerId; 37 | 38 | /// 39 | /// Initializes a new instance of the class. 40 | /// 41 | /// The connection. 42 | public TestClient(IConnection connection) 43 | { 44 | this.connection = connection; 45 | this.connection.PacketReceived += this.OnPacketReceived; 46 | 47 | this.packetHandlers.Add(GameServerEntered.Code, this.HandleLoginLogout); 48 | this.packetHandlers.Add(CharacterList.Code, this.HandleCharacterPackets); 49 | this.characterHandlers.Add(CharacterList.SubCode, this.HandleCharacterList); 50 | this.characterHandlers.Add(CharacterInformation.SubCode, this.HandleCharacterInformation); 51 | 52 | this.packetHandlers.Add(AddNpcsToScope.Code, this.HandleAddNpcsToScope); 53 | this.packetHandlers.Add(AddCharactersToScope.Code, this.HandleAddCharactersToScope); 54 | } 55 | 56 | private void OnPacketReceived(object sender, ReadOnlySequence packetSequence) 57 | { 58 | // In this example I try to show you a pattern how you can read a packet with a minimum of memory allocations. 59 | 60 | // First, we rent some memory from the pool 61 | using var memoryOwner = MemoryPool.Shared.Rent((int)packetSequence.Length); 62 | 63 | // The memory we get from the pool is bigger than the sequence which we received, so we first slice it to the right size. 64 | var packet = memoryOwner.Memory.Slice(0, (int)packetSequence.Length).Span; 65 | 66 | // Then we need to copy the content of the sequence to our packet buffer 67 | packetSequence.CopyTo(packet); 68 | 69 | // now we need to check which kind of packet we received. In this example, I use some simple if statements. 70 | // In more complex scenarios, I suggest to use some kind of data structure which can be maintained easier. 71 | if (this.packetHandlers.TryGetValue(packet.GetPacketType(), out var handler)) 72 | { 73 | handler(packet); 74 | } 75 | } 76 | 77 | private void HandleLoginLogout(Span packet) 78 | { 79 | if (packet.GetPacketSubType() == GameServerEntered.SubCode) 80 | { 81 | this.HandleGameServerEntered(packet); 82 | } 83 | else if (packet.GetPacketSubType() == LoginResponse.SubCode) 84 | { 85 | this.HandleLoginResponse(packet); 86 | } 87 | else if (packet.GetPacketSubType() == LogoutResponse.SubCode) 88 | { 89 | this.HandleLogoutResponse(packet); 90 | } 91 | } 92 | 93 | private void HandleCharacterList(Span packet) 94 | { 95 | CharacterList characterList = packet; 96 | Console.WriteLine($"Received character list, count: {characterList.CharacterCount}, Characters:"); 97 | for (int i = 0; i < characterList.CharacterCount; i++) 98 | { 99 | var character = characterList[i]; 100 | Console.WriteLine($"Index: {character.SlotIndex}, Name: {character.Name}, Level: {character.Level}, Status: {character.Status}"); 101 | } 102 | 103 | Console.Write("Which character should be selected (please enter the name)? "); 104 | var name = Console.ReadLine(); 105 | 106 | this.connection.SendSelectCharacter(name); 107 | 108 | Console.WriteLine("Sent selection packet"); 109 | } 110 | 111 | private void HandleCharacterInformation(Span packet) 112 | { 113 | CharacterInformation characterInformation = packet; 114 | Console.WriteLine($"Character entered the game on map {characterInformation.MapId}, Health: {characterInformation.CurrentHealth}/{characterInformation.MaximumHealth}"); 115 | } 116 | 117 | private void HandleCharacterPackets(Span packet) 118 | { 119 | if (this.characterHandlers.TryGetValue(packet.GetPacketSubType(), out var handler)) 120 | { 121 | handler(packet); 122 | } 123 | } 124 | 125 | private void HandleLoginResponse(Span packet) 126 | { 127 | LoginResponse loginResponse = packet; 128 | if (loginResponse.Success == LoginResponse.LoginResult.Okay) 129 | { 130 | Console.WriteLine("Login successful"); 131 | this.connection.SendRequestCharacterList(); 132 | Console.WriteLine("Requested character list"); 133 | } 134 | else 135 | { 136 | Console.WriteLine($"Login failed, reason: {loginResponse.Success}"); 137 | } 138 | } 139 | 140 | private void HandleGameServerEntered(Span packet) 141 | { 142 | GameServerEntered enteredMessage = packet; 143 | this.playerId = enteredMessage.PlayerId; 144 | 145 | Console.WriteLine($"Received GameServerEntered packet, player id: {enteredMessage.PlayerId}, version: {enteredMessage.VersionString}"); 146 | Console.Write("Enter Username: "); 147 | var username = Console.ReadLine(); 148 | Console.Write("Enter Password: "); 149 | var password = Console.ReadLine(); 150 | 151 | // Because this packet has some special encrypted fields, we use the "StartWrite..." extension method 152 | using var writer = this.connection.StartWriteLoginLongPassword(); 153 | var loginPacket = writer.Packet; 154 | loginPacket.Password.WriteString(password, Encoding.ASCII); 155 | loginPacket.Username.WriteString(username, Encoding.ASCII); 156 | this.xor3Encryptor.Encrypt(loginPacket.Username); 157 | this.xor3Encryptor.Encrypt(loginPacket.Password); 158 | 159 | enteredMessage.Version.CopyTo(loginPacket.ClientVersion); 160 | 161 | loginPacket.TickCount = (uint)Environment.TickCount; 162 | 163 | writer.Commit(); 164 | Console.WriteLine("Sent login packet"); 165 | } 166 | 167 | private void HandleAddCharactersToScope(Span packet) 168 | { 169 | AddCharactersToScope charactersPacket = packet; 170 | for (int i = 0; i < charactersPacket.CharacterCount; i++) 171 | { 172 | var character = charactersPacket[i]; 173 | 174 | Console.WriteLine($"Player in Scope, Id {character.Id}, Name {character.Name} {(this.playerId == character.Id ? " [Hey, that's me]" : string.Empty)}, X: {character.TargetPositionX}, Y: {character.TargetPositionY}, Rotation: {character.Rotation}"); 175 | } 176 | } 177 | 178 | private void HandleAddNpcsToScope(Span packet) 179 | { 180 | AddNpcsToScope npcPacket = packet; 181 | for (int i = 0; i < npcPacket.NpcCount; i++) 182 | { 183 | var npc = npcPacket[i]; 184 | Console.WriteLine($"NPC in Scope, Id {npc.Id}, Number {npc.TypeNumber}, X: {npc.TargetPositionX}, Y: {npc.TargetPositionY}, Rotation: {npc.Rotation}"); 185 | } 186 | } 187 | 188 | private void HandleLogoutResponse(Span packet) 189 | { 190 | LogoutResponse logoutResponse = packet; 191 | Console.WriteLine($"Logout response: {logoutResponse.Type}"); 192 | } 193 | } 194 | } -------------------------------------------------------------------------------- /src/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "documentationRules": { 5 | "companyName": "sven-n", 6 | "copyrightText": "Licensed under the MIT License. See LICENSE file in the project root for full license information.", 7 | "xmlHeader": true 8 | } 9 | } 10 | } 11 | --------------------------------------------------------------------------------