• Advertisement
  • entries
    18
  • comments
    35
  • views
    29241

About this blog

Toasted mystery box

Entries in this blog

Beep boop, making progress on my FM synthesizer. Had a nasty popping sound with my phase-feedback logic and needed some way to rapidly graph out the logic. Powershell to the rescue:
 

# plot-sample.ps1
$samples = 1024;
$width   = 2048;
$height  = 512;

[Reflection.Assembly]::LoadWithPartialName("System.Drawing");

$bmp = new-object Drawing.Bitmap $width, $height;
$g = [Drawing.Graphics]::FromImage($bmp);
$g.SmoothingMode = "AntiAlias";

$penAxis  = new-object Drawing.Pen 0xFF808080, 2;
$penLineA = new-object Drawing.Pen 0xFF0000F0, 2;
$penLineB = new-object Drawing.Pen 0xFFF00000, 2;
$ptsA     = new-object Drawing.PointF[] $samples;
$ptsB     = new-object Drawing.PointF[] $samples;

0..($samples-1) |% {
  $Q = 4*[Math]::pi/$samples;
  $feedback = 0;
} {
  $ampA = [Math]::sin($_*$Q + $feedback); # A[t] = sin(t + 1.05*A[t-1])
  $ampB = [Math]::sin($_*$Q);             # B[t] = sin(t) $feedback = $ampA * 1.05;

  $x = $bmp.Width * $_/$samples;
  $yA = $bmp.Height * (-$ampA/2.1 + .5);
  $yB = $bmp.Height * (-$ampB/2.1 + .5);
  $ptsA[$_] = new-object Drawing.PointF $x, $yA;
  $ptsB[$_] = new-object Drawing.PointF $x, $yB;
} {
  $g.Clear([Drawing.Color]::White);
  $g.DrawLine($penAxis,0,$bmp.Height/2,$bmp.Width,$bmp.Height/2);
  $g.DrawLines($penLineB,$ptsB);
  $g.DrawLines($penLineA,$ptsA);
  $bmp.Save("$pwd\plot.png");
}

plot.png
Looks like the logic should be sound. I did find that the phase-feedback wasn't being sin'd in the synthesizer, and that was causing the popping noise. And here's a sample of what it sounds like right now:
fm-sample.ogg (36.4KB)

And here's the instrument used to generate that sound:

/*
unit: 
	mul   frequency multiplier
	ofs   additive frequency
	amp   volume
	feed  phase feedback

envelope: 
	A     attack rate, in degrees
	D1    decay rate to sustain level, in degrees
	D2    decay rate to silence, in degrees
	R     release rate, in degrees
	S     sustain level

program: 
	u     unit to render
	in    phase buffer input slot, 0 for null input
	out   phase buffer output slot, 0 for audio data
*/

instrument i;
int idx = 0;
//                 mul    ofs    amp    feed    A     D1    D2    R     S
i.units[idx++] = { 2.00f, 0.00f, 0.50f, 1.00f, {80.0, 20.0, 10.0, 45.0, 0.75 }};
i.units[idx++] = { 1.00f, 0.00f, 0.25f, 0.00f, {85.0, 45.0,  5.0, 60.0, 0.50 }};
i.units[idx++] = { 7.00f, 0.33f, 0.12f, 0.00f, {88.5, 60.0,  5.0, 60.0, 0.25 }};
i.units[idx++] = { 1.00f, 0.33f, 0.50f, 0.00f, {87.0, 30.0,  5.0, 60.0, 0.67 }};
i.ct_units = idx;


// program:
// ┌─┐   ┌─┐
// │0├──►│1├──┐
// └─┘   └─┘  │   ┌─┐
//            ├──►│3├──►out
//       ┌─┐  │   └─┘
//       │2├──┘
//       └─┘

idx = 0; 
//                   u  in out
i.program[idx++] = { 0, 0, 2 };
i.program[idx++] = { 1, 2, 1 };
i.program[idx++] = { 2, 0, 1 };
i.program[idx++] = { 3, 1, 0 };
i.ct_program = idx;

Hue.

UPDATE:
To fix the shortcuts disappearing form the start menu, I recommend removing and recreating the default shortcuts to Command Prompt and Windows PowerShell in your start menu before changing the permissions.  There is now a publicly available gist at:  https://gist.github.com/fastcall22/8f046b0e8e58284404427c413329a41c

 

Oh boy, it’s that time of year again where all of my terminal settings get reset!  Thanks, Windows 10 Fall Creators Update!

terminal-before.png.25dde56b4106a27529699bf2880dc529.png

 

But this time, I’m prepared !!
 

I’ve made some adjustments to my previous attempt to Unify Windows 10 Terminal Settings.  Namely, I’ve fixed the ACL not working as expected.  Apparently `set-acl` attempts to reset the owner, which is denied when also removing the write permission.  Additionally, the ACL needs to be reloaded in order to get the correct converted ACLs from the item’s parents.  The workaround is as follows:

# deny current user write permissions
$item = get-item $path;
$acl = $item.GetAccessControl('access');     # https://stackoverflow.com/a/6646551
$acl.SetAccessRuleProtection($true, $true);  # Disable inheritance, assume inherited
$item.SetAccessControl($acl);

$acl = $item.GetAccessControl('access');     # Reload ACL to obtain inherited permissions
$acl.RemoveAccessRuleAll($rule);             # Remove existing rules for the user
$acl.AddAccessRule($rule);
$item.SetAccessControl($acl);

 

I’ve also added back in the registry fix:

$console = (get-item HKCU:\).OpenSubKey("Console", $true)
$console.GetSubKeyNames() |% {
if ( $pscmdlet.ShouldProcess($_, "Remove Subkey") ) {
	$console.DeleteSubKey($_);
}

And after running:

remove-consoleprops -StartMenu -AdjustPermissions -Registry -ErrorAction Continue

… I was pleased to see that it worked as expected!

terminal-after.png.84f1fa24d11036602930716baa7ee1ac.png

I will be working toward publishing these scripts as a GitHub gist, so these files are versioned and others can contribute.

That’s all for now.  Thanks for reading; see you around!

:^)

 

Hello again! It’s been a while since my last post. I was playing around with color themes for PuTTY and the Windows Terminal, and I thought I’d share some of my recent findings on unifying the terminal experience in Windows. This is a continuation of a previous post I wrote some time ago. The previous post unfortunately did not survive the gamedev.net upgrade. I’ll detail how I have been iterating color palettes in a future post.

Unifying Windows 10 Terminal Settings

I spend most of my time at work and at home inside a terminal. I was very excited to use the new Windows 10’s terminal. It gave me reason to customize my terminal experience to match my workflow.

Unfortunately, this is apparently difficult.

It became apparent that over time, I could not reliably expect a certain look or behavior whenever I opened any terminal window. Programs would install and use their own shortcuts with their their own defaults. Windows updates would clear out my user defined settings. Launching the same program in a different way would launch an older version of my settings.

If I wanted to adjust these settings, I would have to hunt down and apply the change to every shortcut, every subkey. This is absolutely unacceptable.

And so, I set off to answer the question: How could I configure Windows to use only one set of settings? I believe I have this mostly worked out, but it’s not perfect. Let’s begin.

About the Terminal Settings in Windows

