﻿<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
	<channel>
		<title />
		<link>https://blog.structed.me/</link>
		<description />
		<copyright>Copyright © Johannes Ebner 2024</copyright>
		<pubDate>Thu, 05 Dec 2024 20:51:26 GMT</pubDate>
		<lastBuildDate>Thu, 05 Dec 2024 20:51:26 GMT</lastBuildDate>
		<item>
			<title>Announcing godot-playfab 1.3.0</title>
			<link>https://blog.structed.me/posts/godot-playfab-1.3.0</link>
			<description>godot-playfab 1.3.0 - Now with Steam Login</description>
			<enclosure url="https://blog.structed.me/images/posts/godot-playfab-1.3.0/godot-playfab_splash-screen-1.3.0.png" length="0" type="image" />
			<guid>https://blog.structed.me/posts/godot-playfab-1.3.0</guid>
			<pubDate>Thu, 05 Dec 2024 00:00:00 GMT</pubDate>
			<content:encoded>&lt;h1 id="godot-playfab-update-1.3.0"&gt;Godot-PlayFab Update 1.3.0&lt;/h1&gt;
&lt;p&gt;After almost a year, I finally found the time to do revamp the CI pipeline that held up some updates. I'd like to take the opportunity to thank &lt;a href="https://github.com/MikeSchulze"&gt;Mike Schulze&lt;/a&gt; for making an awesome &lt;a href="https://github.com/MikeSchulze/gdUnit4-action"&gt;GitHub Action&lt;/a&gt; to run &lt;a href="https://github.com/MikeSchulze/gdUnit4"&gt;GDUnit&lt;/a&gt; Tests!&lt;/p&gt;
&lt;p&gt;Now, to the changes!&lt;/p&gt;
&lt;h2 id="steam-login"&gt;Steam Login&lt;/h2&gt;
&lt;p&gt;Thanks to &lt;a href="https://github.com/TheDedemon"&gt;TheDedemon&lt;/a&gt; aka &lt;em&gt;Rémi Carreira&lt;/em&gt;, godot-layfab now has support for Steam Logins! TheDedemon also contributed a guide on how to use it with Gramps Garcia's &lt;a href="https://godotsteam.com/"&gt;GodotSteam&lt;/a&gt;!&lt;/p&gt;
&lt;h2 id="cleaner-install"&gt;Cleaner Install&lt;/h2&gt;
&lt;p&gt;One of the issues with versions up until now was, that it included not only the addon, but also the demo scenes and the tests. This was confusing to users, as they had to remove the demo scenes and tests manually. This is now fixed!&lt;/p&gt;
&lt;p&gt;When you download the addon, you only get the addon. If you want the demo scenes and tests, you can download them separately. More on that in the next section.&lt;/p&gt;
&lt;h2 id="new-example-project"&gt;New Example Project&lt;/h2&gt;
&lt;p&gt;The new example repository &lt;a href="https://github.com/Structed/godot-playfab-example"&gt;Structed/godot-playfab-example&lt;/a&gt; includes - at this point - two example projects:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A simple example project that shows how to use the addon&lt;/li&gt;
&lt;li&gt;The same project, but with a simple Steam Login implementation using &lt;a href="https://godotsteam.com/"&gt;GodotSteam&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="where-to-get-the-addon"&gt;Where to get the Addon?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;The Godot AssetLib: &lt;a href="https://godotengine.org/asset-library/asset/1321"&gt;https://godotengine.org/asset-library/asset/1321&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;The Itch.io Page: &lt;a href="https://structed.itch.io/godot-playfab"&gt;https://structed.itch.io/godot-playfab&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;The GitHub Release Page: &lt;a href="https://github.com/Structed/godot-playfab/releases"&gt;https://github.com/Structed/godot-playfab/releases&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
		<item>
			<title>Announcing godot-playfab 1.0</title>
			<link>https://blog.structed.me/posts/godot-playfab-1.0</link>
			<description>godot-playfab 1.0 is out! This is a major milestone for the project and I am very excited to share it with you!</description>
			<enclosure url="https://blog.structed.me/images/posts/godot-playfab-1.0/godot-playfab_splash-screen.png" length="0" type="image" />
			<guid>https://blog.structed.me/posts/godot-playfab-1.0</guid>
			<pubDate>Sat, 03 Jun 2023 00:00:00 GMT</pubDate>
			<content:encoded>&lt;h1 id="announcing-godot-playfab-1.0"&gt;Announcing godot-playfab 1.0&lt;/h1&gt;
&lt;p&gt;After two years of development in my spare time, I am very excited to announce that godot-playfab 1.0 is out!&lt;/p&gt;
&lt;p&gt;Grab it here while it's still fresh:&lt;/p&gt;
&lt;p&gt;➡️ &lt;a href="https://github.com/Structed/godot-playfab"&gt;GitHub Repo&lt;/a&gt;: Clone it to get all the bits!&lt;/p&gt;
&lt;p&gt;➡️ &lt;a href="https://github.com/Structed/godot-playfab"&gt;GitHub Repo&lt;/a&gt;: Clone it to get all the bits!&lt;/p&gt;
&lt;p&gt;➡️ &lt;a href="https://godotengine.org/asset-library/asset/1321"&gt;AssetLib&lt;/a&gt;:  directly within Godot 4!&lt;/p&gt;
&lt;p&gt;➡️ &lt;a href="https://structed.itch.io/godot-playfab"&gt;itch.io&lt;/a&gt;: Scratch your itch there!&lt;/p&gt;
&lt;p&gt;➡️ &lt;a href="https://github.com/Structed/godot-playfab/releases/tag/1.0.0"&gt;GitHub Release&lt;/a&gt;: The same as what you get via the Godot Editor download&lt;/p&gt;
&lt;h1 id="whats-new"&gt;What's new&lt;/h1&gt;
&lt;h2 id="godot-4"&gt;Godot 4&lt;/h2&gt;
&lt;p&gt;The biggest change was the upgrade from Godot 3 to Godot 4, which does not mean a change for you as a user of the library per se, but it means that the library is now compatible with Godot 4.0 and up!&lt;/p&gt;
&lt;h2 id="production-quality"&gt;Production Quality&lt;/h2&gt;
&lt;p&gt;The main reason I am making this a 1.0 release is that I consider the library to be production-ready, and I want to convey this fact via the version number. Since &lt;a href="https://store.steampowered.com/app/1637320/Dome_Keeper/"&gt;Dome Keeper&lt;/a&gt; (and some other projects) have been using it since earlier this year, I have been able to iron out most of the bugs and issues and feel comfortable recommending it as production-ready.&lt;/p&gt;
&lt;h2 id="new-unit-testing-framework"&gt;New Unit Testing Framework&lt;/h2&gt;
&lt;p&gt;The Unit Testing Framework I had been using (GUT) was not compatible with Godot 4 at the time I was migrating. But migrating a library like godot-playfab requires to have at least some Unit Test coverage to ensure proper operation and therefore provide operational security to it's users. Particularly when you have a scripting engine with breaking changes.&lt;/p&gt;
&lt;p&gt;I then  found &lt;a href="https://github.com/MikeSchulze/gdUnit4"&gt;GDunit4&lt;/a&gt; by &lt;a href="https://twitter.com/LpzMikeschulze"&gt;Mike Schulze&lt;/a&gt;. It's been a joy working with it an Mike has been great in addressing feedback. Special thanks go out to &lt;a href="https://twitter.com/bitbrain"&gt;&amp;#64;Bitbrain&lt;/a&gt;, who has helped me on this journey as well and inspired me with his GitHub Actions pipeline.&lt;/p&gt;
&lt;h2 id="godot-3"&gt;Godot 3&lt;/h2&gt;
&lt;p&gt;While the library is now compatible with Godot 4, it is still compatible with Godot 3.5 and up. This is important for me, as may projects are still using Godot 3.5 and I want to continue to support them.&lt;/p&gt;
&lt;h3 id="assetlib"&gt;AssetLib&lt;/h3&gt;
&lt;p&gt;However, I have migrated the AssetLib entry to it's own entry: &lt;a href="https://godotengine.org/asset-library/asset/1756"&gt;godot-playfab-3&lt;/a&gt;. This is to ensure that users of Godot 3.5 and up can still get the latest version of the library, while users of Godot 4 can get the latest version of the library for Godot 4.&lt;/p&gt;
&lt;h3 id="fork"&gt;Fork&lt;/h3&gt;
&lt;p&gt;The codebase has been forked for Godot3, and all the code of the Godot 3 version will now have a &lt;code&gt;godot3&lt;/code&gt; prefix in the branch name.
The Godot 4 code will now live in the &lt;code&gt;main&lt;/code&gt;/&lt;code&gt;develop&lt;/code&gt; branches.&lt;/p&gt;
&lt;h3 id="itch.io-github-releases"&gt;Itch.io / GitHub Releases&lt;/h3&gt;
&lt;p&gt;With the reduced investment and the added complexity by forking the codebase in the same code repository, I have decided to no longer publish releases on GitHub and Itch.io. If you want the latest bits, just use the AssetLib version or download directly from the &lt;a href="https://github.com/Structed/godot-playfab/tree/godot3"&gt;godot3 branch&lt;/a&gt;.&lt;/p&gt;
&lt;h1 id="the-future"&gt;The Future&lt;/h1&gt;
&lt;h2 id="godot-3-1"&gt;Godot 3&lt;/h2&gt;
&lt;p&gt;I will continue to support the Godot 3.x version, however I am considering it a legacy version. I will not add any new features to it, but I will continue to fix bugs and issues. But you can always talk to me and convince me otherwise!&lt;/p&gt;
&lt;h2 id="godot-4-1"&gt;Godot 4&lt;/h2&gt;
&lt;p&gt;I will continue supporting the latest Godot4 version and will be adding new features. However, the most important features are already included. Do let me know if you have any feature requests and I am happy top build those in!&lt;/p&gt;
&lt;p&gt;Open a &lt;a href="https://github.com/Structed/godot-playfab/discussions"&gt;Discussion item&lt;/a&gt;, or &lt;a href="https://github.com/Structed/godot-playfab/issues/new/choose"&gt;create an Issue&lt;/a&gt; on GitHub!&lt;/p&gt;
&lt;h1 id="commercial-services"&gt;Commercial Services&lt;/h1&gt;
&lt;p&gt;If you are looking for commercial services, such as support, custom development, or integration, please contact me via &lt;a href="mailto:godot-playfab&amp;#64;structed.me"&gt;Email&lt;/a&gt;, &lt;a href="https://twitter.com/Structed"&gt;Twitter&lt;/a&gt;, &lt;a href="https://mastodon.social/&amp;#64;structed"&gt;Mastodon&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/johannesebner"&gt;Linkedin&lt;/a&gt; ot our &lt;a href="https://discord.gg/7K7q2YuNXe"&gt;godot-playfab Discord&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
		<item>
			<title>The Game Developer's Travel Guide</title>
			<link>https://blog.structed.me/posts/gamedev-travel-guide</link>
			<description>A collection of tips and tricks for traveling as a game developer</description>
			<enclosure url="https://blog.structed.me/images/posts/gamedev-travel-guide/denys-nevozhai-RRNbMiPmTZY-unsplash.jpg" length="0" type="image" />
			<guid>https://blog.structed.me/posts/gamedev-travel-guide</guid>
			<pubDate>Thu, 16 Mar 2023 00:00:00 GMT</pubDate>
			<content:encoded>&lt;h1 id="why-a-travel-guide"&gt;Why a Travel Guide?&lt;/h1&gt;
