java Tile based game ArrayIndexOutOfBoundsException error - java

I have ArrayIndexOutOfBoundsException error in getTile function which is in the World class, my problem is that i cannot floor the background with background image, I cannot add other tiles i got empty window and these errors.
package com.game.Tiles;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
public class Tile {
// static stuff
public static Tile[] tiles = new Tile[512];
public static Tile background = new Background(0);
public static Tile target = new Target(1);
// class
public static final int TILE_WIDTH = 50, TILE_HEIGHT = 50;
protected BufferedImage texture;
protected final int id;
public Tile(BufferedImage texture, int id) {
super();
this.texture = texture;
this.id = id;
tiles[id] = this; // class ı tile a ekliyoruz yukardan gelen id ile
}
public void tick(){
}
public void render(Graphics graphic, int positionX, int positionY){
graphic.drawImage(texture, positionX, positionY, TILE_WIDTH, TILE_HEIGHT, null);
}
public int getId() {
return this.id;
}
public boolean isPasseble()
{
return false;
}
}
package com.game.world;
import java.awt.Graphics;
import com.game.Tiles.Tile;
public class World {
private int height, width;
private int[][] tiles;
public World(){
this.loadWorld();
}
public void tick(){
}
public void render(Graphics graphic){
for (int y = 0; y < this.height; y++) {
for(int x = 0; y < this.width; x++){
this.getTile(x, y).render(graphic, x * Tile.TILE_HEIGHT, y * Tile.TILE_WIDTH);
}
}
}
public Tile getTile(int x, int y){
Tile t = Tile.tiles[this.tiles[x][y]];
if(t == null)
return Tile.background;
return t;
}
private void loadWorld(){
this.width = 30;
this.height = 30;
this.tiles = new int[this.width][this.height];
for (int x = 0; x < this.width; x++) {
for (int y = 0; y < this.height; y++) {
this.tiles[x][y] = 0;
}
}
}
}
Errors:
Exception in thread "Thread-0"
java.lang.ArrayIndexOutOfBoundsException: 30
at com.game.world.World.getTile(World.java:28)
at com.game.world.World.render(World.java:23)
at com.game.States.GameState.render(GameState.java:29)
at com.game.Launcher.Game.render(Game.java:73)
at com.game.Launcher.Game.run(Game.java:98)
at java.lang.Thread.run(Thread.java:745)

