mirror of
https://github.com/libretro/ludo.git
synced 2026-11-08 08:09:44 -10:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd99584fe9 | ||
|
|
933c86b852 | ||
|
|
681dfbc6ab | ||
|
|
f241ef4995 | ||
|
|
6e4b4b634e | ||
|
|
87175ca350 | ||
|
|
7eef38cf2e | ||
|
|
dc56ebfff9 | ||
|
|
d3fd407e77 | ||
|
|
a7cf8d1f99 | ||
|
|
a7da8f7cb9 | ||
|
|
967b0da04f | ||
|
|
c6c0c9c819 | ||
|
|
d41ab5d9d7 | ||
|
|
7e794694f5 | ||
|
|
d861e3d47d |
No files matched your search
@@ -1,7 +1,7 @@
|
||||
APP = Ludo
|
||||
BUNDLENAME = $(APP)-$(OS)-$(ARCH)-$(VERSION)
|
||||
|
||||
CORES = bluemsx fbneo fceumm gambatte genesis_plus_gx handy mednafen_ngp mednafen_pce_fast mednafen_psx mednafen_saturn mednafen_supergrafx mednafen_vb mednafen_wswan mgba np2kai o2em pcsx_rearmed picodrive pokemini prosystem snes9x stella vecx virtualjaguar
|
||||
CORES = bluemsx fbneo fceumm gambatte genesis_plus_gx handy mednafen_ngp mednafen_pce_fast mednafen_psx mednafen_saturn mednafen_supergrafx mednafen_vb mednafen_wswan mgba np2kai o2em pcsx_rearmed picodrive pokemini prosystem snes9x stella vecx virtualjaguar ppsspp mednafen_psx_hw parallel_n64
|
||||
|
||||
ifeq ($(ARCH), arm)
|
||||
CORES := $(filter-out mednafen_saturn,$(CORES))
|
||||
|
||||
@@ -175,6 +175,12 @@ func LoadGame(gamePath string) error {
|
||||
|
||||
ntf.Display(ntf.Info, "Press P to toggle the menu", ntf.Medium)
|
||||
|
||||
vid.InitFramebuffer(vid.Geom.BaseWidth, vid.Geom.BaseHeight)
|
||||
|
||||
if state.Global.Core.HWRenderCallback != nil {
|
||||
state.Global.Core.HWRenderCallback.ContextReset()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -98,6 +98,11 @@ func environment(cmd uint32, data unsafe.Pointer) bool {
|
||||
state.Global.Core.SetFrameTimeCallback(data)
|
||||
case libretro.EnvironmentSetAudioCallback:
|
||||
state.Global.Core.SetAudioCallback(data)
|
||||
case libretro.EnvironmentSetHWRenderer:
|
||||
state.Global.Core.HWRenderCallback = libretro.SetHWRenderCallback(data)
|
||||
state.Global.Core.HWRenderCallback.GetCurrentFramebuffer = vid.CurrentFramebuffer
|
||||
state.Global.Core.HWRenderCallback.GetProcAddress = vid.ProcAddress
|
||||
return true
|
||||
case libretro.EnvironmentGetCanDupe:
|
||||
libretro.SetBool(data, true)
|
||||
case libretro.EnvironmentSetPixelFormat:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/libretro/ludo
|
||||
|
||||
replace github.com/go-gl/glfw => github.com/kivutar/glfw v0.0.0-20191005084155-f62590c2c41c
|
||||
replace github.com/go-gl/glfw => github.com/kivutar/glfw v0.0.0-20191008030156-cee43cb8f5e6
|
||||
|
||||
require (
|
||||
github.com/cavaliercoder/grab v2.0.0+incompatible
|
||||
|
||||
@@ -16,8 +16,8 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF0
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/kivutar/glfont v0.0.0-20190119064645-3f4c07235fb3 h1:6znfTZG6AjHjvkNjSzCt+xdINmWPLTZXSt4UxYYFbxo=
|
||||
github.com/kivutar/glfont v0.0.0-20190119064645-3f4c07235fb3/go.mod h1:yvRxgIrpErStul4gzj88N+5yL15ze5JUVqk6Ze4mU+I=
|
||||
github.com/kivutar/glfw v0.0.0-20191005084155-f62590c2c41c h1:J4Y0LGjPE4W1Pa5ASjE5jC6Qdv+lW83LeqkVxmsKfE8=
|
||||
github.com/kivutar/glfw v0.0.0-20191005084155-f62590c2c41c/go.mod h1:+slYytY+JI/Q/kMyBN7J8ps03jTLgePW+z0YWmLIuXw=
|
||||
github.com/kivutar/glfw v0.0.0-20191008030156-cee43cb8f5e6 h1:y2Th/hDiDrRPSUjR4IgtxYMmmN5nXb2+RQZv0IF0HYs=
|
||||
github.com/kivutar/glfw v0.0.0-20191008030156-cee43cb8f5e6/go.mod h1:+slYytY+JI/Q/kMyBN7J8ps03jTLgePW+z0YWmLIuXw=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
|
||||
@@ -22,6 +22,14 @@ void bridge_retro_frame_time_callback(retro_frame_time_callback_t f, retro_usec_
|
||||
f(usec);
|
||||
}
|
||||
|
||||
void bridge_retro_hw_context_reset(retro_hw_context_reset_t f) {
|
||||
f();
|
||||
}
|
||||
|
||||
void bridge_retro_hw_context_destroy(retro_hw_context_reset_t f) {
|
||||
f();
|
||||
}
|
||||
|
||||
void bridge_retro_audio_callback(retro_audio_callback_t f) {
|
||||
f();
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ void bridge_retro_unload_game(void *f);
|
||||
void bridge_retro_run(void *f);
|
||||
void bridge_retro_reset(void *f);
|
||||
void bridge_retro_frame_time_callback(retro_frame_time_callback_t f, retro_usec_t usec);
|
||||
void bridge_retro_hw_context_reset(retro_hw_context_reset_t f);
|
||||
void bridge_retro_hw_context_destroy(retro_hw_context_reset_t f);
|
||||
void bridge_retro_audio_callback(retro_audio_callback_t f);
|
||||
void bridge_retro_audio_set_state(retro_audio_set_state_callback_t f, bool state);
|
||||
size_t bridge_retro_get_memory_size(void *f, unsigned id);
|
||||
@@ -123,6 +125,22 @@ type FrameTimeCallback struct {
|
||||
Reference int64
|
||||
}
|
||||
|
||||
// HWRenderCallback sets an interface to let a libretro core render with
|
||||
// hardware acceleration.
|
||||
type HWRenderCallback struct {
|
||||
HWContextType uint32
|
||||
ContextReset func()
|
||||
GetCurrentFramebuffer func() uintptr
|
||||
GetProcAddress func(string) uintptr
|
||||
Depth bool
|
||||
Stencil bool
|
||||
BottomLeftOrigin bool
|
||||
VersionMajor, VersionMinor uint
|
||||
CacheContext bool
|
||||
ContextDestroy func()
|
||||
DebugContext bool
|
||||
}
|
||||
|
||||
// AudioCallback stores the audio callback itself and the SetState callback
|
||||
type AudioCallback struct {
|
||||
Callback func()
|
||||
@@ -224,6 +242,7 @@ const (
|
||||
EnvironmentGetPerfInterface = uint32(C.RETRO_ENVIRONMENT_GET_PERF_INTERFACE)
|
||||
EnvironmentSetFrameTimeCallback = uint32(C.RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK)
|
||||
EnvironmentSetAudioCallback = uint32(C.RETRO_ENVIRONMENT_SET_AUDIO_CALLBACK)
|
||||
EnvironmentSetHWRenderer = uint32(C.RETRO_ENVIRONMENT_SET_HW_RENDER)
|
||||
EnvironmentSetGeometry = uint32(C.RETRO_ENVIRONMENT_SET_GEOMETRY)
|
||||
EnvironmentSetSystemAVInfo = uint32(C.RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO)
|
||||
)
|
||||
@@ -246,6 +265,18 @@ const (
|
||||
MemoryVideoRAM = uint32(C.RETRO_MEMORY_VIDEO_RAM)
|
||||
)
|
||||
|
||||
// Hardware contexts
|
||||
const (
|
||||
HWContextNone = uint32(C.RETRO_HW_CONTEXT_NONE)
|
||||
HWContextOpenGL = uint32(C.RETRO_HW_CONTEXT_OPENGL)
|
||||
HWContextOpenGLES2 = uint32(C.RETRO_HW_CONTEXT_OPENGLES2)
|
||||
HWContextOpenGLCore = uint32(C.RETRO_HW_CONTEXT_OPENGL_CORE)
|
||||
HWContextOpenGLES3 = uint32(C.RETRO_HW_CONTEXT_OPENGLES3)
|
||||
HWContextOpenGLESVersion = uint32(C.RETRO_HW_CONTEXT_OPENGLES_VERSION)
|
||||
HWContextVulkan = uint32(C.RETRO_HW_CONTEXT_VULKAN)
|
||||
HWContextDummy = uint32(C.RETRO_HW_CONTEXT_DUMMY)
|
||||
)
|
||||
|
||||
type (
|
||||
environmentFunc func(uint32, unsafe.Pointer) bool
|
||||
videoRefreshFunc func(unsafe.Pointer, int32, int32, int32)
|
||||
@@ -633,6 +664,27 @@ func (core *Core) SetFrameTimeCallback(data unsafe.Pointer) {
|
||||
core.FrameTimeCallback = ftc
|
||||
}
|
||||
|
||||
// SetHWRenderCallback is an environment callback helper to set the HWRenderCallback
|
||||
func SetHWRenderCallback(data unsafe.Pointer) *HWRenderCallback {
|
||||
c := *(*C.struct_retro_hw_render_callback)(data)
|
||||
hwrc := HWRenderCallback{}
|
||||
hwrc.HWContextType = uint32(c.context_type)
|
||||
hwrc.ContextReset = func() {
|
||||
C.bridge_retro_hw_context_reset(c.context_reset)
|
||||
}
|
||||
hwrc.Depth = bool(c.depth)
|
||||
hwrc.Stencil = bool(c.stencil)
|
||||
hwrc.BottomLeftOrigin = bool(c.bottom_left_origin)
|
||||
hwrc.VersionMajor = uint(c.version_major)
|
||||
hwrc.VersionMinor = uint(c.version_minor)
|
||||
hwrc.CacheContext = bool(c.cache_context)
|
||||
hwrc.ContextDestroy = func() {
|
||||
C.bridge_retro_hw_context_destroy(c.context_destroy)
|
||||
}
|
||||
hwrc.DebugContext = bool(c.debug_context)
|
||||
return &hwrc
|
||||
}
|
||||
|
||||
// SetAudioCallback is an environment callback helper to set the AudioCallback
|
||||
func (core *Core) SetAudioCallback(data unsafe.Pointer) {
|
||||
c := *(*C.struct_retro_audio_callback)(data)
|
||||
|
||||
@@ -42,6 +42,7 @@ type Core struct {
|
||||
|
||||
AudioCallback *AudioCallback
|
||||
FrameTimeCallback *FrameTimeCallback
|
||||
HWRenderCallback *HWRenderCallback
|
||||
}
|
||||
|
||||
// DlSym loads a symbol from a dynamic library
|
||||
|
||||
@@ -33,6 +33,7 @@ type Core struct {
|
||||
|
||||
AudioCallback *AudioCallback
|
||||
FrameTimeCallback *FrameTimeCallback
|
||||
HWRenderCallback *HWRenderCallback
|
||||
}
|
||||
|
||||
// DlSym loads a symbol from a dynamic library
|
||||
|
||||
@@ -293,6 +293,9 @@ func (tabs sceneTags) drawHintBar() {
|
||||
w, h := vid.Window.GetFramebufferSize()
|
||||
vid.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, video.Color{R: 0.75, G: 0.75, B: 0.75, A: 1})
|
||||
|
||||
vid.DrawTest(120, 120, 280, 200, 0.05, video.Color{R: 0, G: 0, B: 0, A: 0.2})
|
||||
vid.DrawRect(100, 100, 320, 240, 0.01, video.Color{R: 1, G: 0, B: 1, A: 1})
|
||||
|
||||
_, _, leftRight, a, _, _, _, _, _, guide := hintIcons()
|
||||
|
||||
var stack float32
|
||||
|
||||
@@ -120,6 +120,33 @@ func (video *Video) DrawRect(x, y, w, h, r float32, c Color) {
|
||||
gl.Disable(gl.BLEND)
|
||||
}
|
||||
|
||||
func (video *Video) DrawTest(x, y, w, h, r float32, c Color) {
|
||||
|
||||
fbw, fbh := video.Window.GetFramebufferSize()
|
||||
|
||||
va := []float32{
|
||||
-1, -1, +0, +1,
|
||||
-1, +1, +0, +0,
|
||||
+1, -1, +1, +1,
|
||||
+1, +1, +1, +0,
|
||||
}
|
||||
|
||||
gl.UseProgram(video.testProgram)
|
||||
gl.Uniform4f(gl.GetUniformLocation(video.testProgram, gl.Str("box\x00")), x, y, x+w, y+h)
|
||||
gl.Uniform4f(gl.GetUniformLocation(video.testProgram, gl.Str("color\x00")), c.R, c.G, c.B, c.A)
|
||||
gl.Uniform1f(gl.GetUniformLocation(video.testProgram, gl.Str("radius\x00")), r)
|
||||
gl.Uniform2f(gl.GetUniformLocation(video.testProgram, gl.Str("resolution\x00")), float32(fbw), float32(fbh))
|
||||
gl.Enable(gl.BLEND)
|
||||
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
||||
gl.BindVertexArray(video.vao)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo)
|
||||
gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW)
|
||||
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
|
||||
gl.BindVertexArray(0)
|
||||
gl.UseProgram(0)
|
||||
gl.Disable(gl.BLEND)
|
||||
}
|
||||
|
||||
// DrawCircle draws a circle
|
||||
func (video *Video) DrawCircle(x, y, r float32, c Color) {
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package video
|
||||
|
||||
var testFragmentShader = `
|
||||
#if __VERSION__ >= 130
|
||||
#define COMPAT_VARYING in
|
||||
#define COMPAT_ATTRIBUTE in
|
||||
#define COMPAT_TEXTURE texture
|
||||
#define COMPAT_FRAGCOLOR FragColor
|
||||
out vec4 COMPAT_FRAGCOLOR;
|
||||
#else
|
||||
#define COMPAT_VARYING varying
|
||||
#define COMPAT_ATTRIBUTE attribute
|
||||
#define COMPAT_TEXTURE texture2D
|
||||
#define COMPAT_FRAGCOLOR gl_FragColor
|
||||
#endif
|
||||
|
||||
uniform vec4 box;
|
||||
uniform vec4 color;
|
||||
uniform float radius;
|
||||
uniform vec2 resolution;
|
||||
|
||||
COMPAT_VARYING vec2 fragTexCoord;
|
||||
|
||||
float rec(vec2 uv, vec2 tl, vec2 br)
|
||||
{
|
||||
vec2 d = max(tl-uv, uv-br);
|
||||
return length(max(vec2(0.0), d)) + min(0.0, max(d.x, d.y));
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 st = gl_FragCoord.xy/resolution;
|
||||
st -= 0.5;
|
||||
st *= vec2(resolution.x/resolution.y, -1.0);
|
||||
st += vec2(0.5*resolution.x/resolution.y, 0.5);
|
||||
|
||||
float d = rec(st, box.xy/resolution.y, box.zw/resolution.y);
|
||||
float m = 1.0 - d/radius;
|
||||
COMPAT_FRAGCOLOR = vec4(color.rgb, m*color.a);
|
||||
}
|
||||
` + "\x00"
|
||||
@@ -0,0 +1,26 @@
|
||||
package video
|
||||
|
||||
var testVertexShader = `
|
||||
#if __VERSION__ >= 130
|
||||
#define COMPAT_VARYING out
|
||||
#define COMPAT_ATTRIBUTE in
|
||||
#define COMPAT_TEXTURE texture
|
||||
#define COMPAT_FRAGCOLOR FragColor
|
||||
out vec4 COMPAT_FRAGCOLOR;
|
||||
#else
|
||||
#define COMPAT_VARYING varying
|
||||
#define COMPAT_ATTRIBUTE attribute
|
||||
#define COMPAT_TEXTURE texture2D
|
||||
#define COMPAT_FRAGCOLOR gl_FragColor
|
||||
#endif
|
||||
|
||||
COMPAT_ATTRIBUTE vec2 vert;
|
||||
COMPAT_ATTRIBUTE vec2 vertTexCoord;
|
||||
|
||||
COMPAT_VARYING vec2 fragTexCoord;
|
||||
|
||||
void main() {
|
||||
fragTexCoord = vertTexCoord;
|
||||
gl_Position = vec4(vert, 0.0, 1.0);
|
||||
}
|
||||
` + "\x00"
|
||||
@@ -45,6 +45,7 @@ type Video struct {
|
||||
borderProgram uint32 // program to draw rectangles borders
|
||||
circleProgram uint32 // program to draw textured circles
|
||||
demulProgram uint32 // program to draw premultiplied alpha images
|
||||
testProgram uint32
|
||||
vao uint32
|
||||
vbo uint32
|
||||
texID uint32
|
||||
@@ -52,6 +53,8 @@ type Video struct {
|
||||
pixFmt uint32
|
||||
pixType uint32
|
||||
bpp int32
|
||||
fboID uint32
|
||||
rboID uint32
|
||||
}
|
||||
|
||||
// Init instanciates the video package
|
||||
@@ -125,6 +128,45 @@ func (video *Video) configureContext() uint {
|
||||
return GLSLVersion
|
||||
}
|
||||
|
||||
// InitFramebuffer initializes and configures the video frame buffer based on
|
||||
// informations from the HWRenderCallback of the libretro core.
|
||||
func (video *Video) InitFramebuffer(width, height int) {
|
||||
gl.GenFramebuffers(1, &video.fboID)
|
||||
gl.BindFramebuffer(gl.FRAMEBUFFER, video.fboID)
|
||||
gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, video.texID, 0)
|
||||
|
||||
hw := state.Global.Core.HWRenderCallback
|
||||
|
||||
if hw != nil {
|
||||
if hw.Depth && hw.Stencil {
|
||||
gl.GenRenderbuffers(1, &video.rboID)
|
||||
gl.BindRenderbuffer(gl.RENDERBUFFER, video.rboID)
|
||||
gl.RenderbufferStorage(gl.RENDERBUFFER, gl.DEPTH24_STENCIL8, int32(width), int32(height))
|
||||
|
||||
gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, video.rboID)
|
||||
} else if hw.Depth {
|
||||
gl.GenRenderbuffers(1, &video.rboID)
|
||||
gl.BindRenderbuffer(gl.RENDERBUFFER, video.rboID)
|
||||
gl.RenderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT24, int32(width), int32(height))
|
||||
|
||||
gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, video.rboID)
|
||||
}
|
||||
|
||||
if hw.Depth || hw.Stencil {
|
||||
gl.BindRenderbuffer(gl.RENDERBUFFER, 0)
|
||||
}
|
||||
}
|
||||
|
||||
gl.BindRenderbuffer(gl.RENDERBUFFER, 0)
|
||||
|
||||
//SDL_assert(glCheckFramebufferStatus(gl.FRAMEBUFFER) == gl.FRAMEBUFFER_COMPLETE)
|
||||
|
||||
gl.ClearColor(0, 0, 0, 1)
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT)
|
||||
|
||||
gl.BindFramebuffer(gl.FRAMEBUFFER, 0)
|
||||
}
|
||||
|
||||
// Configure instanciates the video package
|
||||
func (video *Video) Configure(fullscreen bool) {
|
||||
var width, height int
|
||||
@@ -210,6 +252,11 @@ func (video *Video) Configure(fullscreen bool) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
video.testProgram, err = newProgram(GLSLVersion, testVertexShader, testFragmentShader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
video.UpdateFilter(settings.Current.VideoFilter)
|
||||
|
||||
textureUniform := gl.GetUniformLocation(video.program, gl.Str("Texture\x00"))
|
||||
@@ -250,6 +297,11 @@ func (video *Video) Configure(fullscreen bool) {
|
||||
gl.BindTexture(gl.TEXTURE_2D, video.texID)
|
||||
|
||||
video.UpdateFilter(settings.Current.VideoFilter)
|
||||
|
||||
if state.Global.CoreRunning {
|
||||
video.InitFramebuffer(video.Geom.BaseWidth, video.Geom.BaseHeight)
|
||||
state.Global.Core.HWRenderCallback.ContextReset()
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateFilter configures the game texture filter and shader. We currently
|
||||
@@ -387,6 +439,7 @@ func (video *Video) Render() {
|
||||
|
||||
// Refresh the texture framebuffer
|
||||
func (video *Video) Refresh(data unsafe.Pointer, width int32, height int32, pitch int32) {
|
||||
gl.BindFramebuffer(gl.FRAMEBUFFER, 0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, video.texID)
|
||||
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, video.pixType, video.pixFmt, nil)
|
||||
|
||||
@@ -403,6 +456,16 @@ func (video *Video) Refresh(data unsafe.Pointer, width int32, height int32, pitc
|
||||
gl.TexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width, height, video.pixType, video.pixFmt, data)
|
||||
}
|
||||
|
||||
// CurrentFramebuffer returns the current FBO ID
|
||||
func (video *Video) CurrentFramebuffer() uintptr {
|
||||
return uintptr(video.fboID)
|
||||
}
|
||||
|
||||
// ProcAddress returns the address of the proc from GLFW
|
||||
func (video *Video) ProcAddress(procName string) uintptr {
|
||||
return uintptr(glfw.GetProcAddress(procName))
|
||||
}
|
||||
|
||||
var vertices = []float32{
|
||||
// X, Y, U, V
|
||||
-1.0, -1.0, 0.0, 1.0, // left-bottom
|
||||
|
||||
Reference in new issue
Block a user