&lt;p&gt;With GDC 2023 coming up next week and I see &lt;a href="https://twitter.com/SanderVhove/status/1636040056099176448?s=20"&gt;people talking about in on Twitter&lt;/a&gt;, I figured I could write down my tips &amp;amp; tricks for traveling as a gamedev I have gathered over the years!&lt;/p&gt;
&lt;p&gt;It's not inherently for gamedevs (in fact, it's not specific at all), but I consider my target audience to be gamedevs!&lt;/p&gt;
&lt;h1 id="why-am-i-qualified"&gt;Why am I qualified?&lt;/h1&gt;
&lt;p&gt;I worked as a Gaming Solution Architect for over three years now, and I have had quite a few trips to many studios &amp;amp; events. So I know a thing or two about traveling as a gamedev!&lt;/p&gt;
&lt;h1 id="show-me-the-tips"&gt;Show me the tips!&lt;/h1&gt;
&lt;h2 id="get-a-good-trolley"&gt;1. Get a good trolley&lt;/h2&gt;
&lt;p&gt;Get a hard-shell trolley for your carry-on luggage. Hard shell, so it can protect your stuff. Like your laptop!
Also, there are quite a few nifty ones out there with dedicated laptop opening, powerbank-pouch with a USB port, and even a TSA lock!&lt;/p&gt;
&lt;p&gt;Speaking of TSA Lock: if you want to lock your luggage, get a TSA lock. TSA locks are locks that can be opened by TSA agents, so they can check your luggage if they want to. If you use a regular lock, they will break it - or your trolley.&lt;/p&gt;
&lt;h2 id="what-to-pack"&gt;2. What to pack?&lt;/h2&gt;
&lt;h3 id="laptop"&gt;💻 Laptop&lt;/h3&gt;
&lt;p&gt;I always pack my laptop in my carry-on luggage. Specifically on a long-haul flight, you might want to work or play games on it.&lt;/p&gt;
&lt;h3 id="power"&gt;⚡ POWER!&lt;/h3&gt;
&lt;h4 id="powerbank"&gt;🔋 Powerbank&lt;/h4&gt;
&lt;p&gt;Also pack in your carry-on luggage. You never know when you need to charge your phone or laptop. I always carry a powerbank with me, and I always have a USB-C cable with me.&lt;/p&gt;
&lt;h4 id="usb-condom"&gt;🛡️ USB Condom&lt;/h4&gt;
&lt;p&gt;You don't want to get your phone compromised by a USB port in an airport or elsewhere. So always use a USB condom.&lt;/p&gt;
&lt;p&gt;This is a USB adapter which disables the data lane. You can also make it yourself!&lt;/p&gt;
&lt;p&gt;Check out &lt;a href="https://www.howtogeek.com/364032/how-to-protect-yourself-from-public-usb-charging-ports/"&gt;this guide on how you can protect yourself&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="usb-cables"&gt;🧵 USB cables&lt;/h4&gt;
&lt;p&gt;I need many. I have two Android phones (work and private), Samsung Galaxy Watch charging pad, Headphones, and a powerbank.&lt;/p&gt;
&lt;p&gt;Make a list, and pack them in a nice bag!&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/gamedev-travel-guide/IMG_20230316_110710.jpg" alt="Cables in bags"&gt;&lt;/p&gt;
&lt;h3 id="headphones"&gt;🎧 Headphones&lt;/h3&gt;
&lt;p&gt;Noise canceling headphones are quite nice. I use Microsoft Surface Headphones 2.&lt;/p&gt;
&lt;h3 id="power-supply"&gt;🔌 Power Supply&lt;/h3&gt;
&lt;p&gt;DO NOT FORGET YOUR POWER SUPPLY FOR YOUR LAPTOP! I have forgotten it once, and I had to buy a new one. It was not fun. And quite expensive.&lt;/p&gt;
&lt;p&gt;I also carry a multi-USB port power supply, so I can charge multiple devices at once. Each of the ports should have an output of 2A or more to achieve a significant charging speed.&lt;/p&gt;
&lt;h3 id="outlet-adapter"&gt;🐽 Outlet Adapter&lt;/h3&gt;
&lt;p&gt;Traveling to another country? Make sure you have an outlet converter. There are these great &lt;a href="https://www.amazon.de/dp/B07RPDCC7Z"&gt;cubes&lt;/a&gt; which can adapt to a lot of different standards and also have USB ports.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/gamedev-travel-guide/IMG_20230316_150405.jpg" alt="Power Outlet Adapter"&gt;&lt;/p&gt;
&lt;h3 id="hdmi-cable"&gt;📺 HDMI Cable&lt;/h3&gt;
&lt;p&gt;You probably want to watch TV shows on your laptop or play games. So make sure you have an HDMI cable with you. HDMI is still the most common input for TVs across the world.&lt;/p&gt;
&lt;p&gt;Perhaps also get adapters for your switch or whatever console you are bringing to play!&lt;/p&gt;
&lt;h3 id="display-adapter"&gt;📺 ➕ 💻Display Adapter&lt;/h3&gt;
&lt;p&gt;Speaking of adapters: you likely have a modern laptop or console which has only USB-C ports for display adapters. You definitely want to think about bringing one of these. They can be expensive!&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/gamedev-travel-guide/IMG_20230316_150030.jpg" alt="Cables &amp;amp; Adapters"&gt;&lt;/p&gt;
&lt;h3 id="controller"&gt;🎮 Controller&lt;/h3&gt;
&lt;p&gt;If you're like me and use a laptop to play, don't forget to bring a controller.&lt;/p&gt;
&lt;h2 id="general-travel-advice"&gt;General Travel Advice&lt;/h2&gt;
&lt;h3 id="mobile-connectivity"&gt;📶 Mobile Connectivity&lt;/h3&gt;
&lt;p&gt;Make sure you research your options for local mobile connectivity.&lt;/p&gt;
&lt;p&gt;The easiest is of course roaming. But be careful! Roaming can be super expensive!
Do research your options for your own carrier. If these are prohibitively expensive, you can get a local sim card in many countries.
However, some countries like make it extremely hard to get one (legally).&lt;/p&gt;
&lt;h3 id="download"&gt;🔽 Download!&lt;/h3&gt;
&lt;p&gt;Download whatever you need on your travels and is of significant size or might be market-restricted.&lt;/p&gt;
&lt;p&gt;You definitely need to use the download functionalities of Netflix, Amazon Prime etc, because it's not only large download sizes you might need to account for- but 🌍🤺 geofencing!&lt;/p&gt;
&lt;p&gt;I was once in India for a week and I was super remote and I could not get a local sim card, and the Wifi was unstable at best. I could only watch one show because the others were geofenced and I could not download them from India.&lt;/p&gt;
&lt;h3 id="vpn"&gt;❌ VPN&lt;/h3&gt;
&lt;p&gt;VPNs sound interesting, but they are not a great idea. They're a privacy problem at best, and a massive security problem at worst.&lt;/p&gt;
&lt;h3 id="book-an-aisle-seat"&gt;💺 Book an Aisle Seat!&lt;/h3&gt;
&lt;p&gt;You will then always be in the last boarding group. This means you can totally chill and you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Arrive later at the airport&lt;/li&gt;
&lt;li&gt;You don;t have to stand in boarding queue forever&lt;/li&gt;
&lt;li&gt;Less time to sit in on the plane&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;But there is one important caveat:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ You might not have a lot of space for your carry-on luggage! So be sure to travel with a small piece or only use that strategy on short flights where you can take the bag under your seat.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id="conclusion"&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;I hope my tips helped you with your travel!
If they did, or you have some more suggestions, let me know via &lt;a href="https://blog.structed.me/https;/twitter.com/structed" target="_blank"&gt;Twitter (@Structed)&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;Thank you to &lt;a href="https://twitter.com/SanderVhove" target="_blank"&gt;@SanderVhove&lt;/a&gt; for the inspiration to write this post!&lt;/p&gt;
&lt;p&gt;&lt;span class="alert alert-light"&gt;Header Photo by &lt;a href="https://unsplash.com/photos/RRNbMiPmTZY"&gt;Denys Nevozhai via Unsplash&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
		<item>
			<title>Deep dive into PlayFab Events API</title>
			<link>https://blog.structed.me/posts/playfab-events-api</link>
			<description>How to use the Events API to power your game analytics, telemetry and react to them</description>
			<enclosure url="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/PFAF.png" length="0" type="image" />
			<guid>https://blog.structed.me/posts/playfab-events-api</guid>
			<pubDate>Tue, 14 Mar 2023 00:00:00 GMT</pubDate>
			<content:encoded>&lt;h1 id="what-is-the-events-api"&gt;What is the Events API?&lt;/h1&gt;
&lt;p&gt;The &lt;a href="https://docs.microsoft.com/en-us/rest/api/playfab/events/?view=playfab-rest"&gt;Events API&lt;/a&gt; is a new API to send Telemetry and PlayStream events to PlayFab for later analysis or reactive programming (or event-driven architectures).&lt;/p&gt;
&lt;p&gt;The Events API supersedes the legacy &lt;a href="https://docs.microsoft.com/en-us/rest/api/playfab/client/analytics?view=playfab-rest"&gt;Client Analytics API&lt;/a&gt;. You can, however, still use the legacy API for convenience.&lt;/p&gt;
&lt;p&gt;But this article details why you should default to the new Events API 😊&lt;/p&gt;
&lt;h1 id="events-api-vs-legacy-events-api"&gt;Events API vs Legacy Events API&lt;/h1&gt;
&lt;p&gt;The legacy API was designed for ease of use in client applications. It provides a number of convenient endpoints to ingest data pertaining to a certain context (e.g. Player, Character etc) into PlayStream.&lt;/p&gt;
&lt;p&gt;However, the way the legacy API works is that&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;you can only ingest one event per call&lt;/li&gt;
&lt;li&gt;poses limited ingestion capabilities (API rate limits, client HTTP overhead)&lt;/li&gt;
&lt;li&gt;it does not support Telemetry Events&lt;/li&gt;
&lt;li&gt;it does not support the &lt;a href="https://docs.microsoft.com/en-us/gaming/playfab/features/data/entities/"&gt;Entity Programming Model&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Events API resolves all of the above limitations of the legacy API. You can &lt;strong&gt;batch multiple events&lt;/strong&gt; in one request, massively increasing the amount of Events you can ingest into PlayFab while at the same time &lt;strong&gt;saving you money&lt;/strong&gt; and r&lt;strong&gt;educing the overall bandwidth&lt;/strong&gt; used on the client-side. This is particularly important in mobile scenarios. It also supports the &lt;a href="https://docs.microsoft.com/en-us/gaming/playfab/features/data/entities/"&gt;Entity Programming Model&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;biggest advantage&lt;/strong&gt;, however, is likely the costs savings by enabling you to use &lt;em&gt;Telemetry Events&lt;/em&gt;. We will differentiate in the next section.&lt;/p&gt;
&lt;h1 id="playstream-telemetry"&gt;PlayStream &amp;amp; Telemetry&lt;/h1&gt;
&lt;p&gt;You have read PlayStream and Telemetry a couple of times now - but what are these?&lt;/p&gt;
&lt;p&gt;PlayStream and Telemetry share the fundamental design. You can think of them as arbitrary events pertaining to a specific Entity (Title Player, Character etc.), holding a list of key-value pairs as payload.&lt;/p&gt;
&lt;p&gt;When you send any of these events to PlayFab, PlayFab will ingest them into it's data lake for you to later analyse with PlayFab's built-in or external tools.&lt;/p&gt;
&lt;p&gt;While the two event types look the same when you send them, there is a very big difference when they "arrive" at PlayFab:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Telemetry Events are just ingested into a data lake and you can then run analytics on them&lt;/li&gt;
&lt;li&gt;PlayStream Events can be reacted upon after they arrive. More on that below.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="example-sending-telemetry-events"&gt;Example: Sending Telemetry Events&lt;/h2&gt;
&lt;p&gt;This is how you send a Telemetry Event in the Godot engine using (my own) &lt;a href="https://github.com/structed/godot-playfab"&gt;godot-playfab&lt;/a&gt; addon. The Payload is completely arbitrary JSON:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-gdscript"&gt;func _on_WriteTelemetryDirectButton_pressed():
    var payload = {
        "Action": "_on_WriteTelemetryDirectButton_pressed"
    }
    
    PlayFabManager.event.write_title_player_telemetry_event(EVENT_NAME_TELEMETRY, payload)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the unity version:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;EventsModels.WriteEventsRequest request = new EventsModels.WriteEventsRequest();
request.Events = new List&amp;lt;EventsModels.EventContents&amp;gt;();

// Some metadata (required)
EventsModels.EventContents eventInfo = new EventsModels.EventContents();
eventInfo.Name = "unity_client_api_error_occurred";
eventInfo.EventNamespace = eventNamespace;
eventInfo.Entity = entityKey;
eventInfo.OriginalTimestamp = DateTime.UtcNow;

// Custom payload
var payload = new Dictionary&amp;lt;string, object&amp;gt;();
payload["ErrorCode"] = errorCode;
payload["ErrorType"] = type;
eventInfo.Payload = payload;

request.Events.Add(eventInfo);
var result = await eventApi.WriteTelemetryEventsAsync(request);
&lt;/code&gt;&lt;/pre&gt;
&lt;h1 id="working-with-the-playstream"&gt;Working with the PlayStream&lt;/h1&gt;
&lt;p&gt;PlayStream is different in the way that PlayStream events can be &lt;strong&gt;used to react to them in near-realtime&lt;/strong&gt;: you can not only analyse them after the fact with PlayFab's analytics capabilities - but you can also use them for event-driven architectures and reactive programming!&lt;/p&gt;
&lt;p&gt;For instance, you can set up a &lt;a href="https://docs.microsoft.com/en-us/gaming/playfab/features/automation/actions-rules/quickstart"&gt;Rule&lt;/a&gt; in PlayFab to run an Azure Function or gift an item if a specific event is appearing in the PlayStream. Check out my &lt;a href="https://blog.structed.me/posts/extending-playfab-with-azure-functions"&gt;in-depth walk-through&lt;/a&gt; on how to do this - or have a look at our example project, where we used PlayStream to &lt;a href="https://blog.structed.me/posts/playfab-match-history"&gt;implement a Match History&lt;/a&gt;!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ Caution: Do &lt;em&gt;&lt;strong&gt;not&lt;/strong&gt;&lt;/em&gt; block on PlayStream events, e.g. waiting in your game UI for an event to appear in the PlayStream!
PlayStream events are &lt;em&gt;&lt;strong&gt;not real-time&lt;/strong&gt;&lt;/em&gt;!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id="built-in-events"&gt;Built-in events&lt;/h1&gt;
&lt;p&gt;Even without implementing your own events, PlayFab already collects a lot of data for you just through built-in events triggered by API usage.
For example, whenever a user logs in, events are generated.
This data will be available for you to use in analytics, but also - just like your own events - can be used for automation or segmentation of users. When looking at segmentation, PlayFab has already pre-made segmentation for the most common use-cases like "Lapsed players", which shows you a list of users who have not been logging in since more than 30 days.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/playfab-events-api/built-in-events.png" alt="Screenshot of the Title Overview UI" title="Screenshot of the Title Overview UI"&gt;&lt;/p&gt;
&lt;h1 id="using-events"&gt;Using Events&lt;/h1&gt;
&lt;p&gt;Once you ingested Events through the API, whether by creating your own or through built-in facilities, you can start using the data in multiple ways:&lt;/p&gt;
&lt;h2 id="analysing-telemetry"&gt;Analysing Telemetry&lt;/h2&gt;
&lt;p&gt;Using PlayFab's built-in analytics capabilities, you can analyse player behaviour or your system's performance. If you require even more data analytics capabilities, you can also export the data!&lt;/p&gt;
&lt;h3 id="title-overview"&gt;Title Overview&lt;/h3&gt;
&lt;p&gt;When you open the PlayFab Game Manager for a given Title, the first page will always be the &lt;em&gt;Title Overview&lt;/em&gt;. As you can see, PlayFab is already presenting you with a dashboard based on built-in events!&lt;/p&gt;
&lt;p&gt;If you switch to the next Tab, PlayStream Monitor, you can see PlayStream events appearing in near-realtime as PlayFab ingests them, so you can inspect them and can even see where they appear on the world map.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/playfab-events-api/title-overview.png" alt="Screenshot of the Title Overview UI" title="Screenshot of the Title Overview UI"&gt;&lt;/p&gt;
&lt;h3 id="dashboards"&gt;Dashboards&lt;/h3&gt;
&lt;p&gt;A recent addition to the GameManager (and currently in public preview) is the &lt;em&gt;Trends&lt;/em&gt; tab in the &lt;em&gt;Dashboards&lt;/em&gt; section.
There, PlayFab uses built-in events to provide you with a few important trends to see how your game is performing:
&lt;img src="https://blog.structed.me/images/posts/playfab-events-api/trends.png" alt="Screenshot of the Trends UI" title="Screenshot of the Trends UI"&gt;&lt;/p&gt;
&lt;p&gt;On the second tab of the &lt;em&gt;Dashboards&lt;/em&gt; pane, you can view and download reports.
You can also click on a report and get an awesome, detailed dashboard for it!
&lt;img src="https://blog.structed.me/images/posts/playfab-events-api/reports-overview.png" alt="Screenshot of the Reports overview UI" title="Screenshot of the Reports overview UI"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/playfab-events-api/reports-dashboard.png" alt="Screenshot of the Reports detail view UI" title="Screenshot of the Reports detail view UI"&gt;&lt;/p&gt;
&lt;h3 id="data"&gt;Data&lt;/h3&gt;
&lt;p&gt;In the &lt;em&gt;Data&lt;/em&gt; section of the Game Manager, you can go really deep. The &lt;em&gt;Data Explorer&lt;/em&gt; is an integration of &lt;em&gt;Azure Data Explorer&lt;/em&gt; directly into PlayFab's GameManager. There is a "basic" tab, which gives you a basic UI to do some analytics. If you want to go deeper, you can use the "advanced" tab, which lets you write your own queries using the SQL-like &lt;em&gt;Kusto Query Language&lt;/em&gt; (abbreviated as "KQL").
You can also export the raw event data, either periodically (&lt;em&gt;Event Export&lt;/em&gt;) or continuously (&lt;em&gt;Data Connections&lt;/em&gt;) to use the data in other analytics systems of your choice.&lt;/p&gt;
&lt;p&gt;If you want to learn KQL, here's a fun way to learn it - the &lt;a href="https://detective.kusto.io/inbox"&gt;Kusto Detective Agency&lt;/a&gt;!&lt;/p&gt;
&lt;h2 id="automation"&gt;Automation&lt;/h2&gt;
&lt;p&gt;You may use PlayFab's Automation functionality to react to PlayStream Events.
Without writing additional code, you can use PlayFab's &lt;em&gt;Rules&lt;/em&gt; to react to events. Just specify the event type you invented while creating the event, and set up conditions and actions. For example, you might want to send a welcome Email once a player registered for your game:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/playfab-events-api/new-rule.png" alt="Screenshot of the New Rule UI in PlayFab" title="Screenshot of the New Rule UI in PlayFab"&gt;&lt;/p&gt;
&lt;p&gt;You might as well use use CloudScript Functions to trigger backend code execution via Azure Functions to be even more flexible
To achieve that, you can set up a &lt;a href="https://docs.microsoft.com/en-us/gaming/playfab/features/automation/actions-rules/quickstart"&gt;Rule&lt;/a&gt; in PlayFab to run an Azure Function if a specific event is appearing in the PlayStream. Check out my &lt;a href="https://blog.structed.me/posts/extending-playfab-with-azure-functions"&gt;in-depth walk-through&lt;/a&gt; on how to do this - or have a look at our example project, where we used PlayStream to &lt;a href="https://blog.structed.me/posts/playfab-match-history"&gt;implement a Match History&lt;/a&gt;!&lt;/p&gt;
&lt;h1 id="conclusion"&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;In this post, I showed you the differences of the legacy and the new Events API and why you should use the Events API going forward to reduce cost and overhead and enabling you to write Telemetry events, which can further lower your cost.&lt;/p&gt;
&lt;p&gt;You als o got a glimpse of the possibilities of PlayFab's built-in analytics capabilities and how you can use PlayStream events to react to them in near-realtime.&lt;/p&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
		<item>
			<title>Raspberry PI as a Spotify music station</title>
			<link>https://blog.structed.me/posts/raspberrypi-spotify-music-station</link>
			<description>Building a Spotify Connect box to play music.</description>
			<enclosure url="https://blog.structed.me/images/posts/raspberrypi-spotify-music-station/alexander-shatov-JlO3-oY5ZlQ-unsplash.jpg" length="0" type="image" />
			<guid>https://blog.structed.me/posts/raspberrypi-spotify-music-station</guid>
			<pubDate>Sat, 30 Jul 2022 00:00:00 GMT</pubDate>
			<content:encoded>&lt;h1 id="introduction"&gt;Introduction&lt;/h1&gt;
&lt;p&gt;In this post, we set up a RaspberryPi to play music via Spotify Connect.&lt;/p&gt;
&lt;h1 id="prerequisites"&gt;Prerequisites&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;A Spotify Premium account&lt;/li&gt;
&lt;li&gt;A RaspberryPI (I uses a RaspberryPi 2 model B), including:
&lt;ul&gt;
&lt;li&gt;SDCard&lt;/li&gt;
&lt;li&gt;WiFi Dongle or Ethernet cable&lt;/li&gt;
&lt;li&gt;Speakers (with audio jack plug)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="overview"&gt;Overview&lt;/h1&gt;
&lt;p&gt;We will first flash a standard image for the RaspberryPi. But we will make custom configurations to connect to a Network or WiFi and set up SSH to allow for remote administration.&lt;/p&gt;
&lt;h1 id="flashing-the-image"&gt;Flashing the image&lt;/h1&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/raspberrypi-spotify-music-station/imager-steps.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;First, plug in your SDCard. Then, get the &lt;a href="https://www.raspberrypi.com/software/"&gt;Raspberry Pi Imager&lt;/a&gt; from the official website. Once downloaded and install, select an image (1), I used the recommended image.&lt;/p&gt;
&lt;p&gt;The next step is to select a storage (2). Be &lt;em&gt;&lt;strong&gt;VERY&lt;/strong&gt;&lt;/em&gt; careful with this. If you select the wrong storage, you might overwrite another storage drive!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before&lt;/strong&gt; clicking the &lt;strong&gt;WRITE&lt;/strong&gt; button (4), click on the cogwheel in the lower right corner (3). This will show you advanced configurations. Set the options appropriate to you. If you enable WiFi, you should obviously have a WiFi dongle compatible with the Raspberry Pi.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/raspberrypi-spotify-music-station/imager-advanced.png" alt=""&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you enable SSH, and you put the RaspberryPi in the same network as another machine, you can then use SSH to administer it.
So you don't have to set up a display and keyboard!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Once you set your options, exit the dialog and click "WRITE".&lt;/p&gt;
&lt;h1 id="booting-initial-set-up"&gt;Booting &amp;amp; initial set-up&lt;/h1&gt;
&lt;p&gt;Plugin in the power for your RaspberryPi and wait a bit for it to boot.
Then, use &lt;code&gt;ssh&lt;/code&gt; to log in. On a Windows 11 machine with &lt;a href="https://docs.microsoft.com/en-us/windows/terminal/install"&gt;Windows Terminal&lt;/a&gt;, you don't even need to install additional tools. You can just use SSH from the Terminal!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you changed user, hostname and or port in the image configuration before flashing the image, please make sure to use these!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Mine would look like this, where &lt;code&gt;structed&lt;/code&gt; is my username and &lt;code&gt;raspberrypi&lt;/code&gt; the hostname:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;ssh structed@raspberrypi.local
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first logon may take a bit.&lt;/p&gt;
&lt;h3 id="updating-the-system"&gt;Updating the system&lt;/h3&gt;
&lt;p&gt;Once logged in, fire up the configuration tool. Type:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo raspi-config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Navigate to &lt;code&gt;update&lt;/code&gt; and select it to update your system - this will take a while.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/raspberrypi-spotify-music-station/config-update.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Once the update is complete, &lt;code&gt;raspi-config&lt;/code&gt; will automatically restart.
You can now make additional changes, or close it.&lt;/p&gt;
&lt;h3 id="disable-desktop-ui"&gt;Disable Desktop UI&lt;/h3&gt;
&lt;p&gt;If you're like me, you may want to disable the graphical desktop interface to save resources, as I will only run the RaspberriPi without a display.&lt;/p&gt;
&lt;p&gt;Still in &lt;code&gt;raspi-config&lt;/code&gt;, choose &lt;code&gt;System Options&lt;/code&gt;.
In the next menu, choose &lt;code&gt;Boot / Auto Login&lt;/code&gt;.
In the last menu, I chose &lt;code&gt;Console Autologin Text console, automatically logged in as '&amp;lt;username&amp;gt;' user&lt;/code&gt; (where &lt;code&gt;&amp;lt;username&amp;gt;&lt;/code&gt; is your username)&lt;/p&gt;
&lt;p&gt;Once you're done configuring, exit with the &lt;code&gt;Finish&lt;/code&gt; option in the &lt;code&gt;raspi-config&lt;/code&gt; or press &lt;code&gt;ESC&lt;/code&gt; until you exit to shell.&lt;/p&gt;
&lt;h1 id="installing-spotifyd"&gt;Installing &lt;code&gt;spotifyd&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;spotifyd&lt;/code&gt; is a daemon that runs on the Raspberry Pi. It is used to connect to Spotify and play music.
Installing &lt;code&gt;spotifyd&lt;/code&gt; allows us to play Spotify music on this device controlled by any other Spotify Connect enabled device. So once setup, we can control it e.g. from a phone.&lt;/p&gt;
&lt;p&gt;For the setup, we'll essentially follow the official &lt;a href="https://spotifyd.github.io/spotifyd/installation/Raspberry-Pi.html"&gt;&lt;code&gt;spotifyd&lt;/code&gt; installation instructions for RaspberryPi&lt;/a&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to the &lt;a href="https://github.com/Spotifyd/spotifyd/releases"&gt;Releases page&lt;/a&gt; and download the latest release (in my case, my architecture is &lt;code&gt;armhf&lt;/code&gt; for RaspberryPi 2): (&lt;code&gt;spotifyd-linux-armhf-default.tar.gz&lt;/code&gt;) as well as the sha51 file &lt;code&gt;spotifyd-linux-armhf-default.tar.gz&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;wget https://github.com/Spotifyd/spotifyd/releases/download/v0.3.3/spotifyd-linux-armhf-default.tar.gz&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wget https://github.com/Spotifyd/spotifyd/releases/download/v0.3.3/spotifyd-linux-armhf-default.sha512&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start="2"&gt;
&lt;li&gt;&lt;p&gt;Verify the integrity of the downloaded file:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sha512sum -c spotifyd-linux-armhf-default.sha512&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;If the download was OK, it will show this output: &lt;code&gt;spotifyd-linux-armhf-default.tar.gz: OK&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Extract the &lt;code&gt;spotifyd-linux-armhf-default.tar.gz&lt;/code&gt; file to the &lt;code&gt;/opt/spotifyd&lt;/code&gt; directory:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tar -xzf spotifyd-linux-armhf-default.tar.gz -C /usr/bin/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create the systemd service file:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Depending whether you want to run as root or as the logged in user, the steps would be a bit different. I want to run as my &lt;code&gt;structed&lt;/code&gt; user, so I'll use the following steps. IF you want to go for root, &lt;a href="https://spotifyd.github.io/spotifyd/installation/Raspberry-Pi.html"&gt;check the docs&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Create the systemd user config directory: &lt;code&gt;mkdir -p ~/.config/systemd/user/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Create the systemd config file: &lt;code&gt;nano ~/.config/systemd/user/spotifyd.service&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Paste the contents of the &lt;a href="https://github.com/Spotifyd/spotifyd/blob/master/contrib/spotifyd.service"&gt;spotifyd.service&lt;/a&gt; from the &lt;code&gt;spotifyd&lt;/code&gt; repository.&lt;/li&gt;
&lt;li&gt;Save the file.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Configure &lt;code&gt;spotifyd&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create the config directory: &lt;code&gt;mkdir ~/.config/spotifyd/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Create the config file: &lt;code&gt;nano ~/.config/spotifyd/spotifyd.conf&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Please change the values for &lt;code&gt;username&lt;/code&gt;, &lt;code&gt;password&lt;/code&gt;, &lt;code&gt;device_name&lt;/code&gt;!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class="language-toml"&gt;[global]
username = "USER"
password = "PASS"
backend = "alsa"
device = "hw" # Given by `aplay -L`
mixer = "PCM"
volume-controller = "softvol" # or alsa_linear, or softvol
#onevent = command_run_on_playback_event
device_name = "rpi-kids"
bitrate = 320
cache_path = "cache_directory"
volume-normalisation = true
normalisation-pregain = -10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You might also want to reduce the &lt;code&gt;bitrate&lt;/code&gt; (&lt;code&gt;160&lt;/code&gt; or &lt;code&gt;96&lt;/code&gt;) and/or change the volume &lt;code&gt;volume-controller&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For more information on the config file, like how you can work around storing your password in the config file, &lt;a href="https://spotifyd.github.io/spotifyd/config/File.html"&gt;see the documentation&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reload the daemon: &lt;code&gt;systemctl --user daemon-reload&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1 id="testing-spotifyd"&gt;Testing &lt;code&gt;spotifyd&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;Run &lt;code&gt;/usr/bin/spotifyd --no-daemon&lt;/code&gt; to test &lt;code&gt;spotifyd&lt;/code&gt;. Once started, use another device, like your mobile phone, and open Spotify. There, click the "loudspeaker" button (1) to select the device. In my case, it's &lt;code&gt;raspi-kids&lt;/code&gt; (2).
Then click play!&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/raspberrypi-spotify-music-station/spotify-select-device.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;If you have a device plugged in to your headphone jack on the RaspberryPi, you should now hear the music play!&lt;/p&gt;
&lt;h1 id="installing-the-systemd-daemon"&gt;Installing the systemd daemon&lt;/h1&gt;
&lt;p&gt;First, we start the daemon again, as we want to use the daemon instead of calling the binary from the terminal, which blocks input. And it would nto survive a reboot, anyways.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;systemctl --user start spotifyd.service&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;However, for the service to restart and be kept alive upon reboot, we have to issue these commands:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo loginctl enable-linger &amp;lt;username&amp;gt;
systemctl --user enable spotifyd.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first command is required to enable your user to run long-running services. Without it systemd would kill the &lt;code&gt;spotifyd&lt;/code&gt; process as soon as you log out, and only run it when you log in. Now &lt;code&gt;spotifyd&lt;/code&gt; is always running on the Pi, so you can use it as a listening device remotely!&lt;/p&gt;
&lt;h1 id="conclusion"&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;We set up a RaspberryPi for remote administration, so we never require a display or keyboard.
We then set up &lt;code&gt;spotifyd&lt;/code&gt; to play music on the RaspberryPi, and we can control it from a phone or another device with the Spotify app.&lt;/p&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
		<item>
			<title>Modern Game Studio - How to transfer builds and large files in a distributed environment?</title>
			<link>https://blog.structed.me/posts/modern-studio-files</link>
			<description>Set up your modern game studio for file share sync</description>
			<enclosure url="https://blog.structed.me/images/posts/modern-studio-files/sidharth-bhatia-YbRd8Qqem_Y-unsplash.jpg" length="0" type="image" />
			<guid>https://blog.structed.me/posts/modern-studio-files</guid>
			<pubDate>Mon, 29 Nov 2021 00:00:00 GMT</pubDate>
			<content:encoded>&lt;p&gt;A common topic that came up during the pandemic is the "Modern Game Studio" - a studio that enables everyone to work from wherever they are.
Whether you are at home, or a Studio location. Whether you are using a capable workstation with a beefy GPU or just have a "regular" laptop at your disposal.&lt;/p&gt;
&lt;p&gt;We have talked to many partners who were challenged not only with many studio locations, but folks working from home temporarily or some of them have moved to other countries or continents altogether and are not planning to return to an office.&lt;/p&gt;
&lt;p&gt;Among the challenges this new way of working poses to studios is the need to transfer - and sync - large files all over a country, continent or the world.
These could be, for example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Debug builds to be
&lt;ul&gt;
&lt;li&gt;distributed to testers&lt;/li&gt;
&lt;li&gt;distributed to contractors, publishers etc&lt;/li&gt;
&lt;li&gt;stored for long-term retention&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Full crash dumps for developers to look into&lt;/li&gt;
&lt;li&gt;Large asset storage&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="scenario"&gt;Scenario&lt;/h1&gt;
&lt;p&gt;Take, for example, a large game studio which has multiple physical locations around the globe, with one central studio hosting all their services locally.&lt;/p&gt;
&lt;p&gt;The internet connection of the office locations - and particularly the primary location which hosts the services - needs to scale with the demand. Also the local services hosted on-prem need to be up to the task.&lt;/p&gt;
&lt;p&gt;Pre-pandemic, this might have been set up specifically and might have been enough. But adding a lot of home office clients, and then opening new studio locations, creates a huge challenge and usually makes the main office internet connection a choke point, specifically when distributing builds or large, full crash dumps.&lt;/p&gt;
&lt;h2 id="issues-faced"&gt;Issues faced&lt;/h2&gt;
&lt;p&gt;With large file shares, these are the common issues faced with a primary on-site location and multiple other studio locations, and added home office clients:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Main office bandwidth exceeded&lt;/li&gt;
&lt;li&gt;High latency when requesting and uploading files&lt;/li&gt;
&lt;li&gt;Potentially increased internet egress cost&lt;/li&gt;
&lt;li&gt;Non-trivial disaster recovery scenario&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="solution"&gt;Solution&lt;/h1&gt;
&lt;p&gt;A solution we have come up with to make this a better experience for a distributed studio is to use Azure as the central authority for all file shares.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/modern-studio-files/authoritah.jpg" alt="South Park's Eric Cartman yelling &amp;quot;RESPECT MY AUTHORITAH!!!&amp;quot;" title="South Park's Eric Cartman yelling 'RESPECT MY AUTHORITAH!!!'"&gt;&lt;/p&gt;
&lt;h2 id="architecture"&gt;Architecture&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/modern-studio-files/Filesync.png" alt="Architecture showing how Azure can replicate files across Regions and work locations"&gt;&lt;/p&gt;
&lt;p&gt;With &lt;a href="https://docs.microsoft.com/en-us/azure/storage/files/"&gt;Azure Files&lt;/a&gt; and &lt;a href="https://docs.microsoft.com/en-us/azure/storage/file-sync/file-sync-introduction"&gt;File Sync&lt;/a&gt;, you have a central file share repository, which can be synced to on-premise locations. At these locations, a Windows File Server with the Azure File Sync service installed, can act as a local cache.&lt;/p&gt;
&lt;p&gt;While it is possible for Workstations in the cloud or other users to be directly accessing the Storage Account, this is discouraged, because there is no built-in functionality to signal file changes.&lt;/p&gt;
&lt;h3 id="local-cache-tiered-files"&gt;Local Cache &amp;amp; Tiered files&lt;/h3&gt;
&lt;p&gt;While all files of the shares are available to clients, they are tiered: this means, they will only reside in the cloud unless used. To clients however, they appear as available to them.
All file operations hit this local File Server replica, and will be synced to the Azure File Share, using the Azure File Service, which in turn persists it in the Storage Account.&lt;/p&gt;
&lt;h3 id="improving-studio-locations-uplink-to-azure"&gt;Improving Studio locations uplink to Azure&lt;/h3&gt;
&lt;p&gt;When you transfer files from and to Azure, these will normally be routed via public internet. This will incur additional latency and will be subject to local link degradations and generate data egress cost into the public internet.
To improve this quality, security and potentially save on egress cost from Azure, you can set up &lt;a href="https://azure.microsoft.com/en-us/services/expressroute/"&gt;Azure Express Route&lt;/a&gt;.
ExpressRoute can directly connect you with the Microsoft Azure Network and create a private network between your and the Azure network.&lt;/p&gt;
&lt;h1 id="benefits"&gt;Benefits&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Automatic, consistent replication&lt;/li&gt;
&lt;li&gt;Improved data recovery &amp;amp; business continuity by having a backup in the cloud&lt;/li&gt;
&lt;li&gt;Cost savings on data egress by
&lt;ul&gt;
&lt;li&gt;using tiered sync to on-prem locations&lt;/li&gt;
&lt;li&gt;Creating builds in the cloud&lt;/li&gt;
&lt;li&gt;Crash dumps created in the cloud by automated test runners or workstations in the cloud&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;With ExpressRoute
&lt;ul&gt;
&lt;li&gt;Save on data egress using unlimited data plan on ExpressRoute&lt;/li&gt;
&lt;li&gt;Increase quality of connection&lt;/li&gt;
&lt;li&gt;Enhance security&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="conclusion"&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;We have looked ar the challenge and how to potentially solve the distribution of large files over very disparate locations.&lt;/p&gt;
&lt;p&gt;Make sure to check out the following services to learn more about them:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.microsoft.com/en-us/azure/storage/files/"&gt;Azure Files&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.microsoft.com/en-us/azure/storage/file-sync/file-sync-introduction"&gt;File Sync&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://azure.microsoft.com/en-us/services/expressroute/"&gt;Azure Express Route&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Please do not hesitate to &lt;a href="mailto:Johannes.Ebner@microsoft.com?subject=Modern%20Game%20Studio"&gt;contact me&lt;/a&gt; - I'd love to see your approach to the Modern Game Studio - or perhaps my team and I can help you build the Modern Game Studio in the Cloud!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Title Photo by &lt;a href="https://unsplash.com/@sidharthbhatia?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText"&gt;Sidharth Bhatia&lt;/a&gt; on &lt;a href="https://unsplash.com/s/photos/synchronized?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
		<item>
			<title>PlayFab Match History - Extending PlayFab during Hackweek 2021</title>
			<link>https://blog.structed.me/posts/playfab-match-history</link>
			<description>Extending PlayFab with a Match History</description>
			<enclosure url="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/PFAF.png" length="0" type="image" />
			<guid>https://blog.structed.me/posts/playfab-match-history</guid>
			<pubDate>Thu, 21 Oct 2021 00:00:00 GMT</pubDate>
			<content:encoded>&lt;p&gt;In my last post, I talked about &lt;a href="https://blog.structed.me/posts/extending-playfab-with-azure-functions"&gt;extending PlayFab with Azure Functions&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In today's post, we expand on this topic and I will show you what my team mate &lt;a href="https://github.com/annonator"&gt;Andreas&lt;/a&gt; and I built during Microsoft Global Hackathon (aka Hackweek) 2021:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Microsoft Global Hackathon is an internal Hackathon where Microsoft employees all over the world devote full three work-days to a project of their liking.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id="our-project"&gt;Our Project&lt;/h1&gt;
&lt;p&gt;Let me introduce you to: &lt;a href="https://github.com/XBOX-CSM/PlayFabMatchHistoryExtension"&gt;PlayFab Match History Extension&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;This is a proof-of-concept implementation of an extension for &lt;a href="https://playfab.com/"&gt;PlayFab&lt;/a&gt;, which uses a custom &lt;a href="https://docs.microsoft.com/en-us/gaming/playfab/features/automation/playstream-events/"&gt;PlayStream Event&lt;/a&gt; to signal the end of a match on a game server.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/playfab-match-history/PlayFabMatchHistory.drawio.png" alt="Architecture"&gt;&lt;/p&gt;
&lt;h2 id="components-flow"&gt;Components &amp;amp; Flow&lt;/h2&gt;
&lt;p&gt;We use the PlayStream to trigger an &lt;a href="https://docs.microsoft.com/en-us/azure/azure-functions/functions-overview"&gt;Azure Function&lt;/a&gt; via &lt;a href="https://docs.microsoft.com/bs-latn-ba/azure/storage/queues/"&gt;Azure Storage Queues&lt;/a&gt; (instead of HTTP to make it more resilient).&lt;/p&gt;
&lt;p&gt;The Azure Function, triggered via the Storage Queue, persists the event data in an &lt;a href="https://docs.microsoft.com/en-us/azure/cosmos-db/introduction"&gt;Azure Cosmos DB&lt;/a&gt; Container. We use the &lt;a href="https://docs.microsoft.com/en-us/azure/cosmos-db/serverless"&gt;Serverless SKU&lt;/a&gt; here, as we try to generate as less cost as possible during development. If you have higher and constant demand, switching to a &lt;a href="https://docs.microsoft.com/en-us/azure/cosmos-db/how-to-choose-offer"&gt;provisioned RU model (manual or automatic)&lt;/a&gt; is recommended for performance.&lt;/p&gt;
&lt;p&gt;Another Azure Function App exposes a Web API, which allows querying the data in the CosmosDb. The API exposes an &lt;a href="https://www.openapis.org/"&gt;OpenAPI&lt;/a&gt; (Swagger) endpoint and is secured using PlayFab authentication, to also demonstrate this integration.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/playfab-match-history/swagger-screenshot.png" alt="Swagger UI Screenshot"&gt;&lt;/p&gt;
&lt;p&gt;Both Azure Function apps have &lt;a href="https://docs.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview"&gt;Application Insights&lt;/a&gt; enabled for them to monitor performance, anomalies and usage.
Both log to the same Azure Log Analytics Workspace, which enables us to use the &lt;a href="https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/"&gt;Kusto Query Language&lt;/a&gt; to analyse the usage of the Functions.&lt;/p&gt;
&lt;p&gt;The infrastructure is set-up using &lt;a href="https://www.terraform.io/"&gt;Terraform&lt;/a&gt;, to provide reproducible infrastructure ready to deploy the above mentioned application components on.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/playfab-match-history/terraform.png" alt="Terraform snippet"&gt;&lt;/p&gt;
&lt;p&gt;We used &lt;a href="https://github.com/features/actions"&gt;GitHub Actions&lt;/a&gt; to continuously deploy the applications to the provisioned infrastructure on every push and pull request. &lt;a href="https://github.com/XBOX-CSM/PlayFabMatchHistoryExtension/blob/main/.github/workflows/main.yml"&gt;See our workflow&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There are still a few manual steps to do, like creating an Azure Service Principal and setting up your PlayFab title and the Azure Functions binding within your title. You can check out my &lt;a href="https://blog.structed.me/extending-playfab-with-azure-functions"&gt;post on a detailed walk-through on how to set up Azure Function bindings&lt;/a&gt;!&lt;/p&gt;
&lt;h1 id="going-further"&gt;Going Further&lt;/h1&gt;
&lt;p&gt;While this project is not ready for production use, go ahead and fork the project. Then adapt it to your needs!&lt;/p&gt;
&lt;p&gt;Here are some ideas of what you can do with it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Get the amount of matches a player has played/won/lost and a ratio of that&lt;/li&gt;
&lt;li&gt;Get the top match winners&lt;/li&gt;
&lt;li&gt;Determine whom a player plays most with&lt;/li&gt;
&lt;li&gt;Add more meta data and determine the most liked map/weapon etc.&lt;/li&gt;
&lt;li&gt;Connect the Cosmos DB to PowerBI and use it for visualization!&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="conclusion"&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;Let me know on &lt;a href="https://twitter.com/structed"&gt;Twitter&lt;/a&gt; on what you are building with this or let me know your feedback.&lt;/p&gt;
&lt;p&gt;Do feel free to &lt;a href="https://github.com/XBOX-CSM/PlayFabMatchHistoryExtension"&gt;contribute to the project&lt;/a&gt; if you have an idea how to make this better usable for others!&lt;/p&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
		<item>
			<title>Extending PlayFab - Reacting to PlayStream Events with Azure Functions</title>
			<link>https://blog.structed.me/posts/extending-playfab-with-azure-functions</link>
			<description>Step-by-step guide on how to react to PlayFab PlayStream events with Azure Functions</description>
			<enclosure url="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/PFAF.png" length="0" type="image" />
			<guid>https://blog.structed.me/posts/extending-playfab-with-azure-functions</guid>
			<pubDate>Wed, 07 Jul 2021 00:00:00 GMT</pubDate>
			<content:encoded>&lt;h1 id="introduction"&gt;Introduction&lt;/h1&gt;
