Quantcast
Channel: YellowAfterlife
Viewing all 153 articles
Browse latest View live

FAQ: Things I made with Haxe

$
0
0

A wireframe Haxe icon on a familiar calm blue background

Recently someone pointed out that they never knew that GMEdit was made in Haxe, and that you often don't even know that something was made in Haxe. Which is a pretty good point - for instance, you might suspect that a lot of things that I do have some parts of them written in Haxe, but never exact.

So, as for my activities, I looked over the local projects and formed a semi-comprehensive list of which of my works to date were made in Haxe, and to what extent (excluding ones still under non-disclosure agreements, obviously).

Also includes an opening on why I like Haxe anyway.

What is Haxe

Haxe is a high-level, strictly typed programming language with a powerful cross-compiler.

As far as my intents go, its value is in:

  1. Being able to write for different platforms, but without touching their respective languages (which often lack in high-level language features)
  2. Being able to reuse code between platforms that have no shared languages
    (pretty valuable when writing both games and tools for them, and for client+server uses)
  3. Macros! There's a running joke that virtually any syntactic omission in Haxe can be correct with a macro, and... that isn't wrong.

    Most languages offer either text, token, or expression level macros, each of which has it's own advantages and disadvantages.

    Haxe's macros are functions which return expressions (in other words, you can have custom logic to decide what to return). And if you want to get really fancy, you can even construct fields or types with macros. Or even introduce new compiler targets!

Libraries

  • OpenFL-bitfive (Haxe ➜ HTML5)
    My application-oriented HTML5 backend for OpenFL, eventually retired due to too many breaking changes being introduced with each new OpenFL release.
  • hxpico8 (Haxe ➜ PICO-8 Lua)
    Compiles Haxe to PICO-8's Lua subset. Strong focus on laconic output.
  • sfgml (Haxe ➜ GameMaker)
    Compiles Haxe to GameMaker's scripting language (GML).
    As GML is roughly equivalent to JS in it's features (and not ES6 JS, mind you), having any actual compile-time checking is very welcome (if not essential) for higher-complexity work.

Tools

  • HaxMin (Haxe ➜ Neko VM)
    A Haxe-specific JavaScript obfuscator with Reflection/Type API support.
  • LDLS-m (Haxe ➜ JS + NW.js)
    A purpose-specific IDE that I developed for use in local universities.
  • Terrasavr (Haxe + OpenFL-bitfive ➜ Flash, HTML5)
    I'm somewhat known in Terraria-related communities for having made one of the most popular tools for the game. As of writing this, the web version had just recently passed 10 million pageviews. The tool allows to customize character files, add items, and generally search the game's item database.
  • BitFontReader (Haxe + OpenFL-bitfive ➜ HTML5)
    A tool that allows you to draw your BitFontMaker2 fonts in a conventional image editor and convert to BFM2 format. Also the last project that I used OpenFL for.
    There's also a fancier version that I didn't get around to publishing because I'm meaning to rewrite it all to use FontStruct's library to pipe out TTF files directly.
  • Terranion (Haxe ➜ JS)
    Another web-based tool for Terraria, this one focused specifically on viewing stats and relationships between the game's various elements. A largely automated wiki, if you may - can't have inaccuracies when your data is polled directly from the game through .NET API.
  • The Ministry of Fancy Text (Haxe ➜ JS)
    Converts input text to one or other set of decorative Unicode glyphs
  • HeatSigEdit (Haxe ➜ JS)
    A web-based character file editor for Heat Signature.
  • DocMD (Haxe ➜ Neko VM) (screenshot)
    A Markdown variant that I use for most of my documentation pages and blog posts. It's mostly regular Markdown, but supports nested sections (which also nest in output) and has a number of other small tweaks/extensions.

Games

  • Untitled hexagon game (more) (Haxe + OpenFL-bitfive ➜ JS + HTML5)
    I was testing out whether you could have complex levels in mobile web games by baking layers of scenery into static bitmaps. Also, lots of easing.
  • Light & terrain tests (more) (Haxe + OpenFL-bitfive ➜ JS + HTML5)
    Marching squares terrain and a very fancy lighting system.
  • POOL (of doom!) (Haxe + sfgml ➜ GameMaker)
    A first person game that I made for a jam.

Game components

