diff --git a/.gitignore b/.gitignore index f8bdf755..b5ed333c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ windows_install_dependencies setup examples/sdl_opengl_and_sokol/example_shader.h examples/ports/template +examples/music/music_player diff --git a/examples/assets/fonts/LICENSE b/examples/assets/fonts/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/examples/assets/fonts/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/examples/assets/fonts/RobotoMono-Regular.ttf b/examples/assets/fonts/RobotoMono-Regular.ttf new file mode 100644 index 00000000..b158a334 Binary files /dev/null and b/examples/assets/fonts/RobotoMono-Regular.ttf differ diff --git a/examples/assets/images/tvintris.png b/examples/assets/images/tvintris.png new file mode 100644 index 00000000..31bc7f58 Binary files /dev/null and b/examples/assets/images/tvintris.png differ diff --git a/examples/assets/images/v-logo.jpg b/examples/assets/images/v-logo.jpg new file mode 100644 index 00000000..ad6ae8d8 Binary files /dev/null and b/examples/assets/images/v-logo.jpg differ diff --git a/examples/assets/images/v-logo.lossless.webp b/examples/assets/images/v-logo.lossless.webp new file mode 100644 index 00000000..75ecdb05 Binary files /dev/null and b/examples/assets/images/v-logo.lossless.webp differ diff --git a/examples/assets/images/v-logo.png b/examples/assets/images/v-logo.png new file mode 100644 index 00000000..adc02d0c Binary files /dev/null and b/examples/assets/images/v-logo.png differ diff --git a/examples/assets/images/v-logo.svg b/examples/assets/images/v-logo.svg new file mode 100644 index 00000000..edd5f393 --- /dev/null +++ b/examples/assets/images/v-logo.svg @@ -0,0 +1,142 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/assets/images/v-logo.webp b/examples/assets/images/v-logo.webp new file mode 100644 index 00000000..7d6609ee Binary files /dev/null and b/examples/assets/images/v-logo.webp differ diff --git a/examples/assets/images/v-logo_30_30.png b/examples/assets/images/v-logo_30_30.png new file mode 100644 index 00000000..ae8c476c Binary files /dev/null and b/examples/assets/images/v-logo_30_30.png differ diff --git a/examples/assets/sounds/IcyGarden.mid b/examples/assets/sounds/IcyGarden.mid new file mode 100644 index 00000000..a8a57dec Binary files /dev/null and b/examples/assets/sounds/IcyGarden.mid differ diff --git a/examples/assets/sounds/TwintrisThosenine.mod b/examples/assets/sounds/TwintrisThosenine.mod new file mode 100644 index 00000000..4fbceed7 Binary files /dev/null and b/examples/assets/sounds/TwintrisThosenine.mod differ diff --git a/examples/assets/sounds/block.wav b/examples/assets/sounds/block.wav new file mode 100644 index 00000000..e66eb068 Binary files /dev/null and b/examples/assets/sounds/block.wav differ diff --git a/examples/assets/sounds/single.wav b/examples/assets/sounds/single.wav new file mode 100644 index 00000000..bc2fd5c0 Binary files /dev/null and b/examples/assets/sounds/single.wav differ diff --git a/examples/assets/sounds/sound_01.wav b/examples/assets/sounds/sound_01.wav new file mode 100644 index 00000000..3279a5ec Binary files /dev/null and b/examples/assets/sounds/sound_01.wav differ diff --git a/examples/assets/sounds/sound_02.mp3 b/examples/assets/sounds/sound_02.mp3 new file mode 100644 index 00000000..91406b2e Binary files /dev/null and b/examples/assets/sounds/sound_02.mp3 differ diff --git a/examples/assets/sounds/sound_03.flac b/examples/assets/sounds/sound_03.flac new file mode 100644 index 00000000..8d9a990b Binary files /dev/null and b/examples/assets/sounds/sound_03.flac differ diff --git a/examples/assets/sounds/sound_04.ogg b/examples/assets/sounds/sound_04.ogg new file mode 100644 index 00000000..a09fca76 Binary files /dev/null and b/examples/assets/sounds/sound_04.ogg differ diff --git a/examples/assets/sounds/triple.wav b/examples/assets/sounds/triple.wav new file mode 100644 index 00000000..d501c421 Binary files /dev/null and b/examples/assets/sounds/triple.wav differ diff --git a/examples/music/music_player.v b/examples/music/music_player.v new file mode 100644 index 00000000..ce010848 --- /dev/null +++ b/examples/music/music_player.v @@ -0,0 +1,75 @@ +// Copyright(C) 2026 Delyan Angelov. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +module main + +import os +import sdl +import sdl.mixer as mix + +fn main() { + if os.args.len < 2 { + eprintln('Usage: ${os.args[0]} [audio-file ...]') + return + } + + if !sdl.init(sdl.init_audio) { + eprintln('Could not initialize SDL: ${sdl.get_error_v()}') + return + } + defer { sdl.quit() } + + if !mix.init() { + eprintln('Could not initialize SDL_mixer: ${sdl.get_error_v()}') + return + } + defer { mix.quit() } + + mixer := mix.create_mixer_device(sdl.audio_device_default_playback, sdl.null) + if mixer == sdl.null { + eprintln('Could not open audio device: ${sdl.get_error_v()}') + return + } + defer { mix.destroy_mixer(mixer) } + + track := mix.create_track(mixer) + if track == sdl.null { + eprintln('Could not create track: ${sdl.get_error_v()}') + return + } + defer { mix.destroy_track(track) } + + for i := 1; i < os.args.len; i++ { + path := os.args[i] + if !os.exists(path) { + eprintln('Skipping missing file: ${path}') + continue + } + + println('Playing ${path}') + audio := mix.load_audio(mixer, path.str, true) + if audio == sdl.null { + eprintln('Failed to load ${path}: ${sdl.get_error_v()}') + continue + } + + _ := mix.stop_track(track, 0) + if !mix.set_track_audio(track, audio) { + eprintln('Failed to set track for ${path}: ${sdl.get_error_v()}') + mix.destroy_audio(audio) + continue + } + + if !mix.play_track(track, sdl.PropertiesID(0)) { + eprintln('Failed to play ${path}: ${sdl.get_error_v()}') + mix.destroy_audio(audio) + continue + } + + for mix.track_playing(track) { + sdl.delay(25) + } + + mix.destroy_audio(audio) + } +} diff --git a/examples/tvintris/.gitignore b/examples/tvintris/.gitignore new file mode 100644 index 00000000..9dd8a97d --- /dev/null +++ b/examples/tvintris/.gitignore @@ -0,0 +1 @@ +tvintris diff --git a/examples/tvintris/README.md b/examples/tvintris/README.md new file mode 100644 index 00000000..4f238778 --- /dev/null +++ b/examples/tvintris/README.md @@ -0,0 +1,17 @@ +# tVintris + +tvintris.v is a dual-player (local) version based on original source from tetris example by Alex M. +It is largely inspired by ancient game Twintris. +-uses vlib sdl module + +![tVintris screenshot](images/tvintris.png) + +# how to run tVintris + +`$ v run .` + +# Credits + +Colors, Music and Sounds inspired/ripped from amiga title Twintris (1990 nostalgia !) +- Graphician : Svein Berge +- Musician : Tor Bernhard Gausen (Walkman/Cryptoburners) diff --git a/examples/tvintris/tvintris.v b/examples/tvintris/tvintris.v new file mode 100644 index 00000000..69e1bb79 --- /dev/null +++ b/examples/tvintris/tvintris.v @@ -0,0 +1,897 @@ +// Copyright (c) 2019-2022 Alexander Medvednikov. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. + +// SDL2 port+wrapper, Twintris-like dual-game logic, +// and more, by Nicolas Sauzede 2019. +// V Android support added in 2022 by Lars Pontoppidan. + +module main + +import rand +import time +import os +import math +import sdl +import sdl.image as img +import sdl.mixer as mix +import sdl.ttf + +const title = 'tVintris' +const font_name = get_asset_path(os.join_path('fonts', 'RobotoMono-Regular.ttf')) +const music_name = get_asset_path(os.join_path('sounds', 'TwintrisThosenine.mod')) +const music_fallback_name = get_asset_path(os.join_path('sounds', 'sound_04.ogg')) +const snd_block_name = get_asset_path(os.join_path('sounds', 'block.wav')) +const snd_line_name = get_asset_path(os.join_path('sounds', 'single.wav')) +const snd_double_name = get_asset_path(os.join_path('sounds', 'triple.wav')) +const v_logo = get_asset_path(os.join_path('images', 'v-logo_30_30.png')) +const block_size = 20 // pixels + +const field_height = 20 // # of blocks + +const field_width = 10 +const tetro_size = 4 +const win_width = block_size * field_width * 3 +const win_height = block_size * field_height +const timer_period = 250 // ms + +const text_size = 16 +const audio_buf_size = 1024 + +const p2_fire = sdl.KeyCode.l +const p2_up = sdl.KeyCode.up +const p2_down = sdl.KeyCode.down +const p2_left = sdl.KeyCode.left +const p2_right = sdl.KeyCode.right + +const p1_fire = sdl.KeyCode.s +const p1_up = sdl.KeyCode.w +const p1_down = sdl.KeyCode.x +const p1_left = sdl.KeyCode.a +const p1_right = sdl.KeyCode.d + +const n_joy_max = 2 +// joystick name => enter your own device name +const joy_p1_name = 'Generic X-Box pad' +// following are joystick button number +const jb_p1_fire = 1 +// following are joystick hat value +const jh_p1_up = 1 +const jh_p1_down = 4 +const jh_p1_left = 8 +const jh_p1_right = 3 + +// joystick name => enter your own device name +const joy_p2_name = 'RedOctane Guitar Hero X-plorer' +// following are joystick button number +const jb_p2_fire = 0 +// following are joystick hat value +const jh_p2_up = 4 +const jh_p2_down = 1 +const jh_p2_left = 8 +const jh_p2_right = 2 + +// Tetros' 4 possible states are encoded in binaries +const b_tetros = [ + // 0000 0 + // 0000 0 + // 0110 6 + // 0110 6 + [66, 66, 66, 66], + // 0000 0 + // 0000 0 + // 0010 2 + // 0111 7 + [27, 131, 72, 232], + // 0000 0 + // 0000 0 + // 0011 3 + // 0110 6 + [36, 231, 36, 231], + // 0000 0 + // 0000 0 + // 0110 6 + // 0011 3 + [63, 132, 63, 132], + // 0000 0 + // 0011 3 + // 0001 1 + // 0001 1 + [311, 17, 223, 74], + // 0000 0 + // 0011 3 + // 0010 2 + // 0010 2 + [322, 71, 113, 47], + // Special case since 15 can't be used + // 1111 + [1111, 9, 1111, 9], +] +// Each tetro has its unique color +const colors = [ + sdl.Color{0, 0, 0, 0}, // unused ? + sdl.Color{0, 0x62, 0xc0, 0}, // quad : darkblue 0062c0 + sdl.Color{0xca, 0x7d, 0x5f, 0}, // tricorn : lightbrown ca7d5f + sdl.Color{0, 0xc1, 0xbf, 0}, // short topright : lightblue 00c1bf + sdl.Color{0, 0xc1, 0, 0}, // short topleft : lightgreen 00c100 + sdl.Color{0xbf, 0xbe, 0, 0}, // long topleft : yellowish bfbe00 + sdl.Color{0xd1, 0, 0xbf, 0}, // long topright : pink d100bf + sdl.Color{0xd1, 0, 0, 0}, // longest : lightred d10000 + sdl.Color{0, 170, 170, 0}, // unused ? +] +// Background color +const background_color = sdl.Color{0, 0, 0, 0} +// Foreground color +const foreground_color = sdl.Color{0, 170, 170, 0} +// Text color +const text_color = sdl.Color{0xca, 0x7d, 0x5f, 0} + +// TODO: type Tetro [tetro_size]struct{ x, y int } +struct Block { +mut: + x int + y int +} + +enum GameState { + paused + running + gameover +} + +struct AudioContext { +mut: + mixer &mix.Mixer = sdl.null + music &mix.Audio = sdl.null + music_track &mix.Track = sdl.null + volume f32 + waves [3]&mix.Audio +} + +struct SdlContext { +pub mut: + // VIDEO + w int + h int + window &sdl.Window = sdl.null + renderer &sdl.Renderer = sdl.null + screen &sdl.Surface = sdl.null + texture &sdl.Texture = sdl.null + // AUDIO + actx AudioContext + // JOYSTICKS + jnames [2]string + jids [2]sdl.JoystickID + // V logo + v_logo &sdl.Surface = sdl.null + tv_logo &sdl.Texture = sdl.null +} + +fn get_asset_path(path string) string { + $if android { + return path + } $else { + return os.resource_abs_path(os.join_path('..', 'assets', path)) + } +} + +struct Game { +mut: + // Score of the current game + score int + // Count consecutive lines for scoring + lines int + // State of the current game + state GameState + // X offset of the game display + ofs_x int + // keys + k_fire sdl.KeyCode + k_up sdl.KeyCode + k_down sdl.KeyCode + k_left sdl.KeyCode + k_right sdl.KeyCode + // joystick ID + joy_id sdl.JoystickID + // joystick buttons + jb_fire int + // joystick hat values + jh_up int + jh_down int + jh_left int + jh_right int + // Position of the current tetro + pos_x int + pos_y int + // field[y][x] contains the color of the block with (x,y) coordinates + // "-1" border is to avoid bounds checking. + // -1 -1 -1 -1 + // -1 0 0 -1 + // -1 0 0 -1 + // -1 -1 -1 -1 + field [][]int + // TODO: tetro Tetro + tetro []Block + // TODO: tetros_cache []Tetro + tetros_cache []Block + // Index of the current tetro. Refers to its color. + tetro_idx int + // Index of the next tetro. Refers to its color. + tetro_next int + // tetro stats : buckets of drawn tetros + tetro_stats []int + // total number of drawn tetros + tetro_total int + // Index of the rotation (0-3) + rotation_idx int + // SDL2 context for drawing + sdl SdlContext + // TTF context for font drawing + font &ttf.Font = sdl.null +} + +fn (mut sdlc SdlContext) set_sdl_context(w int, h int, titl string) { + sdl.init(sdl.init_video | sdl.init_audio | sdl.init_joystick) + C.atexit(sdl.quit) + ttf.init() + C.atexit(ttf.quit) + sdl.create_window_and_renderer(titl.str, w, h, sdl.WindowFlags(0), &sdlc.window, + &sdlc.renderer) + sdlc.w = w + sdlc.h = h + sdlc.screen = sdl.create_surface(w, h, .argb8888) + sdlc.texture = sdl.create_texture(sdlc.renderer, .argb8888, .streaming, w, h) + sdl.set_texture_blend_mode(sdlc.texture, sdl.blendmode_none) + + if !mix.init() { + println("couldn't init mixer") + } + C.atexit(mix.quit) + sdlc.actx.mixer = mix.create_mixer_device(sdl.audio_device_default_playback, sdl.null) + if isnil(sdlc.actx.mixer) { + println("couldn't open audio") + } else { + println('opening music ${music_name}') + sdlc.actx.music = mix.load_audio(sdlc.actx.mixer, music_name.str, true) + if isnil(sdlc.actx.music) { + error_msg := unsafe { cstring_to_vstring(sdl.get_error()) } + println('failed to load music: ${error_msg}') + println('opening fallback music ${music_fallback_name}') + sdlc.actx.music = mix.load_audio(sdlc.actx.mixer, music_fallback_name.str, true) + } + sdlc.actx.waves[0] = mix.load_audio(sdlc.actx.mixer, snd_block_name.str, true) + sdlc.actx.waves[1] = mix.load_audio(sdlc.actx.mixer, snd_line_name.str, true) + sdlc.actx.waves[2] = mix.load_audio(sdlc.actx.mixer, snd_double_name.str, true) + sdlc.actx.volume = 1.0 + if !isnil(sdlc.actx.music) { + sdlc.actx.music_track = mix.create_track(sdlc.actx.mixer) + if !isnil(sdlc.actx.music_track) + && mix.set_track_audio(sdlc.actx.music_track, sdlc.actx.music) + && mix.play_track(sdlc.actx.music_track, sdl.PropertiesID(0)) { + mix.set_track_loops(sdlc.actx.music_track, -1) + mix.set_track_gain(sdlc.actx.music_track, sdlc.actx.volume) + } else { + error_msg := unsafe { cstring_to_vstring(sdl.get_error()) } + println('failed to start music: ${error_msg}') + } + } + } + mut njoy := 0 + joys := sdl.get_joysticks(&njoy) + if !isnil(joys) { + for i in 0 .. njoy { + joy_id := unsafe { joys[i] } + sdl.open_joystick(joy_id) + jn := unsafe { tos_clone(&u8(sdl.get_joystick_name_for_id(joy_id))) } + println('JOY NAME ${jn}') + for j in 0 .. n_joy_max { + if sdlc.jnames[j] == jn { + println('FOUND JOYSTICK ${j} ${jn} ID=${joy_id}') + sdlc.jids[j] = joy_id + } + } + } + sdl.free(joys) + } + println('opening logo ${v_logo}') + sdlc.v_logo = img.load(v_logo.str) + if !isnil(sdlc.v_logo) { + // println('got v_logo=$sdlc.v_logo') + sdlc.tv_logo = sdl.create_texture_from_surface(sdlc.renderer, sdlc.v_logo) + // println('got tv_logo=$sdlc.tv_logo') + } + sdl.set_joystick_events_enabled(true) +} + +fn main() { + println('tVintris -- tribute to venerable Twintris') + mut game := &Game{} + game.sdl.jnames[0] = joy_p1_name + game.sdl.jnames[1] = joy_p2_name + game.sdl.jids[0] = sdl.JoystickID(0) + game.sdl.jids[1] = sdl.JoystickID(0) + game.sdl.set_sdl_context(win_width, win_height, title) + game.font = ttf.open_font(font_name.str, f32(text_size)) + mut game2 := &Game{} + game2.sdl = game.sdl + game2.font = game.font + + game.joy_id = game.sdl.jids[0] + // println('JOY1 id=${game.joy_id}') + game2.joy_id = game.sdl.jids[1] + // println('JOY2 id=${game2.joy_id}') + + // delay uses milliseconds so 1000 ms / 30 frames (30fps) roughly = 33.3333 ms/frame + time_per_frame := 1000.0 / 30.0 + + game.k_fire = p1_fire + game.k_up = p1_up + game.k_down = p1_down + game.k_left = p1_left + game.k_right = p1_right + game.jb_fire = jb_p1_fire + game.jh_up = jh_p1_up + game.jh_down = jh_p1_down + game.jh_left = jh_p1_left + game.jh_right = jh_p1_right + game.ofs_x = 0 + game.init_game() + game.state = .running + spawn game.run() // Run the game loop in a new thread + + game2.k_fire = p2_fire + game2.k_up = p2_up + game2.k_down = p2_down + game2.k_left = p2_left + game2.k_right = p2_right + game2.jb_fire = jb_p2_fire + game2.jh_up = jh_p2_up + game2.jh_down = jh_p2_down + game2.jh_left = jh_p2_left + game2.jh_right = jh_p2_right + game2.ofs_x = win_width * 2 / 3 + game2.init_game() + game2.state = .running + spawn game2.run() // Run the game loop in a new thread + + mut g := Game{} + mut should_close := false + mut total_frames := u32(0) + + for { + total_frames++ + start_ticks := sdl.get_performance_counter() + + g1 := game + g2 := game2 + // here we determine which game contains most recent state + if g1.tetro_total > g.tetro_total { + g = *g1 + } + if g2.tetro_total > g.tetro_total { + g = *g2 + } + g.draw_begin() + + g1.draw_tetro() + g1.draw_field() + + g2.draw_tetro() + g2.draw_field() + + g.draw_middle() + + g1.draw_score() + g2.draw_score() + + g.draw_stats() + + g.draw_v_logo() + g.draw_end() + + // game.handle_events() // CRASHES if done in function ??? + evt := sdl.Event{} + for sdl.poll_event(&evt) { + match evt.@type { + .quit { + should_close = true + } + .key_down { + key := unsafe { sdl.KeyCode(evt.key.key) } + if key == sdl.KeyCode.escape { + should_close = true + break + } + game.handle_key(key) + game2.handle_key(key) + } + .joystick_button_down { + jb := evt.jbutton.button + joyid := evt.jbutton.which + // println('JOY BUTTON $jb $joyid') + game.handle_jbutton(jb, joyid) + game2.handle_jbutton(jb, joyid) + } + .joystick_hat_motion { + jh := evt.jhat.hat + jv := evt.jhat.value + joyid := evt.jhat.which + // println('JOY HAT $jh $jv $joyid') + game.handle_jhat(jh, jv, joyid) + game2.handle_jhat(jh, jv, joyid) + } + else {} + } + } + if should_close { + break + } + end_ticks := sdl.get_performance_counter() + elapsed_time := f64(end_ticks - start_ticks) / f64(sdl.get_performance_frequency()) + // current_fps := 1.0 / elapsed_time + + // should limit system to (1 / time_per_frame) fps + sdl.delay(u32(math.floor(time_per_frame - elapsed_time))) + } + if !isnil(game.font) { + ttf.close_font(game.font) + } + if !isnil(game.sdl.actx.music_track) { + mix.stop_track(game.sdl.actx.music_track, i64(0)) + mix.destroy_track(game.sdl.actx.music_track) + } + if !isnil(game.sdl.actx.music) { + mix.destroy_audio(game.sdl.actx.music) + } + if !isnil(game.sdl.actx.waves[0]) { + mix.destroy_audio(game.sdl.actx.waves[0]) + } + if !isnil(game.sdl.actx.waves[1]) { + mix.destroy_audio(game.sdl.actx.waves[1]) + } + if !isnil(game.sdl.actx.waves[2]) { + mix.destroy_audio(game.sdl.actx.waves[2]) + } + if !isnil(game.sdl.actx.mixer) { + mix.destroy_mixer(game.sdl.actx.mixer) + } + if !isnil(game.sdl.tv_logo) { + sdl.destroy_texture(game.sdl.tv_logo) + } + if !isnil(game.sdl.v_logo) { + sdl.destroy_surface(game.sdl.v_logo) + } +} + +enum Action { + idle + space + fire +} + +@[inline] +fn (game &Game) fill_rect(s &sdl.Surface, r &sdl.Rect, c &sdl.Color) { + sdl.fill_surface_rect(s, r, sdl.map_surface_rgba(s, c.r, c.g, c.b, c.a)) +} + +fn (g &Game) play_sfx(idx int) { + if idx < 0 || idx >= g.sdl.actx.waves.len { + return + } + if !isnil(g.sdl.actx.mixer) && !isnil(g.sdl.actx.waves[idx]) { + mix.play_audio(g.sdl.actx.mixer, g.sdl.actx.waves[idx]) + } +} + +fn (mut game Game) handle_key(key sdl.KeyCode) { + // global keys + mut action := Action.idle + match key { + .space { action = .space } + game.k_fire { action = .fire } + else {} + } + + if action == .space { + match game.state { + .running { + if !isnil(game.sdl.actx.music_track) { + mix.pause_track(game.sdl.actx.music_track) + } + game.state = .paused + } + .paused { + if !isnil(game.sdl.actx.music_track) { + mix.resume_track(game.sdl.actx.music_track) + } + game.state = .running + } + else {} + } + } + + if action == .fire { + match game.state { + .gameover { + game.init_game() + game.state = .running + } + else {} + } + } + if game.state != .running { + return + } + // keys while game is running + match key { + game.k_up { game.rotate_tetro() } + game.k_left { game.move_right(-1) } + game.k_right { game.move_right(1) } + game.k_down { game.move_tetro() } // drop faster when the player presses + else {} + } +} + +fn (mut game Game) handle_jbutton(jb int, joyid sdl.JoystickID) { + if joyid != game.joy_id { + return + } + // global buttons + mut action := Action.idle + match jb { + game.jb_fire { action = .fire } + else {} + } + + if action == .fire { + match game.state { + .gameover { + game.init_game() + game.state = .running + } + else {} + } + } +} + +fn (mut game Game) handle_jhat(jh int, jv int, joyid sdl.JoystickID) { + if joyid != game.joy_id { + return + } + if game.state != .running { + return + } + // println('testing hat values.. joyid=$joyid jh=$jh jv=$jv') + // hat values while game is running + match jv { + game.jh_up { game.rotate_tetro() } + game.jh_left { game.move_right(-1) } + game.jh_right { game.move_right(1) } + game.jh_down { game.move_tetro() } // drop faster when the player presses + else {} + } +} + +fn (mut g Game) init_game() { + g.score = 0 + g.tetro_total = 0 + g.tetro_stats = [0, 0, 0, 0, 0, 0, 0] + g.parse_tetros() + g.generate_tetro() + g.field = [] + // Generate the field, fill it with 0's, add -1's on each edge + for _ in 0 .. field_height + 2 { + mut row := [0].repeat(field_width + 2) + row[0] = -1 + row[field_width + 1] = -1 + g.field << row + } + mut first_row := g.field[0] + mut last_row := g.field[field_height + 1] + for j in 0 .. field_width + 2 { + first_row[j] = -1 + last_row[j] = -1 + } +} + +fn (mut g Game) parse_tetros() { + for b_tetro_1 in b_tetros { + for b_tetro in b_tetro_1 { + for t in parse_binary_tetro(b_tetro) { + g.tetros_cache << t + } + } + } +} + +fn (mut g Game) run() { + for { + if g.state == .running { + g.move_tetro() + n := g.delete_completed_lines() + if n > 0 { + g.lines += n + } else { + if g.lines > 0 { + if g.lines > 1 { + g.play_sfx(2) + } else if g.lines == 1 { + g.play_sfx(1) + } + g.score += 10 * g.lines * g.lines + g.lines = 0 + } + } + } + time.sleep(timer_period * time.millisecond) // medium delay between game step + } +} + +fn (mut game Game) rotate_tetro() { + // Rotate the tetro + old_rotation_idx := game.rotation_idx + game.rotation_idx++ + if game.rotation_idx == tetro_size { + game.rotation_idx = 0 + } + game.get_tetro() + if !game.move_right(0) { + game.rotation_idx = old_rotation_idx + game.get_tetro() + } + if game.pos_x < 0 { + game.pos_x = 1 + } +} + +fn (mut g Game) move_tetro() { + // Check each block in current tetro + for block in g.tetro { + y := block.y + g.pos_y + 1 + x := block.x + g.pos_x + // Reached the bottom of the screen or another block? + // TODO: if g.field[y][x] != 0 + // if g.field[y][x] != 0 { + row := g.field[y] + if row[x] != 0 { + // The new tetro has no space to drop => end of the game + if g.pos_y < 2 { + g.state = .gameover + g.tetro_total = 0 + return + } + // Drop it and generate a new one + g.drop_tetro() + g.generate_tetro() + g.play_sfx(0) + return + } + } + g.pos_y++ +} + +fn (mut g Game) move_right(dx int) bool { + // Reached left/right edge or another tetro? + for i in 0 .. tetro_size { + tetro := g.tetro[i] + y := tetro.y + g.pos_y + x := tetro.x + g.pos_x + dx + row := g.field[y] + if row[x] != 0 { + // Do not move + return false + } + } + g.pos_x += dx + return true +} + +fn (g &Game) delete_completed_lines() int { + mut n := 0 + for y := field_height; y >= 1; y-- { + n += g.delete_completed_line(y) + } + return n +} + +fn (g &Game) delete_completed_line(y int) int { + for x := 1; x <= field_width; x++ { + f := g.field[y] + if f[x] == 0 { + return 0 + } + } + // Move everything down by 1 position + for yy := y - 1; yy >= 1; yy-- { + for x := 1; x <= field_width; x++ { + mut a := g.field[yy + 1] + b := g.field[yy] + a[x] = b[x] + } + } + return 1 +} + +// Draw a rand tetro index +fn (mut g Game) rand_tetro() int { + cur := g.tetro_next + g.tetro_next = rand.int_in_range(0, b_tetros.len) or { 0 } + return cur +} + +// Place a new tetro on top +fn (mut g Game) generate_tetro() { + g.pos_y = 0 + g.pos_x = field_width / 2 - tetro_size / 2 + g.tetro_idx = g.rand_tetro() + // println('idx=${g.tetro_idx}') + g.tetro_stats[g.tetro_idx] += 2 - 1 + g.tetro_total++ + g.rotation_idx = 0 + g.get_tetro() +} + +// Get the right tetro from cache +fn (mut g Game) get_tetro() { + idx := g.tetro_idx * tetro_size * tetro_size + g.rotation_idx * tetro_size + g.tetro = g.tetros_cache[idx..idx + tetro_size] +} + +fn (g &Game) drop_tetro() { + for i in 0 .. tetro_size { + tetro := g.tetro[i] + x := tetro.x + g.pos_x + y := tetro.y + g.pos_y + // Remember the color of each block + // TODO: g.field[y][x] = g.tetro_idx + 1 + mut row := g.field[y] + row[x] = g.tetro_idx + 1 + } +} + +fn (g &Game) draw_tetro() { + for i in 0 .. tetro_size { + tetro := g.tetro[i] + g.draw_block(g.pos_y + tetro.y, g.pos_x + tetro.x, g.tetro_idx + 1) + } +} + +fn (g &Game) draw_block(i int, j int, color_idx int) { + rect := sdl.Rect{g.ofs_x + (j - 1) * block_size, (i - 1) * block_size, block_size - 1, block_size - 1} + col := colors[color_idx] + g.fill_rect(g.sdl.screen, &rect, &col) +} + +fn (g &Game) draw_field() { + for i := 1; i < field_height + 1; i++ { + for j := 1; j < field_width + 1; j++ { + f := g.field[i] + if f[j] > 0 { + g.draw_block(i, j, f[j]) + } + } + } +} + +fn (g &Game) draw_v_logo() { + if isnil(g.sdl.tv_logo) { + return + } + mut texw := f32(0) + mut texh := f32(0) + sdl.get_texture_size(g.sdl.tv_logo, &texw, &texh) + dstrect := sdl.FRect{(f32(win_width) - texw) / 2, f32(20), texw, texh} + sdl.render_texture(g.sdl.renderer, g.sdl.tv_logo, sdl.null, &dstrect) +} + +fn (g &Game) draw_text(x int, y int, text string, tcol sdl.Color) { + tcol_ := sdl.Color{tcol.r, tcol.g, tcol.b, tcol.a} + tsurf := ttf.render_text_solid(g.font, text, tcol_) + ttext := sdl.create_texture_from_surface(g.sdl.renderer, tsurf) + mut texw := f32(0) + mut texh := f32(0) + sdl.get_texture_size(ttext, &texw, &texh) + dstrect := sdl.FRect{f32(x), f32(y), texw, texh} + sdl.render_texture(g.sdl.renderer, ttext, sdl.null, &dstrect) + sdl.destroy_texture(ttext) + sdl.destroy_surface(tsurf) +} + +@[inline] +fn (g &Game) draw_ptext(x int, y int, text string, tcol sdl.Color) { + g.draw_text(g.ofs_x + x, y, text, tcol) +} + +@[live] +fn (g &Game) draw_begin() { + // println('about to clear') + sdl.render_clear(g.sdl.renderer) + mut rect := sdl.Rect{0, 0, g.sdl.w, g.sdl.h} + col := sdl.Color{0, 0, 0, 0} + // sdl_fill_rect(g.sdl.screen, &rect, background_color) + g.fill_rect(g.sdl.screen, &rect, col) + + rect = sdl.Rect{block_size * field_width + 2, 0, 2, g.sdl.h} + g.fill_rect(g.sdl.screen, &rect, foreground_color) + rect = sdl.Rect{win_width - block_size * field_width - 4, 0, 2, g.sdl.h} + g.fill_rect(g.sdl.screen, &rect, foreground_color) + + mut idx := 0 + for st in g.tetro_stats { + mut s := 10 + if g.tetro_total > 0 { + s += 90 * st / g.tetro_total + } + w := block_size + h := s * 4 * w / 100 + rect = sdl.Rect{(win_width - 7 * (w + 1)) / 2 + idx * (w + 1), win_height * 3 / 4 - h, w, h} + g.fill_rect(g.sdl.screen, &rect, colors[idx + 1]) + idx++ + } +} + +fn (g &Game) draw_middle() { + sdl.update_texture(g.sdl.texture, sdl.null, g.sdl.screen.pixels, g.sdl.screen.pitch) + sdl.render_texture(g.sdl.renderer, g.sdl.texture, sdl.null, sdl.null) +} + +fn (g &Game) draw_score() { + if !isnil(g.font) { + g.draw_ptext(1, 2, 'score: ' + g.score.str() + ' nxt=' + g.tetro_next.str(), text_color) + if g.state == .gameover { + g.draw_ptext(1, win_height / 2 + 0 * text_size, 'Game Over', text_color) + g.draw_ptext(1, win_height / 2 + 2 * text_size, 'FIRE to restart', text_color) + } else if g.state == .paused { + g.draw_ptext(1, win_height / 2 + 0 * text_size, 'Game Paused', text_color) + g.draw_ptext(1, win_height / 2 + 2 * text_size, 'SPACE to resume', text_color) + } + } +} + +fn (g &Game) draw_stats() { + if !isnil(g.font) { + g.draw_text(win_width / 3 + 10, win_height * 3 / 4 + 0 * text_size, 'stats: ' + + g.tetro_total.str() + ' tetros', text_color) + mut stats := '' + for st in g.tetro_stats { + mut s := 0 + if g.tetro_total > 0 { + s = 100 * st / g.tetro_total + } + stats += ' ' + stats += s.str() + } + g.draw_text(win_width / 3 - 8, win_height * 3 / 4 + 2 * text_size, stats, text_color) + } +} + +fn (g &Game) draw_end() { + sdl.render_present(g.sdl.renderer) +} + +fn parse_binary_tetro(t_ int) []Block { + mut t := t_ + res := [Block{}].repeat(4) + mut cnt := 0 + horizontal := t == 9 // special case for the horizontal line + for i := 0; i <= 3; i++ { + // Get ith digit of t + p := int(math.pow(10, 3 - i)) + mut digit := t / p + t %= p + // Convert the digit to binary + for j := 3; j >= 0; j-- { + bin := digit % 2 + digit /= 2 + if bin == 1 || (horizontal && i == tetro_size - 1) { + // TODO: res[cnt].x = j + // res[cnt].y = i + mut point := &res[cnt] + point.x = j + point.y = i + cnt++ + } + } + } + return res +} diff --git a/image/README.md b/image/README.md new file mode 100644 index 00000000..94984c3d --- /dev/null +++ b/image/README.md @@ -0,0 +1,4 @@ +# SDL3 Image Library +Assuming you've installed the dependencies for sdl (already include `SDL3_image` dependency) + +See the Tvintris example for usage diff --git a/image/c.c.v b/image/c.c.v new file mode 100644 index 00000000..5fd82784 --- /dev/null +++ b/image/c.c.v @@ -0,0 +1,24 @@ +module image + +$if !windows { + // SDL libs are loaded dynamically from Java on Android + $if !android || termux { + // sdl_no_compile_flags allow users to provide + // custom flags (e.g. via CFLAGS/LDFLAGS) for the compiler. + // This is especially useful when building/linking against a + // custom compiled version of the libs on *nix. + $if !sdl_no_compile_flags ? { + #flag -lSDL3_image + } + } +} + +$if x64 { + #flag windows -L @VMODROOT/thirdparty/SDL3_image-3.4.0/lib/x64 +} $else { + #flag windows -L @VMODROOT/thirdparty/SDL3_image-3.4.0/lib/x86 +} +#flag windows -I @VMODROOT/thirdparty/SDL3_image-3.4.0/include +#flag windows -lSDL3_image + +#include diff --git a/image/image.c.v b/image/image.c.v new file mode 100644 index 00000000..c0aeabd8 --- /dev/null +++ b/image/image.c.v @@ -0,0 +1,253 @@ +// Copyright(C) 2026 Lars Pontoppidan. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +module image + +// +// SDL_image.h +// +import sdl + +pub const major_version = C.SDL_IMAGE_MAJOR_VERSION // 3 + +pub const minor_version = C.SDL_IMAGE_MINOR_VERSION // 4 + +pub const micro_version = C.SDL_IMAGE_MICRO_VERSION // 0 +pub const patchlevel = micro_version + +// compiledversion is the version number macro for the current SDL_image version. +pub fn compiledversion() int { + return C.SDL_VERSIONNUM(major_version, minor_version, micro_version) +} + +// This macro will evaluate to true if compiled with SDL_image at least X.Y.Z. +fn C.SDL_IMAGE_VERSION_ATLEAST(x int, y int, z int) bool + +// version_atleast evaluates to true if compiled with SDL_image at least X.Y.Z. +pub fn version_atleast(x int, y int, z int) bool { + return C.SDL_IMAGE_VERSION_ATLEAST(x, y, z) +} + +fn C.IMG_LoadTyped_IO(src &C.SDL_IOStream, closeio bool, const_type &char) &C.SDL_Surface + +// load_typed_io loads an image from an SDL data source. +// The 'type' may be one of: "BMP", "GIF", "PNG", etc. +// +// If the image format supports a transparent pixel, SDL will set the +// colorkey for the surface. You can enable RLE acceleration on the +// surface afterwards by calling: +// SDL_SetColorKey(image, SDL_RLEACCEL, image->format->colorkey); +pub fn load_typed_io(src &sdl.RWops, closeio bool, const_type &char) &sdl.Surface { + return C.IMG_LoadTyped_IO(src, closeio, const_type) +} + +// Convenience functions +fn C.IMG_Load(file &char) &C.SDL_Surface +pub fn load(file &char) &sdl.Surface { + return C.IMG_Load(file) +} + +fn C.IMG_Load_IO(src &C.SDL_IOStream, closeio bool) &C.SDL_Surface +pub fn load_io(src &sdl.RWops, closeio bool) &sdl.Surface { + return C.IMG_Load_IO(src, closeio) +} + +fn C.IMG_LoadTexture(renderer &C.SDL_Renderer, const_file &char) &C.SDL_Texture + +// load_texture loads an image directly into a render texture. +pub fn load_texture(renderer &sdl.Renderer, const_file &char) &sdl.Texture { + return C.IMG_LoadTexture(renderer, const_file) +} + +fn C.IMG_LoadTexture_IO(renderer &C.SDL_Renderer, src &C.SDL_IOStream, closeio bool) &C.SDL_Texture +pub fn load_texture_io(renderer &sdl.Renderer, src &sdl.RWops, closeio bool) &sdl.Texture { + return C.IMG_LoadTexture_IO(renderer, src, closeio) +} + +fn C.IMG_LoadTextureTyped_IO(renderer &C.SDL_Renderer, src &C.SDL_IOStream, closeio bool, const_type &char) &C.SDL_Texture +pub fn load_texture_typed_io(renderer &sdl.Renderer, src &sdl.RWops, closeio bool, const_type &char) &sdl.Texture { + return C.IMG_LoadTextureTyped_IO(renderer, src, closeio, const_type) +} + +// Functions to detect a file type, given a seekable source +fn C.IMG_isICO(src &C.SDL_IOStream) bool +pub fn is_ico(src &sdl.RWops) bool { + return C.IMG_isICO(src) +} + +fn C.IMG_isCUR(src &C.SDL_IOStream) bool +pub fn is_cur(src &sdl.RWops) bool { + return C.IMG_isCUR(src) +} + +fn C.IMG_isBMP(src &C.SDL_IOStream) bool +pub fn is_bmp(src &sdl.RWops) bool { + return C.IMG_isBMP(src) +} + +fn C.IMG_isGIF(src &C.SDL_IOStream) bool +pub fn is_gif(src &sdl.RWops) bool { + return C.IMG_isGIF(src) +} + +fn C.IMG_isJPG(src &C.SDL_IOStream) bool +pub fn is_jpg(src &sdl.RWops) bool { + return C.IMG_isJPG(src) +} + +fn C.IMG_isLBM(src &C.SDL_IOStream) bool +pub fn is_lbm(src &sdl.RWops) bool { + return C.IMG_isLBM(src) +} + +fn C.IMG_isPCX(src &C.SDL_IOStream) bool +pub fn is_pcx(src &sdl.RWops) bool { + return C.IMG_isPCX(src) +} + +fn C.IMG_isPNG(src &C.SDL_IOStream) bool +pub fn is_png(src &sdl.RWops) bool { + return C.IMG_isPNG(src) +} + +fn C.IMG_isPNM(src &C.SDL_IOStream) bool +pub fn is_pnm(src &sdl.RWops) bool { + return C.IMG_isPNM(src) +} + +fn C.IMG_isSVG(src &C.SDL_IOStream) bool +pub fn is_svg(src &sdl.RWops) bool { + return C.IMG_isSVG(src) +} + +fn C.IMG_isTIF(src &C.SDL_IOStream) bool +pub fn is_tif(src &sdl.RWops) bool { + return C.IMG_isTIF(src) +} + +fn C.IMG_isXCF(src &C.SDL_IOStream) bool +pub fn is_xcf(src &sdl.RWops) bool { + return C.IMG_isXCF(src) +} + +fn C.IMG_isXPM(src &C.SDL_IOStream) bool +pub fn is_xpm(src &sdl.RWops) bool { + return C.IMG_isXPM(src) +} + +fn C.IMG_isXV(src &C.SDL_IOStream) bool +pub fn is_xv(src &sdl.RWops) bool { + return C.IMG_isXV(src) +} + +fn C.IMG_isWEBP(src &C.SDL_IOStream) bool +pub fn is_webp(src &sdl.RWops) bool { + return C.IMG_isWEBP(src) +} + +// Individual loading functions +fn C.IMG_LoadICO_IO(src &C.SDL_IOStream) &C.SDL_Surface +pub fn load_ico_io(src &sdl.RWops) &sdl.Surface { + return C.IMG_LoadICO_IO(src) +} + +fn C.IMG_LoadCUR_IO(src &C.SDL_IOStream) &C.SDL_Surface +pub fn load_cur_io(src &sdl.RWops) &sdl.Surface { + return C.IMG_LoadCUR_IO(src) +} + +fn C.IMG_LoadBMP_IO(src &C.SDL_IOStream) &C.SDL_Surface +pub fn load_bmp_io(src &sdl.RWops) &sdl.Surface { + return C.IMG_LoadBMP_IO(src) +} + +fn C.IMG_LoadGIF_IO(src &C.SDL_IOStream) &C.SDL_Surface +pub fn load_gif_io(src &sdl.RWops) &sdl.Surface { + return C.IMG_LoadGIF_IO(src) +} + +fn C.IMG_LoadJPG_IO(src &C.SDL_IOStream) &C.SDL_Surface +pub fn load_jpg_io(src &sdl.RWops) &sdl.Surface { + return C.IMG_LoadJPG_IO(src) +} + +fn C.IMG_LoadLBM_IO(src &C.SDL_IOStream) &C.SDL_Surface +pub fn load_lbm_io(src &sdl.RWops) &sdl.Surface { + return C.IMG_LoadLBM_IO(src) +} + +fn C.IMG_LoadPCX_IO(src &C.SDL_IOStream) &C.SDL_Surface +pub fn load_pcx_io(src &sdl.RWops) &sdl.Surface { + return C.IMG_LoadPCX_IO(src) +} + +fn C.IMG_LoadPNG_IO(src &C.SDL_IOStream) &C.SDL_Surface +pub fn load_png_io(src &sdl.RWops) &sdl.Surface { + return C.IMG_LoadPNG_IO(src) +} + +fn C.IMG_LoadPNM_IO(src &C.SDL_IOStream) &C.SDL_Surface +pub fn load_pnm_io(src &sdl.RWops) &sdl.Surface { + return C.IMG_LoadPNM_IO(src) +} + +fn C.IMG_LoadSVG_IO(src &C.SDL_IOStream) &C.SDL_Surface +pub fn load_svg_io(src &sdl.RWops) &sdl.Surface { + return C.IMG_LoadSVG_IO(src) +} + +fn C.IMG_LoadTGA_IO(src &C.SDL_IOStream) &C.SDL_Surface +pub fn load_tga_io(src &sdl.RWops) &sdl.Surface { + return C.IMG_LoadTGA_IO(src) +} + +fn C.IMG_LoadTIF_IO(src &C.SDL_IOStream) &C.SDL_Surface +pub fn load_tif_io(src &sdl.RWops) &sdl.Surface { + return C.IMG_LoadTIF_IO(src) +} + +fn C.IMG_LoadXCF_IO(src &C.SDL_IOStream) &C.SDL_Surface +pub fn load_xcf_io(src &sdl.RWops) &sdl.Surface { + return C.IMG_LoadXCF_IO(src) +} + +fn C.IMG_LoadXPM_IO(src &C.SDL_IOStream) &C.SDL_Surface +pub fn load_xpm_io(src &sdl.RWops) &sdl.Surface { + return C.IMG_LoadXPM_IO(src) +} + +fn C.IMG_LoadXV_IO(src &C.SDL_IOStream) &C.SDL_Surface +pub fn load_xv_io(src &sdl.RWops) &sdl.Surface { + return C.IMG_LoadXV_IO(src) +} + +fn C.IMG_LoadWEBP_IO(src &C.SDL_IOStream) &C.SDL_Surface +pub fn load_webp_io(src &sdl.RWops) &sdl.Surface { + return C.IMG_LoadWEBP_IO(src) +} + +fn C.IMG_ReadXPMFromArray(xpm &&char) &C.SDL_Surface +pub fn read_xpm_from_array(xpm &&char) &sdl.Surface { + return C.IMG_ReadXPMFromArray(xpm) +} + +// Individual saving functions +fn C.IMG_SavePNG(surface &C.SDL_Surface, const_file &char) bool +pub fn save_png(surface &sdl.Surface, const_file &char) bool { + return C.IMG_SavePNG(surface, const_file) +} + +fn C.IMG_SavePNG_IO(surface &C.SDL_Surface, dst &C.SDL_IOStream, closeio bool) bool +pub fn save_png_io(surface &sdl.Surface, dst &sdl.RWops, closeio bool) bool { + return C.IMG_SavePNG_IO(surface, dst, closeio) +} + +fn C.IMG_SaveJPG(surface &C.SDL_Surface, const_file &char, quality int) bool +pub fn save_jpg(surface &sdl.Surface, const_file &char, quality int) bool { + return C.IMG_SaveJPG(surface, const_file, quality) +} + +fn C.IMG_SaveJPG_IO(surface &C.SDL_Surface, dst &C.SDL_IOStream, closeio bool, quality int) bool +pub fn save_jpg_io(surface &sdl.Surface, dst &sdl.RWops, closeio bool, quality int) bool { + return C.IMG_SaveJPG_IO(surface, dst, closeio, quality) +} diff --git a/iostream.c.v b/iostream.c.v index 9642f941..8504dc75 100644 --- a/iostream.c.v +++ b/iostream.c.v @@ -72,6 +72,9 @@ pub struct C.SDL_IOStream { pub type IOStream = C.SDL_IOStream +// RWops is kept for SDL2-era APIs and maps to SDL_IOStream in SDL3. +pub type RWops = C.SDL_IOStream + // C.SDL_IOFromFile [official documentation](https://wiki.libsdl.org/SDL3/SDL_IOFromFile) fn C.SDL_IOFromFile(const_file &char, const_mode &char) &IOStream diff --git a/mixer/c.c.v b/mixer/c.c.v new file mode 100644 index 00000000..e89d2526 --- /dev/null +++ b/mixer/c.c.v @@ -0,0 +1,27 @@ +// Copyright(C) 2021 Lars Pontoppidan. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +module mixer + +$if !windows { + // SDL libs are loaded dynamically from Java on Android + $if !android || termux { + // sdl_no_compile_flags allow users to provide + // custom flags (e.g. via CFLAGS/LDFLAGS) for the compiler. + // This is especially useful when building/linking against a + // custom compiled version of the libs on *nix. + $if !sdl_no_compile_flags ? { + #flag -lSDL3_mixer + } + } +} + +$if x64 { + #flag windows -L @VMODROOT/thirdparty/SDL3_mixer-3.1.2/lib/x64 +} $else { + #flag windows -L @VMODROOT/thirdparty/SDL3_mixer-3.1.2/lib/x86 +} +#flag windows -I @VMODROOT/thirdparty/SDL3_mixer-3.1.2/include +#flag windows -lSDL3_mixer + +#include diff --git a/mixer/mixer.c.v b/mixer/mixer.c.v new file mode 100644 index 00000000..0c07fce6 --- /dev/null +++ b/mixer/mixer.c.v @@ -0,0 +1,159 @@ +// Copyright(C) 2026 Lars Pontoppidan. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +module mixer + +// +// SDL_mixer.h (SDL3) +// +import sdl + +pub const major_version = C.SDL_MIXER_MAJOR_VERSION +pub const minor_version = C.SDL_MIXER_MINOR_VERSION +pub const micro_version = C.SDL_MIXER_MICRO_VERSION + +// compiledversion is the version number macro for the current SDL_mixer version. +pub fn compiledversion() int { + return C.SDL_VERSIONNUM(major_version, minor_version, micro_version) +} + +fn C.MIX_Version() int + +// version gets the version of the dynamically linked SDL_mixer library. +pub fn version() int { + return C.MIX_Version() +} + +fn C.MIX_Init() bool + +// init initializes SDL_mixer. +pub fn init() bool { + return C.MIX_Init() +} + +fn C.MIX_Quit() + +// quit de-initializes SDL_mixer. +pub fn quit() { + C.MIX_Quit() +} + +@[noinit; typedef] +pub struct C.MIX_Mixer {} + +pub type Mixer = C.MIX_Mixer + +@[noinit; typedef] +pub struct C.MIX_Audio {} + +pub type Audio = C.MIX_Audio + +@[noinit; typedef] +pub struct C.MIX_Track {} + +pub type Track = C.MIX_Track + +fn C.MIX_CreateMixerDevice(devid sdl.AudioDeviceID, const_spec &sdl.AudioSpec) &C.MIX_Mixer + +// create_mixer_device creates a mixer that plays sound directly to an audio device. +pub fn create_mixer_device(devid sdl.AudioDeviceID, const_spec &sdl.AudioSpec) &Mixer { + return C.MIX_CreateMixerDevice(devid, const_spec) +} + +fn C.MIX_DestroyMixer(mixer &C.MIX_Mixer) + +// destroy_mixer frees a mixer and closes its audio device, if any. +pub fn destroy_mixer(mixer &Mixer) { + C.MIX_DestroyMixer(mixer) +} + +fn C.MIX_LoadAudio(mixer &C.MIX_Mixer, const_path &char, predecode bool) &C.MIX_Audio + +// load_audio loads audio data from a filesystem path. +pub fn load_audio(mixer &Mixer, const_path &char, predecode bool) &Audio { + return C.MIX_LoadAudio(mixer, const_path, predecode) +} + +fn C.MIX_DestroyAudio(audio &C.MIX_Audio) + +// destroy_audio frees audio data previously loaded. +pub fn destroy_audio(audio &Audio) { + C.MIX_DestroyAudio(audio) +} + +fn C.MIX_CreateTrack(mixer &C.MIX_Mixer) &C.MIX_Track + +// create_track creates a track for mixing audio. +pub fn create_track(mixer &Mixer) &Track { + return C.MIX_CreateTrack(mixer) +} + +fn C.MIX_DestroyTrack(track &C.MIX_Track) + +// destroy_track frees a track. +pub fn destroy_track(track &Track) { + C.MIX_DestroyTrack(track) +} + +fn C.MIX_SetTrackAudio(track &C.MIX_Track, audio &C.MIX_Audio) bool + +// set_track_audio assigns audio data to a track. +pub fn set_track_audio(track &Track, audio &Audio) bool { + return C.MIX_SetTrackAudio(track, audio) +} + +fn C.MIX_PlayTrack(track &C.MIX_Track, options sdl.PropertiesID) bool + +// play_track starts (or restarts) mixing a track. +pub fn play_track(track &Track, options sdl.PropertiesID) bool { + return C.MIX_PlayTrack(track, options) +} + +fn C.MIX_PlayAudio(mixer &C.MIX_Mixer, audio &C.MIX_Audio) bool + +// play_audio plays audio in a fire-and-forget manner. +pub fn play_audio(mixer &Mixer, audio &Audio) bool { + return C.MIX_PlayAudio(mixer, audio) +} + +fn C.MIX_SetTrackLoops(track &C.MIX_Track, num_loops int) bool + +// set_track_loops changes the number of times a track will loop. +pub fn set_track_loops(track &Track, num_loops int) bool { + return C.MIX_SetTrackLoops(track, num_loops) +} + +fn C.MIX_PauseTrack(track &C.MIX_Track) bool + +// pause_track pauses a track. +pub fn pause_track(track &Track) bool { + return C.MIX_PauseTrack(track) +} + +fn C.MIX_ResumeTrack(track &C.MIX_Track) bool + +// resume_track resumes a paused track. +pub fn resume_track(track &Track) bool { + return C.MIX_ResumeTrack(track) +} + +fn C.MIX_StopTrack(track &C.MIX_Track, fade_out_frames i64) bool + +// stop_track halts a track, optionally fading out over time. +pub fn stop_track(track &Track, fade_out_frames i64) bool { + return C.MIX_StopTrack(track, fade_out_frames) +} + +fn C.MIX_TrackPlaying(track &C.MIX_Track) bool + +// track_playing reports whether a track is playing. +pub fn track_playing(track &Track) bool { + return C.MIX_TrackPlaying(track) +} + +fn C.MIX_SetTrackGain(track &C.MIX_Track, gain f32) bool + +// set_track_gain sets track gain; 1.0 is full volume. +pub fn set_track_gain(track &Track, gain f32) bool { + return C.MIX_SetTrackGain(track, gain) +} diff --git a/ttf/c.c.v b/ttf/c.c.v new file mode 100644 index 00000000..14629ad4 --- /dev/null +++ b/ttf/c.c.v @@ -0,0 +1,28 @@ +// Copyright(C) 2021 Lars Pontoppidan. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +module ttf + +$if !windows { + // SDL libs are loaded dynamically from Java on Android + $if !android || termux { + // sdl_no_compile_flags allow users to provide + // custom flags (e.g. via CFLAGS/LDFLAGS) for the compiler. + // This is especially useful when building/linking against a + // custom compiled version of the libs on *nix. + $if !sdl_no_compile_flags ? { + #flag -lSDL3_ttf + } + } +} + +$if x64 { + #flag windows -L @VMODROOT/thirdparty/SDL3_ttf-3.2.2/lib/x64 +} $else { + #flag windows -L @VMODROOT/thirdparty/SDL3_ttf-3.2.2/lib/x86 +} + +#flag windows -I @VMODROOT/thirdparty/SDL3_ttf-3.2.2/include +#flag windows -lSDL3_ttf + +#include diff --git a/ttf/ttf.c.v b/ttf/ttf.c.v new file mode 100644 index 00000000..d0818915 --- /dev/null +++ b/ttf/ttf.c.v @@ -0,0 +1,84 @@ +// Copyright(C) 2026 Lars Pontoppidan. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +module ttf + +// +// SDL_ttf.h (SDL3) +// +import sdl + +pub const major_version = C.SDL_TTF_MAJOR_VERSION +pub const minor_version = C.SDL_TTF_MINOR_VERSION +pub const micro_version = C.SDL_TTF_MICRO_VERSION + +// compiledversion is the version number macro for the current SDL_ttf version. +pub fn compiledversion() int { + return C.SDL_VERSIONNUM(major_version, minor_version, micro_version) +} + +fn C.SDL_TTF_VERSION_ATLEAST(x int, y int, z int) bool + +// ttf_version_atleast evaluates to true if compiled with SDL_ttf at least X.Y.Z. +pub fn ttf_version_atleast(x int, y int, z int) bool { + return C.SDL_TTF_VERSION_ATLEAST(x, y, z) +} + +fn C.TTF_Version() int + +// version gets the version of the dynamically linked SDL_ttf library. +pub fn version() int { + return C.TTF_Version() +} + +@[typedef] +pub struct C.TTF_Font {} + +pub type Font = C.TTF_Font + +fn C.TTF_Init() bool + +// init initializes the TTF engine. +pub fn init() bool { + return C.TTF_Init() +} + +// init_ttf is kept for compatibility with older code. +pub fn init_ttf() bool { + return C.TTF_Init() +} + +fn C.TTF_OpenFont(file &char, ptsize f32) &C.TTF_Font + +// open_font opens a font file and creates a font of the specified point size. +pub fn open_font(file &char, ptsize f32) &Font { + return C.TTF_OpenFont(file, ptsize) +} + +fn C.TTF_OpenFontIO(src &C.SDL_IOStream, closeio bool, ptsize f32) &C.TTF_Font + +// open_font_io opens a font from an SDL_IOStream. +pub fn open_font_io(src &sdl.IOStream, closeio bool, ptsize f32) &Font { + return C.TTF_OpenFontIO(src, closeio, ptsize) +} + +fn C.TTF_RenderText_Solid(font &C.TTF_Font, text &char, length usize, fg C.SDL_Color) &C.SDL_Surface + +// render_text_solid creates a surface and renders the given UTF-8 text. +pub fn render_text_solid(font &Font, text string, fg sdl.Color) &sdl.Surface { + return C.TTF_RenderText_Solid(font, text.str, usize(text.len), fg) +} + +fn C.TTF_CloseFont(font &C.TTF_Font) + +// close_font closes an opened font file. +pub fn close_font(font &Font) { + C.TTF_CloseFont(font) +} + +fn C.TTF_Quit() + +// quit de-initializes the TTF engine. +pub fn quit() { + C.TTF_Quit() +} diff --git a/windows_install_dependencies.vsh b/windows_install_dependencies.vsh index 888d7da6..3fdb47f3 100644 --- a/windows_install_dependencies.vsh +++ b/windows_install_dependencies.vsh @@ -7,8 +7,8 @@ const is_terminal = os.is_atty(1) > 0 const urls = [ 'https://www.libsdl.org/release/SDL3-devel-3.4.0-VC.zip', 'https://www.libsdl.org/projects/SDL_image/release/SDL3_image-devel-3.4.0-VC.zip', - // 'https://www.libsdl.org/projects/SDL_ttf/release/SDL3_ttf-devel-2.0.15-VC.zip', - // 'https://www.libsdl.org/projects/SDL_mixer/release/SDL3_mixer-devel-2.0.4-VC.zip', + 'https://www.libsdl.org/projects/SDL_mixer/release/SDL3_mixer-devel-3.1.2-VC.zip', + 'https://www.libsdl.org/projects/SDL_ttf/release/SDL3_ttf-devel-3.2.2-VC.zip', ] fn main() {