diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..0163641 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @rust3ds/active diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml new file mode 100644 index 0000000..a86d6f6 --- /dev/null +++ b/.github/actions/setup/action.yml @@ -0,0 +1,46 @@ +name: Setup +description: Set up CI environment for Rust + 3DS development + +inputs: + toolchain: + description: The Rust toolchain to use for the steps + required: true + default: nightly + +runs: + using: composite + steps: + # https://github.com/nektos/act/issues/917#issuecomment-1074421318 + - if: ${{ env.ACT }} + shell: bash + name: Hack container for local development + run: | + curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - + sudo apt-get install -y nodejs + + - name: Setup default Rust toolchain + # Use this helper action so we get matcher support + # https://github.com/actions-rust-lang/setup-rust-toolchain/pull/15 + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + components: clippy, rustfmt, rust-src + toolchain: ${{ inputs.toolchain }} + + - name: Install build tools for host + shell: bash + run: sudo apt-get update && sudo apt-get install -y build-essential + + - name: Install cargo-3ds + uses: actions-rs/cargo@v1 + with: + command: install + # TODO: this should probably just be a released version from crates.io + # once cargo-3ds gets published somewhere... + args: >- + --git https://github.com/rust3ds/cargo-3ds + --rev 78a652fdfb01e2614a792d1a56b10c980ee1dae9 + + - name: Set PATH to include devkitARM + shell: bash + # For some reason devkitARM/bin is not part of the default PATH in the container + run: echo "${DEVKITARM}/bin" >> $GITHUB_PATH diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 102c704..20a22ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,8 @@ on: env: # https://blog.rust-lang.org/2022/06/22/sparse-registry-testing.html CARGO_UNSTABLE_SPARSE_REGISTRY: "true" + # actions-rust-lang/setup-rust-toolchain sets some default RUSTFLAGS + RUSTFLAGS: "" jobs: lint: @@ -22,41 +24,24 @@ jobs: - nightly-2023-01-13 # Check for breakage on latest nightly - nightly + # But if latest nightly fails, allow the workflow to continue continue-on-error: ${{ matrix.toolchain == 'nightly' }} runs-on: ubuntu-latest container: devkitpro/devkitarm steps: - # https://github.com/nektos/act/issues/917#issuecomment-1074421318 - - if: ${{ env.ACT }} - name: Hack container for local development - run: | - curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - - sudo apt-get install -y nodejs - - name: Checkout branch uses: actions/checkout@v2 - - name: Setup default Rust toolchain - uses: actions-rs/toolchain@v1 + - uses: ./.github/actions/setup with: - components: clippy, rustfmt, rust-src - profile: minimal toolchain: ${{ matrix.toolchain }} - default: true - - name: Install build tools for host - run: sudo apt-get update && sudo apt-get install -y build-essential - - - name: Install cargo-3ds - uses: actions-rs/cargo@v1 - with: - command: install - # TODO: this should probably just be a released version from crates.io - # once cargo-3ds gets published somewhere... - args: >- - --git https://github.com/rust3ds/cargo-3ds - --rev 7b70b6b26c4740b9a10ab85b832ee73c41142bbb + - name: Hide duplicate warnings from lint job + if: ${{ matrix.toolchain == 'nightly' }} + run: | + echo "::remove-matcher owner=clippy::" + echo "::remove-matcher owner=rustfmt::" - name: Check formatting run: cargo fmt --all --verbose -- --check @@ -68,5 +53,28 @@ jobs: # feature, but https://github.com/actions/runner/issues/2341 means we # can't have both that *and* colored output. + doctests: + strategy: + matrix: + toolchain: + - nightly-2023-01-13 + - nightly + continue-on-error: ${{ matrix.toolchain == 'nightly' }} + runs-on: ubuntu-latest + container: devkitpro/devkitarm + steps: + - name: Checkout branch + uses: actions/checkout@v2 + + - uses: ./.github/actions/setup + with: + toolchain: ${{ matrix.toolchain }} + + - name: Hide duplicated warnings from lint job + run: echo "::remove-matcher owner=clippy::" + + - name: Build doc tests + run: cargo 3ds test --doc --verbose + # TODO: it would be nice to actually build 3dsx for examples/tests, etc. # and run it somehow, but exactly how remains to be seen. diff --git a/ctru-rs/examples/graphics-bitmap.rs b/ctru-rs/examples/graphics-bitmap.rs new file mode 100644 index 0000000..4beacea --- /dev/null +++ b/ctru-rs/examples/graphics-bitmap.rs @@ -0,0 +1,57 @@ +use ctru::prelude::*; +use ctru::services::gfx::Screen; + +/// Ferris image taken from and scaled down to 320x240px. +/// To regenerate the data, you will need to install `imagemagick` and run this +/// command from the `examples` directory: +/// +/// ```sh +/// magick assets/ferris.png -channel-fx "red<=>blue" -rotate 90 assets/ferris.rgb +/// ``` +/// +/// This creates an image appropriate for the default frame buffer format of +/// [`Bgr8`](ctru::services::gspgpu::FramebufferFormat::Bgr8) +/// and rotates the image 90° to account for the portrait mode screen. +static IMAGE: &[u8] = include_bytes!("assets/ferris.rgb"); + +fn main() { + ctru::use_panic_handler(); + + let gfx = Gfx::new().expect("Couldn't obtain GFX controller"); + let mut hid = Hid::new().expect("Couldn't obtain HID controller"); + let apt = Apt::new().expect("Couldn't obtain APT controller"); + let _console = Console::new(gfx.top_screen.borrow_mut()); + + println!("\x1b[21;16HPress Start to exit."); + + let mut bottom_screen = gfx.bottom_screen.borrow_mut(); + + // We don't need double buffering in this example. + // In this way we can draw our image only once on screen. + bottom_screen.set_double_buffering(false); + + // We assume the image is the correct size already, so we drop width + height. + let frame_buffer = bottom_screen.raw_framebuffer(); + + // Copy the image into the frame buffer + unsafe { + frame_buffer.ptr.copy_from(IMAGE.as_ptr(), IMAGE.len()); + } + + // Main loop + while apt.main_loop() { + //Scan all the inputs. This should be done once for each frame + hid.scan_input(); + + if hid.keys_down().contains(KeyPad::START) { + break; + } + + // Flush and swap framebuffers + bottom_screen.flush_buffer(); + bottom_screen.swap_buffers(); + + //Wait for VBlank + gfx.wait_for_vblank(); + } +} diff --git a/ctru-rs/examples/touch-screen.rs b/ctru-rs/examples/touch-screen.rs new file mode 100644 index 0000000..204f324 --- /dev/null +++ b/ctru-rs/examples/touch-screen.rs @@ -0,0 +1,49 @@ +use ctru::prelude::*; + +fn main() { + ctru::use_panic_handler(); + + let gfx = Gfx::new().expect("Couldn't obtain GFX controller"); + let mut hid = Hid::new().expect("Couldn't obtain HID controller"); + let apt = Apt::new().expect("Couldn't obtain APT controller"); + + let console = Console::new(gfx.top_screen.borrow_mut()); + + // We'll hold the previous touch position for comparison. + let mut old_touch: (u16, u16) = (0, 0); + + println!("\x1b[29;16HPress Start to exit"); + + while apt.main_loop() { + hid.scan_input(); + + if hid.keys_down().contains(KeyPad::START) { + break; + } + + // Get X and Y coordinates of the touch point. + // The touch screen is 320x240. + let touch: (u16, u16) = hid.touch_position(); + + // We only want to print the position when it's different + // from what it was on the previous frame + if touch != old_touch { + // Special case for when the user lifts the stylus/finger from the screen. + // This is done to avoid some screen tearing. + if touch == (0, 0) { + console.clear(); + + // Print again because we just cleared the screen + println!("\x1b[29;16HPress Start to exit"); + } + + // Move the cursor back to the top of the screen and print the coordinates + print!("\x1b[1;1HTouch Screen position: {:#?}", touch); + } + + // Save our current touch position for the next frame + old_touch = touch; + + gfx.wait_for_vblank(); + } +} diff --git a/ctru-rs/src/applets/mii_selector.rs b/ctru-rs/src/applets/mii_selector.rs index 9d3b3ca..a6acd61 100644 --- a/ctru-rs/src/applets/mii_selector.rs +++ b/ctru-rs/src/applets/mii_selector.rs @@ -9,7 +9,7 @@ use std::ffi::CString; /// Index of a Mii used to configure some parameters of the Mii Selector /// Can be either a single index, or _all_ Miis #[derive(Debug, Clone, Copy, Eq, PartialEq)] -pub enum MiiIndex { +pub enum Index { Index(u32), All, } @@ -94,40 +94,40 @@ impl MiiSelector { } /// Whitelist a guest Mii - pub fn whitelist_guest_mii(&mut self, mii_index: MiiIndex) { + pub fn whitelist_guest_mii(&mut self, mii_index: Index) { let index = match mii_index { - MiiIndex::Index(i) => i, - MiiIndex::All => ctru_sys::MIISELECTOR_GUESTMII_SLOTS, + Index::Index(i) => i, + Index::All => ctru_sys::MIISELECTOR_GUESTMII_SLOTS, }; unsafe { ctru_sys::miiSelectorWhitelistGuestMii(self.config.as_mut(), index) } } /// Blacklist a guest Mii - pub fn blacklist_guest_mii(&mut self, mii_index: MiiIndex) { + pub fn blacklist_guest_mii(&mut self, mii_index: Index) { let index = match mii_index { - MiiIndex::Index(i) => i, - MiiIndex::All => ctru_sys::MIISELECTOR_GUESTMII_SLOTS, + Index::Index(i) => i, + Index::All => ctru_sys::MIISELECTOR_GUESTMII_SLOTS, }; unsafe { ctru_sys::miiSelectorBlacklistGuestMii(self.config.as_mut(), index) } } /// Whitelist a user Mii - pub fn whitelist_user_mii(&mut self, mii_index: MiiIndex) { + pub fn whitelist_user_mii(&mut self, mii_index: Index) { let index = match mii_index { - MiiIndex::Index(i) => i, - MiiIndex::All => ctru_sys::MIISELECTOR_USERMII_SLOTS, + Index::Index(i) => i, + Index::All => ctru_sys::MIISELECTOR_USERMII_SLOTS, }; unsafe { ctru_sys::miiSelectorWhitelistUserMii(self.config.as_mut(), index) } } /// Blacklist a user Mii - pub fn blacklist_user_mii(&mut self, mii_index: MiiIndex) { + pub fn blacklist_user_mii(&mut self, mii_index: Index) { let index = match mii_index { - MiiIndex::Index(i) => i, - MiiIndex::All => ctru_sys::MIISELECTOR_USERMII_SLOTS, + Index::Index(i) => i, + Index::All => ctru_sys::MIISELECTOR_USERMII_SLOTS, }; unsafe { ctru_sys::miiSelectorBlacklistUserMii(self.config.as_mut(), index) } @@ -185,7 +185,7 @@ impl From for SelectionResult { } } -impl From for MiiIndex { +impl From for Index { fn from(v: u32) -> Self { Self::Index(v) } diff --git a/ctru-rs/src/services/cfgu.rs b/ctru-rs/src/services/cfgu.rs index 0c076a3..a9cc411 100644 --- a/ctru-rs/src/services/cfgu.rs +++ b/ctru-rs/src/services/cfgu.rs @@ -36,12 +36,12 @@ pub enum Language { #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[repr(u32)] pub enum SystemModel { - N3DS = ctru_sys::CFG_MODEL_3DS, - N3DSXL = ctru_sys::CFG_MODEL_3DSXL, - NewN3DS = ctru_sys::CFG_MODEL_N3DS, - N2DS = ctru_sys::CFG_MODEL_2DS, - NewN3DSXL = ctru_sys::CFG_MODEL_N3DSXL, - NewN2DSXL = ctru_sys::CFG_MODEL_N2DSXL, + Old3DS = ctru_sys::CFG_MODEL_3DS, + Old3DSXL = ctru_sys::CFG_MODEL_3DSXL, + New3DS = ctru_sys::CFG_MODEL_N3DS, + Old2DS = ctru_sys::CFG_MODEL_2DS, + New3DSXL = ctru_sys::CFG_MODEL_N3DSXL, + New2DSXL = ctru_sys::CFG_MODEL_N2DSXL, } /// Represents the configuration service. No actions can be performed @@ -163,12 +163,12 @@ impl TryFrom for SystemModel { fn try_from(value: u8) -> Result { match value as u32 { - ctru_sys::CFG_MODEL_3DS => Ok(SystemModel::N3DS), - ctru_sys::CFG_MODEL_3DSXL => Ok(SystemModel::N3DSXL), - ctru_sys::CFG_MODEL_N3DS => Ok(SystemModel::NewN3DS), - ctru_sys::CFG_MODEL_2DS => Ok(SystemModel::N2DS), - ctru_sys::CFG_MODEL_N3DSXL => Ok(SystemModel::NewN3DSXL), - ctru_sys::CFG_MODEL_N2DSXL => Ok(SystemModel::NewN2DSXL), + ctru_sys::CFG_MODEL_3DS => Ok(SystemModel::Old3DS), + ctru_sys::CFG_MODEL_3DSXL => Ok(SystemModel::Old3DSXL), + ctru_sys::CFG_MODEL_N3DS => Ok(SystemModel::New3DS), + ctru_sys::CFG_MODEL_2DS => Ok(SystemModel::Old2DS), + ctru_sys::CFG_MODEL_N3DSXL => Ok(SystemModel::New3DSXL), + ctru_sys::CFG_MODEL_N2DSXL => Ok(SystemModel::New2DSXL), _ => Err(()), } } diff --git a/ctru-rs/src/services/fs.rs b/ctru-rs/src/services/fs.rs index 7788373..d4b855e 100644 --- a/ctru-rs/src/services/fs.rs +++ b/ctru-rs/src/services/fs.rs @@ -99,7 +99,7 @@ pub struct Fs(()); /// ```no_run /// use ctru::services::fs::Fs; /// -/// let fs = Fs::new().unwrap(); +/// let mut fs = Fs::new().unwrap(); /// let sdmc_archive = fs.sdmc().unwrap(); /// ``` pub struct Archive { @@ -119,47 +119,62 @@ pub struct Archive { /// Create a new file and write bytes to it: /// /// ```no_run +/// # use std::error::Error; +/// # fn main() -> Result<(), Box> { +/// # /// use std::io::prelude::*; /// use ctru::services::fs::{Fs, File}; /// -/// let fs = Fs::new()?; -/// let sdmc = fs.sdmc()?; -/// -/// let mut file = File::create(&sdmc, "/foo.txt")?; -/// file.write_all(b"Hello, world!")?; +/// let mut fs = Fs::new()?; +/// let mut sdmc = fs.sdmc()?; +/// # +/// # Ok(()) +/// # } /// ``` /// /// Read the contents of a file into a `String`:: /// /// ```no_run +/// # use std::error::Error; +/// # fn main() -> Result<(), Box> { +/// # /// use std::io::prelude::*; /// use ctru::services::fs::{Fs, File}; /// -/// let fs = Fs::new()?; -/// let sdmc = fs.sdmc()?; +/// let mut fs = Fs::new()?; +/// let mut sdmc = fs.sdmc()?; /// /// let mut file = File::open(&sdmc, "/foo.txt")?; /// let mut contents = String::new(); /// file.read_to_string(&mut contents)?; /// assert_eq!(contents, "Hello, world!"); +/// # +/// # Ok(()) +/// # } /// ``` /// /// It can be more efficient to read the contents of a file with a buffered /// `Read`er. This can be accomplished with `BufReader`: /// /// ```no_run +/// # use std::error::Error; +/// # fn main() -> Result<(), Box> { +/// # /// use std::io::BufReader; /// use std::io::prelude::*; /// use ctru::services::fs::{Fs, File}; /// -/// let fs = Fs::new()?; -/// let sdmc = fs.sdmc()?; +/// let mut fs = Fs::new()?; +/// let mut sdmc = fs.sdmc()?; /// /// let file = File::open(&sdmc, "/foo.txt")?; /// let mut buf_reader = BufReader::new(file); /// let mut contents = String::new(); /// buf_reader.read_to_string(&mut contents)?; /// assert_eq!(contents, "Hello, world!"); +/// # +/// # Ok(()) +/// # } /// ``` pub struct File { handle: u32, @@ -205,13 +220,13 @@ pub struct Metadata { /// ```no_run /// use ctru::services::fs::{Fs, OpenOptions}; /// +<<<<<<< HEAD /// let fs = Fs::new().unwrap(); /// let sdmc_archive = fs.sdmc().unwrap(); -/// let file = OpenOptions::new() -/// .read(true) +======= +/// let mut fs = Fs::new().unwrap(); /// .archive(&sdmc_archive) /// .open("foo.txt") -/// .unwrap(); /// ``` /// /// Opening a file for both reading and writing, as well as creating it if it @@ -220,8 +235,13 @@ pub struct Metadata { /// ```no_run /// use ctru::services::fs::{Fs, OpenOptions}; /// +<<<<<<< HEAD /// let fs = Fs::new().unwrap(); /// let sdmc_archive = fs.sdmc().unwrap(); +======= +/// let mut fs = Fs::new().unwrap(); +/// let mut sdmc_archive = fs.sdmc().unwrap(); +>>>>>>> improve/api /// let file = OpenOptions::new() /// .read(true) /// .write(true) @@ -347,8 +367,13 @@ impl File { /// ```no_run /// use ctru::services::fs::{Fs, File}; /// +<<<<<<< HEAD /// let fs = Fs::new().unwrap(); /// let sdmc_archive = fs.sdmc().unwrap(); +======= + /// let mut fs = Fs::new().unwrap(); + /// let mut sdmc_archive = fs.sdmc().unwrap(); +>>>>>>> improve/api /// let mut f = File::open(&sdmc_archive, "/foo.txt").unwrap(); /// ``` pub fn open>(arch: &Archive, path: P) -> IoResult { @@ -376,9 +401,15 @@ impl File { /// ```no_run /// use ctru::services::fs::{Fs, File}; /// +<<<<<<< HEAD /// let fs = Fs::new().unwrap(); /// let sdmc_archive = fs.sdmc().unwrap(); /// let mut f = File::create(&sdmc_archive, "/foo.txt").unwrap(); +======= + /// let mut fs = Fs::new().unwrap(); + /// let mut sdmc_archive = fs.sdmc().unwrap(); + /// let mut f = File::create(&mut sdmc_archive, "/foo.txt").unwrap(); +>>>>>>> improve/api /// ``` pub fn create>(arch: &mut Archive, path: P) -> IoResult { OpenOptions::new() diff --git a/ctru-rs/src/services/hid.rs b/ctru-rs/src/services/hid.rs index 79a649b..fa91657 100644 --- a/ctru-rs/src/services/hid.rs +++ b/ctru-rs/src/services/hid.rs @@ -46,14 +46,6 @@ bitflags::bitflags! { /// This service requires no special permissions to use. pub struct Hid(()); -/// Represents user input to the touchscreen. -#[derive(Debug, Clone, Copy)] -pub struct TouchPosition(ctru_sys::touchPosition); - -/// Represents the current position of the 3DS circle pad. -#[derive(Debug, Clone, Copy)] -pub struct CirclePosition(ctru_sys::circlePosition); - /// Initializes the HID service. /// /// # Errors @@ -102,47 +94,33 @@ impl Hid { KeyPad::from_bits_truncate(keys) } } -} - -impl Default for TouchPosition { - fn default() -> Self { - TouchPosition(ctru_sys::touchPosition { px: 0, py: 0 }) - } -} -impl TouchPosition { - /// Create a new TouchPosition instance. - pub fn new() -> Self { - Self::default() - } + /// Returns the current touch position in pixels (x, y). + /// + /// # Notes + /// + /// (0, 0) represents the top left corner of the screen. + pub fn touch_position(&mut self) -> (u16, u16) { + let mut res = ctru_sys::touchPosition { px: 0, py: 0 }; - /// Returns the current touch position in pixels. - pub fn get(&mut self) -> (u16, u16) { unsafe { - ctru_sys::hidTouchRead(&mut self.0); + ctru_sys::hidTouchRead(&mut res); } - (self.0.px, self.0.py) - } -} - -impl Default for CirclePosition { - fn default() -> Self { - CirclePosition(ctru_sys::circlePosition { dx: 0, dy: 0 }) + (res.px, res.py) } -} -impl CirclePosition { - /// Create a new CirclePosition instance. - pub fn new() -> Self { - Self::default() - } + /// Returns the current circle pad position in relative (x, y). + /// + /// # Notes + /// + /// (0, 0) represents the center of the circle pad. + pub fn circlepad_position(&mut self) -> (i16, i16) { + let mut res = ctru_sys::circlePosition { dx: 0, dy: 0 }; - /// Returns the current circle pad position in (x, y) form. - pub fn get(&mut self) -> (i16, i16) { unsafe { - ctru_sys::hidCircleRead(&mut self.0); + ctru_sys::hidCircleRead(&mut res); } - (self.0.dx, self.0.dy) + (res.dx, res.dy) } }