5
\$\begingroup\$

I'm very sorry if this question has already been answered, but since I've been struggling with this for many days I thought I'd give it a shot here.

My goal is to somehow render some text on the screen, so I could make a score counter for my game and also maybe add some informative text. If someone could at least point me in the right direction, then I would be very grateful.

Tried to draw text using Bitmaps, but haven't had any luck doing it.

I'm using LWJGL3 and lwjgl-utils.jar for GLU.

So I got this code

package com.thecherno.flappy.font;

import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferInt;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import java.util.HashMap;
import java.util.Map;
import java.awt.GraphicsEnvironment;

import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.util.glu.GLU;

import static org.lwjgl.glfw.GLFW.glfwGetCurrentContext;


/**
 * A TrueType font implementation originally for Slick, edited for Bobjob's Engine
 *
 * @original author James Chambers (Jimmy)
 * @original author Jeremy Adams (elias4444)
 * @original author Kevin Glass (kevglass)
 * @original author Peter Korzuszek (genail)
 *
 * @new version edited by David Aaron Muhar (bobjob)
 */
public class TrueTypeFont {
public final static int
        ALIGN_LEFT = 0,
        ALIGN_RIGHT = 1,
        ALIGN_CENTER = 2;
/** Array that holds necessary information about the font characters */
private IntObject[] charArray = new IntObject[256];

/** Map of user defined font characters (Character <-> IntObject) */
private Map customChars = new HashMap();

/** Boolean flag on whether AntiAliasing is enabled or not */
private boolean antiAlias;

/** Font's size */
private int fontSize = 0;

/** Font's height */
private int fontHeight = 0;

/** Texture used to cache the font 0-255 characters */
private int fontTextureID;

/** Default font texture width */
private int textureWidth = 512;

/** Default font texture height */
private int textureHeight = 512;

/** A reference to Java's AWT Font that we create our font texture from */
private Font font;

/** The font metrics for our Java AWT font */
private FontMetrics fontMetrics;


private int correctL = 9, correctR = 8;

private class IntObject {
    /** Character's width */
    public int width;

    /** Character's height */
    public int height;

    /** Character's stored x position */
    public int storedX;

    /** Character's stored y position */
    public int storedY;
}


public TrueTypeFont(Font font, boolean antiAlias, char[] additionalChars) {
    this.font = font;
    this.fontSize = font.getSize()+3;
    this.antiAlias = antiAlias;

    createSet( additionalChars );

    fontHeight -= 1;
    if (fontHeight <= 0) fontHeight = 1;
}

public TrueTypeFont(Font font, boolean antiAlias) {
    this( font, antiAlias, null );
}
public void setCorrection(boolean on) {
    if (on) {
        correctL = 2;
        correctR = 1;
    } else {
        correctL = 0;
        correctR = 0;
    }
}
private BufferedImage getFontImage(char ch) {
    // Create a temporary image to extract the character's size
    BufferedImage tempfontImage = new BufferedImage(1, 1,
            BufferedImage.TYPE_INT_ARGB);
    Graphics2D g = (Graphics2D) tempfontImage.getGraphics();
    if (antiAlias == true) {
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);
    }
    g.setFont(font);
    fontMetrics = g.getFontMetrics();
    int charwidth = fontMetrics.charWidth(ch)+8;

    if (charwidth <= 0) {
        charwidth = 7;
    }
    int charheight = fontMetrics.getHeight()+3;
    if (charheight <= 0) {
        charheight = fontSize;
    }

    // Create another image holding the character we are creating
    BufferedImage fontImage;
    fontImage = new BufferedImage(charwidth, charheight,
            BufferedImage.TYPE_INT_ARGB);
    Graphics2D gt = (Graphics2D) fontImage.getGraphics();
    if (antiAlias == true) {
        gt.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);
    }
    gt.setFont(font);

    gt.setColor(Color.WHITE);
    int charx = 3;
    int chary = 1;
    gt.drawString(String.valueOf(ch), (charx), (chary)
            + fontMetrics.getAscent());

    return fontImage;

}

