-
Posts
696 -
Joined
-
Last visited
-
Days Won
102
Community Answers
-
Tokei's post in Question about GRF Editor flat map tool was marked as the answer
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: 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. 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. -
Tokei's post in ACT EDITOR: Mob Name & Element Script help was marked as the answer
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; }
-
Tokei's post in Chance appearance from 4th job to 3rd job was marked as the answer
I haven't tested this, but you could just remove the 4th job flag (instead of adding the 3rd job flag):
changebase roclass(eaclass()&~EAJL_FOURTH); Though I'm sure you'll run into issues somewhere.
You could also just use the class costumes, though it's definitely more annoying to setup.
-
Tokei's post in Need help with monster summoning inside a custom instance was marked as the answer
Well, normally you'd use what you did "--en--", or "--ja--". But if you want to pick the names yourself, you can do this:
prontera,99,101,1 script Demon Tomb 565,{ mes "stuff..."; setarray .@mobIds, 1002, 1003, 1004, 1005, 1006; setarray .@mobNames$, "Poring 1", "Poring 2", "Poring 3", "Poring 4", "Poring 5"; .@mobIdx = rand(getarraysize(.@mobIds)); .@mobId = .@mobIds[.@mobIdx]; .@mobName$ = .@mobNames[.@mobIdx]; monster .@map$,99,107,.@mobName$,.@mobId,1,.@label_boss$,2; end; } If you prefer, you can use this approach below as well, same thing (I find it easier to handle in larger scripts, but there's really no difference).
prontera,99,101,1 script Demon Tomb 565,{ mes "stuff..."; setarray .@mobIds, 1002, 1003, 1004, 1005, 1006; .@mobId = .@mobIds[rand(getarraysize(.@mobIds))]; monster .@map$,99,107,'mobNames$[.@mobId],.@mobId,1,.@label_boss$,2; end; OnInstanceInit: 'mobNames$[1002] = "Poring 1"; 'mobNames$[1003] = "Poring 2"; 'mobNames$[1004] = "Poring 3"; 'mobNames$[1005] = "Poring 4"; 'mobNames$[1006] = "Poring 5"; end; }
-
Tokei's post in need help with this script was marked as the answer
A few things to note:
"next" always comes before the switch/select statement, not after the "case" branches. If you absolutely want to do this, then you might want to use "clear" before your "mes" dialogues instead. When you use delitem, you should always change the status right afterwards. Be it a quest or a variable, something needs to be done after a delitem. Here's a very likely scenario in your case: the player registers, but then the script ends there or the player uses "cancel" in the next menu. He'll then have to pay with the coin again. While that's probably not a huge deal in this particular situation, that's something you should look out for. It could lead to some serious issues later on. The usage of labels is a tad excessive. You'd normally only want to use labels if the code is repeated. Otherwise it doesn't make much sense to make things more complicated. "countitem(id) > 1" means that will only trigger if the player has 2 items in his inventory. You wanted > 0 there. The "mastery" variable doesn't work well here; that's a permanent player variable and it's way too easy to break out of the script in your case. And since the mastery amount is saved, speaking with the NPC again will cause issues since it's already set to 3 (which is the main bug you were having since you probably tested the script more than 3 times at least). I'd personally use a flag variable instead, but since you're already doing checks using a specific item, might as well use that. Well, I'd still recommend using a flag, but I don't know how this script will be used. Since the dialogues were all repeats, I added an array instead with "OnInit". That makes it easier to add more masteries later on and it's easier to read/modify. moc_para01,27,35,4 script FARHANA A 532,{ soundeffect "menu.wav",0; mes "^ce7e00 === FARHANA === ^000000"; mes "Hello, welcome to GUILD HALL,"; mes "how may i help you ?"; next; switch(select("- Benefit become a member ?:- I wish to register as guild member:- Nothing")){ case 1: soundeffect "menu.wav",0; mes "^ce7e00 === FARHANA === ^000000"; mes "Becoming a member of GUILD HALL"; mes "will enable you to boost your"; mes "MASTERY, take guild quest, and"; mes "many more..."; next; switch(select("- MASTERY ?:- Guild Quest ?:- Nothing")) { case 1: soundeffect "menu.wav",0; mes "^ce7e00 === FARHANA === ^000000"; mes "Yes mastery, such as gathering,"; mes "cooking, weapon and armor Crafting"; mes "as you progress on doing more of"; mes "those activity you will gain"; mes "more points to unlock more"; mes "higher tier stuff.."; next; soundeffect "menu.wav",0; mes "^ce7e00 === FARHANA === ^000000"; mes "But sadly.. you can only have"; mes "3 main mastery to choose for."; next; soundeffect "menu.wav",0; mes "^ce7e00 === FARHANA === ^000000"; mes "as for the rest.."; mes "they will remain as tier 1"; next; soundeffect "menu.wav",0; mes "^ce7e00 === FARHANA === ^000000"; mes "I would highly recomend to have"; mes "friends with diffrent mastery"; mes "so you can support each others"; next; soundeffect "menu.wav",0; mes "^ce7e00 === FARHANA === ^000000"; mes "i guess that will that be all,"; mes "for now, see you soon.."; close3; case 2: soundeffect "menu.wav",0; mes "^ce7e00 === FARHANA === ^000000"; mes "Guild Quest is a great help,"; mes "each member start from low rank"; mes "such a [D Rank] but you can"; mes "higher rank as you completed"; mes "more quest, higher quest get's"; mes "better reward!"; close3; case 3: goto L_Cancel; } end; case 2: if (mastery_paid_price) goto L_MasteryMenuSelect2; soundeffect "menu.wav",0; mes "^ce7e00 === FARHANA === ^000000"; mes "Ahh.. you wish to register,"; mes "sure i can do that, but i will"; mes "need 1 Durengo Coin"; next; switch(select("- I have it:- I need time")) { case 1: if (countitem(40016) < 1) { soundeffect "menu.wav",0; mes "^ce7e00 === FARHANA === ^000000"; mes "I dont think you have"; mes "1 Durengo Coin..."; next; soundeffect "menu.wav",0; mes "^ce7e00 === FARHANA === ^000000"; mes "You can exchange 1000 zeny"; mes "for 1 Durengo Coin from"; mes "my friend next to me"; close3; } delitem 40016, 1; mastery_paid_price = 1; soundeffect "menu.wav",0; mes "^ce7e00 === FARHANA === ^000000"; mes "I will take your Durengo Coin now"; mes "oh yeah even if you cancel at any"; mes "part of these process, you will not"; mes "getting back that Durengo Coin"; mes "as it is require for precess fee"; next; soundeffect "menu.wav",0; mes "^ce7e00 === FARHANA === ^000000"; mes "Great, now remember you can"; mes "only select 3 Mastery"; mes "and only those Mastery will be able"; mes "to level up, and as for the rest"; mes "it will remain as Tier 1.."; next; soundeffect "menu.wav",0; mes "^ce7e00 === FARHANA === ^000000"; mes "Now before you pick a Mastery"; mes "if you are new to the server"; mes "i would highly sugguest to pick"; mes "GATHERING, PROCESSING and 1 of"; mes "your own pick."; next; soundeffect "menu.wav",0; mes "^ce7e00 === FARHANA === ^000000"; mes "But.. if you know what you are doing"; mes "then go for it.."; next; L_MasteryMenuSelect2: soundeffect "menu.wav",0; mes "^ce7e00 === FARHANA === ^000000"; mes "Now please select your"; mes "1st MASTERY"; mes "desire Mastery"; next; L_MasteryMenuSelect: .@menu$ = ""; for (.@i = 0; .@i < .mastery_count; .@i++) { .@menu$ += "- " + .mastery_names$[.@i] + ":"; } .@menu$ += "- I need time"; .@midx = select(.@menu$) - 1; if (.@midx >= .mastery_count) goto L_Cancel; goto L_MasteryConfirm; end; case 2: goto L_Cancel; } end; case 3: goto L_Cancel; } end; L_Cancel: soundeffect "menu.wav",0; mes "^ce7e00 === FARHANA === ^000000"; mes "I shall see you soon.."; mes "Good Bye.."; close3; L_MasteryConfirm: // The mastery count is used by checking the amount of items the player has instead of using the 'mastery' variable. .@count = 0; for (.@i = 0; .@i < .mastery_count; .@i++) { .@count += countitem(.mastery_items[.@i]); } if (.@count >= 3) { soundeffect "menu.wav",0; mes "^ce7e00 === FARHANA === ^000000"; mes "Great now you have 3 main"; mes "MASTERY, remember if you"; mes "in lost or need help"; mes "ask in discord"; close3; } if (countitem(.mastery_items[.@midx]) > 0) { soundeffect "menu.wav",0; mes "^ce7e00 === FARHANA === ^000000"; mes "You have already picked"; mes strtoupper(.@mastery_names$[.@midx]) + " MASTERY."; next; soundeffect "menu.wav",0; mes "^ce7e00 === FARHANA === ^000000"; mes "Now select again.."; next; goto L_MasteryMenuSelect; } soundeffect "menu.wav",0; mes "^ce7e00 === FARHANA === ^000000"; mes strtoupper(.mastery_names$[.@midx]) + "...."; mes "nice choice, are you sure"; mes "you want to get"; mes strtoupper(.mastery_names$[.@midx]) + " MASTERY ?"; next; switch(select("- YES:- NO")){ case 2: goto L_Cancel; } soundeffect "menu.wav",0; mes "^ce7e00 === FARHANA === ^000000"; mes "Great pick!"; getitem .mastery_items[.@midx], 1; next; soundeffect "menu.wav",0; mes "^ce7e00 === FARHANA === ^000000"; mes "Now select again.."; next; goto L_MasteryMenuSelect; OnInit: // Could just use setarray, but this is easier to read for me. .@i = 0; .mastery_names$[.@i] = "Gathering"; .mastery_items[.@i] = 40008; .@i++; .mastery_names$[.@i] = "Weapon And Tool Crafting"; .mastery_items[.@i] = 40039; .@i++; .mastery_names$[.@i] = "Armor Crafting"; .mastery_items[.@i] = 40040; .@i++; .mastery_names$[.@i] = "Cooking"; .mastery_items[.@i] = 40038; .@i++; .mastery_names$[.@i] = "Slaughter"; .mastery_items[.@i] = 40041; .@i++; .mastery_names$[.@i] = "Processing"; .mastery_items[.@i] = 40007; .@i++; .mastery_count = .@i; end; }
-
Tokei's post in NPC Check Either Item To Start was marked as the answer
You have many ways of doing it. You could use a loop with an array, but this one is pretty straightfoward:
if (countitem(A)) { .@itemid = A; } else if (countitem(B)) { .@itemid = B; } else if (countitem(C)) { .@itemid = C; } if (!.@itemid) { mes "Hello, Good day huh?"; close; } mes "Oh you want this juice?": mes "let me have a look that " + getitemname(.@itemid); next; mes "Nice!, thanks for showing me,"; mes "you can have this juice !"; getitem 7709, 1; close;
-
Tokei's post in Mob attacks - how do you determine when the damage number appears? was marked as the answer
Heya,
This topic is actually very complex and it has tons of exceptions. It also depends of the client version since they do have different behaviors.
When the clif_damage packet is sent, the important value is "sdelay", "amotion", "animation motion" or "attack motion". This is the duration of the attack animation shown by the monster. This value is defined in your mob db file under AttackMotion. The AttackMotion is the speed at which the mob displays its attack animation and it can go between 0 and 500. It works as you think it does (ish): An AttackMotion motion of 100 would be 100 * AttackMotion / 500 = 1/5th of the normal duration. Meanwhile 600 AttackMotion would be capped to 500, giving you the full attack animation duration. Now you would think the animation duration is given by the AttackMotion, but that's just bullshit. That's not the case at all. The damage number shown isn't related to that. The real animation duration that the client reads is given by the Act file itself by the action index 16. If you take Willow as an example, it has 7 frames at 100 ms each, so it has a total duration of 700 ms. Now there's another special "feature" going on here. The damage number will be shown at "frames count - 1". So in the case of Condor, the damage animation will be shown at frame 7. So while the animation duration is indeed 9 x 75 = 675 ms, the damage number will be shown at 7 x 75 = 525 ms. To be clear, it is shown the moment the frame appears and not when it ends, so that's why it's not 8 x 75 but 7 x 75. As for Condor, there is yet again another special "feature" going on. The damage number will be shown earlier if the "atk" sound is triggered. In the case of Willow, the attack sound is at frame 3, so while the animation duration is 700 ms, the damage will actually be shown at 3 x 100 = 300 ms. Now that's why the attack isn't displayed at the end of the animation as you'd expect it to be. The character stops moving on the client once the damage number is shown. More on the server-side of things:
The damage will be inflicted after the AttackMotion timer server-side. Once AttackMotion runs out, the player movement will be stopped, that position will be saved and then a clif_updatestatus will be sent for the new HP value. So as you can imagine, there is absolutely no way that the position in the client and the position on the server would be the same considering all this. The server can't know the Act file true damage animation duration vs attack animation duration. Overall, quite the blunder Gravity made, yet again. And that's just what I've found out through the years. I'm sure there is plenty more weirdness going on there, like how player ranged melee attacks are 4/3 of the AttackMotion on some clients, creating even more position lag issues. Anyway, to fix your issue, change the "atk" sound and it will work out, but fixing that for all mobs doesn't seem very realistic to me.
Edit: Please keep in mind most of the above is only related to monsters; players have different behavior in regards to AttackMotion.
-
Tokei's post in WFIFOSET MAXIMUM ERROR was marked as the answer
Well, the error itself isn't too helpful. 0x8b3 is clif_showscript. So one of the possible cause is a script sending "showscript" (a script command) to a player too many times, probably in an infinity loop would be my guess. Though, the error message only shows the last packet that exceeded the length of the connection buffer, so a prior packet could be responsible too. It's hard to know for sure. If the error is somewhat new, then you should know if this rings a bell or not.
If you still can't figure it out, then you could dump the last packet in your console (in socket.cpp):
ShowDump(s->wdata + s->wdata_size, len); ShowError("WFIFOSET: Maximum write buffer size for client connection %d exceeded, most likely caused by packet 0x%04x (len=%" PRIuPTR ", ip=%lu.%lu.%lu.%lu).\n", fd, WFIFOW(fd,0), len, CONVIP(s->client_addr)); Then you can see what the string used by showscript was and track down the issue a bit more easily. If nothing else, then you'd have to dump the entire write butter, but hopefully you won't have to do that. Goodluck!
-
Tokei's post in How to separate arrays in a menu? was marked as the answer
The separator used for menus is the colon, ":". So just change "%" to "%:".
Though, this line:
.@part = .@count[select(.@menu$)]; doesn't make a whole lot of sense. Keep in mind that select returns the first value as 1 instead of 0, so you have to remove 1. What you probably wanted to write was the following.
.@selected = select(.@menu$) - 1; .@itemid = .@item[.@selected]; Also, not... an issue, but you'd probably want to change your variable names a bit by adding an 's' to your arrays otherwise it becomes very messy (and addding [0] is redundant).
copyarray .@items, $@MobDrop_item, .@count; Also, might as well use sprintf for your drop rates, it makes your life easier. So something like this:
mes "Monster ID: "+getmonsterinfo(.@mob_id,MOB_NAME)+"'"; if (getmobdrops(.@mob_id)) { // 'getmobdrops' returns 1 on success // immediately copy global temporary variables into scope variables, // since we don't know when 'getmobdrops' will get called again for // another mob, overwriting your global temporary variables .@count = $@MobDrop_count; copyarray .@items, $@MobDrop_item, .@count; copyarray .@rates, $@MobDrop_rate, .@count; mes getmonsterinfo(.@mob_id,MOB_NAME) + " - " + .@count + " drops found:"; for (.@i = 0; .@i < .@count; .@i++) { .@menu$ = .@menu$ + sprintf("%d (%s) %d.%02d%%", .@items[.@i], getitemname(.@items[.@i]), .@rates[.@i] / 100, .@rates[.@i] % 100) + ":"; } .@menu$ = .@menu$ + ":"; } else { mes "Unknown monster ID."; } .@selected = select(.@menu$) - 1; .@item = .@items[.@selected]; .@rate = .@rates[.@selected];
-
Tokei's post in quest reward question was marked as the answer
Take a look at System\OngoingQuestInfoList_True.lub (or Sakray.lub, depending of your client version), for example:
[7858] = { Title = "불온한 소리의 정체 조사", IconName = "ico_ep.bmp", Description = { "리벨리온 대원 일부는 격벽 너머에서 들려오는 소리의 정체를 밝히기 위해 이미 움직였다. 루키는 소리의 근원이 괴물이라면 멋지게 해치워 달라고 했다." }, Summary = "괴물 사냥", RewardEXP = "800000", RewardJEXP = "800000", RewardItemList = { { ItemID = 25723, ItemNum = 15 } } },
-
Tokei's post in how to limit this Gstorage Log to 30 only? was marked as the answer
Change the query to do just that, and reverse the output displayed to the player.
.@count = query_sql("SELECT `time`,`name`,`nameid`,`amount` FROM `guild_storage_log` WHERE `guild_id` = '" + getcharid(2) + "' ORDER BY `id` DESC LIMIT 30", .@time$ ,.@name$,.@item_id,.@amount); if(!.@item_id){ mes "there is no log yet"; close; } dispbottom "=========================================================",0xFFD64F; dispbottom "===================== Guild Storage Log =======================",0xFFD64F; dispbottom "=========================================================",0xFFD64F; for(.@i=.@count-1;.@i>=0;.@i--) dispbottom .@time$[.@i] + " Player[" + .@name$[.@i] + "] Item Name[" + getitemname(.@item_id[.@i]) + "] Item Amount[" + .@amount[.@i] + "]",((.@amount[.@i] > 0)?0x03c6fc:0xFF0000); dispbottom "=========================================================",0xFFD64F; Though...! At this point, why not simply use the script command guildopenstorage_log ? It sounds like it does exactly what you want to do with an UI in-game as well. You'd have to change the constant MAX_GUILD_STORAGE_LOG_PACKET to
const int16 MAX_GUILD_STORAGE_LOG_PACKET = 30;
-
Tokei's post in @pk command - Adding timer was marked as the answer
The simplest way to do that is via a status effect. You give the status when the player uses @pk, and when the status runs out, it removes the pk mode.
int atcommand_pkmode( const int fd, struct map_session_data *sd, const char *command, const char *message ) { nullpo_retr(-1, sd); if (!sd->state.pk_mode) { sd->state.pk_mode = 1; sc_start(&sd->bl, &sd->bl, SC_PK_DISABLE, 100, 0, 60 * 60 * 10000); clif_displaymessage(sd->fd, "You are now no longer in PK mode."); } else { sd->state.pk_mode = 0; status_change_end(&sd->bl, SC_PK_DISABLE); clif_displaymessage(sd->fd, "Returned to normal state."); } return 0; }
And define the SC_PK_DISABLE status:
diff --git a/db/re/status.yml b/db/re/status.yml index 96693fdce..40693394c 100644 --- a/db/re/status.yml +++ b/db/re/status.yml @@ -8166,3 +8166,9 @@ Body: NoClearbuff: true End: Sub_Weaponproperty: true + - Status: Pk_Disable + #Icon: EFST_PK_DISABLE + Flags: + NoDispell: true + NoBanishingBuster: true + NoClearance: true diff --git a/src/map/script_constants.hpp b/src/map/script_constants.hpp index 25ce19a67..ddbd1c466 100644 --- a/src/map/script_constants.hpp +++ b/src/map/script_constants.hpp @@ -1844,6 +1844,7 @@ export_constant(SC_M_LIFEPOTION); export_constant(SC_S_MANAPOTION); export_constant(SC_SUB_WEAPONPROPERTY); + export_constant(SC_PK_DISABLE); #ifdef RENEWAL export_constant(SC_EXTREMITYFIST2); diff --git a/src/map/status.cpp b/src/map/status.cpp index 42005bdae..4b6f462fb 100644 --- a/src/map/status.cpp +++ b/src/map/status.cpp @@ -13351,6 +13351,11 @@ int status_change_end(struct block_list* bl, enum sc_type type, int tid) pc_delabyssball( *sd, sd->abyssball ); } break; + case SC_PK_DISABLE: + if (sd) { + sd->state.pk_mode = 0; + } + break; } // Reset the options as needed diff --git a/src/map/status.hpp b/src/map/status.hpp index f103cd018..f2b79e058 100644 --- a/src/map/status.hpp +++ b/src/map/status.hpp @@ -1230,7 +1230,7 @@ enum sc_type : int16 { SC_S_MANAPOTION, SC_SUB_WEAPONPROPERTY, - + SC_PK_DISABLE, #ifdef RENEWAL SC_EXTREMITYFIST2, //! NOTE: This SC should be right before SC_MAX, so it doesn't disturb if RENEWAL is disabled #endif (Also... shouldn't this message "You are now no longer in PK mode." be "You are now in PK mode." instead?)
-
Tokei's post in Make the player move next beside the NPC in 2x2 distance first to claim the reward was marked as the answer
Heya,
A few comments first:
I assume that this script is going to be duplicated since you have multiple copies of it in your replay. I made it into a floating script instead. There are no handy methods to use for making a character walk towards a target with 1 cell distance (it will try to go on top of the target). If you want something more convenient, you'll have to use unitwalk with x/y coordinates, or make your own function. Though for a starting script, unitwalkto will do. Since you can break the script by simply walking away, while unitwalkto is running, it will trigger the OnOpenChest event label for some unknown reason. So you need to check the distance again. Plus, by doing so, you also prevent other type of exploits like using close confine. I added an ID next to the NPC name (World Boss Treasure#ID), which is used to prevent another exploit: If two players talk to the treasure at the same time, they'll both be able to get the items from the treasure box, whether or not you disabled the NPC. NPC variables (those starting with a .) are shared among all the duplicated NPCs. That's why I use an unique ID to track whether the box was already looted or not. - script World Boss Treasure#wb 1324,{ getmapxy(.@map$, .@npc_x, .@npc_y, 0); getmapxy(.@map$, .@player_x, .@player_y, 1); .@distance = distance(.@npc_x, .@npc_y, .@player_x, .@player_y); if (.@distance > 1) { unitwalkto getcharid(3), getnpcid(0), strnpcinfo(0) + "::OnOpenChest"; end; } goto OnOpenChest; end; OnOpenChest: getmapxy(.@map$, .@npc_x, .@npc_y, 0); getmapxy(.@map$, .@player_x, .@player_y, 1); .@distance = distance(.@npc_x, .@npc_y, .@player_x, .@player_y); if (.@distance > 1) { end; } progressbar "ffff00", 10; .@id = atoi(strnpcinfo(2)); // I presume you don't want multiple people to loot the same chest...? if (.npc_disabled[.@id]) { //dispbottom "This chest has been looted by someone else!"; end; } specialeffect EF_COIN; .wb_treasure[.wb_treasure_size++] = getcharid(3); setarray .@catch, 30203, 30202, 607, 504; // List of Junk/Other getitem .@catch[rand(getarraysize(.@catch))], 1; disablenpc strnpcinfo(0); .npc_disabled[.@id] = true; initnpctimer; end; OnReward: .@id = atoi(strnpcinfo(2)); .npc_disabled[.@id] = false; deletearray .wb_treasure; enablenpc strnpcinfo(0); initnpctimer; end; OnTimer7200000: disablenpc strnpcinfo(0); stopnpctimer; end; } prontera,150,180,3 duplicate(World Boss Treasure#wb) World Boss Treasure#1 1324 prontera,152,180,3 duplicate(World Boss Treasure#wb) World Boss Treasure#2 1324 -
Tokei's post in How do i remove unused sprites on all files in a folder using ACT Editor? was marked as the answer
Heya,
The original script can be opened with Scripts > Open scripts folder > script5_remove_unused_sprites.cs. In your case, you'd want something like this:
foreach (var actPath in Directory.GetFiles(@"C:\Test\", "*.act")) { var sprPath = actPath.ReplaceExtension(".spr"); if (!File.Exists(sprPath)) continue; var actFile = new Act(actPath, sprPath); for (int i = actFile.Sprite.Images.Count - 1; i >= 0; i--) { if (actFile.FindUsageOf(i).Count == 0) { actFile.Sprite.Remove(i, actFile, EditOption.AdjustIndexes); } } actFile.SaveWithSprite(actPath, sprPath); } -
Tokei's post in Event que is full was marked as the answer
Hmm, it's hard to phrase it differently. Your events, OnPCDieEvent and OnPCKillEvent, are on a NPC (Deathmatch PvP Warper) which is duplicated. As a result of duplicating your NPCs, the events trigger for each one of those NPCs as well. You've probably figured this out already if you've looked at the error messages. Your duplicated NPCs in your case are:
moscovia,213,182,6 duplicate(Deathmatch PvP Warper) DM PvP Warper#mos 823 morocc,170,87,3 duplicate(Deathmatch PvP Warper) DM PvP Warper#moc 823 comodo,180,150,6 duplicate(Deathmatch PvP Warper) DM PvP Warper#com 823 mid_camp,216,280,3 duplicate(Deathmatch PvP Warper) DM PvP Warper#mid 823 prontera,43,210,3 duplicate(Deathmatch PvP Warper) DM PvP Warper#prt2 823 lighthalzen,166,93,3 duplicate(Deathmatch PvP Warper) DM PvP Warper#lhz 823 prontera,263,209,6 duplicate(Deathmatch PvP Warper) DM PvP Warper#prt02 823 rachel,121,112,6 duplicate(Deathmatch PvP Warper) DM PvP Warper#rac3 823 dicastes01,188,188,6 duplicate(Deathmatch PvP Warper) DM PvP Warper#dic 823 mora,44,147,6 duplicate(Deathmatch PvP Warper) DM PvP Warper#mora 823 malangdo,146,119,3 duplicate(Deathmatch PvP Warper) DM PvP Warper#mal 823 ayothaya,157,111,3 duplicate(Deathmatch PvP Warper) DM PvP Warper#ayo 823 So when someone dies, the first event is queued:
Deathmatch PvP Warper::OnPCDieEvent But since the NPC is duplicated, these events are also queued and will trigger:
DM PvP Warper#mos::OnPCDieEvent DM PvP Warper#moc::OnPCDieEvent DM PvP Warper#com::OnPCDieEvent etc... Now obviously that's not what you wanted and that's causing you issues. You can easily fix the problem by moving the events to another NPC that is not duplicated. So simply create a new NPC, add your events to this one instead, and that's it. So something like this:
- script do_not_duplicate -1,{ end; OnPCDieEvent: // ... end; OnPCKillEvent: // ... end; } (And remove the event labels from Deathmatch PvP Warper as well, of course.)
-
Tokei's post in How to wipe 'mapreg' - Global Permanent Variables was marked as the answer
Long story short, you need to restart your server.
As for why... that is due to how SQL data is used on the map-server. When the map-server starts, it reads all the variables in the mapreg table and loads them into memory. This way, when you want to access the variable in-game (when you use @set $variable), the value is easily accessible without having to look it up with your SQL server. This is done for various other features, like your character inventory, quests, achievements, player stats, etc.
In either case, when you edit or delete the entry in SQL while it is still being loaded in memory, it will not have any impact in-game because that is not where the data is being read from. Doing so can actually cause issues as well if you're not careful. So you need to close your server, truncate the table, and then restart your server.
-
Tokei's post in Weird client error - shadowtable was marked as the answer
Well,
JT_MD_Airboat_Poring
JT_MD_AIRBOAT_PORING
are not the same. If anything, you can also just set it directly:
[jobtbl.JT_MD_AIRBOAT_PORING] = 1.2, or [20887] = 1.2,
-
Tokei's post in Guild tax over 50% not working was marked as the answer
Heya,
Make sure you have the increase guild tax limit patch when you made your client. The restriction is client-sided, not server-sided.
-
Tokei's post in Act editor remove all frames script was marked as the answer
There are many ways to achieve that:
foreach (var action in act) { action.Frames = action.Frames.Take(1).ToList(); } If you need more... "control" with indexes and which animations to remove exactly, you can do it this way too:
for (int aid = 0; aid < act.Actions.Count; aid++) { for (int fid = act.Actions[aid].Frames.Count - 1; fid >= 1; fid--) { act[aid].Frames.RemoveAt(fid); } } If you want to apply this to a batch of files, you can do it this way too:
var path = @"C:\Sprites\"; foreach (var file in Directory.GetFiles(path, "*.act")) { var actFile = new Act(file); actFile.Actions.ForEach(p => p.Frames = p.Frames.Take(1).ToList()); actFile.Save(file); }
-
Tokei's post in [Warning]: Unexpected type for argument 1. Expected string was marked as the answer
Heya,
The error message tells you the error:
if (.msg_die) message .@victimaid, "You have been killed by "+ .@killername$; if (.msg_kill) message .@killeraid, "You just killed "+ .@victimname$; The "message" script command on rAthena requires two strings as parameters. First parameter is the player name (which is pretty weird, not gonna lie), so you should change those to:
if (.msg_die) message .@victimname$, "You have been killed by "+ .@killername$; if (.msg_kill) message .@killername$, "You just killed "+ .@victimname$; Though clearly this script wasn't meant for rAthena.
-
Tokei's post in Getting this error after fixing with NEMO was marked as the answer
Heya,
Well, as the error says, your table index is nil, meaning either EFST_PAD_READER_KNIGHT or EFST_PAD_READER_CRUSADER is not defined (probably both and more below too). It's an issue with your lub, make sure your effectid.lub matches properly. This has nothing to do with NEMO.
-
Tokei's post in GRF Editor (Failed to decompress data.) was marked as the answer
You should open a new thread/question, as this is from 2017. Anyway, it simply means the entry is corrupted or encrypted, most likely encrypted.
-
Tokei's post in Need a fix for Random Option System was marked as the answer
It's a client issue. Change the clif_add_random_options function to skip empty entries. Something like...
/// Fills in part of the item buffers that calls for variable bonuses data. [Napster] /// A maximum of 5 random options can be supported. static uint8 clif_add_random_options( struct ItemOptions buf[MAX_ITEM_RDM_OPT], struct item* it ){ nullpo_retr( 0, it ); uint8 count = 0; memset(buf, 0, sizeof(struct ItemOptions) * MAX_ITEM_RDM_OPT); for( int i = 0; i < MAX_ITEM_RDM_OPT; i++ ){ if( it->option[i].id ){ buf[count].index = it->option[i].id; // OptIndex buf[count].value = it->option[i].value; // Value buf[count].param = it->option[i].param; // Param1 count++; } } #if MAX_ITEM_RDM_OPT < 5 for( ; i < 5; i++ ){ buf[i].index = 0; // OptIndex buf[i].value = 0; // Value buf[i].param = 0; // Param1 } #endif return count; }
-
Tokei's post in How to add parameter on a custom command ?? was marked as the answer
Well you can get the full command used with something like:
OnNavigate: .@cmd$ = implode(.@atcmd_parameters$, " "); if (isloggedin(getcharid(3, .@cmd$)) != 1) { message strcharinfo(0), "Target player '" + .@cmd$ + "' is offline or do not exist."; } sleep2 1; unitwalkto getcharid(3), getcharid(3, .@cmd$); end; Though, unitwalkto is very limited and will most likely fail in most scenarios.
-
Tokei's post in Act Editor - Resize Sprite to smaller was marked as the answer
Heya,
Have you tried Scripts > Magnify?
Use 0.5 to reduce the size by half. It's a decimal value that has no range limit; you can use -0.5 if you wanted to reduce by half and inverse the image too.