Abusing UIKit for Gaming in Xamarin.iOS, Part 2: Using Custom Fonts

This is the second in a series of Abusing UIKit blog posts giving some background on the development that want into producing Smudges, a simple game written entirely in Xamarin.iOS where fun shapes in various colors show up on the screen wherever a tap is detected. It was original created to give my two-year-old something fun to play while going tap-crazy on the screen. The game evolved from those “play-testing” sessions. If you have your own little ones and want something fun to distract them, Smudges is availabe on the App Store. At this point, I plan to continue adding features to it as I can. Let me know what you think about Smudges, or these blog posts, in the comments below or find @patridgedev on Twitter.

Demo of using a custom icon font in a UILabel and adjusting the size

Using an Icon Font

Using an icon font can be great for a typical app for substituting a mess of PNGs. For icons, the size savings is probably minimal, but dealing with a single font file compared to a folder of icon images in numerous DPI variations can be much nicer. Since we are dealing with “plain” text in a label, color is controlled by manipulating the label’s text color. For Smudges, the icon font was all about having something visually enjoyable pop onto the screen beyond a basic rectangular view. Fortunately, most icon fonts have a few characters that are just for fun.

Font, Meet Project. Project, Meet Font

Whether you are using a custom icon font or a regular font in your project, adding it is exactly the same. First, pick your favorite TTF font file. Either drag it onto the Resources folder in Xamarin Studio or right-click to “Add Files…” there. You can also put it in a subfolder within Resources (e.g., “Fonts”) as I did for this project, since later demos may have fonts and sounds.

Once you have a font in your project, there are two more steps to make it work: setting up Info.plist and setting the build action. Since you are already in the solution view, right-click the new font and get its properties. Change the “Build action” to “BundleResource”. Now you need to tell the app that it needs to load custom fonts and which files will be provided by adding some details to Info.plist. Open your projects Info.plist file and change to Source view with the bottom tab choices. Add a new root entry and pick “Fonts provided by application” from the choices. This will create a new array where you will put any file names of the custom fonts within your application bundle. In this case, I added Fonts/fontawesome-webfont.ttf to the array.

Custom fonts under Fonts provided by application in Info.plist

With the file bundled and Info.plist all prepped, it’s just a matter of creating a UIFont with the correct font name.

UIFont IconFont = UIFont.FromName("FontAwesome", CharacterSize);

Getting the Font Name

You may not always know exactly what your font’s name is as seen by iOS. The simple way to find it is to open it with the baked-in OS X Font Book application. The font’s name will be shown as the window title.

Getting the font's name with Font Book

One other method is to run your app and pull all the available font names and search for the one you think is right for the newly embedded font. (This has the added benefit of verifying you have all the previous steps set up correctly; if the font shows up in this list, the app found it and loaded it.)

// Dump out an alphabetical list of all available font names on the device.
var availableFontNames = UIFont.FamilyNames.SelectMany(familyName => UIFont.FontNamesForFamilyName(familyName)).ToList().OrderBy(fontName => fontName);
Console.WriteLine(string.Join("\n", availableFontNames));

Quite a few fonts are already ready to go for your apps to use in iOS. Given that, you may have trouble finding the name of your new font in that large list. It’s definitely worth trying the Font Book approach before you verify your project is configured correctly using this approach.

Debugging to get font names available to your app

Determining character codes

Once you figure out what characters from your font that you want to use, you will need the hexadecimal character codes to use them as strings. While I do tend to look for simple solutions to problems, I haven’t found a simple solution to determining the character codes for a font yet. I often do a silly dance of installing the font in Windows and viewing it within the Character Map application.

Looking up character codes in the Windows Character Map

Since font icons often use the Unicode private use space, you may have to scroll around to find the characters you want to use. Once you find the character you want to use, write down its character code, F0FB in the case of the fighter jet in the screenshot.

Sometimes you get lucky with an icon font. Font Awesome has a handy listing of icons on their site. If you use your browser’s development tools to inspect the desired icon, you can usually find the hexadecimal code in the CSS.

Inspecting CSS of desired character use on the web

Once you find the appropriate CSS rule, grab the hexadecimal character code.

Looking up character codes in browser CSS rule

From there, take the character code and use it as any normal char (or string) in your C# code.

