[{"content":"I\u0026rsquo;ve made some good progress on Quackoban in the past few months, after a small hiatus in the early summer.\nThe first main focus I had was to add Steam Workshop support to the game, which I think is going to be a critical feature for the game, as it will allow players to create and share their own levels with the community.\nWhat is Steam Workshop? Steam Workshop is a platform provided by Valve that allows players to create, share, and download user-generated content for games. By integrating Steam Workshop into Quackoban, players will hopefully be able to share their custom levels with the community and discover new levels created by others.\nWhy add Steam Workshop support? Well, adding Steam Workshop support to Quackoban is something that I think will greatly enhance the game\u0026rsquo;s replayability and engagement.\nBy allowing players to create and share their own level packs, it should become possible for people to express their creativity and challenge others with their own unique puzzles. I am hoping that this will lead to some kind of thriving community around the game! And if not, at least it will give players a potential reason to keep coming back to the game, as there will always be new levels to try out, in a faster pace than I can create them myself.\nI also want to emphasize that I think there are some great puzzle designers out there that might not have the time or resources to create their own game, but they might be able to create some really interesting levels for games like Quackoban. This is a way for them to get their work out there and have it played by others, which I think is really cool.\nWhat kind of support have I added? \u0026ldquo;Steam Workshop support\u0026rdquo; doesn\u0026rsquo;t say much by itself, so let\u0026rsquo;s try to be clear about what I\u0026rsquo;ve added:\nThe ability for players to upload and update their own level packs to Steam Workshop The ability for players to play any level pack that you have subscribed to from Steam Workshop I haven\u0026rsquo;t added an in-game browser of the workshop unfortunately, with the hope that the one provided by Steam itself should be sufficient.\nIf it turns out that players really want an in-game browser, I might add that later.\nWhat does it look like to create and upload a level pack? I figured this might be useful to anyone who is interested in how I\u0026rsquo;ve decided to handle this, so here\u0026rsquo;s a quick overview of how level packs work in Quackoban.\nThe level pack management UI in-game The Official tab has all the official level packs that I have created and released. This tab doesn\u0026rsquo;t allow you to do much, other than view the level packs that are available.\nThe Local tab will have all the level packs that you have created and saved locally on your computer in %AppData%/Roaming/Quackoban/content-packs (including those that you have published to Steam Workshop). This tab has buttons for reloading from disk, opening the corresponding level pack folder, and uploading/updating a level pack on the Steam Workshop.\nThe Steam Workshop tab is (you guessed it) where you can see all the level packs that you have subscribed to from Steam Workshop. This also has a reload button, which will check for newly subscribed level packs.\nWhat does a level pack consist of? A level pack is quite simply a folder with the following content:\nmeta.json A meta.json file that contains the level pack\u0026rsquo;s metadata, such as its name, description, and author. Below is an example:\nicon.png This is just the icon that appears in the level pack management UI, and is also used as the thumbnail for the level pack on Steam Workshop by default. A 16x16px .png is required. It does not need to be called icon.png, it just needs to correspond to the name specified in the meta.json file.\nlevels.ldtk This is the actual level data file, which is created and edited using LDtk. This file contains all the levels in the level pack, as well as their layout and properties. The file does not have to be called levels.ldtk, but it does need to have the .ldtk extension, and there can only be one .ldtk file per level pack.\nI\u0026rsquo;ll be writing a separate post about how to make your own actual levels for Quackoban with LDtk in the future, so no more details for now - but stay tuned for that!\nOnce you\u0026rsquo;ve got that, how do you play your level pack? It\u0026rsquo;s quite simple really! You place the level pack folder in %AppData%/Roaming/Quackoban/content-packs, and then you can reload the level packs in the game, and your level pack should appear in the Local tab.\nThen all you have to do is create a new save and select your level pack from the list of available level packs! Easy!\nHow do you upload your level pack to Steam Workshop? Also, quite simple! Once you\u0026rsquo;ve got your level pack ready and in your local level packs folder, you can click the \u0026ldquo;Publish to Steam Workshop\u0026rdquo; button in the Local tab of the level pack management UI. This will create a private Steam Workshop item for your level pack, which you can then edit and make public in Steam itself.\nIt\u0026rsquo;ll have a default thumbnail, but you can modify that and other things to your heart\u0026rsquo;s content in the Workshop item editor on Steam. Once you\u0026rsquo;re happy, just make it public and then anyone can subscribe to your level pack and play it in Quackoban!\nIf you want to make changes to your level pack after you\u0026rsquo;ve published it, you can just update the level pack in your local folder and then click the \u0026ldquo;Publish update to Steam Workshop\u0026rdquo; button in the Local tab. This will update the existing Steam Workshop item with your file changes.\nFinishing up One of the reasons I have played a lot of other certain puzzle games has been the ability to both create and play other people\u0026rsquo;s own levels. It\u0026rsquo;s a great way to keep a game fresh and interesting, and I hope that by adding Steam Workshop support to Quackoban, it will have a similar effect on the game!\nI am very excited to see what kind of levels people will create and share with the community!\nOh and I just dropped a small patch to the Quackoban Demo with a few resolution improvements and other stuff! Make sure to check out the demo if you haven\u0026rsquo;t already, and wishlist the game to be notified when it\u0026rsquo;s released!\nQuick shoutout to Shifty! I was lucky enough to get a livestreamed playthrough of the Quackoban Demo by @shiftygamedev, and it was really fun to watch! Shifty offers to playtest games for developers, and I highly recommend checking out his Twitch channel if you\u0026rsquo;re interested in game development and want to see some fun playthroughs of in-development indie games.\nHe was (fortunately and unfortunately) the one to identify some of the resolution issues that I have since fixed in the latest patch, so I am very grateful for that. He also highlighted some other things that I will be looking into for future updates, so I am very thankful for his time and feedback! Thanks Shifty!\nThat\u0026rsquo;s it for now, quack quack!\n","permalink":"/posts/quackoban-level-packs-steam-workshop-support/","summary":"\u003cp\u003eI\u0026rsquo;ve made some good progress on Quackoban in the past few months, after a small hiatus in the early summer.\u003c/p\u003e\n\u003cp\u003eThe first main focus I had was to add Steam Workshop support to the game, which I think is going to be a critical feature for the game, as it will allow players to create and share their own levels with the community.\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Steam workshop branding\" loading=\"lazy\" src=\"/posts/quackoban-level-packs-steam-workshop-support/media/steam_workshop.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"what-is-steam-workshop\"\u003eWhat is Steam Workshop?\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://steamcommunity.com/workshop/\"\u003eSteam Workshop\u003c/a\u003e is a platform provided by Valve that allows players to create, share, and download user-generated content for games. By integrating Steam Workshop into Quackoban, players will hopefully be able to share their custom levels with the community and discover new levels created by others.\u003c/p\u003e","title":"Working on Level Packs \u0026 Steam Workshop support in Quackoban"},{"content":"So! It\u0026rsquo;s been a while since the last update, and a lot has happened since then! The game has an available demo on Steam, and the main game\u0026rsquo;s Steam page is live as well! Go wishlist the game to be notified about the release, and to support me and the game!\nWant to try out the demo? Check it out here!\nWant to wishlist the full game to know when it\u0026rsquo;s released? Check it out here!\nNot sold yet? Here\u0026rsquo;s the trailer for the game :)\nNow the game! As evident by the fact that the demo is out, the game is in a much more complete state than it was in the last update! There has been a lot of work on making new levels, polishing stuff, fixing bugs and improving the game in general.\nIt\u0026rsquo;s finally in a state where I feel comfortable sharing it with the world, and I hope you all will like it!\nThere\u0026rsquo;s around 50 levels as of yet, and I plan on shipping with AT LEAST 60, but hopefully I\u0026rsquo;ll have time to make even more! The demo has around 11 (and even more in a very soon update), just enough for me to give you a taste of what the game is like.\nMy gratitude to everyone who has played the demo! I\u0026rsquo;ve seen plenty of positive reactions to the demo and I\u0026rsquo;m very happy to see you all enjoying it! I\u0026rsquo;ve also gotten some great feedback and I am working hard on an update to the demo that will address some of the things mentioned by you all, and add some more content as well.\nWhile Quackoban won\u0026rsquo;t be released at the time of the event, I figured I could mention that the game will be featured in the upcoming Cerebral Puzzle Showcase on May 28th to June 4th 2026! The festival is one hosted by none other than Draknek \u0026amp; Friends, true connoisseurs of puzzle games, and I\u0026rsquo;m extremely honored to have been selected by their team!\nI have a lot of stuff planned for the release later this year (hopefully), and I can\u0026rsquo;t wait to share it with you all! In the meantime, please check out the demo and wishlist the game if you like what you see!\nExciting times ahead! Quack quack everyone!\nSee ya next time! :)\n","permalink":"/posts/quackoban-demo-and-more/","summary":"\u003cp\u003eSo! It\u0026rsquo;s been a while since the last update, and a lot has happened since then! The game has an available \u003ca href=\"https://store.steampowered.com/app/4523490/Quackoban_Demo/\"\u003edemo on Steam\u003c/a\u003e, and the \u003ca href=\"https://store.steampowered.com/app/4444330/Quackoban/\"\u003emain game\u0026rsquo;s Steam page\u003c/a\u003e is live as well! Go wishlist the game to be notified about the release, and to support me and the game!\u003c/p\u003e\n\u003cp\u003eWant to try out the demo? Check it out here!\u003c/p\u003e\n\u003cdiv class=\"steam-widget-container\"\u003e\r\n  \u003ciframe src=\"https://store.steampowered.com/widget/4523490/?utm_source=dcronqvist.se\u0026utm_medium=referral\u0026utm_campaign=presskit\" \r\n          frameborder=\"0\" \r\n          width=\"100%\" \r\n          height=\"190\"\u003e\r\n  \u003c/iframe\u003e\r\n\u003c/div\u003e\r\n\r\n\u003cstyle\u003e\r\n.steam-widget-container {\r\n  margin: 1.5rem 0;\r\n  max-width: 100%;\r\n}\r\n\r\n.steam-widget-container iframe {\r\n  border: none;\r\n  border-radius: var(--radius);\r\n  display: block;\r\n}\r\n\u003c/style\u003e\r\n\n\u003cp\u003eWant to wishlist the full game to know when it\u0026rsquo;s released? Check it out here!\u003c/p\u003e","title":"Quackoban demo and more!"},{"content":"Alright, it\u0026rsquo;s been a while since my last update on the puzzle game I\u0026rsquo;m working on, so here goes. I figure that screenshots and gifs are the best way to showcase the current state of the game.\nBeware that literally EVERYTHING is subject to change.\nOh, and in case it wasn\u0026rsquo;t clear, the game is called Quackoban (for now at least lol).\nGIFs! blocks of the same color snap together\nsome blocks will also move others\nLook at those same-colored blocks snapping together! That\u0026rsquo;s one of the core mechanics of the game, and will be the basis for many of the puzzles.\nsome plates will teleport units\npressure plates will activate stuff\nPressure plates are fun! Stepping on them will activate various other mechanisms in the level, such as sliding blocks, teleporters and more! I\u0026rsquo;m still not entirely sure about how these activation lines/redstone-looking things will look, they might need some polish to fit the art style better.\nsome can\u0026rsquo;t move unless connected to others\nsome plates will rotate entire units\nAha! Yet another core mechanic of the game - some blocks can\u0026rsquo;t move unless they\u0026rsquo;re connected to a certain amount of other blocks. A lot of these mechanics were quite challenging to implement, but they have definitely led to a lot of interesting puzzle possibilities. Actually, that might be for you to decide when you play the game\u0026hellip;\nearn achievements\nunlock customization options through achievements\nexplore the different regions of the map\nplay with keyboard or controller\nYou can also earn achievements and unlock customization options for your duck (also spot the bottom right base duck, that\u0026rsquo;s not a duck is it?)! The game features a world map with different regions to explore, each with its own set of puzzles and new mechanics. And of course, you can play with either keyboard or controller. I still have to ensure that the correct controller\u0026rsquo;s corresponding buttons are shown in the UI though.\nsome blocks will propagate activations\u0026hellip;\nAnd finally, here\u0026rsquo;s a sneak peek of the latest mechanic I\u0026rsquo;ve added. Not sure what to call them to be honest, internally they are still \u0026ldquo;signal carriers\u0026rdquo;. Anyway, these blocks will propagate activations to/from underneath them or to/from adjacent blocks to other signal carriers. This was THE most challenging mechanic to implement so far, I\u0026rsquo;m not convinced that it is working as I want it to still, but I have been able to make some interesting puzzles with it already.\nThat\u0026rsquo;s it for now! Hopefully that gives you an idea of where the game is at right now - even though I haven\u0026rsquo;t shown everything yet. I still have plenty of levels to design and create, but most of the core puzzle mechanics are in place.\nI aim to make sure that it is possible to make user-generated levels as well, for example through Steam Workshop - that way the community can come up with even more creative puzzles than I likely ever could.\nAnyway, I have a lot of level designing ahead of me, but I\u0026rsquo;m excited about the direction the game is taking. Releasing this year is no longer the plan, early next year seems more realistic - hoping to take part in some game festivals as well to gain a bit of exposure.\nStay tuned for more updates as I continue to develop Quackoban! Let me know if you\u0026rsquo;re interested in playtesting through the e-mail contact button in the footer.\nQuack quack!\n","permalink":"/posts/quackoban-initial-showcase-update/","summary":"\u003cp\u003eAlright, it\u0026rsquo;s been a while since my last update on the puzzle game I\u0026rsquo;m working on, so here goes. I figure that screenshots and gifs are the best way to showcase the current state of the game.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eBeware that literally EVERYTHING is subject to change.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eOh, and in case it wasn\u0026rsquo;t clear, the game is called \u003cem\u003eQuackoban\u003c/em\u003e (for now at least lol).\u003c/p\u003e\n\u003ch2 id=\"gifs\"\u003eGIFs!\u003c/h2\u003e\n\u003cdiv class=\"image-grid\" style=\"display: flex; flex-wrap: wrap; gap: 1rem; margin: 2rem 0;\"\u003e\n    \n\u003cfigure\u003e\n    \u003cimg loading=\"lazy\" src=\"media/juicy-connection.gif\"\n         alt=\"blocks of the same color snap together\"/\u003e \u003cfigcaption\u003e\n            \u003cp\u003eblocks of the same color snap together\u003c/p\u003e","title":"Quackoban initial showcase and progress update"},{"content":"So back in October 2024, I started working on a new project. I haven\u0026rsquo;t posted anything in a while, so I figured that I could share the progress I\u0026rsquo;ve made so far - going over the initial prototype, what some of the challenges have been this far, and what I plan to do next.\nWhat\u0026rsquo;s the game? The game is supposed to be a sokoban-like puzzle game. You control a duck that can push boxes around a grid-based map, where the goal is to push boxes into their designated spots. But wait, there\u0026rsquo;s more! The idea is to incorporate a bunch of different mechanics that make the otherwise simple box-pushing gameplay more interesting. The main mechanic that most other mechanics will build on is that boxes of the same color stick together and then move as a single unit (remember that word).\nAnyway, this was the prototype of the game that I threw together in a few days to see if the idea was worth pursuing:\nThere should have been a video here but your browser does not seem to support it. As is evident from the video, the gameplay is riveting and the graphics are stunning. Oh, and the video cuts out right before the win condition is met because it just immediately restarts the level, which is a bit annoying. But hey, it was just a prototype!\nYou\u0026rsquo;ll see what the game currently looks like later in this post, but first, let\u0026rsquo;s go over some of the major challenges (or frustrations) I\u0026rsquo;ve faced with this project so far.\nThe frustration that is \u0026ldquo;boxes of the same color stick together\u0026rdquo; Right, so the main twist of the game is that boxes of the same color stick together and move as a single unit. You\u0026rsquo;d think that it wouldn\u0026rsquo;t be that hard to implement, but it turned out to be a bit of a nightmare. I\u0026rsquo;ve later also learned that the famous Arvi \u0026ldquo;Hempuli\u0026rdquo; Teikari initially had an idea for a similar mechanic in his game Baba Is You, but ultimately decided against it because of \u0026ldquo;nightmare programming problems\u0026rdquo;. I was in for a ride, that\u0026rsquo;s for sure.\nAnyway, the main issue with this mechanic is that it must become the de facto way to move boxes around the grid. A single box must be considered a unit, just as much as a unit of 12 boxes, regardless of shape. Let\u0026rsquo;s take a look at a few different scenarios to illustrate the problem.\nRecursive box pushing In this scenario, the duck is pushing a red unit consisting of 3 boxes stacked vertically. Pushing that unit should push the blue box in front of it, for it to make sense. Getting this to work doesn\u0026rsquo;t look too hard does it? All that\u0026rsquo;s required is that we:\n1: Traverse grid to find entire unit of boxes that is being pushed.\n2: Attempt to move each box in the unit in the direction of the push.\n3: If all boxes in the unit can be moved, then move the entire unit.\nBut it should be relatively clear that step 2 is not as simple as it seems. What if there is a box in the way of one of the boxes in the unit (like in the example image)? We can\u0026rsquo;t just say that the entire unit can\u0026rsquo;t be moved, because that would mean that the blue box wouldn\u0026rsquo;t be pushed either. So we need to also check if the box in the way can be moved, and if it can, then that needs to move as well.\nWhat if we keep adding boxes/units in the way of the box being pushed? We need to keep checking if the boxes in the way can be moved. We need recursion to solve this problem. Let\u0026rsquo;s look at some untested Python for how this could be implemented:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def push_unit(unit, direction): resulting_pushes = [] for box in unit: position_after_push = (box.x + direction.dx, box.y + direction.dy) unit_at_push = get_unit_at_position(position_after_push) if unit_at_push is None: resulting_pushes.append((box, position_after_push)) continue pushed_unit, success = push_unit(unit_at_push, direction) if success: resulting_pushes.extend(pushed_unit) resulting_pushes.append((box, position_after_push)) else: return [], False return resulting_pushes, True I mean, that doesn\u0026rsquo;t look too bad right? The problem is that this isn\u0026rsquo;t going to suffice even for the scenario in the previous image. Attempting to push a unit along one of its axes will result in infinite recursion. Consider the left most blue box in the image above. Attempting to push it along the x-axis will result in it seeing that there is a unit (its own blue unit) at the position it is being pushed to, which will start the same push_unit routine for the exact same unit again.\nI managed to solve this problem by breaking down each unit into its own push-direction-aligned lines. So pushing a unit along the x-axis will break the unit down into its x-axis aligned lines, and pushing along the y-axis will break it down into its y-axis aligned lines.\nBy doing this, we can iterate over all the lines in the unit rather than all of its boxes. Pushing a line is actually a lot simpler, since it is only the front-most box in the line that needs to perform any kind of checking. The front-most box will check if there is a box in the way, and if there is, it will push that box (and its unit) recursively. If the front-most box can be pushed, then the entire line can be pushed, and if all lines in the unit can be pushed, then the entire unit can be pushed.\nBut we are still not done it seems\u0026hellip;\nIn the above image, if the duck pushes the red unit to the right, the red unit will attempt to push the duck, which will attempt to push the red unit, and so on. Yet another infinite recursion problem.\nI solved this by keeping a local grid that doesn\u0026rsquo;t contain the currently pushing unit in the push_unit routine. That way, the red unit will not see the duck as a box that it can push, and we can avoid the infinite recursion.\nLet\u0026rsquo;s take a look at even more untested code for the push_unit routine with the push-direction-aligned lines and the local grid:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 def push_unit(grid, unit, direction): resulting_pushes = [] unit_lines = get_unit_lines(unit, direction) local_grid_without_unit = remove_unit_from_grid(copy_grid(grid), unit) for line in unit_lines: front_box = get_front_of_line(line, direction) position_after_push = (front_box.x + direction.dx, front_box.y + direction.dy) # Passing the local grid without the pushing unit # prevents it from seeing the currently pushing unit unit_at_push = get_unit_at_position( local_grid_without_unit, position_after_push) if unit_at_push is None: resulting_pushes.extend(move_line(line, direction)) continue pushed_unit, success = push_unit( local_grid_without_unit, unit_at_push, direction) if success: resulting_pushes.extend(pushed_unit) resulting_pushes.extend(move_line(line, direction)) else: return [], False return resulting_pushes, True Now, while I haven\u0026rsquo;t written this game in Python, the same logic still applies to the actual C# code that I wrote. I\u0026rsquo;m sure this from-the-top-of-my-head-written Python doesn\u0026rsquo;t even cover many of the edge cases that I had to deal with, but it should give you an idea of the complexity of the problem.\nThis becomes the basis upon all movement in the game, so it is crucial that it works correctly, and can handle all the edge cases that I throw at it - it is a puzzle game after all.\nShow me the game! Alright, alright, enough with the boring technical details. Here\u0026rsquo;s what the game currently looks like (many iterations later of the same level shown in the prototype video):\nThere should have been a video here but your browser does not seem to support it. As you can see, the game has come quite a way since the initial prototype. The graphics are still very much a work in progress, but the gameplay is starting to take shape.\nWhat\u0026rsquo;s the plan? The plan is to continue developing the game, adding more levels, mechanics, and polish. I aim to release later this year, but that might change. I want to make sure that the game is fun and challenging, and that it has a good variety of levels and mechanics to keep players engaged.\nDue to the nature of the game, I don\u0026rsquo;t want to spoil too many of the mechanics or levels that I already have implemented, or am planning to implement. But! I do plan on making some more posts about the game in the future, going over some of the mechanics, how I make levels, and maybe even some of the technical details of the game engine itself. So stay tuned for that!\nOh, and if you\u0026rsquo;re interested in being part of a playtest, feel free to reach out to me. I regularly let people playtest the game to see what they think of mechanics, levels and the overall gameplay. There\u0026rsquo;s an e-mail contact button in the footer of the site - hit me up there and I\u0026rsquo;ll get back to you as soon as I can.\n","permalink":"/posts/working-on-something-new/","summary":"\u003cp\u003eSo back in October 2024, I started working on a new project. I haven\u0026rsquo;t posted anything in a while, so I figured that I could share the progress I\u0026rsquo;ve made so far - going over the initial prototype, what some of the challenges have been this far, and what I plan to do next.\u003c/p\u003e\n\u003ch1 id=\"whats-the-game\"\u003eWhat\u0026rsquo;s the game?\u003c/h1\u003e\n\u003cp\u003eThe game is supposed to be a \u003ca href=\"https://en.wikipedia.org/wiki/Sokoban\"\u003esokoban\u003c/a\u003e-like puzzle game. You control a duck that can push boxes around a grid-based map, where the goal is to push boxes into their designated spots. \u003cstrong\u003eBut wait, there\u0026rsquo;s more\u003c/strong\u003e! The idea is to incorporate a bunch of different mechanics that make the otherwise simple box-pushing gameplay more interesting. The main mechanic that most other mechanics will build on is that boxes of the same color \u003cem\u003estick\u003c/em\u003e together and then move as a single \u003cem\u003eunit\u003c/em\u003e (remember that word).\u003c/p\u003e","title":"I'm working on something new..."},{"content":"In my previous post, I showed how I wanted to transpile my C# lambdas to GLSL shaders in my rendering engine. I created a library, InspecTree, which gives you access to the entire syntax tree of a lambda, statement bodies and all. This was a great start, but I quickly realized that I would want more than just the syntax tree. I would also want to know the types of the variables, the return type of the lambda, which methods that were being called inside the lambda, and more specifically check if a lambda is capturing one or more variables from an outer scope (more on why this would be a problem later).\nBefore reading on, I highly recommend you check out my previous post to get a better understanding of what InspecTree is and how it works. Also check out the official Microsoft documentation on semantic analysis in C# here.\nHow do I semantically analyze a lambda? There\u0026rsquo;s a pretty simple answer to that actually, you need an instance of a SemanticModel which has a bunch of useful methods for retrieving symbols for different syntax trees. With that, you can get the type of different symbols, which methods that are being invoked and a bunch of other useful stuff. A SemanticModel can be retrieved from a Compilation, which leads us to the next question: how do I get a Compilation from my lambdas?\nSource generating a compilation for a lambda First thing to do is add a new property to the InspecTree\u0026lt;T\u0026gt; class so that we can store the SemanticModel for the lambda, so that users of the library can access it properly.\n1 2 3 4 5 6 public class InspecTree\u0026lt;TDelegate\u0026gt; where TDelegate : Delegate { public TDelegate Delegate { get; } public LambdaExpressionSyntax LambdaSyntax { get; } public SemanticModel SemanticModel { get; } } And then, creating a compilation is pretty easy, all we need to do is do something like this:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var source = @\u0026#34;() =\u0026gt; { float x = 2 + 5; Vector4 color = new Vector4(1.0f); if (x \u0026gt; 4) { color = new Vector4(0.2f, 0.3f, x, 1.0f); } return color; } \u0026#34;; var syntaxTree = CSharpSyntaxTree.ParseText(source); var compilation = CSharpCompilation.Create(\u0026#34;compilation\u0026#34;) .AddSyntaxTrees(syntaxTree); But that unfortunately won\u0026rsquo;t suffice\u0026hellip; The problem is that you need to make sure to add all needed references to the compilation. In the above example, we\u0026rsquo;re using the Vector4 type, which is part of the System.Numerics namespace. So we need to add a reference to that assembly during our compilation.\n1 2 3 var compilation = CSharpCompilation.Create(\u0026#34;compilation\u0026#34;) .AddReferences(MetadataReference.CreateFromFile(typeof(Vector4).Assembly.Location)); .AddSyntaxTrees(syntaxTree) But even then, we\u0026rsquo;re not done. Since we\u0026rsquo;re not fully qualifying the Vector4 type, there needs to be a using directive in the source code. So we need to add that as well.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 var source = @\u0026#34; using System.Numerics; () =\u0026gt; { float x = 2 + 5; Vector4 color = new Vector4(1.0f); if (x \u0026gt; 4) { color = new Vector4(0.2f, 0.3f, x, 1.0f); } return color; }\u0026#34;; And now we have a compilation that we can get our semantic model from! Now.. how to do this automatically for all the invocations that are intercepted by InspecTree?\nAutomatically generating a compilation for all intercepted lambdas Fortunately, we already collect all usings in the file that an intercepted lambda appears in in InspecTree. We can just add those to the source code before creating our compilation. We now get something like this for our intercepted invocations:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 [System.Runtime.CompilerServices.InterceptsLocation(@\u0026#34;absolute\\path\\to\\Program.cs\u0026#34;, line: 16, character: 23)] public static string TranspileCSharpToGLSLFragmentShader__INTERCEPTED_absolute_path_to_Program_cs_16_23(Func\u0026lt;Vector4\u0026gt; insp) { var overload_insp_source = @\u0026#34; using System; using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; var overload_insp_lambda = () =\u0026gt; { var x = 2 + 5f; var color = new Vector4(1); if (x \u0026gt; 4) { color = new Vector4(0.2f, 0.3f, x, 1.0f); } return color; } ;\u0026#34;; var overload_insp_syntaxTree = CSharpSyntaxTree.ParseText(overload_insp_source); var overload_insp_lambdaDeclaration = overload_insp_syntaxTree.GetRoot().DescendantNodes().OfType\u0026lt;VariableDeclarationSyntax\u0026gt;().Single(x =\u0026gt; x.Variables.Single().Identifier.Text == \u0026#34;overload_insp_lambda\u0026#34;); var overload_insp_lambdaExpression = overload_insp_lambdaDeclaration.DescendantNodes().OfType\u0026lt;LambdaExpressionSyntax\u0026gt;().Single(); var overload_insp_compilation = CSharpCompilation.Create(\u0026#34;overload_insp\u0026#34;) .AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)) .AddReferences(MetadataReference.CreateFromFile(@\u0026#34;C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\8.0.3\\System.Linq.dll\u0026#34;)) .AddReferences(MetadataReference.CreateFromFile(@\u0026#34;C:\\Program Files\\dotnet\\sdk\\8.0.202\\Roslyn\\bincore\\Microsoft.CodeAnalysis.dll\u0026#34;)) .AddReferences(MetadataReference.CreateFromFile(@\u0026#34;C:\\Program Files\\dotnet\\sdk\\8.0.202\\Roslyn\\bincore\\Microsoft.CodeAnalysis.CSharp.dll\u0026#34;)) .AddSyntaxTrees(overload_insp_syntaxTree); var overload_insp_semanticModel = overload_insp_compilation.GetSemanticModel(overload_insp_syntaxTree); var overload_insp = new InspecTree\u0026lt;Func\u0026lt;Vector4\u0026gt;\u0026gt;( insp, overload_insp_lambdaExpression, overload_insp_semanticModel); return TranspileCSharpToGLSLFragmentShader(overload_insp); } It looks very verbose and a bit messy, but that often ends up being the case for generated code like this. The important thing is that we now have a semantic model for the lambda, which can be used to further analyze the lambda.\nSo what can we do with the semantic model? Like shown in the example for InspecTree, we can now infer the types of the variables in the lambda and check if the lambda is capturing variables from an outer scope.\nHere\u0026rsquo;s a super small example of how I got the types of different expressions in the C# to GLSL transpiler.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private string GetGLSLType(ExpressionSyntax type) { var typeInfo = _semanticModel.GetTypeInfo(type).Type ?? throw new InvalidOperationException($\u0026#34;Could not determine type for {type}\u0026#34;); if (typeInfo.SpecialType == SpecialType.None) { var displayString = typeInfo.ToDisplayString(); return displayString switch { \u0026#34;System.Numerics.Vector4\u0026#34; =\u0026gt; \u0026#34;vec4\u0026#34;, _ =\u0026gt; throw new InvalidOperationException($\u0026#34;Unsupported type {typeInfo.Name}.\u0026#34;) }; } return typeInfo.SpecialType switch { SpecialType.System_Single =\u0026gt; \u0026#34;float\u0026#34;, SpecialType.System_Int32 =\u0026gt; \u0026#34;int\u0026#34;, SpecialType.System_Boolean =\u0026gt; \u0026#34;bool\u0026#34;, _ =\u0026gt; throw new InvalidOperationException($\u0026#34;Unsupported type {typeInfo.SpecialType}.\u0026#34;) }; } Furthermore, I added a checker which checks if the lambda is capturing variables from an outer scope. This is important because if it is, then we cannot transpile the lambda to a GLSL shader without first knowing the values of the captured variables - since the transpiler would not know what to substitute the variables with.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 int number = GetNumberFromSomewhere(); string glslCode = TranspileCSharpToGLSLFragmentShader(() =\u0026gt; { var x = 2 + 5f; var color = new Vector4(number); if (x \u0026gt; 4) { color = new Vector4(0.2f, 0.3f, x, 1.0f); } return color; }); Without being able to first evaluate the variable number, we cannot transpile the lambda to a GLSL shader. This is why it\u0026rsquo;s important to check if the lambda is capturing variables from an outer scope.\nIn the future I may look into the ability to evaluate the captured variables, but for now I\u0026rsquo;m happy with the progress I\u0026rsquo;ve made.\nConclusion I\u0026rsquo;m very happy with the progress I\u0026rsquo;ve made with InspecTree. It\u0026rsquo;s now possible to semantically analyze the lambdas, which opens up a lot of possibilities for the library. I\u0026rsquo;m excited to see what people will do with it, and I\u0026rsquo;m looking forward to seeing what I can do with it myself.\nOver and out! Here\u0026rsquo;s the repository: InspecTree.\n","permalink":"/posts/extending-inspectree/","summary":"\u003cp\u003eIn my \u003ca href=\"/posts/inspecting-csharp-lambdas/\"\u003eprevious post\u003c/a\u003e, I showed how I wanted to transpile my C# lambdas to GLSL shaders in my rendering engine. I created a library, InspecTree, which gives you access to the entire syntax tree of a lambda, statement bodies and all. This was a great start, but I quickly realized that I would want more than just the syntax tree. I would also want to know the types of the variables, the return type of the lambda, which methods that were being called inside the lambda, and more specifically check if a lambda is capturing one or more variables from an outer scope (more on why this would be a problem later).\u003c/p\u003e","title":"Extending InspecTree with support for semantic analysis"},{"content":"What\u0026rsquo;s Tiled? Tiled is a popular open source map editor for 2D games. It supports several file export formats, often times even specialized ones for specific game engines. I recommend you check it out if you haven\u0026rsquo;t already. It\u0026rsquo;s a great tool for creating 2D maps for your games and then exporting them in a format like JSON or XML to be used in your game engine of choice.\nThe problem In my never-ending game development journey, I\u0026rsquo;ve used Tiled for several projects. I\u0026rsquo;ve therefore also used many of the existing libraries for parsing Tiled maps in my .NET projects. However, a lot of these libraries seem to lack in the features that they support, or they are outdated and no longer maintained, or lack in documentation making them difficult to use.\nI wanted to use many of the newer features in Tiled, e.g. custom types. Unfortunately, none of the libraries I found supported the features I wanted. So I decided to create my own library, and made it a challenge for myself to make it as fast and memory efficient as possible, using BenchmarkDotNet to measure and compare performance.\nNote: This article is not about the library I created itself, but rather about the process of creating it. I will not go into detail about the library\u0026rsquo;s features, but rather focus on the process of creating a fast and memory efficient Tiled parsing library in C#. If you just want a link to the library -\u0026gt; DotTiled.\nReflection-based parsers Many of the existing libraries use reflection to parse the Tiled maps. This is a very flexible way of parsing the maps, but it comes with a relatively heavy performance cost. Here\u0026rsquo;s what a reflection-based parser might look like for a Tiled map:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 [XmlRoot(\u0026#34;map\u0026#34;)] public class TiledMap { public enum OrientationType { [XmlEnum(\u0026#34;orthogonal\u0026#34;)] Orthogonal, [XmlEnum(\u0026#34;isometric\u0026#34;)] Isometric, [XmlEnum(\u0026#34;staggered\u0026#34;)] Staggered, [XmlEnum(\u0026#34;hexagonal\u0026#34;)] Hexagonal } [XmlAttribute(\u0026#34;version\u0026#34;)] public int Version = 1; [XmlAttribute(\u0026#34;orientation\u0026#34;)] public OrientationType Orientation = OrientationType.Orthogonal; [XmlAttribute(\u0026#34;width\u0026#34;)] public int Width; [XmlAttribute(\u0026#34;height\u0026#34;)] public int Height; // Classes which have similar parsing [XmlElement(\u0026#34;layer\u0026#34;, typeof(TiledTileLayer))] [XmlElement(\u0026#34;objectgroup\u0026#34;, typeof(TiledObjectGroup))] [XmlElement(\u0026#34;imagelayer\u0026#34;, typeof(TiledImageLayer))] public List\u0026lt;TiledBaseLayer\u0026gt; Layers; } A class like this can then be used with an XmlSerializer to parse the Tiled map from an XML string or file. This is a very simple example and does not represent an entire Tiled map, but you can imagine how this could be extended to support entire maps and all their properties. You could apply similar attributes to classes like these to also support the different types of map formats that Tiled support.\nWhat other options besides reflection are there? Reflection is a powerful tool, and is often times a very appropriate one when it comes to parsing data and mapping them to corresponding classes. However, in this case, I wanted to make a library that was as fast and memory efficient as possible. Reflection is not the best tool for that job. Instead, I decided to use a more manual approach, where I would parse the XML or JSON files myself, without reflection.\nManual parsing C# has a useful XmlReader class that allows you to read XML strings in a forward-only manner. This is a very efficient way of parsing XML, as you only read the parts of the XML that you need, and you don\u0026rsquo;t have to load the entire XML file into memory.\nFor JSON, there is a similar forward-only reader called Utf8JsonReader. While it is fast and efficient, I decided against using it for this project as it is a bit more cumbersome than the relatively lightweight alternative: JsonDocument. JsonDocument is a bit heavier on memory, but it is easier to work with and is still relatively fast. And as benchmarks showed later, it was quite a lot faster than using reflection.\nHere\u0026rsquo;s how I parse a Tiled map using XmlReader:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 internal Map ReadMap() { var version = _reader.GetRequiredAttribute(\u0026#34;version\u0026#34;); var tiledVersion = _reader.GetOptionalAttribute(\u0026#34;tiledversion\u0026#34;); var @class = _reader.GetOptionalAttribute(\u0026#34;class\u0026#34;).GetValueOr(\u0026#34;\u0026#34;); var orientation = _reader.GetRequiredAttributeEnum\u0026lt;MapOrientation\u0026gt;(\u0026#34;orientation\u0026#34;, Helpers.CreateMapper\u0026lt;MapOrientation\u0026gt;( s =\u0026gt; throw new InvalidOperationException($\u0026#34;Unknown orientation \u0026#39;{s}\u0026#39;\u0026#34;), (\u0026#34;orthogonal\u0026#34;, MapOrientation.Orthogonal), (\u0026#34;isometric\u0026#34;, MapOrientation.Isometric), (\u0026#34;staggered\u0026#34;, MapOrientation.Staggered), (\u0026#34;hexagonal\u0026#34;, MapOrientation.Hexagonal) )); var renderOrder = _reader.GetOptionalAttributeEnum\u0026lt;RenderOrder\u0026gt;(\u0026#34;renderorder\u0026#34;, s =\u0026gt; s switch { \u0026#34;right-down\u0026#34; =\u0026gt; RenderOrder.RightDown, \u0026#34;right-up\u0026#34; =\u0026gt; RenderOrder.RightUp, \u0026#34;left-down\u0026#34; =\u0026gt; RenderOrder.LeftDown, \u0026#34;left-up\u0026#34; =\u0026gt; RenderOrder.LeftUp, _ =\u0026gt; throw new InvalidOperationException($\u0026#34;Unknown render order \u0026#39;{s}\u0026#39;\u0026#34;) }).GetValueOr(RenderOrder.RightDown); var compressionLevel = _reader.GetOptionalAttributeParseable\u0026lt;int\u0026gt;(\u0026#34;compressionlevel\u0026#34;).GetValueOr(-1); var width = _reader.GetRequiredAttributeParseable\u0026lt;uint\u0026gt;(\u0026#34;width\u0026#34;); var height = _reader.GetRequiredAttributeParseable\u0026lt;uint\u0026gt;(\u0026#34;height\u0026#34;); var tileWidth = _reader.GetRequiredAttributeParseable\u0026lt;uint\u0026gt;(\u0026#34;tilewidth\u0026#34;); var tileHeight = _reader.GetRequiredAttributeParseable\u0026lt;uint\u0026gt;(\u0026#34;tileheight\u0026#34;); var hexSideLength = _reader.GetOptionalAttributeParseable\u0026lt;uint\u0026gt;(\u0026#34;hexsidelength\u0026#34;); var staggerAxis = _reader.GetOptionalAttributeEnum\u0026lt;StaggerAxis\u0026gt;(\u0026#34;staggeraxis\u0026#34;, s =\u0026gt; s switch { \u0026#34;x\u0026#34; =\u0026gt; StaggerAxis.X, \u0026#34;y\u0026#34; =\u0026gt; StaggerAxis.Y, _ =\u0026gt; throw new InvalidOperationException($\u0026#34;Unknown stagger axis \u0026#39;{s}\u0026#39;\u0026#34;) }); var staggerIndex = _reader.GetOptionalAttributeEnum\u0026lt;StaggerIndex\u0026gt;(\u0026#34;staggerindex\u0026#34;, s =\u0026gt; s switch { \u0026#34;odd\u0026#34; =\u0026gt; StaggerIndex.Odd, \u0026#34;even\u0026#34; =\u0026gt; StaggerIndex.Even, _ =\u0026gt; throw new InvalidOperationException($\u0026#34;Unknown stagger index \u0026#39;{s}\u0026#39;\u0026#34;) }); var parallaxOriginX = _reader.GetOptionalAttributeParseable\u0026lt;float\u0026gt;(\u0026#34;parallaxoriginx\u0026#34;).GetValueOr(0.0f); var parallaxOriginY = _reader.GetOptionalAttributeParseable\u0026lt;float\u0026gt;(\u0026#34;parallaxoriginy\u0026#34;).GetValueOr(0.0f); var backgroundColor = _reader.GetOptionalAttributeClass\u0026lt;Color\u0026gt;(\u0026#34;backgroundcolor\u0026#34;).GetValueOr(Color.Parse(\u0026#34;#00000000\u0026#34;, CultureInfo.InvariantCulture)); var nextLayerID = _reader.GetRequiredAttributeParseable\u0026lt;uint\u0026gt;(\u0026#34;nextlayerid\u0026#34;); var nextObjectID = _reader.GetRequiredAttributeParseable\u0026lt;uint\u0026gt;(\u0026#34;nextobjectid\u0026#34;); var infinite = _reader.GetOptionalAttributeParseable\u0026lt;uint\u0026gt;(\u0026#34;infinite\u0026#34;).GetValueOr(0) == 1; var propertiesCounter = 0; List\u0026lt;IProperty\u0026gt; properties = Helpers.ResolveClassProperties(@class, _customTypeResolver); List\u0026lt;BaseLayer\u0026gt; layers = []; List\u0026lt;Tileset\u0026gt; tilesets = []; _reader.ProcessChildren(\u0026#34;map\u0026#34;, (r, elementName) =\u0026gt; elementName switch { \u0026#34;properties\u0026#34; =\u0026gt; () =\u0026gt; Helpers.SetAtMostOnceUsingCounter(ref properties, Helpers.MergeProperties(properties, ReadProperties()).ToList(), \u0026#34;Properties\u0026#34;, ref propertiesCounter), \u0026#34;tileset\u0026#34; =\u0026gt; () =\u0026gt; tilesets.Add(ReadTileset(version, tiledVersion)), \u0026#34;layer\u0026#34; =\u0026gt; () =\u0026gt; layers.Add(ReadTileLayer(infinite)), \u0026#34;objectgroup\u0026#34; =\u0026gt; () =\u0026gt; layers.Add(ReadObjectLayer()), \u0026#34;imagelayer\u0026#34; =\u0026gt; () =\u0026gt; layers.Add(ReadImageLayer()), \u0026#34;group\u0026#34; =\u0026gt; () =\u0026gt; layers.Add(ReadGroup()), _ =\u0026gt; r.Skip }); return new Map { Version = version, TiledVersion = tiledVersion, Class = @class, Orientation = orientation, RenderOrder = renderOrder, CompressionLevel = compressionLevel, Width = width, Height = height, TileWidth = tileWidth, TileHeight = tileHeight, HexSideLength = hexSideLength, StaggerAxis = staggerAxis, StaggerIndex = staggerIndex, ParallaxOriginX = parallaxOriginX, ParallaxOriginY = parallaxOriginY, BackgroundColor = backgroundColor, NextLayerID = nextLayerID, NextObjectID = nextObjectID, Infinite = infinite, Properties = properties ?? [], Tilesets = tilesets, Layers = layers }; } Pretty verbose, right? In fact, it would have been much more verbose if it wasn\u0026rsquo;t for the amount of extension methods I\u0026rsquo;ve created for XmlReader. The GetRequiredAttribute and GetOptionalAttribute are examples of such extension methods that make it easier for me to semantically argue about the attributes I expect to be there.\nHowever, even though it is quite verbose: it\u0026rsquo;s readable and fast. The ReadTileLayer, ReadObjectLayer, ReadImageLayer, and ReadGroup all look very similar to ReadMap and read their corresponding expected attributes and elements.\nFor JSON, it is extremely similar; I made identical extension methods to JsonElement to be able to retrieve values in a required or optional manner.\nBenchmarking my library DotTiled against other libraries After all this effort, it would be a shame if it turned out that it was only marginally faster than the reflection-based parsing. However, it turned out to be quite a lot faster.\nBelow is the output from BenchmarkDotNet when comparing DotTiled against two other similar libraries that use reflection-based parsing.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 BenchmarkDotNet v0.13.12, Windows 10 (10.0.19045.4780/22H2/2022Update) 12th Gen Intel Core i7-12700K, 1 CPU, 20 logical and 12 physical cores .NET SDK 8.0.202 [Host] : .NET 8.0.3 (8.0.324.11423), X64 RyuJIT AVX2 DefaultJob : .NET 8.0.3 (8.0.324.11423), X64 RyuJIT AVX2 | Method | Categories | Mean | Ratio | Allocated | Alloc Ratio | |------------ |-------------- |---------:|------:|----------:|------------:| | DotTiled | MapJsonString | 4.249 μs | 1.00 | 5.65 KB | 1.00 | | TiledLib | MapJsonString | 5.904 μs | 1.39 | 9.02 KB | 1.60 | | | | | | | | | DotTiled | MapXMLString | 2.986 μs | 1.00 | 16.47 KB | 1.00 | | TiledLib | MapXMLString | 5.372 μs | 1.80 | 23.32 KB | 1.42 | | TiledCSPlus | MapXMLString | 6.318 μs | 2.12 | 33.16 KB | 2.01 | From the above table, it\u0026rsquo;s clear that DotTiled is about 45% faster at parsing a small and simple map (in the XML format) than the runner-up TiledLib, and more than 50% faster than TiledCSPlus.\nThe JSON format is not as drastic, likely due to the usage of JsonElement, which has a larger overhead than the forward-only reader Utf8JsonReader. Still, a close to 30% increase in speed for a simple map is a great result.\nNote: TiledCSPlus does not have support for the JSON map format, which is why it isn\u0026rsquo;t included in the benchmarks for JSON maps.\nWhen it comes to memory, the JSON parsing outperformed TiledLib by almost 40%, which is very impressive! The XML parsing also managed to outperform TiledLib by around 30%.\nConclusion So what\u0026rsquo;s the verdict? Well, DotTiled performed about 40% better than TiledLib in speed on average, and about 35% better than TiledLib in memory usage on average.\nIt seems like my efforts on manual parsing really did pay off in the end, and the result is a fast and memory efficient Tiled map parsing library for anyone to use. The maintainer of Tiled also mentions the library in their documentation.\n","permalink":"/posts/tiled-parsing-library/","summary":"\u003ch1 id=\"whats-tiled\"\u003eWhat\u0026rsquo;s Tiled?\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://www.mapeditor.org/\"\u003eTiled is a popular open source map editor for 2D games\u003c/a\u003e. It supports several file export formats, often times even specialized ones for specific game engines. I recommend you check it out if you haven\u0026rsquo;t already. It\u0026rsquo;s a great tool for creating 2D maps for your games and then exporting them in a format like JSON or XML to be used in your game engine of choice.\u003c/p\u003e\n\u003ch1 id=\"the-problem\"\u003eThe problem\u003c/h1\u003e\n\u003cp\u003eIn my never-ending game development journey, I\u0026rsquo;ve used Tiled for several projects. I\u0026rsquo;ve therefore also used many of the existing libraries for parsing Tiled maps in my .NET projects. However, a lot of these libraries seem to lack in the features that they support, or they are outdated and no longer maintained, or lack in documentation making them difficult to use.\u003c/p\u003e","title":"Making a fast and memory efficient Tiled parsing library in C#"},{"content":"What\u0026rsquo;s an Expression\u0026lt;T\u0026gt;? I strongly suggest you refer to the official documentation for a more in-depth explanation, but in short, an Expression\u0026lt;T\u0026gt; is a type in C# that will give you access to a supplied lambda expression as a syntax-tree-like object. This expression tree can then be traversed and inspected, and you can do all sorts of cool things with it, e.g. transpile it to another language, or even execute it.\nThe problem I have a long-lasting project that is a rendering engine. In that rendering engine, I wanted to be able to write shaders in C# and then transpile them to some shader language, like GLSL or HLSL. With the help of Expression\u0026lt;T\u0026gt;, I could easily inspect the lambda expressions that represented the shaders and transpile them to the desired language. However, that only got me so far\u0026hellip; The shaders I was able to write could only contain a single expression that would return a value for the pixel color. Like this:\n1 2 var shader = _renderer.CreateFragmentShader(() =\u0026gt; new Vector4(1, 0, 0, 1) * opacity); But I wanted more! I wanted to be able to write routines, with control flow, sub-routines, and all that jazz, all in lambdas. I wanted to be able to write something like this:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 var shader = _renderer.CreateFragmentShader(() =\u0026gt; { var color = new Vector4(1, 0, 0, 1); if (someCondition) { color = new Vector4(0, 1, 0, 1); } if (someOtherCondition) { color = SampleTexture2D(someTexture, someUV) * color; } return color; }); And have it be transpiled to something like this:\n1 2 3 4 5 6 7 8 9 10 11 12 13 void main() { vec4 color = vec4(1, 0, 0, 1); if (someCondition) { color = vec4(0, 1, 0, 1); } if (someOtherCondition) { color = texture(someTexture, someUV) * color; } gl_FragColor = color; } But that simply wasn\u0026rsquo;t possible with Expression\u0026lt;T\u0026gt;, due to the following limitation CS0834 - A lambda expression with a statement body cannot be converted to an expression tree. There are ways to create expression trees that contain several statements with the help of BlockExpression. However, those blocks and statements need to be created manually, which isn\u0026rsquo;t very cool. So I set out to solve this problem, and the result is a library I call InspecTree.\nAttempts by others I\u0026rsquo;m obviously not the only one to have encountered this problem. In fact, it\u0026rsquo;s mentioned several times in this GitHub issue on the C# language design repository. The closest thing I could find was a library called ExpressionFutures. It\u0026rsquo;s a library that extends the capabilities of Expression\u0026lt;T\u0026gt; by providing a custom fork of the Roslyn compiler. My aim was to create something that was much more lightweight and easy to use, where you still could make use of the standard C# compiler.\nMy vision I wanted a similar API to Expression\u0026lt;T\u0026gt;, but for entire methods. I envisioned something like this in my rendering engine:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 void Main() { var shader = CreateFragmentShader(color =\u0026gt; { if (someCondition) { color = new Vector4(0, 1, 0, 1); } if (someOtherCondition) { color = SampleTexture2D(someTexture, someUV) * color; } return color; }); } Shader CreateFragmentShader(InspecTree\u0026lt;Func\u0026lt;Vector4, Vector4\u0026gt;\u0026gt; shader) { var glslCode = TranspileToGLSL(shader); return _renderer.CreateShaderFromGLSL(glslCode); } string TranspileToGLSL(InspecTree\u0026lt;Func\u0026lt;Vector4, Vector4\u0026gt;\u0026gt; shader) { var syntaxTree = shader.SyntaxTree; var glslTranspiler = new GLSLTranspiler(); var glslCode = glslTranspiler.Transpile(syntaxTree); return glslCode; } Line 25 .SyntaxTree is the key here. It should give you the syntax tree of the supplied method, which you then can traverse, inspect, and transpile to another language. I wanted to use existing Roslyn APIs to achieve this, and expose a SyntaxTree instance, for which there already exists visitors and other utilities.\nHowever, it quickly became apparent that this would be a much more complex challenge than I first anticipated. You are about to learn about the challenges I faced, and how I solved them in the following sections.\nYou cannot implicitly convert a lambda to a custom class The first challenge I faced was that I wanted to be able to pass a lambda to a method that had a parameter of type InspecTree\u0026lt;T\u0026gt;. I thought that an implicit conversion operator from a Func\u0026lt;T\u0026gt; to an InspecTree\u0026lt;T\u0026gt; would solve this, but that was not the case. The compiler is not able to first implicitly convert a lambda to a Func\u0026lt;T\u0026gt;, and then implicitly convert that Func\u0026lt;T\u0026gt; to an InspecTree\u0026lt;T\u0026gt; (StackOverflow). So I had to come up with another solution.\nI had dabbled a bit with C# source generators in the past, and figured that I could give them a try. I created a source generator that would generate an overload for any method that accepts an InspecTree\u0026lt;T\u0026gt; parameter. This overload would instead accept a Func\u0026lt;T\u0026gt; parameter, and then create an InspecTree\u0026lt;T\u0026gt; instance from that lambda.\nGiven the following consumer project:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 partial class Program { static void Main(string[] args) { Test(x =\u0026gt; { var y = x + 1; return y; }); } static void Test(InspecTree\u0026lt;Func\u0026lt;int, int\u0026gt;\u0026gt; func) { Console.WriteLine(func.Delegate.Invoke(42)) } } The source generator generates the following overload for the Test method:\n1 2 3 4 5 6 7 8 partial class Program { static void Test(Func\u0026lt;int, int\u0026gt; func) { var overload_func = new InspecTree\u0026lt;Func\u0026lt;int, int\u0026gt;\u0026gt;(func); Test(overload_func); } } With that in place, I could now pass a lambda to a method that accepted an InspecTree\u0026lt;T\u0026gt; parameter. The source generator would take care of the conversion for me.\nGetting the syntax tree of a lambda Now we are getting to the juicy part. How do you get the syntax tree of a lambda? Well, it turns out that you can\u0026rsquo;t. At least not directly. I had several ideas on how to solve this though, but I will only mention the first failed attempt and then the successful one.\nFailed: Generate an equivalent SyntaxTree using SyntaxFactory from a source generator for all lambdas in a program. This would be a very complex task. I would need to keep track of lambda -\u0026gt; SyntaxTree mappings, and then somehow inject the generated SyntaxTree into the lambda. I quickly dismissed this idea.\nSuccessful: Use the CSharpSyntaxTree.ParseText in combination with interceptors, a new feature in C# 12. Interceptors are extremely powerful in that they can replace any invocation to a method with a completely different method, and they were perfect for this use case.\nI started by modifying my existing source generator to instead only generate a completely empty overload, like this:\n1 2 3 4 5 6 7 partial class Program { static void Test(Func\u0026lt;int, int\u0026gt; func) { // Empty overload } } The idea was that this overload was now only for the purpose of allowing consumers to pass a lambda to a method that accepted an InspecTree\u0026lt;T\u0026gt; parameter without a compilation error.\nThen I created a new source generator that would generate interceptors for all invocations to this generated overload. In the following example, the only invocation is in the Main method:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 partial class Program { static void Main(string[] args) { Test(x =\u0026gt; { var y = x + 1; return y; }); } static void Test(InspecTree\u0026lt;Func\u0026lt;int, int\u0026gt;\u0026gt; func) { Console.WriteLine(func.Delegate.Invoke(42)) } } The interceptor source generator then finds all invocations to these overloads, retrieves the syntax trees from the supplied arguments to the methods, and then intercepts each invocation individually. The interceptor source generator generates the following code for the invocation in the Main method:\n1 2 3 4 5 6 7 8 9 10 11 12 13 [System.Runtime.CompilerServices.InterceptsLocation(@\u0026#34;absolute/path/to/Program.cs\u0026#34;, line: 5, character: 5)] public static void Test__INTERCEPTED_absolute_path_to_Program_cs__7__5(Func\u0026lt;int, int\u0026gt; func) { var overload_func = new InspecTree\u0026lt;Func\u0026lt;int, int\u0026gt;\u0026gt;(func, \u0026#34;\u0026#34;\u0026#34; x =\u0026gt; { var y = x + 1; return y; } \u0026#34;\u0026#34;\u0026#34;); Test(overload_func); } If there would have been two calls to the Test overload, there would be two interceptors generated, each with their own unique lambda syntax tree string representation.\nAll I had to do after this was to parse the lambda syntax tree string inside the InspecTree\u0026lt;T\u0026gt; constructor, and then I would have the syntax tree of the lambda. Using a simple CSharpSyntaxWalker, I could traverse the syntax tree and extract the information I needed.\nConclusion Although challenging, I managed to find a way to inspect entire C# methods using the Roslyn APIs. Source generators and Interceptors were key in solving this problem, and I\u0026rsquo;m very happy with the result. The library is still in its early stages, but I\u0026rsquo;m planning on making it public soon.\n","permalink":"/posts/inspecting-csharp-lambdas/","summary":"\u003ch1 id=\"whats-an-expressiont\"\u003eWhat\u0026rsquo;s an \u003ccode\u003eExpression\u0026lt;T\u0026gt;\u003c/code\u003e?\u003c/h1\u003e\n\u003cp\u003eI strongly suggest you refer to the \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/expression-trees/expression-trees-execution\"\u003eofficial documentation\u003c/a\u003e for a more in-depth explanation, but in short, an \u003ccode\u003eExpression\u0026lt;T\u0026gt;\u003c/code\u003e is a type in C# that will give you access to a supplied lambda expression as a syntax-tree-like object. This expression tree can then be traversed and inspected, and you can do all sorts of cool things with it, e.g. transpile it to another language, or even execute it.\u003c/p\u003e","title":"InspecTree: C# Expression\u003cT\u003e with statement body support"},{"content":"Well, here we are. My first post. I don\u0026rsquo;t know how often I\u0026rsquo;ll make these, but it feels like fun. I have a few ideas of things I want to write about, so expect posts in a near future.\n","permalink":"/posts/hello-world/","summary":"\u003cp\u003eWell, here we are. My first post. I don\u0026rsquo;t know how often I\u0026rsquo;ll make these, but it feels like fun. I have a few ideas of things I want to write about, so expect posts in a near future.\u003c/p\u003e","title":"Hello World!"},{"content":"Steam Page Presskit download download presskit as .zip (23.9 MB)\rVideos Initial Quackoban Gameplay Trailer YouTube\nScreenshots Logo \u0026amp; Icon ","permalink":"/games/quackoban/","summary":"\u003ch3 id=\"steam-page\"\u003eSteam Page\u003c/h3\u003e\n\u003cdiv class=\"steam-widget-container\"\u003e\r\n  \u003ciframe src=\"https://store.steampowered.com/widget/4444330/?utm_source=dcronqvist.se\u0026utm_medium=referral\u0026utm_campaign=presskit\" \r\n          frameborder=\"0\" \r\n          width=\"100%\" \r\n          height=\"190\"\u003e\r\n  \u003c/iframe\u003e\r\n\u003c/div\u003e\r\n\r\n\u003cstyle\u003e\r\n.steam-widget-container {\r\n  margin: 1.5rem 0;\r\n  max-width: 100%;\r\n}\r\n\r\n.steam-widget-container iframe {\r\n  border: none;\r\n  border-radius: var(--radius);\r\n  display: block;\r\n}\r\n\u003c/style\u003e\r\n\n\u003ch3 id=\"presskit-download\"\u003ePresskit download\u003c/h3\u003e\n\u003ca href=\"/games/quackoban/presskit/quackoban-presskit.zip\" download class=\"download-card\"\u003e\r\n  \u003cspan\u003edownload presskit as .zip (23.9 MB)\u003c/span\u003e\r\n\u003c/a\u003e\r\n\r\n\u003cstyle\u003e\r\n.download-card {\r\n  display: block;\r\n  width: 100%;\r\n  padding: 0.5rem 1rem;\r\n  background-color: var(--entry);\r\n  border: 1px solid #adff2f;\r\n  border-radius: var(--radius);\r\n  color: var(--primary);\r\n  font-size: 0.9rem;\r\n}\r\n\r\n.download-card:hover {\r\n  text-decoration: underline;\r\n}\r\n\u003c/style\u003e\r\n\n\u003ch3 id=\"videos\"\u003eVideos\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eInitial Quackoban Gameplay Trailer\u003c/strong\u003e \u003ca href=\"https://youtu.be/4aNWxdf4fCo\"\u003eYouTube\u003c/a\u003e\u003c/p\u003e\n\u003cdiv style=\"position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;\"\u003e\n      \u003ciframe allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen\" loading=\"eager\" referrerpolicy=\"strict-origin-when-cross-origin\" src=\"https://www.youtube.com/embed/4aNWxdf4fCo?autoplay=0\u0026amp;controls=1\u0026amp;end=0\u0026amp;loop=0\u0026amp;mute=0\u0026amp;start=0\" style=\"position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;\" title=\"YouTube video\"\u003e\u003c/iframe\u003e\n    \u003c/div\u003e\n\n\u003ch3 id=\"screenshots\"\u003eScreenshots\u003c/h3\u003e\n\u003cp\u003e\u003cfigure\u003e\n    \u003cimg loading=\"lazy\" src=\"media/1.png\"\n         alt=\"Gameplay Screenshot 1\"/\u003e \n\u003c/figure\u003e\n\n\u003cfigure\u003e\n    \u003cimg loading=\"lazy\" src=\"media/2.png\"\n         alt=\"Gameplay Screenshot 2\"/\u003e \n\u003c/figure\u003e\n\n\u003cfigure\u003e\n    \u003cimg loading=\"lazy\" src=\"media/3.png\"\n         alt=\"Gameplay Screenshot 3\"/\u003e \n\u003c/figure\u003e\n\n\u003cfigure\u003e\n    \u003cimg loading=\"lazy\" src=\"media/4.png\"\n         alt=\"Gameplay Screenshot 4\"/\u003e \n\u003c/figure\u003e\n\n\u003cfigure\u003e\n    \u003cimg loading=\"lazy\" src=\"media/5.png\"\n         alt=\"Gameplay Screenshot 5\"/\u003e \n\u003c/figure\u003e\n\u003c/p\u003e","title":"Quackoban"}]