private void createSet( char[] customCharsArray ) {
    // If there are custom chars then I expand the font texture twice
    if  (customCharsArray != null && customCharsArray.length > 0) {
        textureWidth *= 2;
    }

    // In any case this should be done in other way. Texture with size 512x512
    // can maintain only 256 characters with resolution of 32x32. The texture
    // size should be calculated dynamicaly by looking at character sizes.

    try {

        BufferedImage imgTemp = new BufferedImage(textureWidth, textureHeight, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = (Graphics2D) imgTemp.getGraphics();

        g.setColor(new Color(0,0,0,1));
        g.fillRect(0,0,textureWidth,textureHeight);

        int rowHeight = 0;
        int positionX = 0;
        int positionY = 0;

        int customCharsLength = ( customCharsArray != null ) ? customCharsArray.length : 0;

        for (int i = 0; i < 256 + customCharsLength; i++) {

            // get 0-255 characters and then custom characters
            char ch = ( i < 256 ) ? (char) i : customCharsArray[i-256];

            BufferedImage fontImage = getFontImage(ch);

            IntObject newIntObject = new IntObject();

            newIntObject.width = fontImage.getWidth();
            newIntObject.height = fontImage.getHeight();

            if (positionX + newIntObject.width >= textureWidth) {
                positionX = 0;
                positionY += rowHeight;
                rowHeight = 0;
            }

            newIntObject.storedX = positionX;
            newIntObject.storedY = positionY;

            if (newIntObject.height > fontHeight) {
                fontHeight = newIntObject.height;
            }

            if (newIntObject.height > rowHeight) {
                rowHeight = newIntObject.height;
            }

            // Draw it here
            g.drawImage(fontImage, positionX, positionY, null);

            positionX += newIntObject.width;

            if( i < 256 ) { // standard characters
                charArray[i] = newIntObject;
            } else { // custom characters
                customChars.put( new Character( ch ), newIntObject );
            }

            fontImage = null;
        }

        fontTextureID = loadImage(imgTemp);



        //.getTexture(font.toString(), imgTemp);

    } catch (Exception e) {
        System.err.println("Failed to create font.");
        e.printStackTrace();
    }
}

private void drawQuad(float drawX, float drawY, float drawX2, float drawY2,
                      float srcX, float srcY, float srcX2, float srcY2) {
    float DrawWidth = drawX2 - drawX;
    float DrawHeight = drawY2 - drawY;
    float TextureSrcX = srcX / textureWidth;
    float TextureSrcY = srcY / textureHeight;
    float SrcWidth = srcX2 - srcX;
    float SrcHeight = srcY2 - srcY;
    float RenderWidth = (SrcWidth / textureWidth);
    float RenderHeight = (SrcHeight / textureHeight);

    GL11.glTexCoord2f(TextureSrcX, TextureSrcY);
    GL11.glVertex2f(drawX, drawY);
    GL11.glTexCoord2f(TextureSrcX, TextureSrcY + RenderHeight);
    GL11.glVertex2f(drawX, drawY + DrawHeight);
    GL11.glTexCoord2f(TextureSrcX + RenderWidth, TextureSrcY + RenderHeight);
    GL11.glVertex2f(drawX + DrawWidth, drawY + DrawHeight);
    GL11.glTexCoord2f(TextureSrcX + RenderWidth, TextureSrcY);
    GL11.glVertex2f(drawX + DrawWidth, drawY);
}

public int getWidth(String whatchars) {
    int totalwidth = 0;
    IntObject intObject = null;
    int currentChar = 0;
    for (int i = 0; i < whatchars.length(); i++) {
        currentChar = whatchars.charAt(i);
        if (currentChar < 256) {
            intObject = charArray[currentChar];
        } else {
            intObject = (IntObject)customChars.get( new Character( (char) currentChar ) );
        }

        if( intObject != null )
            totalwidth += intObject.width;
    }
    return totalwidth;
}

public int getHeight() {
    return fontHeight;
}


public int getHeight(String HeightString) {
    return fontHeight;
}

public int getLineHeight() {
    return fontHeight;
}

public void drawString(float x, float y,
                       String whatchars, float scaleX, float scaleY) {
    drawString(x,y,whatchars, 0, whatchars.length()-1, scaleX, scaleY, ALIGN_LEFT);
}
public void drawString(float x, float y,
                       String whatchars, float scaleX, float scaleY, int format) {
    drawString(x,y,whatchars, 0, whatchars.length()-1, scaleX, scaleY, format);
}


public void drawString(float x, float y,
                       String whatchars, int startIndex, int endIndex,
                       float scaleX, float scaleY,
                       int format
) {

    IntObject intObject = null;
    int charCurrent;


    int totalwidth = 0;
    int i = startIndex, d, c;
    float startY = 0;



    switch (format) {
        case ALIGN_RIGHT: {
            d = -1;
            c = correctR;

            while (i < endIndex) {
                if (whatchars.charAt(i) == '\n') startY -= fontHeight;
                i++;
            }
            break;
        }
        case ALIGN_CENTER: {
            for (int l = startIndex; l <= endIndex; l++) {
                charCurrent = whatchars.charAt(l);
                if (charCurrent == '\n') break;
                if (charCurrent < 256) {
                    intObject = charArray[charCurrent];
                } else {
                    intObject = (IntObject)customChars.get( new Character( (char) charCurrent ) );
                }
                totalwidth += intObject.width-correctL;
            }
            totalwidth /= -2;
        }
        case ALIGN_LEFT:
        default: {
            d = 1;
            c = correctL;
            break;
        }

    }

    GL11.glBindTexture(GL11.GL_TEXTURE_2D, fontTextureID);
    GL11.glBegin(GL11.GL_QUADS);

    while (i >= startIndex && i <= endIndex) {

        charCurrent = whatchars.charAt(i);
        if (charCurrent < 256) {
            intObject = charArray[charCurrent];
        } else {
            intObject = (IntObject)customChars.get( new Character( (char) charCurrent ) );
        }

        if( intObject != null ) {
            if (d < 0) totalwidth += (intObject.width-c) * d;
            if (charCurrent == '\n') {
                startY -= fontHeight * d;
                totalwidth = 0;
                if (format == ALIGN_CENTER) {
                    for (int l = i+1; l <= endIndex; l++) {
                        charCurrent = whatchars.charAt(l);
                        if (charCurrent == '\n') break;
                        if (charCurrent < 256) {
                            intObject = charArray[charCurrent];
                        } else {
                            intObject = (IntObject)customChars.get( new Character( (char) charCurrent ) );
                        }
                        totalwidth += intObject.width-correctL;
                    }
                    totalwidth /= -2;
                }
                //if center get next lines total width/2;
            }
            else {
                drawQuad((totalwidth + intObject.width) * scaleX + x, startY * scaleY + y,
                        totalwidth * scaleX + x,
                        (startY + intObject.height) * scaleY + y, intObject.storedX + intObject.width,
                        intObject.storedY + intObject.height,intObject.storedX,
                        intObject.storedY);
                if (d > 0) totalwidth += (intObject.width-c) * d ;
            }
            i += d;

        }
    }
    GL11.glEnd();
}
public static int loadImage(BufferedImage bufferedImage) {
    try {
        short width       = (short)bufferedImage.getWidth();
        short height      = (short)bufferedImage.getHeight();
        //textureLoader.bpp = bufferedImage.getColorModel().hasAlpha() ? (byte)32 : (byte)24;
        int bpp = (byte)bufferedImage.getColorModel().getPixelSize();
        ByteBuffer byteBuffer;
        DataBuffer db = bufferedImage.getData().getDataBuffer();
        if (db instanceof DataBufferInt) {
            int intI[] = ((DataBufferInt)(bufferedImage.getData().getDataBuffer())).getData();
            byte newI[] = new byte[intI.length * 4];
            for (int i = 0; i < intI.length; i++) {
                byte b[] = intToByteArray(intI[i]);
                int newIndex = i*4;

                newI[newIndex]   = b[1];
                newI[newIndex+1] = b[2];
                newI[newIndex+2] = b[3];
                newI[newIndex+3] = b[0];
            }

            byteBuffer  = ByteBuffer.allocateDirect(
                    width*height*(bpp/8))
                    .order(ByteOrder.nativeOrder())
                    .put(newI);
        } else {
            byteBuffer  = ByteBuffer.allocateDirect(
                    width*height*(bpp/8))
                    .order(ByteOrder.nativeOrder())
                    .put(((DataBufferByte)(bufferedImage.getData().getDataBuffer())).getData());
        }
        byteBuffer.flip();


        int internalFormat = GL11.GL_RGBA8,
                format = GL11.GL_RGBA;
        IntBuffer   textureId =  BufferUtils.createIntBuffer(1);;
        GL11.glGenTextures(textureId);
        GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureId.get(0));


        GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP);
        GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP);

        GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
        GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);

        GL11.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_MODULATE);



        GLU.gluBuild2DMipmaps(GL11.GL_TEXTURE_2D,
                internalFormat,
                width,
                height,
                format,
                GL11.GL_UNSIGNED_BYTE,
                byteBuffer);
        return textureId.get(0);

    } catch (Exception e) {
        e.printStackTrace();
        System.exit(-1);
    }

    return -1;
}
public static boolean isSupported(String fontname) {
    Font font[] = getFonts();
    for (int i = font.length-1; i >= 0; i--) {
        if (font[i].getName().equalsIgnoreCase(fontname))
            return true;
    }
    return false;
}
public static Font[] getFonts() {
    return GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts();
}
public static byte[] intToByteArray(int value) {
    return new byte[] {
            (byte)(value >>> 24),
            (byte)(value >>> 16),
            (byte)(value >>> 8),
            (byte)value};
}