static readonly UIFont IconFont = UIFont.FromName("FontAwesome", 40f);
//...
char jet = '\uf0fb';
var label = new UILabel(RectangleF.Empty) {
    Font = IconFont,
    Text = jet.ToString(),
    TextColor= someForeColor,
    BackgroundColor = someBackgroundColor,
};
label.SizeToFit();
Add(label);

If you happen to see an unusual “W”-looking character, you probably forgot to set it to use your icon font.

Missing font character fail

Adjusting Your Icon Size

Since your new icons aren’t plain images with pre-determined sizes, you need to figure out the size you want to use. If you can get away with it, figure out your font point size beforehand and roll with it as we did above. If you sizing requirements are more fluid, you will have to put a little more effort into it.

I’ve tried a few different approaches trying to get flexible sizing to work. First, I tried the direct route: set the font size overly large and toggle AdjustsFontSizeToFitWidth to have the system do the hard work. There may be a way to get this to work, but I had less-than-ideal results with that approach.

Icon font sizing fail with AdjustsFontSizeToFitWidth

At some point in playing with iOS, you will probably need to know the size of a string before it ends up on the screen. This is where the various overloads for NSString.StringSize come in. There is even an overload that appears to be designed to return the maximum font size for a given width, but I couldn’t get it to return a useful value. Instead, I created a hack function that calculated the largest font size by trying incrementally larger sizes until StringSize came out too large.

// CAUTION: total hack!
public static float GetMaxFontSize(this string source, UIFont font, SizeF sizeRestriction) {
    float maxFontSize = font.PointSize;
    SizeF latest = SizeF.Empty;
    using (NSString nssDescriptionWithoutHtml = new NSString(source.ToString())) {
        while (latest.Width < sizeRestriction.Width && latest.Height < sizeRestriction.Height) {
            latest = nssDescriptionWithoutHtml.StringSize(font.WithSize(maxFontSize), sizeRestriction.Width, UILineBreakMode.Clip);
            if (latest.Width < sizeRestriction.Width && latest.Height < sizeRestriction.Height) {
                maxFontSize += 0.1f;
            }
        }
    }
    return maxFontSize;
}

Using a font size calculation to determine the largest possible size for a given area

Demo

In the demo, you’ll see a UILabel used with a custom icon font (the jet icon seen here). At the bottom is a slider that controls the size of the label, showing how icon fonts can make for sharp icons at any size. As well, a UIButton with a “refresh” icon is there. When tapped, it spins the jet icon using the rotate animation code from one of my first Xamarin.iOS blog posts.

Demo Source Code

For more details on the font-sizing code or any of the rest of this post’s code, just pop over to the Abusing UIKit for Gaming GitHub repo. The custom font code project has been added to this project, and I’ll be adding new projects to that solution as I add posts to this series.

Picking a font, making your own icon font, resources

When it comes time to finding your own icon font, there are a lot of choices. Font Awesome is nice, and the price and license is nice (read: free). If it meets your needs, you are good to go. If you need something different, though, there are a lot of great icon fonts out there to use.

If you already have your own vector assets, there is also the choice of making your own icon font. Trello switched to an icon font at one point. They even put out a nice blog post about it.

One last shameless plug…

Find Smudges on the App Store, available for iPhone, iPad, or iPod Touch. It’s especially fun on iPad where the hardware allows for ten simultaneous touch points.

Abusing UIKit for Gaming in Xamarin.iOS, Part 1: Detecting Taps and Placing Views with UITapGestureRecognizer

This is the first in a series of Abusing UIKit blog posts giving some background on the development that want into producing Smudges, a simple game written entirely in Xamarin.iOS where fun shapes in various colors show up on the screen wherever a tap is detected. It was original created to give my two-year-old something fun to play while going tap-crazy on the screen. The game evolved from those “play-testing” sessions. If you have your own little ones and want something fun to distract them, Smudges is availabe on the App Store. At this point, I plan to continue adding features to it as I can. Let me know what you think about Smudges, or these blog posts, in the comments below or find @patridgedev on Twitter.

Demo animation of placing views with a UITapGestureRecognizer.

Where Did They Touch?

Smudges has a simple game mechanic: tap the screen, new shape appears. The first step is figuring out when and where a tap occurred. The simple approach is to put a UIButton where you need to detect a touch, attaching a handler to its TouchUpInside event.