As in, cases where the actual gameplay code is written in another language, while some complex bits are written in Haxe.

  • Mods & collabs (Haxe ➜ GameMaker (mostly))
    Most of my mods use variations and iterations of the same shared netcode base that offers deterministic networking over UDP with order/integrity control and fast resend.
  • Caveblazers Together (Haxe + sfgml ➜ GameMaker)
    As with most of my other mods, netcode is written in Haxe.
  • Nuclear Throne Together (Haxe ➜ GameMaker, C#)
    The mod's patcher, netcode, and modding API bindings are all written in Haxe.
  • Nidhogg (Haxe + sfgml ➜ GameMaker)
    Most of the game's updated netcode is written in Haxe.
    This also allowed for a Discord Store version without substantial rewriting.
  • Nidhogg 2 (Haxe + sfgml ➜ GameMaker)
    Majority of the game's netcode is written in Haxe; console-specific networking uses GM's equivalent of reflection to bind to one or other set of functions.
  • Rivals of Aether (Haxe + sfgml ➜ GameMaker, Neko VM)
    In-progress overhauled netcode, netcode helpers (fast state saving/loading), and additional tools are all written in Haxe.
  • Knight Club (Haxe + sfgml ➜ GameMaker)
    Core netcode is written in Haxe.

Extensions (that is, things that you use from GM):

  • JSON extension (Haxe + sfgml ➜ GML, JS)
    A JSON encoder/decoder with accurate error reporting and additional features.
    When targeting HTML5, a purer Haxe➜JS version is used.
  • INI extension (Haxe + sfgml ➜ GML, JS)
    An INI encoder/decoder which maintains order, comments, supports escape characters, and iteration over sections/keys - unlike the lighter set of built-in functions.
    Similarly, HTML5 uses a Haxe➜JS build from the same source code.
  • Native Cursors (Haxe + sfgml ➜ GML, JS)
    A CUR encoder written in Haxe and paired with WINAPI C++ and JS APIs accordingly.
  • Debug log server (Haxe ➜ Neko VM)
    A miniature external log window that receives debug messages over TCP and displays them.
  • ZIP writer (Haxe + sfgml ➜ GML)
    An adaptation of Haxe's standard library zip writer for use in GameMaker.
  • cmnChat (Haxe + sfgml ➜ GML) (gif)
    The advanced chat/command log that you see in my recent works.
    Has text selection, clipboard use, command/argument auto-completion, native feel.

Smaller tools:

  • BMFontToGmx (Haxe ➜ Neko VM)
    Converts BMF font files directly into GM font resources.
  • GmxGen (Haxe ➜ Neko VM / C#)
    Updates GameMaker extensions to match hinted definitions in source files.
  • GMVisualizer (Haxe ➜ JS)
    Takes text dumps of GameMaker: Studio objects and prints them to HTML/BB code with syntax highlighting and event/action type icons (example in action).
  • YYDebugView (Haxe ➜ JS)
    A web-based tool for rescuing freshly lost source code from GM's temporary files.
  • YYTextureView (Haxe ➜ JS)
    Allows to view/export/replace texture sheets in GM games.
  • RerouteAudio (Haxe ➜ Neko VM)
    Modifies GM asset packages to automatically organize audio files into subdirectories
    (instead of dumping them into one directory by default)

Bigger tools:

  • GMLive.js (Haxe ➜ JS)
    A web-based GML➜JS compiler and tester.
    I later reused parts of its compiler for other projects, including GMLive.gml
  • GMLive.gml (Haxe + sfgml ➜ GML, Neko VM)
    A livecoding extension for GameMaker, featuring a fairly complete GML compiler and interpreter compiled to GML itself. Helper app was written in Haxe➜Neko VM.
  • GMEdit (Haxe ➜ JS + Electron)
    A high-end code editor for GameMaker - has feature-complete syntax highlighting, auto-completion, and even syntax extensions.
  • GMHyper (Haxe ➜ C++) (coming soon)
    A high-end augmentation system for C++ code generated by GameMaker - accepts hint comments and rewrites generated code to use strict types or case-specific variations of functions. Also offers enhanced debugging and C++ injection.

Related posts:


Introducing: Caveblazers Together!

$
0
0

Caveblazers Together logo

My online multiplayer mod for Caveblazers was released last week[1]!
You can download it via Steam for free if you own the game.

This is a post about how that went.

[1]: Two weeks ago by time of publishing because I'm bad at scoping blog posts

Chronology

As of late, the thought process behind my modding works can be outlined with the following:

[Me, thinking] Wow, this game is real nice! ... if only it had a bit of ONLINE MULTIPLAYER
Between having quick doodles and a largely-text post, I choose doodles

And often goes like this:

Me: Hello, would you like some online multiplayer in your game?
Me: ...
Me: Also, here's a mockup mod of how this should work

Remarkably, as I seem to slowly approach status best described as "that person that's done some netcode for pretty much every high-profile GameMaker game", the odds of people accepting had been growing, but so have been the odds of people just timely hiring me to do online multiplayer before their game is done and I could start wondering if they'd like online multiplayer or not.

So, in this case, I messaged Will (developer of the game) mid-November 2017, pitching the idea of the online multiplayer mod and the general terms for it (releasing as a mod on Steam store and all), along with an (admittedly very hacky) prototype version.

After a few delays everything was confirmed, Will contacted the game's publisher, the publisher contacted their lawyer to write a formal contract, and, fast forward to March 2018 (everyone were busy), I finally could start making the actual thing... well, except at the time I was still doing some code for YoYo Games, so I wasn't able to properly work on it before April.

The development went as following: I would test the game with whomever that had time to do so (most often, makzor) for a few hours (or until a game-breaking error), take notes, and fix things. Then test again a few days later.

Finally, by late July the mod was seemingly ready for a public beta.
Comedically, as both beta and my long-overdue vacation seen slight delays, the launch of beta coincided at the exact day of me leaving for the vacation, prompting me to take the laptop with me.
Honestly, bringing a laptop on a vacation is a pretty bad idea, as then it is tempting to do some work while there, but also wasn't such a bad idea, because, aside of some pleasantly round mountains and various trees climbing their slopes, there wasn't that much to see.

The next months came and went as far as mod development goes - time to time I would squeeze in a few days time between other projects, fix the issues, and roll out a new beta build. Then people would find more bugs, I'd later fix those, and roll out another one.

By early January 2019, it occurred to me that there don't seem to be any more game-breaking bugs left. This means that it's time to release the mod, right?


... Oh

It didn't occur to me that I'd need a trailer for a mod. For games you obviously want one because it's the first thing the potential buyer sees, but for a freeware online multiplayer mod... you kind of know what you're in for, don't you.

So, I started asking around to see who'd be available to make me a tiny trailer, and, lo and behold - everyone were busy. As few weeks passed, I ended up asking Will if he knew anyone that's available, and the small trailer was finally done by a Yogscast video editor, Alex.

Fast-forward through a few more weeks of obligatory Steam pre-moderation, trailer/depot checks, and a two-week pre-release interval, and the mod finally released on March 7! And everyone seemed pleased. And nothing major broke, which seems almost suspicious.

What went well

Online multiplayer itself

A screenshot of a code snippet from Caveblazers Together, viewed in GMEdit
Always nice to work with original, properly structured code

To be fair, I've already done this ten times over, so I knew exactly what to watch out for. There have been some hiccups due to dealing with GameMaker Studio 2's spatial collision system for the first time (more on this later), but overall things went pretty well.

This being a mod and all would, of course, mean that there were some scope implications due to budget constraints (or, well, lack thereof), but also that requirements were a little softer - for instance, I probably wouldn't get away with shipping a title with a netplay menu that is completely functional but looks like this:

But, it's not all budget cuts - you also get a really fancy chat with plenty of commands, settings, and all features that you would expect from one:

A screenshot of a chat command auto-completion in Caveblazers Together

And, for a change as far as modding goes, being able to ask the developer on how something works was welcome - usually you kind of just poke at things until they work.

4-player support

4-player multiplayer in Caveblazers Together

Much like Nuclear Throne, Caveblazers had the beginnings of a local 4-player multiplayer support, and the idea was similarly canned for the reason of there simply not being enough screen space to fit 4 players.

But, now that you can play the game online, screen space is no longer the concern - you can have as many players as you want!

Or, well, as many as the game's balance can hold, which is about 4 really - while the players will have to split resources between them, their potential damage output grows linearly, and many of the game's foes and bosses don't really have the capacity to fight multiple players at once.

4-player multiplayer in Caveblazers Together, boss fight

Still, in spite of lowered (or increased, should your friends keep dying all the time) difficulty, playing with 3 or 4 players online is lots of fun!

Or, should you have an ultra-wide monitor or a 4K TV, you can play >2P CBT locally too:

More ways to play the game

While working on online multiplayer, I found that having chat commands means that you can add arbitrary quantities of uncommon options without overflowing the settings menu, so I did:

  • Mouse aiming
    While Caveblazers is designed for gamepad, it is understandable that some players are more familiar with aiming with mouse than keyboard/gamepad, so you can have that - both for shooting and for inventory management. The option resides in control settings.
  • 360-degree aiming
    Similarly to above, 8-directional aiming is part of the game's original design, and boss patterns make active use of it (as you could otherwise just shoot them from a room corner like a coward), but, if you really want 360-degree aiming - sure, you can have that via /aim360 command toggle.

    What's the catch, you ask? Orcs and other enemies will also gain 360-degree aiming


    Finally, you can fulfil your dream of being shot by all orcs on the level at once

  • Directional aiming
    Somewhat opposite of above: if you are really used to metroidvania controls, or would like to play Caveblazers on a controller with no analog sticks, /moveaim is a solution to that - when pressing the "shoot" button, the game will take not of your currently held movement keys, and you will continue to shoot in that direction until you release the button.
  • Inventory auto-sorting
    An awful lot of rogue-like/lite games at some point start involving increasing amounts of inventory management, Caveblazers included. /autosort can help with that, automatically grouping items by category.
  • Quick start
    If you are practising for the game's more challenging modes, you might find it slightly frustrating to play the first few stages over and over in search for a viable gear combination. This option aimed at making that a little easier, skipping the first level and dropping you off in a randomized shop with the amount of money that you would usually get on the first level. Activated via /startmode quick, deactivated by /startmode normal.
  • Coop revival modes
    By default, if a player dies in Caveblazers, they will enjoy the rest of the level as a ghost, and will be revived at the beginning of the next level while losing half of their unequipped items and half of the blessings (power-ups), along with any stat bonuses that they earned.

    This is okay for more experienced players (after all, in single-player modes you by default don't revive at all), but, if you are trying to introduce your friend(s) to the game, for the first few hours they might spend most of their time as ghosts while constantly losing everything.

    /revmode arena changes this to match behaviour used in Arena Mode - the players will only lose stat bonuses, while keeping their items/blessings.

  • Debug commands
    Finally, the sizeable set of debug commands in CBT lets you easily practice bosses, try out items without re-rolling, and generally get a good look at how everything works.

What did not go so well

Keeping up with updates

Here's a strange thing about optimal conditions for making large-scale mods for games: You want the game to be somewhat stale in terms of content updates, but to still have an active community,

  • If there's no community, chances are that you are doing your work for no one.
  • If the game is being actively updated, every update will cost you time to update the mod for it.

Should your mod affect enough of the game, instead of porting your mod to the new version of the game, you might have to back-port the changes from the new version of the game to whatever form or compiled code presentation that you were using for your work.

Should a large game update also release, that's basically the end of the road for your mod - you'll have to manually port your work over to the new version, figuring out what changed.
(this is why large-scale game mods often die off whenever a major game update releases)


Not to experience such specific kinds of joy, I proposed to have the mod as a "fork" of the main game - this way smaller changes could be merged to the mod automatically, and any tweaks/fixes that I make for the mod could be brought back to the base game, if needed.

So we did that, and everything was fine for a while.

Then, around May 2018, it was decided to finally move the game to GameMaker Studio 2.

That was done, and all seemed fine, until, a few weeks later, I learnt from Will that he had independently imported another copy of the project to GMS2 in preparation to console support, and had been working on that for a while, intending to merge to the repository with my code later.

While GMS1 relied purely on resource paths to differentiate resources (thus this plan would have worked there), GMS2 uses UUIDs (a long, random-ish string, designed to be unlikely to match another of it's kind), and will generate a new set if re-importing the same GMS1 project.

In other words, we now had two projects that were impossible to merge automatically. And, as both of us had made thousands of lines (of code) worth of changes, not very viable to merge manually either - the very problem that I was trying to avoid!

While I later wrote a tool to match up UUIDs between projects, it was still very much unmergeable, so matching the mod to a new base game update would usually mean that Will would copy over the non-console changes from commits in the other repository, I'd make a new build, and then it'd take a couple more builds to iron out minor inconsistencies.

Still, this'd cost more than enough in hours and unwanted headache over the development, and means that online multiplayer couldn't be "just" merged back to the base game later for console support/etc.

Reaching players

If you compare CB and CBT's stats, the game generally fluctuates between 30-70 active players, while the mod goes between 5-15, which seems about right - by a vague estimate, anywhere between 1/10 and 1/6 of all players might be interested in online multiplayer.

But, if you were to ask Steam Spy, you would find that the game has "anywhere between 100 000 and 200 000 owners total" while about 3500 people own the mod.

In other words, while the mod might be of interest for a sizeable portion of game's players, most likely simply haven't heard of it, and this will likely remain to be the mod's largest problem - even while being kindly featured on the game's store page.

And it is hard to blame anyone - with so many games coming out these days, it is barely feasible to keep up with new titles, let alone check out updates for the games that you've played a while ago.

If I were to go back and change something, I would probably have tried out releasing the mod as "early access" right away, even if it'd mean getting complaints about stability for a bit.

Top 5 bugs

Not to end this on a sad note, I present you with a small collection of fascinating issues that I have encountered during development

Not-just-visual effects

A slightly ominious-looking "+1"

If you have played Caveblazers before, you might have seen that there are plenty of small visual effects - sparkles, damage numbers, even UI effects when your magic item recharges.

And this is where the aforementioned spatial collision system comes in.

The idea of it is that the system will automatically divide your level into smaller portions - this way, instead of checking whether your projectile might hit every single enemy on the map, it only needs to check the closest 5 (or whatever amount that's in it's nearby region).

So, if a visual effect appears in a different spot for you (camera no longer shared and all that), collision system could split the level ever so slightly differently between you and other player(s).

Then something else will cause a different region to be split a little differently, and, fast forward to a few minutes later, you now hit a different orc from an overlapping group. Everything goes downhill from there. The butterfly effect as it is.

This took a little bit to figure out because I've not set up tracking for every single object type from get-go, and this would not yield any other symptoms until it was too late (with the offending visual effect most likely gone by then).

Later on, another project ran into this same kind of issue, and a change was made to GMS2 to only count instances with any actual collision checking for subdivision.

Lava kills (your netplay)

Lava in Caveblazers

Lava in Caveblazers is very fancy - it glows, it makes splashes when objects are thrown into it, it'll even make tiny "burps" (loose particles flying out of it) while idle! Attention to detail as it is.

Speaking of tiny burps, however: as all of these things obviously have a certain processing cost, lava will slightly limit its activities when it is not being observed.

And, as there was simply no reason to worry about such things, the lava would use the same pseudo-random number generator as the enemies.

Combine the two, and there you have it: if you were to shake a lava lake that only you can see just hard enough), and thus the lava lava had no reason to "burp", your game would run out of sync with other players as enemies, projectiles, and all other kinds of entities would now yield different values due to a few "missing" uses of PRNG (meaning that later numbers would be different).

Ghost-busting doors

A boss entrance door in Caveblazers
Your ultimate nemesis

This was easily the longest-standing bug in the history of the game, if not one of the bugs to escape the longest among latest projects that I worked on in general.

If you've played CBT during the beta, you might have encountered it once or twice - everything goes completely as usual, then someone's character dies on the floor prior to a boss floor, and, upon entering the boss floor, you would sometimes discover that their character is just gone - no player, no ghost, not even a UI panel in your corner of the screen:


This is what you'd see for the rest of the game unless doing tricks with chat commands

The mod inherited this issue from the base game, and Will couldn't help - he'd seen it maybe once during development of coop mode, and most CBT players would only encounter it once per 20-30 hours of playtime (perhaps even less often).

The bug resisted any attempts to debug it - at one point I would even specifically play the mod with someone while having the debugging tools open (which, in general, meant playing at an absolutely garbage framerate), and we couldn't get it to happen once.

A glimmer of light was finally shed on the issue when the built-in function for returning call stack in GameMaker was expanded in one of the updates to include information about what code resulted in destruction of a game object.

So, I made the mod log this kind of information at pretty much every opportunity, pushed out a new build, asked people to test it, and, a few days later, we finally had a sample


The kind of information logged - not pretty, but enough to debug

And, what would you think - the issue was that your ghost could get crushed by a door!

See, boss room doors (pictured earlier) are sturdy, and will crush absolutely any entity that happens to be on their path as they lock down. Even a ghost.

Usually you wouldn't really notice this, as the doors only briefly close on level start (while characters didn't even pass through the entrance yet) and after everyone got into the battle room itself (meaning that the only thing you could get crushed is items you dropped), but, if your ghost perfectly lined up with the door from its position on the last level (that's about a 1/900 chance), it'd get immediately crushed as the door prepares for its intro sequence.

So I did forbid doors to crush ghosts, Will did the same for the base game, and no one had this issue ever again.

SEE YOU LATER, I said

An "Exit" button in Caveblazers

This is an issue that still exists in the current release - whomever that created the lobby (in other words, is P1) gets to click around the main menu, and, if they click Exit, the game will quit... for both you and remote players.

The reason for this is simple enough - all menu buttons in the game are wired up the same way (and have to function in multiplayer), so CBT doesn't really differentiate between them.

Not as easy to fix as one might think, however - the game also relies on P1 being present for using any important menus, as usually they are warranted to be present.

Marker spam

While playing online, you might sometimes want to say "over here!", but the other player(s) wouldn't know where that is if you're out of their view.

So, CBT allows you to press a button (by default, F) to place a temporary marker at your mouse position and move it with the cursor. The markers are numbered and will display at the edge of the screen even if they are off-screen, making them a perfect way to point at just about any object of interest.

Once the button is released, the marker will wait 7 more seconds before fading out.

And, as it turns out, if you spam markers really fast, eventually the game will start lagging from drawing all that text, which in turn gives you more time to spam markers, and... well, you can see where this is going.

While it is not possible to crash the game this way (markers cost basically zero RAM), people did manage to get down to 5FPS from just having multiple players spam thousands of markers.

The future


What secrets does the future hold? Are any of them shaped like burgers?

You might be wondering as to what's next with the mod.

The primary task would remain to keep the mod updated as new game versions release.
In sprite of aforementioned troubles I hope that this will not be overly costly.

A few people had been asking about whether I intend to add achievements to CBT.
On the technical side, achievements are certainly not hard to do, but I would like to stray clear of just mimicking the base game's achievements or making generic "do X action Y times" ones.
So we'll see if I can gather enough ideas for these.

Those that played my Nuclear Throne mod had been asking if I'm going to introduce a modding API like I did with NTT. Unfortunately, there is no easy answer to that - while such a thing can aid with keeping a game's community active during periods without updates (see: hundreds of NTT mods on itch.io) , it is also very costly (in terms of hours of work required) to do, so it'd be unwise to promise anything while knowing that I'll remain involved with Rivals of Aether development for months to come.

All things considered, I hope that you'll enjoy the work that I've put into the mod.

Have fun!

Related posts:

GameMaker: Checking whether a string is a valid number

$
0
0


Some things are numbers, some aren't

GameMaker Studio 2.2.2 released few days ago, bringing, among improvements, "GML consistency", which changes how automatic type conversion works in corner cases.

A little less known thing, together with that it also changed how GameMaker's string-to-number conversion function (real) works, having it throw an error if your string is definitely not a number.

A slight inconvenience, given that there is not a function to check if a string is a number before attempting conversion.

But, of course, that can be fixed.

What is a number

For all we know, a string containing a number would have the following structure:

  • A minus sign - (optional)
  • Zero or more digits
  • A period . (optional)
  • If period is included, zero or more digits
  • Must contain at least 1 digit total
  • Must not contain anything else (trim your stirng separately if you must)

Let's break this down in steps,

An unsigned integer

This one's easy because GameMaker has a built-in string_digits function, which will take a string and return a new string that only contains digits from it ("a4b5" -> "45"). Thus we can utilize this to check whether a string only contains digits (and also that it is not empty):

/// string_is_uint(string)
var s = argument0;
var n = string_length(string_digits(s));
return n > 0 && n == string_length(s);

As we know that string_digits will return the digits in order, a length comparison will suffice.
Nice and easy.

A signed integer

The only difference between a signed an unsigned integer is that a signed one might have a - in front. So, we need to check that it's either all-digits, or (number of digits - 1) long if there's a -.

As GameMaker allows to implicitly cast true to 1 and false to 0, we can cheat just a little bit:

/// string_is_int(string)
var s = argument0;
var n = string_length(string_digits(s));
return n > 0 && n == string_length(s) - (string_ord_at(s, 1) == ord("-"));

(to be fair, we could also make use of fact that GM's "truthfulness" condition for numbers is num > 0.5 and shorten that to return n && ..., but let's stick to clearer notation here)

A floating-point number

Things are exactly the same, but! There can now be a dot/period.

The implementation is pretty lean about this - 1.1, 1., and .1 are all valid numbers.

So we can simply check if the input contains a ., and further decrease the expected number of digits if that is so:

/// string_is_real(string)
var s = argument0;
var n = string_length(string_digits(s));
return n > 0 && n == string_length(s) - (string_ord_at(s, 1) == ord("-")) - (string_pos(".", s) != 0);

Exponential notation

Did you know that GameMaker allows exponential notation for values passed to real?

1e3 for 1000 (1*103) or .1e2 for 10 (0.1*102) and such.

Not actively documented or anything.

And I don't suppose you would want to let the user enter such values often either.

But still, if you'd want that,

/// string_is_real_exp(string)
var s = argument0;
var n = string_length(string_digits(s));
var p = string_pos(".", s);
var e = string_pos("e", s);
switch (e) {
    case 0: break; // ok!
    case 1: return false; // "e#"
    case 2: if (p > 0) return false; break; // ".e#" or "1e."
    default: if (p > 0 && e < p) return false; break; // "1e3.3"
}
return n && n == string_length(s) - (string_char_at(s, 1) == "-") - (p != 0) - (e != 0);

Doing it yourself

Suppose you want to do things yourself, without utilizing string_digits. You can do that too,

/// string_is_real_exp_pure(string)
var s = argument0;
var n = string_byte_length(s);
var seenDot = false;
var seenExp = false;
var numDigs = 0;
var i = 1;
if (string_byte_at(s, 1) == ord("-")) i += 1;
while (i <= n) {
    var c = string_byte_at(s, i);
    switch (c) {
    case ord("."):
        if (seenDot || seenExp) return false;
        seenDot = true;
        break;
    case ord("e"): case ord("E"):
        if (seenExp || numDigs == 0) return false;
        seenExp = true;
        break;
    default:
        if (c >= ord("0") && c <= ord("9")) {
            numDigs += 1;
        } else return false;
    }
    i += 1;
}
return numDigs > 0;

As a note here, if you are reading this not for GM, pay attention that GML strings have indexes start at 1, so you'll need i = 0, (s, 0), and i < n accordingly.

As a second note, on HTML5 it's beneficial to use string_ord_at+string_length instead of string_byte_at+string_byte_length because JS doesn't work with bytes directly.


Have fun!

Related posts:

A small guide on writing interpreters, part 2

$
0
0
label hello: select dialog("Hello! What would you like to do?") {
    option "Count to 5":
        for (var i = 1; i <= 5; i = i + 1) {
            wait(1);
            trace(i + "!");
        }
        return 1;
    option "Nothing": jump yousure;
}
label yousure: select dialog("You sure?") {
    option "Yes": trace("Well then,"); return 0;
    option "No": jump hello;
}

Example of supported syntax

As some might remember, earlier this year I have published a small guide on writing interpreters, which went over the process of implementing a basic interpreter capable of evaluating expressions consisting of numbers, operators, and variables.

This continuation of the guide further expands upon concept, outlining how to support calls, statements, and branching - enough for a small scripting language.

Intro

Majority of the patterns are going to remain exactly the same as with the first part, therefore familiarizing yourself with it is recommended.

For this part, as there is a little more code, I'm going to outline the actual logic, and link to the associated code snippet for each step.

Expressions vs statements

Before we start, let's clarify what expressions and statements are [in context of this tutorial], and the difference between the two.

An expression, in short, is anything that you could use as a variable value or as a function argument.
Numbers are expressions, binary operations (a + b) too, and so are the function calls (fn(...)).

A statement is what you could put inside {} or as a then-action in an if-then-else.
return, conditional branching, and loops are all statements.

Some things (such as function calls) are allowed to be both a statement or an expression.
Some languages allow anything to be a statement (and, sometimes, statements to be expressions), but we shall not cover such things here for sake of syntactical clarity.

Small additions

Before we get to the actual statements, let's expand the syntax a tiny bit.

String literals

It's about time we get something that isn't numbers in here.

The parsing step is pretty simple - if we encounter a quote character (we shall allow both "string" and 'string'), we loop through the code until we find another of these or reach the end of it (in which case it's an unclosed string and an error). Then we get the snippet between quotes and pack it up into a token.
If you wanted to support escape characters, you would write characters to a buffer instead, and process escape characters to write something other than themselves.

Build and compile steps are pretty much the same as with numbers - we take the token/node and repackage it into a node/action.

Execution sees a few additional changes - as we now have not just the numbers, we shall make sure that operations cannot be carried out on incompatible types.
We shall allow "a" + 5 ~> "a5", but not go full JavaScript with "5" - 2 ~> 3.
Protip: Never go full JavaScript with implicit casts.

Fractional number literals

That is, not just 4, but 4.5 too.

This only requires to tweak the parser a little to permit a single period . in a number.

With this in mind, no additional validation is required before parsing the number.

Function calls

The butter of the most programming languages.

So long as you have functions, most syntactic omissions can be corrected.
Need array access for your oddish custom structure? Make a function for it.
Need to give interpreter access to game's globals? You can also make a function.
You could even allow interpreted code to generate and compile snippets of other interpreted code for ad-hoc reflection! But please don't.

The parser needs a single extra line to support commas.
Ever see a language without commas in function calls? Not a good sight.

The AST builder is where things get interesting.
So, where we would previously just output a single identifier node, we now check if an identifier is followed by a (, and then:

  • Skip over that (
  • Set up an array to hold the arguments, alongside with an argument counter.
  • Also set up a flag so that we can distinguish between the loop finishing normally and hitting EOF.
  • While there are still tokens left ahead,

    • If we're in front of a ), we skip that, set the flag, and leave the loop.
    • Otherwise we read the next argument expression and stuff it into the array.
    • If the expression is followed by a comma, we skip it.
      If it's neither that or a ), that's illegal.

  • If the function exists and the argument count matches, pack up the function call node.
    Function definitions are filled out in a global dictionary separately.

As an aside here, if you wanted to support functions-as-values, you would instead check for a ( after an expression (like it goes with binary operators), store the to-be-called expression with the arguments, and perform all checks during execution.

Compilation is nothing particular - you generate actions for each of the functions arguments, and then for the call itself (which instructs what to call and how many arguments to get off the stack).

Execution is generally straightforward:

  • Pop the now-computed arguments one by one off the stack into a temporary array.
    Keep in mind that due to execution order the last argument is on top of the stack.
    If your language of choice supports pointers, you may instead pass a pointer to the first argument in the stack, but be careful.
  • Reset the global error marker (which your functions can set to throw an error)
  • Call the function in question with the arguments we've got.
    GameMaker has this slightly worse than most language for the lack of a reflection function that would call a function with the specified array of arguments.
    In other words, you have a switch block for each number of arguments.
  • Check for error marker and either push the result onto stack, or handle the error.

Statements

Now that all of that is out of the way, we can finally get to the important things - statements!
This will involve a couple of modifications to how everything works.

The parser will stay largely unchanged for now, we'll only add a return token to it so that we can verify that anything works as such.

For the AST builder, we'll have a new, separate script for building statements. For now it will:

  • If there's a return token, read the subsequent expression to form a return-node.
  • Otherwise, try to read an expression and see if it's the kind that can be a statement too.
    As previously outlined, for now that's just the function calls.
    If it is, we pack it into a discard <expr> node, which will run the expression, but remove the result from the stack. This way we won't have to add a "push to stack?" flag to each such action.
    If it's not, throw an error about not getting a statement.

We shall also change how the base txr_build works - instead of reading a single expression and exiting, we shall read statements until we hit the end of input, and pack them into a block-node.

Compilation for the new node types is straightforward:

  • Both return and discard nodes process their expression first and then add their action, not too unlike the unary - operator.
  • A block {} node just outputs all of it's children statements.
    As we know that statements are warranted to leave the stack balanced, we don't have to output anything else here.

Execution sees minimal additions:

  • If upon reaching the end of the code the stack is empty, we output 0.
  • return action moves the program to the end of the code
    (after pushing the to-be-returned-value to the stack)
  • discard action pops a value from the stack
    (after it being pushed by preceding action(s))

Blocks & branching

Conditionals (and initial setup)

We're going to allow if-then-else statements. The syntax will be as usual:

if <condition-expression> <then-statement> [else <else-statement>]

This maps straightforwardly for the AST builder:

  • Read the condition-expression
  • Read the then-statement.
  • If the next token is else, skip it and read the else-statement, packing it as if-then-else.
    Otherwise it's a regular if-then statement.

Compilation is the interesting part here.
We are going to add jump and jump unless instructions for the runtime, which means that

if (some) trace("hi");
...

would be executed as:

push(some)
if !pop() jump(label1) // <- jump_unless
    push("hi!")
    call(trace, 1)
label1: ...

and

if (some) trace("yes") else trace("no")

would be executed as:

push(some)
if !pop() jump(label1) // <- jump_unless
    push("yes")
    call(trace, 1)
    jump(label2)
label1: // else
    push("no")
    call(trace, 1)
label2: ...

On compiler side we accomplish this by first outputting a jump/jump_unless action with no jump destination, then outputting the statement(s) from the branch, and then patching the jump-action to point at the end of the branch in compiled code.

Runtime side is just about what you would expect - jump simply changes the index of the next action to be executed, and jump_unless only does so if the freshly pop-ed value is not true-ish.

Blocks

Now that we have some actual branching, it's about time that we allow to put more than one statement per branch. To do so, we'll adopt the conventional curly bracket syntax { ...statements }.

For the parser we'll have a whole two extra lines outputting tokens for { and } accordingly.
This is no different from ( and ) or most other things in there.

AST builder part might remind you of how function calls were handled, except that we look for } instead of a ), and do not require delimiters between statements.
(by which I mean - you can later allow ; as a statement delimiter, but with well-thought-out syntax you don't need one)

There's nothing to be done on compiler or runtime sides because we've already made the builder assume the top-level node to be a block.

Setting variables

This would be the point where I get to explaining loops, but, wait - you can't have a loop if you can't change variables!
Not unless it's a GML-style repeat (times) { ... } loop anyway.
Or if your function calls have side effect on program state.
But, you know, still inconvenient.

As it goes, the parser will need another line to recognize our newly added = operator.

In the AST builder, we shall check if a non-statement expression is being followed by a =, and assume that to be a set-statement until further notice.

In the compiler, we shall check if the node is actually of a settable kind (for now, only variable-nodes are), and produce an action that would set the said expression kind if so.

Runtime side is simple here - we pop the new variable value from the stack, and feed it to the function for setting a variable by name.

Comparison operators

These aren't strictly needed, but, you know, may as well.

I'll try to keep this short as there's not much of interest, so here are the changes:

  • On the parser side we handle the new token types while distinguishing between single-character and two-character operators (< vs <=).
  • Builder is left untouched except for handling unary !, which reads the next expression (without any following operators) and packs it as !(value).
  • The compiler is left completely untouched.
  • The runtime gets new branches to handle the new operators.
    In my case I let base GM rules decide if something is true-ish for ! and whether the values are equal for == / !=.

Boolean & bitwise operators

A good scripting language needs boolean operators (AND, OR), and, ideally, short-circuit evaluation. And maybe some bitwise operators too. But let's look at the interesting part first:

Let's suppose that you have fn1() && fn2() and don't want to run fn2 if fn1 returns a true-ish value. While this is quite commonly done by using existing branching instructions,

call(fn1, 0)
if (!pop()) jump(label1) // <- jump_unless
    call(fn2, 0)
    jump(label2)
label1:
    push(false)
label2:

we can do better - by adding an instruction that checks the value on top of the stack, pops it if it's true-ish, or transits to an offset if it's not:

call(fn1, 0)
if (top()) pop() else jump(label1) // <- new action type
    call(fn2, 0)
label1:

thus, instead of 3 additional instructions, we only have a single one. Always nice.

The boolean OR is the same but in swapped order:

call(fn1, 0)
if (top()) jump(label1) else pop()
    call(fn2, 0)
label1:

(could be done by reusing AND and a unary inversion ! operator if you wanted)

As for bitwise operators, there's absolutely nothing remarkable about them - honestly I'm only adding them because I'm tweaking the parser anyway, and it's strange to not have any (see: Lua < 5.3).

The changes are reflective of what's said here - more unremarkable parser expansions, a special handling for AND/OR compilation, and AND/OR actions for runtime.

Loops

So we're finally here

A regular while-loop is by far the easiest one and is basically a looping if-statement:

// while (condition) { loop }
label1: // <- continue
    condition
    if (!pop()) jump(label2)
    
    loop
    jump(label1)
label2: // <- break

Continue/break statements also just transit to start/end of it accordingly.

A do-while loop performs a check after the iteration and is structured accordingly:

// do { loop } while (condition)
label1:
    loop
label2: // <- continue
    condition
    if (pop()) jump(label1) // <- jump_if
label2: // <- break

The loop body/condition order is swapped (and condition transits back to the loop), and continue statements jump forward to the condition, but otherwise it's fairly alike.

A for-loop is a slightly fancier version of the while-loop.
Some people argue that for this reason you can do without them, but it's the fact that doing continue still runs the post-statement that makes these valuable.

// for (init; condition; post) loop
init
label1:
    condition
    if (!pop()) jump(label3)
    
    loop
label2: // <- continue
    post
    jump(label1)
label3: // <- break

As far as the actual implementation goes:

  • We add new keywords to the parser
  • We build the AST as per earlier outlined rules.
    while and do-while loops are as straightforward as it gets.
    for loop has a bit of extra code to allow () around init/cond/post, and to allow semicolons
    (else you have for (i = 0 i < 5 i = i + 1), which is... strange)
    For the same reason, we shall permit optional semicolons after statements in general.
  • Building break/continue is a little more involved for the sole fact that you should not be able to use them just anywhere - only inside a loop. So we are going to have a pair of flags indicating whether both can be used, and a script that saves the previous values, flips them on, builds a statement, and then restores them.
  • Compilation will then make use of this - break/continue statements generate jump-actions with dummy offsets (-10 and -11) and then we replace them with real ones once we know where the points of interests are.
  • Runtime only needs a jump_if action and that's it.

Comments

Firstly, protip: comments aren't real.

That is, they are stripped during parse phase and have no influence on compilation.

But still nice to have! And not too hard to do. So we'll have those.

For single-line comments we check for // and skip till the end of the line.
For multi-line comments we check for /* and skip till we find */ or hit the end of the input.

The end?

Downloadable versions of the sample project can be found on itch.io or GM Marketplace.

Source code is also available in the in the repository - part-2 branch contains everything up until the end of this part of tutorial, and master branch contains any newer tweaks.

Have fun!

Related posts:

Auto-adjusting screen area on Wacom tablets

$
0
0

(mouseover/click to play GIF) Script in action. See: Full-sized version; that wallpaper

If you have a display-less Wacom tablet (Wacom One/Intuos/etc.) and a large enough monitor (or monitors), mapping screen space to tablet can be a bit bothersome. But that can be fixed.

A bit of history

At some point during the past few years I got a new laptop, and a tiny Wacom tablet (One S):

Then I added a monitor for a bit more screen space:

Moving windows to the "tablet's" screen is a bit awkward, but Win+Shift+Left is a thing.

Later I added another monitor:

A triangular screen layout is pretty efficient in terms of how much you have to move the mouse around (other option: macros), but screen switch hotkeys get a bit awkward.

Later I added ano-- ah, hold on, apparently the laptop only has two display ports, and one of them isn't even HDMI 2.0. After considering the options, I settled on a specific 4K TV that's basically just a big monitor (2160p @ 43" has same density as 1080p @ 21.5"):

This was mostly a good idea, except you can't put something in center of the bigger screen with hotkeys alone, and most applications don't position their windows consistently enough for per-application options in Tablet Properties to be of good use.

So I thought - could I, by a chance, trick Tablet Properties into auto-updating the Screen Area to match the currently focused window? Then I wouldn't even have to switch between mouse/tablet most of the time.

As you might suspect, the answer was "yes".

The idea

The tricks outlined here are made possible thanks to this specific window:

It is what you use to define screen area yourself, spotting a screenshot control for quickly defining an area through click & drag, a button to point at exact TL/BR corners (showing a full-screen overlay to do so), and, a recent addition - a set of fields for entering coordinates by hand.

The later are exactly what we need, and we can have the script operate them, with a few gotchas:

  • Coordinates may not exceed desktop bounds
  • Right cannot be less than Left, ditto Bottom vs Top
  • Value changes take effect instantly, but require simulating focus correctly
    (activate field - change value - deactivate field)
  • Do not change the region while the mouse/stylus is being held down
    (else dragging objects between windows can get weird)

Fortunately, AutoIt can help us with most of these, and we can use WinAPI functions for the rest.

The code

As per above, the implementation is relatively straightforward, although the boilerplate code does add up a bit.

#include <WinAPI.au3>
#include <WindowsConstants.au3>
#include <GUIConstantsEx.au3>
#include <SendMessage.au3>
#include <Math.au3>
;
Const $margin = 120 ; in px, how far to let stylus move outside the window bounds
Const $minWidth = 600 ; in px, minimum window width (not to require large movements for tiny windows)
Const $minHeight = 300 ; in px, minimum window height (ditto)
Const $pollRate = 5 ; in ms, lower is more likely to notice short taps
;HotKeySet("{pause}", "DoExit") ; keyboard shortcut to exit script, if you need it
; if these ever change, you would find them via AutoIt Window Info:
Const $idY1 = 53
Const $idY2 = 54
Const $idX1 = 55
Const $idX2 = 56
;
Func Clamp($x, $a, $b)
	If ($x < $a) Then Return $a
	If ($x > $b) Then Return $b
	Return $x
EndFunc
;
Func DoExit()
	Exit 0
EndFunc
;
Global $desktopX1 = _WinAPI_GetSystemMetrics($SM_XVIRTUALSCREEN)
Global $desktopY1 = _WinAPI_GetSystemMetrics($SM_YVIRTUALSCREEN)
Global $desktopW  = _WinAPI_GetSystemMetrics($SM_CXVIRTUALSCREEN)
Global $desktopH  = _WinAPI_GetSystemMetrics($SM_CYVIRTUALSCREEN)
Global $desktopX2 = $desktopX1 + $desktopW
Global $desktopY2 = $desktopY1 + $desktopH
Global $desktopRect[4] = [$desktopX1, $desktopY1, $desktopW, $desktopH]
;
Global $hwnd = 0x0
Global $hfX1, $hfX2, $hfY1, $hfY2
Func IsMouseDown() ; LMB/RMB/MMB
	Return BitAND(_WinAPI_GetAsyncKeyState(0x1), 0x8000) <> 0 _
		Or BitAND(_WinAPI_GetAsyncKeyState(0x2), 0x8000) <> 0 _
		Or BitAND(_WinAPI_GetAsyncKeyState(0x4), 0x8000) <> 0
EndFunc
;
Func SetRegion_1($h0, $h1, $h2, $v)
	_SendMessage($h1, $WM_SETFOCUS, $h0, 0)
	ControlSetText($hwnd, "", $h1, String($v))
	_SendMessage($h1, $WM_KILLFOCUS, $h2, 0)
EndFunc
Func SetRegion($x1, $y1, $x2, $y2)
	Local $h0 = $hwnd
	; so, we can't enter x1 that is >x2 and what a trouble
	If ($y1 < Int(ControlGetText($hwnd, "", $idY2))) Then
		Local $h1 = $hfY1, $v1 = $y1, $h2 = $hfY2, $v2 = $y2
	Else
		Local $h2 = $hfY1, $v2 = $y1, $h1 = $hfY2, $v1 = $y2
	EndIf
	If ($x1 < Int(ControlGetText($hwnd, "", $idX2))) Then
		Local $h3 = $hfX1, $v3 = $x1, $h4 = $hfX2, $v4 = $x2
	Else
		Local $h4 = $hfX1, $v4 = $x1, $h3 = $hfX2, $v3 = $x2
	EndIf
	;
	SetRegion_1($h0, $h1, $h2, $v1)
	SetRegion_1($h1, $h2, $h3, $v2)
	SetRegion_1($h2, $h3, $h4, $v3)
	SetRegion_1($h3, $h4, $h2, $v4)
	; for some reason we have to do a dance after the last input 
	_SendMessage($h2, $WM_SETFOCUS,  $h4, 0)
	_SendMessage($h2, $WM_KILLFOCUS, $h0, 0)
	_SendMessage($h0, $WM_SETFOCUS,  $h2, 0)
EndFunc
;
Local $curr = 0, $prev = 0
Local $noRect[4] = [0,0,0,0]
Local $cr[4] = [0,0,0,0]
Local $isFullScreen = False
Local $delayUpdate = False
Local $nullPtr = Ptr(0)
Local $noWindowNote = True
While 1
	; Config window [re-]opened?
	Local $hwnd_ = WinGetHandle("Portion of Screen")
	If ($hwnd_ == $nullPtr) Then 
		If ($noWindowNote) Then
			ConsoleWrite("Please open 'Mapping > Screen Area > Portion of Screen' from Wacom Tablet Preferences" & @CRLF)
			$noWindowNote = False
		EndIf
		Sleep($pollRate)
		ContinueLoop
	EndIf
	If ($hwnd <> $hwnd_) Then
		$hwnd = $hwnd_
		ConsoleWrite("Config window changed to " & $hwnd & @CRLF)
		For $attempt = 1 To 50 ; window appears before controls for some reason
			Sleep(25)
			$hfY1 = ControlGetHandle($hwnd, "", $idY1)
			$hfY2 = ControlGetHandle($hwnd, "", $idY2)
			$hfX1 = ControlGetHandle($hwnd, "", $idX1)
			$hfX2 = ControlGetHandle($hwnd, "", $idX2)
			If ($hfY1 <> $nullPtr And $hfY2 <> $nullPtr And _
				$hfX1 <> $nullPtr And $hfX2 <> $nullPtr) Then ExitLoop
			If ($attempt == 50) Then ConsoleWrite("Couldn't discover controls" & @CRLF)
		Next
		ConsoleWrite("Controls: "&$hfY1&", "&$hfY2&", "&$hfX1&", "&$hfX2&@CRLF)
	EndIf
	; 
	$curr = WinGetHandle("[ACTIVE]")
	; Try to walk up the window's parents-chain so that your area doesn't readjust
	; simply because you mouseovered a floating panel in Paint.NET:
	While ($curr <> 0)
		Local $par = _WinAPI_GetWindow($curr, 4) ; GW_OWNER
		If ($par <> 0) Then
			$curr = $par
		Else
			ExitLoop
		EndIf
	WEnd
	; figure out whether we need to update
	$update = False
	If ($curr <> $prev) Then ; active window changed?
		$prev = $curr
		;$update = True
		Local $currClass = _WinAPI_GetClassName($curr)
		Local $currTitle = WinGetTitle($curr)
		If ($currClass == "Windows.UI.Core.CoreWindow") Then
			$isFullScreen = ($currTitle == "Start" Or $currTitle == "Search")
		Else
			$isFullScreen = False
		EndIf
		$cr = $noRect
		;ConsoleWrite("Class:" & $currClass & @CRLF)
		;ConsoleWrite("Title:" & $currTitle & @CRLF)
		;ConsoleWrite("Full:" & $isFullScreen & @CRLF)
		; same as below:
		If (IsMouseDown()) Then $delayUpdate = True
	Else
		Local $nr = $isFullScreen ? $desktopRect : WinGetPos($curr)
		If (Not IsArray($nr)) Then
			; we have no permit to even poll the size of this window
		ElseIf (IsMouseDown()) Then
			; allowing to change target area while mouse is held down causes a variety of side effects
			; (such as causing you to drag objects not where you wanted)
			$delayUpdate = True
		ElseIf IsArray($cr) And ($cr[0]<>$nr[0] Or $cr[1]<>$nr[1] Or $cr[2]<>$nr[2] Or $cr[3]<>$nr[3]) Then
			$update = True
			$cr = $nr
		EndIf
	EndIf
	;
	If ($update And IsArray($cr)) Then
		If ($delayUpdate) Then
			ConsoleWrite("Wait+")
			$delayUpdate = False
			Sleep(150)
		EndIf
		;ConsoleWrite("Update!" & @CRLF)
		Local $cx1 = $cr[0]
		Local $cy1 = $cr[1]
		Local $cw = $cr[2]
		Local $ch = $cr[3]
		Local $cx2 = $cx1 + $cw
		Local $cy2 = $cy1 + $ch
		; apply min size:
		If ($cw < $minWidth) Then
			$cx1 = $cx1 + Floor(($cw - $minWidth) / 2)
			$cw = $minWidth
			$cx2 = $cx1 + $cw
		EndIf
		If ($ch < $minHeight) Then
			$cy1 = $cy1 + Floor(($ch - $minHeight) / 2)
			$ch = $minHeight
			$cy2 = $cy1 + $ch
		EndIf
		; apply margin:
		$cx1 = Clamp($cx1 - $margin, $desktopX1, $desktopX2)
		$cx2 = Clamp($cx2 + $margin, $desktopX1, $desktopX2)
		$cy1 = Clamp($cy1 - $margin, $desktopY1, $desktopY2)
		$cy2 = Clamp($cy2 + $margin, $desktopY1, $desktopY2)
		; and update:
		;ConsoleWrite("SetRegion T"&$cy1&" B"&$cy2&" L"&$cx1&" R"&$cx2&@CRLF)
		SetRegion($cx1, $cy1, $cx2, $cy2)
	EndIf
	;
	Sleep($pollRate)
WEnd

How to use this

  1. Firstly, you'll want to install AutoIt if you don't have it installed yet.
  2. Then, create a new au3 script in it, paste the code there, and save it somewhere.
  3. Open Wacom Tablet Properties, switch to Mapping, tick the "Force Proportions" checkbox
  4. Pick "Portion" in "Screen area" to open the "Portion of screen" window.
  5. Run the script (F5)

For subsequent uses, re-open the script and repeat steps 4-5;

You may compile your script into an executable for convenience and/or edit the few constants at the beginning for padding or minimum window size;

Having Start/Search menu open or clicking the desktop will map to the whole desktop for ease of switching active windows;

If you know yourself some AutoIt/BASIC, you can tweak the script to use custom logic with SetRegion (for instance, instead of following the active window, have keyboard shortcuts to snap to different regions of screen and/or different monitors).

Known issues

  • A few applications (most notably, Steam client) misuse windows in novel ways while not indicating their relationships to main window correctly, causing the script to readjust region to a popup window.
  • Using plenty of padding on windows close to screen edges can cause the area to be off-center on the tablet as Tablet Properties does not allow screen area to exceed desktop bounds.
  • Doesn't play well with per-application profiles because you cannot open multiple "portion of screen" windows at once (so you'd have to keep per-app profiles mapped to specific areas)


Have fun!

Related posts:

FAQ: On name, avatar, and [other] Yal

$
0
0

A collection of answers on identity subjects.

Name

While I had semi-stable internet access from early 2008 or so, my current nickname hadn't been in use before mid-2009.

During that period, my strategy on picking nicknames was to use my first name and then slam random letters/digits after it until finding an available combination.

In doing so I found that:

  • People are really good at misspelling both my first and last name.
    "Vadim" can become Vlad, Vladim*, Vladislav, Vadimir*, Vadmin*, and so on
    (names that aren't real at all are marked with *).
    People's struggle with my last name is a little easier to explain - there are simply multiple variations of the same last name with added letters, soft signs, or apostrophes.
  • Some people didn't realize that my name is a real name at all.
    Time to time people would contact me about GameMaker examples/tutorials made by other Vadims. Sometimes I would proceed to look at the thing to answer their question without correcting them. Sometimes people would later contact me, thanking again and apolizing for asking the wrong person. Sometimes they wouldn't.

So I thought to change up the name with criteria being around:

  • Must be unique (0 exact match search results)
  • Must consist of [semi-]common English words so that it's harder to misspell.
  • Must be slightly abstract/mysterious 'cause I was going to get my name known for making abstract games.

After some considerations I settled on "YellowAfterlife", which seemed memorizable enough and didn't seem to be in use by anyone ever, all with a minor (later discovered) humorous caveat being that, if a game/app/service clamps display name length at 8 characters, I am suddenly known as YellowAf (and you might already know what AF usually stands for).

Also, you might be wondering where are those abstract games that I was going to make.

You see, I got really distracted with writing netcode and tools and such.
And then I graduated from university, so I had to prioritize making things that made it more likely for me to make a living. So, as of 2019, the list of my "abstract" games is around:

  • One and Light (2011)
    The originally intended kind of "abstract" - it is a small game, but had bits of dialogue, interesting levels, and gameplay mechanics. I'm meaning to revisit this idea sometime.
  • Polygravity (2011)
    I was able to get a nice art style going on, but, ultimately, the game is just a quirky platformer.
  • Stories at the dawn (2015)
    I find that over time my games tend to lean towards more light-hearted themes, and this short game is a good example.

There'll be a few more notes about naming after a small interruption,

Avatar

Common assumptions for what my avatar depicts include dinosaurs and birds.

The avatar originates from a random doodle I did during some university lecture in 2014:


"occasionally" is a pretty weird word, if you consider

If you were to ask me what this is supposed to mean, I would have no answer, but I do recall the teacher walking by me, looking at my notes (which had a column reserved for doodles), and asking if anything I draw makes any sense, to which I replied with a soft "no".

I had quickly established that the general shape of the dinosaur looked pleasant (1), but proportions made approximately no sense - it could very well have been a horse or a dragon.
With any kind of accuracy out the window anyway, I decided to try drawing some kind of hand with a friendly gesture.
After briefly trying to sketch out a wing (2), it became apparent that I had no idea how to draw one without additional detail, let alone convince a gesture with claws on the end of it.
Then I tried to do a Rayman-styled floating hand (3), only to realize that I have no idea as to how this is supposed to work look pointy fingers.
Then I came with the current pointy hand idea (4), which doesn't seem to make any sense in terms of proportions, but looks good, so I settled with that.

Over time this pointy thumbs-up had grown to be a common emoticon for Slack/Discord chats that I visit, and later a de-facto logo of mine.

I time to time consider whether to switch up avatars, but this one is pretty recognizable by now.

YAL and Yal

My name abbreviates to YAL (or, if you are feeling pedantic, YA), but, did you know that there's also a person that goes by Yal? In fact, we have a visible overlap in our activities:

  • We both make games
    (well, Yal makes games, while I mostly work on games instead of making my own)
  • We both use GameMaker as one of primary tools for projects
  • We both make GM assets
    (I mostly make extensions while Yal makes engines for core mechanics)

People time to time ask me if we are aware of each other.

Yal certainly predates me in use of name, and it's fair to say that we've been vaguely aware of each other for a while (as we both frequented the official GameMaker forums).

But, things only really got confusing when both of us were appointed as forum moderators at once - people had already taken up calling me "YAL" or just "yal" by then, and suddenly a realization that there are two "yal"s.

Rest assured, for a little while people would tell me that they've seen my engines (while they in fact meant Yal's) and thank Yal for my tutorials. Nowadays things are calmer in this regard.

Related posts:

On GameMaker game decompilation

$
0
0

On GameMaker: Studio game decompilation

It seems that questions about whether (and if so, to what extent) GameMaker: Studio and GameMaker Studio 2 games can be decompiled are being asked at a constant pace, and yet there are still no resources to clear up these questions. So I've decided to make a small post on the matter.

(originally published in May 2015, updated in 2019)

How it was in GM≤8.1

Older versions of GameMaker stored game data in a way that remains common to Lua game engines these days - that is, all game data was compressed and encrypted, but, in the end, stored without substantial post-processing.

And thus, eventually someone had the persistence to reverse-engineer the runtime's binaries, and wrote a tool that would reassemble the data from an executable to a largely-valid source file. Needless to say, such a thing was valuable if you were trying to recover lost work, and a bit of a disaster if you were working on a game with any online features or didn't anticipate a small chance of your work being stolen outright.

To protect against that, people then made obfuscators (to one of which I even contributed some methodology) and tools anti-decompilers (tools specifically subverting the expected executable structure), and things were kind of okay... so long as you were aware of possibilities.

How it was in GM:S

Among the key changes in GameMaker: Studio was the rewrite of how code was being compiled - now, instead of storing it in text-y format, it was compiled to bytecode, which made for smaller file sizes, higher performance, and not unnecessary information like comments or names of constants.

When targeting HTML5, GameMaker would compile the abstract syntax tree to JavaScript, combine that with runtime code, and run it all through a minifier-obfuscator to reduce file size and strip out any unnecessary information.

All this at a slight downside of it no longer being possible to execute arbitrary code from a string, but that's entirely fixable as of me updating this post.

Later on, YYC (YoYoCompiler) was introduced. YYC compiles GML to equivalent C++ code and then through an appropriate C++ compiler (most often, Clang), allowing to take advantage of C++ specific optimizations and to have the game code interface with runtime directly, all for great effect on performance and with some pleasant security implications (more on this later).

So, let's further look into what risks each of three output types (non-YYC, YYC, HTML5) has.

Non-YYC

Can it be decompiled?: To an extent.

It's no news that bytecode is by definition more decompile-able than native code, moreso with common stack-based virtual machines - as the code unrolls into instructions somewhat predictably, the process is also reversible - so long as someone is willing to sit through and write matchers for all possible instruction patterns.

GameMaker's reliance on numeric constants means that

draw_sprite_ext(spr_some,1, 100,100, 1,1, 45, c_white, 1.0);

is compiled as if it was

draw_sprite_ext(4, 1, 100, 100, 1, 1, 45, 16777215, 1);

(replacing all constants by their values in process)

Overall, the data that can be scrapped includes general code structure to the point of variable names (as they are shown in errors), but will not include resource names when used in code, constants, macros (which are expanded), or enums.

Additionally, modern versions of GM compiler will cull branches of if-expressions if the condition can be evaluated compile-time. For example,

#macro is_debug true
#macro Release:is_debug false
if (is_debug) {
    ver = "DEBUG";
    // ... some debug code
} else ver = "RELEASE";

would compile to just

ver = "RELEASE";

on Release configuration.

To counter this, tools could be developed to obfuscate most names (except ones referencing built-in functions and variables) and/or manipulate bytecode and file format to require manual cleanup.

While the later is a complicated subject, obfuscation is pretty simple. In fact, you could even do it by hand if you wanted:

Obfuscating variable names in a GameMaker: Studio game by hand
(mouseover/click to play GIF)

At this time, the only tool that offers anything resembling adequate VM decompilation is UndertaleModTool. While it isn't going to make you a project file, it is perfectly suitable for recovering lost scripts (though also see Recovering Lost Work) or making mods for games.

While you might seek for more tempting offers, remember to be careful, for else, as it goes with these delicate topics, you may find yourself downloading (or even having paid for) a prank at best, or malware/spyware at worst.

YYC

Can it be decompiled?: Unlikely.

As if C/C++ decompilation wasn't a complicated enough topic as-is, further decompiling that to non-C++ code is a really strange topic - to the point that I cannot recall any examples off-hand.

While it is still possible to scrap a list of script/object names, or, with substantial effort, detour a script for modding purposes, the nature of YYC makes it incredibly difficult to tell what was anything was supposed to do.

JS/HTML5

Can it be decompiled?: Perhaps, but why

On one hand, JS code generated from GML is the closest-looking thing to the actual GML, and minified JS is still JS, not machine code.

But then some GML-specific bits of syntax (like with-loops) compile more than one JS structure, and everything is minified/obfuscated, so you face somewhat similar problems to YYC.

So, with enough effort, you could figure out which 3-letter function is supposed to do what, and non-code assets are easier extracted than with other platforms (again, for browser-based nature), but it's hard to think of a valid reason of doing this to yourself.

Resources

Can graphics/audio be extracted?: Absolutely

People seem surprised but I'm not sure what they expect - if something is being drawn on screen or played through audio device, it can be intercepted and extracted at a variety of points on the way there. This had been the case for a very long time now.

Various measures can be taken about this but are generally not worth the hassle.

Shaders

As one exception from above rules, we have shaders.

GM:S/GMS2 will compile shaders on game start, and, as result, will store them as-is.

Therefore it is not a good idea to, for some reason, store sensitive information inside your GLSL/HLSL code.

Some people argue (and loudly so) that GM should be encrypting the code, but we know exactly how much that helps - see: GM≤8.1, or pretty much any case with anti-tamper/anti-piracy measures - once a single person cracked the algorithm, it is instantly worthless, and also all existing works can now be extracted.

In conclusion

As time goes on, tools continue to evolve.

While, ultimately, anything that runs on end user's hardware can be reverse-engineered at some point, having baseline adequate security (through use of YYC) is a good thing for games where this matters.

Even so, for games with online features (like multiplayer or leaderboards), you should always have server-side validation to prevent packet/request tamper.

And, never forget - the ultimate goal in making games/software is usually to finish the thing, not come up with the most sophisticated counter-measures.

Have fun!

Related posts:

A summary of my GameMaker assets

$
0
0

This post serves as a collection of descriptions and links to various GameMaker-related assets and extensions that I have published over time.

Back when publishing it in 2017, I thought it was longer than expected, but it had since grown about twice in size. At least the blog has that "navigation" sidebar now.

Also should answer the "how do I support your work" question that some readers have.

Mini-FAQ

How do you price your assets?

Pricing is pretty straightforward, being based on how long it takes to make and how popular I can expect it to be - after all, the ideal case scenario is to make your time investment back in sales.

Consequently, this also means that some ideas stay on "todo" list for a long time if they require a substantial amount of time to make but are too exotic to sell at an anyhow accessible price point.

How are some of these free?

I release assets as free/pay-what-you-want if:

  • Someone paid me to make the thing and was happy with having it publicly available as well (e.g. Steam/GOG/Discord wrappers)
  • I made the thing for some personal project and determined that there shouldn't be substantial maintenance costs if I release it for free.

Why do some assets cost more on GM Marketplace than on itch.io?

By default, itch.io takes a 10% cut of each sale.
(you can adjust this and I have mine at 11%, but let's use 10% for simplicity)

GM Marketplace, on other hand, uses an "industry standard" 30% cut.

So MP assets are priced accordingly (+28%, rounded to nearest good-looking number) and I receive about the same amount of money no matter whether you bought my assets on MP or itch.io...

... unless you bought them in Steam version of GameMaker with Steam wallet money! Then Valve also takes a 30% cut, which, combined with YYG's cut and VAT, lands us at just about 35-45% of transaction making it to me. Pretty cool, huh? And then there are taxes too.

Essentials

GMLive.gml (live coding)

(mouseover/click to play GIF) If you're wondering, that's just how the light theme in GMS2 looks.

GMLive is generally regarded as my best asset to date - it lets you update code, artwork, and rooms while the game's running - you mark things for live update before running the game for the first time (generally through a single function call), and then saving the resource will automatically update them in-game.

If you are doing something that requires quick iterations (UI, animation, timing, procedural generation, parameters), GMLive can turn countless minutes of waiting into literal seconds, saving hours in the end.

The extension can be used locally, when running on a remote machine, on mobile devices, and even when running in browser (GMS2-only; currently requires running in debug mode or this free extension to support current-script name discovery)

Versions: GMS1, GMS2
Platforms: All (GMS2), All except HTML5 (GMS1)
Price: USD 29.95
Links: blog post · itch.io · marketplace · documentation

GMRoomPack (room loading)

To put it simply, this extension allows you to easily inject GM rooms into other GM rooms while the game is running. This can be used for procedural generation, chunked loading (making it manageable to do massive open-world games in GM), prefab systems, or external loading (DLC/etc.).

The extension also offers a simple JSON-based format for generated data,

Versions: GMS1, GMS2
Platforms: All
Price: USD 4.95
Links: itch.io · marketplace · documentation

Tools for advanced users

(... and people that might not consider themselves advanced users but still make big games)

GMEdit (code editor)

GMEdit is an external code editor for GM:S/GMS2 projects with focus on workflow. With a variety of syntax extensions, keyboard shortcuts, custom themes, and plugin support, the editor allows to write and navigate through code faster than ever.

Versions: GMS1, GMS2
Platforms: [editor runs on] Windows, Mac, Linux
Price: Free
Links: itch.io · source code · documentation

sfGML (classes, types)

While GML is a perfectly good language for writing the actual game code, attempting to use the language for complex systems offers a similar set of problems and challenges as you would have in JavaScript - the language lacks compile-time type checking and has its own set of type quirks that can make debugging a mistyped data structure variable a bit of a trouble.

sfGML offers a solution to this problem - it is a compiler for a high-level, strictly-typed language called Haxe, which has classes, interfaces, high-end macros, null safety, and everything else you would or would not expect to have.

And then this wonderful code of yours can be compiled into GML code, thus getting you your compile-time checks and sanity without imposing any runtime overhead that you would have if you packed your system code into native extensions.

You can read a bit more about it on my "works" page.

Versions: GMS1, GMS2
Platforms: All
Price: Free
Links: itch.io · source code · documentation

catch_error (error handling)

Error messages display can be considered one of the weaker points in GameMaker - default error dialog boxes look unappealing, confusing to player, and give no incentive to actually copy the error, having you often hear that the game "had shown an error message and closed" without ever getting to know what the error message was.

This extension addresses that - you can choose how to present the errors (and whether to present them at all), as well as having the option to not have the game close upon encountering majority of "fatal" errors.

Combined with Sentry, you can even have automatic error reporting!

Versions: GMS1, GMS2
Platforms: Windows
Price: USD 14.95
Links: itch.io · marketplace · documentation

RerouteAudio (organize audio files on export)

GameMaker's Windows export has a small but unfortunate flaw: any audio files that were specified to be external ("streamed") are always put into the game's root directory, which, given enough of them, makes it harder to find the game itself in the pile of audio files.

While workarounds existed (like using audio_create_stream or self-extracting archives), these came with either organizational of performance overheads.

This tool addresses that, allowing to auto-sort audio files into subdirectories based on name patterns, subsequently updating the game data to look for them in new places.

Versions: GMS1, GMS2
Platforms: Windows
Price: USD 6.95
Links: itch.io · marketplace

Fixing the built-in functions

Some things might not work like you'd expect them for legacy reasons, but that's a fixable oversight.

Improved INI functions

Built-in functions have a number of minor oversights - from only being able to work with one file to not preserving comments on changes to straight up breaking upon writing multi-line strings. You can see my blog post for more information.

This extension fixes all of that. It preserves comments and structure. It supports escape characters. It can iterate over sections/keys and will not error even if your INI file is actually just garbage data.

Versions: GMS1, GMS2
Platforms: All
Price:: $3.99
Links:: itch.io (has demo) · marketplace · documentation

Improved JSON functions

Akin to above, but addresses flaws in built-in JSON functions instead, which also have a set of issues, even allowing for data access attacks (causing the game to read/modify different data structures than those retrieved from JSON) given a specifically crafted JSON string.

And, much like above, offers an implementation that fixes all of that.

But wait, there's more:

  • You can tell whether a value you'd just read is an array/object/etc.
  • You don't have to manually destroy your JSON objects.
  • Recognizes JSON booleans as a separate type.
  • Decoding is straightforward - if you decode "[1,2,3]", you get an array; if you decode "4.5", you get a real. No "default" keys. [similarly, you can encode any supported value type]

Versions: GMS1, GMS2
Platforms: All
Price:: $3.99
Links:: itch.io (has demo) · marketplace

Prevent window from freezing while dragged

(mouseover/click to play GIF)

Suppose you made an online multiplayer game and now you suddenly find that there's a minor inconvenience happening whenever the player drags the window around - the game freezes while doing so.

The underlying problem goes deep into WinAPI and is not easy to address without causing other side effects, but this extension offers a pleasant workaround - you make your game borderless, and when you do need a border, it is inserted into a blank window that the user can drag around (thus without freezing the game).

Versions: GMS1, GMS2
Platforms: Windows
Price: Free
Links: itch.io

BMFont converter

While GameMaker's font generation algorithm had seen substantial improvements over time, select fonts might still look slightly off. Or you might want to embed one or other effect into your font.

This small tool takes fonts in widely supported BMFont format (generated by BMFont itself or countless other tools) and generates GameMaker font files for them.

Versions: GMS1, GMS2
Platforms: Windows
Price: Free
Links: itch.io

Quality-of-life features

Native cursors

(mouseover/click to play GIF)

Little known fact, drawing your cursor in-game can have anywhere from 0.5 to 2.5 frames worth of latency (on average) depending on GPU settings and double/triple-buffering. The issue becomes furthermore apparent if the player's screen has a higher refresh frequency than your game's framerate.

This extension addresses that, allowing you to utilize system-level cursors, which have as little visual latency as it is technically possible.

Versions: GMS1, GMS2
Platforms: Windows, HTML5
Price: USD 1
Links: itch.io (has demo)

Native mouselock

(mouseover/click to play GIF)

Constraining the mouse is a surprisingly tricky matter - it might be tempting to just window_mouse_set every frame, but in doing so the mouse can still escape the boundaries, as your game code can never run as fast as the mouse is polled (which is up to 2500hz).

This extension addresses that, interfacing with a system-level API to grant you perfect mouselock.

Versions: GMS1, GMS2
Platforms: Windows
Price: USD 1
Links: itch.io (has demo)

Extending games

When your game is finished and popular enough, people might start asking for some form of modding support, and that's a doable thing now.

Apollo (Lua modding)

(mouseover/click to play GIF)

Allows to execute Lua code in GameMaker games, permitting to use it for debugging, mod support, scripting, and numerous other purposes.

You can have Lua access GM functions, scripts, instances, and variables.

With being able to define what should be accessible, this allows for "deep" integrations and high-level scripting API design for GameMaker games.

Comes with a GML->Lua converter to simplify learning process.

Versions: GMS1, GMS2
Platforms: Windows, Mac, Linux
Price:: $14.99
Links:: itch.io (has demo) · marketplace · documentation

TXR (GML-ish modding)

TXR is a demo project for my series on writing custom interpreters. It lets you compile and run snippets of GML-like code.

This can be used as a replacement for execute_string, for small-scale modding support, dialogue systems, or numerous other purposes.

You can choose which scripts to expose to interpreted code, have it access instance variables, process arguments, pause/resume execution, save/load execution state, inspect execution state, etc.

This is obviously a little less fancy than having Lua, but it does work on all platforms.

Versions: GMS2
Platforms: All
Price: Free
Links: Documentation · itch.io · marketplace · source code
Posts: part 1 · part 2

mod.io (mod platform)

A wrapper for the universal mod platform from creators of ModDB.

Versions: GMS1, GMS2
Platforms: All
Price: Free
Links: source code

Platform integrations

Steamworks.gml

Expands GameMaker's set of Steam-related functions to include Steam P2P networking, Steam Matchmaking, and related APIs.

Versions: GMS1, GMS2
Platforms: Windows, Mac, Linux
Price: Free
Links: itch.io (example) · source code · documentation

Steamworks.gmk

Offers a set of basic Steam-related functions (stats, achievements) for games made with older versions of GameMaker.

Versions: GM5..GM8.1
Platforms: Windows
Price: Free
Links: source code

GOG.gml

A wrapper for achievements, stats, and leaderboards for GOG's Galaxy SDK.

Versions: GMS1, GMS2
Platforms: Windows, Mac
Price: Free
Links: itch.io (example) · source code · documentation

Discord.gml

A wrapper for Rich Presence, P2P networking, and matchmaking from Discord store's Game SDK.

Versions: GMS1, GMS2
Platforms: Windows (should build for Mac/Linux fine though)
Price: Free
Links: source code

GMS1-specific

These extensions address oversights that had since been corrected in GMS2

Buffer (de-)compression

Utilizes zlib to add equivalents of GMS2 buffer_compress/buffer_decompress functions to GMS1.

Can also be used in GMS2 to utilize different compression levels.

Versions: GMS1, GMS2
Platforms: Windows
Price: Free
Links: itch.io · marketplace · documentation

Non-sandboxed filesystem

While GMS2.2.3 had finally introduced the option to disable sandboxing on desktop platforms, preceding versions (and GMS1) do not have such a privilege.

This extension addresses that, offering a wide range of replacements for built-in functions that do not mind the sandboxing rules.

You can read and write files in the game directory, My Documents, or wherever else your heart desires (and OS allows).

Is fast, compact, and has zero dependencies. Comes with C++ source code.

Versions: GMS1, GMS2
Platforms: Windows
Price: USD 3.99
Links: itch.io (has demo) · marketplace · documentation

Convert DnD to GML code

If you have ever tried importing a GMS1 project with DnD blocks into GMS2, you might have noticed that the generated code for them is... not very readable. Let's put it that way.

This tool automatically converts DnD blocks into code using hand-picked rules, which makes for far more readable code and preserved comments / structure.

Versions: GMS1, GMS2
Platforms: Windows
Price: Free (web), USD 3 (native)
Links: itch.io

Purpose-specific

I appreciate your dedication to scroll this far through this post.
The following are useful for specific occasions and close to useless for an average game.

Take input from multiple mice/keyboards

Ever wish your game could differentiate between different mouse/tablet/keyboard devices like it can differentiate between gamepads? This might seem impossible because of how uncommon such functionality is in games, but it is not - it's rather that the approaches required are so obscure that they are barely ever implemented.

This extension goes through that trouble and wraps these in a convenient package.

Versions: GMS1, GMS2
Platforms: Windows
Price: USD 14.95
Links: itch.io (has demo) · marketplace · documentation

Generate ZIP archives

This extension allows your GM games to generate perfectly valid ZIP archives.
Complimentary to the build-in zip_unzip function.

Versions: GMS1, GMS2
Platforms: All
Price: Free
Links: itch.io · source code

Flash the game's button in taskbar

(mouseover/click to play GIF)

This tiny extension lets you flash the game's taskbar button to signify something happening or demand attention from the player. Handy for letting the player know that something happened while the game is not focused.

Versions: GMS1, GMS2
Platforms: Windows
Price: USD 1
Links: itch.io (has demo) · marketplace

Show a progress bar inside the game's button in taskbar

(mouseover/click to play GIF)

This similarly tiny extension allows you to display a progress bar of one or other kind inside the game's taskbar button.

Did you know that some people use GameMaker to make software, and not just games?

Versions: GMS1, GMS2
Platforms: Windows
Price: USD 1
Links: itch.io · marketplace

Take screenshot of part/whole desktop

(mouseover/click to play GIF)

Suppose you decide to make a screensaver, or a GIF recorder, or one of those games that let you use your desktop background. Perhaps a better snipping tool? Rest assured, you want to be able to do something about the pixels not just inside your game window.

This extension gives you that - the ability to capture arbitrary pixels of screen - even in multi-monitor setups. And a couple other helper functions too.

Versions: GMS1, GMS2
Platforms: Windows
Price: USD 4.95
Links: itch.io (has demos) · marketplace · documentation

Run/forbid window commands

Suppose you want to show "are you sure you want to exit" popup, or minimize a borderless window when clicking a custom "minimize" button, or similar things. With this extension, you can.

Versions: GMS1, GMS2
Platforms: Windows
Price: Free
Links: itch.io

External log window

A tiny extension and a server helper that let you forward debug messages to a separate window instead of relying on GM's output or printing to files.

Versions: GMS1, GMS2
Platforms: Windows
Price: Free
Links: itch.io

HTML5-specific

The following either only work on HTML5, or bridge inconsistencies between native and HTML5 targets.

Allow Data URIs

This small extension enables majority of built-in functions to accept base64 (e.g. data:image/png;base64,...), which makes for one less problem when implementing interoperation between GML code and JavaScript.

Versions: GMS1, GMS2
Platforms: HTML5
Price: Free
Links: itch.io

Allow pasting text/files

Allows your game to listen to browser paste events and grab data from them.

Versions: GMS1, GMS2
Platforms: HTML5
Price: Free
Links: itch.io

Eliminate the stall on first file read

You know that thing where your game freezes for a moment when you first read a nonexistent file via default (synchronous) functions? This is because it must check if it happens to exist in game directory on the server first.

This extension essentially lets you skip that check if you know that the file cannot possibly exist in the game directory (e.g. it is a save file)

Versions: GMS1, GMS2
Platforms: All
Price: Free
Links: itch.io

HTML5 loading screen

(mouseover/click to play GIF)

Simple, configurable, and even supports animations.

Versions: GMS1, GMS2
Platforms: HTML5
Price: Free
Links: itch.io · blog post

Screenshot save dialog

A tiny extension that adds a unified API for letting the player save screenshots of the game to their computer with a conventional file picker.

Versions: GMS1, GMS2
Platforms: Desktop, HTML5
Price: Free
Links: itch.io

High-precision get_timer

Makes get_timer function actually have microsecond precision as it is supposed to.

You want this if you are doing any performance comparisons.

Note that most browsers now "fuzz" the timings slightly to prevent side-channel attacks, so make sure to do your thing more than once before comparing timings.

Versions: GMS1, GMS2
Platforms: HTML5
Price: Free
Links: itch.io

debug_get_callstack

As of GMS 2.3.3, debug_get_callstack only returns original script names when ran in debug mode. This extension delpoys a small workaround that causes script names to be reported in non-debug as well.

Versions: GMS1, GMS2
Platforms: HTML5
Price: Free
Links: itch.io

PointerLock

As you might know, browsers do not allow the visitor's cursor to be moved about - they instead offer a "PointerLock" API, which locks the mouse at its current position and reports "movement offset" events while it is locked.

This extension offers a unified API for HTML5 and native platforms.

It can also be used in combination with above extension to ensure that the player cannot click outside the game window on Windows if they move the mouse really fast.

Overall intended for games that use mouse movement for mouselook (e.g. first-person games) / other non-point-at purposes.

Versions: GMS1, GMS2
Platforms: Desktop, HTML5
Price: Free
Links: itch.io

Conclusion

It would appear that I made a lot of stuff over time.

Related posts:


Polling additional mouse buttons

$
0
0

a line with question marks pointing at side buttons of a mouse

Computer mice these days tend to have additional buttons on sides/ and people periodically wonder whether they need to do anything particular to poll these buttons.

Technical

Most operating systems allow mice to have 5 buttons total - left, right, middle (wheel click), "X1", and "X2", alongside with horizontal and vertical wheel events.

It is a little challenging to find any specific information on intended use of X1/X2 buttons (as far as software goes), so we can only assume that they were intended to be general-purpose.

Implementation

  • WinAPI (events)
    Use WM_XBUTTON* events and check wParam to see which button it is.
  • WinAPI (raw)
    Use GetAsyncKeyState or GetKeyboardState with VK_XBUTTON1 or VK_XBUTTON2.
  • GameMaker (Windows-related platforms)
    keyboard_check_direct(5) and keyboard_check_direct(6), respectively (as it calls GetAsyncKeyState).
  • GameMaker (rest of the platforms)
    You'll probably need a native extension.

However!

As support for X1/X2 buttons (and even horizontal scrolling outside of native components) remained considerably uncommon through the years, most additional buttons on mice simply emit keyboard events instead - either directly or with help of a driver.

For example, if your mouse has a "Back" button on it, that is most likely emitting a VK_BROWSER_BACK key press, or even simply Alt+Left.

So, what you really want is configurable controls/keyboard shortcuts (without restricting the range of allowed keys), although even otherwise, increasingly often mice come with configuration software that allows to remap the buttons, sometimes even on per-application basis:


Different mouse configuration apps: MSI, Redragon, Microsoft


So that's about all on this matter, really.

Related posts:

GameMaker: Fixing the data structure functions

$
0
0

In this third-of-a-series post, I go over existing issues in GameMaker's data structure functions (some of which you might not have been aware of), and how to fix them.

Problems

Through series of coincidences, GameMaker data structures currently slightly resemble direct memory access in C++, with similar implications.

[If you want to reproduce these samples, run them in an empty-ish project on game start]

Wrong DS type access

Suppose you make a map and a list, and then you accidentally use

var a = ds_map_create();
a[?"hi"] = "oh no";
var b = ds_list_create();
show_debug_message(b[?"hi"]); // map function used on a list.

If you get lucky, you get an error. If you don't get lucky, you get "oh no". Or maybe undefined.

Did you know that running the game in debug mode reserves a couple of ds_map slots and can prevent you from getting the same sequence of errors when you are trying to reproduce them? It's good fun.

Non-data-structure access

Suppose you make a mistake and accidentally attempt data structure operations on an arbitrary number, or even a string containing a number (after 2.2.3 consistency changes). Since data structures are currently referenced by index, there's always a chance that, with some luck (or rather, absence thereof) you'll hit a valid data structure ID and thus get/set something you absolutely weren't meaning to.

Consider the following:

var a = ds_map_create();
var b = 0.3;
b[?"oh"] = "no";
var c = "0.7"; // new!
c[?"now"] = "this";
show_debug_message(json_encode(a));

So, if a was assigned index 0, you get { "oh": "no", "now": "this" }.

Some of the hardest-to-debug bugs that I've encountered originated from issues like this.

Access on destroyed structures

Did you know? For practical reasons (keeping memory usage at bay), GameMaker will reuse data structure indexes.

This is good for performance reasons, but not so good if you forget to "unassign" a variable after freeing the contained structure:

var a = ds_map_create();
ds_map_destroy(a);
var b = ds_map_create();
a[?"oh"] = "no";
show_debug_message(json_encode(b));

This will reliably produce { "oh": "no" }.

And, as an even more unpleasant side effect, attempting to destroy a a second time will indeed destroy the data structure index occupied by b, making for some confusing "data structure does not exist" errors.

Tracking down memory leaks

Admittedly not purely a GameMaker problem, but figuring out where are you forgetting to destroy data structures can be a little messy - while you can write wrapper scripts for _create and _destroy functions, these will not account for destroying nested structures (when marked via ds_*_mark_*).

Some language tools (like modern JS debuggers) offer tools for producing heap dumps (which show how much memory everything takes up, and where things were made from), but GML isn't at that point just yet.

To summarize

As you can see, use of indexes for data structures produces a separate set of issues and side effects.

The issue is acknowledged by YoYo Games, and "GML: data structures as a true datatype (not just an index)" item rests on considerations list, but it is not known just when this will be implemented (and, honestly, they have a lot of things to do, so it is understandable).

Solution

Although there had been a couple attempts of creative workarounds through the years (my JSON extension can also be considered as such), people often hesitated to use these due to inevitable performance impact of one or other kind.

For this reason, this new extension comes in two parts:

Quality Structures One

Offers a set of wrappers for almost every single data structure function while introducing a set of sanity checks.

The idea is as following:

a = ds_map_create(); // a is <index>
a = qs_map_create(); // a is [["qs::map"], <index>, <stack trace>]

So, instead of being just an index, you get a tiny array containing:

  • A reference to a "marker" array, which identifies each data structure type
    (this allows to guard against accessing a wrong data structure type)
  • The underlying data structure index
    (which is used for actual operations and unset upon destruction)
  • A callstack (debug_get_callstack) for where this data structure was created/destroyed from (optional; allows to show context when accessing a destroyed data structure)

And then each wrapper script will check if it's the right thing and if it exists before doing anything. Add a custom JSON encoder, some helpers, and you suddenly have 2000 lines of code. Maybe that's why no one tried doing it this way before? Anyway,

Quality Structures Zero

This extension has identical function names to the above, but instead forwards them to each according built-in function directly - at zero overhead.

So, if performance impact from using QS1 becomes anyhow noticeable, you can create a separate "release" configuration in your GameMaker project, add it there, and set up files in each extension to only apply to their respective configuration.

Accesors

Since you cannot modify the behaviour of accessor operators, the extension also comes with a couple chained accessor functions. So, instead of doing

var map = json_decode(@'{"list":["hi!","hello!"]}');
var list = map[?"list"];
var hello = list[|1];

you would do

var map = qs_json_decode(@'{"list":["hi!","hello!"]}');
var hello = qs_get(map, "list", 1);

Examples

So, let's see what happens with each according example shown earlier with QS1 extension:

Mix up a data structure type? You now get an actual error:

var a = qs_map_create();
qs_set(a, "hi", "oh no");
var b = qs_list_create();
var c = qs_map_find_value(b, "hi"); // (a mishap)
//var c = qs_get(b, "hi"); // (also throws an error)
show_debug_message(c);
############################################################################################
FATAL FATAL ERROR in Room Creation Code for room room0

Expected a map, got qs::list (id 0), created from
  gml_Script_qs_list_create:5
  gml_Script_scr_test:3
  gml_Room_room0_Create:1
 at gml_Script_qs_impl_map_mismatch (line 2) - show_error("Expected a map, got " + qs_debug_dump(argument0), true);
############################################################################################
--------------------------------------------------------------------------------------------
stack frame is
gml_Script_qs_impl_map_mismatch (line 2)
called from - gml_Script_qs_map_find_value (line 9) - } else qs_impl_map_mismatch(argument0);
called from - gml_Script_qs_get (line 7) -               l_val = qs_map_find_value(l_val, l_key);
called from - gml_Script_scr_test (line 4) - show_debug_message(qs_get(b, "hi"));

Access a non-data structure? That's also a clean error:

var a = qs_map_create();
var b = 0.3;
qs_set(b, "oh", "no"); // throws error
var c = "0.7";
qs_set(c, "now", "this"); // also throws error
show_debug_message(qs_json_encode(a));
############################################################################################
FATAL FATAL ERROR in Room Creation Code for room room0

Expected a map, got 0.30 (real)
 at gml_Script_qs_impl_map_mismatch (line 2) - show_error("Expected a map, got " + qs_debug_dump(argument0), true);
############################################################################################
--------------------------------------------------------------------------------------------
stack frame is
gml_Script_qs_impl_map_mismatch (line 2)
called from - gml_Script_qs_map_set (line 7) - } else qs_impl_map_mismatch(argument0);
called from - gml_Script_qs_set (line 23) -        qs_map_set(l_val, l_key, l_new);
called from - gml_Script_scr_test (line 3) -        qs_set(b, "oh", "no"); // throws error

Access a destroyed data structure? That's also a readable error, even if the index had since been given out to a new data structure.

var a = qs_map_create();
qs_map_destroy(a);
var b = qs_map_create();
qs_set(a, "oh", "no");
show_debug_message(qs_json_encode(b));
############################################################################################
FATAL FATAL ERROR in Room Creation Code for room room0

This map is already destroyed from:
  gml_Script_qs_map_destroy:26
  gml_Script_scr_test:2
  gml_Room_room0_Create:1
 at gml_Script_qs_impl_map_amiss (line 16) - show_error(l_msg, true);
############################################################################################
--------------------------------------------------------------------------------------------
stack frame is
gml_Script_qs_impl_map_amiss (line 16)
called from - gml_Script_qs_map_set (line 6) -        } else qs_impl_map_amiss(argument0);
called from - gml_Script_qs_set (line 23) -        qs_map_set(l_val, l_key, l_new);
called from - gml_Script_scr_test (line 4) - qs_set(a, "oh", "no");

Want to see where you're leaking structures? Set qs_debug_active to true, pause the game in the debugger, and take a look at any of g_qs_active_* values (which are ds_maps containing DS ID -> creation origin pairs for currently active structures):

Conclusion

With some creative thinking and a bit of hard work, numerous existing inconveniences can be addressed without sacrificing much in return.

My resulting extension can be found on GM Marketplace and itch.io, as usual:

itch.io GM Marketplace

On a closing note, I leave you with an alternate icon+title that I have considered for this extension:

An icon labelled Quack Structures, which has a generic data "cube" saying "KWEK"

Related posts:

Notepad++: Syntax highlighting for FontForge Script

$
0
0

FontForge comes with two scripting systems - largely-regular Python and an unnamed scripting language that uses the extension .PE.

While the documentation advices to prefer Python scripting where possible, it is still nice to be able to read the existing PE scripts easier.

This User Defined Language for Notepad++ aids with that, adding folding and syntax highlighting for built-in functions and variables of the scripting language.

Originally I made this UDL sometime around 2015 when I started working on pixel art fonts and found that my "pipeline" (pixel art editor -> custom tool -> BitFontMaker2 -> TTF file) commonly required series of post-fixes to accomplish correct line height / spacing / rendering.

Fast-forward to 2019, I'm moving files to the new computer and realize that I never published this UDL. So, here you go:

Download UDL

Alternatively, here's the entirety of that:

<NotepadPlus>
    <UserLang name="FF Script" ext="pe" udlVersion="2.1">
        <Settings>
            <Global caseIgnored="no" allowFoldOfComments="no" foldCompact="no" forcePureLC="0" decimalSeparator="0" />
            <Prefix Keywords1="no" Keywords2="no" Keywords3="yes" Keywords4="no" Keywords5="no" Keywords6="no" Keywords7="no" Keywords8="no" />
        </Settings>
        <KeywordLists>
            <Keywords name="Comments">00# 00// 01 02 03/* 04*/</Keywords>
            <Keywords name="Numbers, prefix1"></Keywords>
            <Keywords name="Numbers, prefix2">0x 0u 0U</Keywords>
            <Keywords name="Numbers, extras1">a b c d e f A B C D E F</Keywords>
            <Keywords name="Numbers, extras2"></Keywords>
            <Keywords name="Numbers, suffix1"></Keywords>
            <Keywords name="Numbers, suffix2"></Keywords>
            <Keywords name="Numbers, range"></Keywords>
            <Keywords name="Operators1">+ - ! ~ ++ -- ( ) [ ] :h :t :r :e * / % == != &gt; &lt; &gt;= &lt;= &amp;&amp; &amp; || | ^ = += -= *= /= %= , ;</Keywords>
            <Keywords name="Operators2"></Keywords>
            <Keywords name="Folders in code1, open"></Keywords>
            <Keywords name="Folders in code1, middle"></Keywords>
            <Keywords name="Folders in code1, close"></Keywords>
            <Keywords name="Folders in code2, open">if while foreach</Keywords>
            <Keywords name="Folders in code2, middle">elseif else</Keywords>
            <Keywords name="Folders in code2, close">endif endloop</Keywords>
            <Keywords name="Folders in comment, open"></Keywords>
            <Keywords name="Folders in comment, middle"></Keywords>
            <Keywords name="Folders in comment, close"></Keywords>
            <Keywords name="Keywords1">AddAccent AddAnchorClass AddAnchorPoint AddATT AddDHint AddExtrema AddHHint AddInstrs AddLookup AddLookupSubtable AddPosSub AddSizeFeature AddVHint ApplySubstitution Array AskUser ATan2 AutoCounter AutoHint AutoInstr AutoKern Autotrace AutoTrace AutoWidth bAutoCounter bDontAutoHint BitmapsAvail BitmapsRegen break bSubstitutionPoints BuildAccented BuildComposit BuildComposite BuildDuplicate CanonicalContours CanonicalStart Ceil CenterInWidth ChangePrivateEntry ChangeWeight CharCnt CharInfo CheckForAnchorClass Chr CIDChangeSubFont CIDFlatten CIDFlattenByCMap CIDSetFontNames Clear ClearBackground ClearCharCounterMasks ClearGlyphCounterMasks ClearHints ClearInstrs ClearPrivateEntry ClearTable Close CompareFonts CompareGlyphs ControlAfmLigatureOutput ConvertByCMap ConvertToCID Copy CopyAnchors CopyFgToBg CopyGlyphFeatures CopyLBearing CopyRBearing CopyReference CopyUnlinked CopyVWidth CopyWidth CorrectDirection Cos Cut DebugCrashFontForge DefaultATT DefaultOtherSubrs DefaultRoundToGrid DefaultUseMyMetrics DetachAndRemoveGlyphs DetachGlyphs DontAutoHint DrawsSomething Error Exp ExpandStroke Export FileAccess FindIntersections FindOrAddCvtIndex Floor FontImage FontsInFile Generate GenerateFamily GenerateFeatureFile GetAnchorPoints GetCvtAt GetEnv GetFontBoundingBox GetLookupInfo GetLookupOfSubtable GetLookups GetLookupSubtables GetMaxpValue GetOS2Value GetPosSub GetPref GetPrivateEntry GetSubtableOfAnchorClass GetTeXParam GetTTFName GlyphInfo HasPreservedTable HasPrivateEntry HFlip Import InFont Inline Int InterpolateFonts IsAlNum IsAlpha IsDigit IsFinite IsHexDigit IsLower IsNan IsSpace IsUpper Italic Join LoadEncodingFile LoadNamelist LoadNamelistDir LoadPlugin LoadPluginDir LoadPrefs LoadStringFromFile LoadTableFromFile Log LookupStoreLigatureInAfm MakeLine MergeFeature MergeFonts MergeKern MergeLookups MergeLookupSubtables MMAxisBounds MMAxisNames MMBlendToNewFont MMChangeInstance MMChangeWeight MMInstanceNames MMWeightedName Move MoveReference MultipleEncodingsToReferences NameFromUnicode NearlyHvCps NearlyHvLines NearlyLines New NonLinearTransform Open Ord Outline OverlapIntersect Paste PasteInto PasteWithOffset PositionReference PostNotice Pow PreloadCidmap Print PrintFont PrintSetup PrivateGuess PrivateToCvt Quit Rand ReadOtherSubrsFile Real Reencode RemoveAllKerns RemoveAllVKerns RemoveAnchorClass RemoveATT RemoveDetachedGlyphs RemoveLookup RemoveLookupSubtable RemoveOverlap RemovePosSub RemovePreservedTable RenameGlyphs ReplaceCharCounterMasks ReplaceCvtAt ReplaceGlyphCounterMasks ReplaceWithReference return Revert RevertToBackup Rotate Round RoundToCluster RoundToInt SameGlyphAs Save SavePrefs SaveTableToFile Scale ScaleToEm Select SelectAll SelectAllInstancesOf SelectBitmap SelectByATT SelectByColor SelectByColour SelectByPosSub SelectChanged SelectFewer SelectFewerSingletons SelectGlyphsBoth SelectGlyphsReferences SelectGlyphsSplines SelectHintingNeeded SelectIf SelectInvert SelectMore SelectMoreIf SelectMoreSingletons SelectMoreSingletonsIf SelectNone SelectSingletons SelectSingletonsIf SelectWorthOutputting SetCharCnt SetCharColor SetCharComment SetCharCounterMask SetCharName SetFeatureList SetFondName SetFontHasVerticalMetrics SetFontNames SetFontOrder SetGasp SetGlyphChanged SetGlyphClass SetGlyphColor SetGlyphComment SetGlyphCounterMask SetGlyphName SetGlyphTeX SetItalicAngle SetKern SetLBearing SetMacStyle SetMaxpValue SetOS2Value SetPanose SetPref SetPrefs SetRBearing SetTeXParams SetTTFName SetUnicodeValue SetUniqueID SetVKern SetVWidth SetWidth Shadow shift Simplify Sin SizeOf Skew SmallCaps Sqrt Strcasecmp Strcasestr Strftime StrJoin Strlen Strrstr Strskipint StrSplit Strstr Strsub Strtod Strtol SubstitutionPoints Tan ToLower ToMirror ToString ToUpper Transform TypeOf UCodePoint Ucs4 UnicodeAnnotationFromLib UnicodeBlockEndFromLib UnicodeBlockNameFromLib UnicodeBlockStartFromLib UnicodeFromName UnicodeNameFromLib UnicodeNamesListVersion UnlinkReference Utf8 Validate VFlip VKernFromHKern Wireframe WorthOutputting WritePfm WriteStringToFile</Keywords>
            <Keywords name="Keywords2">$argc $argv $curfont $firstfont $nextfont $fontchanged $fontname $familyname $fullname $fondname $weight $copyright $filename $fontversion $iscid $cidfontname $cidfamilyname $cidfullname $cidweight $cidcopyright $mmcount $italicangle $loadState $privateState $curcid $firstcid $nextcid $macstyle $bitmaps $order $em $ascent $descent $selection $panose $trace $version $haspython</Keywords>
            <Keywords name="Keywords3"></Keywords>
            <Keywords name="Keywords4"></Keywords>
            <Keywords name="Keywords5"></Keywords>
            <Keywords name="Keywords6"></Keywords>
            <Keywords name="Keywords7"></Keywords>
            <Keywords name="Keywords8"></Keywords>
            <Keywords name="Delimiters">00&quot; 00&apos; 01 02&quot; 02&apos; 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23</Keywords>
        </KeywordLists>
        <Styles>
            <WordsStyle name="DEFAULT" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="COMMENTS" fgColor="008000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="LINE COMMENTS" fgColor="008000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="NUMBERS" fgColor="FF0000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="KEYWORDS1" fgColor="0000FF" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="KEYWORDS2" fgColor="FF8040" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="KEYWORDS3" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="KEYWORDS4" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="KEYWORDS5" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="KEYWORDS6" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="KEYWORDS7" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="KEYWORDS8" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="OPERATORS" fgColor="000080" bgColor="FFFFFF" fontName="" fontStyle="1" nesting="0" />
            <WordsStyle name="FOLDER IN CODE1" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="FOLDER IN CODE2" fgColor="0000FF" bgColor="FFFFFF" fontName="" fontStyle="1" nesting="0" />
            <WordsStyle name="FOLDER IN COMMENT" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="DELIMITERS1" fgColor="808080" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="DELIMITERS2" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="DELIMITERS3" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="DELIMITERS4" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="DELIMITERS5" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="DELIMITERS6" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="DELIMITERS7" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="DELIMITERS8" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
        </Styles>
    </UserLang>
</NotepadPlus>

Related posts:

Notepad++: Syntax highlighting for GameMaker Studio 2

$
0
0

About 3 years after GMS2 release I decide that it might be a good idea to make a GMS2-specific UDL for Notepad++ instead of using my GMS1 UDL.

Features

What it has:

  • Syntax highlighting for built-in keywords/functions/variables/constants.
  • Comments (incl. JSDoc).
  • Strings (incl. escape characters and verbatim strings).
  • Code folding for brackets, comments (//{..//}), and #regions.
  • Ability to specify shared resource names or prefixes (see customizing).

Limitations:

  • #region/#endregion labels highlight as code because you can't have something be both a delimiter and a fold start/end at once.
  • Can't fold #define..#define because NPP does not support using the same identifier as fold-start and fold-middle at once (thus the first #define would fold the entire file).
  • Obviously no contextual highlighting (local variables, enums, etc.).
  • Also no automatic resource highlighting unless you specify prefixes (see customizing).

If you need feature-complete highlighting for GML, there's GMEdit.

Download

With that out of the way, here's the UDL itself:

Download UDL

Customizing

The UDL has a few keyword groups reserved for project-specific resources,

  • Keywords 5: Full asset names (e.g. rm_test).
  • Keywords 6: Asset name prefixes (e.g. rm_ spr_ obj_).
  • Keywords 7: Script name prefixes (e.g. scr_) - highlighted differently from assets.

Unfortunately, if you are using camel-case resource names (objTest rather than obj_test) you'll need to either make the prefix just obj, or add 26 prefixes (objA, objB, ..).

Updating

As GMS2 updates, new functions are being added, and it might seem reasonable to allow to introduce these to UDL in a sane way.

First, you want to either take your existing GMS2 UDL (see above), or install this stripped down version:

<NotepadPlus>
    <UserLang name="GMLv2" ext="gml" udlVersion="2.1">
        <Settings>
            <Global caseIgnored="no" allowFoldOfComments="yes" foldCompact="no" forcePureLC="0" decimalSeparator="0" />
            <Prefix Keywords1="no" Keywords2="no" Keywords3="no" Keywords4="no" Keywords5="no" Keywords6="yes" Keywords7="yes" Keywords8="no" />
        </Settings>
        <KeywordLists>
            <Keywords name="Comments">00// 01 02 03/* 04*/</Keywords>
            <Keywords name="Numbers, prefix1">$</Keywords>
            <Keywords name="Numbers, prefix2">0x</Keywords>
            <Keywords name="Numbers, extras1">a b c d e f A B C D E F</Keywords>
            <Keywords name="Numbers, extras2">a b c d e f A B C D E F</Keywords>
            <Keywords name="Numbers, suffix1"></Keywords>
            <Keywords name="Numbers, suffix2"></Keywords>
            <Keywords name="Numbers, range"></Keywords>
            <Keywords name="Operators1">= + - / * &amp; | ^ ! % ~ &lt; &gt; ( ) [ ] ; . , : @ ? [#</Keywords>
            <Keywords name="Operators2">#</Keywords>
            <Keywords name="Folders in code1, open">{ #region</Keywords>
            <Keywords name="Folders in code1, middle"></Keywords>
            <Keywords name="Folders in code1, close">} #endregion</Keywords>
            <Keywords name="Folders in code2, open">begin</Keywords>
            <Keywords name="Folders in code2, middle"></Keywords>
            <Keywords name="Folders in code2, close">end</Keywords>
            <Keywords name="Folders in comment, open">{ #region</Keywords>
            <Keywords name="Folders in comment, middle"></Keywords>
            <Keywords name="Folders in comment, close">} #endregion</Keywords>
            <Keywords name="Keywords1">#macro&#x000D;&#x000A;not and or xor div mod&#x000D;&#x000A;globalvar var global&#x000D;&#x000A;enum function&#x000D;&#x000A;if then else&#x000D;&#x000A;for while do until repeat break continue&#x000D;&#x000A;switch case default&#x000D;&#x000A;with self other all noone&#x000D;&#x000A;exit return&#x000D;&#x000A;try catch throw</Keywords>
            <Keywords name="Keywords2"></Keywords>
            <Keywords name="Keywords3"></Keywords>
            <Keywords name="Keywords4"></Keywords>
            <Keywords name="Keywords5"></Keywords>
            <Keywords name="Keywords6"></Keywords>
            <Keywords name="Keywords7"></Keywords>
            <Keywords name="Keywords8">@func @function&#x000D;&#x000A;@arg @param @argument&#x000D;&#x000A;@desc @description&#x000D;&#x000A;@return @returns&#x000D;&#x000A;@author</Keywords>
            <Keywords name="Delimiters">00@&quot; 01 02&quot; 03@&apos; 04 05&apos; 06&quot; 07\ 08&quot; 09#define 10 11((EOL)) 12 13 14 15 16 17 18 19 20 21 22 23</Keywords>
        </KeywordLists>
        <Styles>
            <WordsStyle name="DEFAULT" fgColor="000000" bgColor="FFFFFF" fontName="&#x000F;" fontStyle="0" nesting="0" />
            <WordsStyle name="COMMENTS" fgColor="008000" bgColor="FFFFFF" fontName="&#x000F;" fontStyle="2" nesting="0" />
            <WordsStyle name="LINE COMMENTS" fgColor="008000" bgColor="FFFFFF" fontName="-1" fontStyle="2" nesting="131072" />
            <WordsStyle name="NUMBERS" fgColor="FA3232" bgColor="FFFFFF" fontName="0" fontStyle="0" nesting="0" />
            <WordsStyle name="KEYWORDS1" fgColor="1F1F99" bgColor="FFFFFF" fontName="1202" fontStyle="1" nesting="0" />
            <WordsStyle name="KEYWORDS2" fgColor="800000" bgColor="FFFFFF" fontName="-1" fontStyle="0" nesting="0" />
            <WordsStyle name="KEYWORDS3" fgColor="800000" bgColor="FFFFFF" fontName="-1" fontStyle="0" nesting="0" />
            <WordsStyle name="KEYWORDS4" fgColor="800000" bgColor="FFFFFF" fontName="&#xC890;&#xFD64;&#x01DA;" fontStyle="0" nesting="0" />
            <WordsStyle name="KEYWORDS5" fgColor="0078AA" bgColor="FFFFFF" fontName="&#xFAD0;&#xFD64;&#x01DA;" fontStyle="0" nesting="0" />
            <WordsStyle name="KEYWORDS6" fgColor="0078AA" bgColor="FFFFFF" fontName="-1" fontStyle="0" nesting="0" />
            <WordsStyle name="KEYWORDS7" fgColor="800080" bgColor="FFFFFF" fontName="&#xFCE0;&#xFD64;&#x01DA;" fontStyle="0" nesting="0" />
            <WordsStyle name="KEYWORDS8" fgColor="914BAF" bgColor="FFFFFF" fontName="-1" fontStyle="2" nesting="0" />
            <WordsStyle name="OPERATORS" fgColor="000000" bgColor="FFFFFF" fontName="no" fontStyle="0" nesting="0" />
            <WordsStyle name="FOLDER IN CODE1" fgColor="1F1F99" bgColor="FFFFFF" fontName="E60-F11DB97C5CC7}" fontStyle="1" nesting="0" />
            <WordsStyle name="FOLDER IN CODE2" fgColor="1F1F99" bgColor="FFFFFF" fontName="-1" fontStyle="1" nesting="0" />
            <WordsStyle name="FOLDER IN COMMENT" fgColor="008040" bgColor="FFFFFF" fontName="no" fontStyle="2" nesting="0" />
            <WordsStyle name="DELIMITERS1" fgColor="0000FF" bgColor="FFFFFF" fontName="-1" fontStyle="0" nesting="0" />
            <WordsStyle name="DELIMITERS2" fgColor="0000FF" bgColor="FFFFFF" fontName="0" fontStyle="0" nesting="0" />
            <WordsStyle name="DELIMITERS3" fgColor="0000FF" bgColor="FFFFFF" fontName="0" fontStyle="0" nesting="0" />
            <WordsStyle name="DELIMITERS4" fgColor="7A81A9" bgColor="FFFFFF" fontName="&#xC4D0;&#xFD64;&#x01DA;" fontStyle="0" nesting="0" />
            <WordsStyle name="DELIMITERS5" fgColor="FF80FF" bgColor="FFFFFF" fontName="-1" fontStyle="0" nesting="0" />
            <WordsStyle name="DELIMITERS6" fgColor="FF0000" bgColor="FFFFFF" fontName="-1" fontStyle="0" nesting="1024" />
            <WordsStyle name="DELIMITERS7" fgColor="000000" bgColor="FFFFFF" fontName="-1" fontStyle="0" nesting="0" />
            <WordsStyle name="DELIMITERS8" fgColor="000000" bgColor="FFFFFF" fontName="-1" fontStyle="0" nesting="0" />
        </Styles>
    </UserLang>
</NotepadPlus>

Then, to get the up-to-date function/variable/constant definitions, find your fnames file, usually in

C:/ProgramData/GameMakerStudio2/Cache/runtimes/runtime-x.x.x.x

open it with text editor, and copy contents to this field:

and press this button:
Keywords 2 (functions):
Keywords 3 (constants):
Keywords 4 (variables):

In conclusion

Although with some obvious limitations for lack of ability to specify custom logic, this UDL can still be handy for quickly viewing-editing GameMaker Studio 2 files.

Related posts:

2019 in review

$
0
0

I was randomly asked whether I was going to write a summary-of-the-year post or anything, and I thought "sure, why not", and proceeded to spend a day doing that.

Work

This year seemed pretty productive in terms of projects,

Rivals of Aether

This year my involvement with RoA expanded to cover Steam Workshop features, for which I provided a custom scripting language (alike to what is seen in Nuclear Throne Together). With update released, the game have been seeing an impressive amount of user-created content - from well-balanced original characters to content adaptations from various games to hilarious/absurd ideas and joke characters.

There remain to be issues to overcome in terms of netcode updates and console features, but I'm confident that we'll get these ironed out soon.

Forager

Although I was originally introduced to Forager team to aid with networking, this year I was mostly helping with the modding update. As of writing this post, the update is not yet released, but a lot of work had gone into it, and I expect it to do at least as well as Rivals' modding support did.

Next year I'll be more involved on actual netcode.

Nuclear Throne

This year Nuclear Throne came out on Switch, and, more recently, on Epic Game Store, with "free game week" finally offering a chance for those who didn't want to buy the game at full price.

Since Nintendo do not commonly send devkits to Ukraine (in fact, I wasn't even able to get my developer registration form approved), my involvement was limited to consultation on how everything works.

EGS version went much smoother - since the game already had DRM-free builds set up, no additional effort was required, and I remained pleasantly unaware of the port's development up until it was suddenly established that a 64-bit OSX build is required, prompting me to hastily re-assemble a GMS2 desktop version of the game.

Fromto

During 2019 I was on and off working on netcode, menus, and mod.io integration for a game called Fromto. The game is essentially a platform racer that alternates between rounds of trying to reach the finish line and adding more parts to the level to increase your chances / decrease other players' chances to reach the finish line. With max of 4 players, the game gets incredibly chaotic.

I couldn't say that the game had any special requirements, but I used an opportunity to further refine cmnRPC and cmnMenu extensions while working on the game.

Links: website

Unannounced

Aside of these, I also did initial netcode pass for a few projects that have not been publicly announced as of writing this post. There's not much more to say as of now.

Tools

The following are the tools that I have worked on this year:

catch_error

In February I had this wild idea that, since you can rewrite DLL function addresses on Windows, I could make a DLL that would suppress the built-in error boxes - perhaps the most telling thing that you've used GameMaker for your game.

Then I had an even wilder idea that I could make the game not crash at all, so I spent about a few days staring at disassembly in Visual Studio's debugger to get that done.

While an admirable result, I probably wouldn't do this to myself again.

Links: itch.io · blog post

Pixelart scaling tool

While temporarily using a 43" 4K TV as a monitor, I made an important discovery: you can't trust Windows to upscale pixelart wallpapers.

So I made a small tool that can upscale/rescale pixelart images, meaning that you can shrink a 3x wallpaper back to 1x scale, and then scale it to the target resolution for perfectly crisp pixels.

Combined with simple alignment options, this can be used to quickly fit large libraries of pixelart images to the desired size.

Links: website

Tiny Expression Runtime

Thanks to support of a party that preferred to remain anonymous, I was able to take time to write the second part of my "small guide on writing interpreters", and subsequently publish a sample project.

Links: Documentation · itch.io · marketplace · source code

Quality Structures

This year I made a few more extensions that fix various GameMaker inconveniences, the most noteworthy one being this - the extension takes a lot of frustration out of data structure debugging and offers to show descriptive errors before something goes horribly wrong

Links:: itch.io · marketplace · blog post

Pixel fonts

This year I finally got around to publishing a set of pixelart fonts (that I made over course of several years for various personal projects) on itch.io. Most of these have a clean look with emphasis on readability and localization (some have over 500 glyphs!).

I hope to add a handful more to this collection next year.

Links: itch.io

mod.io integration

I wrote the official GameMaker plugin for mod.io, which is a cross-platform mod API - a far better idea than tying your game to Steam Workshop and suffering for it when you decide to publish to more storefronts.

Although the plugin is perfectly functional and a few games use it (incl. Fromto), I'm yet to write a proper step-by-step setup and/or a video, so users are expected to look at the simple example attached.

Smaller tools & updates

Apollo rewrite
Early this year I pretty much completely rewrote my Lua wrapper extension, this time making good use of Lua specifics for performance boost.

Raw Input
Admittedly exotic, this extension lets your GM games distinguish inputs from different mouse/tablet/keyboard devices.

High-precision HTML5 get_timer
In short, GM's implementation of get_timer does not use the performance API that offers a high-precision timer. This extension fixes that oversight.

debug_get_callstack for HTML5
Akin to above, but fixes debug_get_callstack to work with scripts.
(by default it doesn't due to JS obfuscation)

Instance Flags
A small solution to a common problem.

function_get_address
I'm somewhat proud of this because of how elaborate the sequence of bugs that triggers this accidental revival of a GM8 feature is.

YY-YAML
Saves a lot of headache when dealing with branch merges (for version control) in GMS2.

Colored external debug log window
Having colored debug messages that you can properly select is a surprisingly nice thing to have.

Tools (unreleased)

The following are the tools that I worked on, but haven't released yet:

cmnRPC

A small, elegant system for remote procedure calls and managing network state.

Combined with GMEdit's inline functions or the ones coming officially in GMS 2.3 update, the extension removes a lot of friction from writing netcode - you write what you like to send and then how you want to process that information upon receiving, all in one spot.

cmnChat

An advanced chat system that has just about everything that you could expect - text cursor (and selection), clipboard operations, command history, even auto-completion for both commands and their arguments (see a larger GIF).

This year the extension made its way into Caveblazers, Forager, and a few unannounced projects.

cmnMenu

A backbone for your own menu systems, cmnMenu takes care of menu management, massively cutting the amount of code it takes to have a well-working menu, and also allowing menus to be written in a far more intuitive way (if (menu_thing()) { actions to trigger }).

GMS2->GMS1 converter

This is something that I have been thinking about, but did not expect to ever have a reason to actually make.

I was approached by Ratalaika Games about creating a converter very specifically for targeting PS Vita, as GMS2 lacks a Vita export, but all of the games have already been ported to GMS2 for sake of other platforms.

The tool converts majority of GMS2 resources back to GMS1 format (in possible extent), converts most GMS2-specific code structures to GMS1-compatible format, "un-converts" many compatibility scripts added upon importing a GMS1 project into GMS2, and even adds some custom scripts for GMS2-exclusive features (such as layers or animated tiles).

Although currently in development, the tool will be eventually publicly available and open for contributions - something that you can thank RG for.

GMHyper

GMHyper is a tool that can rewrite your YYC code right as it is compiling, allowing for a variety of tricks such as strictly typed variables (to much of a performance boost), tailored versions of various built-in functions (ditto), and usually impossible features like using C++ APIs directly or straight up injecting C++ code into your GML scripts.

Combined with a flexible system for defining custom rules, the tool greatly expands the horizons of what can be done with GM on high level.

GMTogether


A familiar-looking menu while using the tool with Hyper Light Drifter

You might have already seen me posting a small teaser of this on Twitter, but - basically I'm working on a tool that can "inject" online multiplayer into countless GameMaker games.

Although the result might not be as refined as game-specific mods (read: there is only so much you can do about generic lag compensation), the tool warrants equal conditions for players (making it a better choice for versus games than stream-based solutions like Parsec / Remote Play Together), and offers a large number of options that can be tweaked for quality of life and/or compatibility.

Tools (work)

The following are the things that I did but which are not going to see public availability due to being something people paid me to do and/or associated complications:

GML->C# converter

Due to unforeseen circumstances, a client wanted to port their existing (~5K LOC) codebase from GML to C# + Unity. I wrote a tool to perform project-wide analysis and type inference to generate C# code that a C# programmer could then write a custom "GML API" for. The nature of conversion process meant that the resulting code does not need to be edited, allowing the GML version to be updated until the C# version is fully functional.

(...there was supposed to be something else here but I forgot what)

Game mods

Boiling Hot

A small exploration of C# (IL) bytecode injection using Mono.Cecil to add a SUPERHOT-like mode to Unexplored, made in a few evenings in February.

Perhaps one of the best (added value):(lines of code) ratios among my work.

Developers of both Unexplored and SUPERHOT seemed happy with it, so I'd say it did well.

Links: itch.io

Caveblazers Together

After somewhat stretched-out development and some unexpected hickups, Caveblazers Together finally released in March, mostly to no media response.

Although an interesting experiment, I probably won't do another game-specific online multiplayer mod any time soon - the conditions in which it'd make sense to make an online multiplayer mod instead of an official integration are just way too specific, and CBT probably didn't even fit them.

Links: Steam · blog post

Personal life

For the most part, my personal life this year had not been very far off from the "a rock in a forest" absurdist humour account on Twitter (in Russian) - most of the year could be evenly described with "nothing particular happened today".

Due to constant project schedule overlap, I did not end up going on a vacation during summer this year, although I did move to a slightly nicer city around early autumn. As a bonus, I discovered that living in an apartment with adequate amount of sun/sky light coming into a bedroom does help to maintain a consistent sleeping schedule. [who would have thought]

I still walk around a bunch (2.5..5km every other day, with peaks of >10km), though at first I was caught off-guard by number of random staircases that you might take while cutting distance by foot - to the point where sometimes the phone gives up counting and gives you an equivalent of "I don't know, a lot". Being able to occasionally go for a walk and stare at the sea and/or the seagulls is pretty sweet though.

Also, since, apparently, the winter around here now just means "+1..8°C at 100% humidity for 3 months" (which, depending on wind, makes for suboptimal weather for a long walk), I bought a used XB1+Kinect off someone and been playing Just Dance on that.

2020 goals

Take it easy

...-er.

Although jumping between a number of different projects is fun in it's own way and keeps you occupied, the situation becomes a less entertaining as soon as more than one projects needs something done at short notice.

The light pain in a few of my fingers during prolonged work also suggests that it would be better to not have to mash the keyboard for too much at once.

Overall this entails wrapping up some of the existing projects and resisting the urge to take up new ones even if they look Really Cool (this had been a long-lasting problem for me).

Draw more

... and start publishing art more often.

As you might have seen, my recent posts have been spotting doodles here and there, and I've been doing these on a semi-regular basis, but as of yet they remain scattered across a number of Discord servers and other shared chats.

I intend to start publishing these, as many people find my work entertaining.

Make a videogame?

... maybe?

I don't think I've worked on any personal game projects at all this year!
Except for a short game for Meditations, which I successfully wasted because I decided to politely ask what day to pick if I have no strict preference, and, by the time I got a response, all days were taken, so - better luck next time, I guess!

Hopefully I'll get around to doing something, maybe finally play around with Godot.

Work goals

Most of these are pretty obvious, but here's a small list without specific order:

  • Rivals of Aether
    More netcode and Definitive Edition work
  • Forager
    Netcode (once it's time)
  • Nuclear Throne
    Minor fixes, NTT updates
  • Fromto
    Various additions and netcode updates as the game development goes on
  • Nidhogg
    64-bit Mac builds because it's 2019 2020 and Apple are sending you warm wishes as they demolish 3/4 of an average user's Steam library with that OS update.

In conclusion

This was a pretty exciting year, and there's a lot to look forward.

Thanks for reading!

Related posts:

TXR part 1 in Haxe

$
0
0

This is a small note that I have made a quick Haxe variant of the first part of my "small guide on writing interpreters". You can see an interactive Haxe-JS demo above, and find the Haxe source code on bitbucket.

Related posts:

Notepad++: syntax highlighting for BB code

$
0
0

It came to my attention that no one published a user defined language mode for Notepad++ that would allow you to view BB code tags in the editor relatively comfortably, so I decided to correct that omission by publishing mine.

As far as technical details go, there isn't anything of particular complexity here - various tags are defined as delimiters and URLs use the same trick that the built-in Markdown UDL uses.

Download UDL

Alternatively, raw UDL XML:

<NotepadPlus>
    <UserLang name="BB Code" ext="" udlVersion="2.1">
        <Settings>
            <Global caseIgnored="yes" allowFoldOfComments="no" foldCompact="no" forcePureLC="0" decimalSeparator="0" />
            <Prefix Keywords1="yes" Keywords2="no" Keywords3="no" Keywords4="no" Keywords5="no" Keywords6="no" Keywords7="no" Keywords8="no" />
        </Settings>
        <KeywordLists>
            <Keywords name="Comments"></Keywords>
            <Keywords name="Numbers, prefix1"></Keywords>
            <Keywords name="Numbers, prefix2"></Keywords>
            <Keywords name="Numbers, extras1"></Keywords>
            <Keywords name="Numbers, extras2"></Keywords>
            <Keywords name="Numbers, suffix1"></Keywords>
            <Keywords name="Numbers, suffix2"></Keywords>
            <Keywords name="Numbers, range"></Keywords>
            <Keywords name="Operators1">&quot;</Keywords>
            <Keywords name="Operators2"></Keywords>
            <Keywords name="Folders in code1, open"></Keywords>
            <Keywords name="Folders in code1, middle"></Keywords>
            <Keywords name="Folders in code1, close"></Keywords>
            <Keywords name="Folders in code2, open"></Keywords>
            <Keywords name="Folders in code2, middle"></Keywords>
            <Keywords name="Folders in code2, close"></Keywords>
            <Keywords name="Folders in comment, open"></Keywords>
            <Keywords name="Folders in comment, middle"></Keywords>
            <Keywords name="Folders in comment, close"></Keywords>
            <Keywords name="Keywords1">http:// https:// mailto: ftp:// ftps://</Keywords>
            <Keywords name="Keywords2"></Keywords>
            <Keywords name="Keywords3"></Keywords>
            <Keywords name="Keywords4"></Keywords>
            <Keywords name="Keywords5"></Keywords>
            <Keywords name="Keywords6"></Keywords>
            <Keywords name="Keywords7"></Keywords>
            <Keywords name="Keywords8"></Keywords>
            <Keywords name="Delimiters">00[quote 01 02[/quote] 03[spoiler 04 05[/spoiler] 06[code 07 08[/code] 09[b] 10 11[/b] 12[i] 13 14[/i] 15[b] 15[i] 16 17[/b] 17[/i] 18[h1] 18[h2] 18[h3] 19 20[/h1] 20[/h2] 20[/h3] 21[ 22 23]</Keywords>
        </KeywordLists>
        <Styles>
            <WordsStyle name="DEFAULT" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="COMMENTS" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="LINE COMMENTS" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="NUMBERS" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="KEYWORDS1" fgColor="FF0080" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="KEYWORDS2" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="KEYWORDS3" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="KEYWORDS4" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="KEYWORDS5" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="KEYWORDS6" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="KEYWORDS7" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="KEYWORDS8" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="OPERATORS" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="FOLDER IN CODE1" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="FOLDER IN CODE2" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="FOLDER IN COMMENT" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="DELIMITERS1" fgColor="008040" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="254" />
            <WordsStyle name="DELIMITERS2" fgColor="808080" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="DELIMITERS3" fgColor="0078AA" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="DELIMITERS4" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="1" nesting="160" />
            <WordsStyle name="DELIMITERS5" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="2" nesting="160" />
            <WordsStyle name="DELIMITERS6" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="3" nesting="0" />
            <WordsStyle name="DELIMITERS7" fgColor="0078AA" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="0" />
            <WordsStyle name="DELIMITERS8" fgColor="0000FF" bgColor="FFFFFF" fontName="" fontStyle="0" nesting="16778240" />
        </Styles>
    </UserLang>
</NotepadPlus>

Related posts:


GameMaker: Escaping URL parameters

$
0
0


Illustrating why you should escape your URL parameters

If you are using HTTP requests in your GameMaker projects, you may need to encode parameters in URLs accordingly to prevent your requests from becoming accidentally (or less accidentally, if user input is involved) malformed.

With some luck you may have already found the script on YYG helpdesk, but there's a minor caveat - as of writing this post, it does not support Unicode, so any non-Latin glyphs will be lost in process. Also it's not as fast as it could be, coming from pre-GM:S days and all.

I have at one point implemented URL encoding for sfgml and have now made a cleaner-looking, single-script version of the function. This post is about that.

Idea

We are going to look at browser-standard encodeURIComponent for implementation reference;

  • As per page, encoding works on per-UTF8-byte basis.
    Perhaps the most reliable way to process UTF8 bytes of a string in GameMaker is to write the string into a buffer and then pull bytes out of it, so we shall do that;
  • As per page and spec (section 2.2), the only characters that should not be escaped are

    A-Z a-z 0-9 - _ . ! ~ * ' ( )
    

    to avoid having a particularly awful-looking if-statement, we shall make a 256-item lookup array indicating which bytes are allowed to go unescaped;

  • Since we have a fixed amount of possible bytes to encode, we can also pre-generate an array of hex char pairs for each byte (" " becomes "%20", so we need ord("2") | (ord("0") << 8) to produce 2 bytes for the output string)
  • To have largely-linear processing time, we'll use a buffer for building the output string.

This boils down to the following:

  • Create buffers and populate lookup arrays on first run
  • For each UTF8 byte of input string (written to a buffer), append either directly to output buffer (if allowed), or as a "%" followed by two hex digits from the lookup array.
  • Finally, read back and return the string from output buffer.

Implementation

Aforementioned concept translates to code pretty well - constructing the lookup arrays is by far the most verbose part of this.

/// url_encode(url_string)
/// @param url_string
gml_pragma("global", "global._url_encode_ready = false;");
var l_inbuf, l_outbuf, l_allowed, l_hex, l_ind;
if (global._url_encode_ready) {
    l_inbuf = global._url_encode_in;
    l_outbuf = global._url_encode_out;
    l_allowed = global._url_encode_allowed;
    l_hex = global._url_encode_hex;
} else { // first-time setup
    global._url_encode_ready = true;
    l_inbuf = buffer_create(1024, buffer_grow, 1);
    global._url_encode_in = l_inbuf;
    l_outbuf = buffer_create(1024, buffer_grow, 1);
    global._url_encode_out = l_outbuf;
    // establish which characters we do NOT need to encode:
    l_allowed = array_create(256);
    for (l_ind = ord("A"); l_ind <= ord("Z"); l_ind++) l_allowed[l_ind] = true;
    for (l_ind = ord("a"); l_ind <= ord("z"); l_ind++) l_allowed[l_ind] = true;
    for (l_ind = ord("0"); l_ind <= ord("9"); l_ind++) l_allowed[l_ind] = true;
    l_allowed[ord("-")] = true;
    l_allowed[ord("_")] = true;
    l_allowed[ord(".")] = true;
    l_allowed[ord("!")] = true;
    l_allowed[ord("~")] = true;
    l_allowed[ord("*")] = true;
    l_allowed[ord("'")] = true;
    l_allowed[ord("(")] = true;
    l_allowed[ord(")")] = true;
    global._url_encode_allowed = l_allowed;
    // pre-generate two-byte hex char sequences:
    l_hex = array_create(256);
    for (l_ind = 0; l_ind < 256; l_ind++) {
        var l_hv, l_hd = l_ind >> 4;
        if (l_hd >= 10) {
            l_hv = ord("A") + l_hd - 10;
        } else l_hv = ord("0") + l_hd;
        // second char (lower nibble):
        l_hd = l_ind & $F;
        if (l_hd >= 10) {
            l_hv |= (ord("A") + l_hd - 10) << 8;
        } else l_hv |= (ord("0") + l_hd) << 8;
        l_hex[l_ind] = l_hv;
    }
    global._url_encode_hex = l_hex;
}
// write down and measure the input string:
buffer_seek(l_inbuf, buffer_seek_start, 0);
buffer_write(l_inbuf, buffer_text, argument0);
var l_len = buffer_tell(l_inbuf);
// read bytes one-by-one, deciding for each:
buffer_seek(l_inbuf, buffer_seek_start, 0);
buffer_seek(l_outbuf, buffer_seek_start, 0);
repeat (l_len) {
    var l_byte = buffer_read(l_inbuf, buffer_u8);
    if (l_allowed[l_byte]) {
        buffer_write(l_outbuf, buffer_u8, l_byte);
    } else { // if it needs to be encoded, write %<two hex digits>
        buffer_write(l_outbuf, buffer_u8, ord("%"));
        buffer_write(l_outbuf, buffer_u16, l_hex[l_byte]);
    }
}
// finally, rewind and read the string:
buffer_write(l_outbuf, buffer_u8, 0);
buffer_seek(l_outbuf, buffer_seek_start, 0);
return buffer_read(l_outbuf, buffer_string);

How to use

Add the shown script as url_encode and pass parameters that you want escaped through it, like so:

var url = "some?val=" + url_encode("hi hello привіт");
show_debug_message(url); // "some?val=hi%20hello%20%D0%BF%D1%80%D0%B8%D0%B2%D1%96%D1%82"

As can be seen, spaces are escaped, and so is non-Latin text, as per spec.

Have fun!

Related posts:

“GMLive for Unity3D” breakdown

$
0
0

(mouseover/click to play GIF) Click here for full-sized version.

If you follow me on one or other social network, you might have seen me post this gif on April 1st.

Most readers were quick to suspect something, but what if I told you that it was more real than you might have thought?

This small post is about that.

The joke (for unfamiliar)

"GMLive" stands for "GameMaker Live" and is a live-coding extension for GameMaker.

That is, it lets you edit some code, press Ctrl+S, and see the logic update inside the game, without having to recompile/restart it. Can save you countless hours when iterating on code.

With one-minute setup, cross-platform support, and additional features (like sprite/room/shader reloading), it is widely regarded as one of the more useful extensions you can get for GameMaker, let alone in that price bracket.

You get the idea.


(tooting my own horn just a little while being dragged off the scene)

Consequently, "GMLive for Unity" means "GameMaker Live for Unity", and it live-[re]loads your GameMaker code (straight from a GameMaker project, no less) into a Unity game.

Rest assured, there is no practical use case for this.

I also take the opportunity to gently poke fun at Unity's live recompilation - although very powerful, there's a number of caveats (sample), and it can be hard to make use of in an environment where a lot of code comes from assets (and may not be compliant in any way).

Context

On the morning of April 1st someone messages me and asks if I'm going to do anything for April Fools on Russian GameMaker community that I moderate. They suggest to post that the language update is out, but I disagree, as that's evil - everyone are already waiting for that.

I consider what other options there are and recall that I wanted to do "GMLive for <not GameMaker>" for Apr01 at some point. As I already have Unity installed, I decide to go with that. I consider my options:

  • Make a simple mockup "screenshot" like most people would
  • Use system-wide macros to fake the process for a GIF nicely
    (prepare a separate Unity snippet for each step of the process)
  • Use my very own extension to embed a borderless GameMaker window into Unity and use the regular GMLive.
  • Compile GMLive to C#! After all, it's written in Haxe. Shouldn't be too hard.

I guess it's fairly apparent what I went with


We did not go to the Cool Moon because it was easy. Or for any identifiable reason.

Technical specifics

Haxe is a language with the primary feature of compiling to other languages.

The process of getting my code to compile for C# was fairly straightforward:

  • I added conditional flags so that unrelated features (sprite/room/shader reloading) are not processed at all.
  • C# does not have an exact equivalent of "reference to a function" type, so I had to replace a few things with System.Action<>/System.Func<>.
  • Rewrote the stack-like structure used for VM from a GML-specific approach (array with a prefix element indicating depth) to a more regular one. In hindsight, a bit of a mistake, as the original one would still work fine as far as this GIF goes.
  • Disabled VM instructions that I definitely wouldn't need to implement for the GIF.

Upon getting the code to run, it came to my attention that some things do not work,

so I fixed those:

  • HTTP requests were not being recognized by GMLive-server.
    As it turns out, my HTTP mini-server was not exactly spec-compliant and would not expect the client to flush the stream before they're done writing out the method+URL line, which Haxe-C# implementation of HTTP request did. Curiously, UnityWebRequest does not share this trait.
  • I might have over-optimized the GML stack implementation slightly (e.g. instructions that had to pop-push would work with "top" item directly) and this backfired upon trying to translate it into a more regular format, costing me some minutes staring at the debugger.
  • C# turned out to be more type-script than I recalled - for instance, if you have a double boxed inside an object, you cannot unbox it straight into a float - you have to unbox it into a double and then cast it to a float.
  • Furthermore, I ran into this issue with type inference working unusually, costing me some more minutes staring at the generated code and the debugger.


In process I also discover that Haxe's generated C# can be hard to look at:


Click for a full-sized (and comically wide) version.

Mostly you don't have to (since Haxe will generate preprocessor definitions to map back to Haxe code), but if something goes wrong with generation, it's an interesting time.

Generated code's tidiness is a kind of problem that can be dealt with


A little more tooting? No? Okay.

but it certainly takes some work. More so for a fully typed language.


Anyway, with all that out of the way, the code was working as intended, so I implemented the few functions needed for this and recorded the GIF.

And a little cheating

Then, of course, we have a caveat: the GameMaker API.

In GameMaker, GMLive uses GameMaker's built-in functions directly, reasonably enough.

Unity does not have a conveniently named package of GameMaker-like functions, so I'd have to make those myself.

So I do... for what is needed for the GIF. Which is 5 constants, 3 functions, 3 global variables, and two instance variables total:


(click for a full version)

If we were to go by auto-completion file for GameMaker Studio 2.2.5, that's 1877 functions less than is needed for 100% API coverage.

But can you make it practical?

Of course!

With some work and cleanup, it can serve as a good base for a game-specific scripting language - just like how Nuclear Throne (API reference), Forager, and Rivals of Aether use variously customized bits of GMLive's (GML) VM code.

I have talked about specifics and advantages of having a fine-tuned custom language in past.

Perhaps a matter of time / someone having a budget and desiring a non-Lua scripting language.

Conclusion

Overall, this was a fun thing to do in a day.

(mouseover/click to play GIF)

Related posts:

GameMaker: Supporting high-density screens in HTML5 games

$
0
0

While developing HTML5 games using GameMaker Studio, it may eventually come to your attention that the game does not render at native resolution, depending on the device, looking variously blurry as result. This small post is about fixing that.

Why does it happen

For reasons that can be promptly described as "the user may desire to be able to read text", higher-density screens will lie about screen dimensions and element sizes to have existing pages behave largely-correctly by default.

This separates the concepts of "screen pixels" and "page pixels".

How to fix it

devicePixelRatio variable exists for this purpose and reports the multiplier for page-to-screen pixels. Making use of it in GameMaker is not very hard, and, in fact, almost identical to the first code snippet on MDN.

First you would want to create a new extension (via context menu on Extensions section in GameMaker resource tree) and add a JS file to it. Then open that file in extension's file directory via an external editor.

You would need exactly two functions here - one to report devicePixelRatio, and one to change canvas.style.width / canvas.style.height (as the built-in window_set_size will only change canvas.width / canvas.height).

function browser_get_device_pixel_ratio() {
    return window.devicePixelRatio || 1;
}

function browser_stretch_canvas_ext(canvas_id, w, h) {
    var el = document.getElementById(canvas_id);
    el.style.width = w + "px";
    el.style.height = h + "px";
}

Then you would want to expose the functions by adding them via GameMaker extension UI:
(browser_get_device_pixel_ratio needs 0 arguments while browser_stretch_canvas_ext needs 3)


(note: argument/return types don't matter for GML and JS extensions)

Then you would want to add a GML file to the extension. The only reason why this is needed at all is to avoid hardcoding the canvas ID that our JS function takes (so far it's always been "canvas", but you never know).

#define browser_stretch_canvas
/// (width, height)
return browser_stretch_canvas_ext(window_handle(), argument0, argument1);

Similarly, you would want to expose that single 2-argument script via the extension UI.

With that done, the helper extension is complete and ready to use.

How to use

The flow is pretty simple and, again, not unlike the MDN example:

  • Get scaling factor via browser_get_device_pixel_ratio.
  • Calculate true size by multiplying screen size by scaling factor.
  • Set your room/view/application_surface/window sizes to true size.
  • Force correct screen size by passing it to browser_stretch_canvas_ext.

Here's an example of that:

var w = browser_width;
var h = browser_height;

// find screen pixel dimensions:
var rz = browser_get_device_pixel_ratio();
var rw = w * rz;
var rh = h * rz;

// update room/view size:
room_width = rw;
room_height = rh;
camera_set_view_size(view_camera[0], rw, rh);
view_wport[0] = rw;
view_hport[0] = rh;

// resize application_surface, if needed
if (application_surface_is_enabled()) {
    surface_resize(application_surface, rw, rh);
}

// set window size to screen pixel size:
window_set_size(rw, rh);

// set canvas size to page pixel size:
browser_stretch_canvas(w, h);

Downloads

Downloadable versions of the extension and a small sample project can be found on itch.io.

There is obviously more to good mobile scaling than just correct DPI, but this offers a good starting point while that's a story for another day.

Have fun!

Related posts:

Grid-based contour traversal

$
0
0

This is a small post about the algorithm I wrote for the recent pixel font tool to establish paths of shapes and holes on the image.

The task

The input is a grid of booleans representing state (filled / not filled) of each pixel in source image.

The output is an array of contours describing the shapes on the image. Each contour has an array of points, and array of holes in it, each of which also has an array of points describing it.

The output then can be fed to a polygon triangulation algorithm to generate geometry, or just written as-is in case of TTF format.

Also we very specifically do NOT want this:

because that counts as a self-intersecting path and can be very cursed.

Implementation

It goes like this:

1. Populate a temporary numeric grid with two special values representing whether a grid cell is empty or filled.

For my purposes I found use of min/max values for grid's value types to be good enough.

2. Establish the "outer" space by walking along the grid edges and running 8-directional flood fill starting from cells that hold the special "empty" value:

Here I used index 0 for outer space for convenience.
Given this sample, flood fill would run exactly once (as no areas are isolated by filled cells touching the grid edges)

3. Figure out unique "islands" of connected pixels by walking over the grid and doing a 4-directional flood fill with an incremental index starting from any cell that holds the special "filled" value:

As you can see, the I/O/B/8 main shapes get indexes 1, 2, 3, and 4, and the two detached grid cells on B's right edge get indexes 5 and 6 as we iterate over them afterwards. Storing the coordinates of each shape's first encountered (top-left) cell here will also prove handy later.

4. Do the same thing for holes inside the shapes, which by now are the only cells to still hold the special "empty" value:

So O's hole gets index -1 while 8's holes get indexes -2 and -3. Similarly, storing coordinates of each hole's top-left cell will be useful soon;
If you need to associate holes with their surrounding shapes, this is where you do that - since a hole is by definition in the middle of the shape, a cell to the left or top of a hole's top-left corner is warranted to be a shape cell.

5. Finally, with every shape and hole established, we can go over them to actually produce an outline, starting with (previously recorded) top-left corner and moving down (CCW):

The logic for this part is dead simple - so simple that it can be put into a small image:

That is:

  • If there's a wall ahead on the left and no wall blocking the path, move forward one cell (1).
  • If there's a wall ahead on the left and a wall blocking the path, add a point and turn right (2).
  • Otherwise add a point and turn left (3).
    This also covers the corner-touching case (4).

For holes, we simply invert the rule for what counts as wall.

And that's it!

The code

If you came here hoping to peek at a reference implementation, there's that too (in Haxe):

import haxe.ds.Vector;
class Contour {
	public var col:Int;
	public var row:Int;
	public var shape:Polygon = new Polygon();
	public var holes:Array<Polygon> = [];
	public function new() { }
	
	private static var grid:Vector<Vector<Int>>;
	private static var width:Int;
	private static var height:Int;
	private static var result:Array<Contour>;
	
	static function buildPoly(poly:Polygon, colStart:Int, rowStart:Int, z:Bool) {
		var points = poly.points;
		var dir = 3; // ENWS
		var col = colStart, row = rowStart, dirStart = dir;
		inline function add(x:Int, y:Int):Void {
			points.push(new Point(x, y));
		}
		var w = width, h = height, grid = Contour.grid;
		add(col, row);
		var steps = 0;
		while (steps == 0 || (col != colStart || row != rowStart || dir != dirStart)) {
			switch (dir) {
				case 0: { // right (BR corner)
					if ((col + 1 < w && grid[row][col + 1] > 0) == z) {
						if ((row + 1 < h && grid[row + 1][col + 1] > 0) == z) {
							add(col + 1, row + 1);
							dir = 3;
						}
						col += 1;
					} else {
						add(col + 1, row + 1);
						dir = 1;
					}
				};
				case 1: { // up (TR corner)
					if ((row > 0 && grid[row - 1][col] > 0) == z) {
						if ((col + 1 < w && grid[row - 1][col + 1] > 0) == z) {
							add(col + 1, row);
							dir = 0;
						}
						row -= 1;
					} else {
						add(col + 1, row);
						dir = 2;
					}
				};
				case 2: { // left (TL corner)
					if ((col > 0 && grid[row][col - 1] > 0) == z) {
						if ((row > 0 && grid[row - 1][col - 1] > 0) == z) {
							add(col, row);
							dir = 1;
						}
						col -= 1;
					} else {
						add(col, row);
						dir = 3;
					}
				};
				case 3: { // down (BL corner)
					if ((row + 1 < h && grid[row + 1][col] > 0) == z) {
						if ((col > 0 && grid[row + 1][col - 1] > 0) == z) {
							add(col, row + 1);
							dir = 2;
						}
						row += 1;
					} else {
						add(col, row + 1);
						dir = 0;
					}
				};
				default: break;
			}
			steps += 1;
		}
	}
	
	public static inline var defaultEmpty = -0x7FffFFff;
	public static inline var defaultSolid = 0x7FffFFff;
	static function floodFill(col:Int, row:Int, val:Int, is8dir:Bool = false) {
		var grid = Contour.grid;
		var w = Contour.width;
		var h = Contour.height;
		var ref = grid[row][col];
		//
		grid[row][col] = val;
		var queue = [];
		inline function add(_col:Int, _row:Int):Void {
			queue.push(_row);
			queue.push(_col);
		}
		add(col, row);
		while (queue.length > 0) {
			row = queue.shift();
			col = queue.shift();
			if (col > 0) {
				if (grid[row][col - 1] == ref) {
					grid[row][col - 1] = val;
					add(col - 1, row);
				}
			}
			if (col + 1 < w) {
				if (grid[row][col + 1] == ref) {
					grid[row][col + 1] = val;
					add(col + 1, row);
				}
			}
			if (row > 0) {
				if (grid[row - 1][col] == ref) {
					grid[row - 1][col] = val;
					add(col, row - 1);
				}
			}
			if (row + 1 < h) {
				if (grid[row + 1][col] == ref) {
					grid[row + 1][col] = val;
					add(col, row + 1);
				}
			}
			
			if (!is8dir) continue;
			if (col > 0 && row > 0) {
				if (grid[row - 1][col - 1] == ref) {
					grid[row - 1][col - 1] = val;
					add(col - 1, row - 1);
				}
			}
			if (col + 1 < w && row > 0) {
				if (grid[row - 1][col + 1] == ref) {
					grid[row - 1][col + 1] = val;
					add(col + 1, row - 1);
				}
			}
			if (col > 0 && row + 1 < h) {
				if (grid[row + 1][col - 1] == ref) {
					grid[row + 1][col - 1] = val;
					add(col - 1, row + 1);
				}
			}
			if (col + 1 < w && row + 1 < h) {
				if (grid[row + 1][col + 1] == ref) {
					grid[row + 1][col + 1] = val;
					add(col + 1, row + 1);
				}
			}
		}
	}
	
	public static function build(input:Vector<Vector<Bool>>):Array<Contour> {
		var h = input.length;
		if (h == 0) return [];
		var w = input[0].length;
		if (w == 0) return [];
		height = h;
		width = w;
		
		// create an temp grid:
		var grid = new Vector(h);
		for (row in 0 ... h) {
			var inRow = input[row];
			var outRow = new Vector(w);
			for (col in 0 ... w) outRow[col] = inRow[col] ? defaultSolid : defaultEmpty;
			grid[row] = outRow;
		}
		Contour.grid = grid;
		
		// fill boundaries:
		for (row in 0 ... h) {
			if (grid[row][0] == defaultEmpty) floodFill(0, row, 0, true);
			if (grid[row][w - 1] == defaultEmpty) floodFill(w - 1, row, 0, true);
		}
		for (col in 0 ... w) {
			if (grid[0][col] == defaultEmpty) floodFill(col, 0, 0, true);
			if (grid[h - 1][col] == defaultEmpty) floodFill(col, h - 1, 0, true);
		}
		
		// mark shapes:
		var ctrs = [];
		Contour.result = ctrs;
		var solidID = 0;
		for (row in 0 ... height) {
			for (col in 0 ... width) {
				var val = grid[row][col];
				if (val == defaultSolid) {
					floodFill(col, row, ++solidID);
					var ctr = new Contour();
					ctr.col = col;
					ctr.row = row;
					ctrs.push(ctr);
				}
			}
		}
		
		// mark holes:
		var emptyID = 0;
		for (row in 0 ... height) {
			for (col in 0 ... width) {
				var val = grid[row][col];
				if (val == defaultEmpty) {
					floodFill(col, row, --emptyID);
					var adj = grid[row - 1][col];
					var poly = new Polygon();
					buildPoly(poly, col, row, false);
					result[adj - 1].holes.push(poly);
				}
			}
		}
		
		// build contours:
		for (ctr in ctrs) {
			buildPoly(ctr.shape, ctr.col, ctr.row, true);
		}
		
		//
		Contour.result = null;
		return ctrs;
	}
}

class Polygon {
	public var points:Array<Point> = [];
	public function new() { }
}

class Point {
	public var x:Int;
	public var y:Int;
	public function new(x:Int, y:Int) {
		this.x = x;
		this.y = y;
	}
	public function toString() {
		return '($x, $y)';
	}
}

Closing notes

  • If the cells are warranted to not touch grid borders, you can skip the along-the-grid-border loop in step 2.
  • If the grid is warranted to only contain a single shape without holes, you could skip straight to step 5 by looping from top-left until you find the first pixel of the shape and then starting traversal.
  • For use cases where performance is important or garbage collection is expensive, resizing the same grid (to be large enough to fit new data) can be better than creating a new one.

Have fun!

Related posts:

Introducing: A pixel font generator!

$
0
0

I released a new tool today! It takes pixel font "tilesets" and converts them into actual TTF fonts! It is also 100% web-based, meaning that you can try it out right now. And this post details the development process for it. Also it doesn't have a catchy name for now since I've not come up with a good pun yet.

Context and motivation

Perhaps a thing that I'm less known for, but between various technical work I also draw various pixel fonts. Although the published collection is fairly neat and organized, there is also a number of unpublished fonts featuring unusual sizes and styles that seem like a good idea up until the point where you draw Latin W or Cyrillic Й (the ultimate nemesis of a fixed-height font):

Rest assured, if you draw many of these, you need a good process for editing, previewing, and saving the fonts.

I would at first experiment with FontStruct (tracing pixel glyphs in Font Forge or other "conventional" editors didn't seem like a good time), but found that the tool wasn't strictly geared towards pixel fonts (which is fair) - you could absolutely draw a pixel font in it, but tools the tools would usually do region rather than cell operations (e.g. the eraser highlights a rectangle to erase). Still, my account still spots early versions of what would later become 6w4 and 6w3 fonts:

Later I found BitFontMaker, which was a little better, but the paginated glyph grid left some to be desired (I would much rather just draw multiple glyphs on spot), and so did the drawing tools. So, in 2014 I made BitFontReader, which would process a PNG file and pump out JSON for BitFontMaker:

BitFontReader lasted for a while, but, as my fonts started to cover more and more glyphs (for example, 7w7 has almost 500), I found that drawing all your glyphs in a row isn't such a reliable approach - for example, if you miss a glyph, everything afterwards will be offset, so you'll have to find the spot, select everything on the right, move that a few pixels right... so sometime in 2016 I made another tool, still pumping out BitFontMaker JSON, but taking a "tileset" instead:

This was an improvement - drawing a 300-glyph font was no longer so crazy, but the process remained slightly janky (import image, export JSON, import JSON, export TTF, fix-up TTF using FontForge scripts), so I never ended up publicly releasing it, only having sent it to a bunch of people that I talked to about fonts often. Still, this was how I made most of the fonts I released.

Then, in late 2018 Rob Meek (creator of FontStruct) open-sourced the font generator library that was used by the service, and it had been on my mind ever since - if I were to solve a couple other technical challenges, everything could be so nice - I wouldn't have to go through several pages just to get a new TTF file, I wouldn't have to mess with FontForge scripts just to make fonts act right, and I could even make fonts larger than 12x12 pixels!

On that subject, how many "medium-sized" (>12px height) TTF/OTF pixel fonts have you seen? I think I've only seen a few in my life - perhaps because most people have better things to do with their lives than carefully tracing their pixel drawings in a vector-based font editor.

It would take some time for me to finally get around to it.

Technical

Font generation

As per above, with fonthx the actual generation, I'm left to feed it glyph data and input parameters.

Most of that was pretty straightforward, though I found that different software may interpret font parameters differently, leading me on a trail of interesting discoveries like this "you can't really use line gap" article (~17 years later, some software still uses em size for line size), or this gem of a tooltip in FontForge:

A fork of fonthx with additions that I found useful for this tool can be found on GitHub.

Resource loading

fonthx uses haxe.Resource to load a number of text files with charset data.

When targeting JS, files included that way are embedded into source code in base64 format.

Although the files themselves are admirably uniform, base64 versions are less so, and would add up to 3.2MB worth of JS code total. That's kind of a lot - the rest of my application compiles to less than 100KB of minified JS.

After some consideration I decided to get around this by integrating pako (pako_inflate.min.js is mere 22.6KB) and fetching a 320KB ZIP file with charset text files to populate haxe.Resource's internal list on boot.

Contour tracing

So on the aforementioned glyph data: as you might be aware, pixel fonts are made of pixels. TTF/OTF fonts are not made of pixels - not until they are drawn onto your screen anyway. They are made out of paths (CCW paths defining shapes and CW paths defining holes in them). So you want to convert your pixels into paths.

The simplest approach would be to simply generate a square for each pixel:

This is okay, although not very efficient, and some rendering artifacts may appear on unusual font sizes.

BitFontMaker goes for a slightly better approach, merging the squares but keeping the additional vertices:

Ideally, of course, you would want an optimized path - akin to what you would trace by hand:

So I wrote a simple algorithm for that:

You can read more about it in its own post.

In conclusion

Overall I'm pretty happy with how this turned out:

  • The tool is web-based, lightweight, and has no external dependencies.
  • The tool can also be downloaded and used offline (via itch.io).
    (which does not require running native code either)
  • The tool allows for arbitrary sized fonts, an improvement from BitFontMaker.
  • The tool allows to edit most of the useful metadata, also an improvement.
  • Combination of pixel-based input, JSON settings, and a variety of ways to re-import images (file dialog, drag and drop, even pasting them directly) allows for comfortable workflows in existing tools.

Have fun!

Related posts:

Viewing all 153 articles
Browse latest View live