public void destroy() {
    IntBuffer scratch = BufferUtils.createIntBuffer(1);
    scratch.put(0, fontTextureID);
    GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0);
    GL11.glDeleteTextures(scratch);
}
}

And my Main class looks like this

package com.thecherno.flappy;

import com.thecherno.flappy.font.BitmapFonts;
import com.thecherno.flappy.font.Ley2dFontLWJGL3;
import com.thecherno.flappy.font.TrueTypeFont;
import com.thecherno.flappy.graphics.Shader;
import com.thecherno.flappy.input.Input;
import com.thecherno.flappy.level.Level;
import com.thecherno.flappy.maths.Matrix4f;
import org.lwjgl.glfw.GLFWKeyCallback;
import org.lwjgl.glfw.GLFWvidmode;
import org.lwjgl.opengl.GLContext;
import org.lwjgl.util.glu.GLU;

import java.awt.*;
import java.io.IOException;
import java.nio.ByteBuffer;

import static  org.lwjgl.opengl.GL11.*;
import static  org.lwjgl.opengl.GL13.*;
import static  org.lwjgl.system.MemoryUtil.*;

import static org.lwjgl.glfw.GLFW.*;

public class Main implements Runnable {

private int width = 1280;
private int height = 720;

private Thread thread;
private boolean running = false;

// Setting key callback
private GLFWKeyCallback keyCallback;

private long window; // A "pointer" to memory address of window(id)

private Level level;

public void start() {
    running = true;
    thread = new Thread(this, "Game");
    thread.start(); // This will run the run() method.
}

private void init() {

    if (glfwInit() != GL_TRUE) {
        System.err.println("Could not initialize GLFW!");
        return;
    }

    glfwWindowHint(GLFW_RESIZABLE, GL_TRUE);
    window = glfwCreateWindow(width, height, "Flappy", NULL, NULL);
    if (window == NULL) {
        System.err.println("Could not  create GLFW window!");
        return;
    }

    glfwSetKeyCallback(window, keyCallback = new Input());

    ByteBuffer vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor());
    glfwSetWindowPos(window, (GLFWvidmode.width(vidmode) - width) / 2, (GLFWvidmode.height(vidmode) - height) / 2);