UIButton someButton = new UIButton(someFrameRectangle);
AddSubview(someButton);
someButton.TouchUpInside += (sender, e) => {
    Debug.WriteLine("Touched somewhere on this button.");
};

Of course, while incredibly simple, this is only practical for interface elements; it’s not so great for doing something at the exact location of a tap. To know the X-Y location where the user’s finger is touching the screen, you need to wander into UITapGestureRecognizer territory (or beyond).

UITapGestureRecognizer tapRecognizer = new UITapGestureRecognizer() {
    NumberOfTapsRequired = 1, // default is 1, adjust as needed
};
tapRecognizer.AddTarget((sender) => {
    PointF location = tapRecognizer.LocationOfTouch(0, MainView);
    Debug.WriteLine("Touched: {0}", location);
});
someContainerView.AddGestureRecognizer(tapRecognizer);

Simple enough as well, and a great choice for double-taps…and, I suppose, three-finger triple-taps, if you want to get crazy.

Placing the View

Once you have the tap location, doing something with it is fairly trivial.

tapRecognizer.AddTarget((sender) => {
    PointF location = tapRecognizer.LocationOfTouch(0, MainView);
    UIView newView = new UIView(new RectangleF(location, new SizeF(50f, 50f))) {
        BackgroundColor = UIColor.Red,
    };
    someContainerView.AddView(newView);
});

This will happily place a 50×50 rectangle on the screen with its top-left corner sitting right where the tap was detected. Centering on that location was my goal, which is easy enough by setting the new view’s Center instead of a full frame.

tapRecognizer.AddTarget((sender) => {
    PointF location = tapRecognizer.LocationOfTouch(0, MainView);
    UIView newView = new UIView() {
        Size = new SizeF(50f, 50f),
        Center = location,
        BackgroundColor = UIColor.Red,
    };
    someContainerView.AddView(newView);
});

Make it Pretty

One thing I tend to do in demos of any kind is introduce some randomness to the mix. It keeps things much more interesting to everyone, especially me while I am running it for the thousandth time to get everything just right. In this case, let’s pick a random color for each one.

static Random rand = new Random();
public static UIColor GetRandomColor() {
    int red = rand.Next(255);
    int green = rand.Next(255);
    int blue = rand.Next(255);
    UIColor color = UIColor.FromRGBA(
        (red / 255.0f),
        (green / 255.0f),
        (blue / 255.0f),
        1f);
    return color;
}

If you need random numbers that hold up over time, you will want to investigate better random number generators, but System.Random serves its purpose well for these uses. Throwing all that into the mix, we now get a new “fun” gesture recognizer target that places a rectangle of a random color centered right at the touch location.

UITapGestureRecognizer tapRecognizer = new UITapGestureRecognizer() {
    NumberOfTapsRequired = 1
};
tapRecognizer.AddTarget((sender) => {
    PointF location = tapRecognizer.LocationOfTouch(0, MainView);
    UIView newView = new UIView() {
        Size = new SizeF(50f, 50f),
        Center = location,
        BackgroundColor = GetRandomColor(),
    };
    someContainerView.AddView(newView);
});
someContainerView.AddGestureRecognizer(tapRecognizer);

Caution: Memory Leak Ahead

When you go adding views all the time, with enough time, you are bound to run up against memory pressures. The simplest solution is to keep track of added views so you can remove them later. In this demo, I simply schedule a task to do so after some time elapses. In a later demo, I will have a simple demo using a Queue that does just that. Another option is a pool of views, something many game engines offer directly. There’s lots of room for turning this potential limitation into something fun.

Beyond UITapGestureRecognizer

This version allows the detection of a single finger tapping. If you use two fingers to tap at the same time, nothing will happen. In a later post, I will address recognizing simultaneous touches.

Demo Source Code

For details on the view queueing system or any of the rest of this post’s code, just pop over to the GitHub repo for the Abusing UIKit for Gaming in Xamarin.IOS series. I’ll be adding new projects to that solution as I add posts to this series.

One last shameless plug…

Find Smudges on the App Store, available for iPhone, iPad, or iPod Touch. It’s especially fun on iPad where the hardware allows for ten simultaneous touch points.

Capturing Your iOS App in Animated GIF Glory

LICEcap Capture at 30fps

