libtcod / python-tcod

A high-performance Python port of libtcod. Includes the libtcodpy module for backwards compatibility with older projects.
BSD 2-Clause "Simplified" License
413 stars 36 forks source link

How to implement my window resize after custrender removal #91

Closed Ape closed 4 years ago

Ape commented 4 years ago

I noticed that the experimental custrender.py feature has been removed. I had implemented a custom window resize functionality using it, but it doesn't work anymore starting with python-tcod 11.7.0. Instead, it fails with this:

AttributeError: cffi library 'tcod._libtcod' has no function, constant or global variable named 'TCOD_sys_init_sdl2_renderer_'

Here's a demo about the functionality:

resize

The tileset is always rendered pixel perfect without scaling. Small letterboxes are used when needed and window resizing affects the console size (in characters). I implemented zooming by changing to different tileset character sizes.

Is there a way to implement this functionality with the current python-tcod version?

Here's my code in case looking at it helps understanding the situation:

import numpy as np
import tcod
import tcod.console
import tcod.tileset

class Display:
    def __init__(self, game, title, resolution, fullscreen, vsync,
                 bg=(0, 0, 0)):
        self.game = game
        self.bg = bg

        window_flags = tcod.lib.SDL_WINDOW_RESIZABLE
        renderer_flags = 0

        if fullscreen:
            window_flags |= tcod.lib.SDL_WINDOW_FULLSCREEN_DESKTOP
        else:
            window_flags |= tcod.lib.SDL_WINDOW_MAXIMIZED

        if vsync:
            renderer_flags |= tcod.lib.SDL_RENDERER_PRESENTVSYNC

        self.sdl = self._init_sdl(title, resolution, window_flags,
                                  renderer_flags)

        tcod.mouse_show_cursor(False)
        self.resize()

    def __enter__(self):
        return self

    def __exit__(self, type, value, traceback):
        tcod.lib.TCOD_console_delete(tcod.ffi.NULL)

    def resize(self):
        char_size = np.array(tcod.sys_get_char_size())
        grid = self._window_size() // char_size
        game_grid = np.minimum(self.game.map.shape, grid)
        self.console = tcod.console.Console(*game_grid, order="F")
        self.viewport = self._get_viewport()

    def flush(self):
        self._clear()
        self._draw()

    def _init_sdl(self, title, resolution, window_flags, renderer_flags):
        result = tcod.lib.TCOD_sys_init_sdl2_renderer_(
            *resolution,
            title.encode(),
            window_flags,
            renderer_flags,
        )

        if result < 0:
            error = tcod.ffi.string(tcod.lib.TCOD_get_error()).decode()
            raise RuntimeError(error)

        renderer = tcod.lib.TCOD_sys_get_sdl_renderer()

        if not renderer:
            raise RuntimeError("Failed to get SDL renderer")

        return renderer

    def _clear(self):
        tcod.lib.SDL_SetRenderDrawColor(self.sdl, *self.bg, 255)
        tcod.lib.SDL_RenderClear(self.sdl)

    def _draw(self):
        tcod.lib.TCOD_sys_accumulate_console_(self.console.console_c,
                                              self.viewport)
        tcod.lib.SDL_RenderPresent(self.sdl)

    def _window_size(self):
        size = tcod.ffi.new("int[2]")
        tcod.lib.SDL_GetRendererOutputSize(self.sdl, size, size + 1)

        return tuple(size)

    def _get_viewport(self):
        tileset = tcod.tileset.get_default()

        aspect = np.array([
            self.console.width * tileset.tile_width,
            self.console.height * tileset.tile_height,
        ])

        window_size = self._window_size()
        scale = min(window_size / aspect)
        scale = int(scale) if scale >= 1 else scale

        view_size = (scale * aspect).astype(int)
        view_offset = (window_size - view_size) // 2

        return tcod.ffi.new("struct SDL_Rect*", (*view_offset, *view_size))
HexDecimal commented 4 years ago

The tileset is always rendered pixel perfect without scaling. Small letterboxes are used when needed and window resizing affects the console size (in characters).

This can now be done with tcod.console.recommended_size and tcod.console_flush. recommended_size uses the window size and active tileset size to determine the size of the console. This replaces most of your resize method. console_flush should be given integer_scaling=True and a console with the size from recommended_size to achieve the fixed scaling and letterbox. The new console_flush replaces your flush method.

