Skip to content

Commit 20406fd

Browse files
committed
🎉 automatically manage luau-api of project
1 parent dc0ccc8 commit 20406fd

File tree

8 files changed

+251
-92
lines changed

8 files changed

+251
-92
lines changed

TODO.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@
1818
- [x] Add a way to pick the prefered text editor (with vscode as default)
1919
- [x] Write the bundle.py script to watch the specs defined in plugins.md
2020
- [x] Manage files that the editor needs to work that are not inside the editor binary in a cross-platform way.
21-
- [ ] Show plugins in the editor
22-
- [ ] Automatically manage the `luau-api` folder (create it when the project is created, verify its integrity on load, etc...)
21+
- [x] Show plugins in the editor
22+
- [x] Automatically manage the `luau-api` folder (create it when the project is created, verify its integrity on load, etc...)
2323
- [ ] Add all editor hooks (debug menu)
2424
- [ ] Reload plugins in the editor
25-
- [ ] Show supported platforms in the editor
26-
- [ ] Add ability to load/unload plugins from the editor from the filesystem
25+
- [x] Show supported platforms in the editor
26+
- [x] Add ability to load/unload plugins from the editor from the filesystem
2727
- [ ] Add ability to download plugins from the editor
2828
- [ ] Add documentation on how to create and use plugins
2929
- [ ] Test that it works on Linux

docs/user-manual.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -858,10 +858,13 @@ Working on a game with other people is more fun!
858858

859859
## Vectarine and Git
860860