Showing the coolness of your iOS app in a web format can be very difficult, depending on what about your app makes it shine. If your app thrives on animation, especially the new UIKit Dynamics fun, you will need more than one frame to portray what your app does: enter the animated GIF, mother of all awesomeness.

Here is the method I used to make the images for my Xamarin UIGravityBehavior recipe. That said, if you know a better way to do this, toss a comment out there; I’d love to hear about it.

While I use these methods for my Xamarin.iOS creations, they apply equally to native apps and most anything running on a Mac.

The Tool: LICEcap

While the name sounds slightly…off, it definitely gets the job done. LICEcap, from Cockos Incorporated, is a quick disk image install. In fact, here’s LICEcap’s capture of me installing LICEcap.

Installing LICEcap

One thing to notice, these GIFs are not a great way to capture complex color palettes like the gradients in the install image folder. There may be a tool that is better at that, but you will probably compromise file size for fidelity. That install GIF was 81kb.

Capturing the iOS Simulator

With LICEcap, you capture an arbitrary portion of the screen, so you can capture just a portion of your app all the way to the full simulator with your Twitter feed rolling in the background around it. LICEcap loads as a transparent window, looking eerily as if something failed to render. Instead, what the program frames is what will be captured. You pull the various window edges and corners as needed to frame your target. It can be maddening to adjust it just right if you are shooting for specific pixels. You can also set exact pixel measurements with the two bottom inputs.

Using LICEcap to capture an animated GIF

After you set your capture area, you can set a maximum frames per second (FPS) as well. For representative purposes, it seemed I needed fewer FPS than I thought. The resulting file size variation from FPS settings will depend heavily on your recording length and the complexity of the graphics. For my UIGravityBehavior recipe, practically ideal for GIF compression, it wasn’t a big difference from 5fps to 30fps. For some animations, file size can get substantial fairly quickly.

LICEcap Capture at 30fps
LICEcap Capture at 5fps
FPSFile Size
5fps24kb
10fps43kb
15fps46kb
20fps66kb
25fps67kb
30fps68kb

Unless you want a huge recording, you probably want to scale the Simulator to 50% with Command+3. For a full-app capture, you will end up getting the bottom rounded corners of the window unless you crop a few pixels. For most apps, trimming those pixels doesn’t seem to be a loss. At half scale, I find setting LICEcap to 318×564 then lining up with a top corner works quite well. Doing so highlights one frustration with the program. When capturing skinny views with exact dimensions, such as the iOS Simulator in portrait, you need to enter your height first or the field disappears before you can give it a value.

Once you are all set, hit the record button. Give your file a name and a location. You will also have a few options here that may be of interest, such as showing a circle for mouse button presses or loop repeat count. When you are all set hit “Save” and you will get a 3-2-1 countdown in the title bar before recording begins. After everything is complete, simply hit “Stop” to save the result. (You can also pause and resume during a recording.)

Trimming the result

If you find you have a bunch of extra frames in your animation, open your GIF in Preview. From there you can selecting the offending frames and hit delete. But, once you edit in Preview, any infinite looping you had will be gone. I found Gifsicle, a command line utility, fixed this for me quite nicely.

gifsicle -bl ~/path/to/your/file.gif

Other Capture Methods

Still Images

If you really just need a static screenshot, don’t forget the baked-in offerings. The easiest shots can be done in the iOS Simulator, a simple Command+S (File: Save Screen Shot) will save a PNG result of your app to the desktop.

Need to capture the Simulator among other windows? You capture the full screen with Command+Shift+3 and can then crop to your desired size. To capture a partial screenshot, hit Command+Shift+4 and draw what to capture with the crosshairs. These images are also saved to your desktop, time-stamped, but they are formatted “Screen Shot {yyyy-MM-dd} at {h.mm.ss tt}”.

Capturing a Video

If you need a video to do your app justice, by all means, let me introduce you to this neat tool included on your Mac for free: QuickTime Player. Load it up and select “File”, then “New Screen Recording”. Here’s where I would embed a video of me doing that, but you can’t start a new screen recording while doing a screen recording.

Using QuickTime Player for Screen Recordings

Other Options?

Utilities such as Jing capture in a Flash-based format, but I have mixed feelings about requiring Flash, no matter how common it may be. While the animation captures I tried with it were smooth and the color gradients captured fine, the file sizes got substantial quickly.

