Leaving WordPress behind …

I always knew WordPress.com would only be a temporary solution. Well, three years are quite long for “temporary”. I stayed so long because I liked the simplicity it offered. WordPress.com offers you a full-featured WordPress-hosting including free, automatic updates. It helped me getting started. But the lack of features and support is kinda annoying. Now it’s time for a change.

Continue reading “Leaving WordPress behind …”

Advertisements

Silkroad Online – Performing character movement with memory-edits

Some might already have seen it on my Youtube Channel. You can move your character only by writing to a memory location. The client will send the required packets, too. It can be done using any memory-editor, even with a plain WriteProcessMemory.

My initial idea was not to move by memory, but to move at all. I wanted to get inside the movement processing and play with collision detection and path finding. So this article describes an unexpected side-effect.

Research

We know that movement has two packets. Movement request 0x7021 and movement update 0xB021. Request has only 2 references in the same function so it might be a good idea to trace that. Keep in mind, that moving by skill (like ghost walk or teleport) will trigger another packet.

Using Runtime Type Information reveals a class named CNavigationDeadreckon. Dead reckoning is a navigation process used in various sectors including marine and air navigation. See Wikipedia for more information.

Also interesting is the fact, that the client has no clue about object collision when triggering movement. Collision is calculated during moving. Also, the decal marking the location on the floor is freely place-able anywhere.

I also did a lot of memory scanning using Cheat Engine and eventually figured out the location of the cursor decal and two other vectors related to the cursor either.

Analysis

Tracking down the movement request opcode leads to a small function that actually just sends the packet. I usually label everything I can find a proper name for. Also check for cross-references. Maybe you already know something about them. You might also notice that this function is a THISCALL. If we set a breakpoint on the first instruction, we can see the content of the ECX register and look for RTTI.

Looks like we got a proof that CNavigationDeadreckon is indeed involved in the navigation. We also see a couple of floats. Having a closer look reveals that the addresses of the floats match up the results we got in Cheat Engine earlier.

We can now track down the usages by placing a breakpoint on write using your favorite tool (Cheat Engine, possibly).

We land here.

Inspecting the individual MOV-instructions we can clearly see the individual components of the structure being assigned. Aside from the three floats at 0x10, 0x14 and 0x18, we can see a 2 byte value written to 0x0C and another one byte value written to 0x1C that also happens to be always 1.

A famous 2 byte value throughout the whole client is the region on the map. The region specifies the tile of 1920×1920 in-game units we are on. Indeed, the 2 byte value on this one is representing the region.

At this point I just played around and set some values to the coordinate-floats and finally set the 1 byte value to 1. What happened  is documented in the video on YouTube.

What’s left is:

Locating a pointer for CNavigationDeadreckon

Through our breakpoint, we know the class and a code location for this class. We also know that the pointer to the class is in EDI and ECX. This is also proven at the beginning of the function we are looking at right now.

Looking at cross-references brings up this function:

Knowing that ECX carries our object reference inside the function, we have to look for ECX being set before the function – which is not very far away. LEA will make a pointer from an address, in this case its actually equal to the simple math operation ecx = edi + 0x6AC. We now know that our object is encapsulated in another object. We can also place a breakpoint there and check out the value of EDI for any RTTI data.

Another part of CGInterface … if you’re a fellow reader of this blog, you have seen this class already as it appeared in two other articles:

Now we know how to find the pointer to CNavigationDeadreckon and the values to change. Why it works that way and what’s behind navigation inside the client is a whole new piece I really want to cover in a future article.

Using Runtime-Type-Information (RTTI) to extract class names and hierarchy

A lot of people keep asking how I figured out the class names of Silkroad Online. Some believe I already have the source and I’m just trolling around, others think I’m just a mad genius that transforms assembly code into source code (including original comments) just by looking at it.

Sadly, none of it is true. I’m just utilizing information everyone can access. Aside from checking for strings (that carry much useful information, too), I’m checking on the Runtime Type Information (RTTI) fairly often.

This allows me a to accomplish a few major things, that helped me making such fast progress in Silkroad Online:

  • Identify class names of virtual function tables
    • Resulting in at least 1 new known function, the virtual destructor for the class-instance, but usually quite a few more.
  • Rebuild the entire class-hierarchy
    • Also allows naming overwritten functions (in case you already know the parent or child function name)
    • Speeds up analysis since you can check on multiple versions of the same function
  • Identify instances of classes while running the game
    • Since (almost) every instance ships a pointer to the virtual function table, I can use this to get the RTTI of that class and therefore the type name and hierarchy of that instance.

