mirror of
https://github.com/jugeeya/UltimateTrainingModpack.git
synced 2024-11-20 00:46:34 +00:00
Use layout.arc to Build UI (#473)
* Update lib.rs * Remaining fixes * Initial * Forgot to commit * Clippy * Initial * Use feature gate * Fixes * layout.arc actually working. Add training_mod_menu_button * Add docstring, fix from static * Use include_flate to keep binary smaller * Add most menu features back * Add help, tabs * Add help, tabs * Small refactor * Fix clippy, some other issues * Updated menu buttons, inactive text colour (btns), layouts --------- Co-authored-by: Matthew Edell <edell.matthew@gmail.com>
This commit is contained in:
parent
d23dcdf968
commit
3d6ea34eeb
9 changed files with 304 additions and 1169 deletions
|
@ -13,10 +13,10 @@ skyline_smash = { git = "https://github.com/ultimate-research/skyline-smash.git"
|
|||
skyline-web = { git = "https://github.com/skyline-rs/skyline-web.git" }
|
||||
bitflags = "1.2.1"
|
||||
parking_lot = { version = "0.12.0", features = ["nightly"] }
|
||||
include-flate = "0.1.4"
|
||||
lazy_static = "1.4.0"
|
||||
owo-colors = "2.1.0"
|
||||
once_cell = "1.12.0"
|
||||
ramhorns = "0.12.0"
|
||||
paste = "1.0"
|
||||
num = "0.4.0"
|
||||
num-derive = "0.3"
|
||||
|
@ -25,6 +25,7 @@ wsl = "0.1.0"
|
|||
strum = "0.21.0"
|
||||
strum_macros = "0.21.0"
|
||||
minreq = { version = "2", features = ["https-native", "json-using-serde"] }
|
||||
sarc = { version = "1.2.0", features = [], default_features = false }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
toml = "0.5.9"
|
||||
|
@ -54,4 +55,4 @@ plugin-dependencies = [
|
|||
|
||||
[features]
|
||||
outside_training_mode = []
|
||||
web_session_single_thread = []
|
||||
layout_arc_from_file = []
|
||||
|
|
2
ryujinx_build.ps1
vendored
2
ryujinx_build.ps1
vendored
|
@ -1,4 +1,4 @@
|
|||
$IP=(Test-Connection -ComputerName (hostname) -Count 1 | Select -ExpandProperty IPV4Address).IPAddressToString
|
||||
cargo skyline build --release
|
||||
cargo skyline build --release --features layout_arc_from_file
|
||||
Copy-Item target/aarch64-skyline-switch/release/libtraining_modpack.nro 'C:\Users\Jdsam\AppData\Roaming\Ryujinx\mods\contents\01006A800016E000\romfs\skyline\plugins\'
|
||||
cargo skyline listen --ip=$IP
|
2
ryujinx_build.sh
vendored
2
ryujinx_build.sh
vendored
|
@ -6,7 +6,7 @@ SMASH_APPLICATION_PATH="C:\Users\Jdsam\Downloads\Super Smash Bros. Ultimate (Wor
|
|||
RYUJINX_SMASH_SKYLINE_PLUGINS_PATH="/mnt/c/Users/Jdsam/AppData/Roaming/Ryujinx/mods/contents/01006a800016e000/romfs/skyline/plugins"
|
||||
|
||||
# Build with release feature
|
||||
cargo skyline build --release
|
||||
cargo skyline build --release --features layout_arc_from_file
|
||||
|
||||
# Copy over to plugins path
|
||||
cp target/aarch64-skyline-switch/release/libtraining_modpack.nro $RYUJINX_SMASH_SKYLINE_PLUGINS_PATH
|
||||
|
|
BIN
src/static/layout.arc
Normal file
BIN
src/static/layout.arc
Normal file
Binary file not shown.
|
@ -2,29 +2,21 @@ use crate::training::ui;
|
|||
use skyline::nn::ui2d::*;
|
||||
use smash::ui2d::{SmashPane, SmashTextBox};
|
||||
|
||||
pub static NUM_DISPLAY_PANES: usize = 1;
|
||||
|
||||
macro_rules! display_parent_fmt {
|
||||
($x:ident) => {
|
||||
format!("trMod_disp_{}", $x).as_str()
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! display_pic_fmt {
|
||||
($x:ident) => {
|
||||
format!("trMod_disp_{}_base", $x).as_str()
|
||||
format!("TrModDisp{}", $x).as_str()
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! display_header_fmt {
|
||||
($x:ident) => {
|
||||
format!("trMod_disp_{}_header", $x).as_str()
|
||||
format!("TrModDisp{}Header", $x).as_str()
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! display_txt_fmt {
|
||||
($x:ident) => {
|
||||
format!("trMod_disp_{}_txt", $x).as_str()
|
||||
format!("TrModDisp{}Txt", $x).as_str()
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -57,91 +49,7 @@ pub unsafe fn draw(root_pane: &mut Pane) {
|
|||
if let Some(text) = root_pane.find_pane_by_name_recursive(display_txt_fmt!(notification_idx)) {
|
||||
let text = text.as_textbox();
|
||||
text.set_text_string(message);
|
||||
text.set_default_material_colors();
|
||||
text.set_color(color.r, color.g, color.b, color.a);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub static BUILD_PIC_BASE: ui::PaneCreationCallback = |_, root_pane, original_build, layout, out_build_result_information, device, block, parts_build_data_set, build_arg_set, build_res_set, kind| unsafe {
|
||||
macro_rules! build {
|
||||
($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => {
|
||||
paste::paste! {
|
||||
&mut *(original_build(layout, out_build_result_information, device, &mut $block as *mut $resTyp as *mut ResPane, parts_build_data_set, build_arg_set, build_res_set, $kind,) as *mut $typ)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
(0..NUM_DISPLAY_PANES).for_each(|idx| {
|
||||
let block = block as *mut ResPictureWithTex<1>;
|
||||
let mut pic_block = *block;
|
||||
pic_block.set_name(display_pic_fmt!(idx));
|
||||
pic_block.set_pos(ResVec3::default());
|
||||
let pic_pane = build!(pic_block, ResPictureWithTex<1>, kind, Picture);
|
||||
pic_pane.detach();
|
||||
|
||||
// pic is loaded first, we can create our parent pane here.
|
||||
let disp_pane_kind = u32::from_le_bytes([b'p', b'a', b'n', b'1']);
|
||||
let mut disp_pane_block = ResPane::new(display_parent_fmt!(idx));
|
||||
disp_pane_block.set_pos(ResVec3::new(806.0, -50.0 - (idx as f32 * 110.0), 0.0));
|
||||
let disp_pane = build!(disp_pane_block, ResPane, disp_pane_kind, Pane);
|
||||
disp_pane.detach();
|
||||
root_pane.append_child(disp_pane);
|
||||
disp_pane.append_child(pic_pane);
|
||||
});
|
||||
};
|
||||
|
||||
pub static BUILD_PANE_TXT: ui::PaneCreationCallback = |_, root_pane, original_build, layout, out_build_result_information, device, block, parts_build_data_set, build_arg_set, build_res_set, kind| unsafe {
|
||||
macro_rules! build {
|
||||
($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => {
|
||||
paste::paste! {
|
||||
&mut *(original_build(layout, out_build_result_information, device, &mut $block as *mut $resTyp as *mut ResPane, parts_build_data_set, build_arg_set, build_res_set, $kind,) as *mut $typ)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
(0..NUM_DISPLAY_PANES).for_each(|idx| {
|
||||
let disp_pane = root_pane
|
||||
.find_pane_by_name(display_parent_fmt!(idx), true)
|
||||
.unwrap();
|
||||
|
||||
let block = block as *mut ResTextBox;
|
||||
let mut text_block = *block;
|
||||
text_block.set_name(display_txt_fmt!(idx));
|
||||
text_block.set_pos(ResVec3::new(-10.0, -25.0, 0.0));
|
||||
let text_pane = build!(text_block, ResTextBox, kind, TextBox);
|
||||
text_pane.set_text_string(format!("Pane {idx}!").as_str());
|
||||
// Ensure Material Colors are not hardcoded so we can just use SetTextColor.
|
||||
text_pane.set_default_material_colors();
|
||||
text_pane.detach();
|
||||
disp_pane.append_child(text_pane);
|
||||
});
|
||||
};
|
||||
|
||||
pub static BUILD_HEADER_TXT: ui::PaneCreationCallback = |_, root_pane, original_build, layout, out_build_result_information, device, block, parts_build_data_set, build_arg_set, build_res_set, kind| unsafe {
|
||||
macro_rules! build {
|
||||
($block: ident, $resTyp: ty, $kind:ident, $typ: ty) => {
|
||||
paste::paste! {
|
||||
&mut *(original_build(layout, out_build_result_information, device, &mut $block as *mut $resTyp as *mut ResPane, parts_build_data_set, build_arg_set, build_res_set, $kind,) as *mut $typ)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
(0..NUM_DISPLAY_PANES).for_each(|idx| {
|
||||
let disp_pane = root_pane
|
||||
.find_pane_by_name(display_parent_fmt!(idx), true)
|
||||
.unwrap();
|
||||
|
||||
let block = block as *mut ResTextBox;
|
||||
let mut header_block = *block;
|
||||
header_block.set_name(display_header_fmt!(idx));
|
||||
header_block.set_pos(ResVec3::new(0.0, 25.0, 0.0));
|
||||
let header_pane = build!(header_block, ResTextBox, kind, TextBox);
|
||||
header_pane.set_text_string(format!("Header {idx}").as_str());
|
||||
// Ensure Material Colors are not hardcoded so we can just use SetTextColor.
|
||||
header_pane.set_default_material_colors();
|
||||
// Header should be white text
|
||||
header_pane.set_color(255, 255, 255, 255);
|
||||
header_pane.detach();
|
||||
disp_pane.append_child(header_pane);
|
||||
});
|
||||
};
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,77 +1,13 @@
|
|||
use crate::common::{is_ready_go, is_training_mode};
|
||||
use sarc::SarcFile;
|
||||
use skyline::nn::ui2d::*;
|
||||
use training_mod_consts::{OnOff, MENU};
|
||||
use std::collections::HashMap;
|
||||
use parking_lot::Mutex;
|
||||
use skyline::libc::c_void;
|
||||
|
||||
mod damage;
|
||||
mod display;
|
||||
mod menu;
|
||||
pub mod notifications;
|
||||
|
||||
type PaneCreationCallback = for<'a, 'b> unsafe fn(&'a str, &'b mut Pane,
|
||||
extern "C" fn(*mut Layout, *mut u8, *const u8, *mut ResPane, *const u8, *const u8, *const u8, u32) -> *mut Pane,
|
||||
*mut Layout, *mut u8, *const u8, *mut ResPane,
|
||||
*const u8, *const u8, *const u8, u32);
|
||||
type PaneCreationMap = HashMap<
|
||||
(String, String), Vec<(bool, PaneCreationCallback)>
|
||||
>;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref PANE_CREATED: Mutex<PaneCreationMap> = Mutex::new(HashMap::from([
|
||||
(
|
||||
(String::from("info_training"), String::from("pic_numbase_01")),
|
||||
vec![
|
||||
(false, menu::BUILD_CONTAINER_PANE),
|
||||
(false, display::BUILD_PIC_BASE),
|
||||
(false, menu::BUILD_SLIDER_CONTAINER_PANE),
|
||||
]
|
||||
),
|
||||
(
|
||||
(String::from("info_training"), String::from("pic_help_bg_00")),
|
||||
vec![(false, menu::BUILD_FOOTER_BG)]
|
||||
),
|
||||
(
|
||||
(String::from("info_training"), String::from("set_txt_help_00")),
|
||||
vec![(false, menu::BUILD_FOOTER_TXT)]
|
||||
),
|
||||
(
|
||||
(String::from("info_training"), String::from("set_txt_num_01")),
|
||||
vec![
|
||||
(false, menu::BUILD_TAB_TXTS),
|
||||
(false, menu::BUILD_OPT_TXTS),
|
||||
(false, menu::BUILD_SLIDER_TXTS),
|
||||
(false, display::BUILD_PANE_TXT),
|
||||
]
|
||||
),
|
||||
(
|
||||
(String::from("info_training"), String::from("txt_cap_01")),
|
||||
vec![
|
||||
(false, display::BUILD_HEADER_TXT),
|
||||
(false, menu::BUILD_SLIDER_HEADER_TXT),
|
||||
]
|
||||
),
|
||||
(
|
||||
(String::from("info_training_btn0_00_item"), String::from("icn_bg_main")),
|
||||
vec![(false, menu::BUILD_BG_LEFTS)]
|
||||
),
|
||||
(
|
||||
(String::from("info_training_btn0_00_item"), String::from("btn_bg")),
|
||||
vec![(false, menu::BUILD_BG_BACKS)]
|
||||
),
|
||||
]));
|
||||
}
|
||||
|
||||
pub unsafe fn reset_creation() {
|
||||
let pane_created = &mut *PANE_CREATED.data_ptr();
|
||||
pane_created.iter_mut().for_each(|(_identifier, creators)| {
|
||||
creators.iter_mut().for_each(|(created, _callback)| {
|
||||
*created = false;
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[skyline::hook(offset = 0x4b620)]
|
||||
pub unsafe fn handle_draw(layout: *mut Layout, draw_info: u64, cmd_buffer: u64) {
|
||||
let layout_name = &skyline::from_c_str((*layout).layout_name);
|
||||
|
@ -94,76 +30,102 @@ pub unsafe fn handle_draw(layout: *mut Layout, draw_info: u64, cmd_buffer: u64)
|
|||
original!()(layout, draw_info, cmd_buffer);
|
||||
}
|
||||
|
||||
#[skyline::hook(offset = 0x493a0)]
|
||||
pub unsafe fn layout_build_parts_impl(
|
||||
layout: *mut Layout,
|
||||
out_build_result_information: *mut u8,
|
||||
device: *const u8,
|
||||
block: *mut ResPane,
|
||||
parts_build_data_set: *const u8,
|
||||
build_arg_set: *const u8,
|
||||
build_res_set: *const u8,
|
||||
kind: u32,
|
||||
) -> *mut Pane {
|
||||
let layout_name = &skyline::from_c_str((*layout).layout_name);
|
||||
let root_pane = &mut *(*layout).root_pane;
|
||||
// We'll keep some sane max size here; we shouldn't reach above 600KiB is the idea,
|
||||
// but we can try higher if we need to.
|
||||
#[cfg(feature = "layout_arc_from_file")]
|
||||
static mut LAYOUT_ARC : &mut [u8; 600000] = &mut [0u8; 600000];
|
||||
|
||||
let block_name = (*block).get_name();
|
||||
let identifier = (layout_name.to_string(), block_name);
|
||||
let pane_created = &mut *PANE_CREATED.data_ptr();
|
||||
let panes = pane_created.get_mut(&identifier);
|
||||
if let Some(panes) = panes {
|
||||
panes.iter_mut().for_each(|(has_created, callback)| {
|
||||
if !*has_created {
|
||||
callback(layout_name,
|
||||
root_pane,
|
||||
original!(),
|
||||
layout,
|
||||
out_build_result_information,
|
||||
device,
|
||||
block,
|
||||
parts_build_data_set,
|
||||
build_arg_set,
|
||||
build_res_set,
|
||||
kind
|
||||
);
|
||||
|
||||
// Special case: Menu init should always occur
|
||||
if ("info_training".to_string(), "pic_numbase_01".to_string()) != identifier {
|
||||
*has_created = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
/// We are editing the info_training/layout.arc and replacing the original file with our
|
||||
/// modified version from `sd://TrainingModpack/layout.arc` or, in the case of Ryujinx for the cool
|
||||
/// kids `${RYUJINX_DIR}/sdcard/TrainingModpack/layout.arc`
|
||||
///
|
||||
/// When we edit the layout we are doing two things.
|
||||
///
|
||||
/// 1. Creating a new BFLYT inside the layout.arc for whatever component we are making. For example,
|
||||
/// the slider menu button.
|
||||
///
|
||||
/// 2. Adding a Parts pane to the info_training.bflyt with the "Part Name" matching the name of
|
||||
/// our new BFLYT without the file extension (mimicking how it's done with native Parts panes)
|
||||
///
|
||||
/// # Warnings
|
||||
/// When creating a BFLYT from an existing one we need to edit the names of the panes so that
|
||||
/// the default animations no longer modify them. Otherwise the game will override properties,
|
||||
/// i.e. material colours, and we won't be able to control them properly.
|
||||
///
|
||||
/// Once we have the file edited and saved to the correct location we can access the pane
|
||||
/// from the layout as we normally would in our Draw function
|
||||
/// `(root_pane.find_pane_by_name_recursive("name_of_parts_pane")`.
|
||||
///
|
||||
/// # Usage
|
||||
/// Now say I want to edit background colour of the button's label.
|
||||
/// I would have to grab the parts pane and find the pane I want to modify on it, then I'd be able
|
||||
/// to make the modifications as I normally would.
|
||||
///
|
||||
/// ```rust
|
||||
/// let slider_button = root_pane.find_pane_by_name_recursive("name_of_parts_pane");
|
||||
/// let label_bg = slider_button.find_pane_by_name_recursive("name_of_picture_pane");
|
||||
///
|
||||
/// let label_material = &mut *label_bg.as_picture().material;
|
||||
///
|
||||
/// label_material.set_white_res_color(LABEL_WHITE_SELECTED_COLOR);
|
||||
/// label_material.set_black_res_color(LABEL_BLACK_SELECTED_COLOR);
|
||||
/// ```
|
||||
#[skyline::hook(offset = 0x37730d4, inline)]
|
||||
unsafe fn handle_layout_arc_malloc(
|
||||
ctx: &mut skyline::hooks::InlineCtx
|
||||
) {
|
||||
if !is_training_mode() {
|
||||
return;
|
||||
}
|
||||
|
||||
original!()(
|
||||
layout,
|
||||
out_build_result_information,
|
||||
device,
|
||||
block,
|
||||
parts_build_data_set,
|
||||
build_arg_set,
|
||||
build_res_set,
|
||||
kind,
|
||||
)
|
||||
}
|
||||
let decompressed_file = *ctx.registers[21].x.as_ref() as *const u8;
|
||||
let decompressed_size = *ctx.registers[1].x.as_ref() as usize;
|
||||
|
||||
let layout_arc = SarcFile::read(
|
||||
std::slice::from_raw_parts(decompressed_file,decompressed_size)
|
||||
).unwrap();
|
||||
let training_layout = layout_arc.files.iter().find(|f| {
|
||||
f.name.is_some() && f.name.as_ref().unwrap() == &String::from("blyt/info_training.bflyt")
|
||||
});
|
||||
if training_layout.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
#[skyline::hook(offset = 0x32cace0)]
|
||||
pub unsafe fn handle_load_layout_files(
|
||||
meta_layout_root: *mut c_void,
|
||||
loading_list: *mut c_void,
|
||||
layout_arc_hash: *const u32,
|
||||
param_4: i32
|
||||
) -> u64 {
|
||||
println!("Layout.arc hash: {:x}", *layout_arc_hash);
|
||||
original!()(meta_layout_root, loading_list, layout_arc_hash, param_4)
|
||||
let inject_arc;
|
||||
let inject_arc_size : u64;
|
||||
|
||||
#[cfg(feature = "layout_arc_from_file")] {
|
||||
let inject_arc_from_file = std::fs::read("sd:/TrainingModpack/layout.arc").unwrap();
|
||||
inject_arc_size = inject_arc_from_file.len() as u64;
|
||||
|
||||
// Copy read file to global
|
||||
inject_arc_from_file
|
||||
.iter()
|
||||
.enumerate()
|
||||
.for_each(|(idx, byte)| LAYOUT_ARC[idx] = *byte);
|
||||
inject_arc = LAYOUT_ARC.as_ptr();
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "layout_arc_from_file"))] {
|
||||
include_flate::flate!(static INJECT_ARC_FROM_FILE: [u8] from "src/static/layout.arc");
|
||||
|
||||
inject_arc = INJECT_ARC_FROM_FILE.as_ptr();
|
||||
inject_arc_size = INJECT_ARC_FROM_FILE.len() as u64;
|
||||
}
|
||||
|
||||
// Decompressed file pointer
|
||||
let decompressed_file = ctx.registers[21].x.as_mut();
|
||||
*decompressed_file = inject_arc as u64;
|
||||
|
||||
// Decompressed size is in each of these registers
|
||||
*ctx.registers[1].x.as_mut() = inject_arc_size;
|
||||
*ctx.registers[23].x.as_mut() = inject_arc_size;
|
||||
*ctx.registers[24].x.as_mut() = inject_arc_size;
|
||||
}
|
||||
|
||||
pub fn init() {
|
||||
skyline::install_hooks!(
|
||||
handle_draw,
|
||||
layout_build_parts_impl,
|
||||
// handle_load_layout_files
|
||||
handle_layout_arc_malloc
|
||||
);
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ strum_macros = "0.21.0"
|
|||
num = "0.4.0"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
ramhorns = "0.12.0"
|
||||
paste = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_repr = "0.1.8"
|
||||
|
|
|
@ -8,7 +8,6 @@ extern crate bitflags_serde_shim;
|
|||
extern crate num_derive;
|
||||
|
||||
use core::f64::consts::PI;
|
||||
use ramhorns::Content;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
#[cfg(feature = "smash")]
|
||||
|
@ -1238,7 +1237,7 @@ pub static DEFAULTS_MENU: TrainingModpackMenu = TrainingModpackMenu {
|
|||
|
||||
pub static mut MENU: TrainingModpackMenu = DEFAULTS_MENU;
|
||||
|
||||
#[derive(Content, Clone, Serialize)]
|
||||
#[derive(Clone, Serialize)]
|
||||
pub struct Slider {
|
||||
pub selected_min: u32,
|
||||
pub selected_max: u32,
|
||||
|
@ -1246,14 +1245,14 @@ pub struct Slider {
|
|||
pub abs_max: u32,
|
||||
}
|
||||
|
||||
#[derive(Content, Clone, Serialize)]
|
||||
#[derive(Clone, Serialize)]
|
||||
pub struct Toggle<'a> {
|
||||
pub toggle_value: u32,
|
||||
pub toggle_title: &'a str,
|
||||
pub checked: bool,
|
||||
}
|
||||
|
||||
#[derive(Content, Clone, Serialize)]
|
||||
#[derive(Clone, Serialize)]
|
||||
pub struct SubMenu<'a> {
|
||||
pub submenu_title: &'a str,
|
||||
pub submenu_id: &'a str,
|
||||
|
@ -1327,7 +1326,7 @@ impl<'a> SubMenu<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Content, Serialize, Clone)]
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct Tab<'a> {
|
||||
pub tab_id: &'a str,
|
||||
pub tab_title: &'a str,
|
||||
|
@ -1370,7 +1369,7 @@ impl<'a> Tab<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Content, Serialize, Clone)]
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct UiMenu<'a> {
|
||||
pub tabs: Vec<Tab<'a>>,
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue