Jump to content
The forums will be going offline for an extended maintenance period at 1400hrs GMT on 19th June 2025. The number of hours for this downtime is intentionally not advertised due to the nature of these upgrades. ×

Tokei

Members
  • Posts

    700
  • Joined

  • Last visited

  • Days Won

    108

Posts posted by Tokei

  1. On 5/26/2025 at 8:23 PM, Zetsumei said:

    UP 🥲

    Well, your table file offset appears to be invalid. You'd have to either upload the GRF or show me the file header (first 46 bytes of the GRF). Does... this GRF even load at all on a normal client? It's using the 0x300 format, so I can only assume it's a GRF straight up from kRO/jRO?

  2. Rsm Editor


    Rsm Editor is a tool to edit RSM1 and RSM2 model files from Ragnarok Online.

    How to install

    • Download the zip archive provided from the download link on this page.
    • Install the application with Rsm Editor Installer.exe; if you are missing a .NET Framework you will be prompted to download it (the tool requires .NET 4.0).
    • Once you are done, you can start the program from the link on your desktop.
    • If you are opening the software for the first time, you'll be prompted with a setting resource window.
      • You can either add a data folder or a list or GRFs from this window. This will be used for loading textures on your models.
        image.png.48bbb9b6381b06f1a5dd153b5e6d18e8.png

    Key features

    • Can edit both RSM1 and RSM2 file formats. When saving, you can also downgrade or upgrade the model version.
      • The tool uses a different technique for version conversion which should give very accurate results. Position, scale and texture keyframes cannot be converted.
    • Undo and redo for any operation.
    • Quaternions are translated into Euler angles using the YXZ order.
    • Merging different models can be done by right-clicking a mesh node or by drag and dropping another mesh directly.
    • Easy texture modification for each mesh.
    • Enable to set two-faced polygons for the entire model (to avoid invinsible faces in-game).
    • Files can be associated with the tool through Windows (there will not be a setting for this within the software).
    • Backups are created while you are editing the mesh. You can view the list via Debug > Backups manager...
    • Automatically fixes mesh names and root node upon saving.
    • There is no conversion to fbx and vice versa at the moment.

    Useful shortcuts

    • All shortcuts can be modified via File > Settings > Shortcuts.
    • Pressing Enter on a mesh will center the camera around it.
    • You can quickly duplicate a mesh with Ctrl-C/Ctrl-V.
    • You can hold the Shift key to snap the Gizmo when applying a transformation. You can also hold Shift when selecting a frame.
    • You can select a mesh directly by holding Alt while clicking on it in the viewport.
    • You can enable the wireframe view by pressing F8 or View > Wireframe view. This is useful when positioning meshes.

    Modifying transform origin

    • The origin position is used for the rotation and scale transformations.
    • To modify, press F1 or View > Show origin, then move the white gizmo.
      image.png.a210bc66470e7e873cb4d3c4cac4a5b2.png

    Modifying textures or normals

    • If you click on the model rather than a mesh, you'll have a few more options.
      image.png.49879ddcbedf6fc24d1da396faf93c30.png
    • Smooth normals is the recommended method.
    • The list of textures can be edited directly. The textures shown in this node are for all meshes together. If you edit the texture locally from your data folder, you can reload the model textures by using Ctrl-R or Transform > Reload model.

    Animation transform

    • If you've already made an animation for a mesh and you want to apply a rotation (or translation) to all keyframes, you can do so by using the animation transform feature. Press F7 or Animation > Animation transform. This mode applies the transformation to all keyframes at the same time.
      image.png.483f012d315ce61f7bfebdc9fc3098a9.png

    Keyframes edit

    • You can select multiple keyframes by holding down Shift+LeftMouseButton in the timeline view.
      image.png.a0f99b5ae1e58ca153fb4ed14d43431b.png
    • You can unselect specific keys by pressing Ctrl+LeftMouseButton.
    • You can move the selected frames by holding down LeftMouseButton.
    • You can copy and paste keys with Ctrl-C/Ctrl-V. You can select all keys with Ctrl-A or deselect all with Ctrl-D.

    Gizmo orientation

    • By pressing F4 or View > Change gizmo orientation, you can change between global or local orientation.
    • Local orientation sets the gizmo to the current mesh transformation, while global ignores all mesh transformations.
      image.png.275e3c9d84b8bdbc382199dc923f824f.png
    • Each mesh has local transformations and global transformations (this is unrelated to the gizmo orientation). The difference is that local transformations do not apply to children (not entirely true for RSM2 though). Keyframe transformations always apply to children.

    • Submitter
    • Submitted
      04/28/25
    • Category
    • Video
    • Content Author
      Tokeiburu

     

  3. 6 hours ago, primm said:

    Hi. I am trying to make a greymap for nif_dun01. I already removed the environment objects by editing a lot of texture files, but the height for those up the stairs are always retained, making me look like I am floating. Any reason this is happening?

    Hello, if you selected the "Make the ground flat", then the gat tiles will also be set to 0. You'd have to show me what settings you used; though I would guess you probably have another gat file in your RO folder that has a higher priority than your gray map one. I did a quick test on my end and the nif_dun01 map worked fine.

    image.png.aae0dd30f88a953cc1b087602d895f0b.png

  4. Hello,

    The easiest way to add new maps is the following:

    • Make sure to update GRF Editor to its latest version (currently on 1.8.9.7, you can use the rAthena page and follow the mediafire link or here: https://www.mediafire.com/file/aflylbhblrzpz0h/ )
    • Find the new maps in your GRF and then extract the RSW, GND and GAT files.
      • Preferably, use an official GRF since your server's GRF may be encrypted.
      • You can easily test that out by trying to preview the map in GRF Editor. If it doesn't want to load or it throws errors, then the map is encrypted.
    • Open the flat maps maker tool and open the input maps folder by clicking the green arrow:
      • image.png.8997509f1e511f6da6dea9535cde1d31.png
    • Put the map files you previously extracted in that folder, make sure "Use currently opened GRF" is not checked.
    • If you're using the latest GRF Editor version, you'll get a preview on the right side. You can customize the output rather easily in the options, that part is entirely up to you.
    • Optional. The textures will overwrite the original ones; if that's a concern for you, you can set a unique set of textures for your own map in Options > Maps unique ID (set it to "mine_").
    • Now generate the gray maps, and you'll get a maps.grf file.
    • You can either add an extra entry in your data.ini file with your new GRF name, or you can merge the two gray map GRFs with the Edit > Merge option.
    • If any error shows up, it will be displayed in the console in the "Generate maps" tab.
      • image.thumb.png.192229e115629c05deb7e460eed527dd.png

    Now, the reason for your crash is a bit vague. Here are a few possible reasons:

    • You used an old version of GRF Editor or your maps were too recent (kRO released a new map version a while back). Either way, updating GRF Editor will fix that issue.
    • Your GRF priority is wrong in your data.ini. Your maps.grf should have one of the highest priority, so something like:
      [Data]
      0=gepard.grf
      1=maps.grf
      2=yourserver.grf
      3=data.grf
    • You may not have merged the GRF correctly? Not sure how that would crash you though...
    • I can't think of another reason. If the issue persists, can you tell me the map name? I'll just test it on my end to make sure.
  5. This was resolved through PMs a while back; here was the final script for anyone interested:

    // Act Editor Script - v1.0.5.1
    // By Tokeiburu
    GrfColor textForeground = "0xffffff";
    var fontType = "Minecraftia";
    var fontSize = 14;
    var fontBold = true;
    var dataGrfName = @"C:\mobWithNames\data.grf";
    var newGrfName = @"C:\mobWithNames\mobsWithName.grf";
    var mobDbPath = @"C:\mobWithNames\mob_db.yml";
    var attrFixDbPath = @"C:\mobWithNames\attr_fix.yml";
    var jobnamePath = @"C:\mobWithNames\jobname.lub";
    var npcIdentityPath = @"C:\mobWithNames\npcidentity.lub";
    var oldEncoding = EncodingService.DisplayEncoding;
    var displayMobNames = false;
    var maxElementShown = 3;
    var showFirstElementDamage = true;
    //var elementTextColors = false;
    
    // Fixed settings
    var mobFolder = @"data\sprite\¸ó½ºÅÍ\";
    //jobnamePath = @"C:\Gravity Ragnarok - Copy\data\luafiles514\lua files\datainfo\jobname.lub";
    //npcIdentityPath = @"C:\Gravity Ragnarok - Copy\data\luafiles514\lua files\datainfo\npcidentity.lub";
    //dataGrfName = @"C:\Gravity Ragnarok - Copy\data.grf";
    //newGrfName = @"C:\test\mobsWithName.grf";
    //mobDbPath = @"C:\Users\Tokei\Desktop\SVN\rathena4\db\re\mob_db.yml";
    //attrFixDbPath = @"C:\Users\Tokei\Desktop\SVN\rathena4\db\re\attr_fix.yml";
    
    var jobname = new JobnameLubData(jobnamePath, npcIdentityPath);
    
    int[,,] attr_fix = new int[4, 10, 10];
    StringBuilder b = new StringBuilder();
    var ele2id = new Dictionary<string, int>();
    ele2id["Neutral"] = 0;
    ele2id["Water"] = 1;
    ele2id["Earth"] = 2;
    ele2id["Fire"] = 3;
    ele2id["Wind"] = 4;
    ele2id["Poison"] = 5;
    ele2id["Holy"] = 6;
    ele2id["Dark"] = 7;
    ele2id["Ghost"] = 8;
    ele2id["Undead"] = 9;
    
    var id2ele = new Dictionary<int, string>();
    foreach (var entry in ele2id)
    	id2ele[entry.Value] = entry.Key;
    
    try {
    	EncodingService.DisplayEncoding = EncodingService.ANSI;
    	int ival;
    	string[] values;
    	var mobId2ServerName = new Dictionary<int, string>();
    	var mobId2ServerElement = new Dictionary<int, int>();
    	var mobId2ServerElementLevel = new Dictionary<int, int>();
    	
    	{
    		// Load the attr_fix file
    		var parser = new YamlParser(attrFixDbPath);
    		var body = parser.Output["Body"];
    		
    		foreach (var entry in body) {
    			int level = Int32.Parse(entry["Level"]) - 1;
    			
    			foreach (var ele2id_entry in ele2id) {
    				var element_name = ele2id_entry.Key;
    				var element_id = ele2id_entry.Value;
    				
    				var def_elements = entry[element_name];
    			
    				foreach (ParserKeyValue def_ele in def_elements) {
    					attr_fix[level, element_id, ele2id[def_ele.Key]] = Int32.Parse(def_ele.Value);
    				}
    			}
    		}
    	}
    	
    	{
    		// Load the mob_db file
    		var parser = new YamlParser(mobDbPath);
    		var body = parser.Output["Body"];
    		
    		foreach (var entry in body) {
    			int mobId = Int32.Parse(entry["Id"]);
    			mobId2ServerName[mobId] = entry["Name"];
    			
    			int eleId = 0;
    			int eleLevel = 0;
    			
    			if (entry["Element"] != null) {
    				eleId = ele2id[entry["Element"]];
    			}
    			
    			if (entry["ElementLevel"] != null) {
    				eleLevel = Int32.Parse(entry["ElementLevel"]) - 1;
    			}
    			
    			mobId2ServerElement[mobId] = eleId;
    			mobId2ServerElementLevel[mobId] = eleLevel;
    		}
    	}
    	
    	Func<Act, string, int, Act> addNameAbove = (actI, display, offset) => {
    		var pixels = new byte[256 * 20 * 4];
    		BitmapSource bitmapSource = BitmapSource.Create(256, 20, 96, 96, PixelFormats.Bgra32, null, pixels, 256 * 4);
    		var visual = new DrawingVisual();
    		using (DrawingContext drawingContext = visual.RenderOpen()) {
    			var ft = new FormattedText(display, CultureInfo.InvariantCulture, FlowDirection.LeftToRight,
    				new Typeface(fontType), fontSize, new SolidColorBrush(textForeground.ToColor()));
    			
    			if (fontBold)
    				ft.SetFontWeight(FontWeights.Bold);
    			drawingContext.DrawImage(bitmapSource, new Rect(0, 0, 256, 20));
    			drawingContext.DrawText(ft, new Point(2, 2));
    		}
     
    		var image = new DrawingImage(visual.Drawing);
    		var im = new Image { Source = image };
    		im.Measure(new Size(image.Width, image.Height));
    		im.Arrange(new Rect(0.0, 0.0, image.Width, image.Height));
    		im.UpdateLayout();
     
    		var grfImage = GrfToWpfBridge.Imaging.ConvertToBitmapSource(im);
    		var transparent = new GrfColor(0, 0, 0, 0);
     
    		for (int i = 0; i < grfImage.NumberOfPixels; i++) {
    			Buffer.BlockCopy((grfImage.Pixels[4 * i + 3] > 0x80 ? textForeground : transparent).ToBgraBytes(), 0, grfImage.Pixels, 4 * i, 4);
    		}
     
    		grfImage.Convert(GrfImageType.Indexed8); grfImage.Trim(); grfImage.Margin(2);
    		Buffer.BlockCopy(GrfColor.Black.ToBgraBytes(), 0, grfImage.Palette, 2 * 4, 4);
     
    		for (int y = 1; y < grfImage.Height - 1; y++) {
    			for (int x = 1; x < grfImage.Width - 1; x++) {
    				if (grfImage.Pixels[y * grfImage.Width + x] == 1) continue;
     
    				if (grfImage.Pixels[(y - 1) * grfImage.Width + (x)] == 1 || grfImage.Pixels[(y + 1) * grfImage.Width + (x)] == 1 ||
    					grfImage.Pixels[(y) * grfImage.Width + (x - 1)] == 1 || grfImage.Pixels[(y) * grfImage.Width + (x + 1)] == 1)
    					grfImage.Pixels[y * grfImage.Width + x] = 2;
    			}
    		}
    		
    		act = actI;
    		
    		// Doesn't support Bgra32 only sprites
    		if (act.Sprite.Palette == null) return act;
     
    		var unused = act.Sprite.GetUnusedPaletteIndexes().ToList();
    		if (unused.Count <= 1) return act;
    		if (!act.Sprite.Palette.Contains(textForeground)) Buffer.BlockCopy(textForeground.ToBgraBytes(), 0, act.Sprite.Palette.BytePalette, 4 * unused[0], 4);
    		if (!act.Sprite.Palette.Contains(GrfColor.Black)) Buffer.BlockCopy(GrfColor.Black.ToBgraBytes(), 0, act.Sprite.Palette.BytePalette, 4 * unused[1], 4);
     
    		var colors = act.Sprite.Palette.Colors.ToList();
    		var whiteIndex = (byte)colors.IndexOf(textForeground);
    		var blackIndex = (byte)colors.IndexOf(GrfColor.Black);
     
    		for (int i = 0; i < grfImage.NumberOfPixels; i++) {
    			if (grfImage.Pixels[i] == 1) grfImage.Pixels[i] = whiteIndex;
    			else if (grfImage.Pixels[i] == 2) grfImage.Pixels[i] = blackIndex;
    		}
     
    		var index = act.Sprite.InsertAny(grfImage);
    		var max = 0;
     
    		act.AnimationExecute(0, a => a.AllLayers(p => {
    			if ((ival = ((int)(p.ScaleY * p.Height) / 2 - p.OffsetY)) > max)
    				max = ival;
    		}));
     
    		max += 15;
     
    		var layer = new Layer(index, grfImage) { OffsetY = -(max + fontSize / 2) + offset };
    		ival = 0;
     
    		foreach (var action in act) {
    			// Don't show for the death animation
    			if (ival >= 32 && ival < 40)
    				continue;
     
    			foreach (var frame in action) {
    				frame.Layers.Add(layer);
    			}
     
    			ival++;
    		}
     
    		return act;
    	};
    	
    	string displayName;
    	int from = -1;
    	var processed = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
     
    	using (var dataGrf = new GrfHolder(dataGrfName))
    	using (var outputGrf = new GrfHolder(newGrfName, GrfLoadOptions.New)) {
    		int count = jobname.Id2Sprite.Count;
     
    		TaskManager.DisplayTaskC("Adding text label", "Processing...", () => from / (float) count * 100f, isCancelling => {
    			try {
    				foreach (var mobEntry in jobname.Id2Sprite.OrderBy(p => p.Key)) {
    					if (isCancelling()) return;
    					
    					var mobResourceName = mobEntry.Value;
    					var mobId = mobEntry.Key;
    					
    					//b.AppendLine(mobId + "\t" + mobResourceName + "\t" + processed.Contains(mobResourceName));
    					
    					if (processed.Contains(mobResourceName)) {
    						from++;
    						continue;
    					}
    					
    					if (mobId2ServerName.ContainsKey(mobId)) {
    						displayName = mobId2ServerName[mobId];//.ToUpper();
    						processed.Add(mobResourceName);
    					}
    					else {
    						displayName = jobname.Id2Job[mobId].Replace("JT_", "").Replace("_", " ");//.ToUpper();
    					}
    					
    					var actEntry = dataGrf.FileTable.TryGet(GrfPath.Combine(mobFolder, mobResourceName + ".act"));
    					var sprEntry = dataGrf.FileTable.TryGet(GrfPath.Combine(mobFolder, mobResourceName + ".spr"));
    					
    					if (actEntry == null || sprEntry == null) {
    						from++;
    						continue;
    					}
     
    					var mobAct = new Act(actEntry, new Spr(sprEntry));
    					
    					StringBuilder e_builder = new StringBuilder();
    					
    					if (mobId2ServerName.ContainsKey(mobId)) {
    						int eleLevel = mobId2ServerElementLevel[mobId];
    						int eleId = mobId2ServerElement[mobId];
    						var dmgElements = new Dictionary<int, int>();
    						
    						for (int i = 0; i < 9; i++) {
    							dmgElements[i] = attr_fix[eleLevel, i, eleId];
    						}
    						
    						int highest = -1;
    						int max = maxElementShown;
    						
    						foreach (var dmgElement in dmgElements.OrderByDescending(p => p.Value)) {
    							if (dmgElement.Value < highest)
    								break;
    							
    							max--;
    							
    							if (max < 0) {
    								e_builder.Append("...");
    								break;
    							}
    							
    							highest = dmgElement.Value;
    							
    							if (e_builder.Length == 0)
    								e_builder.Append(id2ele[dmgElement.Key] + (showFirstElementDamage ? " (" + dmgElement.Value + "%)" : ""));
    							else
    								e_builder.Append(" - " + id2ele[dmgElement.Key]);
    						}
    						
    						mobAct = addNameAbove(mobAct, e_builder.ToString(), 0);
    						
    						if (displayMobNames)
    							mobAct = addNameAbove(mobAct, displayName, 15);
    					}
    					else {
    						if (!displayMobNames) {
    							from++;
    							continue;
    						}
    						
    						mobAct = addNameAbove(mobAct, displayName, 0);
    					}
    					
    					MemoryStream actStream = new MemoryStream();
    					MemoryStream sprStream = new MemoryStream();
     
    					try {
    						mobAct.SaveWithSprite(actStream, sprStream);
    					}
    					catch {
    						from++;
    						continue;
    					}
    					
    					outputGrf.Commands.AddFile(actEntry.RelativePath, actStream);
    					outputGrf.Commands.AddFile(sprEntry.RelativePath, sprStream);
     
    					if (from % 200 == 0) {
    						GC.Collect();
    						outputGrf.QuickSave();
    						outputGrf.Reload();
    					}
     
    					from++;
    				}
    			}
    			catch (Exception err) {
    				ErrorHandler.HandleException(err);
    			}
    			finally {
    				outputGrf.QuickSave();
    				from = count;
    			}
    		});
    		
    		//ErrorHandler.HandleException(b.ToString());
    	}
    }
    catch (Exception err) {
    	ErrorHandler.HandleException(err);
    }
    finally {
    	EncodingService.DisplayEncoding = oldEncoding;
    }

     

    • Upvote 2
    • Like 1
  6. 3 hours ago, Ploom said:

    Any way I can download the old version 1.8.2.7 or older? Someone told me that I can make a quadmap using those versions only but not on the latest one

    There are no versions that allow you to make a quadtree. You'll need BrowEdit 3 for map edits.

    Though, if you... really want to try older versions, you can do so from the rAthena download page at the bottom (here😞

    image.png.d1974476c6427f101659087216d93c68.png

    (Version 1.0.1 is quite catchy... ahem!)

  7. On 1/28/2025 at 7:05 PM, nadomendes2 said:

    @Tokei When using QuickMerge do you need to close grf1 and grf2?

    You don't need to close grf2, but grf1 will be used and overwritten, so yes it needs to be closed. You don't have to do that if it's currently opened by GRF Editor, it will close and reopen it automatically.

  8. On 12/1/2024 at 6:59 PM, Klone Navir said:

    Hello, thank you so much for your response!

    The color theme is just because I'm using high contrast color mode in Windows, as it's easier on my eyes, it doesn't affect functionality of programs(I tested with and without high contrast mode).

     

    Here's the server's data.grf and also a screenshot of the bytes that you requested:
    https://mega.nz/file/trEnDY6A#bHk_e81spoMmyfE3UYs-XBDWP8iOL3V7yNmdQ5zQBtg
     

    image.png?ex=674e461b&is=674cf49b&hm=beb55f8f5ac74c1d03e4d4dee132cf44a3d4ca48fe1d0e9f35a51ea280af412e&=

     

    In order to make the GRF Editor error go away, you simply have to replace the first 16 bytes with:
    4D 61 73 74 65 72 20 6F 66 20 4D 61 67 69 63 00
    But after having the error go away and GRF Editor still being unable to display the folder directory within, I'm stumped.

    GRF Editor is not detecting any encryption, and I highly doubt that it's corrupted, as it works just fine on the server it's from.

    This isn't a GRF, or it's encrypted from a third party tool. So there's not much to do here. Changing the header won't do much.

  9. On 12/12/2024 at 6:37 PM, OptimusM said:

    Hey man ~!
    Thank you for your work as always 💠

    When i try to open large .lub files using grf editor, like equipmentproperties.lub (7mb) grf editor just closes (crash i think).
    I'm wondering if it's related to that file size or something else, but yeah.

    I'll be looking into this one. Though I need to remake the lub decompiler, it's... a bit nasty.

    On 1/9/2025 at 7:13 PM, fffuwa said:

    hi there, whenever i try saving my GRFs as is, it's triggering this error for me - any reason why?

    image.png.62bcc86cff428bf9bb3eb0f8a4cbb52e.png

    Well, as it says, you're run out of memory. This is hard to reach however, especially GRF Editor is released in x64 and there's practically no memory limit. Make sure you've updated to 1.8.8.8 and see if that resolved your issue.

    • Love 1
  10. 9 hours ago, Klone Navir said:

    Hello, I'm trying to modify my aura on a server that seems to have that within a grf, probably data.grf. When opening that grf with GRF Editor, I get this error:image.png?ex=674d55ff&is=674c047f&hm=7783d18645ef4da0e0433d8d4587a03a8e893814f51e28d45cb184c7ac1404d4&

    After this, I went ahead and hex edited the grf so the header would be Master of Magic and added a 00 padding for the 16 byte file size, since not doing so gave an error about how it was looking for "Master of Magic" and found "Master of Magic", this is the padded error fix:

    4D 61 73 74 65 72 20 6F 66 20 4D 61 67 69 63 00

    After doing this, the error disappeared from GRF Editor and where it said "errors were detected" it just says "finished" as it should, however, the data.grf still doesn't display directory folders on the left panel where it should, so now I have no error to work with and don't understand what's wrong. What can I do now?

    Hmm, is that even a GRF...? You'd have to show me the first ~50 bytes of the GRF (or upload the GRF somewhere) to have a look. If you have to manually change the header, something's definitely not right here. The magic header is always Master of Magic.

    (Also, that color theme is quite broken. That's not how it's supposed to look at all.)

  11. That's normal. It doesn't really make sense to send a new walking packet for your own character after using @refresh. But you can forcefully send it again in the clif_refresh function (in clif.cpp). Just add the following at the end of the function.

    	struct unit_data* ud = unit_bl2ud(&sd->bl);
    
    	if (ud && ud->walktimer != INVALID_TIMER) {
    		unit_walktoxy(&sd->bl, ud->to_x, ud->to_y, 4);
    	}

    Though, it will look a tad weird because the character has to walk from a center cell, so it will "walk back" when using @refresh. You could also send a "unit_stop_walking(&sd->bl, USW_NONE);" if you want the character to stop moving instead. Whichever you prefer.

    • Upvote 1
    • MVP 1
  12. 14 hours ago, Medivh said:

    @Tokei The way the grf is encrypted is no longer the same as before. Did you enter the password in the option that the program gave?
    I tried to do this, it only generated a dll file and the client, and it did not generate another grf like before. Even after taking the dll and putting it in the patch, it did not ask for a password to open the grf.

    Hello, I'll be adding a better guideline on the encryption window as I can understand the confusion there.

    You are correct, the GRF Encryption tool window is only meant to create the client files (client.exe + cps.dll). The "Encrypt GRF" option was removed from that window because it was redundant. To encrypt your GRF, you have to right-click a folder and select encrypt:

    image.png.e507bb72b6fb325b9e06526c1b24b812.png

    This does the same thing as the previously removed option. That is also how you're supposed to encrypt your Thor patches when you want to patch encrypted content.

    As for the password prompt when opening the GRF, that is mostly for visual effect in the first place. An encrypted GRF may not have this prompt. Also, the GRFs can be partially encrypted, so if you choose "Cancel" when this prompt shows up, you can still browse the GRF depending on what you did.

    • Upvote 1
  13. On 11/5/2024 at 1:37 PM, poonsak58 said:

    I use flat map maker in options uncheck 'Make the ground flat' then generate map.
    result the ground map cell display is incorrect.

    Updated to 1.8.8.4 to fix this issue. The flat maps tool has been updated as well.

     

    13 hours ago, askuro3 said:

    My client simply opens and then closes immediately. I've tried everything, I followed the entire tutorial to encrypt the GRF but I still have the same problem. Does anyone know what could be happening?

    Hello,

    • Which client version are you using?
    • Which version of GRF Editor are you using?
    • Which Visual C++ redistributables version did you install?
    • Upvote 1
  14. 20 hours ago, LearningRO said:

    I have same issue even checked that option with client exe 2020

    I was unable to reproduce the issue. Can you show me what the emblem received by the emulator was? It will be stored in your SQL database in the "guild" table under the "emblem_data" field. (You should check the previous field first of course.)

  15. 21 hours ago, Hexer02 said:

    image.png.3fe7946b27af55c647203c5a8bf920f5.png

     

    If you're asking how to make a similar GRF, that is from File > Save advanced > Encrypt File Table.

    If you're asking how to read a similar GRF, that is not made available.

    • Love 1
  16. Updated to 1.8.7.7:

    • Updated the encryption library for added security.
    • This new version may not work in older clients (hard to test on my end, do let me know).
    • This version requires Microsoft Visual C++ 2022 (x86): https://aka.ms/vs/17/release/vc_redist.x86.exe
      • This is also required for your players. It is usually already installed for most, but some may not have it. The previous version of the encryption required VC++ 2010 (x86).
    • A custom approach as mentioned in the above post (https://rathena.org/board/topic/77080-grf-grf-editor/?do=findComment&comment=432591) will always be more secure.
    • If you do decide to rename cps.dll to something else, then you'll have to ensure your previous encryption cps.dll no longer exists. Otherwise, this will be pointless. You can leave the regular cps.dll from Gravity, that will not conflict.
    • Love 4
    • MVP 3
  17. Updated to 1.8.7.6:

    • Added support for using different GRF magic headers (Master of Magic). It can be modified via Container options > Grf type properties > Magic header
      image.thumb.png.0a006109d65613ffa3ec0931a5e9803a.png
    • (This also means that modifying your GRF header now has no real purpose.)
    • Added support for GRF version 0x300 (from jRO).

     

    On 9/12/2024 at 4:12 AM, Sapito Sucio said:

    Could you elaborate on how the compression works? I would like to try and maybe add some other compression options.

    You can refer to this post:

    Look at the compress/uncompress methods. The project files are mostly about encryption, but if you ignore that part, the compress/uncompress methods are still going to be used by the client. Just copy the method signatures and make a new project from scratch, or copy this one.

    • uncompress is meant for decompressing the GRF entry.
    • compress is meant for compressing emblems (it is used by the emulator, so leave zlib compression for this; unless you want to change the compression server-side as well).
    • compress2 is where you'd define your own compression method (this is a custom exported method used for GRF Editor, to not conflict with the default compress method; if not defined, it will load compress instead).
    • Upvote 2
    • Love 1
  18. 4 hours ago, Sephus said:

    Unless you can guarantee that the while loop doesn't always result in a true condition.

    while (node && st->sleep.timer != INVALID_TIMER) {

    For example, if the timer runs indefinitely, the server would be in an infinite loop here. Indefinitely waiting for the timer to arrive as an INVALID_TIMER. Blocking all other processes. Hell even while the timer (for a shorter duration) is being processed, it would wait here blocking all other processes on the server. I don't think the code is safe for the server.

    Such is coding on single threaded applications >:D

    It took me some time to figure out what the concern was, but I think I got it. This comes from a confusion on what the while-loop condition means and the misconception of how timers work in the emulator.

    First thing first, sleep_db is a linked list, each node is connected to the next node, and the final node is connected to a null pointer. So eventually, node->next will return a null pointer, and it will break out of the while-loop. This is your typical way of going through a linked list and this is what we're doing here. We're just going through a linked list, nothing more.

    As for the "while (node && st->sleep.timer != INVALID_TIMER)" condition, think of it more like this:

    if (st->sleep.timer != INVALID_TIMER) {
    	while (node) {
    		if (something) {
    			...
    			break;
    		}
    		node = node->next;
    	}
    }

    It simply combines the condition for simplicity. If the timer is already marked as INVALID_TIMER, then it doesn't need to remove the linked node nor does it need to delete the timer. This has nothing to do with "waiting for the timer to finish", it doesn't do that at all. It's a condition check for whether or not it should bother deleting the timer.

    As you've said before, rAthena is indeed a single threaded application, so waiting for a timer to finish directly doesn't make sense and I want to reiterate that this isn't what this code does.

    Also, I said this previously, but this code is copied from run_script_timer. The code you're worried about is already currently running on your server, so if there was a problem with it, you'd have known by now (it's being executed dozens of time every second as well).

    __

    Quick note on timers: after using add_timer, no threads are created. Your timer tick is added to a static list and then checked constantly through the core loop of the emulator in core.cpp (via do_timer):

    #ifndef MINICORE
    		if( !this->m_run_once ){
    			// Main runtime cycle
    			while( this->get_status() == e_core_status::RUNNING ){
    				t_tick next = do_timer( gettick_nocache() );
    
    				this->handle_main( next );
    			}
    		}
    #endif

    Once the your timer tick is expired, the code will be executed and the timer will be removed. It's rather simple in the end

    Though on that note... That's also why you'll notice that using small sleep timers (like "sleep 100;" in a script) is often not recommend if you rely on these small amounts. The timers are always delayed by 0~20 ms because the core loop for timers only check at a minimum interval to not overload the emulator. So in such a scenario, you'll notice that doing "sleep 100;" 50 times in a row will take roughly 6 seconds rather than 5 seconds. It's quite noticeable after a while, and that's why you should rely more on OnTimer labels instead (those are still delayed by that 0~20 ms, but the delays don't stack up, so they end up being more accurate). Anyhow, hopefully this helps some people deal with timers both in the source and in scripts.

    (So yes, this while-loop is safe!)

  19. 8 hours ago, Smokexyz said:

    @Tokei Your while loop is raising eyebrows because the server will be in a blocking loop until the timer exits. 

    You mean the loop in script_sleep_resume? It will leave the loop once the timer is found in sleep_db. Then it's erased/deleted and set to INVALID_TIMER. There is nothing blocking as far as I can tell. This code is copied from the run_script_timer timer function, which resumes a script after sleep/sleep2 is executed. Or are you talking about another loop or am I missing something here?

  20. Heya,

    Personally, I always disliked that command because it's awkward to use. The answer from Winterfox probably works, but if you want to have more "freedom" when using the command, you can instead my custom command "unitwalk_wait" (you'll have to apply the diff file yourself, of course).

    /// Makes the unit walk to target position.
    /// Returns if it was successful.
    ///
    /// unitwalk_wait(<unit_id>,<x>,<y>{,<timeout>}) -> <bool>

    Basically, the script will pause until the unit has reached its destination (or until it times out). Here is a script example:

    prontera,150,186,4	script	test_walk	77,{
    	mes "Move the NPC.";
    L_Again:
    	getmapxy(.@m$, .@npc_x, .@npc_y, BL_NPC, getnpcid(0));
    	
    	switch(select("Left:Right:Scripted path")) {
    		case 1:
    			unitwalk_wait getnpcid(0), .@npc_x - 3, .@npc_y;
    			break;
    		case 2:
    			unitwalk_wait getnpcid(0), .@npc_x + 3, .@npc_y;
    			break;
    		case 3:
    			clear;
    			mes "Give me a moment...!";
    			close2;
    			donpcevent strnpcinfo(0) + "::OnWalk";
    			end;
    	}
    	
    	goto L_Again;
    	end;
    OnWalk:
    	.@npcid = getnpcid(0);
    	unitwalk_wait .@npcid, 150, 187;
    	misceffect 150;
    	npctalk "Here I go!";
    	sleep 1500;
    	unitwalk_wait .@npcid, 155, 187;
    	unitwalk_wait .@npcid, 160, 187;
    	emotion E_GASP;
    	npctalk "What a rough corner...";
    	sleep 1500;
    	unitwalk_wait .@npcid, 160, 180;
    	npctalk "I have walked plenty, no more sir.";
    	end;
    }

    Source changes:

    diff --git a/src/map/script.cpp b/src/map/script.cpp
    index 8fdc6fabc..16a54d81b 100644
    --- a/src/map/script.cpp
    +++ b/src/map/script.cpp
    @@ -4256,6 +4256,24 @@ TIMER_FUNC(run_script_timer){
     	return 0;
     }
     
    +int script_sleep_resume(struct script_state* st) {
    +	struct linkdb_node* node = (struct linkdb_node*)sleep_db;
    +
    +	while (node && st->sleep.timer != INVALID_TIMER) {
    +		if ((int)__64BPRTSIZE(node->key) == st->oid && ((struct script_state*)node->data)->sleep.timer == st->sleep.timer) {
    +			script_erase_sleepdb(node);
    +			delete_timer(st->sleep.timer, run_script_timer);
    +			st->sleep.timer = INVALID_TIMER;
    +			break;
    +		}
    +		node = node->next;
    +	}
    +	if (st->state != RERUNLINE)
    +		st->sleep.tick = 0;
    +	run_script_main(st);
    +	return 0;
    +}
    +
     /**
      * Remove sleep timers from the NPC
      * @param id: NPC ID
    @@ -19782,6 +19800,66 @@ BUILDIN_FUNC(unitwalk)
     	return SCRIPT_CMD_SUCCESS;
     }
     
    +/// Makes the unit walk to target position.
    +/// Returns if it was successful.
    +///
    +/// unitwalk_wait(<unit_id>,<x>,<y>{,<timeout>}) -> <bool>
    +BUILDIN_FUNC(unitwalk_wait)
    +{
    +	struct block_list* bl;
    +	struct unit_data* ud = NULL;
    +	const char* cmd = script_getfuncname(st);
    +
    +	if (!script_rid2bl(2, bl))
    +	{
    +		script_pushint(st, 0);
    +		return SCRIPT_CMD_FAILURE;
    +	}
    +
    +	ud = unit_bl2ud(bl);
    +
    +	// Timeout reached, the unit didn't walk to the destination... just ignore and move to the next script command.
    +	if (st->sleep.tick != 0) {
    +		st->state = RUN;
    +		st->sleep.tick = 0;
    +		return SCRIPT_CMD_SUCCESS;
    +	}
    +
    +	// Unit was already forced to walk.
    +	if (ud != nullptr && ud->state.force_walk) {
    +		script_pushint(st, 0);
    +		ShowWarning("buildin_%s: Unit has already been forced to walk and not reached it's destination yet.\n", cmd);
    +		return SCRIPT_CMD_FAILURE;
    +	}
    +
    +	if (bl->type == BL_NPC) {
    +		if (!((TBL_NPC*)bl)->status.hp)
    +			status_calc_npc(((TBL_NPC*)bl), SCO_FIRST);
    +		else
    +			status_calc_npc(((TBL_NPC*)bl), SCO_NONE);
    +	}
    +
    +	int x = script_getnum(st, 3);
    +	int y = script_getnum(st, 4);
    +	int timeout = script_hasdata(st, 5) ? script_getnum(st, 5) : 5000;
    +
    +	if (script_pushint(st, unit_can_reach_pos(bl, x, y, 0))) {
    +		if (ud != nullptr)
    +			ud->state.force_walk = true;
    +		add_timer(gettick() + 50, unit_delay_walktoxy_timer, bl->id, (x << 16) | (y & 0xFFFF)); // Need timer to avoid mismatches
    +	}
    +
    +	struct npc_data* nd = map_id2nd(st->oid);
    +
    +	if (ud && nd) {
    +		st->state = RERUNLINE;
    +		st->sleep.tick = timeout;
    +		ud->walk_done_script = st->id;
    +	}
    +
    +	return SCRIPT_CMD_SUCCESS;
    +}
    +
     /// Kills the unit.
     ///
     /// unitkill <unit_id>;
    @@ -27693,6 +27771,7 @@ struct script_function buildin_func[] = {
     	BUILDIN_DEF(setunitdata,"iii"),
     	BUILDIN_DEF(unitwalk,"iii?"),
     	BUILDIN_DEF2(unitwalk,"unitwalkto","ii?"),
    +	BUILDIN_DEF(unitwalk_wait,"iii?"),
     	BUILDIN_DEF(unitkill,"i"),
     	BUILDIN_DEF(unitwarp,"isii"),
     	BUILDIN_DEF(unitattack,"iv?"),
    diff --git a/src/map/script.hpp b/src/map/script.hpp
    index b7beb8483..4c499359a 100644
    --- a/src/map/script.hpp
    +++ b/src/map/script.hpp
    @@ -2231,6 +2231,7 @@ int conv_num(struct script_state *st, struct script_data *data);
     const char* conv_str(struct script_state *st,struct script_data *data);
     void pop_stack(struct script_state* st, int start, int end);
     TIMER_FUNC(run_script_timer);
    +int script_sleep_resume(struct script_state* st);
     void script_stop_sleeptimers(int id);
     struct linkdb_node *script_erase_sleepdb(struct linkdb_node *n);
     void script_attach_state(struct script_state* st);
    diff --git a/src/map/unit.cpp b/src/map/unit.cpp
    index a643967a0..339a025a1 100644
    --- a/src/map/unit.cpp
    +++ b/src/map/unit.cpp
    @@ -488,6 +488,15 @@ static TIMER_FUNC(unit_walktoxy_timer)
     
     		ud->state.force_walk = false;
     
    +		if (ud->walk_done_script) {
    +			ud->state.walk_script = false;
    +			struct script_state*st = (struct script_state* )idb_get(st_db, ud->walk_done_script);
    +
    +			if (st) {
    +				script_sleep_resume(st);
    +			}
    +		}
    +
     		if (ud->walk_done_event[0]){
     			char walk_done_event[EVENT_NAME_LENGTH];
     
    diff --git a/src/map/unit.hpp b/src/map/unit.hpp
    index cfd932615..76ccc0c8f 100644
    --- a/src/map/unit.hpp
    +++ b/src/map/unit.hpp
    @@ -61,6 +61,7 @@ struct unit_data {
     		bool force_walk; ///< Used with script commands unitwalk/unitwalkto. Disables monster idle and random walk.
     	} state;
     	char walk_done_event[EVENT_NAME_LENGTH];
    +	int walk_done_script; ///Script OID to run after a walking event is over
     	char title[NAME_LENGTH];
     	int32 group_id;
     

    I find this command much easier to use. You can also mix it up with the original "unitwalk" when multiple NPCs walk at the same time, as long as unitwalk_wait is the last one. Anyhow, you'll figure it out if you do end up using this command (or whoever else does).

    unitwalk_wait.diff

  21. 6 hours ago, eloscar23 said:

    image.png.6c4f8a32764bbd04813317438cbb6b45.png Hello, when I go to illusion underwater 2 it happens to me!

    Heya,

    There hasn't been a whole lot of changes in the map format. So... just open up BrowEdit 3 > load the map > data\map.rsw > Edit ... > Change the rsw version from 0x206 to... 0x204 would be good enough. Or you could also just lower the build number from 214 to 185. You could lower it all the way down to 0x201 and it wouldn't change much at all (0x201 is the most common map format, practically any client can read this one).

    image.png.7a4344fcbafddbf9293fd5eb53272f12.png

    image.png.098e6e827ce66213fa27e85c825cc8f2.png

     Then just save the map.

    (Edit: Updating your client is the proper way obviously, but if you're not ready to update yet, then the above will work just fine.)

    • Love 2
    • MVP 1
  22. Long story short, you need to use this:

    As for why, palette 0 uses the palette provided in the sprite file. That's why it shows up properly in Act Editor. Custom palettes, which is probably what you're using like everyone else, uses a different placement for the gradients. They do that because that way, they're compatible with 3rd classes (plus it's better organized than whatever mess Gravity did). The mount is transparent because the mount gradients redirect to the pink lines, which is wrong. Using the corrected version from Haziel should fix that.

×
×
  • Create New...