The Windows terminal settings can be stored in a number of different places, which makes unifying them all a huge pain. Understanding Windows Console Host Settings explains in great detail how this works. Here’s a quick rundown of where these settings are stored:

  1. HKCU:\Console, the default console settings. These can be accessed from the “defaults” menu item from any terminal’s application menu.

  2. HKCU:\Console\*, per–executable overrides using the executable path as subkey. If the terminal was launched from the run dialog, then these settings can be changed from the “properties” menu item from the terminal’s application menu.

  3. Shortcut files (*.lnk), where each shortcut may have an embedded `CONSOLE_PROPS` data block with its own terminal settings. If the terminal was launched from a shortcut, then these settings can be changed from the “properties” menu item from the terminal’s application menu. These settings can also be changed from the shortcut’s file properties dialog.

The good news is that according to the article, Microsoft plans to release a tool to manage console settings in the future. Until then, this can be used to fix the settings in the meantime.

Now, on to unifying all the settings. I advise to proceed with caution as making permission changes may cause unexpected errors. Make backups of your registry settings and shortcuts before proceeding and proceed at your own risk.

Normalizing All the Terminal Settings

The items listed above describe where we can find terminal settings, and with them we can apply a fix and write rules to prevent them from diverging in the future:

  1. Deleting the subkeys and denying your Windows user the ability to create keys in `HKCU:\Console`. This will prevent terminal programs launched from the run dialog from diverging from the master terminal settings.

  2. Removing the `CONSOLE_PROPS` from shortcut files. This causes those programs to use the default settings in `HKCU:\Console`, but inspecting the fonts/colors tab on the shortcut’s properties causes them to be re–added.

  3. Denying your Windows User write access to shortcut files prevents `CONSOLE_PROPS` from being re–added.

Disabling Registry–Level Settings

Removing the console subkeys is easy, but preventing them from being created in the future is a little trickier. This can be done by denying the “Create Subkey” permission on `HKCU:\Console`. Note: This may break some installers that expect to create a subkey here.

  1. On the advanced security settings for `HKCU:\Console` properties, disable inheritance.
  2. Add a permission entry for your Windows user that allows all advanced permissions except “Create subkey”.
  3. Add a permission entry for your Windows user that denies “Create subkey”.
  4. Check your Windows user on “Effective Access” and confirm that your Windows user cannot create a subkey.
  5. Check the “Administrator” user on the effective access tab and confirm that administrators can create subkeys.

The MSDN blog post mentions that launching a terminal from the run dialog should use the registry to store its settings, but I haven’t been able to reproduce. It is possible that the permission step is not needed.

Disabling Shortcut–Level Settings

This is the hard part. First, a bit about how shortcuts are indexed. Windows Search uses a few locations to source your Start Menu:

  1. %APPDATA%\Microsoft\Windows\Start Menu
  2. %APPDATA%\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar
  3. C:\Users\Default\AppData\Roaming\Microsoft\Windows\Start Menu
  4. C:\ProgramData\Microsoft\Windows\Start Menu

The shortcuts presented in the start menu are grouped by their `Target` shortcut property. If the search index or task bar found more than one shortcut with the same `Target`, then the first is selected, possibly in the order specified above. If they differ, then you will see two entries in your start menu or your task bar. The comparison is case-insensitive with variables unexpanded: `%SYSTEMROOT%\example.exe` is different from `C:\WINDOWS\example.exe`, and `C:\WINDOWS\example.exe` is the same as `C:\windows\example.exe`. All of them are different from `C:\Windows\sample.exe /ABC`.

Unlike the start menu, the shortcuts in the task bar are grouped by their `Target` property without arguments and with environment variables expanded.

My previous post recommended denying all authenticated users write access to the shortcut, which caused the indexer to skip the shortcut entirely after grouping, effectively removing the shortcut from your start menu search altogether. In this post, we’ll take a different approach.

First, a C# shim to handle COM interactions:

# space.wtfbox.win32.ShellLink.cs
using System;
using System.Runtime.InteropServices;

namespace space.wtfbox.win32 {
    [ ComImport()
    , InterfaceType(ComInterfaceType.InterfaceIsIUnknown)
    , Guid("0000010B-0000-0000-C000-000000000046") ]
    public interface IPersistFile {
        int GetClassID(out Guid pClassID);

        [PreserveSig()]
        int IsDirty();
        int Load([MarshalAs(UnmanagedType.LPWStr)] string pszFileName, int dwMode);
        int Save([MarshalAs(UnmanagedType.LPWStr)] string pszFileName, [MarshalAs(UnmanagedType.Bool)] bool fRemember);
        int SaveCompleted([MarshalAs(UnmanagedType.LPWStr)] string pszFileName);
        int GetCurFile(out IntPtr ppszFileName);
    }

    [ ComImport()
    , InterfaceType(ComInterfaceType.InterfaceIsIUnknown)
    , Guid("45E2B4AE-B1C3-11D0-B92F-00A0C90312E1") ]
    public interface IShellLinkDataList {
        void AddDataBlock(IntPtr pDataBlock);
        [PreserveSig()]
        int CopyDataBlock(uint dwSig, out IntPtr ppDataBlock);
        void RemoveDataBlock(uint dwSig);
        void GetFlags(out int dwFlags);
        void SetFlags(uint dwFlags);
    }

    // custom interface
    public class ShellLink : IDisposable {
        public ShellLink(object lnk) {
            _handle = (IPersistFile)lnk;
        }

        public ShellLink(object lnk, string path, int mode) {
            _handle = (IPersistFile)lnk;
            Load(path,mode);
        }

        public void Dispose() {
            Marshal.ReleaseComObject(_handle);
        }

        public void Load(string path, int mode){
            _handle.Load(path, mode);
        }

        public void RemoveDataBlock(uint signature) {
            (_handle as IShellLinkDataList).RemoveDataBlock(signature);
        }

        public bool HasDataBlock(uint signature) {
            IntPtr block;
            int hResult = (_handle as IShellLinkDataList).CopyDataBlock(signature,out block);
            if ( hResult != 0 ) {
                return false;
            }

            Marshal.FreeHGlobal(block);
            return true;
        }

        public int Save() {
            return _handle.Save(null,true);
        }

        private IPersistFile _handle;
        public const uint CONSOLE_PROPS = 0xA0000002;
    }
}

And an accompanying PowerShell cmdlet:

# Unify-ConsoleProps.ps
[CmdLetBinding(SupportsShouldProcess=$true)]
param(
	[Parameter(Mandatory=$false, ValueFromPipeline=$true)]
	[IO.FileInfo[]] $InputArray,
	[switch] $StartMenu,
	[switch] $AdjustPermissions
)

# load dependencies
try {
	[space.wtfbox.win32.ShellLink] | out-null;
}
catch {
	write-verbose "Compiling helper assembly";
	add-type -TypeDefinition (get-content -raw "$PSScriptRoot\space.wtfbox.win32.ShellLink.cs") | out-null;
}

# setup parameters
if ( $StartMenu -and -not $InputArray.Count ) {
	$InputArray = (
		"$env:appdata\microsoft\windows\start menu",
		"$env:appdata\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar"
	) |% {
		get-childitem $_ -recurse -include *.lnk;
	}
}

if ( -not $InputArray ) {
	# nothing to do
	return;
}


if ( $AdjustPermissions ) {
	$user   = [Security.Principal.WindowsIdentity]::GetCurrent();
	$rights = [Security.AccessControl.FileSystemRights]"ReadAndExecute, Synchronize";
	$rule   = new-object Security.AccessControl.FileSystemAccessRule $user.Name, $rights, "Allow";
}


# process shortcuts
$ShellLink = [space.wtfbox.win32.ShellLink];
$lnk = new-object $ShellLink (new-object -ComObject lnkfile);
try {
	$InputArray |% {
		$path = $_.FullName;
		try {
			# load and check for CONSOLE_PROPS
			$lnk.Load($path, 2);
			if ( -not $lnk.HasDataBlock($ShellLink::CONSOLE_PROPS) ) {
				return;
			}

			# remove CONSOLE_PROPS and save
			if ( $pscmdlet.ShouldProcess($path, "Remove CONSOLE_PROPS") ) {
				write-verbose "Removing CONSOLE_PROPS from $path";
				$lnk.RemoveDataBlock($ShellLink::CONSOLE_PROPS);
				$hr = $lnk.Save();
			}

			if ( $AdjustPermissions -and $pscmdlet.ShouldProcess($path, "Adjust permissions") ) {
				# deny current user write permissions
				$item = get-item $path;
				$acl = $item.GetAccessControl('access');     # https://stackoverflow.com/a/6646551
				$acl.SetAccessRuleProtection($true, $true);  # Disable inheritance, assume inherited
				$item.SetAccessControl($acl);

				$acl = $item.GetAccessControl('access');     # Reload ACL to obtain inherited permissions
				$acl.RemoveAccessRuleAll($rule);
				$acl.AddAccessRule($rule);
				$item.SetAccessControl($acl);
			}

			$_;
		}
		catch {
			"Failed to process $($path): $_" | write-error;
		}
	}
}
finally {
	$lnk.Dispose();
}

Then run the command as an administrator:

Unify-ConsoleProps -FixRegistry -CommonPaths -AdjustPermissions -Verbose -ErrorAction Inquire

Afterwards, all custom settings under `HKCU:\Console` will be removed, all shortcuts will be stripped of their `CONSOLE_PROPS` block, and your Windows user will be denied created them. Every terminal hence forth will be using the settings under `HKCU:\Console` as intended.

Conclusion

Because this is not perfectly future proof, this will need to be run after a major Windows update or a program is installed with shortcuts having CONSOLE_PROPS embedded. At least this is only needed until Microsoft releases the terminal settings tool.

Hopefully that day will come soon.

As a professional web developer, I often get asked what’s needed to get started with web development or I’ll find questions relating to web development on the forums.  But, I find that it is often difficult to answer succinctly, because a “Hello, World!” web server involves so many different, yet vital pieces.  Perhaps I’ll detail them out in a series of articles.  Until then, I’ve taken some time to summarize the topics involved in developing a web application:

  • Systems administration. How processes work and communicate, how to add users and groups, how to manage file permissions and/or ACLs, how to set up logging, backups, and automated tasks, how to effectively use a terminal to navigate an operating system, how to setup and configure dependencies and 3rd party software. (Bonus points for having Docker or Vagrant do this for you.) Typically Linux–based systems are used, but does not necessarily exclude Microsoft. Additionally:  How to spin up a virtual machine, and manage servers over SSH or RDP.

  • HTTP and web servers. Web servers are the glue between the frontend and the backend, and HTTP is the basis of the communication between them:  How web servers work, how to configure them to serve both static and dynamic content, and how to leverage HTTP headers and HTTP response codes appropriately.  Web servers:  apache2, nginx, Microsoft IIS.

  • Backend. This is the actual application or “business logic solution”: How to write a program or script that processes requests, performs work, and prepares proper HTTP responses. How to leverage web frameworks to assist in this process.  A backend can be a strict data API to be consumed by one or multiple frontends (web, mobile, console), or a traditional multi–page application (simpler). Languages: Perl, PHP, Python, Ruby, Javascript, and even C/C++. Web frameworks: Laravel, Django, Ruby on Rails, nodejs, ASP.NET MVC, and many more.  Also helpful to know:  JSON, REST, SOAP.

  • Frontend. This is the user–facing piece of the backend:  How HTML works to lay out a page, how CSS works to assist in page layout and theme, how to leverage Javascript to breathe life into the frontend, and how to asynchronously interact with a backend.  Languages: HTML5, Javascript, Typescript, CSS, SASS, SCSS.  Also helpful: JSON, XML, XPath.

  • Browsers: How to use the developer tools in any browser to tweak styles, debug javascript, and debug HTTP requests made by the browser and HTTP responses made by the web server or the backend.

  • Databases. Relational databases are array–of–structures on steroids: How to setup a (relational/document) database and schema, setup database users and their permissions, how to write queries, and how to write database migrations.  Flat files and NoSQL can be okay, too.

  • Version control. Very important in any software project: How to use a version control system, how to write meaningful commits, how to resolve merges, how to submit merge requests, and how to manage branches.

  • Devops: How to configure and build the backend project and/or the frontend project. How to automate builds, run tests, report problems.

(Not pictured here:  Websockets.)

With these in mind, read What happens when you type google.com into your browser and press enter for an example of how a sample request is made.

Hopefully, this has been helpful to game developers looking to build a real–time multiplayer web game or even a high score tracking server.

Thanks for reading, see you around!

Selective quote

Oops, four months just went by in a blink. I hear it gets worse when you get older. Scary.

