Leaderboard
Popular Content
Showing content with the highest reputation on 07/01/23 in all areas
-
Heya, This post is meant to explain the file format of RSM2 for those who are interested and want to play with them. I haven't seen many projects exploring the topic and I've finished digging through the file for GRF Editor. I shared some of the structure pubicly in BrowEdit's Discord almost a year ago, but the fields were still unknown at that point. Also before anyone asks, no I am not making a public converter for RSM2 > RSM1. That's not fully possible anyway. General The structure of a RSM file is quite simple. It's a list of mesh data with transformations applied to them. Each mesh has a transformation matrix, a position, a parent, etc. Then you have the transformation components on the mesh: Offset/Translation RotationAngle RotationAxis Scale And at last, you have the animation components on the mesh: RotationKeyFrame ScaleKeyFrame All the code presented below comes from GRF Editor. Also the structure varies quite a bit even among the 2.2 version and the 2.3 version. I was unable to find any model using versions 2.0 or 2.1. I'd guess they were only used internally...? Who knows. Animation duration changes In previous versions, below 2.2, the AnimationLength field and the frame animation field represented time in milliseconds. So a model such as ps_h_01.rsm has 48000 as a value for AnimationLength, which means the animation lasts for a whole 48 seconds before it resets. The key frames for the transformations work in the same manner. In version 2.2 and above, the AnimationLength field reprensents the total amount of frames in the model. So a model such as reserch_j_01.rsm2 has a value of 300. The keyframes would therefore range between 0 and 300. The duration is given by the new FramesPerSecond field, which is 30 for almost all 2.0 models currently existing. The delay between frames would then be 1000 / FramesPerSecond = 33.33 ms. The duration would be 1000 / FramesPerSecond * AnimationLength = 1000 / 30 * 300 = 10000 ms in our example. Shading Nothing new there, but I thought I'd go over the topic quickly. The ShadeType property is used to calculate the normals. There are three types that have been found in models to this day: 0: none; the normals are all set to (-1, -1, -1). 1: flat; normals are calculated per triangle, with a typical cross product of the 3 vertices. 2: smooth; each face of a mesh belongs to a smooth group, the normal is then calculated by adding the face normal of each connected vertices. In the real world, most models end up using the smooth shading type. The smooth group is a bit confusing at first if you've never heard of it, but some reading on the topic will help you. These are common techniques. Textures In previous versions, below 2.3, the textures were defined at the start of the file. Each mesh then defines a list of indices. So for example, a mesh could define these indices: "2, 5, 0" which means the mesh has 3 textures. Each face of the mesh then has a TextureId property from 0 to 2 in our example. If the face TextureId is 1, it would refer to the second indice previously defined, which is 5. This means that the texture used for this face would be the 5th texture defined at the start of the model. In version 2.3 and above, the textures are defined per mesh instead. There are no longer using texture indices. The TextureId defined for each face refers directly to the texture defined of that particular mesh. So say the TextureId for a face is 1, then the first texture defined on the mesh is the corresponding one. Transformation order In version 2.2 and above, the Scale/Offset/RotationAngle/RotationAxis properties were removed. Instead, it relies on animation frames or the TransformationMatrix. The order looks as such: /// <summary> /// Calculates the MeshMatrix and MeshMatrixSelf for the specified animation frame. /// </summary> /// <param name="animationFrame">The animation frame.</param> public void Calc(int animationFrame) { MeshMatrixSelf = Matrix4.Identity; MeshMatrix = Matrix4.Identity; // Calculate Matrix applied on the mesh itself if (ScaleKeyFrames.Count > 0) { MeshMatrix = Matrix4.Scale(MeshMatrix, GetScale(animationFrame)); } if (RotationKeyFrames.Count > 0) { MeshMatrix = Matrix4.Rotate(MeshMatrix, GetRotationQuaternion(animationFrame)); } else { MeshMatrix = Matrix4.Multiply2(MeshMatrix, new Matrix4(TransformationMatrix)); if (Parent != null) { MeshMatrix = Matrix4.Multiply2(MeshMatrix, new Matrix4(Parent.TransformationMatrix).Invert()); } } MeshMatrixSelf = new Matrix4(MeshMatrix); Vertex position; // Calculate the position of the mesh from its parent if (PosKeyFrames.Count > 0) { position = GetPosition(animationFrame); } else { if (Parent != null) { position = Position - Parent.Position; position = Matrix4.Multiply2(new Matrix4(Parent.TransformationMatrix).Invert(), position); } else { position = Position; } } MeshMatrixSelf.Offset = position; // Apply parent transformations Mesh mesh = this; while (mesh.Parent != null) { mesh = mesh.Parent; MeshMatrixSelf = Matrix4.Multiply2(MeshMatrixSelf, mesh.MeshMatrix); } // Set the final position relative to the parent's position if (Parent != null) { MeshMatrixSelf.Offset += Parent.MeshMatrixSelf.Offset; } // Calculate children foreach (var child in Children) { child.Calc(animationFrame); } } The original vertices are then multiplied by MeshMatrixSelf for their final positions. MeshMatrix is the resulting transformation matrix of a particular mesh only, without taking into account its parents matrixes or the mesh position. The MeshMatrixSelf is the final transformation matrix that will be applied to the vertices. Contrary to previous versions, the TransformationMatrix is applied all the way to the children. The matrix invert function may not be available in all common librairies, so here is the implementation used: public Matrix4 Invert() { if (this.IsDistinguishedIdentity) return this; if (this.IsAffine) return this.NormalizedAffineInvert(); float num1 = this[2] * this[7] - this[6] * this[3]; float num2 = this[2] * this[11] - this[10] * this[3]; float num3 = this[2] * this[15] - this[14] * this[3]; float num4 = this[6] * this[11] - this[10] * this[7]; float num5 = this[6] * this[15] - this[14] * this[7]; float num6 = this[10] * this[15] - this[14] * this[11]; float num7 = this[5] * num2 - this[9] * num1 - this[1] * num4; float num8 = this[1] * num5 - this[5] * num3 + this[13] * num1; float num9 = this[9] * num3 - this[13] * num2 - this[1] * num6; float num10 = this[5] * num6 - this[9] * num5 + this[13] * num4; float num11 = this[12] * num7 + this[8] * num8 + this[4] * num9 + this[0] * num10; if (IsZero(num11)) return false; float num12 = this[0] * num4 - this[4] * num2 + this[8] * num1; float num13 = this[4] * num3 - this[12] * num1 - this[0] * num5; float num14 = this[0] * num6 - this[8] * num3 + this[12] * num2; float num15 = this[8] * num5 - this[12] * num4 - this[4] * num6; float num16 = this[0] * this[5] - this[4] * this[1]; float num17 = this[0] * this[9] - this[8] * this[1]; float num18 = this[0] * this[13] - this[12] * this[1]; float num19 = this[4] * this[9] - this[8] * this[5]; float num20 = this[4] * this[13] - this[12] * this[5]; float num21 = this[8] * this[13] - this[12] * this[9]; float num22 = this[2] * num19 - this[6] * num17 + this[10] * num16; float num23 = this[6] * num18 - this[14] * num16 - this[2] * num20; float num24 = this[2] * num21 - this[10] * num18 + this[14] * num17; float num25 = this[10] * num20 - this[14] * num19 - this[6] * num21; float num26 = this[7] * num17 - this[11] * num16 - this[3] * num19; float num27 = this[3] * num20 - this[7] * num18 + this[15] * num16; float num28 = this[11] * num18 - this[15] * num17 - this[3] * num21; float num29 = this[7] * num21 - this[11] * num20 + this[15] * num19; float num30 = 1.0f / num11; this[0] = num10 * num30; this[1] = num9 * num30; this[2] = num8 * num30; this[3] = num7 * num30; this[4] = num15 * num30; this[5] = num14 * num30; this[6] = num13 * num30; this[7] = num12 * num30; this[8] = num29 * num30; this[9] = num28 * num30; this[10] = num27 * num30; this[11] = num26 * num30; this[12] = num25 * num30; this[13] = num24 * num30; this[14] = num23 * num30; this[15] = num22 * num30; return this; } New transformation animations TranslationKeyFrames In version 2.2 and above, PosKeyFrames are added. If you've seen the previous formats, you may be confused by this. I've seen PosKeyFrames in many implementations, but version 1.6 adds ScaleKeyFrames, not TranslationKeyFrames. The name is self-explanatory: it translates the mesh. TextureKeyFrames In version 2.3 and above, TextureKeyFrames are added. Similar to other transformations, they are defined as: struct TextureKeyFrame { public int Frame; public float Offset; } The TextureKeyFrames target a specific texture ID from the mesh and have different animation types. The Offset affects the UV offsets of the textures. The animation types are: 0: Texture translation on the X axis. The texture is tiled. 1: Texture translation on the Y axis. The texture is tiled. 2: Texture multiplication on the X axis. The texture is tiled. 3: Texture multiplication on the Y axis. The texture is tiled. 4: Texture rotation around (0, 0). The texture is not tiled. Main mesh In previous versions, below 2.2, there could only be one root mesh. This is no longer the case with newer versions. Code And those were all the changes! Here is a full description of the structure (which is again based on GRF Editor). # # RSM structure # private Rsm(IBinaryReader reader) { int count; // The magic of RMS files is always GRSM Magic = reader.StringANSI(4); MajorVersion = reader.Byte(); MinorVersion = reader.Byte(); // Simply converting the version to a more readable format Version = FormatConverters.DoubleConverter(MajorVersion + "." + MinorVersion); // See "Animation duration changes" above for more information. AnimationLength = reader.Int32(); ShadeType = reader.Int32(); Alpha = 0xFF; // Apparently this is the alpha value of the mesh... but it has no impact in-game, so... if (Version >= 1.4) { Alpha = reader.Byte(); } if (Version >= 2.3) { FrameRatePerSecond = reader.Float(); count = reader.Int32(); // In the new format, strings are now written with their length as an integer, then the string. In previous versions, strings used to be 40 in length with a null-terminator. // The syntax below may be a bit confusing at first. // reader.Int32() reads the length of the string. // reader.String(int) reads a string with the specific length. for (int i = 0; i < count; i++) { MainMeshNames.Add(reader.String(reader.Int32())); } count = reader.Int32(); } else if (Version >= 2.2) { FrameRatePerSecond = reader.Float(); int numberOfTextures = reader.Int32(); for (int i = 0; i < numberOfTextures; i++) { _textures.Add(reader.String(reader.Int32())); } count = reader.Int32(); for (int i = 0; i < count; i++) { MainMeshNames.Add(reader.String(reader.Int32())); } count = reader.Int32(); } else { // Still unknown, always appears to be 0 though. Reserved = reader.Bytes(16); count = reader.Int32(); for (int i = 0; i < count; i++) { _textures.Add(reader.String(40, '\0')); } MainMeshNames.Add(reader.String(40, '\0')); count = reader.Int32(); } // The Mesh structure is defined below for (int i = 0; i < count; i++) { _meshes.Add(new Mesh(reader, Version)); } // The rest of the structure is a bit sketchy. While this is apparently what it should be (some models do indeed have those), they have absolutely no impact in-game and can be safely ignored when rendering the model. if (Version < 1.6) { count = reader.Int32(); for (int i = 0; i < count; i++) { _scaleKeyFrames.Add(new ScaleKeyFrame { Frame = reader.Int32(), Sx = reader.Float(), Sy = reader.Float(), Sz = reader.Float(), Data = reader.Float() }); } } count = reader.Int32(); for (int i = 0; i < count; i++) { VolumeBoxes.Add(new VolumeBox() { Size = new Vertex(reader.Float(), reader.Float(), reader.Float()), Position = new Vertex(reader.Float(), reader.Float(), reader.Float()), Rotation = new Vertex(reader.Float(), reader.Float(), reader.Float()), Flag = version >= 1.3 ? reader.Int32() : 0, }); } } # # Mesh structure # public Mesh(IBinaryReader reader, double version) { int count; if (version >= 2.2) { Name = reader.String(reader.Int32()); ParentName = reader.String(reader.Int32()); } else { Name = reader.String(40, '\0'); ParentName = reader.String(40, '\0'); } if (version >= 2.3) { count = reader.Int32(); for (int i = 0; i < count; i++) { Textures.Add(reader.String(reader.Int32())); } // This is more so for backward compatibility than anything. The texture indices now refer to the texture list of the mesh directly. for (int i = 0; i < count; i++) { _textureIndexes.Add(i); } } else { count = reader.Int32(); for (int i = 0; i < count; i++) { _textureIndexes.Add(reader.Int32()); } } // The TransformationMatrix is 3x3 instead of 4x4 like everything else in the universe. TransformationMatrix = new Matrix3( reader.Float(), reader.Float(), reader.Float(), reader.Float(), reader.Float(), reader.Float(), reader.Float(), reader.Float(), reader.Float()); if (version >= 2.2) { // In 2.2, the transformations are already applied to the mesh, or calculated from the animation key frames. None of these properties are used anymore. Offset = new Vertex(0, 0, 0); Position = new Vertex(reader); RotationAngle = 0; RotationAxis = new Vertex(0, 0, 0); Scale = new Vertex(1, 1, 1); } else { // The Offset is the translation vector for the mesh. translated > scaled > rotated >TransformationMatrix. Offset = new Vertex(reader.Float(), reader.Float(), reader.Float()); // Position is the distance between the mesh and its parent. Position = new Vertex(reader.Float(), reader.Float(), reader.Float()); RotationAngle = reader.Float(); RotationAxis = new Vertex(reader.Float(), reader.Float(), reader.Float()); Scale = new Vertex(reader.Float(), reader.Float(), reader.Float()); } count = reader.Int32(); for (int i = 0; i < count; i++) { _vertices.Add(new Vertex(reader.Float(), reader.Float(), reader.Float())); } count = reader.Int32(); for (int i = 0; i < count; i++) { _tvertices.Add(new TextureVertex { Color = version >= 1.2 ? reader.UInt32() : 0xFFFFFFFF, U = reader.Float(), V = reader.Float() }); } count = reader.Int32(); // A face has changed a little in the new version. The SmoothGroup isn't only bound to the face itself, but can be bound to the vertex itself instead. for (int i = 0; i < count; i++) { Face face = new Face(); int len = -1; if (version >= 2.2) { len = reader.Int32(); } face.VertexIds = reader.ArrayUInt16(3); face.TextureVertexIds = reader.ArrayUInt16(3); face.TextureId = reader.UInt16(); face.Padding = reader.UInt16(); face.TwoSide = reader.Int32(); if (version >= 1.2) { face.SmoothGroup[0] = face.SmoothGroup[1] = face.SmoothGroup[2] = reader.Int32(); if (len > 24) { // It is unsure if this smooth group is applied to [2] or not if the length is 28. Hard to confirm. face.SmoothGroup[1] = reader.Int32(); } if (len > 28) { face.SmoothGroup[2] = reader.Int32(); } } _faces.Add(face); } // This was weirdly predicted to be in model version 1.6... which never existed? Either way, it is safe to set it as >= 1.6 if (version >= 1.6) { count = reader.Int32(); for (int i = 0; i < count; i++) { _scaleKeyFrames.Add(new ScaleKeyFrame { Frame = reader.Int32(), Sx = reader.Float(), Sy = reader.Float(), Sz = reader.Float(), Data = reader.Float() // Useless, has in impact in-game }); } } count = reader.Int32(); for (int i = 0; i < count; i++) { _rotFrames.Add(new RotKeyFrame { Frame = reader.Int32(), // Qx, Qy, Qz, Qw Quaternion = new TkQuaternion(reader.Float(), reader.Float(), reader.Float(), reader.Float()) }); } if (version >= 2.2) { count = reader.Int32(); for (int i = 0; i < count; i++) { _posKeyFrames.Add(new PosKeyFrame { Frame = reader.Int32(), X = reader.Float(), Y = reader.Float(), Z = reader.Float(), Data = reader.Int32() // Useless, has in impact in-game }); } } // Texture animations, look at "Textures" above for more information if (version >= 2.3) { count = reader.Int32(); for (int i = 0; i < count; i++) { int textureId = reader.Int32(); int amountTextureAnimations = reader.Int32(); for (int j = 0; j < amountTextureAnimations; j++) { int type = reader.Int32(); int amountFrames = reader.Int32(); for (int k = 0; k < amountFrames; k++) { _textureKeyFrameGroup.AddTextureKeyFrame(textureId, type, new TextureKeyFrame { Frame = reader.Int32(), Offset = reader.Float() }); } } } } } I'm also sharing the program I used to test the RSM2 files. It's a bit messy, but it does the job and might help someone. This testing program no longer has any purpose to me as it's been merged into GRF Editor already. https://github.com/Tokeiburu/RSM2/tree/master/Rsm2 The provided model is the following (it contains all the new features of RSM2): The chain on the right as well as the lights use these new texture animations. The red ball uses the translation key frames. This test project can read any RSM or RSM2 file as well as save them (you can edit RSM/RSM2 models via source). Changing the header version to change the output file will cause issues depending on which version you go from and to. With that said, have fun...! One day I'll make GRF Editor sources public again, one day.1 point
-
View File Market Clone (No SRC modification needed) This is exactly the same concept as the original file by Annieruru/Dastgir which is found on this post : @marketclone - creates a clone of your self as if you are making a chatroom. @marketkill - removes the clone you spawned. Yes, there is no need for SRC modification. This is purely done script wise but this will only work for latest server files. Any server files later than October 2, 2022 shouldn't have any troubles running this script. Otherwise, you are required to apply this commit: https://github.com/rathena/rathena/commit/9c2576f47ac12f54738bc714b858fde3a9d6315b to work. Any issues regarding the script, leave me a message or tag me on the support page of this script. If there is any good suggestions for improvements I do wont give support if compatibility is the issue. Compatibility is your responsibility. Use at your own risk. Submitter pajodex Submitted 06/22/23 Category Utilities Video Content Author pajodex, Annieruru, Dastgir, Napster1 point
-
Hello people, its been a long time huh? Well I've been away from RO community for some years already, but recently I've been requested to work on some hairstyles, the idea was to rework some of the janky still beloved custom hairstyles, however things happened and this project will not go further, so I'm here to share what was done so far. Keep in mind that those are the spr and act files with the palette adapted for a specific server use, so I do not assure that it will fully work on your server, I'll leave the pngs public on my DeviantArt soon, so you can grab the heads and pack yourself if you know how to use the ACT Edtior. Also I want to note that the heads are not all mine since I've worked to fix the work of other authors, you don't need my permission to use it on your server, but as always, don't put a paywall behind the work posted here and don't try to sell or claim the job done yours. Now lets go for the screenshots and files. Here a sample of every hairstyle: Some Comparisons (New on the top, Old on the bottom): (here the new is on the bottom and the old on the top) Anf of course the DOWNLOAD: data.zip < This is a Data folder with the hairs inside, remember to rename the number to the macthing hair on your server or you'll replace the wrong ones! The names on the folder does not match with the 84 pack, so even if you use it, make sure to give the proper ids. And to finish, thank you for passing by and checking out my work, I barely visit this forum anymore so keep in mind that I'll not give support by this topic or DMs here. I also got permission to make these files public so please lets avoid the drama. See ya!1 point
-
Been a bit of a slow time on Ragnarok lately, been super busy with work. Making a few small polish updates here and there, like changes to minimaps and such. The biggest change is that I overhauled the game's achievement system. To be honest, I'm not really that convinced the game needs an achievement system, but if it's going to be in the game, it might as well at least be functional. In its current state it was clearly an afterthought, with like 80% of the achievements being broken and it not giving you any rewards even for the ones you can complete. So I pretty much rewrote the entire thing from scratch. For starters, there's actually a glitch in current Rathena that makes a lot of the objectives impossible to complete, such as "Human's Greed Has No End", which just requires you to fail to refine once. Obviously this should be easy to get, but you'll probably find that you don't have it. This is because of an error in achievement.cpp: Find this block in "achievement_update_objectives" if (!ad->condition) return false; This block tells it to tell the game to never approve an achievement that has no condition. Obviously this is wrong, as many achievements have no conditions. Outright remove that block. Amend the block below to be this to avoid a null pointer exception: if (ad->condition && !achievement_check_condition(ad->condition, sd, current_count)) // Parameters weren't met return false; Achievements with no conditions now work, as Hollgrehenn will be happy to demonstrate: Some of the other achievements require you to find various Treasure Boxes in the field maps. These work, but in PRE-RE, the script is not enabled by default. The script that contains them is npc/re/other/achievements.txt. This now works and you can get the achievements, but they're quite obnoxious to find, because the treasure chests only appear if you're within a 5 cell range of them. This is the last two numbers on the npc line. I increased this to 30. Most of them are still hidden pretty well, but I moved some of them that were either totally out in the open, or hidden so obnoxiously well that you'd never see them without using a guide. Now, many of the odd nooks and crannies on the various maps have a purpose, like this weird spot in the map below Prontera: This is a good start, but the system still had a long way to go. Of the achievements that remain, many are unobtainable in PRE-RE (ie, reach level 100), are tied to quests that aren't in the game, or are just obnoxious (like defeating Celine Kimi 25 times). So I basically purged most of the rest of them and made my own. The achievements are stored in both the achievement_db.yml in the database folder and achievement_list.lub in the System folder, which need to match. Here's an example of one of my new achievements in case you ever wanted to make your own. This is the entry in achievement_db.yml - ID: 128010 Group: "AG_BATTLE" Name: "Mother's Love" Target: - Id: 0 MobID: 1147 Count: 1 Score: 10 This is quite straightforward. AG_BATTLE achievements simply require the named mob to be killed COUNT times. Since the count here is 1 and the MobID is 1147, this requires killing Maya once. And here's achievement_list.lub, which contains the front-end information for that achievement: [128010] = { UI_Type = 1, group = "BATTLE", major = 5, minor = 0, title = "Mother's Love", content = { summary = "Defeat Maya", details = "Defeat the MVP in Ant Hell" }, resource = { [1] = { text = "Eliminate Maya", count = 1 } }, reward = {}, score = 10 }, The structure here is not too hard to understand. The major and minor tell the game what group it is in, this one is in the "MVP" category and the "Midgard" subcategory. Here's the achievement in-game: I've changed some of the labels in this window (for example, the "Past" category is new), this is in the msgstringtable inside the grf file. Almost all of the MVPs in the game now have an achievement tied to them, which only requires you to beat them once. They are almost all dumb puns or references, like this one for Dracula. Actually, this isn't a very good screenshot since his death animation already ended, but soloing him with this character was a pain so I'm going to post it anyway. Finally, there needed to be some reward for actually doing these achievements. A few achievements had buffs or titles associated with them (many of these were commented out, but that's easy enough to re-enable), but for the ones that are tied to your overall achievement level, I've put a few decently worthwhile things in there. The first few levels are super easy to get and only reward very basic things like OBBs, but if you get to higher levels you can start getting a few nice things like OCAs and some boxes that contain a few of the premium refining materials, which are otherwise very hard to get on my server. It's nothing game-changing, but it's something. Incidentally, I also changed the achievement level thresholds because they were very weird initially, this is also in achievement.cpp. With that, I think this system is now decently workable and fun. I still kinda don't like the fact that some achievements are mutually exclusive so you can't get them all (ie, there are some tied to changing to certain jobs, which you can only do some of on any given character), but there's so many achievements that I doubt anyone would be crazy enough to try to get them all anyway (you hit achievement level 20 long before you run out of achievements to get). Speaking of things that are mutually exclusive, I also added something to the Summoner class that I think is interesting: Previously, the Doram had the "Soul Attack" skill, which causes your normal attack to become a ranged attack skill. I felt this was too much of a no-brainer for one skill point, so I turned it into a 5-point skill, though it then felt a little weak. I had also been toying with the idea of the Doram getting some new buffs that aren't tied to their skill trees, though I didn't want to significantly increase the power of the class since it was already pretty good, so I finally came up with this. In addition to Soul Attack (which is called Spirit Attack here), the Summoner now has two other buffs in Spirit Shield and Spirit Focus. However, you can only take one of them, once you know one of them, the others are no longer available (this required quite a bit of custom code as nothing in the game currently works this way). Each provides a pretty decent benefit at level 5, but you have to choose. Spirit Shield grants VIT Def and at level 5 it reduces incoming damage by 50% if your HP is full. This is nice for preventing one shots, but it's even more powerful if combined with Tuna Party, which gives you barrier HP. Yes, the damaging halving effect also halves damage done to the barrier, which is kind of bananas. To stop this from being ludicrously OP, the barrier has a bit less health than it had originally, costs more SP, and you can't recast it if you already have it, it has to expire or be broken first. Spirit Focus reduces cast time and gives a 50% chance to maintain concentration if being hit while spellcasting at level 5. Of the three, the level 5 benefit is probably the weakest here (there's plenty of ways to get uninterruptable cast that works all the time), but the reduced cast time effect is very powerful. Spirit Attack grants mastery attack (which is better on my server than on most others because I use Renewal's mastery attack formula where it's added at the beginning of the damage calculation), and at level 5, it also makes your normal attack ranged. However, it has another bonus - it also adds +2 range to all ranged attacks, similar to Vulture's Eye, except that it affects EVERYTHING. Ranged attacks, heals, buffs, skills you get via cards or equipment, you name it, if it has over 4 range, it gains +2 range. I think there's a lot of interesting applications for this. But of course, whichever one you choose, you have to give up the others. Which to take? I think it's an interesting dilemma that helps make the class feel unique.1 point
-
Afternoon everyone! Many moons ago we migrated our wiki content to Github in an effort to alleviate resources on our VM, increase our data's stability and in-general, significantly reduce the number of things that could go wrong. That was the plan. The plan worked well enough on all three counts above, however, Github Wiki's have this wonderful knack of not doing what they're supposed to when you need them to be helpful. For example, users needing special permissions to push new content to it (until very recently this has been "maintainer" access only), which means everyone has to rely on rA Staff to update the Wiki, which we evidently haven't had much time for. Now there's a new kid on the block; User-Guides! Guides from the Wiki will slowly be migrated to our new User Guides site (which you can help with, if you want to), which is a HTML website hosted using Github Pages, automatically generated from Markdown files that are stored in a Github Repository. Neat, huh! View the site at https://rathena.github.io/user-guides/ Because this new guide platform is built on a git repo, it means that anyone can contribute to it by opening a Pull Request. I've written some guidance here: https://github.com/rathena/user-guides/blob/master/CONTRIBUTING.md "What about all those guides on the forums" I hear you ask. Let's face it, 20% of them don't make any sense, 30% of them are inaccurate, 90% of them are outdated, 100% of them are developed with either bias towards specific software tools or just badly written due to language barriers or laziness. Far too often are links to out-of-date guides re-posted on the forums and in Discord, and we can't keep them updated because they're forum posts so only Mods can edit them (which is not what they're here for). From now on, well-written guides are to be posted to the User Guides repo. Guides posted on these forums will result in a message being sent to the author stating why we're going to hide the thread and then give a link to this post. What can you NOT post to User Guides? rAthena is a software emulation project. As such, guides pertaining to 3rd Party Tools or software will not be approved. This includes "How-To's" regarding the client, which is not actually within the scope of the rAthena project. We don't want to be sued ?, thanks. We have a new channel in Discord called #user-guides for discussions related to this area of the project. We would be very happy to see you helping our effort to migrate and write good, informative guides for our community to use and keep updated for years to come! Next we'll be working on doing something very similar to our source's /doc/ directory! Akky~1 point
-
Version 20200123
1260 downloads
Introduction: A skin packs that I found in TWRO forum, made by a user named crystalchan. This skin package consists of ~10 recoloured skin, and 1 YoRha (Nier Automata) theme skin. Unfortunately, the skin pack are all translated in chinese language, most likely are usable only to those who can read chinese. Preview: Full Preview: https://imgur.com/a/I8w9FPO How to use: Extract the folder and put them in the /Skin/ folder or add it into the GRF Disclaimer: You're free to share with everyone but do not monetize the contents in any way! These materials are meant for study or research purpose. Use these at your own risk, and always give credit to original author. Thank you.Free1 point