So … no source, no magic, no super brain. Just using available information.

What is Runtime Type Information?

Runtime Type Information is a mechanism that was build to expose information about a type at runtime. But from a reverse engineers point of view, it’s also a valuable information source for static analysis. In general, RTTI can mean information about type names, data-types or even memory structure.

As many of you know, Silkroad Online is written in C++. And some of you might know: C++ does not make heavy use of type reflection, as polymorphism and meta-template programming are superior to reflection by far. For C++, Runtime Type Info is only capable of identifying types. It can make sure an instance is of a certain type or derived of a certain type and even name this type.

How does RTTI look like?

Good question. I’m not going as in-depth as usual now. There is tons of comprehensive information on the internet already. My own summary would not add any value to that.

igorsk of OpenRCE wrote an excellent article about all the internals of RTTI on MSVC.

A quick look

Runtime Type Information basically looks like this:

What we got here is a virtual function table. 4 byte before the first entry of the virtual function table is the location of a pointer to the “Complete Object Locator”. Some disassemblers offer plugins to handle RTTI automatically, such as Class Informer for IDA (the new IDA 7 has build in RTTI-support).

After using Class Informer, the pointer to the Complete Object Locator now looks like this. The plugin walked through the RTTI-structure and collected all information it could find.

Now we know this vftable belongs to a class named “CIFEquipment” and it is derived from “CIFWnd”. Additionally, we also see the rest of the hierarchy. If we follow the locator, we find this structure which combines the type info for the current type (yellow) and the hierarchy (not yellow :P).

Basic type info

Following this first reference, we land at the type_info instance that basically tells us the type-name.

The name is in its mangled form, but you can easily demangle that by hand. You’ll get the idea.

Hierarchy info

Following the second reference, we land at the Class Hierarchy Descriptor.

Class Informer did a whole lot of work here. But you can trace that all down by hand. Its not overly complicated.

Utilizing RTTI at runtime

I’ll give you a quick example how to manually find the type_info for an instance of a class. We’ll use 0x0110F80C as an address here. A very imporant offset for Silkroad as it allows access to the 2D interface.

It points to some unknown memory region. Just like any other pointer would do. In order to get to the type_info, we need to check out this memory region.

Nothing special yet … but if we treat the first bytes as a 4 byte value, we get a pointer, again.

And at this point, IDA already knows that this is a pointer to the virtual function table of CGInterface. And if we check that pointer out …

… we can see the Object Locator of CGInterface and start checking out the type_info from there. So we know know for sure that this memory location is from a type named CGInterface.

… but Flo, i am too lazy …

Say no more. I got you. ReClassEx by dude719 and ReClass.NET by KN4CK3R both handle RTTI very well.

… but Flo, i can’t find any RTTI in other applications

Runtime Type Information is only generated for types where it is required. If you’re using dynamic_cast for example. Also, not all classes have virtual function tables at all. A vftable is also, only build when needed.

If you can’t find a useful pointer on the first 4 bytes of your pointer, then your class probably has no virtual function table or your pointer is wrong.

Just like this one. Even though I followed a valid pointer to an object, the first 4 byte only tell me “BAADF00D”, which is a Microsoft-way of saying: “Sorry, there is no data to look at”.

If you have a virtual function table, but the Complete Object Locator is missing, this class is build without any Runtime Type Information.

Runtime Type information is considered bad style so don’t expect it to magically solve all of your problems. I think Joymax was aware of this because in the early builds of Silkroad, there was no RTTI. It appeared somewhere during the development, and yet i was unable to find a single point where it is used. Joymax even build their own Runtime Type Info System, which is used in many places. Okay, its not their own … its stolen heavily inspired from here.

Silkroad Online – Reconstructing UI-access for the sake of colors

This article acts as a follow-up to my previous article.

We want to use our knowledge from the previous adventure to access the SystemMessage-log and print some coloured text.

Research

We can use the /frame command to figure out the IDs of the Control. We can also use Media.pk2/resinfo since we know most of the GUI-Stuff is happening in there. The relevant id is 68. We also know that all strings being logged come from textdata.

Analysis