Anyway, here's a CSS rule to keep the new selective quote modal at the bottom right corner of the screen. I like to select-along while I read, and this pesky modal is always in my way:
[code=css:0]@namespace url(http://www.w3.org/1999/xhtml);@-moz-document domain("www.gamedev.net") {#ddk33_qpopup_popup { position: fixed !important; left: unset !important; top: unset !important; right: 0px !important; bottom: 0px !important;}}And a screen shot:
Zkbg3Xw.png


That's all for now.

Waiting for Visual Studio to update, so I thought I'd write something.

When working with streams such as file or audio processing, you usually run into the same problem: How to copy the most data without knowing how large the stream of data is. I solved a similar problem at work and in my audio synthesizer. In the case of the synthesizer, I wanted an echo effect that fed back N stored seconds of audio.

Here's how a simple block transfer could look:
// assuming:HANDLE src;HANDLE dst;int src_size;// and:const int block_size = 8 * 1000; // 8 KBchar* block = alloc(block_size);// then:int remaining = src_size;for ( int bytes_read = 0; bytes_read = read(src, block, block_size)); remaining -= bytes_read ){ // (process block) write(dst, block, bytes_read); remaining -= bytes_read;}For a block_size of 100 and a src_size of 550, you would make 6 writes of sizes:


100 100 100 100 100 50

The echo effect was a bit different: I have two buffers of known length, but one of the buffers is circular and has a read and write head at two different positions within that buffer.
// assuming:int samples = 44100;int audio_buffer_len = samples / 100;float* out_audio = new float[audio_buffer_len]; // 1-channel audio at 44.1KHz, 100ms buffer// and some state:struct delay_effect { float delay_amp = 0.25f; int delay_stride = samples * 2 / 3; // 667ms or 29400 samples int buffer_size = delay_stride + 320; // some extra padding float* buffer = new float[buffer_size]; int wdx = 0; // write position within buffer int rdx = delay_stride; // read position within buffer // the extra padding on buffer is needed to keep the write head from interfering // with the read head and vice-versa void apply(float* out_audio, int audio_buffer_len);};// then:void delay_effect::apply(float* out_audio, int audio_buffer_len) { float* out_ap = out_audio; int samples_remaining = audio_buffer_len; while ( samples_remaining ) { // find minimum number of samples needed such that: // 1. read head does not overflow buffer // 2. write head does not overflow buffer // 3. audio buffer is not overflowed int take = min( min(buffer_size - wdx, buffer_size - rdx), samples_remaining ); // apply effect to out_audio // (SIMD candidate: snap to 16-byte boundaries and lengths, use float4s instead) float* in_bp = buffer + rdx; float* out_bp = buffer + wdx; for ( int idx = 0; idx < take; ++idx ) { // read from buffer, apply damping, accumulate into output audio, // and store the mixed audio into y float y = (*out_ap++ += *in_bp++ * damping); // feed audio back into buffer *out_bp++ = y; } // advance read and write position, wrapping around buffer length wdx = (wdx + take) % buffer_size; rdx = (rdx + take) % buffer_size; samples_remaining -= take; }}Stepping through, you might see something like this for an audio buffer sized 50, and an echo buffer sized 190, and a delay of 85 samples:
buf# loop# take rdx wdx 1 1 50 0 85 2 1 50 50 135 3 1 5 100 185 3 2 45 105 0 4 1 40 150 45 4 2 10 0 85 5 1 50 10 95 6 1 45 60 145 6 2 5 105 0 7 1 50 110 5As you can see, the entire audio buffer is covered: sum(`take`) group by `buf#`
And neither rdx+take nor wdx+take extends past the echo buffer's 190 samples.


Okay, that's all for now.
See you around.

Hi there. It's been a while. Spent some time today fixing up the stupid IPBoard chat. I somehow managed to monkey-patch IPBoard's styles to maximize use of screen space:

Before:
xzmlbsw.png

After:
F4UgxdS.png
70KqX4X.png
RxzwX7Y.png

The style can be found below and can be used installed using the Stylish addon (Firefox/Chrome). Chrome users must remove the @-moz-document directive and surrounding braces.

@namespace url(http://www.w3.org/1999/xhtml);@-moz-document url("http://www.gamedev.net/chat/") {* { box-sizing: border-box !important; }body { padding: 0 !important;}html, body { height: 100%; width: 100%; overflow: hidden;}#ipbwrapper { max-width: initial; max-height: initial; height: 100%; height: 100%;}#chatters-online-wrap,#scrollbar_container,.ipsLayout_content,#ipbwrapper > .ipsLayout { height: 100%;}.ipsLayout_content,#ipbwrapper > .ipsLayout > .ipsLayout_right { height: 100%; float: right; display: table;}.ipsLayout_content > *,#ipbwrapper > .ipsLayout > .ipsLayout_right > * { display: table-row;}#chatters-online,#messages-display { position: absolute; left: 0px; top: 0x; width: 100%; height: 100%;} @media (max-width: 960px) { .ipsBox_container.ipsLayout_right { width: 250px !important; float: right !important; } .ipsBox_container.ipsLayout_content { overflow: hidden !important; height: 100% !important; width: initial !important; }}#chatters-online { position: absolute; width: 100%; height: 100%; overflow: scroll; left: 0px; top: 0px;}#chatters-online-wrap { overflow: hidden; position: relative; left: 0px; top: 0px;}#chatters-online-wrap + .ipsPad.right { float: initial; text-align: right;}}Anywho, that's all for now. See you around.


EDIT:
Quick fixed chatrooms, small screen sizes.

Hi there.

Someone on the chat today was asking about the pimpl idiom, and I thought I'd write up a quick little example. The purpose of an opaque pointer is to move the the implementation specific dependencies (types, macros, includes, and et. al.) away from the header and into the actual source where its used. Including "Foobar.h" from "Unrelated.cpp" also includes iostream, string, vector, windows.h (namespace polluting!), and the entirety of "expensive third party library". If "Foobar" used an opaque pointer, then "Unrelated.cpp" can use a Foobar, without having to know about "string", "vector", "windows.h", or the third party library.

Let's say, as a contrived example, you wanted to wrap std::vector. Such a class might look something like:


// simple_list.h#include #include class simple_list {public: void add(const std::string& str); void dump() const;private: std::vector _data;};// simple_list.cpp#include "simple_list.h"void simple_list::add(const std::string& str) { _data.push_back(str);}Notice how including "simple_list.h" will also include "vector" and "string".


An opaque pointer will hide the details of its implementation:
// simple_list.hclass simple_list {public: simple_list(); ~simple_list(); simple_list(const simple_list&) = delete; simple_list& operator = ( const simple_list& ) = delete;public: void add(const char* str); void dump() const;private: struct impl; impl* _impl;};// simple_list.cpp#include "simple_list.h"#include #include struct simple_list::impl { std::vector data;};simple_list::simple_list() { _impl = new impl;}simple_list::~simple_list() { delete impl;}simple_list::add(const char* str) { _impl->data->push_back(str);}Note how simple_list.h no longer requires the "vector" or "string" headers, and how the implementation structure is well contained in simple_list.cpp. Unrelated.cpp can use a "simple_list" without first knowing about "vector" or "string".


Also note how simple_list now has a dynamic allocation with it. This is one of the main disadvantages of an opaque pointer, but it can be avoided.

This is my favorite approach to the opaque pointer idiom: Just reserve some space for the implementation:
// simple_list.h#pragma onceclass simple_list {public: // noncopyable simple_list(); ~simple_list(); simple_list(const simple_list&) = delete; simple_list& operator= ( const simple_list& ) = delete;public: // interface void add(const char* str); void dump() const;private: // pimpl struct internal_impl; internal_impl& impl(); const internal_impl& impl() const;private: // data int _data[6];};// simple_list.cpp#include "simple_list.h"#include #include #include #include struct simple_list::internal_impl { internal_impl() {} ~internal_impl() {} std::vector _data;};simple_list::simple_list() { static_assert(sizeof(internal_impl) < sizeof(_data), "simple_list::_data too small for internal_impl"); new (_data) internal_impl;}simple_list::~simple_list() { impl().~internal_impl();}void simple_list::add(const char* str) { impl()._data.push_back(str);}void simple_list::dump() const { using namespace std; const auto& vec = impl()._data; cout << '{'; if ( vec.size() ) { copy(vec.begin(), vec.end()-1, ostream_iterator(cout, ", ")); cout << vec.back(); } cout << '}';}simple_list::internal_impl& simple_list::impl() { return *reinterpret_cast(_data);}const simple_list::internal_impl& simple_list::impl() const { return *reinterpret_cast(_data);}// main.cpp#include "simple_list.h"int main(int, char*[]) { simple_list f; f.add("the"); f.add("quick"); f.add("brown"); f.add("fox"); f.dump();}The simple_list now reserves 6*sizeof(int) bytes for the implementation, backed by a static_assert on the constructor to make sure the buffer doesn't overrun. Memory alignment and cache lines considered, this might be an acceptable compromise: Trade off the double indirection with a possibility of some wasted space.


Though, the point is moot, because in this contrived example, the vector and all its strings will store its data on the heap anyways.

That's all for now. See you around.

Learning Blender

Hi there! Long time no see.

Been busy, but learning Blender. I figured if I knew my way around various GNU programs (GNU screen, less, vim) and their keyboard shortcuts, how bad could Blender be? I've spent the past few days learning blender, and I am loving it! Though, compared to 3dsmax, the object modifier stack is a bit limiting. I originally set out to make a configurable flipper, such that I could adjust the detail as needed; number of sides to each round side of the flipper, bevel depth, and etc. Unfortunately, blender does not have a "select volume operator", nor could I adjust the number of sides of a cylinder after creating it. Oh well, I guess I'll have to make do-- make a template "flipper", which retains as many operators as possible, then make a duplicate, apply operators, and work with the clone.

Anyway, here's what I've been working on. The specular on the ground plane is a bit ridiculous.
XbmDePH.png

In the long-term future, I'm planning on trying out the blender game engine and perhaps get a pinball game going. Depends on a lot of things.

Okay, bye.

Pongout 0.83 WIP

Well, it's about time I posted some progress pics of the game I've been working on the past month or so. smile.png

Pongout is a crossover of breakout and pong, written in moonscript. It originally started as a test to see how Box2D will behave in a breakout game, and then suddenly the rest of this happened. This week was spent bug fixing, polishing, and reworking the third level. Those who have been following my earlier alpha and beta versions in the chat will be delighted to know that the game now runs in borderless fullscreen.

The game currently doesn't have any in-game menus or in-game instructions, so I've included the instructions below.

Download


Platform-independent: pongout_0.83.love (0.11 MB), requires LOVE
Prepackaged Windows x64: pongout_0.83_win_x64.zip (3.63 MB)


Feedback is appreciated. smile.png
There is an odd issue on some machines (read: my machines) that causes the game to be extra choppy unless an application that uses a high-performance timer is running in the background. If you run into this issue as well, please let me know.


Controls


Mouse Controls paddlesLeft mouse Launches ballsleft/right Apply torquer Resetescape QuitDebug controls: (forfeits highscore)] Hit all bricks oncen Next level?

Objective


The goal of the game is to score as many points as possible by skillfully using the ball and paddle to destroy all bricks on the playing field.


legend.png

Legend


(A) Ball
(B) Brick
(C?) Paddle
(D) Unbreakable brick
(E) Ghost
(F) Score earned and score earned from global multiplier
(G) Lives remaining
(H) Current score
(I) Global multiplier
(J) High score


Ball


A ball accumulates multiplier by hitting normal bricks, up to 10x multiplier. As it does so, its speed will increase up to 50%. Hitting a paddle will reset the ball's multiplier and speed.

Use torque to get your ball unstuck or skillfully launch your ball back into the playing field. Use with discretion, as each use applies an increased speed penalty.


Paddle


Can redirect balls depending on where on the paddle they hit. Use this to aim the ball in the right direction.


Brick


Hit these for points and multiplier. Unbreakable bricks are worth less and do not increment multiplier.

Once all the breakable bricks are destroyed, the field is cleared and a new level will begin.


Ghost


Occupies breakable bricks. Hitting a brick inhabited by a ghost will grant a small amount of global multiplier and the ghost will attempt to locate to another vacant brick.

If there are no remaining bricks to occupy or if all breakable bricks are occupied with ghosts, then the ghosts will begin to panic and bonus time begins.


Bonus Time


Panicked ghosts will consume their host bricks over a period of time. Destroy them quickly before the level ends to get up to 1500 bonus points each.


Scoring


There are four parts to scoring: Base score, multiplier, flat score, and global multiplier. Base score is affected by multiplier whereas flat score is not, and both are affected by global multiplier:

score += global*(flat + base*multiplier)

Normal brick: 10 base and 1 multiplier
Unbreakable brick: 1 base
Ghost: 1/16% global multiplier and/or between 100 and 1500 flat during bonus time.


Screenshots


Level 1
level-1.png

Level 3 rework
level-3.png

Changelog:
Added new emotes.
Fixed literal backslash escaping.
Changed users command to link users to their profiles.
And various other things since version 1.0 that I've forgotten.

// ==UserScript==// @name IPBoard Extensions// @namespace fastcall22.com// @include http://www.gamedev.net/chat/*// @version 1.5// @grant none// ==/UserScript==(function ipb_ext(){try{ var Chat = IPBoard.prototype.chat; // util Chat.ext_post_message = function(content_or_nodes,content_is_html) { var post = document.createElement('li'); ['post','chat-myown'].forEach(function(cl){ post.classList.add(cl); }); if ( typeof content_or_nodes == 'string' ) { var content = content_or_nodes; var target = content_is_html ? 'innerHTML' : 'textContent'; post[target] = content; } else { var nodes = content_or_nodes; [].concat(nodes).forEach(function(elem){ post.appendChild(elem); }); } document.getElementById('storage_chatroom') .appendChild(post); post.scrollIntoView(false); }; Chat.ext_search_users = function(query) { var result = {}; var q_reg = new RegExp(query||'^'); for ( var id in Chat.forumIdMap._object ) { var name = Chat.nameFormatting._object[id][2]; if ( q_reg.test(name) ) result[name] = id; } return result; }; Chat.ext_user_action = function(action,target_id){ ipb.chat.sendMessageToChild( "server="+serverHost + "&path="+serverPath + "&room="+roomId + "&user="+userId + "&access_key=" + accessKey + "&action="+action + "&against="+target_id ); ipb.chat.lastAction = parseInt(new Date().getTime().toString().substring(0, 10)); }; // config var null_f = function(){}; Chat.ext_empty_event = { stopPropagation: null_f, preventDefault: null_f }; Chat.ext_confirm_action = null; var make_action = function(action){ return function(query) { if ( !query.trim() ) return; var users = Chat.ext_search_users(query); Chat.ext_confirm_action = function() { for ( var k in users ) Chat.ext_user_action(action,users[k]); }; var str = ''; for ( var k in users ) { if ( str ) str += ', '; str += k; } Chat.ext_post_message("Send /confirm to "+action+": "+str+". Send /cancel to ignore"); }; }; Chat.ext_commands = { kick: make_action('kick'), ban: make_action('ban'), test: function() {Chat.test = !(Chat.test || false)}, debug: function() {Chat.debug = !(Chat.debug || false)}, users: function(query) { var users = {}; var keys = []; [].slice.call(document.querySelectorAll('#chatters-online li')) .forEach(function(li){ var a = li.querySelector('a.ipsUserPhotoLink'); var user_id = li.getAttribute('id').replace(/^user_/,''); var user_profile = a.getAttribute('href'); var user_name = Chat.nameFormatting._object[user_id][2]; var out_a = document.createElement('a'); out_a.setAttribute('href',user_profile); out_a.textContent = user_name; users[user_name] = out_a; keys.push(user_name); }); var nodes = []; keys.sort().forEach(function(key) { if ( nodes.length ) nodes.push(document.createTextNode(', ')); nodes.push(users[key]); }); Chat.ext_post_message(nodes); }, confirm: function() { if ( !Chat.ext_confirm_action ) return; Chat.ext_confirm_action(); Chat.ext_confirm_action = null; }, cancel: function() { Chat.ext_confirm_action = null; }, mute: function() { document.getElementById('sound_toggle').click(); }, quit: function(){ document.getElementById('leave_room').click(); }, }; Chat.ext_escape = { 'n': '\n', 't': '?', 'e': '?', '\\': '\\', }; Chat.ext_emotes = { fear: ':f34r:', lenny: '( ?? ?? ??)', wat: '?__?', shrug: '?\\_(?)_/?', whee: '?( ? )?', flip: '(????)??????', why: '?(????)', }; Chat.ext_bbc = { rick: function(param,content) { // never gonna give you up // never gonna let you down // never gonna run around // and desert you } }; // helpers Chat.arg_parse = /\s*(?:(\w+?)(?:="(.+?)"))?/g; Chat.bbc_filter = function ext_bbc_filter(_,tag,param,content) { var f = Chat.ext_bbc[tag]; if ( !f ) return _; return f(param,content); }; Chat.ext_filters = [ /* commands */ [ /^\/(\w+)\s*(.*)$/, function ext_command_filter(_,cmd,argstr) { var args = argstr.split(' '); var f = Chat.ext_commands[cmd]; if ( !f ) return _; f.apply(null,args); return ''; } ], /* emotes */ [ /:(\w+):/g, function ext_emote_filter(_,tag) { return Chat.ext_emotes[tag] || _; } ], /* backslash */ [ /\\(?:(x[0-9a-fA-f]{4})|([a-z\\]))/g, function ext_backslash_filter(_,hex,code) { if ( hex ) return '&#'+hex+';'; return Chat.ext_escape || _; } ], /* BBC w/ content */ [ /\[(\w+)(?:=(.+))?\](.*?)\[\/\1\]/g, Chat.bbc_filter ], /* self-closing BBC tags */ [ /\[(\w+)(?:=(.+))?\s*()\/\]/g, Chat.bbc_filter ], /* general */ [ /\t/g, '?' ] ]; Chat.ext_filter_message = function(str) { Chat.ext_filters.forEach(function(f){ if ( !str.length ) return; if ( Chat.debug ) console.log(f,str); str = String.prototype.replace.apply(str,f); }); return str; }; Chat.ext_send = function ext_send(e) { if ( e.preventDefault ) e.preventDefault(); if ( e.stopPropagation ) e.stopPropagation(); var message = (document.getElementById('message_textarea').value || '').trim(); if ( Chat.debug ) console.log('send',message); if ( !message ) return; var filtered = Chat.ext_filter_message(message) || null; document.getElementById('message_textarea').value = filtered; if ( filtered ) return Chat.ipb_sendChat.call(this,Chat.ext_empty_event); }; Chat.ipb_sendChat = Chat.sendChat; Chat.sendChat = Chat.ext_send; Chat.ext_rewired = true; console.log('okay');} catch ( ex ) { console.log("Error: " + ex); console.log((new Error).stack);}})();commands: kick, ban, quit, and users
escaping: e, n, t, e, and \
emots: lenny, wat, shrug, whee, flip, and why



As usual, add this as a greasemonkey script or dump it into your javascript console

This a continuation of the Yet Another Generic Space Shooter (YAGSS) game.

Most of the graphics programming I've learned has been done through software rendering and fixed-function OpenGL, so I wasn't sure what to expect when writing my first real vertex shader. The sprites themselves would be simple, only having x, y, rotation, and scale as properties, and a simple tinting effect.

It was confusing at first to get the input layout just right for both of the two vertex buffers, the vertex and instance buffers. It turns out that InstanceDataStepRate is very important, otherwise you'll draw the same instance repeatedly. Oops.

As for the vertex shader itself, I had the greatest idea ever: Why not have the GPU do the between-state interpolation? The properties are simple enough, and it would save bandwidth, as I'd only need to update the constant buffer containing the interpolation amount in between frames, rather than the entire instances' state every frame. To do this, I packed the sprite's position, rotation, and scale into a single float4. Doubling this, I have a previous and current state. From there, I can interpolate between the two states.

// bufferscbuffer constants { float4 camera_transform; // (x,y,z,w) = (x,y,scale_x,scale_y) float4 time; // (x,y,z,w) = (app_time,game_time,interp,0)};// typesstruct in_instance { // vertex float2 pos : POSITION; float4 color : COLOR; // instance // 0 previous state, 1 current state (backwards for some reason, w/e) float4 transform[2] : TEXCOORD0; // (x,y,z,w) = (x,y,rotation,scale) float4 tint[2]: TEXCOORD2;};struct in_pixel { float4 pos : SV_POSITION; float4 color : COLOR;};// mainin_pixel main( in in_instance IN ) { // interpolate float interp = time.z; float4 t = lerp(IN.transform[1],IN.transform[0],interp); float4 tint = lerp(IN.tint[1],IN.tint[0],interp); // setup model transform float c = cos(t.z) * t.w; float s = sin(t.z) * t.w; float2 U = float2( c, s ); float2 V = float2( -s, c ); // transform float2 vpos = IN.pos; float2 pos = U*vpos.x + V*vpos.y + t.xy - camera_transform.xy; pos /= camera_transform.zw; // output in_pixel OUT; OUT.pos = float4(pos,0,1); OUT.color = float4(lerp(IN.color.rgb,tint.rgb,tint.a),IN.color.a); return OUT;}The first sprite shader. Textures not included.

The update logic went something like this:

bool did_update = false;for ( ticks_bucket += ticks_elapsed; ticks_bucket >= ticks_per_update; ticks_bucket -= ticks_per_update ) { update(time_per_update); // mutates instance array did_update = true;}if ( did_update ) update_instance_buffer(); // copies instance array directly to vertex bufferfloat interp = ticks_bucket / (float)ticks_per_update;shader_constants.interp = interp;update_constant_buffers();render(interp);Game loop excerpt (pseudo).

What I didn't realize until rewriting the shader, was that there was a significant performance penalty because the model's transform is rebuilt for every instance's vertex, even though there were only four vertices. I decided to see how it compared to a more "standard" approach. Here's what the second approach looked like:

// bufferscbuffer constants { row_major float2x3 camera_transform; row_major float2x3 atlas_transform; // TODO: remove this, sprite atlas should be normalized floats float4 time; // (x,y,z,w) = (app_time,game_time,interp,0)};// typesstruct in_instance { // vertex float2 pos : POSITION; float2 texcoord : TEXCOORD0; float4 color : COLOR; // instance row_major float2x3 transform : TEXCOORD1; row_major float2x3 tex_transform : TEXCOORD3; float4 tint : TEXCOORD5;};struct in_pixel { float4 pos : SV_POSITION; float4 color : COLOR; float2 texcoord : TEXCOORD;};// mainin_pixel main( in in_instance IN ) { // transform position in_pixel OUT; float2 pos = IN.pos; pos = mul(IN.transform, float3(pos,1)); pos = mul(camera_transform, float3(pos,1)); // transform texture float2 texcoord = IN.texcoord; texcoord = mul(IN.tex_transform, float3(texcoord,1)); texcoord = mul(atlas_transform, float3(texcoord,1)); // OUT.pos = float4(IN.camera_transform * float3(IN.transform*float3(IN.pos,1),1),0,1); OUT.pos = float4(pos,0,1); OUT.color = float4(lerp(IN.color.rgb,IN.tint.rgb,IN.tint.a),IN.color.a); OUT.texcoord = texcoord; return OUT;}Second attempt. Includes textures and spooky matrix math!

And the game loop:

// updatefor ( ticks_bucket += ticks_elapsed; ticks_bucket >= ticks_per_update; ticks_bucket -= ticks_per_update ) update(time_per_frame);float interp = ticks_bucket / (float)ticks_per_update;update_constant_buffer();copy(begin(entities),end(entities),begin(instances),entity_renderer(interp));update_instance_buffer();render(interp);Compared to the first attempt (and before textures were added in to the shader), I was able to push out 38% more untextured sprites from ~55000 to ~76000 sprites. It was then I decided that I probably won't even reach 3% of that number for this game. Oh well. Now about this second approach...
The interpolation is done inside the entity_renderer, taking entities and translating their interpolatable properties into instances to be sent to the GPU. The matrix-vector math was tricky, but by arranging it like so, I was able to shave off a row from each matrix:

[[px] [py] [ 1]][[Ux Vx dx] [[Ux*px + Vx*py + dx] [Uy Vy dy]] [Uy*px + Vy*py + dy]]Anyways, here's the engine now. Textures 'n' stuff:
f70ncEu.png

I'm still playing with the blending modes. And, I may end up ditching baking the glow effect into the textures. I might be able to save on fillrate by using a fullscreen shader effect, rather than rendering a bunch of bloated sprites, just for their glow effects.

In any case, I guess I should start actually building the game now...
?

I'm writing yet another generic space shooter while learning WTL, DirectX, and modern graphics programming along the way. Today was spent writing a script to build SVG fragments into a texture atlas. Here's the result of today's work:



atlas_sample.png


Sample generated atlas generated from SVG fragments

SVG fragment for "box"


[subheading]The game[/subheading]
This game is a mix of shoot-em-up, adventure, with some RPG, resource management, and some puzzle elements. Here's the engine so far:




game.png


75625 unique untextured sprites, averaging 70 FPS

I had prototyped a similar game in immediate mode OpenGL many years ago. The most tedious thing I remember from that project was that I had to rerender every sprite by hand every time I tweaked the style. It wasn't unusual to have sprites with different stroke widths and inconsistent glows.

With this game, I wanted to avoid all that nonsense by generating assets automatically. This way, I could tweak and adjust the style and sizes, without having to visit each asset by hand and rerendering them.


[subheading]The Objectives[/subheading]
The objectives are simple:
1. Reusable SVG template. A skeleton SVG file which will be used to generate fully-formed SVG elements. This will include the style, and the filters. A script will apply SVG fragments and dimensions to the skeleton and feed this to the SVG rasterizer.
2. Automated SVG rasterization. A library which will transform an SVG into a PNG. A library that accepts a SVG as a string instead of a file is preferred, but not necessary.
3. Texture atlas. The resulting PNGs will be packed into a single texture, and will also output an accompanying layout file.


[subheading]The Script[/subheading]
Because these assets will be compiled and assembled on my development machine, I chose to write the script in nodejs, because of its vast selection of libraries.


[subheading]The libraries[/subheading]
From what I could find and scan, following libraries that do most of the heavy lifting are javascript wrappers that facilitate inter-process communication between nodejs and the actual binary. It's not really a concern, as almost all dependencies, except imagemagick, were installed from node's package manager. Here are the libraries I chose for this script:

1. svg-to-png. Uses chromimum to render SVG files to PNGs. Imagemagick can render SVGs as well, but I didn't learn this until the end of the day.
2. binpacking.
3. gm with imagemagick. Wrapper library to wrap image rendering operations. Used to generate the texture atlas.
4. underscore. Provides functional-like methods and the templating engine.


[subheading]The Style[/subheading]
Here's the first iteration of the style.



style.png


Player's ship sprite. Notice that the stroke and blur effect expands outside of the canvas and the geometry snaps nicely to the canvas edges.


[subheading]The Template[/subheading]
From here, I exported an optimized SVG from Inkscape and split out the template and guts into two separate files, while additionally factoring out the common path attributes to a stylesheet.
<%= frag %> Initial SVG template. The <%= %> tags are evaluated dynamically.



SVG fragment for the player's sprite. The comment at the beginning signifies to the script the area this sprite's geometry occupies.

It took quite a few tries to get the filter and feGaussianBlur to behave correctly, especially with esoteric attributes on the filter tags. In retrospect, it may have been easier to keep the fragments as full Inkscape SVG documents and had the script extract the geometry and filter out attributes. This would also have elimiated the need for the size comment.


[subheading]The Script[/subheading]
And finally, this ugly beast of a script:


(function(){ // dependencies var fs = require('fs'), rasterizer = require('svg-to-png'), binpacking = require('binpacking'), gm = require('gm').subClass({imageMagick:true}), u = require('underscore'); // config var E = function(){}, defs_dir = 'defs', temp_dir = 'temp', ends_with = function(str) { return function(val) { return val.substr(val.length-str.length,val.length) == str; } }; // prepare svg template var build = u.template(''+fs.readFileSync('template.svgt')), outline_px = 4, outline_u = 1, blur_offset_u = 1.5; // compile defs into templates console.log('compiling...'); var compiled = []; u.filter(fs.readdirSync(defs_dir),ends_with('.svgfrag')) .forEach(function(name){ // load frag, find viewbox var frag = ''+fs.readFileSync(defs_dir+'/'+name); var box = (/^/.exec(frag) || []).slice(1,5); // parse viewbox: box=left, bottom, right, top if ( box.length != 4 ) { console.error('size meta malformed in '+name); return; } for ( var kdx in box ) { var val = parseFloat(box[kdx]); if ( isNaN(val) ) { console.error('size parameter #'+kdx+' invalid'); return; } box[kdx] = val; } // calculate viewbox and texture size var T = blur_offset_u + outline_u/2; box[0] -= T; box[1] -= T; box[2] += T; box[3] += T; var vw = box[2] - box[0], vh = box[3] - box[1], tw = Math.floor(vw*outline_px), th = Math.floor(vh*outline_px) // stuff frag into template var svg = build(tmp = { vx: box[0], vy: box[1], vw: vw, vh: vh, tw: tw, th: th, frag: frag, }); // record output var base_name = name.replace(/\..+$/, ''); var out_name = base_name+'.svg'; var out_svg = temp_dir+'/'+out_name; var out_png = temp_dir+'/'+base_name+'.png'; var cwd = process.cwd(); compiled.push({ name: base_name, svg: process.cwd()+'/'+out_svg, // ??? png: out_png, w: tw+2, h: th+2, }); // save to file fs.writeFileSync(out_svg, svg); }); // render compiled svgs to pngs console.log('rendering...'); try { process.chdir('temp'); // ??? rasterizer .convert(u.pluck(compiled,'svg'),'../..') // ??? .then(function(){ // sort and pack compiled svgs console.log('packing...'); var packer = new binpacking.GrowingPacker(); compiled.sort(function(a,b){ if ( a.w < b.w ) return 1; if ( a.w > b.w ) return -1; if ( a.h < b.h ) return 1; if ( a.h > b.h ) return -1; return 0; }); packer.fit(compiled); // render packed images to an atlas // and build map for atlas var img = gm(packer.root.w+1,packer.root.h+1,"#00000000"); var atlas = ''; for ( var cdx in compiled ) { var c = compiled[cdx]; var f = c.fit; if ( !f.used ) { console.error('unused: ', c); continue; } img .stroke('#FFFF00FF',1) .fill('#FFFFFF00') .drawRectangle(f.x,f.y,f.x+c.w,f.y+c.h) .draw('image Copy '+(f.x+1)+','+(f.y+1)+' 0,0 '+c.png); atlas += [c.name,f.x,f.y,f.w,f.h].join("\t"); atlas += "\n"; } img.write('atlas.png',function(e){ if ( e ) console.error(e); console.log('done'); }); fs.writeFileSync('atlas.txt', atlas); }); } finally { process.chdir('..'); }})();Svg-to-png was especially dumb, because for some reason it expects at least two levels of directories for input files and output directory, or something dumb like that. Had to do some chdir to trick it to output to the correct directory. Specifying inputs ['temp/a.svg','temp/b.svg'] and output 'temp' from working directory 'resources/svg' would place 'a.png' in 'temp/resources/svg/a.png'. Yeah, that was fun. No documentation on this behavior, either.


[subheading]Post-Mortem[/subheading]
Overall, the day was well spent. I got a script that works while learning about nodejs, imagemagick, and et. al. I can tweak and change the style at any time and rebuild the assets. Extracting the SVG fragments from Inkscape was a bit of a pain: Sometimes the coordinates of the optimized SVG were translated 10 units +y or 1000 units +y for no reason. Other than that, I'm happy with the results.


Also, what's a content pipeline?
And how do I IPBoard journal??

So I finally sat down and spent a Saturday morning extending IPBoard chat client. It works by rerouting IPBoard.prototype.chat to a custom function, which then transforms the message text through a series of WTF-regexes. This script should be installed as a Greasemonkey script, but it could also be copy-pasta'd into a developer tool's javascript window.
 

Features

 


Extra commands:

 

 

/users
/kick regex
/ban regex
/confirm
/quit
/mute

 


Text emotes:

 

 

:lenny: ( ?? ?? ??)
:wat: ?__?

 


Backslash escaping:

 

 

\n Newline
\e Zero width space (for escaping)
\t Em-width space
\xYYYY Equivalent to &#YYYY;

 


Support for custom BBCode tags.


Here is the script:
// ==UserScript==// @name IPBoard Chat Extensions// @namespace fastcall22.com// @description For GAMSDEV// @include http://www.gamedev.net/chat/// @version 1// @grant none// ==/UserScript==(function ipb_ext(){ var Chat = IPBoard.prototype.chat; // util Chat.ext_post_message = function(textContent) { var post = document.createElement('li'); post.setAttribute('class','post chat-myown'); post.textContent = textContent; document.getElementById('storage_chatroom') .appendChild(post); post.scrollIntoView(false); }; Chat.ext_search_users = function(query) { var result = {}; var q_reg = new RegExp(query||'.'); for ( var id in Chat.forumIdMap._object ) { var name = Chat.nameFormatting._object[id][2]; if ( q_reg.test(name) ) result[name] = id; } return result; }; Chat.ext_user_action = function(action,target_id){ ipb.chat.sendMessageToChild( "server="+serverHost + "&path="+serverPath + "&room="+roomId + "&user="+userId + "&access_key=" + accessKey + "&action="+action + "&against="+target_id ); ipb.chat.lastAction = parseInt(new Date().getTime().toString().substring(0, 10)); }; // config var null_f = function(){}; Chat.ext_empty_event = { stopPropagation: null_f, preventDefault: null_f }; Chat.ext_confirm_action = null; var make_action = function(action){ return function(query) { if ( !query.trim() ) return; var users = Chat.ext_search_users(query); Chat.ext_confirm_action = function() { for ( var k in users ) Chat.ext_user_action(action,users[k]); }; var str = ''; for ( var k in users ) { if ( str ) str += ', '; str += k; } Chat.ext_post_message("Send /confirm to "+action+": "+str+". Send /cancel to ignore"); }; }; Chat.ext_commands = { kick: make_action('kick'), ban: make_action('ban'), test: function() {Chat.test = !(Chat.test || false)}, debug: function() {Chat.debug = !(Chat.debug || false)}, users: function(query) { var list = Chat.ext_search_users(query); var str = ''; for ( var k in list ) { if ( str ) str += ', '; str += k; } Chat.ext_post_message(str); }, confirm: function() { if ( !Chat.ext_confirm_action ) return; Chat.ext_confirm_action(); Chat.ext_confirm_action = null; }, cancel: function() { Chat.ext_confirm_action = null; }, mute: function() { document.getElementById('sound_toggle').click(); }, quit: function(){ document.getElementById('leave_room').click(); }, }; Chat.ext_escape = { 'n': '\n', 't': '?', 'e': '?', }; Chat.ext_emotes = { fear: ':f34r:', lenny: '( ?? ?? ??)', wat: '?__?', }; Chat.ext_bbc = { rick: function(param,content) { // never gonna give you up // never gonna let you down // never gonna run around // and desert you } }; // helpers Chat.bbc_filter = function ext_bbc_filter(_,tag,param,content) { var f = Chat.ext_bbc[tag]; if ( !f ) return _; return f(param,content); }; Chat.ext_filters = [ /* commands */ [ /^\/(\w+)\s*(.*)$/, function ext_command_filter(_,cmd,argstr) { var args = argstr.split(' '); var f = Chat.ext_commands[cmd]; if ( !f ) return _; f.apply(null,args); return ''; } ], /* emotes */ [ /:(\w+):/g, function ext_emote_filter(_,tag) { return Chat.ext_emotes[tag] || _; } ], /* backslash */ [ /\\(?:(x[0-9a-fA-f]{4})|([a-z]))/g, function ext_backslash_filter(_,hex,code) { if ( hex ) return '&#'+hex+';'; return Chat.ext_escape || _; } ], /* BBC w/ content */ [ /\[(\w+)(?:=(.+))?\](.*?)\[\/\1\]/g, Chat.bbc_filter ], /* self-closing BBC tags */ [ /\[(\w+)(?:=(.+))?\s*()\/\]/g, Chat.bbc_filter ], /* general */ [ /\t/g, '?' ] ]; Chat.ext_filter_message = function(str) { Chat.ext_filters.forEach(function(f){ if ( !str.length ) return; if ( Chat.debug ) console.log(f,str); str = String.prototype.replace.apply(str,f); }); return str; }; Chat.ext_send = function ext_send(e) { if ( e.preventDefault ) e.preventDefault(); if ( e.stopPropagation ) e.stopPropagation(); var message = (document.getElementById('message_textarea').value || '').trim(); if ( Chat.debug ) console.log('send',message); if ( !message ) return; var filtered = Chat.ext_filter_message(message) || null; document.getElementById('message_textarea').value = filtered; if ( filtered ) return Chat.ipb_sendChat.call(this,Chat.ext_empty_event); }; Chat.ipb_sendChat = Chat.sendChat; Chat.sendChat = Chat.ext_send;})();Notable points of interest:
Chat.ext_send
Chat.ext_filter_message
Chat.ext_filters


The extension is pretty dumb, but it works. So, have fun! smile.png

 

  • Advertisement