I implemented zooming by changing to different tileset character sizes.

This will work as normal, just be sure to change the tileset before calling recommended_size.

This would also mean switching back to tcod.console_init_root. The only things not implemented right now would be starting as a maximized window, but you can still get the window pointer with tcod.lib.TCOD_sys_get_sdl_window, and then use tcod.lib.SDL_SetWindowFullscreen

I can give code examples if I didn't explain this well enough.

Ape commented 4 years ago

Thanks, always nice to have my code simplified.

Am I doing this right? Should I call tcod.console_init_root and then create separate console instances on resize? Like this:

import numpy as np
import tcod
import tcod.console

class Display:
    def __init__(self, game, title, console_size, fullscreen, vsync,
                 bg=(0, 0, 0)):
        self.game = game
        self.bg = bg

        tcod.console_init_root(*console_size, title=title,
                               fullscreen=fullscreen, order="F", vsync=vsync)
        tcod.mouse_show_cursor(False)

        self.resize()

    def resize(self):
        max_size = np.array(tcod.console.recommended_size())
        console_size = np.minimum(self.game.map.shape, max_size)
        self.console = tcod.console.Console(*console_size, order="F")

    def flush(self):
        tcod.console_flush(self.console, integer_scaling=True)

This seems to at least have the effect that if I change the font with tcod.console_set_custom_font and call my resize method, the font isn't actually changed, but tcod.console.recommended_size() is still calculated using the new font.

HexDecimal commented 4 years ago

You're doing it right. The font changes not being reflected in the rendering is a regression in libtcod, which I'll be fixing right now.

HexDecimal commented 4 years ago

Version 11.11.0 has been released. It should show the correct tileset as you change it.

One last thing is that tcod.console_init_root does return the important "root console" object and you should at least call that objects close method when you're done with the libtcod window. Not doing so will give a warning as your program exits.

Ape commented 4 years ago

Thanks.

I now found an issue that the screen isn't redrawn properly after font change (+ my resize method). The screen sometimes just goes black after changing fonts and tiles are redrawn only when I change them (individually). It might be just an issue in my code, but it didn't happen before.

HexDecimal commented 4 years ago

I figured I was going to forget something. Sounds like the "console cache" thinking that it doesn't need to redraw the tiles because the console size hasn't changed. I'll get that fixed too.

HexDecimal commented 4 years ago

The blank screen was slightly confusing but I figured out how that happened. This should be the last fix.

Version 11.11.1 is deploying, it will be about 2 hours after this post for it to be available. Unless you install from the master branch or something.

HexDecimal commented 4 years ago

Is this current solution good enough to close this issue?

Or are you still having trouble with something?

Ape commented 4 years ago

Sorry, I didn't test the latest version yet. Seems like 11.11.1 is missing some files: https://pypi.org/project/tcod/11.11.1/#files

HexDecimal commented 4 years ago

I didn't notice that the Windows wheels failed to upload. I've redeployed it now.

Ape commented 4 years ago

I think resizing and redrawing are now working perfectly.

I still have one issue with the updated version. I used to be able to use color codes like this with console.print:

"123%c%c%c%c456%c789" % (tcod.COLCTRL_FORE_RGB, 255, 0, 0, tcod.COLCTRL_STOP)

It would print 123456789, where bold characters are correctly red. In the current version it prints this instead: 1235689.

HexDecimal commented 4 years ago

I still have one issue with the updated version. I used to be able to use color codes like this with console.print: ...

Should be fixed in the latest release: 11.11.3.

Ape commented 4 years ago

That previous example with color codes works now, but there seems to be still an issue with this:

"%c%c%c%c123%c\n456" % (tcod.COLCTRL_FORE_RGB, 255, 0, 0, tcod.COLCTRL_STOP)

If the new line character comes directly after the color code, it won't have an effect. If there is text between the color code and the new line character then it works.

HexDecimal commented 4 years ago

Sorry for that. I've been having a hard time with the C string parser. At this point I'm just adding your examples to unit tests as you report them.

HexDecimal commented 4 years ago

Released more fixes with 11.11.4, if you find another problem then I might ask you to build from the sources later.

Ape commented 4 years ago

I think everything works again!