Looking for strings will end up in this function almost every time.It looks really big, but its actually not very complex. Its just a really really big switch statement. It contains many different strings for nearly all kinds of messages. We still don’t want to analyse a function this big, but its still worth a label because you don’t want to run into it over and over again. So give it a nice name and move on.

Not all messages go through that function. Some are placed inside message handlers. Just trace some messages while playing the game, you will figure out some. For example “[%d] gold gained”.

We notice the call to GetTranslation and already know the return value is a std::wstring-pointer. The following branch is also nothing new for us. Its checking if the wstring is using the internal or the external buffer. The buffer is then passed to a function I already labelled MessageLogPrintf.

At this point we could already close out the article. Just define a function pointer to this address.

But that’s boring. Lets have a look inside. Nothing really important yet … call to vswprintf, not entirely unexpected. Lets dig into PrintSystemMessage. Aha. We got a call to GetGUIObjectByID. We already know this call from the previous article. There is our ID (44h => 68). And there are some big numbers. 0xFFBACFF2 … you might have seen these already when messing with the chat. These are colors. Colors encoded by “D3DCOLOR_ARGB”, a macro of DirectX.

Printing colorful messages

A simple call at first

Lets have a look at CIFSystemMessage::write. A bunch of parameters. If you look at the cross-references, you’ll notice the first argument is always 0xFF and the last two are always 0 and 1. We also see ECX being set right before the call, indicating a THISCALL. EAX is obviously carrying the color, so the last parameter left is obviously the message.

systemmessage->write(0xFF, color, message, 0, 1);

This is what we got so far.

In case you are wondering where I get all these class names: They are written in ginterface.txt

Reconstructing this

Highlighting makes everything better. ECX is the this-pointer for the function call. Its copied from EDI, so we need to figure out where EDI is set.

With the help of highlighting, we clearly see that there is only one write-instruction to EDI. Its the return value of GetGUIObjectByID. This is already a THISCALL, so we need a this-pointer for it, too. Note, that its not ECX being used, ECX has an offset of 0x36C added to it. ECX is also not set in this function, so look for cross-references to our current function. There are many, check out some of them until you see a familiar pattern.

Its g_CGInterface, again. Lets continue on our code. Remember to implement that 0x36C offset somehow. I added a simple member-variable:

CIFSystemMessage *systemmessage = g_CGInterface->field_36C.GetGUIObjectByID(68, 1);
systemmessage->write(0xFF, color, message, 0, 1);

Transforming into working code

Having our pseudo-code set up, we can implement what’s required to make this code work.

Reconstructing CGInterface

This step might get confusing at first, I’ll try to keep it as simple as possible. Lets start with the obvious things first: CGInterface is a class. CGInterface has a member-variable named field_36C. This field has an offset of 0x36C, so we add a padding of 0x36C bytes before it.

class CGInterface {
public:
    char _pad0[0x36C]; // Add a padding to shift the field
    void* field_36C;
};

We can also make g_CGInterface work. The value used for ECX in the assembly is 0x0110F80C. Thats a pointer to a pointer. We dereference it once, so we get the instance of CGInterface. (Yes, thats a little tricky for beginners. Don’t give up yet. It took me many times to get it right the first time, too.)

#define g_CGInterface (*(CGInterface**)0x0110F80C)

Reconstructing CIFSystemMessage

This has only one function. As we can see in the disassembly, its name is “write” and it has 5 arguments.

class CIFSystemMessage {
public:
    void write(int a1, int color, wchar_t* msg, int a2, int a3);
}

We can also implement that function right away because we know everything required. We just make it a simple redirect to the original function.

void CIFSystemMessage::write(int a1, int color, wchar_t* msg, int a2, int a3)
{
    reinterpret_cast(0x007B89E0)(this, a1, color, message, a2, a3);
}

A little helper for field_36C

Sadly, the part field_36C.GetGUIObjectByID is not working yet. We need a little helper for this one. Lets name the helper field_36C_t, because we don’t have cool a name for it.

class field_36C_t {
public:
    void* GetGUIObjectByID(int ident, int a2);
};

We just make that function a redirect, too. Just like the “write”-function.

void* field_36C_t::GetByID(int ident, int a2)
{
    return reinterpret_cast(0x008B51F0)(this, ident, a2);
}

Last task is to change the type of field_36C in CGInterface from void* to field_36C_t (no pointer!).

Execute