If there is some awesome, affordable or free tool out there that you use to make capturing animations insanely easy? I’d love to hear about it.

Xamarin.iOS C# Recipe: Animating Views with iOS 7 UIGravityBehavior (UIKit Dynamics)

UIGravityBehavior, oddly addictive

Now that iOS 7 has landed, and Xamarin gave us same-day C# support, it’s time to start poking at the new bits. One such piece is UIKit Dynamics. With UIKit Dynamics, you can greatly simplify all sorts of view animations. While this simple recipe will only address UIGravityBehavior, iOS 7 adds a bunch of other predefined behaviors and allows the creation of custom ones as well.

Source code is available on GitHub.

Basic Gravity Animation

To have a view simply start falling off the screen, you just toss the desired view at a new UIGravityBehavior and add that behavior to a UIDynamicAnimator.

UIDynamicAnimator animator;
public override void ViewDidLoad() {
    base.ViewDidLoad();
    View.BackgroundColor = UIColor.White;
    animator = new UIDynamicAnimator(View);

    var item = new UIView(new RectangleF(new PointF(50f, 0f), new SizeF(50f, 50f))) {
        BackgroundColor = UIColor.Blue,
    };
    View.Add(item);
    UIGravityBehavior gravity = new UIGravityBehavior(item);
    animator.AddBehavior(gravity);
}

Basic UIGravityBehavior

You can continue to put items under the effect of gravity by adding them to the behavior.

gravity.AddItem(someOtherView);

That’s really it for basic gravity, but craziness is only a step beyond that. You can modify the Angle, GravityDirection, and Magnitude of your gravity as well.

Potential Memory/Performance Trap

It’s worth noting that those items that go flying off the screen under the effects of gravity will continue to keep falling, even though they are no longer rendered on the screen. If you need to worry about resources, you could be computing locations for things long since forgotten.

Customizing Gravity

The three UIGravityBehavior properties, Angle, GravityDirection, and Magnitude, can all be changed to your liking and they are interconnected, changing one can affect the others.

Magnitude

The default gravity acceleration is 1000 pixels/sec^2. You could very easily bump the gravity to Jupiter levels (roughly 2.5 times that of Earth).

UIGravityBehavior gravity = new UIGravityBehavior(item) {
    Magnitude = 25000,
};

Direction (GravityDirection and Angle)

You can change the direction of gravity with two different interconnected variables, Angle and GravityDirection; change one and you affect the other. The default gravity angle pulls straight down, as measured in radians from the righthand side: PI / 2 radians, approximately 1.57. This is also expressed by the gravity direction vector, the default being (0, 1). Gravity’s direction can be adjusted to your whim, and reversing it is quite simple using a vector pointing the opposite direction.

UIGravityBehavior gravity = new UIGravityBehavior(item) {
    GravityDirection = new CGVector(0, -1),
};

Alternatively, with more math involved, you can set the radian angle.

UIGravityBehavior gravity = new UIGravityBehavior(item) {
    Angle = -(float)Math.PI / 2,
};

Action (Interacting with the Animation)

While not strictly a property for modifying gravity, UIGravityBehavior, like all the other behaviors in UIKit Dynamics, allows you to execute some code for each animation frame using its Action. You could use it for debugging a wonky behavior or for cleaning up views that you may have let fly off the screen.

UIGravityBehavior gravity = new UIGravityBehavior(item) {
    Action = () => {
        Console.WriteLine(item.Frame.Location);
    }
};

Bending Gravity to your Will (Advanced Recipe)

Having control over gravity can quickly go to your head. In the off chance you need to change gravity’s effect on your UI elements on demand, check out the secondary controller in the demo code. In this proof-of-concept, you tap to place an object under gravity’s control, simple enough with a UITapGestureRecognizer.

View.AddGestureRecognizer(new UITapGestureRecognizer((gesture) => {
    PointF tapLocation = gesture.LocationInView(View);
    var item = new UIView(new RectangleF(PointF.Empty, sizeInitial)) {
        BackgroundColor = ColorHelpers.GetRandomColor(),
        Center = tapLocation,
    };
    items.Enqueue(item);
    View.Add(item);
    gravity.AddItem(item);

    // ...
}));

Any time you want, though, you can swipe to alter its direction using a series of UISwipeGestureRecognizers (each one can only recognize a single swipe direction).