    glfwMakeContextCurrent(window);
    glfwShowWindow(window);
    GLContext.createFromCurrent();

    // GLInit
    glEnable(GL_COLOR_MATERIAL);
    glEnable(GL_TEXTURE_2D); // Enable Texture Mapping
    glClearColor(0.5f,0.5f,0.5f,0f); // Black Background
    glDisable(GL_DITHER);
    glDepthFunc(GL_LESS); // Depth function less or equal
    glEnable(GL_NORMALIZE); // calculated normals when scaling
//        glEnable(GL_CULL_FACE); // prevent render of back surface
    glEnable(GL_ALPHA_TEST); // allows alpha channels or transperancy
    glAlphaFunc(GL_GREATER, 0.1f); // sets aplha function
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // High quality visuals
    glHint(GL_POLYGON_SMOOTH_HINT, GL_NICEST); //  Really Nice Perspective Calculations
    glShadeModel(GL_SMOOTH); // Enable Smooth Shading
//        glViewport(0, 0, 800, 600);
    glMatrixMode(GL_PROJECTION); // Select The Projection Matrix
    glLoadIdentity(); // Reset The Projection Matrix
    GLU.gluPerspective(30, width / (float) height, 1f, 300f);  //Aspect Ratio Of The Window
    glMatrixMode(GL_MODELVIEW); // Select The Modelview Matrix
    glDepthMask(true);                             // Enable Depth Mask
    //