Whats left is executing what we just wrote all these wrappers for.

wchar_t message[] = L"Hello World";
int color = D3DCOLOR_ARGB(255, 0, 255, 0);

CIFSystemMessage *systemmessage = g_CGInterface->field_36C.GetGUIObjectByID(68, 1);
systemmessage->write(0xFF, color, message, 0, 1);

Silkroad Online – Simple access to UI-elements

I was asked if I can share my knowledge about in-Game notifications. Luckily, this is something I recently achieved, so lets go.

To make things clear from the start: We are talking about these nice little things.

Research

I’m pretty sure every single one of you can tell what each color is used for. Red is Notices from Game Masters. Blue is (mostly) game-related information. And green is used for annoying messages during Fortress War, Capture the Flag and other things like quests (who does quests…?)

The quest-related data is likely to be client-side. Also the green annoying messages tend to be client-side because they are localized. We can look for their localization identifiers from the clients textdata.

We could know that the overall interface-layout is stored in the Media.pk2/resinfo/ginterface.txt. Looking for CIFNotify will show three notification areas.

GDR_NOTICE:CIFNotify
{
    ID=INTEGER,"20"
}

GDR_UPDATE_QUEST_INFO:CIFNotify
{
    ID=INTEGER,"42"
}

GDR_WARNING_WND:CIFNotify
{
    ID=INTEGER,"35"
}

I filtered out the relevant information for you. Everything in this file has an ID, which is (probably) unique. Since the whole UI is backed by these files, there must be a function that can get controls by their ID.

Analysis

There are tons of strings to look at. And all of them lead to the correct location, but how do we know which one is the right one? Most of the references lead in BIG! functions like this oneor this one …

or maybe this one …

You COULD reverse engineer these … I’ve marked interesting locations in pink. You have to look very closely on the first one to spot it. It’s really a pain to do.

Avoiding reversing big functions

We don’t want to reverse these. Having a basic knowledge of what they are for is enough most of the time. When researching, we also stumbled upon ginterface.txt. We can search for either one of these IDs. Lets make a quick assumption: Which number will appear less often in the code? 20, 35 or 42? 20 is an even number that’s dividable by 4, it will appear on various occasions, lets drop this one. 42 is also an even number, but not dividable by 4, there is a chance that it will appear less often, and 35 is uneven, making it, in theory, unlikely to appear.

Considering that 35 is WARNING_WND aka. blue notification and 42 is UDPATE_QUEST_INFO aka. green notification, i consider 42 of the more valuable choice. We can easily trigger a notification ourselves.

Looking for immediate value “42” will return about 700 occurrences. We only need the ones in code, we don’t need arithmetic operations. If you are brave, you can assume that you only need PUSH-instructions at all. Filtering out all but PUSH-instructions will reduce the list to 67 occurrences. That’s a number we can handle. Set a breakpoint on every occurrence and hit that run-button. Remove any triggered breakpoint until the client is up and running. You might need to restart a few times because the client does not like to get interrupted in certain situations. Don’t get trolled because some p-servers use notifications to welcome their players. This might kill your important breakpoint.

Once up and running trigger a notification. You’ll land here:

Obviously, you won’t have these labels. Take these as a gift, you would have reversed these sooner or later. Cross-references to GetGUIObjectByID reveal over 3000 usages, CIFNotify::ShowMessage has only three, one is this one, and the other two are the two missing colors. They look similar to this one.

Reconstructing the signature

Calling convention and basic parameters

Lets figure out the obvious things first: The function has one parameter. Aside from the fact that IDA told us we can assume it from the RETN-instruction. It moves the stack by 4 bytes when returning. Typical for an STDCALL. Second, the function seems to have no return value. EAX is not touched before leaving the function. Also there is a call right before returning, which could also indicate that the return value of ShowMessage is kept. We can’t tell for sure at this point. Third and last, ECX is accessed without being properly initialized. This is pretty much a safe indicator for THISCALL-calling convention on MSVCx86. Taking into account, that we know that the two other functions look pretty much the same, we can also define these:

class UnknownClass {
 void ShowMessage_PINK(void* a1);
 void ShowMessage_GREEN(void* a1);
 void ShowMessage_BLUE(void* a1);
};

Extracting parameter types