View.AddGestureRecognizer(new UISwipeGestureRecognizer((gesture) => {
    gravity.GravityDirection = new CGVector(1, 0);
}) { Direction = UISwipeGestureRecognizerDirection.Right, });
View.AddGestureRecognizer(new UISwipeGestureRecognizer((gesture) => {
    gravity.GravityDirection = new CGVector(-1, 0);
}) { Direction = UISwipeGestureRecognizerDirection.Left, });
View.AddGestureRecognizer(new UISwipeGestureRecognizer((gesture) => {
    gravity.GravityDirection = new CGVector(0, -1);
}) { Direction = UISwipeGestureRecognizerDirection.Up, });
View.AddGestureRecognizer(new UISwipeGestureRecognizer((gesture) => {
    gravity.GravityDirection = new CGVector(0, 1);
}) { Direction = UISwipeGestureRecognizerDirection.Down, });

I’m not sure I would call it a “game”, but I will admit to spending far more time poking at the screen than was necessary.

Controlling UIGravityBehavior by swipe

Keep Learning

Xamarin has put together a fantastic C# iOS 7 introduction and a great overview on iOS 7 UI changes. Expect more great iOS 7 C# tutorials to come out of Xamarin’s docs team as well as others participating in the Xamarin Recipe Cook-off. (Disclaimer: I am probably getting a free Xamarin t-shirt for this post.)

UIKit Dynamics are far more than just this one pre-baked gravity behavior. I highly recommend checking out the 2013 WWDC videos: Getting Started with UIKit Dynamics and Advanced Techniques with UIKit Dynamics.

Making MonoDevelop for Mac (or Xamarin Studio) more like Visual Studio

If I were only living in the land of MonoDevelop, I probably wouldn’t care. I stil spend a large amount of time using Visual Studio, though, and switching contexts becomes very difficult when the IDEs are so different. While I haven’t gone as far as to swap the command and alt keys to match keyboard behavior on Windows, I do try to unite things as much as possible.

If you are simply looking for a list of the default MonoDevelop keyboard shortcuts to learn, check out something more like this post from Dan Quirk.

[Note: Most of these things apply to Xamarin Studio as well.]

Text editing

Word break mode

This made the biggest difference in my tolerance for MonoDevelop. The default word break mode tends to ignore whitespace, making it behave completely differently moving the cursor forward versus backward in text.

Preferences->Text Editor->Behavior; Navigation->Word break mode; set to SharpDevelop.

This will bring your option+left/right and option+backspace/delete calls to be more in line with the ctrl versions under Visual Studio. The emacs version looks like it would work, but I opted for SharpDevelop.

Key bindings

ctrl+Space

Bring up contextual menu for Intellisense (Edit.CompleteWord in VS, “Show Completion Window” in MD).

ctrl+.

Bring up contextual menu to resolve with using statement (View.ShowSmartTag in VS, “Quick fix…” in MD).

VS: ctrl+. MD: option+enter (does not appear to work)

Changes:

Since the default MD shortcut doesn’t work, you definitely want to try something. Setting this to the VS default, though, will cause the following cascade of changes.

  • Command+. is already “Navigate To…” in MD (VS uses ctrl+,).
  • Command+, is already “Preferences…” in MD (VS default doesn’t bind anything to Tools.Options but command+,, pretty standard Mac shortcut).
  • Remove command+, from “Preferences…”.

Export/Import settings

Once you have MonoDevelop just the way you want it, some day you will likely need to move them to a different machine (or give them to a like-minded colleague). You’ll need to dig around your user library to find these, but they can be copied into and out of ~/Library/MonoDevelop-3.0 (or whatever version you are using). Xamarin Studio will be under ~/Library/XamarinStudio-{version}.

For just specific preferences, you may be able to move around just a single file.

  • Key bindings: ~/Library/MonoDevelop-3.0/KeyBindings/Custom.mac-kb.xml
  • C# formatting profile: ~/Library/MonoDevelop-3.0/Policies/Default.mdpolicy.xml

Sources

Versions

Since these things are likely to change, let me add the versions of these applications. If things have become too dated here, feel free to drop me a line (comments, email, whatever).

  • Mac OS: 10.7.5
  • MonoDevelop: 3.0.4.7
  • MonoTouch: 3.0.4