mirror of
https://github.com/minetest/minetest.git
synced 2024-11-27 01:53:45 +01:00
Scripting WIP
This commit is contained in:
parent
dcedfdacd1
commit
526eedf98e
@ -163,6 +163,12 @@ function TNT:on_punch(hitter)
|
||||
end
|
||||
|
||||
-- Called when object is right-clicked
|
||||
function TNT:on_rightclick(clicker)
|
||||
pos = self.object:getpos()
|
||||
pos = {x=pos.x, y=pos.y+0.1, z=pos.z}
|
||||
self.object:moveto(pos)
|
||||
end
|
||||
--[[
|
||||
function TNT:on_rightclick(clicker)
|
||||
print("TNT:on_rightclick()")
|
||||
print("self: "..dump(self))
|
||||
@ -173,6 +179,7 @@ function TNT:on_rightclick(clicker)
|
||||
pos = {x=pos.x+0.5+1, y=pos.y+0.5, z=pos.z+0.5}
|
||||
--minetest.env:add_node(pos, 0)
|
||||
end
|
||||
--]]
|
||||
|
||||
print("TNT dump: "..dump(TNT))
|
||||
|
||||
|
@ -20,7 +20,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
#ifndef ACTIVEOBJECT_HEADER
|
||||
#define ACTIVEOBJECT_HEADER
|
||||
|
||||
#include "common_irrlicht.h"
|
||||
#include "irrlichttypes.h"
|
||||
#include <string>
|
||||
|
||||
#define ACTIVEOBJECT_TYPE_INVALID 0
|
||||
|
@ -1276,8 +1276,10 @@ LuaEntityCAO proto_LuaEntityCAO;
|
||||
LuaEntityCAO::LuaEntityCAO():
|
||||
ClientActiveObject(0),
|
||||
m_selection_box(-BS/3.,0.0,-BS/3., BS/3.,BS*2./3.,BS/3.),
|
||||
m_node(NULL),
|
||||
m_meshnode(NULL),
|
||||
m_spritenode(NULL),
|
||||
m_position(v3f(0,10*BS,0)),
|
||||
m_yaw(0),
|
||||
m_prop(new LuaEntityProperties)
|
||||
{
|
||||
ClientActiveObject::registerType(getType(), create);
|
||||
@ -1295,65 +1297,39 @@ ClientActiveObject* LuaEntityCAO::create()
|
||||
|
||||
void LuaEntityCAO::addToScene(scene::ISceneManager *smgr)
|
||||
{
|
||||
if(m_node != NULL)
|
||||
if(m_meshnode != NULL || m_spritenode != NULL)
|
||||
return;
|
||||
|
||||
video::IVideoDriver* driver = smgr->getVideoDriver();
|
||||
|
||||
scene::SMesh *mesh = new scene::SMesh();
|
||||
scene::IMeshBuffer *buf = new scene::SMeshBuffer();
|
||||
video::SColor c(255,255,255,255);
|
||||
video::S3DVertex vertices[4] =
|
||||
{
|
||||
/*video::S3DVertex(-BS/2,-BS/4,0, 0,0,0, c, 0,1),
|
||||
video::S3DVertex(BS/2,-BS/4,0, 0,0,0, c, 1,1),
|
||||
video::S3DVertex(BS/2,BS/4,0, 0,0,0, c, 1,0),
|
||||
video::S3DVertex(-BS/2,BS/4,0, 0,0,0, c, 0,0),*/
|
||||
video::S3DVertex(BS/3.,0,0, 0,0,0, c, 0,1),
|
||||
video::S3DVertex(-BS/3.,0,0, 0,0,0, c, 1,1),
|
||||
video::S3DVertex(-BS/3.,0+BS*2./3.,0, 0,0,0, c, 1,0),
|
||||
video::S3DVertex(BS/3.,0+BS*2./3.,0, 0,0,0, c, 0,0),
|
||||
};
|
||||
u16 indices[] = {0,1,2,2,3,0};
|
||||
buf->append(vertices, 4, indices, 6);
|
||||
// Set material
|
||||
buf->getMaterial().setFlag(video::EMF_LIGHTING, false);
|
||||
buf->getMaterial().setFlag(video::EMF_BACK_FACE_CULLING, false);
|
||||
//buf->getMaterial().setTexture(0, NULL);
|
||||
// Initialize with the stick texture
|
||||
buf->getMaterial().setTexture
|
||||
(0, driver->getTexture(getTexturePath("mese.png").c_str()));
|
||||
buf->getMaterial().setFlag(video::EMF_BILINEAR_FILTER, false);
|
||||
buf->getMaterial().setFlag(video::EMF_FOG_ENABLE, true);
|
||||
buf->getMaterial().MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL;
|
||||
// Add to mesh
|
||||
mesh->addMeshBuffer(buf);
|
||||
buf->drop();
|
||||
m_node = smgr->addMeshSceneNode(mesh, NULL);
|
||||
mesh->drop();
|
||||
// Set it to use the materials of the meshbuffers directly.
|
||||
// This is needed for changing the texture in the future
|
||||
m_node->setReadOnlyMaterials(true);
|
||||
updateNodePos();
|
||||
//video::IVideoDriver* driver = smgr->getVideoDriver();
|
||||
|
||||
if(m_prop->visual == "single_sprite"){
|
||||
} else if(m_prop->visual == "cube"){
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
void LuaEntityCAO::removeFromScene()
|
||||
{
|
||||
if(m_node == NULL)
|
||||
return;
|
||||
|
||||
m_node->remove();
|
||||
m_node = NULL;
|
||||
if(m_meshnode){
|
||||
m_meshnode->remove();
|
||||
m_meshnode = NULL;
|
||||
}
|
||||
if(m_spritenode){
|
||||
m_spritenode->remove();
|
||||
m_spritenode = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void LuaEntityCAO::updateLight(u8 light_at_pos)
|
||||
{
|
||||
if(m_node == NULL)
|
||||
return;
|
||||
|
||||
u8 li = decode_light(light_at_pos);
|
||||
video::SColor color(255,li,li,li);
|
||||
setMeshVerticesColor(m_node->getMesh(), color);
|
||||
if(m_meshnode){
|
||||
setMeshVerticesColor(m_meshnode->getMesh(), color);
|
||||
}
|
||||
if(m_spritenode){
|
||||
m_spritenode->setColor(color);
|
||||
}
|
||||
}
|
||||
|
||||
v3s16 LuaEntityCAO::getLightPosition()
|
||||
@ -1363,25 +1339,17 @@ v3s16 LuaEntityCAO::getLightPosition()
|
||||
|
||||
void LuaEntityCAO::updateNodePos()
|
||||
{
|
||||
if(m_node == NULL)
|
||||
return;
|
||||
|
||||
m_node->setPosition(m_position);
|
||||
if(m_meshnode){
|
||||
m_meshnode->setPosition(pos_translator.vect_show);
|
||||
}
|
||||
if(m_spritenode){
|
||||
m_spritenode->setPosition(pos_translator.vect_show);
|
||||
}
|
||||
}
|
||||
|
||||
void LuaEntityCAO::step(float dtime, ClientEnvironment *env)
|
||||
{
|
||||
if(m_node)
|
||||
{
|
||||
/*v3f rot = m_node->getRotation();
|
||||
rot.Y += dtime * 120;
|
||||
m_node->setRotation(rot);*/
|
||||
LocalPlayer *player = env->getLocalPlayer();
|
||||
assert(player);
|
||||
v3f rot = m_node->getRotation();
|
||||
rot.Y = 180.0 - (player->getYaw());
|
||||
m_node->setRotation(rot);
|
||||
}
|
||||
pos_translator.translate(dtime);
|
||||
}
|
||||
|
||||
void LuaEntityCAO::processMessage(const std::string &data)
|
||||
@ -1392,8 +1360,17 @@ void LuaEntityCAO::processMessage(const std::string &data)
|
||||
u8 cmd = readU8(is);
|
||||
if(cmd == 0)
|
||||
{
|
||||
// do_interpolate
|
||||
bool do_interpolate = readU8(is);
|
||||
// pos
|
||||
m_position = readV3F1000(is);
|
||||
// yaw
|
||||
m_yaw = readF1000(is);
|
||||
|
||||
if(do_interpolate)
|
||||
pos_translator.update(m_position);
|
||||
else
|
||||
pos_translator.init(m_position);
|
||||
updateNodePos();
|
||||
}
|
||||
}
|
||||
@ -1410,11 +1387,15 @@ void LuaEntityCAO::initialize(const std::string &data)
|
||||
return;
|
||||
// pos
|
||||
m_position = readV3F1000(is);
|
||||
// yaw
|
||||
m_yaw = readF1000(is);
|
||||
// properties
|
||||
std::istringstream prop_is(deSerializeLongString(is), std::ios::binary);
|
||||
m_prop->deSerialize(prop_is);
|
||||
|
||||
infostream<<"m_prop: "<<m_prop->dump()<<std::endl;
|
||||
|
||||
pos_translator.init(m_position);
|
||||
|
||||
updateNodePos();
|
||||
}
|
||||
|
@ -410,13 +410,16 @@ public:
|
||||
core::aabbox3d<f32>* getSelectionBox()
|
||||
{return &m_selection_box;}
|
||||
v3f getPosition()
|
||||
{return m_position;}
|
||||
{return pos_translator.vect_show;}
|
||||
|
||||
private:
|
||||
core::aabbox3d<f32> m_selection_box;
|
||||
scene::IMeshSceneNode *m_node;
|
||||
scene::IMeshSceneNode *m_meshnode;
|
||||
scene::MyBillboardSceneNode *m_spritenode;
|
||||
v3f m_position;
|
||||
float m_yaw;
|
||||
struct LuaEntityProperties *m_prop;
|
||||
SmoothTranslator pos_translator;
|
||||
};
|
||||
|
||||
|
||||
|
@ -1506,7 +1506,10 @@ LuaEntitySAO::LuaEntitySAO(ServerEnvironment *env, v3f pos,
|
||||
m_init_name(name),
|
||||
m_init_state(state),
|
||||
m_registered(false),
|
||||
m_prop(new LuaEntityProperties)
|
||||
m_prop(new LuaEntityProperties),
|
||||
m_yaw(0),
|
||||
m_last_sent_yaw(0),
|
||||
m_last_sent_position(0,0,0)
|
||||
{
|
||||
// Only register type if no environment supplied
|
||||
if(env == NULL){
|
||||
@ -1562,6 +1565,13 @@ void LuaEntitySAO::step(float dtime, bool send_recommended)
|
||||
lua_State *L = m_env->getLua();
|
||||
scriptapi_luaentity_step(L, m_id, dtime);
|
||||
}
|
||||
|
||||
if(send_recommended == false)
|
||||
return;
|
||||
if(m_base_position.getDistanceFrom(m_last_sent_position) > 0.05*BS
|
||||
|| fabs(m_yaw - m_last_sent_yaw) > 1.0){
|
||||
sendPosition(true);
|
||||
}
|
||||
}
|
||||
|
||||
std::string LuaEntitySAO::getClientInitializationData()
|
||||
@ -1571,6 +1581,8 @@ std::string LuaEntitySAO::getClientInitializationData()
|
||||
writeU8(os, 0);
|
||||
// pos
|
||||
writeV3F1000(os, m_base_position);
|
||||
// yaw
|
||||
writeF1000(os, m_yaw);
|
||||
// properties
|
||||
std::ostringstream prop_os(std::ios::binary);
|
||||
m_prop->serialize(prop_os);
|
||||
@ -1619,3 +1631,34 @@ void LuaEntitySAO::rightClick(Player *player)
|
||||
scriptapi_luaentity_rightclick_player(L, m_id, player->getName());
|
||||
}
|
||||
|
||||
void LuaEntitySAO::setPos(v3f pos)
|
||||
{
|
||||
m_base_position = pos;
|
||||
sendPosition(false);
|
||||
}
|
||||
|
||||
void LuaEntitySAO::moveTo(v3f pos)
|
||||
{
|
||||
m_base_position = pos;
|
||||
}
|
||||
|
||||
void LuaEntitySAO::sendPosition(bool do_interpolate)
|
||||
{
|
||||
m_last_sent_yaw = m_yaw;
|
||||
m_last_sent_position = m_base_position;
|
||||
|
||||
std::ostringstream os(std::ios::binary);
|
||||
// command (0 = update position)
|
||||
writeU8(os, 0);
|
||||
// do_interpolate
|
||||
writeU8(os, do_interpolate);
|
||||
// pos
|
||||
writeV3F1000(os, m_base_position);
|
||||
// yaw
|
||||
writeF1000(os, m_yaw);
|
||||
// create message and add to list
|
||||
ActiveObjectMessage aom(getId(), false, os.str());
|
||||
m_messages_out.push_back(aom);
|
||||
}
|
||||
|
||||
|
||||
|
@ -215,11 +215,20 @@ public:
|
||||
u16 punch(const std::string &toolname, v3f dir,
|
||||
const std::string &playername);
|
||||
void rightClick(Player *player);
|
||||
|
||||
void setPos(v3f pos);
|
||||
void moveTo(v3f pos);
|
||||
private:
|
||||
void sendPosition(bool do_interpolate);
|
||||
|
||||
std::string m_init_name;
|
||||
std::string m_init_state;
|
||||
bool m_registered;
|
||||
struct LuaEntityProperties *m_prop;
|
||||
|
||||
float m_yaw;
|
||||
float m_last_sent_yaw;
|
||||
v3f m_last_sent_position;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
@ -34,6 +34,7 @@ extern "C" {
|
||||
#include "script.h"
|
||||
//#include "luna.h"
|
||||
#include "luaentity_common.h"
|
||||
#include "content_sao.h" // For LuaEntitySAO
|
||||
|
||||
/*
|
||||
TODO:
|
||||
@ -110,6 +111,29 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
v3f readFloatPos(lua_State *L, int index)
|
||||
{
|
||||
v3f pos;
|
||||
lua_pushvalue(L, index); // Push pos
|
||||
luaL_checktype(L, -1, LUA_TTABLE);
|
||||
lua_getfield(L, -1, "x");
|
||||
pos.X = lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
lua_getfield(L, -1, "y");
|
||||
pos.Y = lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
lua_getfield(L, -1, "z");
|
||||
pos.Z = lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
lua_pop(L, 1); // Pop pos
|
||||
pos *= BS; // Scale to internal format
|
||||
return pos;
|
||||
}
|
||||
|
||||
/*
|
||||
Global functions
|
||||
*/
|
||||
|
||||
// Register new object prototype
|
||||
// register_entity(name, prototype)
|
||||
static int l_register_entity(lua_State *L)
|
||||
@ -149,16 +173,18 @@ static const struct luaL_Reg minetest_f [] = {
|
||||
{NULL, NULL}
|
||||
};
|
||||
|
||||
static int l_entity_set_deleted(lua_State *L)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
/*
|
||||
LuaEntity functions
|
||||
*/
|
||||
|
||||
static const struct luaL_Reg minetest_entity_m [] = {
|
||||
{"set_deleted", l_entity_set_deleted},
|
||||
{NULL, NULL}
|
||||
};
|
||||
|
||||
/*
|
||||
Getters for stuff in main tables
|
||||
*/
|
||||
|
||||
static void objectref_get(lua_State *L, u16 id)
|
||||
{
|
||||
// Get minetest.object_refs[i]
|
||||
@ -324,12 +350,28 @@ private:
|
||||
return *(ObjectRef**)ud; // unbox pointer
|
||||
}
|
||||
|
||||
static ServerActiveObject* getobject(ObjectRef *ref)
|
||||
{
|
||||
ServerActiveObject *co = ref->m_object;
|
||||
return co;
|
||||
}
|
||||
|
||||
static LuaEntitySAO* getluaobject(ObjectRef *ref)
|
||||
{
|
||||
ServerActiveObject *obj = getobject(ref);
|
||||
if(obj == NULL)
|
||||
return NULL;
|
||||
if(obj->getType() != ACTIVEOBJECT_TYPE_LUAENTITY)
|
||||
return NULL;
|
||||
return (LuaEntitySAO*)obj;
|
||||
}
|
||||
|
||||
// Exported functions
|
||||
|
||||
static int l_remove(lua_State *L)
|
||||
{
|
||||
ObjectRef *o = checkobject(L, 1);
|
||||
ServerActiveObject *co = o->m_object;
|
||||
ObjectRef *ref = checkobject(L, 1);
|
||||
ServerActiveObject *co = getobject(ref);
|
||||
if(co == NULL) return 0;
|
||||
infostream<<"ObjectRef::l_remove(): id="<<co->getId()<<std::endl;
|
||||
co->m_removed = true;
|
||||
@ -338,8 +380,8 @@ private:
|
||||
|
||||
static int l_getpos(lua_State *L)
|
||||
{
|
||||
ObjectRef *o = checkobject(L, 1);
|
||||
ServerActiveObject *co = o->m_object;
|
||||
ObjectRef *ref = checkobject(L, 1);
|
||||
ServerActiveObject *co = getobject(ref);
|
||||
if(co == NULL) return 0;
|
||||
infostream<<"ObjectRef::l_getpos(): id="<<co->getId()<<std::endl;
|
||||
v3f pos = co->getBasePosition() / BS;
|
||||
@ -353,6 +395,32 @@ private:
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int l_setpos(lua_State *L)
|
||||
{
|
||||
ObjectRef *ref = checkobject(L, 1);
|
||||
//LuaEntitySAO *co = getluaobject(ref);
|
||||
ServerActiveObject *co = getobject(ref);
|
||||
if(co == NULL) return 0;
|
||||
// pos
|
||||
v3f pos = readFloatPos(L, 2);
|
||||
// Do it
|
||||
co->setPos(pos);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int l_moveto(lua_State *L)
|
||||
{
|
||||
ObjectRef *ref = checkobject(L, 1);
|
||||
//LuaEntitySAO *co = getluaobject(ref);
|
||||
ServerActiveObject *co = getobject(ref);
|
||||
if(co == NULL) return 0;
|
||||
// pos
|
||||
v3f pos = readFloatPos(L, 2);
|
||||
// Do it
|
||||
co->moveTo(pos);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int gc_object(lua_State *L) {
|
||||
//ObjectRef *o = checkobject(L, 1);
|
||||
ObjectRef *o = *(ObjectRef **)(lua_touserdata(L, 1));
|
||||
@ -426,6 +494,8 @@ const char ObjectRef::className[] = "ObjectRef";
|
||||
const luaL_reg ObjectRef::methods[] = {
|
||||
method(ObjectRef, remove),
|
||||
method(ObjectRef, getpos),
|
||||
method(ObjectRef, setpos),
|
||||
method(ObjectRef, moveto),
|
||||
{0,0}
|
||||
};
|
||||
|
||||
@ -438,6 +508,7 @@ void scriptapi_export(lua_State *L, Server *server)
|
||||
realitycheck(L);
|
||||
assert(lua_checkstack(L, 20));
|
||||
infostream<<"scriptapi_export"<<std::endl;
|
||||
StackUnroller stack_unroller(L);
|
||||
|
||||
// Register global functions in table minetest
|
||||
lua_newtable(L);
|
||||
@ -459,14 +530,6 @@ void scriptapi_export(lua_State *L, Server *server)
|
||||
lua_newtable(L);
|
||||
lua_setfield(L, -2, "luaentities");
|
||||
|
||||
// Load and run some base Lua stuff
|
||||
/*script_load(L, (porting::path_data + DIR_DELIM + "scripts"
|
||||
+ DIR_DELIM + "base.lua").c_str());*/
|
||||
|
||||
// Create entity reference metatable
|
||||
//luaL_newmetatable(L, "minetest.entity_reference");
|
||||
//lua_pop(L, 1);
|
||||
|
||||
// Create entity prototype
|
||||
luaL_newmetatable(L, "minetest.entity");
|
||||
// metatable.__index = metatable
|
||||
@ -488,6 +551,7 @@ void scriptapi_add_environment(lua_State *L, ServerEnvironment *env)
|
||||
realitycheck(L);
|
||||
assert(lua_checkstack(L, 20));
|
||||
infostream<<"scriptapi_add_environment"<<std::endl;
|
||||
StackUnroller stack_unroller(L);
|
||||
|
||||
// Create EnvRef on stack
|
||||
EnvRef::create(L, env);
|
||||
@ -498,9 +562,6 @@ void scriptapi_add_environment(lua_State *L, ServerEnvironment *env)
|
||||
luaL_checktype(L, -1, LUA_TTABLE);
|
||||
lua_pushvalue(L, envref);
|
||||
lua_setfield(L, -2, "env");
|
||||
|
||||
// pop minetest and envref
|
||||
lua_pop(L, 2);
|
||||
}
|
||||
|
||||
// Dump stack top with the dump2 function
|
||||
@ -524,6 +585,7 @@ void scriptapi_add_object_reference(lua_State *L, ServerActiveObject *cobj)
|
||||
realitycheck(L);
|
||||
assert(lua_checkstack(L, 20));
|
||||
infostream<<"scriptapi_add_object_reference: id="<<cobj->getId()<<std::endl;
|
||||
StackUnroller stack_unroller(L);
|
||||
|
||||
// Create object on stack
|
||||
ObjectRef::create(L, cobj); // Puts ObjectRef (as userdata) on stack
|
||||
@ -539,9 +601,6 @@ void scriptapi_add_object_reference(lua_State *L, ServerActiveObject *cobj)
|
||||
lua_pushnumber(L, cobj->getId()); // Push id
|
||||
lua_pushvalue(L, object); // Copy object to top of stack
|
||||
lua_settable(L, objectstable);
|
||||
|
||||
// pop object_refs, minetest and the object
|
||||
lua_pop(L, 3);
|
||||
}
|
||||
|
||||
void scriptapi_rm_object_reference(lua_State *L, ServerActiveObject *cobj)
|
||||
@ -549,6 +608,7 @@ void scriptapi_rm_object_reference(lua_State *L, ServerActiveObject *cobj)
|
||||
realitycheck(L);
|
||||
assert(lua_checkstack(L, 20));
|
||||
infostream<<"scriptapi_rm_object_reference: id="<<cobj->getId()<<std::endl;
|
||||
StackUnroller stack_unroller(L);
|
||||
|
||||
// Get minetest.object_refs table
|
||||
lua_getglobal(L, "minetest");
|
||||
@ -567,9 +627,6 @@ void scriptapi_rm_object_reference(lua_State *L, ServerActiveObject *cobj)
|
||||
lua_pushnumber(L, cobj->getId()); // Push id
|
||||
lua_pushnil(L);
|
||||
lua_settable(L, objectstable);
|
||||
|
||||
// pop object_refs, minetest
|
||||
lua_pop(L, 2);
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -20,7 +20,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
#ifndef SERVEROBJECT_HEADER
|
||||
#define SERVEROBJECT_HEADER
|
||||
|
||||
#include "common_irrlicht.h"
|
||||
#include "irrlichttypes.h"
|
||||
#include "activeobject.h"
|
||||
#include "utility.h"
|
||||
|
||||
@ -64,13 +64,16 @@ public:
|
||||
/*
|
||||
Some simple getters/setters
|
||||
*/
|
||||
v3f getBasePosition()
|
||||
{return m_base_position;}
|
||||
void setBasePosition(v3f pos)
|
||||
{m_base_position = pos;}
|
||||
ServerEnvironment* getEnv()
|
||||
{return m_env;}
|
||||
v3f getBasePosition(){ return m_base_position; }
|
||||
void setBasePosition(v3f pos){ m_base_position = pos; }
|
||||
ServerEnvironment* getEnv(){ return m_env; }
|
||||
|
||||
/*
|
||||
Some more dynamic interface
|
||||
*/
|
||||
virtual void setPos(v3f pos){ setBasePosition(pos); }
|
||||
virtual void moveTo(v3f pos){ setBasePosition(pos); }
|
||||
|
||||
/*
|
||||
Step object in time.
|
||||
Messages added to messages are sent to client over network.
|
||||
|
Loading…
Reference in New Issue
Block a user