This line should
for(int x = 0; y < this.width; x++){
should be
for(int x = 0; x < this.width; x++){

Related

Getting a NullPointerException for some reason

I'm following a tutorial from theChernoProject, however in episode 22 i keep getting a nullPointerException whem trying to render the pixels array from the sprite ?
Error:
Exception in thread "Game" java.lang.ExceptionInInitializerError
at com.santec.game.learning.thecherno.graphics.Screen.render(Screen.java:40)
at com.santec.game.learning.thecherno.Game.render(Game.java:109)
at com.santec.game.learning.thecherno.Game.run(Game.java:78)
at java.lang.Thread.run(Unknown Source)
Caused by: java.lang.NullPointerException
at com.santec.game.learning.thecherno.graphics.Sprite.load(Sprite.java:23)
at com.santec.game.learning.thecherno.graphics.Sprite.<init>(Sprite.java:17)
at com.santec.game.learning.thecherno.graphics.Sprite.<clinit>(Sprite.java:10)
... 4 more
Classes:
Game.java :
package com.santec.game.learning.thecherno;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import javax.swing.JFrame;
import com.santec.game.learning.thecherno.graphics.Screen;
import com.santec.game.learning.thecherno.input.Keyboard;
public class Game extends Canvas implements Runnable {
private static final long serialVersionUID = 1L;
public static int width = 300;
public static int height = width / 16 * 9;
public static int scale = 3;
public static String title = "Rain";
private Thread thread;
private JFrame frame;
private boolean running = false;
private Screen screen;
private Keyboard key;
private BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
private int[] pixels = ((DataBufferInt)image.getRaster().getDataBuffer()).getData();
public Game() {
Dimension size = new Dimension(width * scale, height * scale);
setPreferredSize(size);
screen = new Screen(width, height);
frame = new JFrame();
key = new Keyboard();
addKeyListener(key);
}
public synchronized void start() {
running = true;
thread = new Thread(this, "Game");
thread.start();
}
public synchronized void stop() {
running = false;
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void run() {
long lastTime = System.nanoTime();
long timer = System.currentTimeMillis();
final double ns = 1000000000.0 / 60.0;
double delta = 0;
int frames = 0;
int updates = 0;
while(running) {
long now = System.nanoTime();
delta += (now - lastTime) / ns;
lastTime = now;
while(delta >= 1) {
update();
updates++;
delta--;
}
render();
frames++;
if(System.currentTimeMillis() - timer > 1000) {
timer += 1000;
frame.setTitle(title + ": " + updates + " ups | " + frames + " fps");
updates = 0;
frames = 0;
}
}
stop();
}
int x = 0, y = 0;
public void update() {
key.update();
if(key.up) y--;
if(key.down) y++;
if(key.right) x++;
if(key.left) x--;
}
public void render() {
BufferStrategy bs = getBufferStrategy();
if(bs == null) {
createBufferStrategy(3);
return;
}
screen.clear();
screen.render(x, y);
for(int i = 0; i < pixels.length; i++) {
pixels[i] = screen.pixels[i];
}
Graphics g = bs.getDrawGraphics();
{
g.setColor(Color.cyan);
g.drawImage(image, 0, 0, getWidth(), getHeight(), null);
}
g.dispose();
bs.show();
}
public static void main(String[] args) {
Game game = new Game();
game.frame.setResizable(false);
game.frame.setTitle(game.title);
game.frame.add(game);
game.frame.pack();
game.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
game.frame.setLocationRelativeTo(null);
game.frame.setVisible(true);
game.start();
}
}
graphics/SpriteSheet.java :
package com.santec.game.learning.thecherno.graphics;
import java.awt.image.BufferedImage;
import java.io.IOException;
import javax.imageio.ImageIO;
public class SpriteSheet {
private String path;
public final int SIZE;
public int[] pixels;
// Spritesheets
public static SpriteSheet terrain = new SpriteSheet("/textures/terrain.png", 96);
public SpriteSheet(String path, int size) {
this.path = path;
SIZE = size;
pixels = new int[SIZE * SIZE];
load();
}
private void load() {
try {
BufferedImage image = ImageIO.read(SpriteSheet.class.getResource(path));
int w = image.getWidth();
int h = image.getHeight();
image.getRGB(0, 0, w, h, pixels, 0, w);
} catch (IOException e) {
e.printStackTrace();
}
}
}
graphics/Screen.java :
package com.santec.game.learning.thecherno.graphics;
import java.util.Random;
public class Screen {
private int width, height;
public int[] pixels;
public final int MAP_SIZE = 8;
public final int MAP_SIZE_MASK = MAP_SIZE - 1;
public int[] tiles = new int[MAP_SIZE * MAP_SIZE];
private Random random = new Random();
public Screen(int width, int height) {
this.width = width;
this.height = height;
pixels = new int[width * height];
for(int i = 0; i < MAP_SIZE * MAP_SIZE; i++) {
tiles[i] = random.nextInt(0xffffff);
tiles[0] = 0;
}
}
public void clear() {
for(int i = 0; i < pixels.length; i++) {
pixels[i] = 0;
}
}
public void render(int xOffset, int yOffset) {
for(int y = 0; y < height; y++) {
int yy = y + yOffset;
if(yy >= height || yy < 0) break;
for(int x = 0; x < width; x++) {
int xx = x + xOffset;
if(xx >= width || xx < 0) break;
int tileIndex = ((xx >> 4) & MAP_SIZE_MASK) + ((yy >> 4) & MAP_SIZE_MASK) * 8;
pixels[x + y * width] = Sprite.grass.pixels[(xx & 15) + (yy & 15) * Sprite.grass.SIZE];
}
}
}
}
graphics/Sprite.java :
package com.santec.game.learning.thecherno.graphics;
public class Sprite {
public final int SIZE;
private int x, y;
public int[] pixels;
private SpriteSheet sheet;
public static Sprite grass = new Sprite(16, 0, 1, SpriteSheet.terrain);
public Sprite(int size, int x, int y, SpriteSheet sheet) {
SIZE = size;
this.x = x * size;
this.y = y * size;
this.sheet = sheet;
load();
}
private void load() {
for(int y = 0; y < SIZE; y++) {
for(int x = 0; x < SIZE; x++) {
pixels[x + y * SIZE] = sheet.pixels[(x + this.x) + (y + this.y) * sheet.SIZE];
}
}
}
}
You never initialize the pixels array.
add
this.pixels = new int[SIZE*SIZE]; // assuming that's the desired length or that array
to the start of your load method.

Using Rectangle to Check Intersection of Images

So, I am instructed to: "Modify the Sprite class by implementing the "overlaps" function to return true if any portion of the Sprite passed in as an argument overlaps the current Sprite. You will need to use the x and y coordinates as well as the size of the Sprites."
I was advised to use Rectangles to make this work and check for an intersection. However, I'm not sure if what I have is the correct way to do this: (It's implemented in the boolean overlaps method)
As always, thanks so much for your time.
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
import javax.imageio.ImageIO;
import java.io.File;
import java.io.IOException;
import java.util.Random;
abstract class Sprite
{
private int x;
private int y;
private int size;
private int w;
private int h;
private int xSlope;
private int ySlope;
private Image image;
private static Random rand;
public Sprite(int xIn, int yIn, int width, int height, String imagePath, int imageSize) {
if (rand == null) {
rand = new Random();
}
size = imageSize;
setImage(imagePath);
x = xIn;
y = yIn;
w = width;
h = height;
xSlope = rand.nextInt(11) - 5;
ySlope = rand.nextInt(11) - 5;
}
public int getX() { return x; }
public int getY() { return y; }
public int getSize() { return size; }
public void setSize(int s) { size = s; }
public void setX(int xIn) { x = xIn; }
public void setY(int yIn) { y = yIn; }
public void setImage(String imagePath) {
try {
image = ImageIO.read(new File(imagePath));
} catch (IOException ioe) {
System.out.println("Unable to load image file.");
}
}
public Image getImage() { return image; }
public boolean overlaps(Sprite s) {
int locX = this.getX();
int locY = this.getY();
int overX = s.getX();
int overY = s.getY();
Rectangle R1 = new Rectangle(locX, locY, this.w, this.h);
Rectangle R2 = new Rectangle(overX, overY, s.w, s.h );
boolean intersects = R1.intersects(R2);
return intersects;
}
public void update(Graphics g) {
g.drawImage(getImage(), x, y, getSize(), getSize(), null);
}
public void move() {
// Move the Sprite
int x = getX() + xSlope;
int y = getY() + ySlope;
if (x < 0) x = w;
if (x > w) x = 0;
if (y < 0) y = h;
if (y > h) y = 0;
setX(x);
setY(y);
}
}

Java bit shifting and colours

hi i'm quite new at java and i am having a problem where my colors were monochromatic so i changed something and then now all i have is a black screen these are my graphics classes
package game.game.gfx;
public class Screen
{
public static final int MAP_WIDTH =64;
public static final int MAP_WIDTH_MASK = MAP_WIDTH -1;
public int[]pixels;
public int xOffset =0;
public int yOffset =0;
public int width;
public int height;
public SpriteSheet sheet;
public Screen(int width,int height,SpriteSheet sheet)
{
this.width = width;
this.height = height;
this.sheet = sheet;
pixels = new int[width*height];
}
public void render(int xPos,int yPos,int tile,int colour){
xPos -=xOffset;
yPos -=yOffset;
int xTile = tile % 32;
int yTile = tile / 32;
int tileOffset = (xTile<<3)+(yTile<<3)*sheet.width;
for(int y=0;y<8;y++){
if(y+yPos < 0 || y + yPos >=height) continue;
int ySheet=y;
for(int x=0;x<8;x++){
if(x+xPos <0||x+xPos >=width) continue;
int xSheet=x;
int col = (colour >> (sheet.pixels[xSheet + ySheet*sheet.width+tileOffset]*8))& 255;
if(col<255)pixels[(x+xPos)+(y+yPos)*width]=col;
}
}
}
}
========
SpriteSheet
package game.game.gfx;
import java.awt.image.BufferedImage;
import java.io.IOException;
import javax.imageio.ImageIO;
public class SpriteSheet
{
public String path;
public int width;
public int height;
public int[]pixels;
public SpriteSheet(String path)
{
BufferedImage image = null;
try {
image = ImageIO.read(SpriteSheet.class.getResourceAsStream(path));
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if (image == null)
{
return;
}
this.path = path;
this.width = image.getWidth();
this.height = image.getHeight();
pixels = image.getRGB(0, 0, width, height, null, 0, width);
for (int i =0; i < pixels.length;i++)
{
pixels[i] =(pixels[i]&0xff)/64;
}
for (int i =0;i<8;i++)
{
System.out.println(pixels[i]);
}
}
}
=====
colours
package game.game.gfx;
public class Colours {
public static int get(int colour1,int colour2,int colour3,int colour4){
return (get(colour4)<<24)+(get(colour3)<<16+(get(colour2)<<8)+get(colour1));
}
private static int get(int colour) {
if(colour < 0)return 255;
int r =colour/ 100 % 10;
int g =colour/ 10 % 10;
int b =colour % 10;
return r*36+g*6+b;
}
}

The following code will not render

I believe the code will speak for itself, but in general the point of the code is the have a Map class that will take in an array of BufferedImages, x values, and y values, to compose a map of many layers (first layer being the BufferedImage array at 0, starting at the x value at 0 and the y value at 0, and so on). The main job of the map class, is to take each pixel of each image and convert them to Block Objects, which are just simply rectangles with a color (Includes a BufferedImage, because after it works, I will replace the color with the Image. Also includes an integer to specify which layer (1 being index 0) its allowed on with 0 meaning it can exist among all layers). In the end, when I call Render() on a Map object, the map object should do all the work in rendering the blocks into the correct positions. The largest problem with all of this is that I get no sytax or compiler errors, so my logic is what is messed up and I can not figure it out!
Thanks in advance, and if the question is confusing please tell me!
The Map Class:
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
public class Map {
private int width;
private int height;
public int getWidth() { return width; }
public int getHeight() { return height; }
private int xPos;
private int yPos;
public int getX(int i)
{
return xPos;
}
public int getY(int i)
{
return yPos;
}
public void setPosition(int x, int y) { xPos = x; yPos = y; }
private int[] xStarts;
private int[] yStarts;
private ArrayList<BufferedImage> layersList = new ArrayList<BufferedImage>();
public void addLayer(BufferedImage image) { layersList.add(image); }
public void setLayer(int i, BufferedImage image) { layersList.set(i, image); }
private Block[][][] blocksArray;
private boolean beenInitialized = false;
public Map(BufferedImage[] images, int[] x, int[] y){
for (BufferedImage image : images){
layersList.add(image);
xStarts = x;
yStarts = y;
}
}
public void initialize(){
int widthMax = 0;
int heightMax = 0;
for (BufferedImage image : layersList){
if (image.getHeight() > heightMax) { heightMax = image.getHeight(); }
if (image.getWidth() > widthMax) { widthMax = image.getWidth(); }
}
width = widthMax;
height = heightMax;
blocksArray = new Block[layersList.size()][width][height];
for (int i = 0; i < layersList.size(); i++){
int currentLayer = i;
for (int y = 0; y < layersList.get(i).getHeight(); y++){
for (int x = 0; x < layersList.get(i).getWidth(); x++){
int colorCode = layersList.get(i).getRGB(x, y);
boolean error = true;
Block b = null;
for (int c = 0; c < Block.BLOCKS.size(); c++){
if (Block.BLOCKS.get(i).getColorCode() == colorCode && (Block.BLOCKS.get(i).getLayerCode() == currentLayer || Block.BLOCKS.get(i).getLayerCode() == 0)){
b = Block.BLOCKS.get(c);
error = false;
}
}
if (!error){
blocksArray[currentLayer][x][y] = b;
} else {
Block bb = new Block(false, colorCode);
bb.initialize();
blocksArray[currentLayer][x][y] = bb;
}
}
}
}
beenInitialized = true;
}
public void render(Graphics2D g2d){
if (beenInitialized){
for (int i = 0; i < layersList.size(); i++){
for (int y = yStarts[i]; y < layersList.get(i).getHeight() + yStarts[i]; y += Block.SIZE){
int currentY = 0;
for (int x = xStarts[i]; x < layersList.get(i).getWidth() + xStarts[i]; x += Block.SIZE){
int currentX = 0;
blocksArray[i][currentX][currentY].setPosition(x, y);
blocksArray[i][currentX][currentY].render(g2d);
currentX ++;
}
currentY++;
}
}
}
}
public void updatePosition(int x, int y){
xPos += x;
yPos += y;
}
}
The Block Class:
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
public class Block {
public static final int SIZE = 32;
public static final boolean DEBUG = true;
public static ArrayList<Block> BLOCKS = new ArrayList<Block>();
private Color debugColor;
public Color getColor() { return debugColor; }
public void setColor(Color color) { debugColor = color; }
private BufferedImage blockIcon;
public BufferedImage getIcon() { return blockIcon; }
public void setIcon(BufferedImage icon) { blockIcon = icon; }
private int xPos;
private int yPos;
public int getX() { return xPos; }
public int getY() { return yPos; }
public void setPosition(int x, int y) { xPos = x; yPos = y; }
private Rectangle blockShape;
public Rectangle getShape() { return blockShape; }
private int colorCode;
public int getColorCode() { return colorCode; }
private boolean colides;
public boolean doesColide() { return colides; }
private int layerCode;
public int getLayerCode() { return layerCode; }
private boolean beenInitialized = false;
public Block(boolean colides, int layerCode){
this.colides = colides;
this.layerCode = layerCode;
}
public void initialize(){
blockShape = new Rectangle(xPos, yPos, SIZE, SIZE);
int r = (colorCode >> 16) & 0x000000FF;
int g = (colorCode >> 8) & 0x000000FF;
int b = (colorCode) & 0x000000FF;
debugColor = new Color(r, g, b);
BLOCKS.add(this);
beenInitialized = true;
}
public void render(Graphics2D g2d){
if (beenInitialized){
if (DEBUG){
g2d.setColor(debugColor);
if (colides){
g2d.fill(blockShape);
} else {
g2d.draw(blockShape);
}
} else{
}
}
}
}
And finally the Game Class (I threw this together JUST to show a window for testing):
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class Game extends JFrame{
public Game(){
super("Test");
try{
layer1 = ImageIO.read(getClass().getResourceAsStream("/layer1.png"));
layer2 = ImageIO.read(getClass().getResourceAsStream("/layer2.png"));
} catch (Exception ex) { ex.printStackTrace(); }
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setResizable(false);
setLayout(new BorderLayout());
add(new panel(), BorderLayout.CENTER);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
public static void main(String[] args){
new Game();
}
private int[] xStartPositions = {0, 0};
private int[] yStartPositions = {0, 0};
private BufferedImage layer1;
private BufferedImage layer2;
private BufferedImage[] imageArray = {layer1, layer2};
private Map map;
public class panel extends JPanel{
public panel(){
setMinimumSize( new Dimension(1200, 675));
setMaximumSize( new Dimension(1200, 675));
setPreferredSize( new Dimension(1200, 675));
setVisible(true);
map = new Map(imageArray, xStartPositions, yStartPositions);
}
public void paint(Graphics g){
Graphics2D g2d = (Graphics2D) g;
map.render(g2d);
}
}
}
The initialize method of Map is never called, therefore Map will never render...
Some feedback...
Don't ever override paint, use paintComponent instead (it's very rare that you would need to override paint...
Make sure you are calling super.paintXxx - there's a lot of important working going on in the background that you don't want to miss or replicate...
Instead of extending from a top level container like JFrame, start by extending from JPanel and add this to a frame you create instead
Beware of static variables, this might cause you more problems ;)
You may also want to have a read through Initial Threads

Java Slick2d - Loading tiles from spritesheet throwing exception

Block Class
public class Block {
public enum BlockType {
Dirt,
Grass,
Selection
}
BlockType Type;
Vector2f Position;
Image texture;
boolean breakable;
public Block(BlockType Type, Vector2f Position, Image texture, boolean breakable) {
this.Type = Type;
this.Position = Position;
this.texture = texture;
this.breakable = breakable;
}
public BlockType getType() {
return Type;
}
public void setType(BlockType value) {
Type = value;
}
public Vector2f getPosition() {
return Position;
}
public void setPosition(Vector2f value) {
Position = value;
}
public Image gettexture() {
return texture;
}
public void settexture(Image value) {
texture = value;
}
public boolean getbreakable() {
return breakable;
}
public void setbreakable(boolean value) {
breakable = value;
}
}
Tile Generation Class
public class TileGen {
Block block;
public Block[] tiles = new Block[3];
public int width, height;
public int[][] index;
boolean selected;
int mouseX, mouseY;
int tileX, tileY;
Image dirt, grass, selection;
SpriteSheet tileSheet;
public void init() throws SlickException {
tileSheet = new SpriteSheet("assets/tiles/tileSheet.png", 64, 64, new Color(0,0,0));
grass = tileSheet.getSprite(0,0);
dirt = tileSheet.getSprite(64,0);
selection = tileSheet.getSprite(128,0);
tiles[0] = new Block(BlockType.Grass, new Vector2f(tileX,tileY), grass, true);
tiles[1] = new Block(BlockType.Dirt, new Vector2f(tileX,tileY), dirt, true);
width = 50;
height = 50;
index = new int[width][height];
Random rand = new Random();
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
index[x][y] = rand.nextInt(2);
}
}
}
public void update(GameContainer gc) {
Input input = gc.getInput();
mouseX = input.getMouseX();
mouseY = input.getMouseY();
tileX = mouseX / width;
tileY = mouseY / height;
if(input.isMouseButtonDown(Input.MOUSE_LEFT_BUTTON)) {
selected = true;
}
else{
selected = false;
}
System.out.println(tileX);
}
public void render() {
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
tiles[index[x][y]].texture.draw(x * 64, y *64);
if(IsMouseInsideTile(x, y))
selection.draw(x * 64, y * 64);
if(selected && tiles[index[x][y]].breakable) {
if(tiles[index[tileX][tileY]].texture == grass)
tiles[index[tileX][tileY]].texture = dirt;
}
}
}
}
public boolean IsMouseInsideTile(int x, int y)
{
return (mouseX >= x * 64 && mouseX <= (x + 1) * 64 &&
mouseY >= y * 64 && mouseY <= (y + 1) * 64);
}
I am not sure what I am doing wrong, I am new to slick2d. When I try to init my tiles from the spritesheet it throws an exception. The init in my tileGen class is where the problem is.
Exception in thread "main" java.lang.RuntimeException: SubImage out of sheet bounds: 64,0
at org.newdawn.slick.SpriteSheet.getSprite(SpriteSheet.java:208)
at com.synyst3r1.game.TileGen.init(TileGen.java:32)
at com.synyst3r1.game.Game.init(Game.java:23)
at org.newdawn.slick.AppGameContainer.setup(AppGameContainer.java:390)
at org.newdawn.slick.AppGameContainer.start(AppGameContainer.java:314)
at com.synyst3r1.game.Game.main(Game.java:50)
The (x, y) in getSprite() are the cell position, not the pixel position. So, you want getSprite(1, 0) and getSprite(2, 0) (assuming your image is 192x64).

Categories

Resources