We can go in different directions. Depending on your knowledge and experience, all can lead to success. But lets have a look at the function again, first. The first and only parameter should be the message, am i right? That makes sense. But why is the message pushed before the call to GetGUIObjectByID? Doesn’t that mean its a parameter of GetGUIObjectByID? No, it doesn’t. It doesn’t necessarily need to be a parameter even if it gets pushed long before the call it belongs to. It’s the compilers choice to do so. We can also verify that its not a parameter by examining GetGUIObjectByID and its parameter-count. In conclusion: Our parameter is passed to CIFNotify::ShowMessage instead.

We now could analyze ShowMessage to see how the message is used. Depending on your knowledge and experience, this will lead to success. Sometimes its easier to look at how a function is used, which is also true for this case.

But we can still carry on assuming things. We know that GREEN and BLUE notifications come from the textdata, which runs through the localization engine CTextStringManager. Lets assume our call is fed by the return-value of CTextStringManager::GetTranslation. If you are a reader of this blog, you might remember i told you the return value of CTextStringManager::GetTranslation before. I assumed it would be std::string. I was wrong. But don’t worry, that only proves that I’m still a little human inside. std::wstring would have been the correct answer.

With that assumption being set, lets see if we can find any evidence for it. Look through the different cross-references of all three functions.That looks like a proof. You only have to believe me that I’m right with std::wstring this time ;). We can also proof this. These are the guts of CIFNotify::ShowMessage.

Notice the length check of the parameter and the separation for internal and external buffer. A typical thing for std::string and std::wstring. Both can store 16 byte internally, but std::wstring has 2 bytes per character, which makes 8 characters in total.

What about this?

Looking for the origin of the this-pointer can be the most time consuming job in this whole process. Luckily for us, its quite easy in this case. Looking back at the call to ShowMessage_BLUE:

Our relevant register ECX is set right before the call, which is another typical sign for a THISCALL, but, its set to an intermediate value. No pointers, no offsets, nothing. Just an intermediate value.

Putting everything together, we end up with something like this:

class CGInterface {
 void ShowMessage_PINK(std::wstring *msg);
 void ShowMessage_GREEN(std::wstring *msg);
 void ShowMessage_BLUE(std::wstring *msg);
};

Calling a function like this

To be honest, function pointers look very confusing all the time. Don’t be scared. Lets start with a simple function with no return and a wstring-parameter.

void (*)(std::wstring *msg);

Now lets add the calling convention:

void (__thiscall *)(int pthis, std::wstring *msg);

Note that we can not use the name this, because its a reserved keyword. Also note, that i’m not using the correct type for this, for simplicity reasons.

Lets make that a type, so it doesn’t get super confusing:

typedef void (__thiscall * fpShowMessage_t)(int pthis, std::wstring *msg);

We can now use a reinterpret_cast to call it. For this, we need two “new” components: The address of the function and the intermediate value for the this-pointer.

#define g_CGInterface *(reinterpret_cast(0x0110F80C))
#define funPtr 0x0077B580

typedef void (__thiscall * fpShowMessage_t)(int pthis, std::wstring *msg);

// Define our message
std::wstring mymsg("Hello World");
// Call the function
reinterpret_cast(funPtr)(g_CGInterface, &mymsg);

What you should know…

Microsofts C++ library can be a bitch. If you compile on DEBUG, std::wstring will have an debug-allocator embedded, causing the structure to shift, the offsets to be wrong and your game to crash. Also, implementations and structure of STL-types might change over time. Silkroad Online is build on VC80, aka. 2005. 13 years ago. The newer your library and compiler, the more likely is a broken code. Consider reconstructing std::wstring for your own use so you won’t run into problems using newer libraries.

Silkroad Online – A brief insight to GFXFileManager

A few days ago, I completed the first major part of my research to the GFXFileManager. The interface and a reference implementation of the FileManager is complete. Only a few unknown functions are left.

TL;DR; Full Source Code on GitHub: https://github.com/florian0/GFXFileManager

The gameclient uses the GFXFileManager.dll to access the *.pk2-containers. There is no pk2 related code in the client itself. If you ever had a look at the leaked vsro serverfiles, you might have noticed that the server uses it, too, eventho the gameserver uses no pk2-files at all. In fact, the Dll can be used to access files inside pk2-containers and files on the plain hard disk. The gameclient uses the Dll to access SOME files on the hard disk.

Looking at the exports

The Dll exports 3 different functions:

GFXDllCreateObject