861-
Vectarine works well with version control systems like [Git](https://git-scm.com/). If you already now Git, use it!
861+
Vectarine works well with version control systems like [Git](https://git-scm.com/). If you already now Git, use it! You can add "luau-api" to your .gitignore
862+
as it is automatically generated by Vectarine when the project is loaded.
862863

863864
If you don't know Git, do not use it, it is complex to learn.
864865

866+
867+
865868
## Vectarine and shared folders
866869

867870
You use shared folder using Google Drive, Dropbox to have multiple people working on the same project.

editor/src/editorinterface.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ use crate::{
3333
},
3434
egui_sdl2_platform,
3535
export::exportinterface::draw_editor_export,
36-
pluginsystem::trustedplugin::{self, PluginEntry},
36+
pluginsystem::trustedplugin::{self, PluginEntry, TrustedPlugin},
3737
projectstate::ProjectState,
3838
};
3939
use editorconsole::draw_editor_console;
@@ -101,6 +101,7 @@ impl EditorState {
101101
let video = self.video.clone();
102102
let window = self.window.clone();
103103
let debouncer = self.debouncer.clone();
104+
let trusted_plugins = self.get_trusted_plugins();
104105

105106
LocalFileSystem.read_file(
106107
geteditorpaths::get_editor_config_path()
@@ -138,6 +139,7 @@ impl EditorState {
138139
gl,
139140
video,
140141
window,
142+
&trusted_plugins,
141143
|loaded_project| {
142144
if let Ok(loaded_project) = loaded_project {
143145
project.replace(Some(loaded_project));
@@ -200,9 +202,12 @@ impl EditorState {
200202
self.gl.clone(),
201203
self.video.clone(),
202204
self.window.clone(),
205+
&self.get_trusted_plugins(),
203206
|project| {
204207
match project {
205-
Ok(p) => self.project.borrow_mut().replace(p),
208+
Ok(p) => {
209+
self.project.borrow_mut().replace(p);
210+
}
206211
Err(e) => {
207212
callback(Err(e));
208213
return;
@@ -303,6 +308,16 @@ impl EditorState {
303308
platform.handle_event(event, sdl, &self.video.borrow());
304309
}
305310
}
311+
312+
pub fn get_trusted_plugins(&self) -> Vec<TrustedPlugin> {
313+
self.plugins
314+
.iter()
315+
.filter_map(|entry| match entry {
316+
PluginEntry::Trusted(trusted_plugin) => Some(trusted_plugin.clone()),
317+
PluginEntry::Malformed(_) => None,
318+
})
319+
.collect::<Vec<TrustedPlugin>>()
320+
}
306321
}
307322

308323
pub fn handle_close_events(latest_events: &[sdl2::event::Event]) {

editor/src/editorinterface/editorplugins.rs

Lines changed: 96 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{borrow::Cow, fs};
1+
use std::{borrow::Cow, fs, path::PathBuf};
22

33
use egui_extras::{Column, TableBody, TableBuilder};
44
use runtime::egui::{self, Label};
@@ -8,7 +8,9 @@ use crate::{
88
EditorState,
99
extra::geteditorpaths::{get_editor_plugins_path, get_end_of_path},
1010
},
11-
pluginsystem::trustedplugin::{self, PluginEntry, TrustedPlugin},
11+
pluginsystem::trustedplugin::{
12+
self, PluginEntry, TrustedPlugin, get_available_filename_for_trusted_plugin,
13+
},
1214
projectstate::ProjectState,
1315
};
1416

@@ -42,6 +44,10 @@ pub fn draw_editor_plugin_manager(editor: &mut EditorState, ctx: &egui::Context)
4244
}
4345

4446
fn draw_editor_plugin_manager_content(editor: &mut EditorState, ui: &mut egui::Ui) {
47+
// Both refresh buttons do the same as there is no case where you want to refresh one list without refreshing the other.
48+
// There are 2 buttons in the UI to drive away the point that there are 2 different concepts: game plugins and trusted plugins.
49+
let mut should_refresh_plugins = false;
50+
4551
ui.horizontal(|ui|{
4652
if ui.button("Open trusted plugins folder")
4753
.on_hover_text("Open the folder where trusted plugins are stored. You can add plugins there and they will appear in the list of trusted plugins.")
@@ -54,24 +60,27 @@ fn draw_editor_plugin_manager_content(editor: &mut EditorState, ui: &mut egui::U
5460
}
5561

5662
if ui.button("Refresh trusted plugin list").clicked() {
57-
editor.plugins = trustedplugin::load_plugins();
63+
should_refresh_plugins = true;
5864
}
5965
});
6066

61-
let plugins = &mut editor.plugins;
62-
63-
if plugins.is_empty() {
67+
if editor.plugins.is_empty() {
6468
ui.label("No plugins found").on_hover_text("Plugins are programs that extend Vectarine's functionality. They are files ending with '.vecta.plugin'. You can download plugins or create them using the template provided by Vectarine GitHub repository.");
6569
} else {
6670
ui.heading("Trusted plugins").on_hover_text("Trusted plugins are the list of plugins known to the editor. Only plugins of a game that are also inside the trusted list are executed.");
6771

6872
draw_table_header_for_plugin(ui, "trusted", |body| {
69-
for plugin in plugins.iter_mut() {
73+
for plugin in editor.plugins.iter_mut() {
7074
let row_height = 20.0;
7175
match plugin {
7276
PluginEntry::Trusted(trusted_plugin) => {
7377
let game_project = editor.project.borrow();
74-
draw_trusted_plugin_row(body, trusted_plugin, game_project.as_ref());
78+
draw_trusted_plugin_row(
79+
body,
80+
trusted_plugin,
81+
game_project.as_ref(),
82+
&mut should_refresh_plugins,
83+
);
7584
}
7685
PluginEntry::Malformed(malformed) => {
7786
let filename = malformed
@@ -114,7 +123,8 @@ fn draw_editor_plugin_manager_content(editor: &mut EditorState, ui: &mut egui::U
114123
.on_hover_text("Game plugins are the list of plugins belonging to the current game. Only plugins that are also trusted are executed.");
115124

116125
let project = editor.project.borrow();
117-
if let Some(project) = &project.as_ref() {
126+
127+
if let Some(project) = project.as_ref() {
118128
ui.horizontal(|ui| {
119129
#[allow(clippy::collapsible_if)]
120130
if ui
@@ -131,37 +141,61 @@ fn draw_editor_plugin_manager_content(editor: &mut EditorState, ui: &mut egui::U
131141
}
132142

133143
if ui.button("Refresh game plugins list").clicked() {
134-
let trusted_plugins = editor
135-
.plugins
136-
.iter()
137-
.filter_map(|entry| match entry {
138-
PluginEntry::Trusted(trusted_plugin) => Some(trusted_plugin.clone()),
139-
PluginEntry::Malformed(_) => None,
140-
})
141-
.collect::<Vec<TrustedPlugin>>();
142-
project.refresh_plugin_list(&trusted_plugins);
144+
should_refresh_plugins = true;
143145
}
144146
});
145147

146-
let mut game_plugins = project.plugins.borrow_mut();
148+
{
149+
let game_plugins = project.plugins.borrow();
150+
if game_plugins.is_empty() {
151+
ui.label("Because you use no native plugins, all platforms are supported.");
152+
} else {
153+
let is_untrusted_plugin_in_list =
154+
game_plugins.iter().any(|p| p.trusted_plugin.is_none());
155+
if is_untrusted_plugin_in_list {
156+
ui.label("Because some plugins are not trusted, the list of supported platform is unknown");
157+
} else {
158+
let supported_platforms = game_plugins
159+
.iter()
160+
.filter_map(|p| p.trusted_plugin.as_ref())
161+
.map(|trusted_plugin| trusted_plugin.supported_platforms.clone())
162+
.reduce(|a, b| &a & &b);
163+
if let Some(supported_platforms) = supported_platforms {
164+
ui.label(format!(
165+
"Your game only supports the following platforms: {}",
166+
supported_platforms
167+
.iter()
168+
.map(|p| p.to_string())
169+
.collect::<Vec<_>>()
170+
.join(", ")
171+
)).on_hover_text("For a platform to be supported, it needs to be supported by all the plugins you use.");
172+
} else {
173+
ui.label("Something went wrong while checking the supported platform. Try refreshing the list of plugins.");
174+
}
175+
}
176+
}
177+
}
178+
179+
let mut plugin_to_trust: Option<PathBuf> = None;
147180

148181
draw_table_header_for_game_plugin(ui, "game", |body| {
149-
for plugin in game_plugins.iter_mut() {
182+
let game_plugins = project.plugins.borrow();
183+
for plugin in game_plugins.iter() {
150184
let row_height = 20.0;
151-
let filename = plugin
185+
let display_filename = plugin
152186
.path
153187
.file_name()
154188
.map(|p| p.display().to_string())
155189
.unwrap_or_else(|| get_end_of_path(&plugin.path));
156190

157-
match plugin.trusted_plugin.as_mut() {
191+
match plugin.trusted_plugin.as_ref() {
158192
Some(trusted_plugin) => {
159193
body.row(row_height, |mut row| {
160194
row.col(|ui| {
161195
ui.label(&trusted_plugin.name);
162196
});
163197
row.col(|ui| {
164-
ui.label(filename);
198+
ui.label(display_filename);
165199
});
166200
row.col(|ui| {
167201
ui.label("This plugin is trusted");
@@ -171,31 +205,54 @@ fn draw_editor_plugin_manager_content(editor: &mut EditorState, ui: &mut egui::U
171205
None => {
172206
body.row(row_height, |mut row| {
173207
row.col(|ui| {
174-
ui.label("⚠ Untrusted").on_hover_text("This plugin is not trusted and won't be executed. You can add it to the list of trusted plugin to allow its execution.");
208+
ui.label("⚠ Untrusted").on_hover_text("This plugin is not trusted and won't be executed. You can add it to the list of trusted plugins to allow its execution.");
175209
});
176210
row.col(|ui| {
177-
ui.label(filename);
211+
ui.label(display_filename);
178212
});
179213
row.col(|ui| {
180214
if ui.button("Trust").clicked() {
181-
// Trust it and refresh lists.
215+
plugin_to_trust = Some(plugin.path.clone());
216+
should_refresh_plugins = true;
182217
}
183218
});
184219
});
185220
}
186221
}
187222
}
188223
});
224+
225+
// We perform actions on the plugin list outside of drawing code to avoid mutating the list of plugins while iterating on it.
226+
// Trusting a plugin means copying it the trusted folder. We refresh the list afterwards
227+
if let Some(plugin_to_trust) = plugin_to_trust {
228+
let editor_plugin_folder = get_editor_plugins_path();
229+
let plugin_filename = plugin_to_trust
230+
.file_name()
231+
.map(|fname| fname.display().to_string())
232+
.unwrap_or_else(|| "plugin.vectaplugin".to_string());
233+
let _ = std::fs::create_dir_all(&editor_plugin_folder);
234+
let destination = editor_plugin_folder
235+
.join(get_available_filename_for_trusted_plugin(&plugin_filename));
236+
let _ = std::fs::copy(&plugin_to_trust, destination);
237+
}
189238
} else {
190239
ui.label("No project loaded")
191240
.on_hover_text("Load a project to see its plugins.");
192241
}
242+
243+
if should_refresh_plugins {
244+
editor.plugins = trustedplugin::load_plugins();
245+
if let Some(project) = project.as_ref() {
246+
project.refresh_plugin_list(&editor.get_trusted_plugins());
247+
}
248+
}
193249
}
194250

195251
fn draw_trusted_plugin_row(
196252
body: &mut TableBody,
197253
plugin: &mut TrustedPlugin,
198254
game_project: Option<&ProjectState>,
255+
should_refresh_plugins: &mut bool,
199256
) {
200257
let ui = body.ui_mut();
201258
let font_id = egui::TextStyle::Body.resolve(ui.style());
@@ -258,9 +315,19 @@ fn draw_trusted_plugin_row(
258315
p.trusted_plugin.as_ref().map(|plugin| plugin.hash) == Some(plugin.hash)
259316
});
260317
if is_added {
261-
ui.label("Added");
318+
if ui
319+
.button("Stop trusting")
320+
.on_hover_text(
321+
"Deletes the trusted plugin file from the editor plugin folder",
322+
)
323+
.clicked()
324+
{
325+
let _ = fs::remove_file(&plugin.path);
326+
*should_refresh_plugins = true;
327+
}
262328
} else if ui.button("Add to game").clicked() {
263329
game_project.add_plugin(plugin.clone());
330+
*should_refresh_plugins = true;
264331
}
265332
}
266333
});
@@ -337,8 +404,8 @@ fn draw_table_header_for_game_plugin(
337404
.resizable(true)
338405
.auto_shrink(true)
339406
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
340-
.column(Column::auto()) // Name
341-
.column(Column::auto().at_most(200.0).clip(true)) // Path
407+
.column(Column::auto().at_least(100.0)) // Name
408+
.column(Column::auto().at_least(200.0).clip(true)) // Filename
342409
.column(Column::auto()) // Actions
343410
.min_scrolled_height(0.0)
344411
.max_scroll_height(available_height);

editor/src/pluginsystem/trustedplugin.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,3 +221,32 @@ pub fn is_dynamic_library_file(path: &Path) -> bool {
221221
};
222222
DYNAMIC_LIB_SUFFIXES.contains(&ext.to_string_lossy().as_ref())
223223
}
224+
225+
pub fn is_trusted_plugin_name_is_available(name: &str) -> bool {
226+
let editor_plugin_path = get_editor_plugins_path();
227+
!editor_plugin_path.join(name).exists()
228+
}
229+
230+
/// Returns a name for a trusted plugin that does not exist yet, using the preferred name if possible.
231+
pub fn get_available_filename_for_trusted_plugin(preferred_name: &str) -> String {
232+
let (name, extension) = preferred_name
233+
.split_once('.')
234+
.unwrap_or((preferred_name, ""));
235+
let mut considered_index = 0;
236+
loop {
237+
let name = format!(
238+
"{}{}.{}",
239+
name,
240+
if considered_index == 0 {
241+
String::new()
242+
} else {
243+
format!("_{}", considered_index)
244+
},
245+
extension
246+
);
247+
if is_trusted_plugin_name_is_available(&name) {
248+
return name;
249+
}
250+
considered_index += 1;
251+
}
252+
}

0 commit comments

Comments
 (0)