added an object spawner
This commit is contained in:
parent
20e0c1b3ee
commit
442705c786
|
|
@ -36,6 +36,8 @@ pub const Console = struct {
|
|||
var i: usize = 0;
|
||||
const command = core.stringStrip(c);
|
||||
|
||||
core.console_log("console_eval > {s}", .{c});
|
||||
|
||||
while (i < command.len) : (i += 1) {
|
||||
if (command[i] == ' ') {
|
||||
const func = command[0..i];
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@
|
|||
|
||||
const std = @import("std");
|
||||
|
||||
pub const gameObjectList = @import("utils/gameObjectList.zig");
|
||||
pub const GameObjectList = gameObjectList.GameObjectList;
|
||||
pub const GameObjectInterface = gameObjectList.GameObjectInterface;
|
||||
pub const GameObjectData = gameObjectList.GameObjectData;
|
||||
|
||||
pub const inputs = @import("inputs/inputStack.zig");
|
||||
pub const getInputStack = inputs.getInputStack;
|
||||
pub const ActionEvent = inputs.ActionEvent;
|
||||
|
|
@ -156,9 +161,9 @@ pub fn shutdown_module(_: std.mem.Allocator) void {
|
|||
MemoryTracker.MTPrintStatsDelta();
|
||||
|
||||
logging.shutdownLogging();
|
||||
ecs.shutdown();
|
||||
|
||||
debug_draw.shutdownDrawInterface();
|
||||
|
||||
ecs.shutdown();
|
||||
algorithm.destroyNameRegistry();
|
||||
gEngine.deinit();
|
||||
gPackerFS.destroy();
|
||||
|
|
|
|||
|
|
@ -115,6 +115,16 @@ pub fn createEntity() !Entity {
|
|||
return .{ .handle = try gEcsRegistry.baseSet.createObject(.{}) };
|
||||
}
|
||||
|
||||
pub fn destroyEntity(e: Entity) void {
|
||||
if (gEcsRegistry.baseSet.get(e.handle)) |entityEntry| {
|
||||
for (entityEntry.containers.items) |ref| {
|
||||
ref.vtable.destroyObject(ref.ptr, e.handle);
|
||||
}
|
||||
entityEntry.containers.deinit(gEcsRegistry.allocator);
|
||||
gEcsRegistry.baseSet.destroyObject(e.handle);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn CreateEntity_Lua(state: lua.LuaState) i32 {
|
||||
const ud = state.newZigUserdata(Entity) catch return 0;
|
||||
ud.* = createEntity() catch {
|
||||
|
|
@ -255,6 +265,11 @@ pub const EcsRegistry = struct {
|
|||
for (self.systems.items) |ref| {
|
||||
ref.vtable.destroy(ref.ptr);
|
||||
}
|
||||
|
||||
for (self.baseSet.dense.items) |*entry| {
|
||||
entry.value.containers.deinit(self.allocator);
|
||||
}
|
||||
|
||||
self.systems.deinit(self.allocator);
|
||||
self.tickableSystems.deinit(self.allocator);
|
||||
self.baseSet.deinit();
|
||||
|
|
@ -277,6 +292,7 @@ pub fn defineComponent(comptime Component: type, allocator: std.mem.Allocator) !
|
|||
}
|
||||
|
||||
pub fn undefineComponent(comptime Component: type) void {
|
||||
core.engine_logs("undefining component " ++ @typeName(Component));
|
||||
Component.BaseContainer.destroy();
|
||||
}
|
||||
|
||||
|
|
@ -292,6 +308,10 @@ pub const Entity = struct {
|
|||
},
|
||||
};
|
||||
|
||||
pub fn destroy(self: *@This()) void {
|
||||
destroyEntity(self.*);
|
||||
}
|
||||
|
||||
pub fn fromHandle(handle: core.ObjectHandle) @This() {
|
||||
return .{ .handle = handle };
|
||||
}
|
||||
|
|
@ -299,6 +319,10 @@ pub const Entity = struct {
|
|||
pub fn addComponent(self: @This(), comptime Component: type) ?*Component {
|
||||
const rv = Component.BaseContainer.createWithHandleECS(self.handle);
|
||||
|
||||
const list = &gEcsRegistry.baseSet.get(self.handle).?.containers;
|
||||
const allocator = gEcsRegistry.allocator;
|
||||
list.append(allocator, getTypeContainer(Component)) catch return null;
|
||||
|
||||
// if (@hasDecl(Component, "init")) {
|
||||
// rv.init(self.handle);
|
||||
// }
|
||||
|
|
@ -365,6 +389,7 @@ pub fn getTypeContainer(comptime T: type) EcsContainerRef {
|
|||
|
||||
pub const EcsEntry = struct {
|
||||
containersCount: u32 = 0,
|
||||
containers: std.ArrayListUnmanaged(EcsContainerRef) = .{},
|
||||
};
|
||||
|
||||
pub const BaseSet = p2.SparseSet(EcsEntry);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,131 @@
|
|||
// helper, add this to your game manager class and invoke it's tick,
|
||||
//
|
||||
// will automatically manage spawning and despawning entities and manage de-initialization for you
|
||||
// i actually don't reccomend using this for the most part for more serious games
|
||||
// but it's something that will help a lot when implementing gamejams
|
||||
//
|
||||
// what i would rather reccomend instead is creating components instead with SparseMap base container for most things
|
||||
//
|
||||
// however if you want to define prefabs, then something like this might be useful.
|
||||
//
|
||||
// Like i said, more geared towards gamejams and toys than serious work.
|
||||
|
||||
pub const GameObjectInterfaceVTable = struct {
|
||||
destroy: *const fn (*anyopaque) void,
|
||||
tick: ?*const fn (*anyopaque, f64) void,
|
||||
getEntity: ?*const fn (*anyopaque) core.Entity,
|
||||
};
|
||||
|
||||
pub const GameObjectData = struct {
|
||||
index: u32,
|
||||
list: *GameObjectList,
|
||||
|
||||
pub fn release(self: *@This(), outer: anytype) void {
|
||||
core.engine_log("releases {d}", .{self.list.objects.items.len});
|
||||
if (self.list.objects.items.len > 1) {
|
||||
self.list.objects.items[self.list.objects.items.len - 1].data.index = self.index;
|
||||
}
|
||||
_ = self.list.objects.swapRemove(@intCast(self.index));
|
||||
|
||||
self.list.allocator.destroy(outer);
|
||||
}
|
||||
};
|
||||
|
||||
pub const GameObjectInterface = struct {
|
||||
vtable: *const GameObjectInterfaceVTable,
|
||||
ptr: *anyopaque,
|
||||
data: *GameObjectData,
|
||||
};
|
||||
|
||||
pub const GameObjectList = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
objects: std.ArrayListUnmanaged(GameObjectInterface) = .{},
|
||||
|
||||
pub fn create(allocator: std.mem.Allocator) !*@This() {
|
||||
const self = try allocator.create(@This());
|
||||
self.* = .{ .allocator = allocator };
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
pub const SpawnFunc = *const fn (*anyopaque) ?GameObjectInterface;
|
||||
|
||||
pub fn spawnObjectFunction(comptime T: type) SpawnFunc {
|
||||
const Wrapped = struct {
|
||||
pub fn w_spawn(list_p: *anyopaque) ?GameObjectInterface {
|
||||
const ctx: *GameObjectList = @ptrCast(@alignCast(list_p));
|
||||
|
||||
return ctx.spawnObject(T) catch null;
|
||||
}
|
||||
};
|
||||
|
||||
return Wrapped.w_spawn;
|
||||
}
|
||||
|
||||
pub fn spawnObject(self: *@This(), comptime T: type) !GameObjectInterface {
|
||||
const Wrapped = struct {
|
||||
pub fn w_tick(ptr: *anyopaque, dt: f64) void {
|
||||
const object: *T = @ptrCast(@alignCast(ptr));
|
||||
object.tick(dt);
|
||||
}
|
||||
|
||||
pub fn w_getEntity(ptr: *anyopaque) core.Entity {
|
||||
const object: *T = @ptrCast(@alignCast(ptr));
|
||||
return object.getEntity();
|
||||
}
|
||||
|
||||
pub fn w_destroy(ptr: *anyopaque) void {
|
||||
const object: *T = @ptrCast(@alignCast(ptr));
|
||||
object.destroy();
|
||||
}
|
||||
|
||||
pub const VTable = GameObjectInterfaceVTable{
|
||||
.tick = if (@hasDecl(T, "tick")) w_tick else null,
|
||||
.getEntity = if (@hasDecl(T, "getEntity")) w_getEntity else null,
|
||||
.destroy = w_destroy,
|
||||
};
|
||||
};
|
||||
|
||||
const new = try T.create(self.allocator);
|
||||
new.data = .{
|
||||
.index = @intCast(self.objects.items.len),
|
||||
.list = self,
|
||||
};
|
||||
|
||||
const interface = GameObjectInterface{
|
||||
.vtable = &Wrapped.VTable,
|
||||
.ptr = new,
|
||||
.data = &new.data,
|
||||
};
|
||||
|
||||
try self.objects.append(self.allocator, interface);
|
||||
|
||||
return interface;
|
||||
}
|
||||
|
||||
pub fn newObject(self: *@This(), comptime T: type) !*T {
|
||||
const interface = try self.spawnObject(T);
|
||||
|
||||
return @ptrCast(@alignCast(interface.ptr));
|
||||
}
|
||||
|
||||
pub fn tick(self: *@This(), dt: f64) void {
|
||||
for (self.objects.items) |interface| {
|
||||
if (interface.vtable.tick) |tick_fn| {
|
||||
tick_fn(interface.ptr, dt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn destroy(self: *@This()) void {
|
||||
for (self.objects.items) |interface| {
|
||||
interface.vtable.destroy(interface.ptr);
|
||||
}
|
||||
self.objects.deinit(self.allocator);
|
||||
self.allocator.destroy(self);
|
||||
}
|
||||
};
|
||||
|
||||
const std = @import("std");
|
||||
const core = @import("../core.zig");
|
||||
|
|
@ -23,6 +23,7 @@ test "simple systems setup for core" {
|
|||
|
||||
try test_loadingConfigs();
|
||||
try test_consoleCommands();
|
||||
try test_gameObjects(std.testing.allocator);
|
||||
|
||||
try memory.dumpTimeline("test-core-timeline.txt");
|
||||
}
|
||||
|
|
@ -47,6 +48,8 @@ fn test_loadingConfigs() !void {
|
|||
if (core.getConfigVar([]const u8, "platform.windowName")) |height| {
|
||||
core.engine_log("config string: {s}", .{height});
|
||||
}
|
||||
|
||||
try core.assert(core.configVar(f64, "platform.extent.height", 0) == core.getConfigVar(f64, "platform.extent.height").?);
|
||||
}
|
||||
|
||||
fn test_consoleCommands() !void {
|
||||
|
|
@ -81,3 +84,54 @@ fn test_consoleCommands() !void {
|
|||
core.console.evaluate("echo lmfao");
|
||||
core.console.evaluate("foo lmfao");
|
||||
}
|
||||
|
||||
fn test_gameObjects(allocator: std.mem.Allocator) !void {
|
||||
engine_logs("test: gameobjects... ");
|
||||
|
||||
const objList = try core.GameObjectList.create(allocator);
|
||||
defer objList.destroy();
|
||||
|
||||
const GameObject = struct {
|
||||
data: core.GameObjectData = undefined,
|
||||
entity: core.Entity,
|
||||
|
||||
pub fn create(alloc: std.mem.Allocator) !*@This() {
|
||||
const self = try alloc.create(@This());
|
||||
self.entity = try core.createEntity();
|
||||
_ = self.entity.addComponent(core.Scene);
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn getEntity(self: *@This()) core.Entity {
|
||||
return self.entity;
|
||||
}
|
||||
|
||||
pub fn tick(self: *@This(), dt: f64) void {
|
||||
_ = dt;
|
||||
engine_log("gameObject ticking.... {d} {d}", .{ self.data.index, self.entity.handle.index });
|
||||
}
|
||||
|
||||
pub fn destroy(self: *@This()) void {
|
||||
self.entity.destroy();
|
||||
self.data.release(self);
|
||||
}
|
||||
};
|
||||
|
||||
const gameObject = try objList.newObject(GameObject);
|
||||
|
||||
const gameObject2 = try objList.newObject(GameObject);
|
||||
objList.tick(0.016);
|
||||
gameObject.destroy();
|
||||
|
||||
objList.tick(0.016);
|
||||
gameObject2.destroy();
|
||||
objList.tick(0.016);
|
||||
|
||||
const spawnFunc = core.GameObjectList.spawnObjectFunction(GameObject);
|
||||
const interface = spawnFunc(objList).?;
|
||||
|
||||
core.engine_log("handle = {x}", .{interface.vtable.getEntity.?(interface.ptr).handle.index});
|
||||
|
||||
objList.tick(0.016);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
pub const FpCamera = @import("FpCamera.zig");
|
||||
|
||||
pub const inputDebugger = @import("inputDebugger.zig");
|
||||
|
||||
pub const ObjectSpawner = @import("objectSpawner.zig");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
allocator: std.mem.Allocator,
|
||||
objectList: *core.GameObjectList,
|
||||
windowOpen: bool = true,
|
||||
spawnFuncs: std.ArrayListUnmanaged(SpawnEntry) = .{},
|
||||
spawnPosition: core.Vectorf = .{},
|
||||
|
||||
pub const SpawnEntry = struct {
|
||||
typeName: []const u8,
|
||||
spawnFunc: core.GameObjectList.SpawnFunc,
|
||||
};
|
||||
|
||||
pub fn create(allocator: std.mem.Allocator) !*@This() {
|
||||
const self = try allocator.create(@This());
|
||||
self.* = .{
|
||||
.allocator = allocator,
|
||||
.objectList = try core.GameObjectList.create(allocator),
|
||||
};
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn tick(self: *@This(), dt: f64) void {
|
||||
self.objectList.tick(dt);
|
||||
|
||||
if (self.windowOpen) {
|
||||
if (ig.begin("object Spawner", null, .{})) {
|
||||
for (self.spawnFuncs.items) |entry| {
|
||||
if (ig.smallButton(entry.typeName.ptr)) {
|
||||
if (entry.spawnFunc(self.objectList)) |new| {
|
||||
const entity = new.vtable.getEntity.?(new.ptr);
|
||||
self.prepareEntity(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
ig.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn addSpawnFunction(self: *@This(), comptime name: []const u8, comptime T: type) !void {
|
||||
try self.spawnFuncs.append(self.allocator, .{ .typeName = name, .spawnFunc = core.GameObjectList.spawnObjectFunction(T) });
|
||||
}
|
||||
|
||||
pub fn prepareEntity(self: *@This(), entity: core.Entity) void {
|
||||
if (entity.fetch(core.Scene)) |scene| {
|
||||
scene.setPosition(self.spawnPosition);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn destroy(self: *@This()) void {
|
||||
self.spawnFuncs.deinit(self.allocator);
|
||||
self.objectList.destroy();
|
||||
self.allocator.destroy(self);
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
const backlog = @import("Backlog");
|
||||
const core = backlog.core;
|
||||
const rend = backlog.rend;
|
||||
const ig = backlog.imgui.api;
|
||||
|
|
@ -199,7 +199,7 @@ pub fn SparseMultiSetAdvanced(comptime T: type, comptime SparseSize: u32) type {
|
|||
return self.createObjectInternal(initValue, handle.index, handle.generation);
|
||||
}
|
||||
|
||||
pub fn destroyObject(self: *@This(), handle: SetHandle) bool {
|
||||
pub fn destroyObject(self: *@This(), handle: SetHandle) false {
|
||||
// to destroy an object
|
||||
// get handle and get the dense position, swap and remove.
|
||||
// Then insert the tombstone value into the sparse handle
|
||||
|
|
@ -225,8 +225,6 @@ pub fn SparseMultiSetAdvanced(comptime T: type, comptime SparseSize: u32) type {
|
|||
if (self.containerListener) |l| {
|
||||
l.onHandleRemoved(l.ptr, self.containerID, handle);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
var prng = std.Random.DefaultPrng.init(0x1234);
|
||||
|
|
@ -469,7 +467,7 @@ pub fn SparseSetAdvanced(comptime T: type, comptime SparseSize: u32) type {
|
|||
// Will fail if the handle already exists.
|
||||
pub fn createWithHandle(self: *@This(), handle: SetHandle, initValue: T) !ConstructResult {
|
||||
if (handle.index >= DefaultSparseSize) {
|
||||
std.debug.print("this should never happen handle index is huge: {d}\n", .{handle.index});
|
||||
std.debug.print("this should never happen handle index is huge: {x} {d}\n", .{ handle.index, handle.index });
|
||||
@panic("impossible handle");
|
||||
}
|
||||
var currentDenseHandle = self.sparse[handle.index];
|
||||
|
|
@ -709,6 +707,7 @@ pub const EcsContainerInterface = interface.MakeInterface("EcsContainerInterface
|
|||
handleExists: *const fn (*const anyopaque, SetHandle) bool,
|
||||
get: *const fn (*const anyopaque, SetHandle) ?*anyopaque,
|
||||
createWithHandle: *const fn (*anyopaque, SetHandle) *anyopaque,
|
||||
destroyObject: *const fn (*anyopaque, SetHandle) void,
|
||||
getContainerID: *const fn (*const anyopaque) u32,
|
||||
onRegister: *const fn (*anyopaque, u32, ContainerListener) void,
|
||||
evictFromRegistry: *const fn (*anyopaque) void,
|
||||
|
|
@ -736,6 +735,12 @@ pub const EcsContainerInterface = interface.MakeInterface("EcsContainerInterface
|
|||
return @ptrCast(ptr.get(handle));
|
||||
}
|
||||
|
||||
pub fn destroyObject(p: *anyopaque, handle: SetHandle) void {
|
||||
// std.debug.print("creat with handle {p}\n", .{p});
|
||||
var ptr = @as(*TargetType, @ptrCast(@alignCast(p)));
|
||||
ptr.destroyObject(handle);
|
||||
}
|
||||
|
||||
pub fn createWithHandle(p: *anyopaque, handle: SetHandle) *anyopaque {
|
||||
// std.debug.print("creat with handle {p}\n", .{p});
|
||||
var ptr = @as(*TargetType, @ptrCast(@alignCast(p)));
|
||||
|
|
@ -769,6 +774,7 @@ pub const EcsContainerInterface = interface.MakeInterface("EcsContainerInterface
|
|||
.handleExists = Wrap.handleExists,
|
||||
.get = Wrap.get,
|
||||
.createWithHandle = Wrap.createWithHandle,
|
||||
.destroyObject = Wrap.destroyObject,
|
||||
.getContainerID = Wrap.getContainerID,
|
||||
.onRegister = Wrap.onRegister,
|
||||
.evictFromRegistry = Wrap.evictFromRegistry,
|
||||
|
|
|
|||
|
|
@ -20,8 +20,9 @@ doomplayer: *DoomPlayer = undefined,
|
|||
videoplayerObject: core.Entity = undefined,
|
||||
doomplayerObject: core.Entity = undefined,
|
||||
|
||||
inputEnabled: bool = true,
|
||||
objectSpawner: *extras.ObjectSpawner = undefined,
|
||||
|
||||
inputEnabled: bool = true,
|
||||
videoFullbright: bool = false,
|
||||
|
||||
moveLight: bool = true,
|
||||
|
|
@ -47,16 +48,16 @@ const assetReferences = [_]assets.AssetImportReference{
|
|||
"m_skybox",
|
||||
.{ .path = "meshes/skybox.obj" },
|
||||
),
|
||||
// assets.MakeImportRefOptions(
|
||||
// "Mesh",
|
||||
// "m_fox",
|
||||
// .{ .path = "gltf-samples/Fox/glTF/Fox.gltf" },
|
||||
// ),
|
||||
// assets.MakeImportRefOptions(
|
||||
// "Texture",
|
||||
// "t_fox",
|
||||
// .{ .path = "gltf-samples/Fox/glTF/Texture.png" },
|
||||
// ),
|
||||
assets.MakeImportRefOptions(
|
||||
"Mesh",
|
||||
"m_fox",
|
||||
.{ .path = "gltf-samples/Fox/glTF/Fox.gltf" },
|
||||
),
|
||||
assets.MakeImportRefOptions(
|
||||
"Texture",
|
||||
"t_fox",
|
||||
.{ .path = "gltf-samples/Fox/glTF/Texture.png" },
|
||||
),
|
||||
assets.MakeImportRefOptions(
|
||||
"Texture",
|
||||
"t_empire",
|
||||
|
|
@ -79,6 +80,35 @@ const assetReferences = [_]assets.AssetImportReference{
|
|||
),
|
||||
};
|
||||
|
||||
pub const FoxObject = struct {
|
||||
data: core.GameObjectData = undefined,
|
||||
entity: core.Entity,
|
||||
|
||||
pub fn create(alloc: std.mem.Allocator) !*@This() {
|
||||
const self = try alloc.create(@This());
|
||||
const fox = try core.createEntity();
|
||||
const scene = fox.addComponent(core.Scene).?;
|
||||
|
||||
scene.setScale(0.1, 0.1, 0.1);
|
||||
const mesh = fox.addComponent(rend.MeshComponent).?;
|
||||
mesh.setMesh("m_fox");
|
||||
mesh.setTexture("t_fox");
|
||||
|
||||
self.entity = fox;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn getEntity(self: *@This()) core.Entity {
|
||||
return self.entity;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *@This()) void {
|
||||
self.entity.destroy();
|
||||
self.data.release(self);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn prepare(self: *@This()) !void {
|
||||
core.engine_log(">>>>>>> game prepare", .{});
|
||||
var z = core.tracy.ZoneN(@src(), "PREPARING GAME");
|
||||
|
|
@ -87,6 +117,9 @@ pub fn prepare(self: *@This()) !void {
|
|||
try script.loadTypes("scripts");
|
||||
try script.runScriptFile("scripts/prepare.lua");
|
||||
|
||||
self.objectSpawner = try extras.ObjectSpawner.create(self.allocator);
|
||||
try self.objectSpawner.addSpawnFunction("fox", FoxObject);
|
||||
|
||||
try assets.loadList(assetReferences);
|
||||
self.videoplayer = try VideoPlayer.create(self.allocator);
|
||||
try self.videoplayer.startPlayback("LAPWING2.ogv");
|
||||
|
|
@ -293,6 +326,7 @@ pub fn tick(self: *@This(), dt: f64) void {
|
|||
const fdt: f32 = @floatCast(dt);
|
||||
|
||||
extras.inputDebugger.tick();
|
||||
self.objectSpawner.tick(dt);
|
||||
self.videoplayer.tick(dt);
|
||||
self.doomplayer.tick(dt);
|
||||
self.fpcamera.tick(dt);
|
||||
|
|
@ -312,6 +346,7 @@ pub fn tick(self: *@This(), dt: f64) void {
|
|||
|
||||
const forward = self.fpcamera.getForward();
|
||||
const debugCenter = self.fpcamera.getPosition().add(forward.fmul(self.centerDist));
|
||||
self.objectSpawner.spawnPosition = debugCenter;
|
||||
|
||||
if (self.moveLight) {
|
||||
core.debugSphere(debugCenter, 0.3, .{});
|
||||
|
|
@ -339,6 +374,7 @@ pub fn deinit(self: *@This()) void {
|
|||
self.doomplayer.destroy();
|
||||
self.fpcamera.destroy();
|
||||
|
||||
self.objectSpawner.destroy();
|
||||
self.videoplayer.destroy();
|
||||
self.allocator.destroy(self);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue