sebadorn.de https://sebadorn.de/themes/dark_paper/img/favicon.png sebadorn.de https://sebadorn.de/ Wed, 13 Nov 2024 03:41:07 +0100 https://sebadorn.de/feed/ Mastodon – Das wiederentdeckte soziale Netzwerk Thu, 20 Jul 2023 22:21:00 +0200 https://sebadorn.de/2023/07/20/mastodon-das-wiederentdeckte-soziale-netzwerk https://sebadorn.de/2023/07/20/mastodon-das-wiederentdeckte-soziale-netzwerk Seba Informatics <p class="illu"><img src="https://sebadorn.de/media/2023/07/mastodon.png" alt="Das Mastodon-Logo mit der Startseite von mastodon.gamedev.place im Hintergrund" height="432" /></p> <p>Seit der Übernahme von Twitter durch Elon Musk sehen viele Benutzer die dortigen Entwicklungen mit Sorge: Große Kündigungswellen; Angestellte, die im Büro über&shy;nachten; mangelnde Moderation und Abschalten der Pressestelle; Hin-und-Her um das blaue Verifizierungshäkchen, welches durch Twitter Blue nichts mehr mit Verifizierung zu tun hat und so Identitäts-Imitation Tür und Tor geöffnet hat; das Killen von Dritt&shy;anbieter-Apps … Ich habe bereits ein paar Punkte ausgelassen, aber solange es die Seite noch gibt, werden sicherlich weitere dazukommen.</p> <p>Wenig erstaunlich also, wenn Nutzer stetig und besonders beim nächsten Vorkommnis abspringen oder zumindest parallel andere Social Media-Dienste ausprobieren.</p> <p>Twitter-Gründer Jack Dorsey hat einen neuen Dienst namens <em>Bluesky</em> gegründet, der sich aktuell in der Beta-Phase mit Warteliste befindet, Meta hat kürzlich <em>Threads</em> gestartet, welches an Instagram gekoppelt ist – Anmeldungen sind innerhalb der EU jedoch bisher nicht möglich.</p> <p>Und dann gibt es da wie üblich auch wieder die Open Source-Community, mit einer eigenen, nicht kommerziellen Antwort: <a href="https://joinmastodon.org/de">Mastodon</a>. Wobei dieses nicht neu ist, sondern jetzt aufgrund der Entwicklungen etwas vom Platz im Scheinwerferlicht erhält.</p> <!--more--> <h3>Der Einstieg – Was sind Instanzen?</h3> <p>Der Einstieg ist nicht ganz so offensichtlich, wie bei anderen Social Media-Seiten, die als kommerzielles Produkt eines Unternehmens betrieben werden. Mastodon ist von Natur aus ein <strong>verteiltes System</strong>: Jeder kann einen Server mit einer Instanz betreiben, die dann untereinander kommunizieren können. Als normaler Benutzer muss man sich daher entscheiden, auf welcher Instanz man sich anmeldet.</p> <p>Ein guter Vergleich, den ich gelesen habe, ist der mit E-Mail: Man sucht sich einen E-Mail-Anbieter aus, teilt anderen seine Adresse mit und schickt sich dann Nachrichten.</p> <p>Die größte Instanz dürfte <a href="https://mastodon.social/">mastodon.social</a> sein, welche von der Mastodon gGmbH selbst betrieben wird. Eine Übersicht über weitere findet sich unter <a href="https://joinmastodon.org/de/servers">joinmastodon.org/de/servers</a>. Dort lässt sich auch nach Themen filtern und so z.&thinsp;B. deutsche Regionen oder ein Server zu einem bestimmten Interesse finden. Ich selbst habe mich optimistisch auf <a href="https://mastodon.gamedev.place">mastodon.gamedev.place</a> angemeldet.</p> <p>Wichtige Informationen dazu:</p> <ul> <li><strong>Wenn du ein Konto auf Instanz A hast, kannst du trotzdem Leuten auf anderen Instanzen folgen</strong>, ihnen schreiben usw.</li> <li>Betreiber einer Instanz können den Kontakt zu anderen Instanzen jedoch <strong>blockieren</strong>, was auch viel genutzt wird. Eine Liste der von z.&thinsp;B. mastodon.social ausgeschlossenen Server findet sich auf <a href="https://mastodon.social/about">mastodon.social/about</a> unter „Moderated servers“</li> <li>Im <strong>lokalen Feed</strong> sieht man entsprechend nur Toots/Tröts des eigenen Servers. Hierfür ist die Wahl der Instanz relevant</li> <li><strong>Man kann sein Konto auch nachträglich zu einer anderen Instanz umziehen</strong></li> </ul> <h3>Unterschiede zu Twitter</h3> <p>Ein offensichtlicher Unterschied sind die Bezeichnungen: Ein <em>Tweet</em> ist ein <em>Toot</em> bzw. <em>Tröt</em>, der <em>Retweet</em> ist ein <em>Boost</em> bzw. lautet einfach <em>Teilen</em> und den <em>Quote Tweet</em> … gibt es nicht.</p> <p>Dafür kann man Nachrichten auch als Lesezeichen abspeichern, was entgegen dem Favoritisieren dem Verfasser nicht angezeigt wird. Generell hat man auch weniger Einblick in die Likes anderer Leute. Man kann zwar zu einem Post sehen, wer diesen geliked hat, aber man kann auf einem fremden Konto nicht einsehen, was diese Person alles als „Gefällt mir“ markiert hat.</p> <p>Als letzer Punkt noch ein großer Unterschied: <strong>Es gibt keinen persönlichen Feed-Algorithmus.</strong> Du bekommst keine Vorschläge angezeigt, keinen Feed zusammengestellt und keine gesponsorten Nachrichten dazwischengeschoben. Das nimmt unter Umständen etwas vom Suchtfaktor, aber ist das schlecht? Kein Doom-Scrolling und geringere Chancen, dass emotional geladene oder bewusst kontroverse Messages eingestreut werden, um die Interaktionen und Views – das „Engagement“ – hochzutreiben.</p> <h3>Apps fürs Smartphone und Tablet</h3> <p>Mastodon ist keine App-First-Anwendung, weswegen eine offizielle App erst im April 2022 erschien. Seitdem scheint sich viel daran getan zu haben. Es gibt aber auch einige empfehlenswerte Drittanbieter-Apps, sodass sich mit Sicherheit eine passende fürs Smartphone oder Tablet finden lässt.</p> <ul> <li><a href="https://joinmastodon.org/de/apps">Die offizielle Mastodon-App</a> <em>(für Android und iOS, kostenlos)</em> – Weiter unten auf der Seite befindet sich zudem eine Übersicht vieler weiterer Third-Party-Apps</li> <li><a href="https://tusky.app/">Tusky</a> <em>(nur Android, kostenlos)</em> – Eine App, die schon länger dabei ist, welche ich verwende und die auch über den F-Droid-Store verfügbar ist</li> <li><a href="https://tapbots.com/ivory/">Ivory</a> <em>(nur iOS, bezahlt auf Abonnement-/Subscription-Basis)</em> – Von den Entwicklern der beliebten Tweetbot-App für Twitter, welche eingestellt werden musste</li> </ul> <h3>Threads und Interoperabilität – Digitale Nachbarn?</h3> <p>Spannend wird auch, ob es in Zukunft möglich sein wird, dass Leute auf Threads mit jenen bei Mastodon interagieren können. Denn von Meta ist angekündigt worden, das auch von Mastodon verwendete <a href="https://activitypub.rocks/">ActivityPub</a>-Protokoll zu unterstützen.</p> <p>Ob angesichts der bei Threads rasant steigenden Benutzerzahlen da allerdings großer Fokus drauf gelegt wird oder es dann doch unter den Tisch gefallen lassen wird, bleibt abzuwarten. Auch wäre denkbar, dass einige Mastodon-Admins aus datenschutz&shy;rechtlichen Bedenken vielleicht Threads aussperren werden – das Facebook-Unternehmen Meta hat in der Hinsicht schließlich zu Recht einen sehr schlechten Ruf.</p> Dead Cells: Unlocking the Daily Challenge blueprints in the bugged Linux GOG version Mon, 28 Jun 2021 22:20:00 +0200 https://sebadorn.de/2021/06/28/dead-cells-unlocking-the-daily-challenge-blueprints-in-the-bugged-linux-gog-version https://sebadorn.de/2021/06/28/dead-cells-unlocking-the-daily-challenge-blueprints-in-the-bugged-linux-gog-version Seba Informatics <p class="illu"> <img src="https://sebadorn.de/media/2021/06/dead_cells_daily_failed_740px.jpg" alt="Dead Cells error message." height="416" /> </p> <p>Dead Cells has a <strong>Daily Challenge</strong> or <strong>Daily Run</strong> mode, but it is just a minor optional part. Except that is required in order to unlock three weapons (“blueprints”) for the main game. Just by starting a Daily Run on different days unlocks the following blueprints:</p> <ol> <li><em>Swift Sword</em> (internal name <code>SpeedBlade</code>) the first time</li> <li><em>Lacerating Aura</em> (internal name <code>DamageAura</code>) after the fifth time</li> <li><em>Meat Skewer</em> (internal name <code>DashSword</code>) after ten starts</li> </ol> <p>However, <strong>specifically the Linux version on GOG</strong> has an issue where it is impossible to start the Daily Challenge. The Linux version on Steam is not affected. To use anything online-related in Dead Cells – which thankfully is not a lot as it is a local single-player game – the GOG GALAXY client is required. <a href="https://www.gog.com/wishlist/galaxy/release_the_gog_galaxy_client_for_linux">Which does not exist for Linux.</a> Until a GOG GALAXY client for Linux is released, I probably should not buy games from GOG any&shy;more and just go with Steam, because otherwise I cannot trust that all features will work. Which is a pitty because I really like GOG for being DRM-free.</p> <p>The Dead Cells issue is known for years and by now it seems rather unlikely it will be fixed. At some point a note was added to the store page:</p> <blockquote>Additional Notes: Galaxy client is required to access Daily Runs and Streaming features.</blockquote> <h3>Let's hack the game</h3> <p class="illu"> <img src="https://sebadorn.de/media/2021/06/dead_cells_blueprint_drops_660px.jpg" alt="The blueprints dropped in a level." height="330" /> </p> <p>At the point of writing I was using Ubuntu 20.04.2 and the Dead Cells game version 1.14.0 / v24. You will need a hex editor which can handle big files of around 1&thinsp;GB. I used and can recommend <a href="https://wiki.gnome.org/Apps/Ghex">GHex</a>. You can install it with:</p> <pre class="language-shell">$ sudo apt install ghex</pre> <!--more--> <p>In the rest of the article when I use a relative path, assume it is relative to the game installation directory, which in my case is <code>~/GOG Games/Dead Cells/</code>.</p> <p>First you should create a backup of the file <code>game/res.pak</code> because we will edit it and any mistake will lead to the game crashing. If that happens just replace the edited file with your backup. Or if you do not have one, reinstall the game. While you are at it, you might as well backup your saves – that is the directory <code>game/save/</code>.</p> <p>This is the plan:</p> <ol> <li><strong>Find out the internal names of the desired blueprints.</strong></li> <li><strong>Assign them to a common enemy as an often dropped reward.</strong></li> <li><strong>Play the game to unlock it. Repeat this until all 3 are unlocked.</strong></li> </ol> <p>The tricky part is, that the edited file has to be the <em>exact same size as before</em>. My guess is this has to do with internal offsets for parsing, because <code>res.pak</code> is a container for many different files. The part we are going to edit belongs to a JSON file called <code>data.cdb</code>.</p> <h4>Step 1: Daily Reward blueprint names</h4> <p>When you open <code>game/res.pak</code> in GHex you just have to search for “dailyReward”. You will find a JSON list with the blueprints. Nicely formatted the section looks like this:</p> <pre class="language-json">"name": "dailyReward", "lines": [ { "item": "SpeedBlade", "step": 1 }, { "item": "DamageAura", "step": 5 }, { "item": "DashSword", "step": 10 } ]</pre> <p>One trick that almost worked, was to set <code>"step": 0</code>. I started the game, tried unsuccessfully to start a Daily Run, and then found the SpeedBlade to be available at The Collector. However, it only worked for this one blueprint and not the other two.</p> <h4>Step 2: Assign to enemy</h4> <p>The most common enemy which also drops blueprints is the <em>Zombie</em>. Let us have a look at the relevant part of its JSON description:</p> <pre class="language-json">"blueprints": [ { "rarity": "Rare", "item": "HorizontalTurret", "minDifficulty": 0 }, { "rarity": "Always", "item": "Bleeder", "minDifficulty": 0 }, { "rarity": "Rare", "item": "PrisonerBobby", "minDifficulty": 1 } ], "id": "Zombie", "name": "Zombie"</pre> <p>The <em>Bleeder</em> entry looks like the perfect target to be replaced. “Always” does not actually mean the blueprint is always dropped, but it has a very decent chance. When I played to unlock, I actually got the item while still in the first level each time – just not directly from the first few enemies.</p> <p>But keep in mind: The resulting file has to be the same size as before. Changing too much will just make every&shy;thing harder on us. <code>Bleeder</code> is 7 characters long while <code>SpeedBlade</code> and <code>DamageAura</code> have a length of 10, and <code>DashSword</code> has 9 characters. To counter-balance that, I shortened the name “Zombie” by the difference. An example:</p> <pre class="language-json">"blueprints": [ { "rarity": "Rare", "item": "HorizontalTurret", "minDifficulty": 0 }, { "rarity": "Always", "item": "DamageAura", "minDifficulty": 0 }, { "rarity": "Rare", "item": "PrisonerBobby", "minDifficulty": 1 } ], "id": "Zombie", "name": "Zom"</pre> <p>A screenshot from the edited part in GHex (highlighted areas by me):</p> <p class="illu"> <img src="https://sebadorn.de/media/2021/06/dead_cells_ghex_edit_660px.png" alt="Screenshot of the edited part in GHex." height="190" /> </p> <h4>Step 3: Play the game</h4> <p>I just did some runs on the lowest difficulty. Once unlocked – or to be more precise: once the blueprints are available at The Collector – you can replace the edited <code>res.pak</code> with the original again. The blueprints will stay.</p> <p class="illu"> <img src="https://sebadorn.de/media/2021/06/dead_cells_aura_and_skewer_660px.jpg" alt="The blueprints in the unlock list at The Collector." height="362" /> </p> <h3>CellPacker</h3> <p>While it did not work for editing, <a href="https://github.com/ReBuilders101/CellPacker">CellPacker</a> was a great help because I could use it to extract the <code>data.cdb</code> file. It is a lot more comfortable having an auto-formatted JSON file at hand. What did not work for me was to repackage <code>res.pak</code> with the tool – the operation required a whopping 12&thinsp;GB RAM and the game crashed using the file.</p> <br> <hr> <h3>Resources</h3> <ul> <li><a href="https://www.reddit.com/r/deadcells/comments/kg74y1/daily_challenge_mode_doesnt_work_on_the_linux_gog/">r/deadcells: Daily Challenge mode doesn't work on the Linux GOG version of the game</a></li> <li><a href="https://www.reddit.com/r/deadcells/comments/cqergd/editing_save_files/">r/deadcells: Editing save files</a></li> <li><a href="https://github.com/ReBuilders101/CellPacker">github.com/ReBuilders101/CellPacker</a></li> </ul> <p>Related article: <a href="https://sebadorn.de/2018/08/11/dead-cells-ps4-controller-support-on-linux">Dead Cells: PS4 controller support on Linux</a></p> And then it was gone – Notes about developing my js13k entry of 2020 Sun, 27 Sep 2020 16:00:00 +0200 https://sebadorn.de/2020/09/27/and-then-it-was-gone-notes-about-developing-my-js13k-entry-of-2020 https://sebadorn.de/2020/09/27/and-then-it-was-gone-notes-about-developing-my-js13k-entry-of-2020 Seba Informatics Game Dev <p class="illu"> <img src="https://sebadorn.de/media/2020/09/js13k20-start.png" alt="Start screen of my game." height="600" /> </p> <p>This was the 3rd time I participated in the <a href="https://js13kgames.com/">js13kGames game jam</a>, so I was already aware of one thing: The size limit isn't my greatest problem, at least not to the extent of having to do code golfing right out of the gate. Instead I should make sure to have a good code base – multiple files for classes and namespaces as needed, named constants/variables instead of hardcoded values.</p> <p>One really big advantage was, that I had already implemented things like an <a href="https://github.com/sebadorn/js13k-2020-404/blob/js13k-submission/dev/js/Input.js">input system for keyboard and gamepad</a>, and <a href="https://github.com/sebadorn/js13k-2020-404/blob/js13k-submission/dev/js/Level.js#L61">2D collision detection</a> in other projects of mine. I could just reuse some code with minor adjustments and save quite some time.</p> <h3>1. Improvement through little things</h3> <p class="illu"> <video controls loop="true" width="660" height="320"> <source src="https://sebadorn.de/media/2020/09/js13k20-animations.webm" type="video/webm"> </video> </p> <p>First some ideas on how to improve the general impression of the game, even though the player may not be actively aware of them.</p> <!--more--> <p><strong>Details to make the scene more alive.</strong> Especially animations can play a big role, and having a somewhat self-consistent art style – e.&thinsp;g. having a super detailed character would clash with uni-colored blocks as platforms. Some small details here are:</p> <ul> <li>Standing on a platform and pressing down makes the character look down.</li> <li>The character torso slowly bops up and down for breathing.</li> <li>The character blinks.</li> <li>Little dust clouds rise up when jumping.</li> <li>Breaking platforms have little dust clouds.</li> <li>The background moves slower than the foreground, giving a little sense of distance.</li> </ul> <p><strong>Coyote time.</strong> Directly after walking of a platform, the player still has a few frames time to jump even though the character isn't on ground anymore.</p> <p><strong>The game is paused when a gamepad is disconnected.</strong> If a gamepad was connected, it may have been used for playing the game. Let's assume it getting disconnected mid-game is an accident.</p> <p class="illu"> <img src="https://sebadorn.de/media/2020/09/js13k20-pause.png" alt="Pause screen." height="320" /> </p> <p><strong>Pausing means not doing anything (or not much).</strong> If a game is a bit more taxing and makes the CPU or GPU sweat, it's good to have the possibility to give it a break and let the temperature drop down again. If you have a pause option – which on its own is already good – it should truly pause and not keep rendering a complex scene.</p> <p>I went a bit further. When paused, the game renders the pause screen once and then stops the main loop – no updates, no rendering. The game only keeps listening for key events and does a slower gamepad polling to know when to continue.</p> <h3>2. Watch your (time) step</h3> <p>It did not happen a lot, but some entries did not account for <strong>different monitor refresh rates</strong>. So when the game was developed for 60 FPS, and it maxed out at 144 FPS on my system, the game ran too fast. In the best case animations just looked weird, but mostly it meant having a hard time with the controls.</p> <p>The solution is to develop for a certain frame rate and introduce a <em>time factor</em> to account for differences. This factor is then used when updating the physics and animation progresses. In the following example that factor is the variable <code>dt</code>. If the target FPS is 60 and the game runs at 120, <code>dt</code> would be 0.5. Because the update and draw functions are called twice as often as targeted, the progress has to be slowed down by half.</p> <pre class="language-javascript">js13k.Renderer = { /** * Start the main loop. Update logic, render to the canvas. * @param {number} [timestamp = 0] */ mainLoop( timestamp = 0 ) { js13k.Input.update(); if( timestamp && this.last ) { // Time that passed between frames. [ms] const timeElapsed = timestamp - this.last; // Target speed of 60 FPS (=> 1000 / 60 ~= 16.667 [ms]). const dt = timeElapsed / ( 1000 / js13k.TARGET_FPS ); // update and draw the level … this.update( dt ); this.draw(); } this.last = timestamp; requestAnimationFrame( t => this.mainLoop( t ) ); } };</pre> <p>I am not saying my solution is the best way to do it. It is short and works well enough, though. An excellent article on the topic is: <a href="https://gafferongames.com/post/fix_your_timestep/">Fix Your Timestep! by Glenn Fiedler</a></p> <h3>3. Improving performance</h3> <p>When I started drawing the spikes with <code>lineTo()</code> calls, the performance went noticably down. But since most level objects did not change, they could be cached by <strong>pre-rendering them to an invisible canvas</strong>. This unchanging canvas is then used in the main loop with <code>drawImage()</code>.</p> <pre class="language-javascript">/** * Render an object to an offscreen canvas. * @param {js13k.LevelObject} o * @param {number} o.w - Width. * @param {number} o.h - Height. * @return {HTMLCanvasElement} */ function toOffscreenCanvas( o ) { // This canvas is never added to the document body. const canvas = document.createElement( 'canvas' ); canvas.width = o.w; canvas.height = o.h; const ctx = canvas.getContext( '2d' ); ctx.imageSmoothingEnabled = false; o.draw( ctx ); return canvas; }</pre> <p>The best part: This pre-rendered image can still be used for the breaking animation. The shaking before breaking apart is just a randomized offset in the drawing position, and breaking it in half means two <code>drawImage()</code> calls – one for the left half and one for the right half, cutting the image in half.</p> <p><strong>Another optimization: Only render what is currently in the viewport.</strong> The levels in my game are long, there is never everything at once on the screen. So before drawing a platform or an effect, there is a check if at least some part of the bounding box is inside the current viewport.</p> <pre class="language-javascript">/** * Draw an object only if it is inside the visible viewport area. * @param {CanvasRenderingContext2d} ctx * @param {number} vpX - Viewport X offset. * @param {number} vpW - Viewport width. * @param {number} vpY - Viewport Y offset. * @param {number} vpH - Viewport height. * @param {js13k.LevelObject} o * @param {number} o.x - X position in level. * @param {number} o.y - Y position in level. * @param {number} o.w - Width. * @param {number} o.h - Height. */ function drawIfVisible( ctx, vpX, vpW, vpY, vpH, o ) { if( // outside the viewport to the right o.x > vpX + vpW || // outside the viewport to the left o.x + o.w < vpX ) { return; } if( // outside the viewport to the bottom o.y > vpY + vpH || // outside the viewport to the top o.y + o.y < vpY ) { return; } o.draw( ctx ); }</pre> <h3>4. What could have been better</h3> <p>One of my main goals were <strong>good controls</strong>. Moving and jumping should <em>feel amazing</em>. I did not reach that goal and had to settle with <em>good enough</em>. I even reinstalled <a href="http://www.celestegame.com/">Celeste</a> to compare how the character controls felt and what was possible: Could you still change direction mid-air when falling after a jump? (Yes.)</p> <p>Falling of a block while steering in a direction still looks strange as the character more glides away than falls down.</p> <p class="illu"> <img src="https://sebadorn.de/media/2020/09/js13k20-wallhanging.png" alt="Hanging on a wall." height="412" /> </p> <p>One big-ish decision was to allow multiple <strong>wall jumps</strong> <em>up the same wall</em>. In a way it is a bad design decision: On one hand there is a time limit until the character loses grip and slides down; on the other hand they can just jump up and cling to the same wall higher up again. But it just felt better and didn't make the game that much easier. All levels are still solvable without using this “trick”.</p> <p>An optimization area I neglected was <strong>memory and garbage collection</strong>. In Firefox and on a really old laptop I noticed some micro stutters and lost frames, making the scene suddenly skip. In a game about precise platforming this is a little disaster. From what I read here and there, these stutters could be caused by garbage collection. Some untested thoughts on what to improve:</p> <ul> <li>Not deleting destroyed platforms, just keep them flagged as such so they are not updated anymore and skipped in rendering.</li> <li>On level begin create a pool of effects – like the dust clouds from jumping – and reuse them instead of creating them when the jump happens.</li> </ul> <p>The second level goes vertically upwards. When the player is already at a higher checkpoint and falls down, it is better for them to <em>not</em> land on a previous checkpoint and instead directly fall to their death. Otherwise they are back at this previous checkpoint. That is just frustrating. <strong>Older checkpoints should not overwrite later ones.</strong></p> <img src="https://sebadorn.de/media/2020/09/js13k20-character.gif" alt="Player character." width="18" height="24" /> <hr> <p>The voting for this year's competition is still under way. Articles by other participants can be found on <a href="https://js13kgames.github.io/resources/">js13kgames.github.io/resources/</a>.</p> <hr> <h3>Resources</h3> <ul> <li><a href="https://2020.js13kgames.com/entries/and-then-it-was-gone">My js13kGames entry “And then it was gone”</a></li> <li><a href="https://github.com/sebadorn/js13k-2020-404">GitHub repository for “And then it was gone”</a></li> <li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas">MDN: Optimizing canvas</a></li> </ul> Changing the message language for Bash Thu, 18 Jun 2020 22:28:00 +0200 https://sebadorn.de/2020/06/18/changing-the-message-language-for-bash https://sebadorn.de/2020/06/18/changing-the-message-language-for-bash Seba Informatics <p>The system language of my Ubuntu installation is set to German. Per default this also means that my command line output is in German if the application supports it. This can be a bit of a hindrance when developing, because error messages and warnings will also be in German which makes it harder to search for solutions – most discussions in help forums and blogs are in English.</p> <p>So let's change the terminal language. In your <code>~/.bashrc</code> file add the following lines:</p> <pre class="language-bash">unset LC_ALL export LC_MESSAGES=C</pre> <p>If <code>LC_ALL</code> had a value, it would overwrite the setting for <code>LC_MESSAGES</code>, so it has to be unset first. I first tried setting <code>LC_ALL=C</code>, but this had the undesired side effect of certain keys behaving differently. I have a German keyboard with QWERTZ layout, but keys like “ä”, “ö”, “ü” suddenly did different things. I can only assume I would have run into some other issues as well. So keep it simple and just change the messages.</p> <p>The next terminal you open will have the setting applied. Also note that this only affects your terminal and no other applications – except those launched from said terminal.</p> <p>To check your language settings you can use <code>locale</code>. My output using Bash looks like this:</p> <pre class="language-bash">$ locale LANG=de_DE.UTF-8 LANGUAGE=de_DE:en LC_CTYPE="de_DE.UTF-8" LC_NUMERIC=de_DE.UTF-8 LC_TIME=de_DE.UTF-8 LC_COLLATE="de_DE.UTF-8" LC_MONETARY=de_DE.UTF-8 LC_MESSAGES=C LC_PAPER=de_DE.UTF-8 LC_NAME=de_DE.UTF-8 LC_ADDRESS=de_DE.UTF-8 LC_TELEPHONE=de_DE.UTF-8 LC_MEASUREMENT=de_DE.UTF-8 LC_IDENTIFICATION=de_DE.UTF-8 LC_ALL=</pre> <p><em>Side note:</em> I also tried <code>LC_MESSAGES=en_US.UTF-8</code>, but that didn't work – no idea why. I also didn't look further into it since I have a working solution.</p> <h3>Sources</h3> <ul> <li><a href="https://askubuntu.com/a/818241/898336">askubuntu.com: Switch command output language from native language to english</a></li> <li><a href="https://help.ubuntu.com/community/Locale">help.ubuntu.com: Locale</a></li> </ul> js13kGames: Tricks applied in Risky Nav Sat, 15 Sep 2018 19:19:00 +0200 https://sebadorn.de/2018/09/15/js13kgames-tricks-applied-in-risky-nav https://sebadorn.de/2018/09/15/js13kgames-tricks-applied-in-risky-nav Seba Informatics Game Dev <p class="illu"><img src="https://sebadorn.de/media/2018/09/risky_nav.png" alt="Risky Nav" /></p> <p>From the 13th August to the 13th September I participated in the <a href="https://js13kgames.com/">js13kGames</a> competition. My entry <a href="https://2018.js13kgames.com/entries/risky-nav">Risky Nav can be seen here</a> and the <a href="https://github.com/sebadorn/js13k-2018-offline">source code is on GitHub here</a>. In this blog post I will explain some of the tricks and techniques I used in making my game.</p> <p>The game is tile based, so everything – player, monsters, goal – is always positioned at a (x, y) position on a 2D map.</p> <h3>About the background</h3> <p>The background is a single image which is created once right at the beginning. It is drawn on a canvas and each tile is 1px big. In the rendering loop it is then up-scaled to the desired tile size. To avoid a blurry image, it is necessary to disable anti-aliasing.</p> <pre class="language-javascript">context.imageSmoothingEnabled = false; let w = bgCanvas.width * tileWidth; let h = bgCanvas.height * tileHeight; function renderLoop() { context.drawImage( bgCanvas, 0, 0, w, h ); }</pre> <h3>About the fog/shadow</h3> <p>The fog/shadow around the player is done in a similar way as the background. The image is pre-rendered with each tile being 1px and then up-scaled in the main loop. But it moves with the player. The darkness is determined by the euclidean distance from the player.</p> <pre class="language-javascript">for( let y = 0; y < fogMapHeight; y++ ) { for( let x = 0; x < fogMapWidth; x++ ) { // Euclidean distance from origin. let de = Math.sqrt( x * x + y * y ); // Darkness only starts 2 tiles away from the player. // f has to be a value between 0 and 1. let f = ( de < 2 ) ? 0 : Math.min( 1.15 - Math.min( 3 / de, 1 ), 1 ); fogCtx.fillStyle = `rgba(0,0,0,${f})`; fogCtx.fillRect( x, y, 1, 1 ); } }</pre> <!--more--> <h3>Path finding</h3> <p>The path finding for the navigation in the game can be done in a rather cheap fashion, thanks to a lot of restrictions. First, we only have a 2D map in which no diagonal movement is possible. Second, the goal and all obstacles have a static position so the map never changes. The only changing variable is the player position.</p> <p>This allows to pre-calculate all the possible paths before the game starts. In the game we then only have to find the shortest path out of those already known. The algorithm to create a path map is:</p> <ol> <li>Initialize all fields with the value <code>0</code>.</li> <li>Give the field of the goal the value <code>1</code>. This is our step counter.</li> <li>Mark every accessible field around the goal (up, down, left, right) as one step higher – with the value <code>2</code>.</li> <li>Repeat this for every field. So fields around the second one have the step value <code>3</code> and so on.</li> </ol> <p>Here is an example. For better readability the unaccessible fields ("stones" in the game) are marked with a <code>-</code> instead.</p> <pre class="language-plaintext"> [ ][ ][ ][ ][ ][-] [ ][-][ ][ ][ ][ ] [ ][-][ ][ ][1][ ] [ ][ ][ ][ ][-][ ] [ ][ ][ ][ ][ ][ ] After the first marking: [ ][ ][ ][ ][ ][-] [ ][-][ ][ ][2][ ] [ ][-][ ][2][1][2] [ ][ ][ ][ ][-][ ] [ ][ ][ ][ ][ ][ ] And after that: [ ][ ][ ][ ][3][-] [ ][-][ ][3][2][3] [ ][-][3][2][1][2] [ ][ ][ ][3][-][3] [ ][ ][ ][ ][ ][ ] Final form: [7][6][5][4][3][-] [8][-][4][3][2][3] [7][-][3][2][1][2] [6][5][4][3][-][3] [7][6][5][4][5][4] </pre> <p>When searching for a shortest path while the game is already running, we can now look up the player position in this map. We then instantly know how many steps it will take from our current position to the goal. To gather the concrete path, we check every field around the current one and select the one with a step value of 1 smaller. So assuming we stand on a field of value <code>5</code>, we then select a field around us with value <code>4</code>. Repeat and move to a field of value <code>3</code> and so on until we are at field <code>1</code> – which is the goal.</p> <h3>Better arrow key control</h3> <p>Just using the <code>keydown/up</code> events is not the best way to handle the controls. The <code>keydown</code> event fires repeatedly, but there is quite a break between the first event and the following repeated ones. Also the speed the events are fired at is most likely different from the speed of the game loop.</p> <p>So instead you use the <code>keydown/up</code> event to keep track of the key states. In the game loop you then query the current state.</p> <ul> <li>For every <code>keydown</code> you save as information the key and the current timestamp.</li> <li>For every <code>keyup</code> you reset that timestamp to <code>0</code> or <code>null</code> or delete it.</li> </ul> <pre class="language-javascript"> let state = {}; document.body.onkeydown = ( ev ) => { state[ev.which] = Date.now(); }; document.body.onkeyup = ( ev ) => { state[ev.which] = 0; };</pre> <p>In the game loop you can then do the following:</p> <pre class="language-javascript">if( state[40] ) { player.moveDown(); } else if( state[37] ) { player.moveLeft(); } else if( state[39] ) { player.moveRight(); } else if( state[38] ) { player.moveUp(); }</pre> <p>However, this can still be improved. Suppose you press multiple arrow keys at once. Currently it is a fixed order: Down is priorized over left is priorized over right is priorized over up. This can be confusing or downright annoying because it is not intuitive. The last pressed arrow key should always be the one being used. Let's add a function:</p> <pre class="language-javascript">function getLastArrowKey() { let down = state[40] || 0; let left = state[37] || 0; let right = state[39] || 0; let up = state[38] || 0; let values = [down, left, right, up]; values.sort( ( a, b ) => b - a ); let max = values[0]; if( !max ) { return null; } if( max === down ) { return 40; } if( max === left ) { return 37; } if( max === right ) { return 39; } if( max === up ) { return 38; } return null; }</pre> <p>This will return the arrow key which has the most current timestamp. Now adjust the handling in the game loop:</p> <pre class="language-javascript">let arrow = getLastArrowKey(); if( arrow === 40 ) { player.moveDown(); } else if( arrow === 37 ) { player.moveLeft(); } else if( arrow === 39 ) { player.moveRight(); } else if( arrow === 38 ) { player.moveUp(); }</pre> <p>Applying those two techniques, the player will now move 1) at a steady rate the moment a key is first pressed down, and 2) even if multiple arrow keys are pressed, the last one is used. Even if the last one is released, then the one which has been pressed before that (and has not been released yet) will be used.</p> <h3>The little things</h3> <p>Some other, maybe not so obvious gameplay improvements:</p> <ul> <li>Player and goal have a minimum distance at game start. Otherwise it would be too easy.</li> <li>The player always starts near a map border, but not the bottom one. If the player started at the bottom, he most likely would walk up and the character face would not be visible.</li> <li>Monsters are randomly placed, but always a minimum distance from the player. Avoid that the player gets killed right away in the first seconds.</li> <li>It is <em>Game Over</em> on the second hit. After the first hit, the player is invincible for 2 seconds to give him a chance to get away.</li> <li>Using path finding, check if there is a guaranteed path between player and goal before the game starts.</li> <li>The monsters are dump and try to take the most direct way to the player. This means they could get stuck on stones. To prevent this, they take a random step if the next tile is not an accessible one.</li> </ul> Dead Cells: PS4 controller support on Linux Sat, 11 Aug 2018 01:28:00 +0200 https://sebadorn.de/2018/08/11/dead-cells-ps4-controller-support-on-linux https://sebadorn.de/2018/08/11/dead-cells-ps4-controller-support-on-linux Seba Informatics <p><strong><a href="https://dead-cells.com/">Dead Cells</a></strong> is a game, it is really good, and it is available for Linux. However right after installation (version 1.0 from GOG) it did not recognize my PS4 controller. It could not be a problem with the controller itself or Linux in general, because the DS4 worked with other applications – for example it showed up perfectly fine in <a href="https://jstest-gtk.gitlab.io/">jstest-gtk</a> (0.1.0).</p> <p>After some research I came across <a href="https://www.reddit.com/r/linux_gaming/comments/6rxhj8/what_happened_with_the_dualshock_4_and_the_new/">this reddit post</a>. Dead Cells uses the <a href="https://www.libsdl.org/">SDL library</a>, so maybe that's it. I followed the instructions and built and ran <a href="https://gitlab.com/sdl-jstest/sdl-jstest">sdl2-jstest</a>. The output should contain an entry like this for the DS4 (2nd gen):</p> <pre class="language-plaintext">Joystick Name: 'Sony Interactive Entertainment Wireless Controller' Joystick GUID: 030000004c050000cc09000011810000 Joystick Number: 0 Number of Axes: 6 Number of Buttons: 13 Number of Hats: 1 Number of Balls: 0 GameControllerConfig: Name: 'PS4 Controller' Mapping: '030000004c050000cc09000011810000,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,' </pre> <p>… or like this for the DS4 (1st gen):</p> <pre class="language-plaintext">Joystick Name: 'Sony Computer Entertainment Wireless Controller' Joystick GUID: 030000004c050000c405000011810000 Joystick Number: 0 Number of Axes: 6 Number of Buttons: 13 Number of Hats: 1 Number of Balls: 0 GameControllerConfig: Name: 'PS4 Controller' Mapping: '030000004c050000c405000011810000,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,'</pre> <br /> <p>Take the value behind <em>Mapping</em> and add a line in your <code>/etc/environment</code> file like this:</p> <pre class="language-shell">SDL_GAMECONTROLLERCONFIG='030000004c050000cc09000011810000,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,'</pre> <br /> <p>After the next reboot everything should be working. Or if you want to test it right away without reboot, then you can just add it to the start script of the game. Assuming you used the standard installation path from the GOG installer, the file is located at <code>~/GOG Games/Dead Cells/start.sh</code>. Change the file so it now begins with:</p> <pre class="language-shell">#!/bin/bash # GOG.com (www.gog.com) # Game export SDL_GAMECONTROLLERCONFIG='030000004c050000cc09000011810000,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,'</pre> <br /> <p>That's what worked for me. If it still doesn't for you, try adding some udev rules as described in my article <a href="https://sebadorn.de/2017/12/07/using-nw-js-to-communicate-with-a-ds4-controller">Using NW.js to communicate with a DS4 controller</a>.</p> JavaScript source protection with NW.js Sat, 26 May 2018 19:21:00 +0200 https://sebadorn.de/2018/05/26/javascript-source-protection-with-nw-js https://sebadorn.de/2018/05/26/javascript-source-protection-with-nw-js Seba Informatics <p>You can minify and uglify JavaScript files, but technically the source code of your distributed <a href="https://nwjs.io/">NW.js</a> application is still readable. But NW.js also provides the means to <a href="https://nwjs.readthedocs.io/en/latest/For%20Users/Advanced/Protect%20JavaScript%20Source%20Code/">compile JavaScript to a binary file</a> and then load it as part of the application. The command line tool <code>nwjc</code> to create the binary file is included in the SDK version.</p> <p>Assuming you have a JavaScript file <code>js/private.js</code>:</p> <pre class="js">'use strict'; function secretFunction( foo ) { return foo * 4; };</pre> <p>Then you can compile it like this to a file <code>js/private.bin</code>:</p> <pre class="shell">$ ./nwjs-sdk-v0.30.5-linux-x64/nwjc js/private.js js/private.bin</pre> <p>Internally the tool uses the <a href="https://v8project.blogspot.de/2015/09/custom-startup-snapshots.html">V8 snapshot feature</a>, which means the versions have to match. A binary file created with NW.js 0.30 can only be loaded by 0.30. Binary files also <strong>do not work cross-platform</strong>. For each platform it is necessary to compile its own binary file with the SDK for the same platform.</p> <p>To then load the binary file in your application, it works like this:</p> <pre class="js">let win = nw.Window.get(); win.evalNWBin( null, 'js/private.bin' ); let value = secretFunction( 4 ); // returns 16</pre> <p>Note however that the loading is per window. If you open another window in your application, the file has to be loaded there again.</p> <p>Using the DevTools you can of course find the functions and variables which have been loaded from the file. The function implementation however is protected:</p> <pre class="plain">&gt; String( secretFunction ) &lt; "function secretFunction() { [native code] }"</pre> <h3>DevTools issues</h3> <p><strong>Update 2018-12-15:</strong> Since NW.js 0.34 this issue seems to be fixed. Loading binary files works even with the DevTools open.</p> <hr /> <p>There is an issue with loading binary files and the DevTools. Basically you cannot have the DevTools open and then load the file. There will be no error, but the contents will not be available. <a href="https://github.com/nwjs/nw.js/issues/5225">This is a known issue.</a></p> <p>My temporary solution is to just close the DevTools. But just closing them right before is not enough, you also have to use a timeout before loading the file:</p> <pre class="js">let win = nw.Window.get(); // Function is only available in the SDK build. if( typeof win.closeDevTools === 'function' ) { win.closeDevTools(); } setTimeout( () => { win.evalNWBin( null, 'js/private.bin' ); }, 500 );</pre> <p>But why not check first if the DevTools are open? Then you could open them again afterwards. <a href="https://nwjs.readthedocs.io/en/latest/References/Window/#winisdevtoolsopen">According to the API documentation</a> there is <code>win.isDevToolsOpen()</code>. But it exists only in the documentation. Using the SDK build there is de facto no such function. This too is <a href="https://github.com/nwjs/nw.js/issues/4487">a known issue</a>.</p> <h3>Wine for Windows</h3> <p>I successfully used <a href="https://www.winehq.org/">Wine</a> 3 to compile a binary file for the Windows version of a NW.js application and then load it there. So if you are on Linux or macOS you will not need Windows for your build process. You should of course still test your application to make sure it works on all targeted platforms.</p> Using NW.js to communicate with a DS4 controller Thu, 07 Dec 2017 19:55:00 +0100 https://sebadorn.de/2017/12/07/using-nw-js-to-communicate-with-a-ds4-controller https://sebadorn.de/2017/12/07/using-nw-js-to-communicate-with-a-ds4-controller Seba Informatics <p class="illu"><img src="https://sebadorn.de/media/2017/12/ds4_green.jpg" style="width: 600px; height: 314px;" alt="DS4 green light" /></p> <p><a href="https://nwjs.io/">NW.js</a> still provides the <a href="https://developer.chrome.com/apps/api_index">Chrome Apps API</a> which has been removed from Chrome, but not ChromeOS. This will allow us to access in a platform-independant manner devices which are connected with the PC per USB.</p> <p>Without this API, a 3rd party Node.js module like <a href="https://github.com/node-hid/node-hid">node-hid</a> could be used. This will however come with platform-dependant libraries and will have to be updated or rebuild each time the Node.js version changes.</p> <p>This article concentrates on sending data to the controller. However it is also possible to retrieve data like pressed buttons using the established connection. Aside from using chrome.hid there is also the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API/Using_the_Gamepad_API">Gamepad API</a> for read-only access.</p> <h3>Identifying the controller</h3> <p>First we need a way to identify the DS4. Devices come with a vendor Id and product Id. According to the <a href="https://wiki.gentoo.org/wiki/Sony_DualShock">Gentoo Wiki</a> they are as follows:</p> <table> <tr> <th>Device</th> <th>Vendor Id</th> <th>Product Id</th> </tr> <tr> <td>DS4 (1st gen)</td> <td>hex <code>054C</code> / dec <code>1356</code></td> <td>hex <code>05C4</code> / dec <code>1476</code></td> </tr> <tr> <td>DS4 (2nd gen)</td> <td>hex <code>054C</code> / dec <code>1356</code></td> <td>hex <code>09CC</code> / dec <code>2508</code></td> </tr> </table> <p>Having tested with both devices, I can also confirm the Ids.</p> <h3>Get the device</h3> <p>For all communication with the device, we will use the <a href="https://developer.chrome.com/apps/hid">chrome.hid API</a>. First we define a filter using the vendor and product Id, and then query the available devices:</p> <pre class="brush: javascript">var filter = { filter: [ { vendorId: 1356, productId: 1476 }, { vendorId: 1356, productId: 2508 } ] }; chrome.hid.getDevices( filter, ( devices ) => { // Error handling. if( chrome.runtime.lastError ) { console.error( chrome.runtime.lastError ); return; } if( !devices ) { return; } var device = devices[0]; // Next: Connect to the device. };</pre> <!--more--> <h4>Linux</h4> <p>On Linux you have to add some udev rules to be able to access the devices as non-root. Create a file <code>/etc/udev/rules.d/61-dualshock.rules</code> with the following content:</p> <pre class="brush: plain">SUBSYSTEM=="input", GROUP="input", MODE="0666" SUBSYSTEM=="usb", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="05c4", MODE:="666", GROUP="plugdev" KERNEL=="hidraw*", SUBSYSTEM=="hidraw", MODE="0664", GROUP="plugdev" SUBSYSTEM=="input", GROUP="input", MODE="0666" SUBSYSTEM=="usb", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="09cc", MODE:="666", GROUP="plugdev" KERNEL=="hidraw*", SUBSYSTEM=="hidraw", MODE="0664", GROUP="plugdev"</pre> <p>Then reload the udev rules with the following command. If your DS4 was plugged in, you will have to disconnect it and then connect it again afterwards.</p> <pre class="brush: bash">sudo udevadm control --reload-rules</pre> <p>Source: <a href="https://www.npmjs.com/package/dualshock-controller">https://npmjs.com/package/dualshock-controller</a></p> <h3>Connect</h3> <p>In order to connect with the device the device Id is necessary. On success the callback will be passed a connection object containing a connection Id we will need.</p> <pre class="brush: javascript">chrome.hid.connect( device.deviceId, ( connection ) => { // Error handling. if( chrome.runtime.lastError ) { console.error( chrome.runtime.lastError ); return; } if( !connection ) { return; } var connectionId = connection.connectionId; // Next: Send a command. } );</pre> <h3>Send the command</h3> <p>The command will be passed as a buffer of length 10 of type <code>uint8</code> so the values are between <code>0</code> and <code>255</code>. The report Id has to be <code>5</code>.</p> <pre class="brush: javascript">const reportId = 5; var data = new Uint8Array( 10 ); var i = 0; // Unchanging beginning. data[i++] = 0xFF; data[i++] = 0x04; data[i++] = 0x00; // Options we can use. data[i++] = 0x00; // rumble right data[i++] = 0x00; // rumble left data[i++] = 0x00; // red LED brightness data[i++] = 0xFF; // green LED brightness data[i++] = 0x00; // blue LED brightness data[i++] = 0x00; // flash on duration data[i++] = 0x00; // flash off duration chrome.hid.send( connectionId, reportId, data.buffer, () => { // Error handling. if( chrome.runtime.lastError ) { console.error( chrome.runtime.lastError ); } } );</pre> <p>Setting all RGB values to <code>0</code> will turn off the light. The rumble values let the controller side rumble stronger the higher the value. Sending a <code>0</code> while it still rumbles – it stops after a few seconds on its own – will stop it immediately.</p> <p>Source for the format: <a href="https://github.com/ehd/node-ds4">https://github.com/ehd/node-ds4</a></p> <h3>Disconnect</h3> <p>When you don't need the device anymore, disconnect it.</p> <pre class="brush: javascript">chrome.hid.disconnect( connectionId, () => { // Error handling. if( chrome.runtime.lastError ) { console.error( chrome.runtime.lastError ); } } );</pre> Android file transfer over MTP is a nightmare Fri, 02 Sep 2016 23:00:00 +0200 https://sebadorn.de/2016/09/02/android-file-transfer-over-mtp-is-a-nightmare https://sebadorn.de/2016/09/02/android-file-transfer-over-mtp-is-a-nightmare Seba Informatics <p><strong>Update, 30.09.2017:</strong> After trying it again today, I found none of the problems listed below anymore. It's fast and painless. So that's good!</p> <hr /> <p>It was bad before, but after the upgrade to Ubuntu 16.04 – which may or may not be related, it probably is – it went from bad to downright painful. Connecting device and PC works fine. What does not is …</p> <ul> <li>It is slow. Opening the SD card directory with my music takes several seconds.</li> <li>After deleting files the amount of available space is not updated. I free space for new files but get a warning telling me there isn't enough free space. At least I have the option to try and copy the files anyway, ignoring the warning.</li> <li><strong>The new problem:</strong> After just one transfer, I cannot access the device per MTP anymore. No content is listed for the music directory anymore. Seemingly trying to load the contents it ultimately fails to do so. Unplugging does not help. Restarting the PC does not help. Restarting the device does not help. Using a different PC with macOS does not help. What helps is … using the Android file manager and deleting a file. Something is seriously broken here.</li> </ul> <p>In early Android versions the device was mounted as USB mass storage. Those were the good, old days. <a href="http://www.howtogeek.com/192732/android-usb-connections-explained-mtp-ptp-and-usb-mass-storage/">At least they had understandable reasons for replacing it.</a></p> <p class="illu"><img src="//sebadorn.de/media/2016/09/nautilus-ftp.png" alt="Nautilus FTP" /></p> <p><strong>An alternative to using MTP is FTP.</strong> I used the app <a href="https://play.google.com/store/apps/details?id=lutey.FTPServer">FTPServer</a> to start an FTP server on my device while connected to my home WiFi. In the FTPServer settings I chose <code>wlan0</code> as standard interface and pointed the server root to the directory <code>/mnt/extSdCard</code>. Now I can use FileZilla or Nautilus (see image above) to connect to the server from my PC. The transfer speed is of course a little limited, but the file transfer itself? – works just fine.</p> <p>While I'm happy to have found a good alternative to the nightmare that is MTP, I find this whole situation quite ridiculous. At some point I'd like to upgrade from my Galaxy S3. While I'm not considering iOS or Windows Phone, I'm also disappointed of Android. Hopefully the <a href="http://www.ubuntu.com/phone">Ubuntu phones</a> take of.</p> Improvements after Mozilla’s Observatory results Sat, 27 Aug 2016 16:15:00 +0200 https://sebadorn.de/2016/08/27/improvements-after-mozillas-observatory-results https://sebadorn.de/2016/08/27/improvements-after-mozillas-observatory-results Seba Informatics <p>Mozilla made their <a href="https://observatory.mozilla.org/">Observatory service</a> public, which lets you check the security of sites. A first run resulted in an <code>F</code> for <code>sebadorn.de</code>. Following some of the sug&shy;gestions I could improve that to a <code>B-</code>.</p> <h3>1. Redirect HTTP to HTTPS</h3> <p>Thanks to <a href="https://letsencrypt.org/">Let’s Encrypt</a> I already offered HTTPS, but I didn't enforce it. Now visitors to <code>http://sebadorn.de</code> are redirected to <code>http<strong>s</strong>://sebadorn.de</code>. I did so by adding the following rule to my <code>.htaccess</code> file:</p> <pre class="brush: apache">&lt;IfModule mod_rewrite.c&gt; RewriteEngine On RewriteCond %{HTTP_HOST} ^sebadorn\.de [NC] RewriteCond %{SERVER_PORT} 80 RewriteRule ^(.*)$ https://sebadorn.de/$1 [R,L] &lt;/IfModule&gt;</pre> <h3>2. Add some more headers</h3> <pre class="brush: apache">&lt;IfModule mod_headers.c&gt; Header always edit Set-Cookie (.*) "$1; HttpOnly; Secure" Header set Content-Security-Policy "frame-ancestors 'self'" Header set X-Content-Type-Options "nosniff" Header set X-Frame-Options "SAMEORIGIN" Header set X-XSS-Protection "1; mode=block" &lt;/IfModule&gt;</pre> <dl> <dt>Set-Cookie</dt> <dd>Cookies about to be set received additional directives: <em>HttpOnly</em> and <em>Secure</em>. <em>HttpOnly</em> disallows cookies being read by JavaScript and <em>Secure</em> enforces an HTTPS connection. (<a href="https://wiki.mozilla.org/Security/Guidelines/Web_Security#Cookies">Source</a>)</dd> <dt>X-Content-Type-Options</dt> <dd>Setting this header to <em>nosniff</em> tells browsers not to try and guess the MIME type of contents, which potentially prevents XSS attacks. (<a href="https://wiki.mozilla.org/Security/Guidelines/Web_Security#X-Content-Type-Options">Source</a>)</dd> <dt>X-Frame-Options</dt> <dd>Setting this header to <em>SAMEORIGIN</em> or <em>DENY</em> prevents other pages from displaying the site in a frame which prevents clickjacking. (<a href="https://wiki.mozilla.org/Security/Guidelines/Web_Security#X-Frame-Options">Source</a>)</dd> <dt>X-XSS-Protection</dt> <dd>Setting this header to <em>1; mode=block</em> tells browsers to try and detect XSS attacks and in this case stop loading the page. (<a href="https://wiki.mozilla.org/Security/Guidelines/Web_Security#X-XSS-Protection">Source</a>)</dd> </dl> Remove intro/outro from an MP3 without re-encoding Thu, 03 Dec 2015 21:44:00 +0100 https://sebadorn.de/2015/12/03/remove-intro-outro-from-an-mp3-without-re-encoding https://sebadorn.de/2015/12/03/remove-intro-outro-from-an-mp3-without-re-encoding Seba Informatics Music <p>I have this podcast I listen to repeatedly. But most episodes have an intro and outro part before the actual episode, which gets quite annoying. One possibility is, of course, to just use an audio editor like Audacity, cut the unwanted parts, and save the file. But this would result in the MP3 being re-encoded and losing in quality. This shouldn't be necessary since I only want to cut off some data, right?</p> <p>The tool for the job is <code>mp3splt</code>. I wrote this little bash script:</p> <pre class="brush: bash">#!/usr/bin/env bash F_IN=$1 F_OUT=${F_IN%.mp3} START=$2 END=EOF-$3 OUT_DIR=./nointro # Split at "-". FILE_SPLIT=(${F_IN//-/ }) # Remove "_" and " " characters. TRACK=${FILE_SPLIT[0]//_/} TRACK=${TRACK// /} mp3splt -f -d "$OUT_DIR" "$F_IN" "$START" "$END" -o "$F_OUT" eyeD3 --track=$TRACK "$OUT_DIR/$F_IN"</pre> <p>Example: <code>./shorten.sh 26-FacelessOldWoman.mp3 1.18 1.15.5</code></p> <p>First, the track number will be extracted from the file name. In my case, the number is always at the beginning and separated by a minus from the title. So we split the string and remove some unwanted characters (whitespace, underscore).</p> <p>In the example, the first 1min 18sec will be removed and the last 1min 15.5sec. The resulting MP3 will be saved with the same name in the directory set in <code>OUT_DIR</code>.</p> <p>Most id3 tags will be kept – including the embedded cover –, but the track number will be overwritten by mp3splt. That is why I extracted the track number from the file name before. Now I can set it again with <code>eyeD3</code>.</p> <p>Done.</p> Stackless BVH traversal Fri, 13 Mar 2015 23:42:00 +0100 https://sebadorn.de/2015/03/13/stackless-bvh-traversal https://sebadorn.de/2015/03/13/stackless-bvh-traversal Seba Informatics <p class="abstract">Im Path Tracing verwendet man spezielle Datenstrukturen für die Geometrie, um diese schneller gegen die Strahlen testen zu können. Eine der üblichsten ist dabei die Bounding Volume Hierarchy (BVH) – ein Binärbaum, der die Szene immer weiter unterteilt. Einen solchen Baum würde man normalerweise rekursiv durchlaufen. Auf der GPU mit OpenCL steht jedoch keine Rekursion zur Verfügung.</p> <p class="abstract">Was man daher macht, ist, selbst einen kleinen Stack zu verwalten, in dem man sich den nächsten zu besuchenden Knoten merkt. Dieser Stack benötigt jedoch zusätzlichen privaten Speicher, welcher knapp bemessen ist, und dadurch die Anwendung ausbremst. Wünschens&shy;wert ist daher ein Verfahren, das ohne Stack auskommt.</p> <h3>Aufbau und Ablauf</h3> <p class="illu"><img src="//sebadorn.de/media/2015/03/bvh-stackless-nextNode.png" alt="BVH-Baum." style="width: 600px; height: 400px" /></p> <ul> <li>Jeder Knoten hat entweder genau zwei oder keine Kindknoten.</li> <li>Jeder Knoten hat zudem ein Attribut <em>nextNode</em>. <ul> <li>Für den linken Kindknoten zeigt <em>nextNode</em> auf den rechten Geschwisterknoten.</li> <li>Für den rechten Kindknoten zeigt <em>nextNode</em> auf den Elternknoten.</li> </ul> </li> </ul> <!--more--> <p>Vom Wurzelknoten aus wird immer zuerst dem linken Kindknoten gefolgt. Falls der Strahl die Bounding Box nicht trifft, es sich um einen Blatt&shy;knoten handelt oder zuletzt ein rechter Knoten besucht wurde, dann wird als nächstes der Knoten besucht, auf den <em>nextNode</em> zeigt. Ist man wieder beim Wurzel&shy;knoten angelangt, beendet der Algorithmus.</p> <p><strong>Nachteil:</strong> Knoten werden auf dem Weg nach oben erneut besucht. Ein unnötiger Schritt, der nur zusätzliche Zeit in Anspruch nimmt.</p> <h3>Optimierung</h3> <p class="illu"><img src="//sebadorn.de/media/2015/03/bvh-stackless-nextNode-right-optimized.png" alt="BVH – optimierter nextNode" style="width: 600px; height: 400px;" /></p> <p>Die Optimierung ist denkbar einfach. Dafür muss jediglich der <em>nextNode</em> der rechten Knoten angepasst werden. Anstatt auf den Elternknoten, können rechte Kindknoten direkt auf den nächsten zu testenden Knoten zeigen.</p> <p>Dafür setzt man zunächst <em>nextNode</em> immer weiter auf einen höheren Elternknoten, bis es sich bei diesem um einen linken Kindknoten handelt. Danach setzt man <em>nextNode</em> auf den rechten Geschwister&shy;knoten. Auf diese Weise werden alle bereits besuchten Eltern&shy;knoten über&shy;sprungen. Existiert kein rechter Kindknoten, ist man beim Wurzel&shy;knoten angelangt und zeigt auf diesen, was den Endpunkt markiert.</p> <p>Den so erzeugten Baum kann man dann folgendermaßen abwandern:</p> <pre class="brush: cpp">int index = 0; do { bvhNode node = scene->bvh[index]; float tNear = 0.0f; float tFar = INFINITY; bool isNodeHit = ( intersectBox( ray, node, &tNear, &tFar ) && tFar > 0.00001f && ray->t > tNear ); // In case of no hit: Go right or up. index = node.nextNode; if( !isNodeHit ) { continue; } // Not a leaf node, progress further down to the left. if( !isLeaf( node ) ) { index = node.leftChild; } // Node is leaf node. else { intersectFaces( scene, ray, node, tNear, tFar ); } } while( index > 0 );</pre> <h3>Anmerkung: Links/rechts</h3> <p>Welcher der beiden Kindknoten als „linker“ und welcher als „rechter“ gesetzt wird, ist egal. Ich setze als linken Kindknoten denjenigen mit mehr Ober&shy;fläche. Mehr Ober&shy;fläche bedeutet, dass ihn potentiell mehr Strahlen treffen und man so eventuell ein früheres Ende erreicht. Tatsächlich hat diese Vorsortierung die Performance gering&shy;fügig verbessert.</p> <h3>Resultat</h3> <p>Mein Algorithmus kommt wie gewünscht ohne Stack aus. Weiterer Speicher wird nur für eine Variable <em>index</em>, sowie ein neues Knoten-Attribut <em>nextNode</em> benötigt. Beim Abwandern des Baumes wird jeder Knoten höchstens ein einziges Mal besucht.</p> <p>Zumindest in meiner Implemen&shy;tierung <a href="#fn-bvh-impl">[1]</a> verbesserten sich die Render-Zeiten in allen meinen Test-Szenen. Dies kann aber auch an einer schlechten Implemen&shy;tierung des Stack-basierten Algorithmus liegen, mit dem ver&shy;glichen wurde. Ich habe beide Verfahren im Projekt belassen und versuche sie von Zeit zu Zeit weiter zu optimieren und erneut zu vergleichen.</p> <hr /> <p class="annotation" id="fn-bvh-impl">[1] <a href="https://github.com/sebadorn/Physically-based-Rendering/blob/4bbf393a597a1bc1cf3e3536a57abd6f3026c8a7/source/PathTracer.cpp#L261">Vorbereitung der BVH-Datenstruktur</a> und <a href="https://github.com/sebadorn/Physically-based-Rendering/blob/4bbf393a597a1bc1cf3e3536a57abd6f3026c8a7/source/opencl/pt_bvh.cl#L128">BVH traversal</a>.</p> Standalone-Anwendungen mit node-webkit Sun, 19 Oct 2014 16:28:00 +0200 https://sebadorn.de/2014/10/19/standalone-anwendungen-mit-node-webkit https://sebadorn.de/2014/10/19/standalone-anwendungen-mit-node-webkit Seba Informatics <p class="illu"><img src="//sebadorn.de/media/2014/10/node-webkit-demo.png" style="width: 602px; height: 425px;" alt="node-webkit demo window" /></p> <p>Gestern habe ich ein wenig in <a href="https://github.com/rogerwang/node-webkit">node-webkit</a> reingeschnuppert. Damit sollen sich ganz einfach Desktop-Anwendungen auf Basis von HTML, JavaScript und Node.js erstellen lassen. Die erstellte Anwendung lässt sich dann relativ einfach für verschiedene Betriebs&shy;systeme verpacken. Für nicht allzu rechen-intensive Spiele scheint mir das recht interessant. Tatsächlich verwendet auch das kürzlich erschiene <a href="http://www.wizardslizard.com/">A Wizard's Lizard</a> node-webkit <a class="fn" href="#fn-lizard">[1]</a>.</p> <p>Für eine kleine Demo habe ich das <a href="https://github.com/mrdoob/three.js/">Beispiel von three.js</a> genommen und als Anwendung verpackt. Das HTML und JavaScript lasse ich hier mal aus. Mein <code>package.json</code> für ein Fenster ohne Toolbar sieht wie folgt aus:</p> <pre class="brush: javascript">{ "main": "index.html", "name": "nw-demo", "window": { "frame": true, "height": 600, "kiosk": false, "toolbar": false, "width": 900 } }</pre> <!--more--> <p>Den Inhalt des Projekt-Verzeichnisses verpackt man als nächstes als ZIP-Archiv:</p> <pre class="brush: bash">zip -r demo.nw *</pre> <p>Diese Datei lässt sich mit <code>nw demo.nw</code> ausführen, benötigt aber eben weiterhin node-webkit auf dem System. Im nächsten Schritt wird node-webkit daher in das Archiv integriert:</p> <pre class="brush: bash">cat ~/node-webkit-v0.10.5/nw demo.nw > demo && chmod +x demo</pre> <p>Versucht man nun <code>./demo</code> auszuführen, gibt es eine Fehlermeldung:</p> <blockquote class="code error"> [13445:1019/161018:FATAL:content_main_runner.cc(751)] Check failed: base::i18n::InitializeICU(). </blockquote> <p>Im selben Verzeichnis wie die Standalone-Anwendung müssen auch die Dateien <strong>nw.pak</strong> und <strong>icudtl.dat</strong> liegen. Beide Dateien finden sich im selben Verzeichnis wie <code>nw</code>, hier also <code>~/node-webkit-v0.10.5/</code>. Einfach herüberkopieren und die Anwendung lässt sich starten.</p> <hr /> <p>Meine kleine Demo habe ich <a href="https://github.com/sebadorn/project-templates/tree/master/node-webkit">auf GitHub hochgeladen</a>. Darin befindet sich auch ein Build-Skript für Linux, das sich um alles kümmert. Es muss nur der Pfad zur node-webkit-Installation angepasst werden.</p> <hr /> <p class="annotation" id="fn-lizard">[1] <a href="http://forum.lostdecadegames.com/topic/115/node-webkit-and-steam">forum.lostdecadegames.com/topic/115/node-webkit-and-steam</a></p> Sublime Text 3 portable unter Linux Fri, 05 Sep 2014 15:24:00 +0200 https://sebadorn.de/2014/09/05/sublime-text-3-portable-unter-linux https://sebadorn.de/2014/09/05/sublime-text-3-portable-unter-linux Seba Informatics <p><a href="https://sublimetext.com/3">Sublime Text</a> ist derzeit der Editor meiner Wahl, zusammen mit einigen Zusatz-Paketen. Installiert habe ich ihn über <a href="http://www.webupd8.org/2013/07/sublime-text-3-ubuntu-ppa-now-available.html">das Web Upd8 PPA</a>. Das ist allerdings hinderlich, wenn man ihn auch als normalen Text-Editor zum spontanen Bearbeiten verwenden möchte. In dem Fall springt immer das zuletzt geöffnete Projekt mit auf. Eine Extra-Installation wäre hier praktisch.</p> <ol> <li>Lade <a href="https://sublimetext.com/3">Sublime Text 3</a> als tarball herunter.</li> <li>Entpacke das Archiv nach <code>~/.st3_portable</code>. (Name egal.)</li> <li>Erstelle in dem Verzeichnis einen Ordner namens <code>Data</code>. (Wichtig!)</li> </ol> <p>Das reicht schon, um auf eine portable Version umzustellen. Für die Verwendung als Gelegenheits-Text-Editor steht aber noch ein wenig mehr an. So soll, wenn eine Datei geöffnet wird, auch das zugehörige Verzeichnis in der Sidebar angezeigt werden. Dafür erstellt man sich ein kleines Bash-Skript <code>~/.st3_portable/st3_p.sh</code>:</p> <pre class="brush:bash">#!/bin/bash cd ~/.st3_portable/ FOLDER=$(dirname "$1") ./sublime_text "$FOLDER" "$1"</pre> <p>Das Argument <code>$1</code> ist die zu öffnende Datei. Im letzten Schritt muss nun noch eine <code>.desktop</code>-Datei angelegt werden. Erstelle eine Datei <code>~/.local/share/applications/st3-portable.desktop</code> mit dem Inhalt:</p> <pre class="brush:plain">[Desktop Entry] Version=1.0 Type=Application Name=Sublime Text 3 (portable) Comment=Sophisticated text editor for code, markup and prose Exec=/home/seba/.st3_portable/st3_p.sh %F Terminal=false MimeType=text/plain; Icon=sublime-text Categories=TextEditor;Development;Utility; StartupNotify=true X-Desktop-File-Install-Version=0.21</pre> <p>Anstatt <code>/home/seba/</code> sollte da natürlich der eigene Benutzername stehen. Relative Pfadangaben bzw. <code>~/</code> funktionieren hier nicht. Ist die Datei gespeichert, sollten sich Dateien nun per <em>Rechtsklick › Öffnen mit › Sublime Text 3 (portable)</em> öffnen lassen.</p> Mit dem Upgrade zu Ubuntu 14.04 hätte ich noch warten sollen Sat, 17 May 2014 13:10:00 +0200 https://sebadorn.de/2014/05/17/mit-dem-upgrade-zu-ubuntu-14-04-haette-ich-noch-warten-sollen https://sebadorn.de/2014/05/17/mit-dem-upgrade-zu-ubuntu-14-04-haette-ich-noch-warten-sollen Seba Informatics <p>Da meine Masterarbeit abgeschlossen ist, habe ich gestern das Upgrade von Ubuntu 13.10 auf 14.04 gewagt. Zwar gab es keine kritischen Probleme, aber doch einige Ärgernisse – wovon sich die meisten mit Recherche und Herumprobieren beheben ließen.</p> <h3>Konflikt zwischen Wine und NVIDIA-OpenCL</h3> <p>Es macht keinen Sinn, aber scheinbar hat jemand in den Paket-Abhängigkeiten gepfuscht oder sonst irgendetwas. Wollte man <code>wine</code> installieren, musste <code>nvidia-libopencl1-331</code> deinstalliert werden und vice versa. Ich brauche beide Pakete. Wie sich herausstellte, lässt sich Wine 1.7 aber installieren.</p> <p>Dafür fügt man das <a href="https://launchpad.net/~ubuntu-wine/+archive/ppa">Wine Team PPA</a> zu seinen Softwarequellen hinzu und kann dann <code>wine1.7</code> installieren. Daraufhin lässt sich auch <code>playonlinux</code> installieren.</p> <p>Sollte <code>/usr/lib/libOpenCL.so</code> dann immer noch fehlen, muss wahrscheinlich nur ein Symlink nachgetragen werden:</p> <pre class="brush: bash">sudo ln -s /usr/lib/i386-linux-gnu/libnvidia-opencl.so.1 /usr/lib/libOpenCL.so</pre> <h3>Fallout 3 läuft nicht mehr unter Wine</h3> <p><del>Ein leider noch ungelöstes Problem.</del> Vor dem Upgrade lief Fallout 3 unter Wine ziemlich gut – nicht perfekt, aber ziemlich gut. Jetzt öffnet sich nur noch ein inhaltsloses Fenster. Eine Neu-Installation hat nicht geholfen. Mit Debugging bin ich auch noch nicht weit gekommen, da in den Logs kein eindeutiger Fehler zu finden ist. Wer Tipps hat, ich wäre dankbar. Vielleicht tauchen ja demnächst Leidensgenossen auf <a href="http://appdb.winehq.org/objectManager.php?sClass=version&iId=14322">appdb.winehq.org</a> auf.</p> <p><strong>[Update 2014-05-18]:</strong> Wine 1.7.19 ist erschienen und behebt genau dieses Problem (<a href="http://www.winehq.org/announce/1.7.19">siehe Changelog</a>).</p> <!--more--> <h3>In Firefox keine YouTube-HTML5-Videos über 360p</h3> <p>Grund ist mangelnde Unterstützung des H.264-Codecs. Das ist jedoch nichts Neues und bisher gab es <a href="http://askubuntu.com/questions/389437/how-do-i-get-html5-h-264-video-working-on-firefox">dafür einen Workaround mit GStreamer</a>. Der funktioniert nun nicht mehr, da die benötigten Pakete durch neuere ersetzt wurden.</p> <p>Eine Möglichkeit ist sicherlich, ein PPA zu finden, dass die alte Version zur Verfügung stellt, aber ich habe ehrlich gesagt genug davon, andauernd andere PPAs einbinden zu müssen. Was YouTube anbelangt steige ich wieder auf Flash um. Mit Firefox 30 könnte sich das wieder ändern, wenn dann hoffentlich GStreamer 1.0 unterstützt wird.</p> <h3>GNOME-Terminal ohne Transparenz</h3> <p>Wie aus <a href="https://bugzilla.gnome.org/show_bug.cgi?id=698544#c45">dieser Antwort eines GNOME-Entwicklers</a> hervorgeht, wurde Hintergrund-Transparenz im Rahmen größerer Code-Aufräumarbeiten (vorerst?) entfernt. Ich will aber transparente Terminals, also bin ich auf Terminator umgestiegen.</p> <pre class="brush: bash">sudo apt-get install terminator</pre> <h3>Einige GNOME Shell-Erweiterungen sind nicht aktuell</h3> <p class="illu"><img style="width: 687px; height: 623px;" src="//sebadorn.de/media/2014/05/gnome-shell-extensions.png" alt="Installierte GNOME Shell-Erweiterungen." /></p> <p>Ich mag die GNOME Shell. Aber nur durch einige Erweiterungen wird sie wirklich gut, ansonsten wäre sie arg unattraktiv für mich. Die eine oder andere Erweiterung wurde von ihrem Autor leider noch nicht aktualisiert, was zum Glück kein allzu großes Problem darstellt. Diese Erweiterungen sind in JavaScript geschrieben und liegen einfach in einem HOME-Unterverzeichnis (<code>~/.local/share/gnome-shell/extensions/</code>). Die folgenden Anpassungen waren ausreichend für <a href="">Nvidia GPU Temperature Indicator </a> und <a href="https://extensions.gnome.org/extension/82/cpu-temperature-indicator/">Sensors</a>.</p> <ul> <li> In <code>metadata.json</code> die aktuelle Version eintragen: <pre class="brush: js">"shell-version": ["3.10"],</pre> </li> <li> Anstatt <code>PanelMenu.SystemStatusButton</code> verwendet man <code>PanelMenu.Button</code>. <pre class="brush: js">PanelMenu.Button.prototype._init.call(this, 0.0, 'sensorsMenu', false);</pre> </li> <li> Aus der Funktion <code>.addActor()</code> wurde <code>.actor.add()</code>. </li> </ul> <h3>Symbole im Cairo-Dock verschwinden</h3> <p>Zusätzlich zur GNOME Shell habe ich noch am Bildschirmrand ein Cairo-Dock für die offenen Anwendungen. Leider waren viele Symbole plötzlich nicht mehr zu sehen. Die Lösung war erfreulich unkompliziert. Cairo-Dock muss im OpenGL-Modus gestartet werden:</p> <pre class="brush: bash">cairo-dock -o</pre> Bilder aus der Masterthesis: Physik-basiertes Rendering Thu, 10 Apr 2014 02:59:00 +0200 https://sebadorn.de/2014/04/10/bilder-aus-der-masterthesis-physikbasiertes-rendering https://sebadorn.de/2014/04/10/bilder-aus-der-masterthesis-physikbasiertes-rendering Seba Informatics Studenthood <p class="illu"><img style="width: 800px; height: 600px;" src="//sebadorn.de/media/2014/04/pbr-04.png" alt="Modell: Eichhörnchen" /></p> <p>Das große Eichhörnchen ist aus Glas (Brechungsindex 1,5). Die dunklen Stellen – z.B. an der Schnauze – sollten nicht dunkel sein, aber der Pfad hat hier seine maximale Länge erreicht, weshalb keine Licht- und somit Farbinformationen gewonnen werden konnten. Die ganzen Erklärungen, was ich in diesem Projekt überhaupt gemacht habe, kommen irgendwann in einem anderen Blogeintrag.</p> <!--more--> <p class="illu"><img style="width: 800px; height: 600px;" src="//sebadorn.de/media/2014/04/pbr-05.png" alt="Modell: Eichhörnchen" /></p> <p>Gleiche Szene wie eben, nur wurde als Licht ein eher dunkles, warmes Orange (CIE A) gewählt. Das kleine Eichhörnchen wurde zudem zur weißen Lichtquelle umdefiniert.</p> <p class="illu"><img style="width: 800px; height: 600px;" src="//sebadorn.de/media/2014/04/pbr-06.png" alt="Modell: Icosphere" /></p> <p>Ein Ikosaeder in der Mitte und die Wände links und rechts davon spiegeln. Allerdings ist die Spiegelung nicht perfekt, sondern leicht matt gewählt, weshalb das Bild mit zunehmender Spiegelungsanzahl verschwimmt. Die schwarze Wand am Schluss gibt es nicht wirklich, hier haben die Pfade nur wieder ihr Maximum erreicht.</p> <p class="illu"><img style="width: 800px; height: 600px;" src="//sebadorn.de/media/2014/04/pbr-07.png" alt="Modell: Icosphere" /></p> <p>Die weißen Linien zeichnen die Bounding Boxen ein. Als Datenstruktur wurde eine <em>Bounding Volume Hierarchy</em> verwendet – die Dreiecke wurden in kleine Gruppen eingeteilt.</p> <p class="illu"><img style="width: 800px; height: 600px;" src="//sebadorn.de/media/2014/04/pbr-03.png" alt="Modell: Blenders Suzanne" /></p> <p>Matter, grüner Boden. Der Affenkopf ist das Suzanne-Modell von <a href="http://www.blender.org/">Blender</a>. Hier ist sehr schön Color Bleeding zu sehen – die Farbe des grünen Bodens strahlt auf die Objekte ab.</p> <p class="illu"><img style="width: 800px; height: 600px;" src="//sebadorn.de/media/2014/04/pbr-01.png" alt="Modell: Applejack" /></p> <p>Das <a href="http://kp-shadowsquirrel.deviantart.com/art/Pony-Model-Download-Center-215266264">Applejack-Modell von KP-ShadowSquirrel</a>. Die Farben und Material-Eigenschaften habe allerdings ich verbockt. Die richtigen Farben zu finden ist recht schwer, wenn man sie nicht einfach per Color Picker und Schieberegler zusammenklicken kann. Da mein Renderer mit Wellenlängen und Intensitäten zur Farbrepräsentation arbeitet, war viel Ausprobieren und Herantasten nötig.</p> <p class="illu"><img style="width: 800px; height: 600px;" src="//sebadorn.de/media/2014/04/pbr-02.png" alt="Modell: Applejack" /></p> <p>Noch einmal mit anderer Beleuchtung. Alle Bilder wurden für circa 2&nbsp;Minuten bei 10&nbsp;FPS auf der Grafikkarte gerendert.</p> Der kleine Kampf CyanogenMod auf dem Galaxy S1 zu installieren Fri, 16 Aug 2013 19:17:00 +0200 https://sebadorn.de/2013/08/16/der-kleine-kampf-cyanogenmod-auf-dem-galaxy-s1-zu-installieren https://sebadorn.de/2013/08/16/der-kleine-kampf-cyanogenmod-auf-dem-galaxy-s1-zu-installieren Seba Informatics <p class="illu"><img style="width: 600px; height: 348px;" src="//sebadorn.de/media/2013/08/s1-cyanogenmod.jpg" alt="CyanogenMod wird auf dem Galaxy S1 installiert" /></p> <p>Um das Ende vorwegzunehmen: Die Installation selbst hat reibungslos geklappt und auf meinem alten Samsung Galaxy S1 läuft nun super-flüssig CyanogenMod 10.1 (Android 4.2.2). Auf den Schritten dahin bin ich nur unterwegs ein wenig angeeckt.</p> <p>Ich folgte dabei (so gut es ging) der <a href="https://wiki.cyanogenmod.org/w/Install_CM_for_galaxysmtd">offiziellen Anleitung</a>.</p> <h3>Heimdall für 64-Bit-Linux funktioniert nicht mit 64-Bit-Linux</h3> <p>Heimdall wird benötigt, um eine Recovery-Datei auf das Smartphone zu schieben, während es sich im <em>Download Mode</em> befindet. Heimdall gibt es für Windows, Linux und Mac. Nur funktioniert die 64-Bit-Linux-Version scheinbar nicht bei jedem, worauf in der Anleitung aber auch hingewiesen wird. Die 32-Bit-Version erfüllt aber ihren Zweck. Von daher war das nicht wirklich ein Problem.</p> <!--more--> <h3>Endlose Boot-Schleife</h3> <p>Nun sollte ich also in den <em>ClockworkMod Recovery Mode</em> booten, indem ich zum Einschalten <strong>Volume up + Home + Power</strong> drücke. Dies machte ich, aber das Gerät kam nicht über den Boot-Begrüßungs-Screen (der mit dem Samsung-Logo) hinweg. Ab und zu flackerte dieser mal kurz, ansonsten ging es aber nicht mehr weiter. Ich probierte es mehrmals, manchmal konnte ich es wieder über den Power-Knopf ausschalten, manchmal entfernte ich einfach den Akku.</p> <p>Ein kurzes <a href="https://youtu.be/uQWg-SiV-L8">YouTube-Erklär-Video</a> brachte mich schließlich auf den Trichter, dass mein <strong>Timing</strong> schlecht war. Man darf die Tastenkombination nur so lange drücken, bis das Samsung-Logo erscheint. Entweder habe ich vorher immer zu lange oder zu kurz gehalten, aber diesmal hat es geklappt.</p> <h3>Wie bekomme ich das Image nach /sdcard?</h3> <p>Nun musste also das CyanogenMod-ZIP auf den internen Speicher verfrachtet werden. Die Anleitung empfiehlt dafür <strong>ADB</strong> – ich hingegen rate davon ab, das schont Nerven. Meine aktuelle ADB-Version hat das Smartphone immer als offline aufgeführt und entsprechend konnte ich auch nicht damit interagieren. Ich habe verschiedene USB-Ports ausprobiet, ein anderes Kabel genommen, das Smartphone neugestartet … nope, no dice!</p> <p>Daraufhin hatte ich über das ClockwordMod Recovery-Menü das Gerät als <strong>Mass Storage</strong> gemountet. Dadurch konnte ich über Ubuntu darauf zugreifen und die Datei einfach hinüberkopieren … oder auch nicht. Der Kopiervorgang hat sich jedes Mal kurz vor Ende aufgehangen und die Datei hat es nie auf das Gerät geschafft.</p> <p>Schließlich habe ich meinen Laptop gepackt und das darauf noch installierte, seit Jahren nicht mehr gebootete Vista gestartet. Smartphone angeschlossen und es wurde als Wechseldatenträger erkannt – gut! USB-Stift mit dem ZIP eingesteckt und der Explorer hängt sich auf – schlecht. Abgemeldet, neu angemeldet. Diesmal habe ich zuerst das ZIP hinüberkopiert und dann das Smartphone angeschlossen. Erfolg! Das ZIP war auf dem Gerät!</p> <blockquote> <p><strong>Vorschlag:</strong> Bevor ihr auch nur mit dem ersten Schritt der Anleitung beginnt, ladet das benötigte ZIP herunter und übertragt es ganz normal auf euer Smartphone. (Den internen Speicher, nicht auf eine externe Karte!) Dann müsst ihr weder mit ADB herumhantieren, noch genervt einen Alternativ-Weg suchen.</p> </blockquote> <p>Von da an hat es geklappt. Und ja, der nicht zu leugnende, kleine Linux-Fanboy in mir ist ein wenig angefressen wegen der benötigten Windows-Hilfe.</p> <h3>Wie bekomme ich den Play Store da drauf?</h3> <p>Okay, <a href="http://goo.im/gapps">hier gibt es also ein ZIP zum Download</a>. Aber sind Apps nicht normalerweise APK-Dateien? Was soll also mit dem ZIP … oooh.</p> <p>Das Gerät wieder mit <strong>Volume Up + Home + Power</strong> einschalten und diesmal anstatt dem CyanogenMod-ZIP das Gapps-ZIP installieren.</p> <hr /> <p class="illu"><img style="width: 600px; height: 500px;" src="//sebadorn.de/media/2013/08/s1-systeminfo.jpg" alt="S1 auf dem CyanogenMod läuft" /></p> <p>Unterm Strich ist also nichts Tragisches passiert. Es war nur teils anstrengend und als ich im Boot-Loop steckte, hatte ich schon kurzzeitig das Schlimmste befürchtet.</p> Opera –› Firefox Wed, 17 Jul 2013 20:20:00 +0200 https://sebadorn.de/2013/07/17/opera-firefox https://sebadorn.de/2013/07/17/opera-firefox Seba Informatics <p>Opera 15 ist erschienen (zumindest für Windows und Mac) und bringt den großen Engine-Sprung von Presto zu Blink mit sich. Eine nachvollziehbare und sinnvolle Umstellung. Allerdings erscheint mir die Veröffentlichung überstürzt und Opera 15 ein halbgares Produkt. Wozu gibt es denn die <a href="http://my.opera.com/desktopteam/blog/opera-features-and-release-cycle">drei Release-Kanäle</a> <em>Opera (Stable)</em>, <em>Next</em> und <em>Developer</em>?</p> <p>Opera 15 hat keine Bookmarks, herrjenocheins! Stattdessen soll dafür das Speed Dial ausreichen, mit dem sich zwar auch Seiten speichern, gruppieren und suchen lassen, aber das eben keine einfache Auflistung bietet. Bin ich ein „Power User“-Randfall, nur weil ich 100+ Bookmarks habe?</p> <p class="illu"><img style="width: 600px; height: 369px;" src="//sebadorn.de/media/2013/07/opera15-speeddial.jpg" alt="Opera 15: Speeddial" /></p> <p>Das generelle Feedback war auch entsprechend negativ und Opera hat verlauten lassen, dass <a href="http://my.opera.com/desktopteam/blog/2013/07/10/ctrl-z-of-ctrl-d">ein paar Prioritäten umverlagert werden</a>. Dann fehlen aber immer noch die Site Preferences. Vermutlich fehlt noch mehr, aber für mich ist der Deal hier schon geplatzt. Nunja, ganz abgesehen davon gibt es ohnehin noch keine Linux-Version.</p> <p>Der neue Blink-Opera wird wohl frühestens in einem Jahr auf einen vernünftigen Stand kommen – vorausgesetzt, er soll überhaupt die Funktionalität der 12er-Version wieder erreichen. Wenn ein Unternehmen ankündigt, es wolle sein Produkt massentauglicher gestalten, überkommt mich ein ungutes Gefühl.</p> <p>Also könnte ich entweder bei Opera 12 bleiben, der noch eine Weile sicherheitstechnisch gepflegt werden wird, oder ich gewöhne mich schon mal um. Seit einiger Zeit mache ich auch die Erfahrung, dass Opera 12 von Firefox und Chrome – gerade was JavaScript-Geschwindigkeit betrifft – abgehängt wird und die eine oder andere Seite ein Problem mit Opera 12 hat, wie z.B. YouTube oder Mega.co.nz.</p> <p>Jetzt bin ich also bei Firefox und versuche mir mit Addons alles so herzurichten, dass es mir an Nichts fehle. Warum Firefox und nicht Chrome? Firefox lässt sich etwas besser bzw. mehr konfigurieren und Mozilla ist mir auch das sympathischere Unternehmen – das meine ich weniger negativ für Google, als vielmehr positiv für Mozilla.</p> <!--more--> <h3>Übernahme der Bookmarks</h3> <p>Die Bookmarks lassen sich in Opera als HTML exportieren. Firefox kann diese HTML-Datei wieder importieren und schon hat man alle Bookmarks inklusive Verzeichnisstruktur. Vorher sollte man in Opera vielleicht noch das Müll-Verzeichnis leeren, da diese sonst zusammen mit den anderen Bookmarks im obersten Verzeichnis landen.</p> <p>Mir ist bewusst, dass es eine <a href="https://addons.opera.com/en/extensions/details/bookmarks-manager/">Opera 15 Extension für Bookmarks</a> gibt. Diese ist hochgradig unnütz. Die Bedienung und Optik ist lachhaft schlecht und der Import hat bei mir erst gar nicht funktioniert – was aber auch daran liegen mag, dass ich aus der Opera-Linux-Version exportierte und in Opera-Windows-in-einer-VM importierte.</p> <h3>Addons</h3> <p><a href="https://addons.mozilla.org/de/firefox/addon/adblock-plus/"><strong>Adblock Plus</strong></a> zusammen mit dem <a href="https://addons.mozilla.org/de/firefox/addon/elemhidehelper/"><strong>Element Hiding Helper</strong></a>. Der Zweck dürfte bekannt sein: Werbung und andere nervige Elemente blockieren.</p> <hr /> <p><a href="https://addons.mozilla.org/de/firefox/addon/add-to-search-bar/"><strong>Add to Search Bar</strong></a> ist praktisch, um sehr einfach neue Suchmaschinen hinzuzufügen: Einfach ein Rechtsklick in das Suchfeld der besuchten Seite und auswählen. Die eingerichteten Suchen können entweder über das Suchfeld genutzt werden oder – wie ich es bevorzuge – per Kürzel über die Adressleiste verwendet werden.</p> <p class="illu"><img style="width: 600px; height: 407px;" src="//sebadorn.de/media/2013/07/firefox-search.png" alt="Firefox Search" /></p> <hr /> <p><a href="https://addons.mozilla.org/de/firefox/addon/firegestures"><strong>FireGestures</strong></a> um die Mausgesten aus Opera nachzubilden. Die verschiedenen Maus&shy;gesten lassen sich zudem frei einrichten und so habe ich auch ein paar Bewegungen, die ich von Opera gewohnt war, wieder so eingestellt.</p> <hr /> <p><a href="https://addons.mozilla.org/de/firefox/addon/fxchrome/"><strong>FXChrome</strong></a> bietet einen sehr sauberen, Chrome-ähnlichen Look. Es ist so ein typisch minimalistisches Design, wie ich es liebe.</p> <hr /> <p><a href="https://addons.mozilla.org/de/firefox/addon/greasemonkey/"><strong>Greasemonkey</strong></a> für Userscripts. Insofern man überhaupt Unterstützung für Userscripts braucht. Opera 12 hatte es build-in und so geht es mit Firefox.</p> <hr /> <p><a href="https://www.eff.org/https-everywhere"><strong>HTTPS Everywhere</strong></a>, damit von mehr Seiten oder auch nur bestimmten Elementen auf Seiten (wie z.B. die Gravatars auf meinem Blog) die HTTPS-Variante verwendet wird, insofern vorhanden.</p> <hr /> <p><a href="https://addons.mozilla.org/de/firefox/addon/private-tab/"><strong>Private Tab</strong></a> für eben solche, da Firefox an sich nur Private Windows anbietet. Verstehe ich nicht so recht, aber nun gut, dem ließ sich ja leicht Abhilfe schaffen.</p> <hr /> <p><a href="https://addons.mozilla.org/de/firefox/addon/super-start/?src=search"><strong>Super Start</strong></a>, da das Speed Dial von Firefox so gar nicht meinen Wünschen entspricht. Vor allem nervt mich, dass dort immer die letzten besuchten Seiten auftauchen, es sei denn, ich belege alle Plätze.</p> <p>Die Vorschaubilder der Seiten haben allerdings erst einmal Kartoffel-Aufnahmen-Qualität. In <code>about:config</code> den Wert von <code>extensions.superstart.site.snapshot.width</code> auf <code>512</code> zu erhöhen verbessert dies deutlich.</p> <hr /> <p><a href="https://addons.mozilla.org/de/firefox/addon/yesscript/"><strong>YesScript</strong></a>, um gezielt Seiten die Ausführung von JavaScript zu untersagen. Also ein Blacklist-Verfahren – im Gegensatz zu <a href="https://addons.mozilla.org/de/firefox/addon/noscript/"><em>NoScript</em></a>, das eine Whitelist führt.</p> <h3>Fein-Einstellungen</h3> <p><strong>Letzter Tab soll nicht den ganzen Browser schließen:</strong> In <code>about:config</code> muss der Wert für <code>browser.tabs.closeWindowWithLastTab</code> auf <code>false</code> geändert werden.</p> <p>Hat noch jemand Vorschläge für nützliche Addons oder Einstellungen?</p> PGP Public Key meiner Mail-Adresse Tue, 11 Jun 2013 17:59:00 +0200 https://sebadorn.de/2013/06/11/pgp-public-key-fuer-meine-email-adresse https://sebadorn.de/2013/06/11/pgp-public-key-fuer-meine-email-adresse Seba Informatics <p>In Anbetracht <a href="https://netzpolitik.org/2013/prism-amerikanischer-geheimdienst-nsa-hat-direkten-zugriff-auf-alle-daten-der-grosen-internet-unternehmen/">aktueller Ereignisse</a> habe ich mir das Thema PGP/Mail-Verschlüsselung noch einmal angeschaut und möchte die Möglichkeit einfach mal all jenen, die meine Mail-Adresse haben, anbieten. Die Einrichtung ging mit dieser <a href="https://support.mozillamessaging.com/en-US/kb/digitally-signing-and-encrypting-messages">Anleitung für Thunderbird</a> recht schnell und unkompliziert vonstatten.</p> <p><strong><a href="http://sebadorn.de/public_keys/seba_dorn@gmx.de.asc">PGP Public Key für meine GMX-Adresse</a></strong> – es bediene sich, wer möchte. Für meine anderen Adressen kann ich das auch noch nachholen, falls Bedarf besteht.</p> <p>Wer es sich auch einrichtet: Bloß nicht so schusselig sein und den <em>Private</em> Key irgendwo in die Cloud (DropBox etc.) legen; sonst kann man sich das Verfahren auch gleich sparen.</p> <p>Ansonsten: Danke und viel Glück, Herr Snowden. Hoffentlich wird das mit dem Asyl in Island was.</p> <hr /> <p><strong>[Update 2013-06-13]</strong> Machen wir das doch so: <a href="http://sebadorn.de/public_keys/">sebadorn.de/public_keys</a></p> Opera Extensions und JavaScript-Whitelisting Sun, 21 Apr 2013 00:02:00 +0200 https://sebadorn.de/2013/04/21/opera-extensions-und-javascript-whitelisting https://sebadorn.de/2013/04/21/opera-extensions-und-javascript-whitelisting Seba Informatics <h3>JavaScript-Whitelisting</h3> <p>Gemeint ist, wenn man JavaScript erst im Browser deaktiviert und dann nur für jede gewünschte Seite gesondert aktiviert. Man führt also eine Whitelist für Seiten, die JavaScript ausführen dürfen. Warum kann das eine gute Idee sein?</p> <ul> <li><strong>Sicherheit und geschützte Privatsphäre:</strong> Böse Dinge wie dynamisches Nachladen von schädlichen Inhalten von Fremd-Servern wird erschwert. Das allgegenwärtige Tracking durch hauptsächlich Werbefirmen (das beinhaltet auch und gerade Google) wird nahezu unmöglich.</li> <li><strong>Internet im Light-Modus:</strong> Kein Popup, das nach Facebook-Likes oder Twitter-Gefollowere bettelt; keine nervige Bottom-Menubar, die aufklappt, und überhaupt: 99% weniger Werbung.</li> </ul> <h3>Das Problem mit Opera</h3> <p>Deaktiviert man global JavaScript, funktionieren Extensions nicht mehr, selbst wenn die besuchte Seite auf der Whitelist steht. Konkret ist das Problem, dass keine Content-Skripte mehr in die besuchte Seite injiziert werden – ein elementärer Bestandteil der meisten Extensions.</p> <p>Der Witz an der Sache ist, dass Userscripts – diese einzelnen JavaScript-Dateien, die auf .user.js enden – weiterhin funktionieren, denn dafür gibt es sogar eine extra Einstellung. Auch die Opera Developer Toolbar „Dragonfly“ funktioniert weiterhin.</p> <p>Also: Warum nicht auch Extensions?</p> <h3>Workaround</h3> <ol> <li><em>Aktiviere</em> JavaScript global.</li> <li>Füge eine Site Preference hinzu für die Adresse „<code>*</code>“ und <em>deaktiviere</em> für diese JavaScript. Dabei handelt es sich um eine Wildcard, die alle Seiten betrifft – besucht und unbesucht.</li> <li>Füge eine Site Preference hinzu für die Adresse „<code>wuid-*</code>“ und <em>aktiviere</em> für diese JavaScript. Dies ist für die Extensions, damit z.B. das Background-Skript und die Optionen-Seite funktionieren.</li> </ol> <p>Ab sofort laufen auf jeder Seite, die auf der Whitelist steht, auch die Extensions.</p> Browser-Addons: Nachrichten-Broadcast an alle Tabs Mon, 25 Mar 2013 17:25:00 +0100 https://sebadorn.de/2013/03/25/browser-addons-nachrichten-broadcast-an-alle-tabs https://sebadorn.de/2013/03/25/browser-addons-nachrichten-broadcast-an-alle-tabs Seba Informatics <p class="illu"><img style="width: 600px; height: 400px;" src="//sebadorn.de/media/2013/03/3-browser-broadcast.png" alt="Browser: Chrome, Firefox, Opera" /></p> <h3>Situation</h3> <p>Meine Extension injiziert auf bestimmten Seiten ein Content Script. Man kann dann auf dieser veränderten Seite auch Einstellungen ändern, die im der Extension zugewiesenen Storage gespeichert wird. Dafür wird wie üblich eine Nachricht mit den Änderungen an den Hintergrundsprozess geschickt.</p> <h3>Problem</h3> <p>Angenommen, ich habe zwei oder mehr Tabs offen. Nun verändere ich die Einstellung A in einem dieser Tabs. Die Änderung wird gespeichert, aber alle anderen Tabs zeigen nach wie vor den alten Zustand an. Erst nach einem Neuladen der Seite wird die Änderung sichtbar.</p> <p>Was wir wollen, ist eine Synchronisation der Tabs, was bedeutet, wir müssen alle im Hintergrund eintreffenden Änderungen allen anderen Tabs ebenfalls mitteilen. Und das geht so …</p> <!--more--> <h3>Lösung: Opera</h3> <pre class="brush: js">opera.extension.broadcastMessage( msg );</pre> <p>Eeyup, das war es.</p> <h3>Lösung: Chrome</h3> <p>In Chrome müssen wir uns die IDs der Tabs merken, die unser Content Script beinhalten. Dafür sendet besagtes Content Script zuallererst immer eine Nachricht an den Hintergrund, damit wir uns seine Tab-ID merken können. Wird der Tab geschlossen, verwerfen wir die Tab-ID wieder.</p> <pre class="brush: js">var openTabs = []; chrome.extension.onMessage.addListener( handler ); function handler( e, sender, sendResponse ) { if( e.data.msg == "content script loaded" ) { openTabs.push( sender.tab.id ); chrome.tabs.onRemoved.addListener( onTabRemove ); } ... }; function onTabRemove( tabId, info ) { var idx = openTabs.indexOf( tabId ); if( idx >= 0 ) { openTabs.splice( idx, 1 ); } };</pre> <p>Ein Broadcast an alle relevanten Tabs sieht dann wie folgt aus.</p> <pre class="brush: js">for( var i = 0; i < openTabs.length; i++ ) { chrome.tabs.sendMessage( openTabs[i], msg, myCallbackFunction ); }</pre> <h3>Lösung: Firefox</h3> <p>In Firefox gehen wir ähnlich vor wie in Chrome und merken uns alle Tabs, die unser Content Script inne haben, in der Variable <code>workers</code>.</p> <pre class="brush: js">var self = require( "self" ); var pageMod = require( "page-mod" ); var workers = []; pageMod.PageMod( { include: "*.mozilla.org", attachTo: ["existing", "top"], contentScriptWhen: "ready", contentScriptFile: [self.data.url( "myContentScript.js" )], onAttach: handleOnAttach } ); function handleOnAttach( worker ) { workers.push( worker ); worker.on( "detach", function() { forgetWorker( this ); } ); }; function forgetWorker( worker ) { var idx = workers.indexOf( worker ); if( idx >= 0 ) { workers.splice( idx, 1 ); } };</pre> <p>Unser Broadcast funktioniert dann wie folgt.</p> <pre class="brush: js">for( var i = 0; i < workers.length; i++ ) { workers[i].postMessage( msg ); }</pre> <hr /> <p class="annotation"><a href="http://dev.opera.com/articles/view/opera-extensions-messaging/">Opera extensions: messaging</a></p> <p class="annotation"><a href="http://developer.chrome.com/extensions/tabs.html">chrome extensions: chrome.tabs</a></p> <p class="annotation"><a href="https://addons.mozilla.org/en-US/developers/docs/sdk/latest/modules/sdk/page-mod.html">Firefox SDK: page-mod</a></p> Add-ons für Chrome, Firefox und Opera selbst hosten Wed, 13 Mar 2013 18:14:00 +0100 https://sebadorn.de/2013/03/13/addons-fuer-chrome-firefox-und-opera-selbst-hosten https://sebadorn.de/2013/03/13/addons-fuer-chrome-firefox-und-opera-selbst-hosten Seba Informatics <p class="illu"><img style="width: 600px; height: 400px;" src="//sebadorn.de/media/2013/02/3-browser.png" alt="3 Browser" /></p> <p>Ich habe eine Browser-Extension für Chrome, Firefox und Opera erstellt. Alle Dateien liegen bei mir lokal, ich habe also z.B. nicht Firefoxs <a href="https://builder.addons.mozilla.org/">Add-on Builder</a> verwendet, sondern nur <a href="https://addons.mozilla.org/en-us/developers/builder">das SDK</a>. Als die Extension fertig war, wollte ich sie auch selbst hosten. Ich meine, wie schwer kann das schon sein? Ja, nun …</p> <p>In diesem Artikel gehen wir davon aus, dass sich die Browser-spezifischen Extension-Dateien in den Unterverzeichnissen <code>Chrome/</code>, <code>Firefox/</code> und <code>Opera/</code> befinden.</p> <!--more--> <p>Der Artikel ist lang geworden. Hier sind ein paar Sprungmarken:</p> <ul> <li><a href="#extension-hosting-opera">Hosting und Updates mit Opera</a></li> <li><a href="#extension-hosting-chrome">Hosting und Updates mit Chrome</a></li> <li><a href="#extension-hosting-firefox">Hosting und Updates mit Firefox</a></li> </ul> <hr /> <h3 id="extension-hosting-opera">Opera</h3> <p>In Operas <code>config.xml</code> muss ein Verweis auf die XML-Datei auf dem hostenden Server stehen, die später nach Updates gefragt wird.</p> <pre class="brush: xml">&lt;widget …&gt; … &lt;update-description href="http://example.org/updates-opera.xml" /&gt; &lt;/widget&gt;</pre> <p>Dann packen wir die Extension ein. Dafür wird einfach alles in ein ZIP-Archiv mit der Datei-Endung OEX gepackt.</p> <pre class="brush: bash">cd Opera/ zip -r MyExtension.oex *</pre> <p>Die XML-Datei für Updates <code>updates-opera.xml</code> beinhaltet lediglich einen Pfad zur Extension, sowie die aktuelle Versionsnummer dieser.</p> <pre class="brush: xml">&lt;update-info xmlns="http://www.w3.org/ns/widgets" src="http://example.org/MyExtension.oex" version="1.1"&gt;&lt;/update-info&gt;</pre> <p>Das war es auch schon. Ein Hindernis ist jedoch, dass die eigene Domain erst von den Benutzern als „Trusted Website“ in Opera eingetragen werden muss – <a href="http://sebadorn.de/mlp/mle/mle_install_opera.png">wie auf diesem Screenshot zu sehen.</a></p> <hr /> <h3 id="extension-hosting-chrome">Chrome</h3> <p>In <code>manifest.json</code> wird für Updates ein zusätzlicher Eintrag benötigt.</p> <pre class="brush: js">{ … "update_url": "http://example.org/updates-chrome.xml" }</pre> <p>Wird die Extension <strong>zum ersten Mal verpackt</strong>, lautet der Befehl:</p> <pre class="brush: bash">google-chrome --pack-extension=Chrome/</pre> <p>Dabei wird ein Private Key erzeugt und hier mit dem Namen <code>Chrome.pem</code> abgelegt. <em>Diese Datei sollte geheim bleiben – also nicht einfach in ein öffentliches Repository hochladen!</em></p> <p>Bei jedem späteren Mal, dass die Extension verpackt wird, lautet der Befehl dann:</p> <pre class="brush: bash">google-chrome --pack-extension=Chrome/ --pack-extension-key=Chrome.pem</pre> <p>In der XML-Datei <code>updates-chrome.xml</code> steht die ID der Extension, der Pfad zur Installations-Datei und die aktuelle Versionsnummer. Die ID erfährt man von der Chrome-Erweiterungs-Seite (<code>chrome://extensions</code>) – dafür muss man ein Häkchen bei „Developer Mode“ setzen. Diese ID ändert sich nicht mehr, auch nicht mit späteren Updates.</p> <pre class="brush: xml">&lt;?xml version="1.0" encoding="UTF-8"?&gt; &lt;gupdate xmlns="http://www.google.com/update2/response" protocol="2.0"&gt; &lt;app appid="replace-this-with-your-extension-id"&gt; &lt;updatecheck codebase="http://example.org/MyExtension.crx" version="1.1" /&gt; &lt;/app&gt; &lt;/gupdate&gt;</pre> <p>Ein Problem bei der Installation ist, dass dies nicht direkt über die Website erfolgen kann. Die Extension muss vom Benutzer heruntergeladen und dann auf die Chrome-Erweiterungs-Seite (<code>chrome://extensions</code>) <a href="http://sebadorn.de/mlp/mle/mle_install_chrome.png">gezogen werden (Drag&amp;Drop)</a>. Updates funktionieren dann jedoch ganz normal.</p> <hr /> <h3 id="extension-hosting-firefox">Firefox</h3> <p>Ich sage es vorne weg: Firefox hat mich ordentlich angepisst. Mir ist klar, dass alles im Sinne von Sicherheit geschieht und durchaus sinnvoll ist. Aber aus Entwicklersicht war es eine Qual überhaupt erst einmal herauszufinden, wie vorzugehen ist. Erschwerend kam hinzu, dass ich kein SSL verwende. Dafür ist die Firefox-Extension die einzige, die für eine Installation keine größere Mitarbeit seitens des Benutzers erfordert.</p> <p>Zunächst wird die Extension mit dem cfx tool aus <a href="https://addons.mozilla.org/en-us/developers/builder">dem SDK</a> verpackt.</p> <pre class="brush: bash">CFX=~/.firefox-addon-sdk-1.13.2/bin/cfx cd Firefox/ $CFX xpi --update-url http://example.org/updates-firefox.rdf</pre> <p>Nun müssen wir allerdings das für das XPI erstellte <code>install.rdf</code> bearbeiten. Also extrahieren wir diesen Teil.</p> <pre class="brush: bash">unzip MyExtension.xpi install.rdf</pre> <p>Nun benötigen wir ein weiteres Tool, genannt McCoy. Dafür gibt es eine <a href="https://developer.mozilla.org/en-US/docs/McCoy">grafische Variante von Mozilla</a> selbst, oder eine <a href="http://blog.techno-barje.fr//post/2009/10/05/Mozilla-Mccoy-tool-from-the-command-line/">gepatchte Version, die man über das Terminal</a> verwenden kann, was ich hier mache.</p> <p>Zuerst erstellen wir für unsere Extension einen Schlüssel. Ähnlich wie bei Chrome, wird dies <strong>nur einmalig</strong> gemacht und dann immer wieder verwendet.</p> <pre class="brush: bash">MCCOY=~/.mccoy/mccoy $MCCOY -createKey "ThinkOfSomeKey"</pre> <p>Als nächstes fügen wir unseren Public Key in das <code>install.rdf</code> ein und packen es wieder in das XPI.</p> <pre class="brush: bash">$MCCOY -installRDF install.rdf -key "ThinkOfSomeKey" zip -f MyExtension.xpi install.rdf</pre> <p>Nun bilden wir einen Hash vom XPI, da wir diesen noch brauchen werden. Welches Hashing-Verfahren eingesetzt wird, ist relativ egal. Ich habe mich für <em>sha256</em> entschieden.</p> <pre class="brush: bash">XPI_HASH=$(sha256sum MyExtension.xpi | sed "s/ .*//g" -)</pre> <p>Gratuliere, wir sind ungefähr bei der Hälfte! Schauen wir uns jetzt etwas Leichtes an: Den Button auf der Website, um den Installations-Prozess einzuleiten. Dafür haben wir eine HTML-Seite mit einem Element, das die ID <code>trigger_ff</code> hat. Stehe das Nachfolgende nun in einer JS-Datei namens <code>foo.js</code>:</p> <pre class="brush: js">var trigger = document.getElementById( "trigger_ff" ); trigger.addEventListener( "click", function( e ) { e.preventDefault(); var params = { "MyExtension": { URL: "http://example.org/MyExtension.xpi", IconURL: "http://example.org/MyIcon_32x32.png", Hash: "sha256:%XPI_HASH%", toString: function() { return this.URL; } } }; InstallTrigger.install( params ); }, false );</pre> <p>Den Hash können wir beispielsweise so einfügen:</p> <pre class="brush: bash">sed -i "s;%XPI_HASH%;sha256:$XPI_HASH;g" foo.js</pre> <p>Als nächstes widmen wir uns der RDF-Datei für Updates. Erstelle eine Datei <code>updates-firefox.rdf</code>.</p> <pre class="brush: xml">&lt;?xml version="1.0" encoding="utf-8"?&gt; &lt;RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#"&gt; &lt;Description about="urn:mozilla:extension:MyExtension@example.org"&gt; &lt;em:updates&gt; &lt;Seq&gt; &lt;li&gt; &lt;Description&gt; &lt;em:version&gt;1.1&lt;/em:version&gt; &lt;em:targetApplication&gt; &lt;Description&gt; &lt;em:id&gt;{ec8030f7-c20a-464f-9b0e-13a3a9e97384}&lt;/em:id&gt; &lt;em:minVersion&gt;18.0&lt;/em:minVersion&gt; &lt;em:maxVersion&gt;19.*&lt;/em:maxVersion&gt; &lt;em:updateLink&gt;http://example.org/MyExtension.xpi&lt;/em:updateLink&gt; &lt;em:updateHash&gt;%XPI_HASH%&lt;/em:updateHash&gt; &lt;/Description&gt; &lt;/em:targetApplication&gt; &lt;/Description&gt; &lt;/li&gt; &lt;/Seq&gt; &lt;/em:updates&gt; &lt;/Description&gt; &lt;/RDF&gt;</pre> <p>Die ID <code>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</code> wird nicht verändert, denn hierbei handelt es sich um die ID für Firefox – es wird gesagt, dass dies eine Extension für Firefox ist und z.B. nicht für Thunderbird.</p> <p>Klickt der Benutzer den Installations-Button, erfährt Firefox den Hash und überprüft, ob die zu installierende Datei auch die ist, die hier angepriesen wurde. Dieser Vergleich findet auch bei Updates statt. Daher müssen wir auch hier den Platzhalter <code>%XPI_HASH%</code> ersetzen.</p> <pre class="brush: bash">sed -i "s;%XPI_HASH%;sha256:$XPI_HASH;g" updates-firefox.rdf</pre> <p><em><strong>Hinweis:</strong> Der Hash muss für jede neue Version erneut errechnet und eingefügt werden!</em></p> <p>Nun kommt wieder McCoy zum Einsatz, indem wir in das Update-RDF unsere Signatur einfügen.</p> <pre class="brush: bash">$MCCOY -signRDF updates-firefox.rdf -key "ThinkOfSomeKey"</pre> <p>Wenn man möchte, kann man das erzeugte RDF auch noch überprüfen lassen.</p> <pre class="brush: bash">$MCCOY -verifyRDF updates-firefox.rdf -key "ThinkOfSomeKey"</pre> <p>Geschafft.</p> <hr /> <h3>Relevantes</h3> <p>Wer mein Build-Skript und Templates für die XML/RDF/JS-Dateien sehen möchte, findet diese <a href="https://github.com/sebadorn/MLE/blob/master/build.sh">in meinem GitHub-Repository</a>.</p> <p>Diese Tutorials und Dokumentation könnten von Interesse sein:</p> <ul> <li><a href="http://dev.opera.com/addons/extensions/">Opera Extensions: Documentation</a></li> <li><a href="http://developer.chrome.com/extensions/packaging.html">Chrome Extensions: Packaging</a></li> <li><a href="https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/">Firefox SDK Documentation</a></li> <li><a href="https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/tutorials/getting-started-with-cfx.html">Firefox SDK: Getting Started with cfx</a></li> <li><a href="https://developer.mozilla.org/en-US/docs/Installing_Extensions_and_Themes_From_Web_Pages">Firefox: Installing Extensions and Themes From Web Pages</a></li> </ul> Mozilla Dev Derby: Pete’s Adventure Fri, 01 Mar 2013 21:29:00 +0100 https://sebadorn.de/2013/03/01/mozilla-dev-derby-petes-adventure https://sebadorn.de/2013/03/01/mozilla-dev-derby-petes-adventure Seba Informatics <p class="illu"><img style="width: 600px; height: 420px;" src="//sebadorn.de/media/2013/03/petes-adventure.png" alt="Pete’s Adventure" /></p> <p>Im Januar habe ich zum zweiten Mal bei der <a href="https://developer.mozilla.org/en-US/demos/devderby">Mozilla Dev Derby</a> einen Beitrag eingereicht. Das Thema lautete diesmal <strong>Drag&amp;Drop</strong> und da dachte ich mir, dass sich doch etwas Hübsches finden lassen müsste. Eine erste Idee, auf die ich mich ziemlich versteift hatte, gab ich später doch wieder auf – ich hätte meinen eigenen Ansprüchen nicht Genüge tun können. Von mir stammt nun <a href="https://developer.mozilla.org/ms/demos/detail/petes-adventure">Pete's Adventure</a> (<a href="https://sebadorn.de/devderby/dragndrop/">Mirror</a>), eine kurze interaktive Geschichte, mit der ich doch sehr zufrieden bin. <a href="https://github.com/sebadorn/Mozilla-Dev-Derby--Drag-and-Drop">Code und Ressourcen sind auf GitHub zu finden.</a></p> <h3>Drag&amp;Drop</h3> <p>Drag&amp;Drop mit JavaScript ist nicht schwer. Ein paar wenige (dumme) Besonderheiten muss man nur kennen. Zum Beispiel, dass es in Firefox nur funktioniert, wenn auch irgendwelche Daten gesetzt sind. Ein leerer String reicht da schon:</p> <pre class="brush: js">var note = document.getElementById( "note" ); note.addEventListener( "dragstart", function( e ) { e.dataTransfer.setData( "text/plain", "" ); }, false );</pre> <p>Ein Drop wird zudem nur erkannt, wenn man die Events <code>dragenter</code> und <code>dragover</code> für das Element stoppt:</p> <pre class="brush: js">function stop( e ) { e.preventDefault(); } var dropzone = document.getElementById( "dropzone" ); dropzone.addEventListener( "dragenter", stop, false ); dropzone.addEventListener( "dragover", stop, false ); dropzone.addEventListener( "drop", doSomething, false );</pre> <p>Das dürften auch schon die größten Stolpersteine gewesen sein. Nun warte ich weiter auf das Ergebnis des Wettbewerbs, das eigentlich sehr bald verkündet werden müsste.</p> <hr /> <p><strong>Update 2013-03-02:</strong> Ha, nächster Tag und das Ergebnis ist da. <a href="https://hacks.mozilla.org/2013/03/announcing-the-winners-of-the-january-2013-dev-derby/">Erster Platz!</a></p> <hr /> BOINC – CPU-Zeit spenden für die Wissenschaft Sun, 28 Oct 2012 18:35:00 +0100 https://sebadorn.de/2012/10/28/boinc-cpu-zeit-spenden-fuer-die-wissenschaft https://sebadorn.de/2012/10/28/boinc-cpu-zeit-spenden-fuer-die-wissenschaft Seba Informatics <p class="illu"><img style="width: 600px; height: 251px;" src="//sebadorn.de/media/2012/10/boinc_1.png" alt="BOINC logo" /></p> <p>Nicht jeder wissenschaftlichen Forschungsgruppe steht ein Super-Rechner zur Ver&shy;fügung. Eine Möglichkeit, dennoch aufwendige Simulationen durchführen zu lassen, ist die <strong>Berkeley Open Infrastructure for Network Computing</strong> – kurz BOINC. Das Projekt basiert dabei auf freiwilliger Unterstützung durch zumeist Privatpersonen, die ihre Rechenleistung zur Verfügung stellen. Die installierten Clients erhalten Arbeitspakete, berechnen diese und senden ihre Ergebnisse zurück an das Projekt-Backend.</p> <h3>Client</h3> <p class="illu"><img style="width: 600px; height: 330px;" src="//sebadorn.de/media/2012/10/boinc_2.jpg" alt="BOINC client" /></p> <p>Der Client existiert sowohl für Windows, Mac als auch Linux – nicht zuletzt dank Freigabe des Quellcodes in 2003. Ziel des Clients ist es, <em>ungenutzte</em> CPU-Zeit zu verwenden: Sollte man gerade nur Musik hören, im Internet surfen oder andere unstrapaziöse Dinge erle&shy;digen. In den Optionen lässt sich auch einstellen, ob nur zu bestimmten Tageszeiten ge&shy;rechnet werden soll und ob ein CPU-Auslastungslimit zu beachten ist. Natürlich kann man das Programm auch einfach von Hand starten und beenden – was meine Vorgehens&shy;weise ist.</p> <p><strong>Bedenken</strong> sollte man allerdings, dass gerade der Dauerbetrieb eine nahezu durch&shy;gängige Belastung der CPU bedeutet – und damit höherer Stromverbrauch und Wärme&shy;entwicklung. Fängt plötzlich der Lüfter an aufzudrehen, sollte man es vielleicht doch lieber sein lassen oder zumindest einen Blick auf die Temperaturen haben.</p> <h3>Teilnehmen</h3> <ul> <li>Alle relevanten Informationen finden sich auf der offiziellen BOINC-Seite: <a href="https://boinc.berkeley.edu/">https://boinc.berkeley.edu/</a>.</li> <li>Dort sind ebenfalls zahlreiche <a href="https://boinc.berkeley.edu/projects.php">verfügbare Projekte</a> aus verschiedenen Forschungsgebieten aufgelistet.</li> <li>Ich selbst nehme bisher nur am <a href="http://www.worldcommunitygrid.org">World Community Grid</a> teil.</li> </ul> Der erste Selbstbau-PC Sun, 19 Aug 2012 19:56:00 +0200 https://sebadorn.de/2012/08/19/der-erste-selbstbau-pc https://sebadorn.de/2012/08/19/der-erste-selbstbau-pc Seba Informatics <p class="illu"><img style="width: 600px; height: 360px;" src="https://sebadorn.de/media/2012/08/pc.png" alt="PC" /></p> <p>Lang hat es gedauert. Endlich habe ich einen neuen PC. Nachdem ich zweimal Fertigsysteme zurückgeschickt habe, entschloss ich mich, selbst Hand anzulegen. Dazu musste ich mich nur erst einmal informieren, da meine Hardware-Kenntnisse nur knapp über 0 schwebten. So habe ich mich durch Reviews, <a href="http://www.computerbase.de/forum/showthread.php?t=215394">Empfehlungen</a> und <a href="http://tinyurl.com/falconguide">Info-Grafiken</a> gearbeitet und mir <a href="http://www.youtube.com/watch?v=uUY0tP5jYIo">ein zwei&shy;stün&shy;diges Video angeschaut</a>, wie man die Teile dann zusammenbaut.</p> <!--more--> <p>Schließlich kam der große Moment: Eingeschaltet und das <a href="http://de.wikipedia.org/wiki/Extensible_Firmware_Interface">EFI</a> lud, wunderbar! Die Festplatte gab Klick&shy;laute von sich und wurde nicht erkannt. Reklamation und weiter ging es einige Tage später.</p> <p>Neue Festplatte ist da. Wenig euphemistisch baute ich sie ein, doch – Tada! – sie funktioniert. Also konnten die Betriebssysteme und Software folgen. Als ich dann Spiele ausprobierte, stutzte ich. Die Grafikkarte kam auf 60°C beim Browsen und 90°C (Lüfter auf 90%) nach 5&nbsp;Minuten Spiel (Minecraft in "Balanced"-Modus und Bastion). Also Reklamation und weiter ging es eine Woche später. Zu diesem Zeitpunkt war ich extrem angepisst; beste <em>Ich-habe-keinen-Bock-mehr-auf-den-Scheiß!</em>-Stimmung.</p> <p>Vor dem Einbau schaute ich die Ersatz-Grafikkarte noch einmal streng an. Ein Kabel hing leicht in den Raum zwischen Karte und Lüfter. Entweder hätte es den Lüfter blockiert oder es wäre mit der Zeit kaputtgeschabt worden. Mit ein wenig Kabelbinder war dem zum Glück leicht beizukommen. Die Temperaturen liegen nun in einem guten Bereich<sup>[1]</sup>.</p> <h3>Eckdaten (grob)</h3> <p>Weder habe ich vor zu overclocken, noch plane ich Grafikkarten in SLI zu verwenden. Aus&shy;reich&shy;ender Support von Linux ist zudem ein Muss.</p> <ul> <li><strong>Motherboard:</strong> ASRock Z68 Extreme3 Gen3</li> <li><strong>CPU:</strong> i5 2400</li> <li><strong>GPU:</strong> GTX 560 Ti</li> <li><strong>RAM:</strong> 8GB 1333MHz</li> <li><strong>HDD:</strong> WD Caviar Black 500GB</li> </ul> <h3>Gelerntes</h3> <p>Man kann CPUs <strong>undervolten</strong>. Weniger Saft bedeutet weniger Wärme. Für die Hardware be&shy;steht dabei angeblich kein Risiko und schlimmstenfalls schmiert der PC unter Last ab. Dann setzt man die Einstellung einfach wieder eine Stufe zurück. Ich bin jetzt bei einem Offset von -1.125V, was mir ca. -3°C bringt.</p> <p>Man kann <strong>Arbeitsspeicher übertakten</strong> und es gibt Modelle mit Heatsinks für bessere Wärmeableitung. Ich bezweifel ganz ehrlich, dass man außerhalb von Benchmarks eine Verbesserung beobachten kann.</p> <p>Es gibt <a href="http://www.amazon.de/s/ref=nb_ss_w/303-8157015-2701015?__mk_de_DE=%C5M%C5Z%D5%D1&url=search-alias%3Daps&field-keywords=gtx+690&x=0&y=0">Grafikkarten, die teurer sind</a> als der PC, den ich mir nun zusammengestellt habe. Und es gibt Leute, die kaufen sich gleich zwei und betreiben sie in SLI.</p> <h3>Fazit</h3> <p>Ganz so leise wie mein alter ist er nicht. Damit habe ich auch nie gerechnet, schon allein weil im alten die Grafikkarte noch passiv gekühlt war und der neue über zwei Gehäuselüfter ver&shy;fügt. Aber ich habe mich schnell an das leichte Summen gewöhnt – für das ich erst einmal die Lüfter-Geschwindigkeiten herunterdrehen musste, die standard&shy;mäßig auf „Full On“ standen.</p> <p>Wirklich gespart habe ich durch das Zusammenbauen nicht. Dafür steckt das drin, was ich wollte. Und als Informatiker habe ich auch auf diesem Gebiet endlich mal ein wenig Erfahrung gesammelt.</p> <hr /> <p class="annotation">[1] Ungefähr 50 bis 60°C bei 40 bis 50% Lüftergeschwindigkeit nach knapp 1&nbsp;Stunde Minecraft/Bastion. Wirklich höher ging es danach nicht mehr. Im Normalbetrieb sind es recht konstant 42°C bei 30%.</p> <p class="annotation">Einbau der CPU: <em>„Habe ich die CPU richtig herum draufgelegt? Nochmal nachschauen. Ja, okay. Dann schließe ich jetzt … Liegt die wirklich richtig drauf? Ah, okay, war richtig. Das lässt sich aber schwer schließen. Ohje&shy;ohje&shy;ohje&shy;ohje… Okay, ist zu.“</em></p> <p class="annotation">Festschrauben des Motherboards: <em>„Und jetzt hier die … Fuck, mir ist die Schraube runtergefallen auf das Board! Hoffentlich hat das nichts zerkratzt! Jetzt ganz vorsichtig aufsammeln … komm her, du! Gah. Gah! Okay.“</em></p>