|
Don't expect anything new or exciting in here for some time, there's a lot of infrastructure work to get done before I start expanding this at all.
I wanted a place to keep logs of what I'll be working on and since I've been learning a fair amount from snippets/sources posted on here I figured I'd actually post something for once.
I've been working with C# professionally for ~2 years and programming as a hobby for around 10 years. Recently I've been tutoring a friend with his uni work and he asked if I knew anything about Runescape private servers. I have experience with server emulation, but nothing Runescape related so I found a couple of existing 530 C# projects and dove in. While there's a lot of bad programming habits in there, I've understood enough to get started.
Since he needed to learn about everything from source control to good design, we'll be using a variety of tech to help with project management and teach him some useful skills.
- Visual Studio 2017 for general bits and pieces
- GitLab for hosting and Github Desktop Client for source control
- Hack n Plan for organisation and time management, which is very similar to what I use in my day job.
- Resharper for the occasional kick up the ass
- NUnit for writing unit/integration tests
- Possibly EntityFramework for database work, or I may go with vanilla sql, or may even roll my own library, I haven't decided yet!
It's pretty generic, it's missing a lot of data (stairs/ladders/doors/levers, music areas etc) and has a few bugs but it runs reliably and has a nice amount of features finished already.
A lot of NPCs are missing, shops etc.
Quest interfaces don't open, can't start quests etc. This is something I'm looking forward to working on.
Achievement Diaries are the same of course, but in 530 they're a bit rubbish anyway.
Although the server appears to support HD mode, the client is missing ...something. The download I've been using has a generic decompiled Java client, so there's no point trying to make sense of it as I have plans to ditch it in the future anyway.
Roadmap as a way of tracking progress:
Done:
Lots of reading!
In progress:
Convert cache from XML to JSON, introduce JSON bindings and immutable objects for the cache.
Separate out logic and information to conform to SOLID principle. - This is going to be a long process, as the code is messy and badly written from top to bottom.
Future work:
Write database solution. I didn't find many sources online that had database work done and those that do can be improved on.
A good encryption solution for storing passwords, the amount of SHA1/SHA256/MD5 servers I see are insane. Currently this source saves passwords in plaintext, saving players as XML files
Bug fixes in existing code.
Investigate missing features and data.
Write utilities for viewing, editing and saving information directly from the cache -> server's JSON format and vice versa.
Ditch client and roll our own in Unity3D.
Welcome to rune-server, sounds like a cool project
Looks really good! I'd love to see where this project can go.
Good luck homie
Best of luck mate.
I appreciate the heads up
My day job is closer to DevOps engineer than Software Engineer, so I have a lot of experience with SQL Server 2012 and up, MySQL etc. I'm hoping I have enough knowledge to avoid most of the obvious pot holes.
Thanks for the words of encouragement everyone else. I'll be posting some resources about how I'm refactoring for performance and readability, hopefully people can learn from this project
Well I promised to post some code snippets, so here we are. Focusing on improvements for data loading, here's an example of changes I've made going from XML to JSON.
Before:
After:Code:private void loadDoors() { if (!File.Exists(Misc.getServerPath() + @"\data\doors.xml")) { Misc.WriteError(@"Missing data\doors.xml"); return; } try { StreamReader objStreamReader = new StreamReader(Misc.getServerPath() + @"\data\doors.xml"); XmlSerializer serializer = new XmlSerializer(typeof(List<Door>)); doors = (List<Door>)serializer.Deserialize(objStreamReader); } catch (Exception e) { Misc.WriteError((e.InnerException == null ? e.ToString() : e.InnerException.ToString())); } Console.WriteLine("Loaded " + doors.Count + " door configurations."); }
For those unfamiliar with C# and related libraries, JsonConvert comes from Newtonsoft Json, one of the most widely used libraries available on Nuget. It's a library for handling JSON with modules for converting XML to JSON and vice versa. Calling DeserializeObject<T> requires a type, which we use Json bindings for. This is a middle ground between json as a string and an object that can be used by the compiler. We can then take that Json binding and convert it to a concrete object which is fully or partially immutable. Here's what this particular Json Binding looks likeCode:private void LoadDoors() { var doorPath = Misc.getServerPath() + @"\data\doors.json"; if (!File.Exists(doorPath)) { Misc.WriteError(@"Missing data\doors.json"); return; } var json = File.ReadAllText(doorPath); var doorBindings = JsonConvert.DeserializeObject<DoorJsonBinding[]>(json); m_Doors = doorBindings.Select(ToDoor).ToList(); Console.WriteLine("Loaded " + m_Doors.Count() + " door configurations."); }
The JsonProperty attributes that we can specify per property gives JsonConvert more restrictions (or freedom) to work with the input Json. We're using Required.Always to specify that there must always be a property matching these names (which are case insensitive) and they must always have a value (not null). Other options include allowing nulls, allowing the property to be absent but NOT be null when present and allowing anything at all.Code:public sealed class DoorJsonBinding { [JsonProperty(Required = Required.Always)] public Door.DefaultState DefaultStatus { get; set; } [JsonProperty(Required = Required.Always)] public int OpenDoorId { get; set; } [JsonProperty(Required = Required.Always)] public int ClosedDoorId { get; set; } [JsonProperty(Required = Required.Always)] public CoordinateJsonBinding ClosedDoorLocation { get; set; } [JsonProperty(Required = Required.Always)] public CoordinateJsonBinding OpenDoorLocation { get; set; } [JsonProperty(Required = Required.Always)] public int OpenDirection { get; set; } [JsonProperty(Required = Required.Always)] public int ClosedDirection { get; set; } [JsonProperty(Required = Required.Always)] public bool Closable { get; set; } }
Another good attribute to use is PropertyName. Working with JSON that isn't written by you can lead to dealing with property names which don't describe behaviour, can be confusing to keep track of etc. Using PropertyName allows you to map a property within the JSON to a differently named property in a Json Binding.
Given that the server I'm working with is a C&P conversion from Java, there are a lot of bad habits pasted over which can disappear. One of them is the unnecessary use of methods to get and set property values in a class. I'll keep going with our door example:
Before:
After:Code:public class Door { public Location closedDoorLocation; // Why is this public when we have methods to get and set it :/ public Door() // Empty constructor :( Having no constructor does the same thing when calling new Door(); { } // We should never be setting the closed door location manually, this should be // determined by data and never change once the object is constructed :( public void setClosedDoorLocation(Location closedDoorLocation) { this.closedDoorLocation = closedDoorLocation; //Use of "this" to provide context is only necessary because parameter and property names are the same } public Location getClosedDoorLocation() { return closedDoorLocation; } }
We have exactly the level of functionality that we need, with a safety net in case anyone tries to set a door location by mistake.Code:public class Door { // Can only be set inside object's constructor, trying to change the value will // lead to an error stating the property is readonly. public Location ClosedDoorLocation { get; } public Door(Location closedDoorLocation) { ClosedDoorLocation = closedDoorLocation; } }
Once all the objects are converted, the next step will probably be to pull the skill data out of hardcoded anonymous objects and in to JSON as well. This way they can be expanded more easily, not need comments to denote field names and can be reloaded from disk.
Enjoy your project bro. Good luck.
Discovered and fixed an interesting bug which I'd introduced about a week ago while doing some refactoring. I thought I'd broken NPC spawning, but it turns out I'd caused all NPCs to spawn at 0, 0, 0. I think this was caused by some fragility with XML and mixing and matching JSON Bindings and XML. The Location data for all the spawns was read from XML and gave no errors when it broke and defaulted all data to Int32 default value. The rest of the NPC data was being read as JSON and used to spawn the right NPCs at their locations. As soon as I began reading the spawn info as JSON, all was right with the world again.
Teleporting to 0, 0, 0 crashed the client very quickly, with a very unhelpful error message since I'm working with a decompiled, but I managed to spot all 850 NPCs stacked on top of me, including GWD bosses and so on. Couldn't grab screenshots in time
I've implemented a basic generic method to read and convert JSON in to any type that I need, to avoid code duplication. Here's the method:
So instead of requiring something along the lines of this for every single JSON file I want to read fromCode:public static T DeserializeJsonFromFile<T>(string filePath) { if (!File.Exists(filePath)) { //TODO: throw tailored exception later } //Assume that file read will be successful for now var json = File.ReadAllText(filePath); return JsonConvert.DeserializeObject<T>(json); }
I can cut out the code duplication and re-use my generic methodCode:var path = Misc.getServerPath() + @"\data\npcs.json"; if (!File.Exists(path)) { //TODO: Tear out and replace this shit error handling Misc.WriteError(@"Missing data\npcs.json"); return; } var json = File.ReadAllText(path); var spawnBindings = JsonConvert.DeserializeObject<NpcSpawnJsonBinding[]>(json);
Code:var path = Misc.getServerPath() + @"\data\npcs.json"; var spawnBindings = DeserializeObject<NpcSpawnJsonBinding[]>(path);
« Previous Thread | Next Thread » |
Thread Information |
Users Browsing this ThreadThere are currently 1 users browsing this thread. (0 members and 1 guests) |