extern "C" __declspec(dllexport) int __stdcall  GFXDllCreateObject(int mode, IFileManager** object, int version) {

    if (version != TARGET_VERSION) {
        char message[100];
        sprintf(message, "Dll Version(%x)\nNecessary Version (%x)", version, TARGET_VERSION);
        MessageBox(0, message, "Invalid Version(GFXFileManager.dll)", MB_OK|MB_APPLMODAL);
        return 0;
    }

    if (mode == MODE_ARCHIVE) {
        *object = new CPFileManager();
    } else if (mode == MODE_FILESYSTEM) {
        *object = new CWFileManager();
    } else {
        *object = 0;
    }

    return 0;
}

A function that basically switches between two different objects. CPFileManager, which is used for pk2-containers, and CWFileManager, which is used for plain disk access. Since they are returned by the same function, they must have a common base interface. Well, they have. Its called IFileManager.

GFXDllDeleteObject

extern "C" __declspec(dllexport) void __stdcall GFXDllReleaseObject(IFileManager* object) {
    delete object;
}

The counterpart to the create-function: A generic deleting function.

GFXFMInfo

extern "C" __declspec(dllexport) int __stdcall GFXFMInfo(gfxinfo_t *pInfo, int index) {

    if ( g_gfx_infos[index].in_use )
    {
        memcpy(pInfo, &g_gfx_infos[index], sizeof(gfxinfo_t));
    }
    else
    {
        memset(pInfo, 0, sizeof(gfxinfo_t));
    }

    return 0;
}

And a function that delivers information about an instance of CWFileManager or CPFileManager. It’s most likely for debugging purposes. The FMInfo structure pointer is linked to the to FMInstance. But the IFileManager-interface does not offer a function to access this nor is there are codepath that delivers the index of the FMinfo in the array. As of current state of reversing, there is no code for reusing fields of the array. So after sizeof(gfxinfo_t) open containers (which is 500), something might fail. This should, hopefully, never happen.

 

About IFileManager

This is an abstract base for all classes that implement any kind of container. It provides 55 methods in total, where 4 are not abstract. Overall, the IFileManager interface is designed to act as a wrapper around the WinAPI. Creating and removing directory, reading and writing files, accessing file-times and searching are possible. My reverse engineered class interface is based on the CWFileManager implementation. Most of the functions were simple wrappers around their original WinAPI, so figuring out most the names wasn’t that hard. On the unknown ones, I used the Joymax Pk2 Tool. It helped, especially, on the search-related stuff.

Note, that there are two Open and two Create functions. Either one that simply returned an identifier, which can basically be any number that fits 32 bit, and another, that works basically the same, but fills an object called CJArchiveFm with information.

About CJArchiveFm

This class is not a part of the GFXFileManager. It acts as a buffered reader/writer. The buffersize is, for all binaries I checked, 4096 bytes. If you take a look at the insides, it really just comes down to using the GFXFileManager Read/Write methods for accessing the file. So its nothing  special, just another wrapper around a wrapper. Joymax loves wrappers.

Final thoughts on CWFileManager and CPFileManager

While these two implementations share the same base, they don’t behave the same. Some functions are stubs in CWFileManager, while they are present in CPFileManager. Some functions just behave different, like opening a container using CPFileManager will virtually change directory into the archive, while opening a folder using CWFileManager will exactly do nothing. Also the behaviour of the search-functions is different. CWFileManager only provides the original searchresult-data supplied by FindFirstFile and FindNextFile of the WinAPI. CPFileManager will populate a wrapper-struct around these information (who ever had this idea …).

 

Next steps

Only a few methods are left unknown. Some are known to be named incorrect. First of all, it really requires some in-depth testing. My short tests succeeded, but that doesn’t necessarily mean that it is flawless. Another interesting step might be reversing the CPFileManager-implementation.

Feel free to test and report bugs. Also feel free to fork and implement your own container, if you are able to.

Silkroad Online – Death Penalty Item Drops

A death should hurt – that’s what the game designers at Joymax must have thought. In fact, a death in a game should hurt and throw you back at some point of the game. For Silkroad, a death will not only cost you about 2% of experience, but also make you drop items at a certain ratio.

func-1

The server-side code is really simple. Follow me on this first upsidedown-journey through the Silkroad Online game-server.
Continue reading “Silkroad Online – Death Penalty Item Drops”