Make Ultima Online Great Again | zuluHOTEL Zulu Hotel Canada https://zuluhotel.ca Sun, 23 Feb 2020 19:26:17 +0000 Sun, 23 Feb 2020 19:26:17 +0000 PicoCMS Archery and Tactics <p>Hello friends,</p> <p>I hope the year 2020 is treating you all well. This blog post is the first of what I hope will be many this year, and today we're going to look at specializations (&quot;classes&quot;), the Ranger in particular.</p> <p>For years, the Ranger has been particularly problematic, and it all boils down to the <em>zuluctf</em> game from the early days of Zulu Hotel, back when the emulator was an early version of <a href="https://polserver.com/">POL</a> and there were no classes except for in the Capture the Flag mini game. Those classes were &quot;War Axe&quot;, &quot;Mace&quot;, &quot;Bow&quot;, &quot;Mage&quot;, &quot;Cleric&quot;, and &quot;Warrior&quot;. Over the years those were refined into the standard breakdown we're familiar with today, and they have an agreeable symmetry: All the specializations have 8 core skills with no overlap, and things were okay when the POL codebase was in flux, but as things converged to be more &quot;OSI-like&quot; (or EA-like if you prefer), certain inter-dependencies became standardized.</p> <p>One of those things is that on the official servers, Tactics gives a percentage bonus to weapon damage. POL picked this up early on, but of course that poses a problem: Rangers don't have Tactics as a class skill. POL hardcoded the Tactics bonus in their core starting with version 094 or 095, and if you dig into the script releases for some early Zulu Hotel clones you can see there are specific <code>if</code> checks as a workaround:</p> <pre><code class="language-pascal">for i := 0 to SKILLID__HIGHEST var amount := GetEffectiveSkill( who , i ); total := total + amount; if( i in classe_skills ) classe := classe + amount; if( i == 31) rangersak := 1; endif endif endfor if ( rangersak == 1) total := total - GetEffectiveskill(who , SKILLID_TACTICS); endif</code></pre> <p>However in RunUO:</p> <pre><code class="language-csharp"> damage += ( damage * ( ( attacker.Skills[SkillName.Tactics].Value - 50.0 ) / 100.0 ) );</code></pre> <p>That's a straight bonus to damage amounting to +50% at 100 effective skill in the standard RunUO codebase. Ours is of course edited slightly to account for the 130 skill cap, but it's illustrative regardless. </p> <p>We could <code>if-else</code> the Tactics check away, but as our illustrious tester Turmoil recently documented, this causes a dramatic decrease in Archery damage. A spec warrior in fact does more damage with a bow than a spec ranger, due to the Tactics increase. It also precludes any use of +Tactics bows that drop in loot.</p> <p>We could rejig loot on archery weapons, and we could bump up archery damage in response, but now we're getting into some tricky balance issues, so I opted for the simpler route: The <em>Ranger</em> spec now has <em>Tactics</em> as an integral skill, and when calculating on-spec skills we average out the number of skills so that the exact number of unique skills doesn't really matter.</p> Sun, 05 Jan 2020 00:00:00 +0000 https://zuluhotel.ca/devblog/archery-tactics https://zuluhotel.ca/devblog/archery-tactics Armor Rating and the Protection Spell <p>I think a lot of folk misunderstand the amount of work that has to go into taking a stock RunUO install and paring it down to the T2A or Renaissance era with any degree of fidelity. With AOS and its wacky new protection stats system, anyone developing a pre-AOS server really needs to pick through the source rather meticulously. RunUO is littered with conditionals for Core.AOS and in some cases the focus on the later eras has meant that the non-AOS cases have fallen by the wayside.</p> <p>Case in point: Protection.</p> <pre><code>public override void OnCast() { if ( Core.AOS ) { if ( CheckSequence() ) Toggle( Caster, Caster ); FinishSequence(); } else { if ( m_Registry.ContainsKey( Caster ) ) { Caster.SendLocalizedMessage( 1005559 ); // This spell is already in effect. } else if ( !Caster.CanBeginAction( typeof( DefensiveSpell ) ) ) { Caster.SendLocalizedMessage( 1005385 ); // The spell will not adhere to you at this time. } else if ( CheckSequence() ) { if ( Caster.BeginAction( typeof( DefensiveSpell ) ) ) { double value = (int)(Caster.Skills[SkillName.EvalInt].Value + Caster.Skills[SkillName.Meditation].Value + Caster.Skills[SkillName.Inscribe].Value); value /= 4; if ( value &lt; 0 ) { value = 0; } else if ( value &gt; 75 ) { value = 75.0; } Registry.Add( Caster, value ); new InternalTimer( Caster ).Start(); Caster.FixedParticles( 0x375A, 9, 20, 5016, EffectLayer.Waist ); Caster.PlaySound( 0x1ED ); } else { Caster.SendLocalizedMessage( 1005385 ); // The spell will not adhere to you at this time. } } FinishSequence(); } }</code></pre> <p>The above snippet is from the Protection Spell. If you look closely, you'll see that if Core.AOS is false (which it is for our server), then the <code>Toggle()</code> function never gets called. Instead, it does a few checks, adds the spell object to the registry, starts a timer.... and then does nothing. There is no armor bonus granted when using this spell. Arch Protection works; and if you poke at the code you'll see it has its own, different logic.</p> <p>Daleron and I also spent some time digging into just how exactly one's armour rating or AR is calculated. It's something I'd always sort of glossed over; one of those things that just works in the background and you never bother with it. Turns out it's pretty simple in RunUO, though some of it seems pretty arbitrary:</p> <ul> <li>Every layer/slot is multiplied by a scalar</li> <li>Those products are then summed together</li> <li>Damage reduction is calculated as follows: damage absorbed = 0.5<em>AR + 0.5</em>AR*[random number between 0 and 1]</li> </ul> <p>The AR summation is so that different pieces contribute a different amount. Gorgets and gloves contribute 7%, helmets contribute 14%, arms/pauldrons contribute 15%, legs 22%, chestplates 35%, and finally shields contribute 100% of their AR rating to your total.</p> <p>Conveniently, the Zulu Archive tells me that a suit of Ryous provides 100 AR. Based on the RunUO internals it should work out something like this:</p> <p><img src="/assets/ryous.jpg" alt="Ryous" /></p> <p>In POL, it's hilariously buggy. first of all, in the itemdesc.cfg the Ryous items are all set for an AR of 70, not 100. More on that later. Now, In my copy of ZHF, built with POL093, every item has an equipscript and an unequipscript that live under scripts/control/. After some digging I found the relevant functions.</p> <p>When equipping:</p> <pre><code>function DoArMods( who , item ) var ar := Cint(GetObjProperty( item, "ArBonus" )); if( ar ) who.ar_mod := who.ar_mod + Cint(ar/2); endif endfunction</code></pre> <p>When unequipping (irrelevant stuff removed):</p> <pre><code>program unequip( who, it ) [...] temp := Cint(GetObjProperty( it, "ArBonus" )); if( temp ) who.ar_mod := who.ar_mod - Cint(temp/2); endif [...] endprogram</code></pre> <p>Let's just take a sec and list the actual armor values I got from ingame testing, one piece at a time.</p> <ul> <li>Gloves: 4 AR</li> <li>Gorget: 4 AR</li> <li>Helmet: 9 AR</li> <li>Arms: 9 AR</li> <li>Legs: 9 AR</li> <li>Chest: 30 AR</li> <li>Shield: 32 AR (dependent on Parrying skill)</li> </ul> <p>Tallying these numbers gets you 97.</p> <p>The <strong>ArBonus</strong> and <strong>ar_mod</strong> CProps get set when you equip items with a bonus to armor, such as a helmet of hardening. Unfortunately POL093 does AR processing in the core which is closed-source, and documentation is hard to find these days. As best I can tell, though, it's all fucked up.</p> <p>What if you equip the gloves and then the legs, you should have 13 AR, right?</p> <p>Wrong. You have 14. Because POL.</p> <p>The Ryous shield on its own bestows a mighty 32 AR. Well, if I equipped the legs and the gloves for a total of 4 + 9 = 14, then maybe I will get even more than 36!.</p> <p>No. Fuck you. You get 33. The gloves only count for 1 if you have a shield on.</p> <p>Well what about all 3? It should be, er.... well it should be 45.</p> <p>Wrong again! It's 43.</p> <p>How about just the legs and the arms? 18? Nope, 19.</p> <p>It goes on and on, but basically the pieces give different ratings depending on when you equip them. The whole suit, irrespective of order, eventually reaches 99 AR with the shield. That's not 100 (info vault), it's not 70 (itemdesc.cfg), and it's not 97 (grade 3 math). The funniest part to me was that if you equip everything but the shield you get 69 AR, and then get ripped off for that last 2 AR when you put the shield on and only get 99, not 101.</p> <p>Somebody, somewhere, wrote some really funky code in the old POL core(s).</p> <p>And this is why all serious UO development has moved to RunUO.</p> <p>As always, get at me on IRC.</p> Mon, 08 Apr 2019 00:00:00 +0000 https://zuluhotel.ca/devblog/armor-protection https://zuluhotel.ca/devblog/armor-protection Battle Royale <p>Confession: I play a lot of PLAYERUNKNOWN's BattleGrounds (hereafter PUBG). I like the Battle Royale idea of a massive free-for-all with one victor left standing at the end.</p> <p>So when searching for an interesting way to test PVP mechanics in our upcoming Beta Test, we were kicking around a few ideas. A generic &quot;pvp arena&quot; works for a bit but after a while you get bored of just duelling. So Daleron and I decided we'd just pound out a bastardization of PUBG in UO.</p> <p>For those of you who haven't played PUBG or its more-successful clone, Fortnite, the premise is thus: 100 players parachute onto an island, armed with nothing, and fight to the death with whatever gear they can find. There are weapons, armor, grenades, healing supplies, and all that good stuff sitting on tables and floors scattered across the huge island of Erangel (and later other maps). This loot is waiting for the player in houses, hunting cabins, hospitals, power plants, and apartment buildings. You gear up as best you can and then it's kill or be killed.</p> <p>Meanwhile, a timer is constantly counting down: There are three &quot;circles&quot; you need to care about in PUBG: Red, white, and blue. The Red circle appears occasionally and anywhere inside its radius gets hit with a massive artillery strike. The Blue circle forms the boundary between safe and danger zones. Outside the Blue Circle you take damage over time and inside it you are safe (from the zone, not from other players). Periodically, the Blue Circle begins to contract, forcing players ever closer together. The White Circle does nothing except show where the Blue Circle will pause. Ignoring the silly Red zone, the game flow goes like this:</p> <ol> <li>Parachute from a C-130 Hercules onto the island</li> <li>White zone appears</li> <li>Blue zone slowly contracts until it reaches the white zone, then stops</li> <li>White zone contracts</li> <li>Wait a short period of time</li> <li>Go to step 3, repeat until one player remains.</li> </ol> <p><a href="/assets/uopubg2.png"><center><img src="/assets/uopubg2-thumb.png"></center></a></p> <center>(Click to enlarge)</center> <p>I decided to implement this as a <a href="https://en.wikipedia.org/wiki/Finite-state_machine">Finite-State Machine</a> with some adaptations to fit the PUBG game flow (which is of course a first-person 3D shooter) into UO (a tile-based third-person isometric pseudo-3D game): </p> <pre><code>public enum BattleState { Idle, //game not running, nobody able to join Joining, //players able to join Parachuting, //if it was pubg you'd be in the herc or parachuting Playing, // game in progress }</code></pre> <p>In Zulu Hotel our circles are actually rectangles because it's tile-based and the coordinate system is rectangular, and thus makes the math easier/faster. We use the graphics from the <a href="http://www.uoguide.com/Energy_Field">Energy Field</a> spell and others to denote the White and Blue zones. We don't have Red zones.</p> <p>Instead of Erangel, our game is played on Verity Isle (aka Moonglow Island). It turns out this is a great place for this minigame because it's massive and has several distinct areas already in: There's the city of Moonglow itself, with the docks and the bank, the Lycaeum, the Telescope, a grave yard, the Council of Mages stronghold, large expanses of wooded terrain, and farmhouses dotting the rest of the island. Instead of assault rifles and energy drinks, our island has katanas, war maces, and other weapons scattered about. You can find arrows for your bow, magic reagents, bandages, potions, and all manner of shields and armor.</p> <p>Instead of parachuting from a Hercules, when <code>BattleState</code> is in the <code>Joining</code> state, the game just kills all its participants and lets them disperse freely across the island as ghosts. When the countdown is complete we call the BeginPlay() function:</p> <pre><code>public static void BeginPlay() { foreach( Mobile pm in _Players ){ pm.Resurrect(); _AlivePlayers.Add(pm); pm.OnDeathEvent += OnPlayerDeath; pm.SendMessage("Last one standing in Moonglow wins!"); } _state = BattleState.Playing; _ZoneCenter = _FinalZoneCenters[ Utility.RandomMinMax(0, _FinalZoneCenters.Length - 1) ]; _CurrentStageEnumerator = _ZoneStages.GetEnumerator(); _NextStageEnumerator = _ZoneStages.GetEnumerator(); _NextStageEnumerator.MoveNext(); ZoneTick(); HandleZoneDamageTimer(); }</code></pre> <p>Right at the top of the function you can see that it iterates through all the Mobiles in the list of players (<code>_Players</code>), resurrects each player, and then play begins. From there the game follows the same flow as I outlined above, alternating between contracting the zone and waiting for someone to win, until the final zone is just a single tile and any remaining combatants are literally forced to stand on top of each other at which point I suspect Warrior-spec characters will probably have a distinct advantage.</p> <p>Speaking of classes, I'm excited to see how skilled players will play to their strengths and make the terrain work for them. I went through and hand-chose a long list of what PUBG players call &quot;final circles&quot;: points on the map about which the zone contracts to its smallest. I chose about 40 points with a good variety of characteristics: Some urban, which I imagine will benefit Warrior-spec characters since they can use obstacles for cover. Some wooded and open, which I imagine will benefit Mages and Rangers. Powerplayers gonna do what powerplayers gonna do.</p> <p>There's no reason you can't take a Bard or a Thief into the arena, of course, but fair warning: I've been focusing my efforts on &quot;the Big 3&quot; pvp classes, and we'll get to the others afterwards. Some of the later Zulu Hotel spin-offs and clones implemented a &quot;Song book&quot; for Bards to cast spells, but I'm taking an old-school approach: Get the fundamentals of the original ZH2 and/or ZH3 correct and then see where it goes from that point. We have Necromancer and Earth spellbooks for Mages but that's it so far. If a &quot;Song book&quot; or whatever ZH Scandinavia calls it needs to be added we can do that in the future.</p> Fri, 06 Sep 2019 00:00:00 +0000 https://zuluhotel.ca/devblog/battle-royale https://zuluhotel.ca/devblog/battle-royale Beta Keys <p>When Daleron and I decided that we wanted to hold a limited-access Beta Test for Zulu Hotel, we considered a number of different approaches, and in this post I will compare and contrast the three main potential courses of action. But first, a quick look at the requirements of any solution.</p> <p>Our priorities for Beta Testing are, in no particular order:</p> <ol> <li>Limit access to a reasonable number of testers so that it's easier to manage.</li> <li>Avoid unnecessary complexity which can lead to an increase in attack surface size or number of potential vulnerabilities, and adds a time/opportunity cost if it breaks.</li> <li>Get a diverse group of testers involved, to avoid groupthink and tunnel-vision.</li> <li>Minimize ongoing work requirements, i.e. we wanted a &quot;set it and forget it&quot;-type of solution.</li> </ol> <p>It turns out that there aren't a lot of solutions out there that tick all of those boxes, but here are a few we looked at.</p> <p>The first and most straightforward is to manually hand out access, in the form of account credentials. The <code>admin</code> command can create accounts with easy temporary passwords, and there's a <code>password</code> command for players to secure their accounts after the fact. However this solution doesn't really scale well. Quite frankly, each of us have better things to do than copy-pasting account credentials into IRC or an email (like, say, developing. Or our actual jobs).</p> <p>So that option went right out the window. The next thing we considered was, well, maybe we can create a web-based account request form where you register with a username/password combo and it requires a special code to complete the registration. I liked this idea but the more we discussed it the more it seemed like it would balloon out of control (criterion #2) and we'd still have to find a way to distribute beta invite keys on reddit or other venues (criterion 4).</p> <p>On top of that, UO shards have a history of—shall we say—brittle connections with web-based tools. Web-based account creation modules used to be an easy way to launch denial-of-service attacks on <a href="http://polserver.org">POL-based</a> shards, and frankly I'd rather have fewer web-facing gateways anyway.</p> <p>We toyed briefly with maybe hacking the client to take a third credential (i.e. username, password, <em>and</em> access code) but that would be an enormous amount of work, and so on a random weeknight back in August I decided. Fuck it. We'll leave auto-account on, and delete all the starting locations at character generation time, so that you can only start in <em>The Lobby</em>, which is just a tiny little island with nothing on it off the coast of Trinsic. That's it. There's nothing else and you can't do anything there. But this is <a href="https://reddit.com/r/prequelmemes">where the fun begins</a>: A player who's already in the Beta doesn't start in the Lobby - they start in Serpent's Hold, and from Serpent's Hold you can access the Beta's single dungeon (the Fire dungeon of course, but with different spawns). The little island attached to Serpent's Hold has some &quot;nature&quot;-type spawns as well for training Animal Taming, Herding, and the other related skills. But every single spawn has a small chance to drop a new item, called a Fiery Moonstone which, when double-clicked, will evaporate. When it does, you will hear the spirits whisper the words of power to you.</p> <center><img src="/assets/moonstone.png"></center> <p>The words of power are drawn at random from the Britannian phonetic alphabet:</p> <ul> <li>An</li> <li>Bal</li> <li>Corp</li> <li>Des</li> <li>Ex</li> <li>Flam</li> <li>Grav</li> <li>Hur</li> <li>In</li> <li>Jux</li> <li>Kal</li> <li>Lor</li> <li>Mani</li> <li>Nox</li> <li>Ort</li> <li>Por</li> <li>Quas</li> <li>Rel</li> <li>Sanct</li> <li>Tym</li> <li>Uus</li> <li>Vas</li> <li>Xen</li> <li>Wis</li> <li>Ylem</li> <li>Zu</li> </ul> <center><img src="/assets/invitecode.png"></center> <p>That string of words you receive from the evaporating stone is the invitation code. If you as a player in-game issue the <code>beta</code> command you will be presented with a gump asking for your invite code. A successful code will allow one character (and only one) to leave the Lobby and join the rest of the Beta Testers. (You can see here I made a typo).</p> <center><img src="/assets/betagump.png"></center> <p>On the technical side, what's happening is that the invite stone item creates a random string from the above list and stores it into a key-value hash table (a Dictionary in C# parlance) where the string is the key, and a Boolean is the value, defaulting to <code>false</code>. When someone correctly enters an existing key it gets marked as <code>true</code> in the hash table, meaning it has been used. Could it be brute-forced or guessed? Absolutely. But even using just four words in a beta key means we have about 0.0002% chance of guessing correctly, and to brute-force all 457000 permutations in hopes of finding a key that's been generated but not used would take about 4 years. And frankly, if someone will go through all that just to get into the beta test, then that's the kind of tester I want!</p> <p>The more we thought about this solution the more it makes sense: It's low-maintenance, it encourages play (friend wants an invite code? Go literally hunt for one!) as opposed to AFKing, it doesn't expose any additional attack surface, and when we're done Beta I'll just simply set <code>Core.Beta = false;</code> and the items will stop dropping in loot. Minimal, mostly in-game driven, etc.</p> <p>Stay tuned - next I'll be writing about our new combat minigame.</p> Thu, 05 Sep 2019 00:00:00 +0000 https://zuluhotel.ca/devblog/beta-keys https://zuluhotel.ca/devblog/beta-keys Christmas 2019 Update <p>Well friends,</p> <p>It's Christmas Eve, my two-year-old is sleeping soundly in her bed and presents are wrapped, so what better to do than to hack away at RunZH?</p> <p>As you may have seen from the <a href="/beta/">beta page</a> or the in-game MOTD (I update it! You should read it! It's relevant!), I've officially declared that we're now testing crafting. So naturally, I was poking around at the crafting system.</p> <p>Couple of big bugs stood out immediately:</p> <ol> <li>I haven't actually implemented the &quot;Zulu Hides&quot; yet</li> <li>If you lumberjack wood from a tree, it keeps the properties of e.g. Golden Reflections, but it doesn't call itself that. It just calls itself &quot;log&quot;.</li> <li>Several of the GatherNode flock escaped their enclosure (the map) and fled off into the Great Beyond.</li> </ol> <p><img src="/assets/escape.png" alt="Tracking code exposes the location of the escapee" /></p> <p>You can see here that the x and y coordinates of the Golden Reflections GatherNode are (11371, -772) which is neat enough. But then you realise the map is only 7168 tiles wide, and negative Y coordinates should be impossible!</p> <p>Well, it turns out that my algorithm was flawed. For a GatherNode with position (X, Y) and velocity (u, v):</p> <pre><code>Should GatherNode drift? If Yes: X' = X + u Y' = Y + v Would (X', Y') be out of bounds? If Yes: Reflect about the nearest axis: u = -u or v = -v as appropriate X = X + u Y = Y + v</code></pre> <p>Clever readers may spot the bug I didn't: I only check if (X', Y') is out of bounds, but it's possible at the corners of the map to have the reflected vector still end up outside the map, and that dear friends is how the odyssey of the Golden Reflections GatherNode began. The poor little Node had been traveling for upwards of 2200 world saves only for its escape to be foiled!</p> <p>Alas for poor Golden Reflections GatherNode, the code is much simpler now and the tyrannical modulo operator brooks no disagreement nor tolerates any escape. We cannot remove all freedom, however (we aren't barbarians), so we allow the little GatherNodes the opportunity to mutate their velocity vectors every so often.</p> <pre><code class="language-csharp">public void Mutate() { if( m_mutatechance &lt;= Utility.RandomDouble() ) { m_vX = Utility.RandomMinMax(-10, 10); m_vY = Utility.RandomMinMax(-10, 10); } } public void Drift() { if ( m_driftchance &gt;= Utility.RandomDouble() ) { m_X = ( m_X + m_vX ) % m_xbound; m_Y = ( m_Y + m_vY ) % m_ybound; Mutate(); //keep it gangsta } }</code></pre> <p>Merry Christmas!</p> Tue, 24 Dec 2019 00:00:00 +0000 https://zuluhotel.ca/devblog/christmas-update https://zuluhotel.ca/devblog/christmas-update Retrospective: First week of Beta <p>Well, we've just finished our first week in Beta and I'm absolutely thrilled at the response so far. There are about 40 people idling in Discord and sadly just 2 in IRC. Oh well.</p> <p>Exactly as planned, beta testing turned up quite a few bugs of varying severity. I'm pleased to report nobody's found a bug that has a security impact (i.e. gain privileges or crash the server). I've expanded my TODO list greatly, and also mashed a few bugs, including but not limited to:</p> <ul> <li>Items in Battle Royale now spawn pre-identified</li> <li>Battle Royale now tells you which way to go if you're outside the poison field taking damage</li> <li>Attempting to join Battle Royale will now give the player an indication of when the next game will start</li> <li>A few of the lesser-used skills now gain past 100 (whoops)</li> <li>Cleaned up some debug console spam</li> <li>Blacksmiths buy coloured ore and logs</li> <li>A bunch of other minor bugs too numerous to list here</li> </ul> <p>On my list of &quot;known bugs&quot; are (among others):</p> <ul> <li>The game needs a &quot;sell all&quot; command </li> <li>Lumberjacking doesn't work (this one is weird because I feel like I must have erased some changes I made previously)</li> <li>Ore and gold are really heavy</li> <li>The crafting gump doesn't show you how much ore you have except for Iron</li> </ul> <p>On these last three, I would like to point out that I'm specifically testing PVP at this time. I'm noting all the bugs down but we're going to do a dedicated crafting blitz in a week or two. Part of the reason we're not working on this yet is because Daleron is busy hacking away at a minimalist client updater, so that we can update e.g. the cliloc strings (required to fix the crafting gump) without making people re-download 1.3 GB of data. I'll get him to do a guest blog about his updater when it's released.</p> <p>In the meantime, I've been seeing great activity in fits and starts, and watching groups of 4 get pwn3d by the balron has been hilarious. Looking forward to clearing out a bunch of bugs so that I can get going on adding content.</p> <p>Future plans include (in no particular order):</p> <ul> <li>AOS Custom Housing</li> <li>Sprucing up some of the less-used skills</li> <li>Thief and Bard overhaul</li> <li>Smooth boat movement</li> <li>Going &quot;1.0&quot;!</li> <li>Open-sourcing the server code</li> </ul> <p>See you soon!</p> Sat, 02 Nov 2019 00:00:00 +0000 https://zuluhotel.ca/devblog/first-week https://zuluhotel.ca/devblog/first-week Humility (no, not the shrine) <p>Sometimes your intuition is just totally wrong.</p> <p>A while back I wrote <a href="/devblog/skillgain-part-2">a fun little post</a> about different functions for computing the chance to gain skill. You might recall that your humble devblogger suggested that the natural logarithm would be slow and unwieldy when trying to approximate the complementary error function, and that a polynomial solution was better.</p> <p>Well, I actually did some benchmarking today on my thinkpad (Sandy Bridge i7, 8GB). Turns out that C# doesn't even have an implementation for erfc() so I just tested the other three:</p> <pre><code>jester@thinkpad:~/documents/code/csharp$ mono benchmark.exe Iterating to 1 Testing Cosine method... Done. Iterations: 1, Elapsed: 9.051ms Testing Polynomial method... Done. Iterations: 1, Elapsed: 0.037ms Testing Logarithm method... Done. Iterations: 1, Elapsed: 0.012ms Iterating to 10 Testing Cosine method... Done. Iterations: 10, Elapsed: 0.015ms Testing Polynomial method... Done. Iterations: 10, Elapsed: 0.027ms Testing Logarithm method... Done. Iterations: 10, Elapsed: 0.034ms Iterating to 100 Testing Cosine method... Done. Iterations: 100, Elapsed: 0.024ms Testing Polynomial method... Done. Iterations: 100, Elapsed: 0.094ms Testing Logarithm method... Done. Iterations: 100, Elapsed: 0.025ms Iterating to 1000 Testing Cosine method... Done. Iterations: 1000, Elapsed: 0.138ms Testing Polynomial method... Done. Iterations: 1000, Elapsed: 0.681ms Testing Logarithm method... Done. Iterations: 1000, Elapsed: 0.13ms Iterating to 10000 Testing Cosine method... Done. Iterations: 10000, Elapsed: 1.284ms Testing Polynomial method... Done. Iterations: 10000, Elapsed: 5.977ms Testing Logarithm method... Done. Iterations: 10000, Elapsed: 1.022ms Iterating to 100000 Testing Cosine method... Done. Iterations: 100000, Elapsed: 11.411ms Testing Polynomial method... Done. Iterations: 100000, Elapsed: 46.885ms Testing Logarithm method... Done. Iterations: 100000, Elapsed: 6.943ms Iterating to 1000000 Testing Cosine method... Done. Iterations: 1000000, Elapsed: 69.204ms Testing Polynomial method... Done. Iterations: 1000000, Elapsed: 225.343ms Testing Logarithm method... Done. Iterations: 1000000, Elapsed: 34.558ms Iterating to 10000000 Testing Cosine method... Done. Iterations: 10000000, Elapsed: 420.749ms Testing Polynomial method... Done. Iterations: 10000000, Elapsed: 2069.943ms Testing Logarithm method... Done. Iterations: 10000000, Elapsed: 346.445ms Iterating to 100000000 Testing Cosine method... Done. Iterations: 100000000, Elapsed: 4179.357ms Testing Polynomial method... Done. Iterations: 100000000, Elapsed: 20475.724ms Testing Logarithm method... Done. Iterations: 100000000, Elapsed: 3452.435ms</code></pre> <p>Ignore that first aberrant result - several orders of magnitude slower than the next trial. That's not a typo, just a small bug in the benchmarking code that I wrote. The extra delay is due to the constructor for DateTime being called (and probably nothing being cached yet). In reality, that first calculation takes only 12 microseconds or so.</p> <p>You can see that the Polynomial method is significantly slower, taking upwards of 20 seconds to crunch that equation 100 million times. I was surprised to see the logarithm method run almost 7 times faster! The default RunUO method is just a little bit slower than the Cosine method here.</p> <p>Just goes to show that it's better not to assume, especially when it comes to software performance. This covers the probability to gain for a single attempt: later on in a different post I'll examine how that translates to what the player experiences.</p> Fri, 10 Aug 2018 00:00:00 +0000 https://zuluhotel.ca/devblog/humility https://zuluhotel.ca/devblog/humility A new map <p>The original UO map holds a special place in my heart. I have so many fun nostalgic memories of getting wrecked at the Britain Grave Yard by PKs, frantic attempts to get to my corpse that's being camped by a balrog or something, and so many hijinx.</p> <p>But unfortunately there aren't as many UO players as there used to be, and Felucca is just too massive to support a population of even a few hundred players. And that's to say nothing of T2A.</p> <p>A few years ago, the Rel Por shard launched, burned bright in glory, and then died a swift death. Reasons for this are complicated and depend on who you ask, so I won't get into it here. What everyone who played Rel Por agrees on, though, is that the map was fantastic. One central city with a ring of smaller towns around it, and the landmasses carefully constructed to funnel players together to maximize the chances of player interaction. It just so happens that the Rel Por people released their map to the public, and I've decided to use it for Zulu Hotel. After all, they say good artists imitate but great artists steal outright.</p> <center> <a href="/assets/sosaria.bmp"><img src="/assets/sosaria-thumb.png"></a> </center> <center>(Click to enlarge)</center> <p>Behold, the world of Sosaria! At its center, New Britain, where Lord British has re-established his rule following the cataclysm that destroyed Felucca. Everyone starts there, and fans out from the center. There is plenty of space for housing and hunting and pvp all together, and yet it's only about a third the size of the original UO landmass. </p> Sat, 15 Feb 2020 00:00:00 +0000 https://zuluhotel.ca/devblog/new-map https://zuluhotel.ca/devblog/new-map Q and A <p>Believe it or not I'm sitting on a seaside patio in Hawaii doing Zulu Hotel stuff. It's been a rainy day and my wife is at the pool with our 2-year-old daughter so I figured, why not?</p> <p>Work is still ongoing, and the good news is that we've figured out the last few hurdles that were in our way to making the Magic system work the way we wanted. The bad news is that fruity drinks, sun, and sand are getting in the way of my productivity. In any case, a few questions were asked on IRC and I didn't answer because I've not been very diligent at checking IRC lately. So without further ado:</p> <p>Q: Kieeps asks if we will strip anything &quot;non-zuluish&quot; out of the server such as Champion Spawns. A: I wrote in IRC that the short answer is &quot;yes&quot; but the more I think about it the more I think that's a bad answer. We're going to have an invite-only playable beta very soon, probably within the next month. It's highly unlikely that that playable beta will feature Champion Spawns or other capstone content, but that doesn't mean we're stripping it from the game. The code is still there, but the spawns are disabled. Specifically regarding Champion Spawns we may introduce them in the future (or not, depending on what players want to see) but other OSI-specific systems will likely be out. You won't see Imbuing in the final product, for example. Q: A user named &quot;bol&quot; asked if Zulu Hotel is open-source or free-software and if they could try a beta/see the documentation. A: The documentation isn't really available or written yet, but the answer to the first question is yes. We're building this server on top of RunUO and ServUO which are both licensed under the terms of the GNU General Public License Version 2, which you can read about on gnu.org</p> Mon, 01 Apr 2019 00:00:00 +0000 https://zuluhotel.ca/devblog/q-and-a https://zuluhotel.ca/devblog/q-and-a Resource density maps for fun and profit <p>Let's face it. UO's crafting is simplistic and not super fun. You double click your hammer, double click the ore, click some menus, clank clank clank, and you've got a new Peachblue platemail gorget. When you mine for ore, or chop wood for lumber, the game pretty much just checks your gathering skill against a list of &quot;thresholds&quot;, one for each resource. As your skill grows, so does the list of ores that you can gather. OSI's implementation involved hitting &quot;veins&quot; of various ores. If you hit an iron vein, that's all you'd get until the vein despawned or was exhausted.</p> <p>The implementation in POL (and thus original Zulu Hotel) was at once more simplistic and more complicated. In my old POL093 copy I found lying around which appears to be based on ZH Scandinavia, it goes something like this:</p> <ol> <li>Call a core function <code>HarvestResource()</code> to see if you strike any resources, and the base amount that you receive.</li> <li>Roll a 1d100 (a 100-sided die)</li> <li>Compare that dice-roll against a list. If you roll something like 93 or above, you get the special gems like Radiant Nimbus Diamond. If you roll 4 or less your tool breaks.</li> <li>Roll another 1d100</li> <li>Calculate your harvesting skill divided by 5, plus 35.</li> <li>Check the number from step 5 against the roll from step 4. If your step 4 roll is less than the result of step 5, you get coloured ore. Otherwise, you get iron.</li> <li>Assuming you are getting coloured ore, then you calculate the maximum amount of gatherable ore by dividing your mining skill by 30, and rounding down. Double that if using an Omero's.</li> <li>Then you roll 1d155 and compare that number to the difficulty ratings in the ore definition file.</li> <li>Assuming the player can actually mine it, then you multiply that value from step 1 by a bunch of things including the tool bonus from Omero's/Xarafax and also from being a spec crafter.</li> <li>Oh, also you get double ores if you are spec.</li> </ol> <p>Yuck.</p> <p>Why not strive for a solution that's at both more elegant and more extensible? The system we're building will do the following:</p> <ol> <li>(When writing the server) Give each ore/log/whatever an Abundance rating that describes how rare it is. Iron has a very high abundance. Nimbus has a very low abundance.</li> <li>Determine which ores are actually present in the given area that the player is trying to mine. Make a list of these.</li> <li>Take the list from step 2 and remove all the ores that the player is not skilled enough to harvest.</li> <li>Take the list from step 3 and normalize the abundance ratings so that they add up to 100%.</li> </ol> <p>Now it's a simple matter. If you're standing in Minoc Mines, and the normalized abundance rating of Dripstone comes out to 36%, then you will receive Dripstone ore approximately 36% of the time at that location.</p> <h4>Why is this better?</h4> <p>If it ain't broke, don't fix it, right? Well, okay, the system from POL isn't <em>broken</em> per se, but it's not easily maintained or altered. A simpler system lends itself well to expansion. Here's a little thought experiment. </p> <ul> <li>Imagine you have a stack of plain white paper, like you would use in a printer.</li> <li>Take one piece of paper at a time and make a single dot on each page with a pen or pencil. Make the dot in a random place on each piece.</li> <li>Now imagine you are a really great artist (I'm not, but maybe you are) and you gradually shade the piece of paper with your pencil, getting lighter and lighter as you move away from the dot you just made. In every direction you uniformly get lighter at the same rate.</li> <li>At some places you'll hit the edge before you get to fully white. At some other places you'll hit fully white well before the edge.</li> <li>Now label all the pieces of paper with a different ore, starting with iron and moving on up to Nimbus. Now put them all back into a stack.</li> </ul> <p>What you have now is a series of layers, not unlike a photoshop document. Each layer describes the abundance of the resource. So an inch away from the dot you made initially, you might be at 60% darkness. That particular piece of paper is labeled &quot;pyrite&quot;. So Pyrite ore is 60% abundant at that particular spot. Make sense?</p> <ul> <li>Now imagine that your piece of paper is the shape of Britannia.</li> <li>Now imagine that the server moves the virtual dots around on the virtual pieces of paper, so that concentrations of ore will actually drift (very slowly) over time. </li> </ul> <p>At server birth you might be able to strike Goddess ore in Minoc mines, but a few months later Goddess might be very rare. It might be possible to find a location where only Nimbus or New Zulu is abundant in any significant numbers. That means as long as you are skilled enough you would get Nimbus or New Zulu every single time.</p> <h4>What are the gameplay implications?</h4> <p>In my mind this system has a lot of potential upsides, some of which are:</p> <ul> <li>It creates an opening where people can play the market. Maybe Lavarock (which grants protection against fire damage) is super rare right now, but you've been stockpiling it for weeks. Time to sell it to people grinding fire elemental lords at a hefty premium.</li> <li>It creates more incentive to go out and &quot;hunt&quot; for good resource spots instead of everyone just macroing AFK in Minoc mines. * It might be far more profitable to mine on one of the random mountainsides near Skara Brae, or inside Despise. Then again, it might not. Players have to go discover by themselves.</li> <li>It also creates a market for those same spots. I'll sell you a rune to a great spot for mining Ebon Twilight Sapphires for fifty thousand gold pieces.</li> <li>By the same token, PKs can go out hunting miners, and the players scouting mining locations. This creates additional chances for random &quot;field pvp&quot; to occur and makes the game exciting.</li> </ul> <p>Coupled with some other changes we've got planned for crafting, I think this will really open up that side of the game.</p> Tue, 05 Apr 2016 00:00:00 +0000 https://zuluhotel.ca/devblog/resource-density-maps https://zuluhotel.ca/devblog/resource-density-maps