&lt;p&gt;As my teammate Andreas Pohl wrote in his article &lt;a href="https://developer.microsoft.com/en-us/games/blog/build-vs-buy-which-online-service-is-right-for-my-game/"&gt;Build vs. Buy - Which online service is right for my game&lt;/a&gt;, PlayFab is a great way to get started with a backend for your game.&lt;/p&gt;
&lt;p&gt;Now you started with PlayFab and you have come to the point where you want to extend the functionality with your own game logic, for example by leveraging &lt;a href="https://docs.microsoft.com/en-us/gaming/playfab/features/automation/playstream-events/"&gt;Azure PlayFab's PlayStream&lt;/a&gt; and reacting to it's events. How would you do that?&lt;/p&gt;
&lt;p&gt;Good news: PlayFab has an integration with &lt;a href="https://azure.microsoft.com/en-us/services/functions/"&gt;Azure Functions&lt;/a&gt;! In this article, I will show you how you can react to PlayStream Events with Azure Functions and Azure Storage.&lt;/p&gt;
&lt;h2 id="discourse-what-are-azure-functions"&gt;Discourse: What are Azure Functions?&lt;/h2&gt;
&lt;p&gt;From the &lt;a href="https://docs.microsoft.com/en-us/azure/azure-functions/functions-overview"&gt;official documentation&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Azure Functions allows you to run small pieces of code (called "functions") without worrying about application infrastructure. With Azure Functions, the cloud infrastructure provides all the up-to-date servers you need to keep your application running at scale.&lt;/p&gt;
&lt;p&gt;A function is "triggered" by a specific type of event. &lt;a href="https://docs.microsoft.com/en-us/azure/azure-functions/functions-triggers-bindings"&gt;Supported triggers&lt;/a&gt; include responding to changes in data, responding to messages, running on a schedule, or as the result of an HTTP request.&lt;/p&gt;
&lt;p&gt;While you can always code directly against a myriad of services, integrating with other services is streamlined by using bindings. Bindings give you &lt;a href="https://docs.microsoft.com/en-us/azure/azure-functions/functions-triggers-bindings"&gt;declarative access to a wide variety of Azure and third-party services&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="goal"&gt;Goal&lt;/h2&gt;
&lt;p&gt;This is a detailed end-to-end example on how to extend Azure PlayFab with Azure Functions.&lt;/p&gt;
&lt;p&gt;We will be using C# &amp;amp; .NET Core to create an Azure Function and bind it to a &lt;a href="https://docs.microsoft.com/en-us/gaming/playfab/features/automation/playstream-events/"&gt;PlayStream Event&lt;/a&gt;. The event to bind to will be the &lt;code&gt;player_created&lt;/code&gt; Event, which fires every time a new player registers. The binding will then call the Azure Function.&lt;/p&gt;
&lt;p&gt;This Azure Function will add "User Data" to this new Player.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Title User Data is title-specific custom data for the user which is readable and writable by the client. But you can of course set this via the Server as well. Player Data and User Data are terms used interchangeable within PlayFab terminology. Please refer to the &lt;a href="https://docs.microsoft.com/en-us/gaming/playfab/features/data/playerdata/quickstart"&gt;documentation&lt;/a&gt; for more information.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="prerequisites"&gt;Prerequisites&lt;/h2&gt;
&lt;h3 id="editoride"&gt;Editor/IDE&lt;/h3&gt;
&lt;p&gt;While you can follow along with basically any Editor/IDE, I will be using &lt;a href="https://visualstudio.microsoft.com/"&gt;Visual Studio 2019&lt;/a&gt; (you may use the the &lt;strong&gt;free&lt;/strong&gt; Visual Studio 2019 Community Edition!) because it has all the batteries included for our tasks ahead.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you do not use Visual Studio, but want to use another IDE/Toolset, there are examples for creating &amp;amp; publishing Azure Function Apps with &lt;a href="https://docs.microsoft.com/en-us/azure/azure-functions/functions-create-first-function-vs-code?pivots=programming-language-csharp"&gt;Visual Studio Code&lt;/a&gt; or via &lt;a href="https://docs.microsoft.com/en-us/azure/azure-functions/functions-create-first-azure-function-azure-cli?tabs=bash%2Cbrowser&amp;amp;pivots=programming-language-csharp"&gt;CLI&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="azure-access"&gt;Azure Access&lt;/h3&gt;
&lt;p&gt;If you do not yet have used Azure, you &lt;a href="https://azure.microsoft.com/en-us/free/gaming/"&gt;sign up for free&lt;/a&gt; and get some additional goodies with &lt;a href="https://visualstudio.microsoft.com/dev-essentials/"&gt;Visual Studio Dev Essentials&lt;/a&gt; program.&lt;/p&gt;
&lt;h3 id="azure-playfab-access"&gt;Azure PlayFab access&lt;/h3&gt;
&lt;p&gt;And, of course, since this is a Tutorial on how to use &lt;a href="https://playfab.com/"&gt;Azure PlayFab&lt;/a&gt;, you need an existing Title on PlayFab as well. You could use the same Microsoft Account you used for signing up to Azure. Remember: There is a &lt;a href="https://developer.playfab.com/en-US/sign-up"&gt;free tier to get you started&lt;/a&gt;!&lt;/p&gt;
&lt;h1 id="getting-started"&gt;Getting Started&lt;/h1&gt;
&lt;p&gt;As there are so many languages and platforms supported by Azure PlayFab and Azure Functions, I had to decide for one and chose the language and platform I am best in: Pure C# &amp;amp; .NET Core. But there are great guides for various Game Engines like Unity and Unreal, and PlayFab provides SDKs for a plethora of languages that are very similar to the C# SDK because they are all auto-generated from the same REST API.&lt;/p&gt;
&lt;p&gt;So while this is C#, most of what we do here is very similar in other languages/runtimes&lt;/p&gt;
&lt;h2 id="project-setup"&gt;Project Setup&lt;/h2&gt;
&lt;h3 id="create-an-azure-functions-project"&gt;Create an Azure Functions Project&lt;/h3&gt;
&lt;p&gt;To create an Azure Functions Project, open up Visual Studio and either use the splash screen (see screenshot below) or use File --&amp;gt; New Project…&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/VisualStudio-NewProject-1.png" alt="Visual Studio - Splash Screen"&gt;&lt;/p&gt;
&lt;p&gt;Select the project type “Azure Functions” and click “Next”&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/VisualStudio-NewProject-2.png" alt="Visual Studio - Create new Azure Functions Project"&gt;&lt;/p&gt;
&lt;p&gt;Choose a name for your Project and select the folder for it to be placed in:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/VisualStudio-NewProject-3.png" alt="Visual Studio - Fill out project details"&gt;&lt;/p&gt;
&lt;p&gt;This will create a folder named according to your Solution name, in which it will create another folder for the project, with the Project’s name you chose.&lt;/p&gt;
&lt;p&gt;Now, in the next dialog, you may either choose an &lt;code&gt;HttpTrigger&lt;/code&gt; or a &lt;code&gt;QueueTrigger&lt;/code&gt; for your Function. Please choose &lt;code&gt;QueueTrigger&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Please be aware that Azure PlayFab terminates PlayStream-invoked Azure Functions after 1 second, thus &lt;code&gt;QueueTrigger&lt;/code&gt; is the better choice here since it will immediately yield back to Azure PlayFab.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For the Storage Account drop-down, you may use Storage Emulator (which comes with Visual Studio or you can use the new &lt;a href="https://docs.microsoft.com/en-us/azure/storage/common/storage-use-azurite"&gt;"Azurite"&lt;/a&gt;, which is the new generation of the Storage Emulator) or click “Browse…” to select a Storage Account. As we will need to test it with Azure PlayFab anyways, let’s go ahead and use an Azure Storage Account. Please click "Browse…" to create a new Storage Account:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/VisualStudio-NewProject-4.png" alt="Select Functions Trigger"&gt;&lt;/p&gt;
&lt;p&gt;In the next dialog, click "Create a storage account"&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/VisualStudio-NewProject-NewStorageAccount-1.png" alt="Select storage account"&gt;&lt;/p&gt;
&lt;p&gt;When creating the storage account, make sure to create a unique name. It actually has to be globally unique, because it will be part of the URI. You will see a warning if it is not unique.&lt;/p&gt;
&lt;p&gt;Also, you should create a new resource group for this project. A resource group is basically the topmost container in Azure and can contain all the different resources we are going to create. For example, if you want to remove all of the things we are creating in this article, you could just remove the resource group. &lt;a href="https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/overview"&gt;You can read more about the Resource Manager deployment model and resource groups in the documentation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Last, let's think about the Account type. While it is enough for the sake of this tutorial to choose "LRS - Locally Redundant Storage", it it is worth considering &lt;a href="https://docs.microsoft.com/en-us/azure/storage/common/storage-redundancy"&gt;other redundancy options&lt;/a&gt; for different use-cases.&lt;/p&gt;
&lt;p&gt;Click "Create" when you have filled all fields and created a resource group.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/VisualStudio-NewProject-NewStorageAccount-2.png" alt="Create storage account"&gt;&lt;/p&gt;
&lt;p&gt;Now back to the Storage Account select screen, select the one you just created and click "Add".&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/VisualStudio-NewProject-NewStorageAccount-3.png" alt="Add storage account config"&gt;&lt;/p&gt;
&lt;p&gt;The Storage Account should now appear in the drop-down, beneath "Storage Emulator" and "Browse...". Select it.&lt;/p&gt;
&lt;p&gt;I would advise to leave "Connection string setting name" empty for now. Using this would create a new setting and would be a bit confusing for now.&lt;/p&gt;
&lt;p&gt;For "Queue name", make up a name for the storage queue (which we will actually create later). It does not have to have a unique name, but it may only use characters, numbers and dashes.&lt;/p&gt;
&lt;p&gt;Just call it &lt;code&gt;login-events&lt;/code&gt; for now.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/VisualStudio-NewProject-4-1.png" alt="Set up storage settings"&gt;&lt;/p&gt;
&lt;p&gt;Once you are done setting up the Storage Account configuration, click the “Create” button and wait for the project to generate. Visual Studio will eventually open the project and show you the code of the generated Azure Function stub (your namespace name may vary):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;using System;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.Extensions.Logging;