    glEnable(GL_DEPTH_TEST);
    glActiveTexture(GL_TEXTURE1);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    System.out.println("OpenGL: " + glGetString(GL_VERSION));

    Shader.loadAll();
    Shader.BG.enable();

    Matrix4f pr_matrix = Matrix4f.orthographic(-10.0f, 10.0f, -10.0f * 9.0f / 16.0f, 10.0f * 9.0f / 16.0f, -1.0f, 1.0f);
    Shader.BG.setUniformMat4f("pr_matrix", pr_matrix);
    Shader.BG.setUniform1i("tex", 1);

    Shader.BIRD.setUniformMat4f("pr_matrix", pr_matrix);
    Shader.BIRD.setUniform1i("tex", 1);

    Shader.PIPE.setUniformMat4f("pr_matrix", pr_matrix);
    Shader.PIPE.setUniform1i("tex", 1);

    Shader.IGTITLE.setUniformMat4f("pr_matrix", pr_matrix);
    Shader.IGTITLE.setUniform1i("tex", 1);

    Shader.BG.disable();

    level = new Level();
}

public void run() {
    init();

    long lastTime = System.nanoTime();
    double delta = 0.0;
    double ns = 1000000000.0 / 60.0;
    long timer = System.currentTimeMillis();
    int updates = 0;
    int frames = 0;

    while (running) {
        long now = System.nanoTime();
        delta += (now - lastTime) / ns;
        lastTime = now;
        if (delta >= 1.0) {
            update();
            updates++;
            delta--;
        }
        render();
        frames++;
        if (System.currentTimeMillis() - timer > 1000) {
            timer += 1000;
            System.out.println(updates + " ups, " + frames + " fps");
            updates = 0;
            frames = 0;
        }

        if (glfwWindowShouldClose(window) == GL_TRUE) {
            running = false;
        }

    }
    // Key callback thingy.
    keyCallback.release();
    glfwDestroyWindow(window);
    glfwTerminate();
}

