Jump to content

  • Log In with Google      Sign In   
  • Create Account

Fastcall's Development Blog

Selective quote

Posted by , 21 August 2016 - - - - - - · 677 views

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:

@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:
Posted Image


That’s all for now.

It’s happening

Posted by , 26 April 2016 - - - - - - · 989 views

It’s happening

Oh boy, it’s happening.

http://i.imgur.com/fy3Usda.png (665 KB)


Learned how to cycles render. Basic water ‘n’ stuff-- still needs some work.


Posted by , 25 April 2016 - - - - - - · 844 views


Just a doodle.


http://i.imgur.com/DeyN9TZ.png (177.8KB)

Block Transfers, Buffers, and Audio

Posted by , 15 April 2016 - - - - - - · 839 views

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:
int    src_size;

// and:
const int block_size = 8 * 1000; // 8 KB
char* 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),

        // 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    5
As 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.

Maximized IPBoard Chat

Posted by , 21 February 2016 - - - - - - · 2,275 views

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:

Posted Image

Posted Image
Posted Image
Posted Image

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%;

#ipbwrapper > .ipsLayout {
	height: 100%;

#ipbwrapper > .ipsLayout > .ipsLayout_right {
	height: 100%;
	float: right;
	display: table;

.ipsLayout_content > *,
#ipbwrapper > .ipsLayout > .ipsLayout_right > * {
	display: table-row;

#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.

Quick fixed chatrooms, small screen sizes.

Quick Opaque Pointer Example

Posted by , in Uncategorized, C++ 31 December 2015 - - - - - - · 1,728 views

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<std::string>. Such a class might look something like:

// simple_list.h
#include <vector>
#include <string>

class simple_list {
	void add(const std::string& str);
	void dump() const;

	std::vector<std::string> _data;

// simple_list.cpp
#include "simple_list.h"

void simple_list::add(const std::string& 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.h
class simple_list {
	simple_list(const simple_list&) = delete;
	simple_list& operator = ( const simple_list& ) = delete;

	void add(const char* str);
	void dump() const;

	struct impl;
	impl* _impl;

// simple_list.cpp
#include "simple_list.h"
#include <vector>
#include <string>

struct simple_list::impl {
	std::vector<std::string> data;

simple_list::simple_list() {
	_impl = new impl;

simple_list::~simple_list() {
	delete impl;

simple_list::add(const char* 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 once

class simple_list {
public: // noncopyable

	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 <vector>
#include <iterator>
#include <iostream>
#include <string>

struct simple_list::internal_impl {
	internal_impl() {}
	~internal_impl() {}

	std::vector<std::string> _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() {

void simple_list::add(const char* 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<string>(cout, ", "));
		cout << vec.back();
	cout << '}';

simple_list::internal_impl& simple_list::impl() {
	return *reinterpret_cast<internal_impl*>(_data);

const simple_list::internal_impl& simple_list::impl() const {
	return *reinterpret_cast<const internal_impl*>(_data);

// main.cpp
#include "simple_list.h"

int main(int, char*[]) {
	simple_list f;


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

Posted by , in Graphics 29 November 2015 - - - - - - · 1,221 views

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.
Posted Image


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

Posted by , in OpenGL, Lua, Moonscript, LÖVE, Pongout 26 September 2015 - - - - - - · 1,495 views
Well, it's about time I posted some progress pics of the game I've been working on the past month or so. Posted Image

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.

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

Feedback is appreciated. Posted Image
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.

Mouse        Controls paddles
Left mouse   Launches balls
left/right   Apply torque
r            Reset
escape       Quit

Debug controls: (forfeits highscore)
]            Hit all bricks once
n            Next level

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.

Posted Image
(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

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.

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

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.

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.

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.

Level 1
Posted Image

Level 3 rework
Posted Image

Forcing Consistent Console Properties in Windows

Posted by , in PowerShell, Terminal 22 August 2015 - - - - - - · 1,128 views
Windows, Terminal, cmd and 1 more...
This doesn't really belong as a gamedev.net article, so I'm posting this here.

If you spend most of your working day inside terminals like I do, then you might agree to having a consistent terminal experience. Font style, colors, and options such as "QuickEdit" and "Insert Mode" should be consistent throughout all terminal applications in Windows. Unfortunately, Windows makes this difficult to do, since the settings are stored in more than once place. When you run "cmd" or "powershell" from the command line, you run the executable directly. However, when search and run the same command from the start menu, you run a shortcut located in one of two places. These shortcuts have their own terminal settings embedded within them, taking preference over the default settings in HKCU:\Console. Usually this isn't a problem, but if you happened to run cmd or powershell from the start menu rather than the run dialog by accident, well...

I agree that the time spent researching all of this could have been spent manually migrating the terminal settings to each shortcut manually a few times over, but I wanted a way to rapidly iterate terminal palettes, and eventually share my findings.

Let's start with configuring a single terminal and merging them to the default settings in HKCU:\Console. Run "cmd" or "powershell" from the run dialog and change the its settings. Next we'll merge those changes into the default terminal settings and remove all other inherited settings:
# make new settings default
$parent = get-item "HKCU:\Console";
$child = get-item "HKCU:\Console\%SystemRoot%_System32_cmd.exe";
$child.Property |% {
	$parent.SetValue($_, $child.GetValue($_));

# remove all subkeys from HKCU:\Console
$reg = "Microsoft.PowerShell.Core\Registry";
ls "HKCU:\Console" |% {
	remove-item "$reg::$_";
PowerShell is a bit ham-handed with the stringifying RegistryKey. "HKCU:\Console" becomes "HKEY_CURRENT_USER\Console" and is treated as a path on the filesystem. To resolve this, we prepend the registry provider scope, $reg, to the path.

Now here's where it gets tricky. Every shortcut can have an embedded CONSOLE_PROPS block which is preferred over the registry keys. We can't have that. So we must remove the data block from each of the shortcuts. There are a few interfaces that will assist us: IPersistFile and IShellLinkDataList. We can load the shortcut using IPersistFile and manipulate its data blocks with IShellLinkDataList.

First, a quick C# shim to handle the COM interactions:
// shell-link.cs
using System;
using System.Runtime.InteropServices;

namespace fastcall22 {
	public interface IPersistFile {
		int GetClassID(out Guid pClassID);

		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);

	public interface IShellLinkDataList {
		void AddDataBlock(IntPtr pDataBlock);
		int CopyDataBlock(uint dwSig, out IntPtr ppDataBlock);
		void RemoveDataBlock(uint dwSig);
		void GetFlags(out int dwFlags);
		void SetFlags(uint dwFlags);

	public class ShellLink : IDisposable {
		private IPersistFile _handle;

		public ShellLink(object lnk) {
			_handle = (IPersistFile)lnk;

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

		public void Dispose() {

		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;

			return true;

		public void Save() {

		public const uint CONSOLE_PROPS = 0xA0000002;
		public const uint CONSOLE_FE_PROPS = 0xA0000004;
And load it with powershell:
// shell-link.ps1
add-type -TypeDefinition ([IO.Text]::ReadAllText("shell-link.cs"));
Now, for actually stripping shortcuts of their CONSOLE_PROPS. This next step will require administrator privileges to write to programdata. See the update below about denying the authenticated users group write permission.
"$env:appdata\microsoft\windows\start menu",
"$env:appdata\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar",
"C:\Users\Default\AppData\Roaming\Microsoft\Windows\Start Menu",
"C:\programdata\microsoft\windows\start menu" |% {
	ls $_ -recurse -include *.lnk
} |% -begin {
	$SL = [fastcall22.ShellLink];
	$lnk = new-object fastcall22.ShellLink (new-object -ComObject lnkfile);

	$group = new-object Security.Principal.NTAccount "Users";
	$rule = new-object Security.AccessControl.FileSystemAccessRule $group,"Write","Deny";
} -process {
	$path = $_.FullName;
	try {
		# load and check for CONSOLE_PROPS
		if ( -not $lnk.HasDataBlock($SL::CONSOLE_PROPS) ) {

		# remove CONSOLE_PROPS and save

		# deny users group write permissions to prevent re-adding CONSOLE_PROPS
		$acl = get-acl $path;
		set-acl $path $acl;

		# OK
		"$($path): OK" | write-host -foreground gray;
	} catch {
		"$($path): $_" | write-host -foreground red;
} -end {
	remove-variable lnk;
And you're done!

I've modified the script to deny the Users group write permissions on shell links, because exiting a shell launched form a shortcut will re-add CONSOLE_PROPS to that shortcut. Additionally, examining the shortcut in the file properties re-adds CONSOLE_PROPS as well, regardless if you close without saving.

Windows, pls.

IPBoard Extensions v1.5

Posted by , 13 August 2015 - - - - - - · 847 views

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(){
	var Chat = IPBoard.prototype.chat;

	// util
	Chat.ext_post_message = function(content_or_nodes,content_is_html) {
		var post = document.createElement('li');

		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;



	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){
			"server="+serverHost +
			"&path="+serverPath +
			"&room="+roomId +
			"&user="+userId +
			"&access_key=" + accessKey +
			"&action="+action +
		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() )

			var users = Chat.ext_search_users(query);
			Chat.ext_confirm_action = function() {
				for ( var k in users )
			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'))
				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.textContent = user_name;
				users[user_name] = out_a;
			var nodes = [];
			keys.sort().forEach(function(key) {
				if ( nodes.length )
					nodes.push(document.createTextNode(', '));

		confirm: function() {
			if ( !Chat.ext_confirm_action )
			Chat.ext_confirm_action = null;
		cancel: function() {
			Chat.ext_confirm_action = null;
		mute: function() {
		quit: function(){
	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 */ [
		function ext_command_filter(_,cmd,argstr) {
			var args = argstr.split(' ');
			var f = Chat.ext_commands[cmd];
			if ( !f )
				return _;
			return '';
	], /* emotes */ [
		function ext_emote_filter(_,tag) {
			return Chat.ext_emotes[tag] || _;
	], /* backslash */ [
		function ext_backslash_filter(_,hex,code) {
			if ( hex )
				return '&#'+hex+';';
			return Chat.ext_escape[code] || _;
	], /* BBC w/ content */ [
	], /* self-closing BBC tags */ [
	], /* general */ [
		' '
	] ];

	Chat.ext_filter_message = function(str) {
			if ( !str.length )

			if ( Chat.debug )
			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 )
		if ( !message ) 

		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;

} 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

October 2016 »

2324 25 26272829

Recent Entries

Recent Comments

Latest Visitors