namespace PlayFabEventStreamHandler
{
    public static class Function1
    {
        [FunctionName("Function1")]
        public static void Run([QueueTrigger("login-events", Connection = "")]string myQueueItem, ILogger log)
        {
            log.LogInformation($"C# Queue trigger function processed: {myQueueItem}");
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;!-- ![View of scaffold code in Visual Studio](../images/posts/extending-playfab-with-azure-functions/VisualStudio-NewProject-5.png) --&gt;
&lt;blockquote&gt;
&lt;p&gt;You might have noticed, that the Function was named &lt;code&gt;Function1&lt;/code&gt;, because Visual Studio just does not know what we want to do. Please rename the File, Class and the Function name in the Annotation as &lt;code&gt;PlayStreamEventHandler&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Renaming is done easiest in Visual Studio by right-clicking the Class name in the code editor and selecting "Rename" (or just press F2). This will open a tiny dialog asking you where you want to replace. Select "Rename file" and "Include strings". This will make sure Class and File are renamed as well as the Attribute string.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/VisualStudio-RenameFile.png" alt="Rename Function1"&gt;&lt;/p&gt;
&lt;p&gt;While asking for a Connection string setting in the dialog, we did not give it a name. The reason is, that Visual Studio adds a connection string to the Storage Account we previously selected, by default It is called &lt;a href="https://docs.microsoft.com/en-us/azure/azure-functions/functions-app-settings#azurewebjobsstorage"&gt;&lt;code&gt;AzureWebJobsStorage&lt;/code&gt;&lt;/a&gt;. This is set in the &lt;code&gt;local.settings.json&lt;/code&gt; config file:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/VisualStudio-local.settings.json.png" alt="Connection String"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;local.settings.json&lt;/code&gt; is a config file &lt;em&gt;only&lt;/em&gt; for local configuration. For deployed functions, you can either use the &lt;a href="https://docs.microsoft.com/en-us/azure/azure-functions/functions-how-to-use-azure-function-app-settings"&gt;Application Settings built into Azure Functions&lt;/a&gt;, or for more advanced scenarios, you could use &lt;a href="https://docs.microsoft.com/en-us/azure/azure-app-configuration/overview"&gt;Azure App Config&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;But because we did not give the connection setting a name earlier, there was no name set in the function. If you look closely at the Azure Function code, you can spot it in the Signature of the &lt;code&gt;Run&lt;/code&gt; method. Look at the &lt;code&gt;Connection = ""&lt;/code&gt; property of the &lt;code&gt;QueueTrigger&lt;/code&gt; attribute:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public static void Run([QueueTrigger("login-events", Connection = "")]string myQueueItem, ILogger log)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We just need to put in &lt;code&gt;AzureWebJobsStorage&lt;/code&gt; instead of the empty string to load the right connection string:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public static void Run([QueueTrigger("login-events", Connection = "AzureWebJobsStorage")]string myQueueItem, ILogger log)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="add-the-azure-playfab-package"&gt;Add the Azure PlayFab package&lt;/h3&gt;
&lt;p&gt;The Azure PlayFab SDK for C# is available via NuGet. You can either add the latest version via the dotnet CLI&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dotnet add package PlayFabAllSDK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or via the NuGet package manager in Visual Studio:
Right-click “Dependencies” and click “Manage NuGetPackages”&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/VisualStudio-ManageNugetPackagesContextMenu.png" alt="Context menu right-click Solution Explorer"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/VisualStudio-NuGetPackageManager.png" alt="NuGet browser: Install PlayFab SDK"&gt;&lt;/p&gt;
&lt;p&gt;Open the “Browse” tab and search for PlayFab. Select the &lt;code&gt;PlayFabAllSDK&lt;/code&gt; and click “Install” on the details pane to install. You might need to accept licenses when you do.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Please note: you should also update the installed packages because the Visual Studio template ships with a slightly older version of the SDKs for Azure Functions and WebJobs Storage.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="create-a-storage-queue"&gt;Create a Storage Queue&lt;/h3&gt;
&lt;p&gt;The last missing piece before we have all the infrastructure to test the Function the first time is a &lt;a href="https://azure.microsoft.com/en-us/services/storage/queues/"&gt;Storage Queue&lt;/a&gt;. Now let's create one!&lt;/p&gt;
&lt;p&gt;Go to the Azure Portal and open up your Storage Account’s Queue blade:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/AzurePortal-StorageAccount-Queue.png" alt="Azure Storage Account - Queues overview"&gt;&lt;/p&gt;
&lt;p&gt;Created a Queue by clicking the “+ Queue” button. Make sure to use the same name you specified when creating the Azure Function with the Queue Trigger (see screenshot below). We used to call it &lt;code&gt;login-events&lt;/code&gt;´.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/VisualStudio-QueueTriggerAttribute.png" alt="Code - Name of subscribed queue"&gt;&lt;/p&gt;
&lt;h3 id="test-the-function"&gt;Test the Function&lt;/h3&gt;
&lt;p&gt;Once you have created the Queue, click the Queue entry in the Portal to get to the detail Blade.
Here, click “+ Add message”. In the new Dialog, just add some text and click “OK”.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/AzurePortal-StorageAccount-AddQueue.png" alt="Storage Queue: Add new message"&gt;&lt;/p&gt;
&lt;p&gt;Back in Visual Studio, press F5 (or click Build --&amp;gt; Debug) to launch the local Azure Functions runtime in debug mode.&lt;/p&gt;
&lt;p&gt;Now watch the console window of your Function: it will log out how it processes the queue message you just added!&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/Termina-FunctionFirstRun.png" alt="Terminal shows running Function"&gt;&lt;/p&gt;
&lt;p&gt;Checking back in the Azure Portal’s view of the Queue, you click the “Refresh” button to see how the message disappeared, because it got processed.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you want to inspect the Queue without actually de-queuing it, you can use the &lt;a href="https://storageexplorer.com/"&gt;Azure Storage Explorer&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id="set-up-playfab"&gt;Set up PlayFab&lt;/h1&gt;
&lt;p&gt;To call your Azure Function from Azure PlayFab, we need to register the Function binding via the Azure Storage Queue and set up an Event Trigger (aka &lt;em&gt;“Rule”&lt;/em&gt;).&lt;/p&gt;
&lt;h2 id="register-the-function"&gt;Register the Function&lt;/h2&gt;
&lt;p&gt;Log in to &lt;a href="https://playfab.com/"&gt;Azure PlayFab&lt;/a&gt; and click on the Title you want to register the Azure Function for.&lt;/p&gt;
&lt;p&gt;Once you are in the Title’s overview, click “Automation” (1), which opens the “Cloud Script” tab. In this tab, select the sub-tab “Functions (Preview)” (2), which will list all the previously registered Azure Functions. Now click the big orange button in the top right labeled “REGISTER FUNCTION” (3) to open the registration dialog.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/PlayFab-RegisterFunction-1.png" alt="PlayFab: Functions listing"&gt;&lt;/p&gt;
&lt;p&gt;Let's go through the individual settings for a registration:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/PlayFab-RegisterFunction-2.png" alt="PlayFab: Registering an Azure Function with QueueTrigger"&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Trigger type: set to “Queue”&lt;/li&gt;
&lt;li&gt;Function name: This is not actually the name of the Azure Function you deployed, but the name of the binding we’re currently setting up – so it’s the identifier you will be working with from PlayFab. Choose a name that fits your needs!&lt;/li&gt;
&lt;li&gt;Queue name: this is the queue name the &lt;code&gt;QueueTrigger&lt;/code&gt; of the Azure Function listens to. In the examples above, this was &lt;code&gt;login-events&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Connection string: The ConnectionString to the Azure Storage Account Queue.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To finish registering the Function registration, click &lt;em&gt;“REGISTER FUNCTION”&lt;/em&gt;. The Registration should now appear in the overview.&lt;/p&gt;
&lt;h2 id="set-up-a-rule"&gt;Set up a Rule&lt;/h2&gt;
&lt;p&gt;To set up a Rule, within a PlayFab Title, go to &lt;em&gt;“Automation”&lt;/em&gt; (1) in the left pane, select the &lt;em&gt;“Rules”&lt;/em&gt; Tab (2) and click &lt;em&gt;“NEW RULE”&lt;/em&gt; (3).&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/PlayFab-AddRule-1.png" alt="PlayFab: Adding a Rule"&gt;&lt;/p&gt;
&lt;p&gt;A Rule is a configuration that listens on a &lt;a href="https://docs.microsoft.com/en-us/gaming/playfab/features/automation/playstream-events/"&gt;&lt;em&gt;PlayStream&lt;/em&gt; event&lt;/a&gt; emitted by Azure PlayFab. When such an event occurs, the Rule checks whether the configured &lt;em&gt;Conditions&lt;/em&gt; are met and if they are, triggers the configured &lt;em&gt;Action&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;In our case, we want no conditions, but want to “Execute Azure Function”. In fact, what it does, is calling its own internal binding we just set up and provides it the PlayStream event data and additional arguments you may specify here (in JSON format) as argument to that binding.&lt;/p&gt;
&lt;p&gt;When that binding is called, it will either call the Function via HTTP trigger, or, as we configured it, by writing the data to the storage queue for the Azure Function to pick up via a &lt;code&gt;QueueTrigger&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/PlayFab-AddRule-2.png" alt="PlayFab: Details of adding a Rule"&gt;&lt;/p&gt;
&lt;p&gt;Click “&lt;em&gt;SAVE ACTION&lt;/em&gt;” to save (yes, this saves the Rule 😉). You should now see the Rule in the overview.&lt;/p&gt;
&lt;h2 id="trigger-the-playfab-rule"&gt;Trigger the PlayFab Rule&lt;/h2&gt;
&lt;p&gt;Now we want to test the Rule and trigger the Azure Function via an Azure PlayFab PlayStream Event. To do this, we need to raise an event of type &lt;code&gt;com.playfab.player_logged_in&lt;/code&gt;, as we specified when we created the Rule.&lt;/p&gt;
&lt;p&gt;To achieve this, let’s create a small CLI application to log a Player in.&lt;/p&gt;
&lt;p&gt;In Visual Studio, create a new .NET Core Application (preferably in the same Solution as the Function app) and then install the &lt;code&gt;PlayFabAllSDK&lt;/code&gt;, just like above.&lt;/p&gt;
&lt;p&gt;Now, you can paste the following C# code to your &lt;code&gt;Program.cs&lt;/code&gt; to have a working program that signs into your game and if the Player does not yet exist, it will create a new one.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Well, at least it is almost working. You need to substitute XXXX with your own Title ID. You can get the Title ID from your &lt;a href="https://developer.playfab.com/en-US/my-games"&gt;Studio’s Overview Page&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Below code listing as &lt;a href="https://gist.github.com/Structed/ff3190248833f40f611f0941f33793b9"&gt;GitHub Gist&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;using PlayFab;
using PlayFab.ClientModels;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace GameCli
{
    class Program
    {
        private static bool running = true;
        static void Main(string[] args)
        {
            PlayFabSettings.staticSettings.TitleId = "XXXXX"; // Please change this value to your own titleId from PlayFab Game Manager

            var request = new LoginWithCustomIDRequest { CustomId = "Player-" + Guid.NewGuid(), CreateAccount = true };
            var loginTask = PlayFabClientAPI.LoginWithCustomIDAsync(request);

            while (running)
            {
                if (loginTask.IsCompleted) // You would probably want a more sophisticated way of tracking pending async API calls in a real game
                {
                    OnLoginComplete(loginTask);
                }

                // Presumably this would be your main game loop, doing other things
                Thread.Sleep(1);
            }

            Console.WriteLine($"Done! Created new player named \"{request.CustomId}\". Press any key to close");
            Console.ReadKey(); // This halts the program and waits for the user
        }

        private static void OnLoginComplete(Task&amp;lt;PlayFabResult&amp;lt;LoginResult&amp;gt;&amp;gt; taskResult)
        {
            var apiError = taskResult.Result.Error;
            var apiResult = taskResult.Result.Result;

            if (apiError != null)
            {
                Console.ForegroundColor = ConsoleColor.Red; // Make the error more visible
                Console.WriteLine("Something went wrong with your first API call.  :(");
                Console.WriteLine("Here's some debug information:");
                Console.WriteLine(PlayFabUtil.GenerateErrorReport(apiError));
                Console.ForegroundColor = ConsoleColor.Gray; // Reset to normal color
            }
            else if (apiResult != null)
            {
                Console.WriteLine($"Was newly created? {apiResult.NewlyCreated}");
                Console.WriteLine("Congratulations, you made your first successful API call!");
            }

            running = false; // Because this is just an example, successful login triggers the end of the program
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now execute the program and watch how the event is executed in PlayFab via the PlayStream Monitor!&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/PlayFab-PlayStream-ExecutedFunction.png" alt="The function was executed!"&gt;&lt;/p&gt;
&lt;h1 id="backend-implementation"&gt;Backend implementation&lt;/h1&gt;
&lt;p&gt;It's time to start the actual backend implementation!&lt;/p&gt;
&lt;h2 id="using-the-playfab-context"&gt;Using the PlayFab Context&lt;/h2&gt;
&lt;p&gt;Before we even get started with writing backend code, you will need a &lt;a href="https://github.com/PlayFab/PlayFab-Samples/blob/master/Samples/CSharp/AzureFunctions/CS2AFHelperClasses.cs"&gt;Helper classes file&lt;/a&gt;, provided by PlayFab. &lt;a href="https://github.com/PlayFab/PlayFab-Samples/blob/master/Samples/CSharp/AzureFunctions/CS2AFHelperClasses.cs"&gt;Download the file&lt;/a&gt; and save it to your project (directly beneath the &lt;code&gt;*.csproj&lt;/code&gt;
or the Function class file). Here is why:&lt;/p&gt;
&lt;p&gt;Every time a Function is triggered, The &lt;code&gt;Context&lt;/code&gt; in which it was executed is sent as a parameter. The Context includes the following when the Function is triggered via a PlayStream Event:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The Entity Profile&lt;/li&gt;
&lt;li&gt;The PlayStream event which triggered the script.&lt;/li&gt;
&lt;li&gt;A boolean that indicates whether a PlayStream event is sent as part of the function being executed&lt;/li&gt;
&lt;li&gt;The Payload data you specified when setting up the binding in PlayFab&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;For more information on the Context and what it contains in other environments (like if an Azure Function is called from a Game Client), please see the &lt;a href="https://docs.microsoft.com/en-us/gaming/playfab/features/automation/cloudscript-af/cloudscript-af-context"&gt;Using CloudScript context models Tutorial&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="parsing-the-context"&gt;Parsing the Context&lt;/h3&gt;
&lt;p&gt;We already receive the Context in the Function we have created, it is the &lt;code&gt;myQueueItem&lt;/code&gt; parameter:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;        [FunctionName("PlayStreamEventHandler")]
        public static void Run([QueueTrigger("login-events", Connection = "AzureWebJobsStorage")]string myQueueItem, ILogger log)
        {
            log.LogInformation($"C# Queue trigger function processed: {myQueueItem}");
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, it is just a JSON string, and we want it to be parsed to a C# POCO ("Plain Old C# Object").&lt;/p&gt;
&lt;p&gt;To do this, we first need the Class to convert to. The easiest way to do this is to use the PlayFab helper classes we downloaded and added above. It will contain all the Context Objects you need for the various deserialization use-cases.&lt;/p&gt;
&lt;p&gt;Now, we can actually deserialize the Context to &lt;code&gt;PlayerPlayStreamFunctionExecutionContext&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;        [FunctionName("PlayStreamEventHandler")]
        public static void Run([QueueTrigger("login-events", Connection = "AzureWebJobsStorage")]string myQueueItem, ILogger log)
        {
            var context = JsonConvert.DeserializeObject&amp;lt;PlayerPlayStreamFunctionExecutionContext&amp;lt;dynamic&amp;gt;&amp;gt;(myQueueItem);
            log.LogInformation($"C# Queue trigger function processed: {myQueueItem}");
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="authentication"&gt;Authentication&lt;/h2&gt;
&lt;p&gt;Authentication in Azure PlayFab might seem a little bit overwhelming at first, but it enables high flexibility and best operational security by allowing you to only make requests in the relevant context.&lt;/p&gt;
&lt;p&gt;For server-side actions, like we want to do here, this is particularly simple: we only need to set the &lt;code&gt;TitleId&lt;/code&gt; and the &lt;code&gt;DeveloperSecretKey&lt;/code&gt; on &lt;code&gt;PlayFabSettings.staticSettings&lt;/code&gt;. Let's do that!&lt;/p&gt;
&lt;h3 id="retrieving-the-titleid"&gt;Retrieving the &lt;code&gt;TitleId&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This one is the simplest - you can just get it from the context we have deserialized:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;    PlayFabSettings.staticSettings.TitleId = context.TitleAuthenticationContext.Id;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="retrieving-the-developersecretkey"&gt;Retrieving the &lt;code&gt;DeveloperSecretKey&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The DeveloperSecretKey must be retrieved from the &lt;a href="https://developer.playfab.com/"&gt;Azure PlayFab Portal&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Log in and go to your Title you are working on.
Then, click the little cogwheel beneath your Title's name (1) and select "Title Settings" (2). Last, select the "Secret Keys" Tab (3)&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/PlayFab-AddSecretKey.png" alt="PlayFab - Add Secret Key (DeveloperSecretKey)"&gt;&lt;/p&gt;
&lt;p&gt;On This page, go ahead and click "NEW SECRET KEY". Specify a name and click "SAVE SECRET KEY".&lt;/p&gt;
&lt;p&gt;Now back on the Secret Key Overview, copy the secret key.&lt;/p&gt;
&lt;h3 id="store-the-secret"&gt;Store the secret&lt;/h3&gt;
&lt;p&gt;Since we do not want to store the secret directly in the code, we will create a new setting &lt;code&gt;PlayFabDeveloperSecretKey&lt;/code&gt; in &lt;code&gt;local.settings.json&lt;/code&gt;. Substitute &lt;code&gt;&amp;lt;YOUR SECRET KEY&amp;gt;&lt;/code&gt; with the secret you just copied from PlayFab:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json"&gt;    {
        "IsEncrypted": false,
        "Values": {
            "AzureWebJobsStorage": "&amp;lt;connection string to queue storage&amp;gt;"
            "PlayFabDeveloperSecretKey": "&amp;lt;YOUR SECRET KEY&amp;gt;",
            "FUNCTIONS_WORKER_RUNTIME": "dotnet"
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="authenticating-using-playfab-static-settings"&gt;Authenticating using PlayFab Static Settings&lt;/h3&gt;
&lt;p&gt;Now that we have all the information, let's go ahead and put together the code.&lt;/p&gt;
&lt;p&gt;Go to your Function's code, and paste the following code below your parsed context:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;    PlayFabSettings.staticSettings.TitleId = context.TitleAuthenticationContext.Id;
    PlayFabSettings.staticSettings.DeveloperSecretKey = Environment.GetEnvironmentVariable("DeveloperSecretKey");
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The function should now look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;    namespace PlayFabEventStreamHandler
    {
        public static class PlayStreamEventHandler
        {
            [FunctionName("PlayStreamEventHandler")]
            public static async Task Run([QueueTrigger("login-events", Connection = "AzureWebJobsStorage")] string myQueueItem, ILogger log)
            {
                var context = JsonConvert.DeserializeObject&amp;lt;PlayerPlayStreamFunctionExecutionContext&amp;lt;dynamic&amp;gt;&amp;gt;(myQueueItem);
                log.LogInformation($"C# Queue trigger function processed: {myQueueItem}");

                PlayFabSettings.staticSettings.TitleId = context.TitleAuthenticationContext.Id;
                PlayFabSettings.staticSettings.DeveloperSecretKey = Environment.GetEnvironmentVariable("DeveloperSecretKey");
            }
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is already all we need to make requests using &lt;code&gt;PlayFabServerAPI&lt;/code&gt; or &lt;code&gt;PlayFabAdminAPI&lt;/code&gt; - now we only need to make a request!&lt;/p&gt;
&lt;h2 id="updating-player-data"&gt;Updating Player Data&lt;/h2&gt;
&lt;p&gt;Create a new Method &lt;code&gt;UpdatePlayerData&lt;/code&gt; as below:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;    private static async Task UpdatePlayerData(ILogger log, PlayerPlayStreamFunctionExecutionContext&amp;lt;dynamic&amp;gt; context)
    {
        var updateUserDataRequest = new PlayFab.ServerModels.UpdateUserDataRequest {
            PlayFabId = context.PlayerProfile.PlayerId,
            Data = new Dictionary&amp;lt;string, string&amp;gt; {
                {"&amp;lt;KEY&amp;gt;", "&amp;lt;VALUE&amp;gt;"}
            }
        };
        PlayFabResult&amp;lt;PlayFab.ServerModels.UpdateUserDataResult&amp;gt; result = await PlayFabServerAPI.UpdateUserDataAsync(updateUserDataRequest);
        
        if (playFabResult.Error != null)
        {
            log.LogError($"Code: {playFabResult.Error.Error}\n{playFabResult.Error.ErrorMessage}");
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;an call it in your Function's &lt;code&gt;Run()&lt;/code&gt; method:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;    await UpdatePlayerData(log, context);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your full function code should now look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;    using Microsoft.Azure.WebJobs;
    using Microsoft.Extensions.Logging;
    using Newtonsoft.Json;
    using PlayFab;
    using PlayFab.AdminModels;
    using PlayFab.Samples;
    using System;
    using System.Collections.Generic;
    using System.Threading.Tasks;
    
    namespace PlayFabEventStreamHandler
    {
        public static class PlayStreamEventHandler
        {
            [FunctionName("PlayStreamEventHandler")]
            public static async Task Run([QueueTrigger("login-events", Connection = "AzureWebJobsStorage")] string myQueueItem, ILogger log)
            {
                var context = JsonConvert.DeserializeObject&amp;lt;PlayerPlayStreamFunctionExecutionContext&amp;lt;dynamic&amp;gt;&amp;gt;(myQueueItem);
                log.LogInformation($"C# Queue trigger function processed: {myQueueItem}");
    
                PlayFabSettings.staticSettings.TitleId = context.TitleAuthenticationContext.Id;
                PlayFabSettings.staticSettings.DeveloperSecretKey =
                    Environment.GetEnvironmentVariable("DeveloperSecretKey");
    
                await UpdatePlayerData(log, context);
                await CreateNews(log, context);
                await CreateItem(log, context);
            }
            
            private static async Task UpdatePlayerData(ILogger log, PlayerPlayStreamFunctionExecutionContext&amp;lt;dynamic&amp;gt; context)
            {
                var updateUserDataRequest = new PlayFab.ServerModels.UpdateUserDataRequest {
                    PlayFabId = context.PlayerProfile.PlayerId,
                    Data = new Dictionary&amp;lt;string, string&amp;gt; {
                        {"&amp;lt;KEY&amp;gt;", "&amp;lt;VALUE&amp;gt;"}
                    }
                };
                PlayFabResult&amp;lt;PlayFab.ServerModels.UpdateUserDataResult&amp;gt; result = await PlayFabServerAPI.UpdateUserDataAsync(updateUserDataRequest);

                if (playFabResult.Error != null)
                {
                    log.LogError($"Code: {playFabResult.Error.Error}\n{playFabResult.Error.ErrorMessage}");
                }
            }
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ok, that's quite a chunk. Now what does &lt;code&gt;UpdatePlayerData()&lt;/code&gt; do? Let's analyze the different blocks:&lt;/p&gt;
&lt;h4 id="creating-a-request-object"&gt;Creating a Request object&lt;/h4&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;    var updateUserDataRequest = new PlayFab.ServerModels.UpdateUserDataRequest {
        PlayFabId = context.PlayerProfile.PlayerId,
        Data = new Dictionary&amp;lt;string, string&amp;gt; {
            {"&amp;lt;KEY&amp;gt;", "&amp;lt;VALUE&amp;gt;"}
        }
    };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each PlayFab API request has a corresponding Request object, which is named accordingly and is located in the Namespace &lt;code&gt;PlayFab.&amp;lt;API TYPE&amp;gt;Models&lt;/code&gt;, where &lt;code&gt;&amp;lt;API TYPE&amp;gt;&lt;/code&gt; corresponds to one of the many APIs, like the Server, Admin, User or Authentication APIs, among others.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;UpdateUserDataRequest&lt;/code&gt; &lt;em&gt;requires&lt;/em&gt; a &lt;code&gt;PlayFabId&lt;/code&gt; to know which Object to update. In our case, we want to update a Player, thus we need to pass a &lt;code&gt;PlayerId&lt;/code&gt;, so it knows which Player to update. You can retrieve that from the Context that we deserialized earlier.&lt;/p&gt;
&lt;p&gt;Now to the core: Adding a &lt;code&gt;Dictionary&amp;lt;string, string&amp;gt;&lt;/code&gt; as &lt;code&gt;Data&lt;/code&gt;, containing Key/Value pairs for the Data we want to set on the Player- e.g. the amount of Mana.&lt;/p&gt;
&lt;h4 id="sending-the-request"&gt;Sending the Request&lt;/h4&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;    PlayFabResult&amp;lt;PlayFab.ServerModels.UpdateUserDataResult&amp;gt; result = await PlayFabServerAPI.UpdateUserDataAsync(updateUserDataRequest);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sending the request is as simple as calling the static Method &lt;code&gt;PlayFabServerAPI.UpdateUserDataAsync()&lt;/code&gt; and passing it the request object we created before as a parameter. This method will return a Result object, we can then check for errors.&lt;/p&gt;
&lt;h4 id="checking-for-errors"&gt;Checking for errors&lt;/h4&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;    if (playFabResult.Error != null)
    {
        log.LogError($"Code: {playFabResult.Error.Error}\n{playFabResult.Error.ErrorMessage}");
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Basically, whenever the &lt;code&gt;Error&lt;/code&gt; property on the Request object is non-null, an Error has occurred. There are a couple more Properties on the Error object which help you identify the problem before going further.&lt;/p&gt;
&lt;h2 id="testing-locally"&gt;Testing locally&lt;/h2&gt;
&lt;p&gt;Start the the Function locally, e.g. by pressing F5 in Visual Studio. Then, launch the &lt;code&gt;game-cli&lt;/code&gt; to generate a new Player which then creates a PlayStream event, triggering the Rule which calls the Function. As we trigger the function using a Queue, we can again just read them locally.&lt;/p&gt;
&lt;p&gt;Once the Function has processed the new trigger, go back to Azure PlayFab and see whether the newly created player is listed in Azure PlayFab's &lt;em&gt;Player&lt;/em&gt; listing. Then go to the player's detail view and click "Player Data (Title)". You should now see the newly added key/value pair - in my case it's &lt;code&gt;foo&lt;/code&gt; and &lt;code&gt;bar&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/PlayFab-AddedPlayerData.png" alt="PlayFab: added PlayerData"&gt;&lt;/p&gt;
&lt;h2 id="deploying-the-function"&gt;Deploying the Function&lt;/h2&gt;
&lt;p&gt;Now that we have a working Azure Function, let’s deploy it to Azure!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you do not use Visual Studio, but want to use another IDE/Toolset, there are examples for creating &amp;amp; publishing Azure Function Apps with &lt;a href="https://docs.microsoft.com/en-us/azure/azure-functions/functions-create-first-function-vs-code?pivots=programming-language-csharp"&gt;Visual Studio Code&lt;/a&gt; or via &lt;a href="https://docs.microsoft.com/en-us/azure/azure-functions/functions-create-first-azure-function-azure-cli?tabs=bash%2Cbrowser&amp;amp;pivots=programming-language-csharp"&gt;CLI&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="deploy-with-the-publish-command"&gt;Deploy with the "Publish" command&lt;/h3&gt;
&lt;p&gt;To get started and for the sake of this demo, we will be using “right click deploy”, the deployment mechanism integrated in Visual Studio. While this is convenient for a demo, please consider &lt;a href="https://dev.azure.com/"&gt;Azure DevOps&lt;/a&gt; or &lt;a href="https://github.com/features/actions"&gt;GitHub Actions&lt;/a&gt; to build your &lt;strong&gt;automated&lt;/strong&gt; Continuous Integration &amp;amp; Continuous Deployment (CI/CD) pipelines for a real project – because manual deployments are error prone, require knowledge by a team member etc. Thus, there is the saying: &lt;em&gt;“Friends don’t let Friends right-click publish!”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In Visual Studio, right-click the project and click “Publish”:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/VisualStudio-RightClick-Publish.png" alt="Right-click publish"&gt;&lt;/p&gt;
&lt;p&gt;This will open a dialog where you can configure to which Azure Function App you want to deploy – and you can even create a new one using this dialog!&lt;/p&gt;
&lt;p&gt;While this deployment dialog also supports other deployment methods, please select “Azure”.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/VisualStudio-Publish-1.png" alt="Publish - select &amp;quot;Azure&amp;quot;"&gt;&lt;/p&gt;
&lt;p&gt;Then, “&lt;em&gt;Azure Functions App (Windows)&lt;/em&gt;”.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/VisualStudio-Publish-2.png" alt="Publish - Select Azure Functions (Windows)"&gt;&lt;/p&gt;
&lt;p&gt;In the following Dialog you can choose the Azure Function App or slot beneath an app to deploy to.&lt;/p&gt;
&lt;p&gt;If you haven’t already created an Azure Function App to deploy to, you can use the “&lt;em&gt;Create a new Azure Function…&lt;/em&gt;” link to do so:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/VisualStudio-Publish-3.png" alt="Publish - Resources overview"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/VisualStudio-Publish-4.png" alt="Publish - Create new Azure Functions App"&gt;&lt;/p&gt;
&lt;p&gt;Now select the Function App you want to deploy to:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.structed.me/images/posts/extending-playfab-with-azure-functions/VisualStudio-Publish-5.png" alt="Publish - Resources overview - Select Function App"&gt;&lt;/p&gt;
&lt;p&gt;Once selected, Visual Studio will pull the “Publishing Profile” from Azure.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;WARNING&lt;/strong&gt; Never add the user part of the publishing profile (&lt;code&gt;*.pubxml.user&lt;/code&gt;) to source control, it contains your encrypted password!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now click “Publish” to deploy your Azure Function App.&lt;/p&gt;
&lt;h3 id="testing-the-online-function"&gt;Testing the online Function&lt;/h3&gt;
&lt;p&gt;Since you have now deployed the Azure Function, let's go ahead and test it.&lt;/p&gt;
&lt;p&gt;First, make sure no local instance of the Function is running, so it is not competing against the online Azure Function when reading from the Azure Storage Queue.&lt;/p&gt;
&lt;p&gt;Next, launch the &lt;code&gt;game-cli&lt;/code&gt; again, which should log-in a new Account and trigger the Function via the Storage Queue.&lt;/p&gt;
&lt;p&gt;You should now see the entry we created in the PlayFab data.&lt;/p&gt;
&lt;h1 id="taking-it-further"&gt;Taking it further&lt;/h1&gt;
&lt;p&gt;Another possibility is to use Azure Functions to extend PlayFab even further, like enabling real-time messaging with &lt;a href="https://azure.microsoft.com/en-us/services/signalr-service/"&gt;Azure SignalR Service&lt;/a&gt; or a global database like &lt;a href="https://azure.microsoft.com/en-us/services/cosmos-db/"&gt;CosmosDb&lt;/a&gt;!&lt;/p&gt;
&lt;h1 id="conclusion"&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;In this article, we learned how to extend PlayFab by reacting to PlayFab PlayStream events with Azure Functions using Rules in PlayFab.&lt;/p&gt;
&lt;p&gt;We created the binding, added a rule and set-up an Azure Function app with a Storage Queue trigger. The Azure Function reads a message from the Storage Queue, which contains the PlayFab context. The context is used to reference the registered Player when we update it's Player data on Playfab.&lt;/p&gt;
&lt;p&gt;Last, we learned about the greater potential of extending Azure PlayFab with Azure Functions.&lt;/p&gt;
&lt;p&gt;I hope you enjoyed this end-to-end example. You can follow me on &lt;a href="https://twitter.com/structed"&gt;Twitter @structed&lt;/a&gt; and ask me about this post or all things Azure or have a look at my &lt;a href="https://github.com/structed"&gt;GitHub&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;Please also find the full source for this example at my GitHub repository: &lt;a href="https://github.com/Structed/sample-playfab-eventstream-handler"&gt;https://github.com/Structed/sample-playfab-eventstream-handler&lt;/a&gt;&lt;/p&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
		<item>
			<title>Deploy Statiq to Azure Static Web App</title>
			<link>https://blog.structed.me/posts/deploy-statiq-to-azure-static-web-app</link>
			<description>How to deploy a Statiq-based static site as an Azure Static Web App</description>
			<enclosure url="https://blog.structed.me/images/posts/deploy-statiq-to-azure-static-web-app/AzureStaticWebAppsLogo.png" length="0" type="image" />
			<guid>https://blog.structed.me/posts/deploy-statiq-to-azure-static-web-app</guid>
			<pubDate>Tue, 18 May 2021 00:00:00 GMT</pubDate>
			<content:encoded>&lt;h1 id="deploy-statiq-to-azure-static-web-app"&gt;Deploy Statiq to Azure Static Web App&lt;/h1&gt;
&lt;p&gt;Azure Static Web Apps just went GA a few days ago.
I took the opportunity to deploy my blog as a "SWA" (Static Web App).
In this post, I am writing about my experience with the service and how you can deploy a &lt;a href="https://statiq.dev/"&gt;Statiq&lt;/a&gt;-based site.&lt;/p&gt;
&lt;h1 id="what-is-azure-static-web-app"&gt;What is "Azure Static Web App"?&lt;/h1&gt;
&lt;p&gt;It is a new Azure service which lets you deploy a static app (only static resources like images, HTML, JavaScript, CSS etc.).
These resources will then automatically be served from "points of Presence" (PoP) around the world to make sure every user gets the site delivered in the fastest possible way. SWA uses Azure's CDN for this.&lt;/p&gt;
&lt;h2 id="serverless-batteries-included"&gt;Serverless Batteries Included&lt;/h2&gt;
&lt;p&gt;However, with your Static Web App, you &lt;em&gt;can&lt;/em&gt; also deploy an API in the form of Azure Functions. You may either use functions deployed with your app, or reference an existing Function App.&lt;/p&gt;
&lt;h2 id="security"&gt;Security&lt;/h2&gt;
&lt;p&gt;To make your site secure, SWA offers authentication with Azure AD, GitHub and Twitter, so you can cordon-off parts (or all) of your site to  authenticated users in roles you define.&lt;/p&gt;
&lt;p&gt;For this to work, SWA also supports &lt;strong&gt;free TLS certificates&lt;/strong&gt; for your custom domains - even for your Apex Domains!&lt;/p&gt;
&lt;h2 id="great-developer-experience"&gt;Great Developer Experience&lt;/h2&gt;
&lt;p&gt;With GitHub and Azure DevOps integration, you can easily build a CI/CD process with staging environments for your pull requests.&lt;/p&gt;
&lt;p&gt;This is complemented with a &lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurestaticwebapps&amp;amp;WT.mc_id=javascript-11478-cxa"&gt;Visual Studio Code extension&lt;/a&gt; and a &lt;a href="https://github.com/Azure/static-web-apps-cli"&gt;CLI tool&lt;/a&gt; for local tests.&lt;/p&gt;
&lt;h2 id="pricing"&gt;Pricing&lt;/h2&gt;
&lt;p&gt;The free edition should cover most of your needs. Only if you have one of the following requirements, a monthly fee and bandwidth charges (&amp;gt;100GB) are due:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;gt;2 Domains&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Bring your own Azure Function (separate plan, not the one included with SWA)&lt;/li&gt;
&lt;li&gt;Higher storage requirements&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Check out the &lt;a href="https://azure.microsoft.com/en-us/pricing/details/app-service/static/"&gt;pricing page&lt;/a&gt; for more details.&lt;/p&gt;
&lt;h1 id="what-is-statiq"&gt;What is "Statiq"?&lt;/h1&gt;
&lt;p&gt;&lt;a href="https://statiq.dev/"&gt;Statiq&lt;/a&gt; is a .NET Static Generator Framework written in C#.&lt;/p&gt;
&lt;p&gt;Essentially, you configure pieces of code which are added to Pipelines. These pipelines process input (like Markdown, images, JSON) to an output.&lt;/p&gt;
&lt;p&gt;Statiq offers &lt;code&gt;Statiq.Framework&lt;/code&gt;, as the base and &lt;code&gt;Statiq.Web&lt;/code&gt; as the "happy path" on top to generate static sites.&lt;/p&gt;
&lt;p&gt;I am using &lt;code&gt;Static.Web&lt;/code&gt; for my Blog. Since I usually do not have to change any code when publishing articles, I do not need to recompile all the time, but just run &lt;code&gt;dotnet run&lt;/code&gt; to re-generate my content.&lt;/p&gt;
&lt;p&gt;Check out my &lt;a href="https://blog.structed.me/setting-up-a-blog-with-statiq"&gt;previous blog post on how I use Statiq&lt;/a&gt;.&lt;/p&gt;
&lt;h1 id="setting-up-a-github-actions-workflow"&gt;Setting up a GitHub Actions Workflow&lt;/h1&gt;
&lt;p&gt;If you use SWA with GitHub, SWA creates a GitHub Actions workflow in your repo. This sets up two things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Build and deploy for pushes into the &lt;code&gt;main&lt;/code&gt; branch&lt;/li&gt;
&lt;li&gt;Builds for opening, syncing, re-opening and closing of Pull Requests to the &lt;code&gt;main&lt;/code&gt; branch&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here is the YAML that was generated for me (&lt;code&gt;StructedSiteAssembler&lt;/code&gt; is my subdirectory for the blog application)&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;name: Azure Static Web Apps CI/CD

on:
  push:
    branches:
      - main
  pull_request:
    types: [opened, synchronize, reopened, closed]
    branches:
      - main

jobs:
  build_and_deploy_job:
    if: github.event_name == 'push' || (github.event_name == 'pull_request' &amp;amp;&amp;amp; github.event.action != 'closed')
    runs-on: ubuntu-latest
    name: Build and Deploy Job
    steps:
      - uses: actions/checkout@v2
        with:
          submodules: true
      - name: Build And Deploy
        id: builddeploy
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_WHITE_PEBBLE_014EC7A10 }}
          repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
          action: "upload"
          ###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
          # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
          app_location: "/StructedSiteAssembler" # App source code path
          api_location: "api" # Api source code path - optional
          output_location: "output" # Built app content directory - optional
          ###### End of Repository/Build Configurations ######

  close_pull_request_job:
    if: github.event_name == 'pull_request' &amp;amp;&amp;amp; github.event.action == 'closed'
    runs-on: ubuntu-latest
    name: Close Pull Request Job
    steps:
      - name: Close Pull Request
        id: closepullrequest
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_WHITE_PEBBLE_014EC7A10 }}
          action: "close"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This specific config is needed, because it contains all the magic to deploy to SWA, which is not exposed to the public. This bothers me a bit, because it actually locks down the platform quite a bit and makes it harder to debug. But I never mind that, as GitHub Actions provides the flexibility needed, as you will see later.&lt;/p&gt;
&lt;p&gt;The docker image used here has another thing included: &lt;em&gt;&lt;a href="https://github.com/microsoft/Oryx"&gt;Oryx&lt;/a&gt;&lt;/em&gt;. It detects which development stack you are using and generates &amp;amp; runs a build script for you. The output of which is then deployed to SWA.&lt;/p&gt;
&lt;p&gt;The actual "SWA magic" is in here:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;      - name: Build And Deploy
        id: builddeploy
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_WHITE_PEBBLE_014EC7A10 }}
          repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
          action: "upload"
          ###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
          # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
          app_location: "/StructedSiteAssembler" # App source code path
          api_location: "api" # Api source code path - optional
          output_location: "output" # Built app content directory - optional
          ###### End of Repository/Build Configurations ######
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;.NET is detected and built correctly, but Statiq requires to run the app afterwards to actually create the output. This is where the "out of the box experience" did not work for me.&lt;/p&gt;
&lt;h1 id="the-solution"&gt;The Solution&lt;/h1&gt;
&lt;p&gt;While Oryx correctly determines a lot of popular stacks, Statiq is not very widely adopted (yet!), so we have to do a little workaround. Thankfully, the build within the GitHub Action  &lt;code&gt;Azure/static-web-apps-deploy@v1&lt;/code&gt; can be skipped. Thus, it is only used to upload the artifact and deploy to the Azure Static Web App.&lt;/p&gt;
&lt;p&gt;To do this, we just need to add a few lines which&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Set up .NET&lt;/li&gt;
&lt;li&gt;Restore packages&lt;/li&gt;
&lt;li&gt;Build the app&lt;/li&gt;
&lt;li&gt;Run the app to generate output&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;between &lt;code&gt;- uses: actions/checkout@v2&lt;/code&gt; and &lt;code&gt;- name: Build And Deploy&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;            - uses: actions/setup-dotnet@v1
              with:
                  dotnet-version: 5.0.203
            - run: dotnet restore
            - run: dotnet build ./StructedSiteAssembler/StructedSiteAssembler.csproj --configuration Release --no-restore
            - run: dotnet run --project ./StructedSiteAssembler/StructedSiteAssembler.csproj --output output
            - name: Build And Deploy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, as we still need the &lt;code&gt;Build And Deploy&lt;/code&gt; step to deploy to SWA - but not the actual "Build" step with Oryx - we have the option to disable the build. Looking at &lt;a href="https://docs.microsoft.com/en-us/azure/static-web-apps/github-actions-workflow#skip-app-build"&gt;the docs&lt;/a&gt;, we can learn that you can use &lt;code&gt;skip_app_build: true&lt;/code&gt; to skip the build. But it also requires us to set the &lt;code&gt;app_location&lt;/code&gt; to the actual output directory. The value of &lt;code&gt;output_location&lt;/code&gt; is ignored, if you set &lt;code&gt;skip_app_build: true&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Given my folder structure:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.
├───.github
│   └───workflows
└───StructedSiteAssembler
    ├───bin
    ├───input
    │   ├───images
    │   ├───posts
    │   └───scss
    ├───output
    │   ├───images
    │   ├───img
    │   ├───js
    │   ├───posts
    │   ├───scss
    │   ├───tags
    │   └───vendor
    ├───theme
    │   └───input
    │       ├───img
    │       ├───js
    │       ├───scss
    │       └───vendor
    └───wwwroot
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;my config would looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;                  app_location: "/StructedSiteAssembler/output" # App source code path
                  api_location: "" # Api source code path - optional
                  output_location: "" # Built app content directory - optional
                  skip_app_build: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An here is the full Action setup for my blog:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;name: Azure Static Web Apps CI/CD

on:
    push:
        branches:
            - main
    pull_request:
        types: [opened, synchronize, reopened, closed]
        branches:
            - main

jobs:
    build_and_deploy_job:
        if: github.event_name == 'push' || (github.event_name == 'pull_request' &amp;amp;&amp;amp; github.event.action != 'closed')
        runs-on: ubuntu-latest
        name: Build and Deploy Job
        steps:
            - uses: actions/checkout@v2
              with:
                  submodules: true
            - uses: actions/setup-dotnet@v1
              with:
                  dotnet-version: 5.0.203
            - run: dotnet restore
            - run: dotnet build ./StructedSiteAssembler/StructedSiteAssembler.csproj --configuration Release --no-restore
            - run: dotnet run --project ./StructedSiteAssembler/StructedSiteAssembler.csproj --output output
            - name: Build And Deploy
              id: builddeploy
              uses: Azure/static-web-apps-deploy@v1
              with:
                  azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_WHITE_PEBBLE_014EC7A10 }}
                  repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
                  action: "upload"
                  ###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
                  # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
                  app_location: "/StructedSiteAssembler/output" # App source code path
                  api_location: "" # Api source code path - optional
                  output_location: "" # Built app content directory - optional
                  skip_app_build: true
                  ###### End of Repository/Build Configurations ######

    close_pull_request_job:
        if: github.event_name == 'pull_request' &amp;amp;&amp;amp; github.event.action == 'closed'
        runs-on: ubuntu-latest
        name: Close Pull Request Job
        steps:
            - name: Close Pull Request
              id: closepullrequest
              uses: Azure/static-web-apps-deploy@v1
              with:
                  azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_WHITE_PEBBLE_014EC7A10 }}
                  action: "close"

&lt;/code&gt;&lt;/pre&gt;
&lt;h1 id="conclusion"&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;Azure Static Web Apps is a great service to host a site, or even a browser-based game! With it's very fast response times all over the World thanks to Azure's CDN, the tight integration with Azure Functions and built-in authentications makes it a great, flexible and very low cost choice for many use-cases.&lt;/p&gt;
&lt;p&gt;I am certain we will be seeing tighter integration with more frontend frameworks and improved monitoring in the coming months.&lt;/p&gt;
&lt;h1 id="further-reading"&gt;Further Reading&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/staticwebdev/awesome-azure-static-web-apps"&gt;awesome-azure-static-web-apps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Azure/static-web-apps-cli"&gt;Static Web Apps CLI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Azure/static-web-apps-deploy"&gt;GitHub Action for Static Web Apps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurestaticwebapps"&gt;Visual Studio Code Extension&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
		<item>
			<title>Setting up my Website &amp; Blog using Statiq</title>
			<link>https://blog.structed.me/posts/setting-up-a-blog-with-statiq</link>
			<description>Step-by-step guide on how to set up your website and blog using Statiq by Dave Glick</description>
			<enclosure url="https://blog.structed.me/images/patrick-tomasso-GfDyRbLofHg-unsplash.jpg" length="0" type="image" />
			<guid>https://blog.structed.me/posts/setting-up-a-blog-with-statiq</guid>
			<pubDate>Sun, 18 Apr 2021 00:00:00 GMT</pubDate>
			<content:encoded>&lt;p&gt;I have wished for my own site &amp;amp; blog for a &lt;em&gt;very&lt;/em&gt; long time. Yet, I am clearly not a full-stack or even remotely a frontend developer.
I am classic backend guy - the sort of which one would now call DevOps engineer or SRE.&lt;/p&gt;
&lt;p&gt;Basically, I have not the slightest clue about how to build web-frontends with JavaScript, all these frameworks and CSS, LESS, SASS - you name it. I don't know much about these technologies and I think the're a mess. Whenever I do frontend development, I have a strong feeling, it is non-deterministic.&lt;/p&gt;
&lt;p&gt;So I &lt;em&gt;really&lt;/em&gt; struggled with building my site and never did it for many years.&lt;/p&gt;
&lt;p&gt;Then there comes &lt;a href="https://twitter.com/daveaglick"&gt;Dave Glick&lt;/a&gt; and his latest creation: &lt;a href="https://statiq.dev/"&gt;Statiq&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Statiq is is a &amp;quot;Static Site Generator&amp;quot; - effectively a .NET Core CLI application that uses so called &lt;em&gt;Pipelines&lt;/em&gt; to ingest arbitrary data from any source, processes it via one or more &lt;em&gt;Modules&lt;/em&gt; to &lt;em&gt;Documents&lt;/em&gt; (which are an object containing content and metadata) and ultimately generating static content. This generated output could really be anything: a static website, a PDF, JSON data etc.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Find out more about &lt;a href="https://statiq.dev/framework/#how-it-works"&gt;how Statiq works&lt;/a&gt; in the Statiq.Framework docs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id="layout"&gt;Layout&lt;/h1&gt;
&lt;p&gt;Statiq-based sites, as mentioned above, consist of code for pipelines, metadata and content files.&lt;/p&gt;
&lt;p&gt;A typical project folder structure looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.
└───YourProjectName
    ├───input
    │   ├───images
    │   ├───posts
    │   └───scss
    ├───output
    │   ├───images
    │   ├───img
    │   ├───js
    │   ├───posts
    │   ├───scss
    │   ├───tags
    │   └───vendor
    ├───theme
    │   └───input
    │       ├───img
    │       ├───js
    │       ├───scss
    │       └───vendor
    └───wwwroot
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;input&lt;/code&gt; defines all the content &amp;amp; metadata that is used as input for the Statiq pipelines, which eventually get transformed to &lt;code&gt;ouptput&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;theme&lt;/code&gt; contains all the pipeline code. I am using &lt;a href="https://github.com/statiqdev/CleanBlog"&gt;CleanBlog&lt;/a&gt; (by the Statiq development team) as a sub-module to my repository. However, I have forked it so I can make my own custom changes and contribute to the upstream repository.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;output&lt;/code&gt; is where all the generated content goes. The contents of this folder is what you would deploy to your web-server.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="creating-content-publishing"&gt;Creating content &amp;amp; publishing&lt;/h1&gt;
&lt;p&gt;You can create content by adding files to the &lt;code&gt;input&lt;/code&gt; directory. Check out the Statiq Docs for more info.&lt;/p&gt;
&lt;p&gt;To transform the &lt;code&gt;input&lt;/code&gt; to &lt;code&gt;output&lt;/code&gt;, just run &lt;code&gt;dotnet run&lt;/code&gt;. This will run all the pipelines and generate the static content.&lt;/p&gt;
&lt;p&gt;Since you might not be sure what the effects of your content additions are before generating and loading it in a browser, you can run &lt;code&gt;dotnet run -- preview&lt;/code&gt; to start a preview server. It watches the input files for changes and automatically regenerates, refreshing your browser.&lt;/p&gt;
&lt;p&gt;&lt;span class="alert alert-light"&gt;Header Photo by &lt;a href="https://unsplash.com/&amp;#64;impatrickt?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText"&gt;Patrick Tomasso&lt;/a&gt; on &lt;a href="https://unsplash.com/s/photos/static?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText"&gt;Unsplash&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
	</channel>
</rss>