private void update() {
    glfwPollEvents();
    /*if (Input.keys[GLFW_KEY_SPACE]) {
        System.out.println("FLAP!");
    }*/
    level.update();

    // Press Escape to exit the game.
    if (Input.isKeyDown(GLFW_KEY_ESCAPE)) {
        glfwDestroyWindow(window);
        glfwTerminate();
        System.exit(0);
    }

    if (level.isGameOver()) {
        level = new Level();
    }
}

private void render() {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    level.render();

    glMatrixMode(GL_PROJECTION);                        // Select The Projection Matrix
    glPushMatrix();                                     // Store The Projection Matrix
    glLoadIdentity();                                   // Reset The Projection Matrix
    glOrtho(0, width, 0, height, -1, 1);                          // Set Up An Ortho Screen
    glMatrixMode(GL_MODELVIEW);                         // Select The Modelview Matrix
    glPushMatrix();                                     // Store The Modelview Matrix
    glLoadIdentity();

    TrueTypeFont trueTypeFont;
    String fontName = "Agent Orange";
    if (!TrueTypeFont.isSupported(fontName)) fontName = "serif";
    Font font = new Font(fontName, Font.ITALIC | Font.BOLD, 40);
    trueTypeFont = new TrueTypeFont(font, true);
    //DRAW STRING FROM THE - CENTER
    //HALF SCALE OF 0.5f
    trueTypeFont.drawString(width / 2, trueTypeFont.getHeight() * 3,
            "at the possible sound of your name.\n" +
                    "No I wouldnt go that far.\n" +
                    "No.", 0.5f, 0.5f, TrueTypeFont.ALIGN_CENTER);

    int error = glGetError();

    if (error != GL_NO_ERROR) {
        System.out.println(error);
    }
    glfwSwapBuffers(window);
}

public static void main(String[] args) {
    new Main().start();
}
}

For rendering text I added this code in the Main class

    TrueTypeFont trueTypeFont;
    String fontName = "Agent Orange";
    if (!TrueTypeFont.isSupported(fontName)) fontName = "serif";
    Font font = new Font(fontName, Font.ITALIC | Font.BOLD, 40);
    trueTypeFont = new TrueTypeFont(font, true);
    //DRAW STRING FROM THE - CENTER
    //HALF SCALE OF 0.5f
    trueTypeFont.drawString(width / 2, trueTypeFont.getHeight() * 3,
            "at the possible sound of your name.\n" +
                    "No I wouldnt go that far.\n" +
                    "No.", 0.5f, 0.5f, TrueTypeFont.ALIGN_CENTER);

At the moment all I get are some white rectangles, so I assume that something is rendering. I've read that rendering white boxes is somehow a typical problem, but I can't find a solution. I'm not using any resources like .ttf or .png fonts/bitmaps for this code, should I? How?

Also, sorry for my english. I would be forever grateful if someone could help me out here.

Best wishes to all of You!

\$\endgroup\$
  • \$\begingroup\$ A friend sent me the post to this link. I hate seeing posts that say "do it a different way" and I know you are using Glu and trying to use TrueTypeFont, but if you aren't opposed to rolling out your own text renderer I can share some code that will do most of what you want. \$\endgroup\$ – Michael Platt Jan 13 '17 at 16:42
0
\$\begingroup\$

Don't know if still relevant, just wanted to update you that I had the very same issue and this fixed it:

(this is not my answer, all credits go to adventurerOK @ https://stackoverflow.com/questions/10064804/java-lwjgl-slick-unicodefont-acting-up)

It seems I have this error now, it will only draw the outlined rectangle before using the font. This seems to be a Slick error, and is a major problem so they should fix it before adding new features. The only work-around is to render fonts last on your frame.

EDIT: Problem Solved!

add this line after you render your font:

GL11.glDisable(GL11.GL_TEXTURE_2D);

The Slick people should really add that line in as it causes so many bugs!

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.