Discuss Scratch
- Discussion Forums
- » Advanced Topics
- » Chloe’s first class list extension
- cookieclickerer33
-
Scratcher
1000+ posts
Chloe’s first class list extension
Note: all the images you see here are pictures of this extension running in scratch, no trickery!

I made a custom extension that adds first class lists, copy the code from the below area and then in turbowarp press “load from url” then “load from text” then paste it in, if you want no 1 frame delay for the blocks running you have to make the extension unsandboxxed.
Here’s the rundown of all the blocks you see here:

The
For example

And

The

The
And finally there’s
You can set these to variables just fine and they display properly, there’s also no trickery here, the system just works with no caviots
I hope you all like this!

I made a custom extension that adds first class lists, copy the code from the below area and then in turbowarp press “load from url” then “load from text” then paste it in, if you want no 1 frame delay for the blocks running you have to make the extension unsandboxxed.
class ListExtension {
constructor(runtime) {
this.runtime = runtime;
this.customList = [];
}
getInfo() {
return {
id: 'listextension',
name: 'First class lists',
color1: '#ff0000', // Red
color2: '#e83600', // Dark red orange
color3: '#9e0514', // reddish brown
blocks: [
{
opcode: 'listItems',
blockType: Scratch.BlockType.REPORTER,
text: 'list [ITEM1] and [ITEM2]',
arguments: {
ITEM1: {
type: Scratch.ArgumentType.STRING,
defaultValue: 'Item 1',
},
ITEM2: {
type: Scratch.ArgumentType.STRING,
defaultValue: 'Item 2',
},
},
},
{
opcode: 'getItemAtIndex',
blockType: Scratch.BlockType.REPORTER,
text: 'item [INDEX] of [LIST]',
arguments: {
INDEX: {
type: Scratch.ArgumentType.NUMBER,
defaultValue: 1,
},
LIST: {
type: Scratch.ArgumentType.BLOCK,
defaultValue: {
opcode: 'lists_create_with',
type: Scratch.ArgumentType.LIST,
},
},
},
},
{
opcode: 'indexInList',
blockType: Scratch.BlockType.REPORTER,
text: 'the [FIRSTORLAST] index of [ITEM] in [LIST]',
arguments: {
FIRSTORLAST: {
type: Scratch.ArgumentType.DROPDOWN,
menu: 'firstLast',
defaultValue: 'first',
},
ITEM: {
type: Scratch.ArgumentType.STRING,
defaultValue: 'Item',
},
LIST: {
type: Scratch.ArgumentType.BLOCK,
defaultValue: {
opcode: 'lists_create_with',
type: Scratch.ArgumentType.LIST,
},
},
},
},
{
opcode: 'indicesOf',
blockType: Scratch.BlockType.REPORTER,
text: 'indices of [ITEM] in [LIST]',
arguments: {
ITEM: {
type: Scratch.ArgumentType.STRING,
defaultValue: 'Item',
},
LIST: {
type: Scratch.ArgumentType.BLOCK,
defaultValue: {
opcode: 'lists_create_with',
type: Scratch.ArgumentType.LIST,
},
},
},
},
{
opcode: 'listAttribute',
blockType: Scratch.BlockType.REPORTER,
text: 'get [ATTRIBUTE] of [LIST]',
arguments: {
ATTRIBUTE: {
type: Scratch.ArgumentType.DROPDOWN,
menu: 'listAttributes',
defaultValue: 'length',
},
LIST: {
type: Scratch.ArgumentType.BLOCK,
defaultValue: {
opcode: 'lists_create_with',
type: Scratch.ArgumentType.LIST,
},
},
},
},
{
opcode: 'setItemAtIndex',
blockType: Scratch.BlockType.REPORTER,
text: 'set item [INDEX] to [ITEM] in [LIST]',
arguments: {
INDEX: {
type: Scratch.ArgumentType.NUMBER,
defaultValue: 1,
},
ITEM: {
type: Scratch.ArgumentType.STRING,
defaultValue: 'New Item',
},
LIST: {
type: Scratch.ArgumentType.BLOCK,
defaultValue: {
opcode: 'lists_create_with',
type: Scratch.ArgumentType.LIST,
},
},
},
},
],
menus: {
firstLast: {
acceptReporters: false,
items: ['first', 'last'],
},
listAttributes: {
acceptReporters: false,
items: ['length', 'reverse'], // Add more options as needed
},
},
};
}
listItems(args) {
return args.ITEM1 + '\n' + args.ITEM2;
}
getItemAtIndex(args) {
const index = args.INDEX;
const list = args.LIST;
if (list) {
// Split the list by newline characters.
const items = list.split('\n');
if (index >= 1 && index <= items.length) {
// Return the item at the specified index.
return items[index - 1];
}
}
// Return an empty string if there's no list or the index is out of bounds.
return '';
}
indexInList(args) {
const firstOrLast = args.FIRSTORLAST;
const item = args.ITEM;
const list = args.LIST;
items: ['first', 'last']
if (list) {
// Split the list by newline characters.
const items = list.split('\n');
const indices = [];
// Find all indices where the item appears.
for (let i = 0; i < items.length; i++) {
if (items[i] === item) {
indices.push(i + 1); // Adding 1 to match Scratch's indexing.
}
}
if (indices.length > 0) {
if (firstOrLast === 'first') {
return indices[0]; // Return the first index.
} else if (firstOrLast === 'last') {
return indices[indices.length - 1]; // Return the last index.
}
}
}
// Return an empty string if there's no list or the item is not found.
return '';
}
indicesOf(args) {
const item = args.ITEM;
const list = args.LIST;
if (list) {
// Split the list by newline characters.
const items = list.split('\n');
const indices = [];
// Find all indices where the item appears.
for (let i = 0; i < items.length; i++) {
if (items[i] === item) {
indices.push(i + 1); // Adding 1 to match Scratch's indexing.
}
}
return indices.join('\n'); // Join indices with newline characters.
}
// Return an empty string if there's no list.
return '';
}
listAttribute(args) {
const attribute = args.ATTRIBUTE;
const list = args.LIST;
if (list) {
// Split the list by newline characters.
const items = list.split('\n');
if (attribute === 'length') {
return items.length;
} else if (attribute === 'reverse') {
return items.reverse().join('\n'); // Reverse the list and join with newline characters.
}
}
// Return an empty string if there's no list or for unsupported attributes.
return '';
}
setItemAtIndex(args) {
const index = args.INDEX;
const newItem = args.ITEM;
const list = args.LIST;
if (list) {
// Split the list by newline characters.
const items = list.split('\n');
if (index >= 1 && index <= items.length) {
// Replace the item at the specified index with the new item.
items[index - 1] = newItem;
return items.join('\n'); // Join the modified list with newline characters.
}
}
// Return the original list if there's no list or the index is out of bounds.
return list;
}
}
Scratch.extensions.register(new ListExtension());
The
(list [a] and [b] :: list)Block returns a first class list with the items specified, if you want more than 2 items do
(list [a] and (list [b] and [c] :: list) :: list)This returns a list containing a,b,c
(item (1) of (\ \ \:: #590101):: list)Returns the item number specified of the first class list of the second input
(The [first v] index of [thing] in (\ \ \:: #590101)::list)Returns the item number of the first or last item that is the same as the provided string
(The [last v] index of [thing] in (\ \ \:: #590101)::list)
For example

And

The
(Indices of [thing] in (\ \ \:: #590101):: list)Block returns a list of all the item numbers that match the provided string, as an example:

The
(Set item (1) to [thing] in (\ \ \:: #590101):: list)Block works just like
replace item ( v) of [list v] with [thing]But with first class lists
And finally there’s
(Get [length v] of (\ \ \:: #590101) :: list)The “length” option returns the amount of items in the list, and the “reverse” option returns the list with the item numbers of each item swapped (so the first item becomes the last, the last becomes the first etc.)
(Get [reverse v] of (\ \ \:: #590101)::list)
You can set these to variables just fine and they display properly, there’s also no trickery here, the system just works with no caviots
I hope you all like this!
Last edited by cookieclickerer33 (Nov. 1, 2023 11:44:01)
- mybearworld
-
Scratcher
1000+ posts
Chloe’s first class list extension
Interesting! How can you use that extension?
- cookieclickerer33
-
Scratcher
1000+ posts
Chloe’s first class list extension
Interesting! How can you use that extension?https://scratch.mit.edu/projects/916486596/editor/ Has a comment that has the text code
Show me what you make I’m very intrigued to see what people can do with this
It’s fully open source so it should be fine, just import it to turbowarp
My dream is to get this into the extension gallery if I expand it more
Last edited by cookieclickerer33 (Oct. 31, 2023 11:45:20)
- cookieclickerer33
-
Scratcher
1000+ posts
Chloe’s first class list extension
Just a quick note: text strings count as a list with 1 item
- cookieclickerer33
-
Scratcher
1000+ posts
Chloe’s first class list extension
Oops! Idk how I missed that but the comment was missing the C at the beginning of the work “class” so the extension wouldn’t load!!
It should work now that I updated the project
It should work now that I updated the project
- Clippy-Cat
-
Scratcher
66 posts
Chloe’s first class list extension
https://scratch.mit.edu/projects/916486596/editor/ Has a comment that has the text codeI believe that @mybearworld was asking for a link to a website such as GitHub. However, since you posted it as a comment within a Scratch Project, here's the code contained in there:
Show me what you make I’m very intrigued to see what people can do with this
It’s fully open source so it should be fine, just import it to turbowarp
My dream is to get this into the extension gallery if I expand it more
class ListExtension { constructor(runtime) { this.runtime = runtime; this.customList = []; // just to be safe this is here } getInfo() { return { id: 'listextension', name: 'First class lists', color1: '#ff0000', // Red color2: '#e83600', // Dark red orange color3: '#9e0514', // reddish brown blocks: [ { opcode: 'listItems', blockType: Scratch.BlockType.REPORTER, text: 'list [ITEM1] and [ITEM2]', arguments: { ITEM1: { type: Scratch.ArgumentType.STRING, defaultValue: 'Item 1', }, ITEM2: { type: Scratch.ArgumentType.STRING, defaultValue: 'Item 2', }, }, }, { opcode: 'getItemAtIndex', blockType: Scratch.BlockType.REPORTER, text: 'item [INDEX] of [LIST]', arguments: { INDEX: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1, }, LIST: { type: Scratch.ArgumentType.BLOCK, defaultValue: { opcode: 'lists_create_with', type: Scratch.ArgumentType.LIST, }, }, }, }, { opcode: 'indexInList', blockType: Scratch.BlockType.REPORTER, text: 'the [FIRSTORLAST] index of [ITEM] in [LIST]', arguments: { FIRSTORLAST: { type: Scratch.ArgumentType.DROPDOWN, menu: 'firstLast', defaultValue: 'first', }, ITEM: { type: Scratch.ArgumentType.STRING, defaultValue: 'Item', }, LIST: { type: Scratch.ArgumentType.BLOCK, defaultValue: { opcode: 'lists_create_with', type: Scratch.ArgumentType.LIST, }, }, }, }, { opcode: 'indicesOf', blockType: Scratch.BlockType.REPORTER, text: 'indices of [ITEM] in [LIST]', arguments: { ITEM: { type: Scratch.ArgumentType.STRING, defaultValue: 'Item', }, LIST: { type: Scratch.ArgumentType.BLOCK, defaultValue: { opcode: 'lists_create_with', type: Scratch.ArgumentType.LIST, }, }, }, }, { opcode: 'listAttribute', blockType: Scratch.BlockType.REPORTER, text: 'get [ATTRIBUTE] of [LIST]', arguments: { ATTRIBUTE: { type: Scratch.ArgumentType.DROPDOWN, menu: 'listAttributes', defaultValue: 'length', }, LIST: { type: Scratch.ArgumentType.BLOCK, defaultValue: { opcode: 'lists_create_with', type: Scratch.ArgumentType.LIST, }, }, }, }, { opcode: 'setItemAtIndex', blockType: Scratch.BlockType.REPORTER, text: 'set item [INDEX] to [ITEM] in [LIST]', arguments: { INDEX: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1, }, ITEM: { type: Scratch.ArgumentType.STRING, defaultValue: 'New Item', }, LIST: { type: Scratch.ArgumentType.BLOCK, defaultValue: { opcode: 'lists_create_with', type: Scratch.ArgumentType.LIST, }, }, }, }, ], menus: { firstLast: { acceptReporters: false, items: ['first', 'last'], }, listAttributes: { acceptReporters: false, items: ['length', 'reverse'], // Add more options as needed }, }, }; } listItems(args) { return args.ITEM1 + '\n' + args.ITEM2; } getItemAtIndex(args) { const index = args.INDEX; const list = args.LIST; if (list) { // Split the list by newline characters. const items = list.split('\n'); if (index >= 1 && index <= items.length) { // Return the item at the specified index. return items[index - 1]; } } // Return an empty string if there's no list or the index is out of bounds. return ''; } indexInList(args) { const firstOrLast = args.FIRSTORLAST; const item = args.ITEM; const list = args.LIST; items: ['first', 'last'] if (list) { // Split the list by newline characters. const items = list.split('\n'); const indices = []; // Find all indices where the item appears. for (let i = 0; i < items.length; i++) { if (items[i] === item) { indices.push(i + 1); // Adding 1 to match Scratch's indexing. } } if (indices.length > 0) { if (firstOrLast === 'first') { return indices[0]; // Return the first index. } else if (firstOrLast === 'last') { return indices[indices.length - 1]; // Return the last index. } } } // Return an empty string if there's no list or the item is not found. return ''; } indicesOf(args) { const item = args.ITEM; const list = args.LIST; if (list) { // Split the list by newline characters. const items = list.split('\n'); const indices = []; // Find all indices where the item appears. for (let i = 0; i < items.length; i++) { if (items[i] === item) { indices.push(i + 1); // Adding 1 to match Scratch's indexing. } } return indices.join('\n'); // Join indices with newline characters. } // Return an empty string if there's no list. return ''; } listAttribute(args) { const attribute = args.ATTRIBUTE; const list = args.LIST; if (list) { // Split the list by newline characters. const items = list.split('\n'); if (attribute === 'length') { return items.length; } else if (attribute === 'reverse') { return items.reverse().join('\n'); // Reverse the list and join with newline characters. } } // Return an empty string if there's no list or for unsupported attributes. return ''; } setItemAtIndex(args) { const index = args.INDEX; const newItem = args.ITEM; const list = args.LIST; if (list) { // Split the list by newline characters. const items = list.split('\n'); if (index >= 1 && index <= items.length) { // Replace the item at the specified index with the new item. items[index - 1] = newItem; return items.join('\n'); // Join the modified list with newline characters. } } // Return the original list if there's no list or the index is out of bounds. return list; } } Scratch.extensions.register(new ListExtension());
- cookieclickerer33
-
Scratcher
1000+ posts
Chloe’s first class list extension
Oh my, thank you so so much, clippy!!! I didn’t know the code tab didn’t show the full thing and made it into a scroll bar!!
And yes that is what it’s referring to I should clarify that
And yes that is what it’s referring to I should clarify that
- cookieclickerer33
-
Scratcher
1000+ posts
Chloe’s first class list extension
I edited the original post to have the code in the code bb code tag instead of in a project comment. It’s hard to select the text in project comments anyway so this is better. I would have done this if I knew the code tag made a scroll bar and didn’t just plaster tons of text into the post
- Jackfriendjoe2
-
Scratcher
14 posts
Chloe’s first class list extension
Note: all the images you see here are pictures of this extension running in scratch, no trickery!Cool extension, awesome when loaded into turbowarp and messed around with, sadly i dont know javascript but making these seems fun!
I made a custom extension that adds first class lists, copy the code from the below area and then in turbowarp press “load from url” then “load from text” then paste it in, if you want no 1 frame delay for the blocks running you have to make the extension unsandboxxed.Here’s the rundown of all the blocks you see here:class ListExtension {
constructor(runtime) {
this.runtime = runtime;
this.customList = [];
}
getInfo() {
return {
id: 'listextension',
name: 'First class lists',
color1: '#ff0000', // Red
color2: '#e83600', // Dark red orange
color3: '#9e0514', // reddish brown
blocks: [
{
opcode: 'listItems',
blockType: Scratch.BlockType.REPORTER,
text: 'list [ITEM1] and [ITEM2]',
arguments: {
ITEM1: {
type: Scratch.ArgumentType.STRING,
defaultValue: 'Item 1',
},
ITEM2: {
type: Scratch.ArgumentType.STRING,
defaultValue: 'Item 2',
},
},
},
{
opcode: 'getItemAtIndex',
blockType: Scratch.BlockType.REPORTER,
text: 'item [INDEX] of [LIST]',
arguments: {
INDEX: {
type: Scratch.ArgumentType.NUMBER,
defaultValue: 1,
},
LIST: {
type: Scratch.ArgumentType.BLOCK,
defaultValue: {
opcode: 'lists_create_with',
type: Scratch.ArgumentType.LIST,
},
},
},
},
{
opcode: 'indexInList',
blockType: Scratch.BlockType.REPORTER,
text: 'the [FIRSTORLAST] index of [ITEM] in [LIST]',
arguments: {
FIRSTORLAST: {
type: Scratch.ArgumentType.DROPDOWN,
menu: 'firstLast',
defaultValue: 'first',
},
ITEM: {
type: Scratch.ArgumentType.STRING,
defaultValue: 'Item',
},
LIST: {
type: Scratch.ArgumentType.BLOCK,
defaultValue: {
opcode: 'lists_create_with',
type: Scratch.ArgumentType.LIST,
},
},
},
},
{
opcode: 'indicesOf',
blockType: Scratch.BlockType.REPORTER,
text: 'indices of [ITEM] in [LIST]',
arguments: {
ITEM: {
type: Scratch.ArgumentType.STRING,
defaultValue: 'Item',
},
LIST: {
type: Scratch.ArgumentType.BLOCK,
defaultValue: {
opcode: 'lists_create_with',
type: Scratch.ArgumentType.LIST,
},
},
},
},
{
opcode: 'listAttribute',
blockType: Scratch.BlockType.REPORTER,
text: 'get [ATTRIBUTE] of [LIST]',
arguments: {
ATTRIBUTE: {
type: Scratch.ArgumentType.DROPDOWN,
menu: 'listAttributes',
defaultValue: 'length',
},
LIST: {
type: Scratch.ArgumentType.BLOCK,
defaultValue: {
opcode: 'lists_create_with',
type: Scratch.ArgumentType.LIST,
},
},
},
},
{
opcode: 'setItemAtIndex',
blockType: Scratch.BlockType.REPORTER,
text: 'set item [INDEX] to [ITEM] in [LIST]',
arguments: {
INDEX: {
type: Scratch.ArgumentType.NUMBER,
defaultValue: 1,
},
ITEM: {
type: Scratch.ArgumentType.STRING,
defaultValue: 'New Item',
},
LIST: {
type: Scratch.ArgumentType.BLOCK,
defaultValue: {
opcode: 'lists_create_with',
type: Scratch.ArgumentType.LIST,
},
},
},
},
],
menus: {
firstLast: {
acceptReporters: false,
items: ['first', 'last'],
},
listAttributes: {
acceptReporters: false,
items: ['length', 'reverse'], // Add more options as needed
},
},
};
}
listItems(args) {
return args.ITEM1 + '\n' + args.ITEM2;
}
getItemAtIndex(args) {
const index = args.INDEX;
const list = args.LIST;
if (list) {
// Split the list by newline characters.
const items = list.split('\n');
if (index >= 1 && index <= items.length) {
// Return the item at the specified index.
return items[index - 1];
}
}
// Return an empty string if there's no list or the index is out of bounds.
return '';
}
indexInList(args) {
const firstOrLast = args.FIRSTORLAST;
const item = args.ITEM;
const list = args.LIST;
items: ['first', 'last']
if (list) {
// Split the list by newline characters.
const items = list.split('\n');
const indices = [];
// Find all indices where the item appears.
for (let i = 0; i < items.length; i++) {
if (items[i] === item) {
indices.push(i + 1); // Adding 1 to match Scratch's indexing.
}
}
if (indices.length > 0) {
if (firstOrLast === 'first') {
return indices[0]; // Return the first index.
} else if (firstOrLast === 'last') {
return indices[indices.length - 1]; // Return the last index.
}
}
}
// Return an empty string if there's no list or the item is not found.
return '';
}
indicesOf(args) {
const item = args.ITEM;
const list = args.LIST;
if (list) {
// Split the list by newline characters.
const items = list.split('\n');
const indices = [];
// Find all indices where the item appears.
for (let i = 0; i < items.length; i++) {
if (items[i] === item) {
indices.push(i + 1); // Adding 1 to match Scratch's indexing.
}
}
return indices.join('\n'); // Join indices with newline characters.
}
// Return an empty string if there's no list.
return '';
}
listAttribute(args) {
const attribute = args.ATTRIBUTE;
const list = args.LIST;
if (list) {
// Split the list by newline characters.
const items = list.split('\n');
if (attribute === 'length') {
return items.length;
} else if (attribute === 'reverse') {
return items.reverse().join('\n'); // Reverse the list and join with newline characters.
}
}
// Return an empty string if there's no list or for unsupported attributes.
return '';
}
setItemAtIndex(args) {
const index = args.INDEX;
const newItem = args.ITEM;
const list = args.LIST;
if (list) {
// Split the list by newline characters.
const items = list.split('\n');
if (index >= 1 && index <= items.length) {
// Replace the item at the specified index with the new item.
items[index - 1] = newItem;
return items.join('\n'); // Join the modified list with newline characters.
}
}
// Return the original list if there's no list or the index is out of bounds.
return list;
}
}
Scratch.extensions.register(new ListExtension());
The(list [a] and [b] :: list)Block returns a first class list with the items specified, if you want more than 2 items do(list [a] and (list [b] and [c] :: list) :: list)This returns a list containing a,b,c(item (1) of (\ \ \:: #590101):: list)Returns the item number specified of the first class list of the second input(The [first v] index of [thing] in (\ \ \:: #590101)::list)Returns the item number of the first or last item that is the same as the provided string
(The [last v] index of [thing] in (\ \ \:: #590101)::list)
For example
And
The(Indices of [thing] in (\ \ \:: #590101):: list)Block returns a list of all the item numbers that match the provided string, as an example:
The(Set item (1) to [thing] in (\ \ \:: #590101):: list)Block works just likereplace item ( v) of [list v] with [thing]But with first class lists
And finally there’s(Get [length v] of (\ \ \:: #590101) :: list)The “length” option returns the amount of items in the list, and the “reverse” option returns the list with the item numbers of each item swapped (so the first item becomes the last, the last becomes the first etc.)
(Get [reverse v] of (\ \ \:: #590101)::list)
You can set these to variables just fine and they display properly, there’s also no trickery here, the system just works with no caviots
I hope you all like this!
- mybearworld
-
Scratcher
1000+ posts
Chloe’s first class list extension
(#6)I was just asking for a way to try it out, and got it! This is a pretty cool extension. I have a suggestion - maybe you could use a proper input field? With addons, this currently looks like this:
I believe that @mybearworld was asking for a link to a website such as GitHub. However, since you posted it as a comment within a Scratch Project, here's the code contained in there:

- god286
-
Scratcher
1000+ posts
Chloe’s first class list extension
I edited the original post to have the code in the code bb code tag instead of in a project comment. It’s hard to select the text in project comments anyway so this is better. I would have done this if I knew the code tag made a scroll bar and didn’t just plaster tons of text into the postYou could even minify the code into one line using https://jsminify.org/

- DifferentDance8
-
Scratcher
1000+ posts
Chloe’s first class list extension
The extension doesn't work when doing what Clippy-Cat said, and looking in the console didn't show me any errors.
- cookieclickerer33
-
Scratcher
1000+ posts
Chloe’s first class list extension
Well you aren’t able to type inside the list input slots, otherwise it would be very strange and it wouldn’t be clear what’s a list and what isn’t(#6)I was just asking for a way to try it out, and got it! This is a pretty cool extension. I have a suggestion - maybe you could use a proper input field? With addons, this currently looks like this:
I believe that @mybearworld was asking for a link to a website such as GitHub. However, since you posted it as a comment within a Scratch Project, here's the code contained in there:
To put it simply that input slot is the Boolean input slot but shaped like a reporter (or if you know blockly, it’s actually a circular drop-down slot with no drop-down added into it)
This makes it clear what needs to go in what slots like how in snap there’s a list input.
- cookieclickerer33
-
Scratcher
1000+ posts
Chloe’s first class list extension
The extension doesn't work when doing what Clippy-Cat said, and looking in the console didn't show me any errors.You can only import 1 url extension at a time in turbowarp (from what I can tell)
If you already have a url extension imported (or tried an import and it failed) then it won’t let you import any others
Try again using the code of the fourm post btw if you used the comment in the project, it worked fine for me
Last edited by cookieclickerer33 (Nov. 2, 2023 11:28:37)
- Discussion Forums
- » Advanced Topics
-
» Chloe’s first class list extension





