config OpenSim.ini additions (or maybe could be separate AgentGate.ini but it's so short)
[AgeGate]
Enabled = true
WelcomeRegionName = G-Welcome ; exact region name to redirect HG into
WelcomeLanding = 128,128,25
BlockLocalChatForUnverified = true ; HG can't speak on channel 0
RedirectOnFail = true ; always redirect HG from M/A
AllowedGBuildersGroup = "G Builders" ; who may build on G - or maybe just use groups system that exists?
; or AllowedGBuildersGroupID = 00000000-0000-0000-0000-000000000000
Then we build a plugin "AgeGateModule.cs" so we don't have to modify OpenSim code base
Fiona.AgentGate.addin.xml
AgentGateModule.cs
// Fiona Sweet free to use/modify for any purpose
// if you alter the code put your name so i don't get the blame :)
// AgeGateModule.cs - OpenSimulator 0.9.3.9333
// Enforces: locals == verified; HG == G-only (auto-redirect to Welcome).
// Blocks local chat (channel 0) for unverified (HG) users.
// Locks G land to staff/allowed group only (best-effort; pair with parcel flags).
using System;
using System.Collections.Generic;
using Nini.Config;
using OpenMetaverse;
using OpenSim.Framework;
using OpenSim.Region.Framework.Interfaces;
using OpenSim.Region.Framework.Scenes;
using OpenSim.Services.Interfaces;
namespace Fiona.AgeGate
{
public class AgeGateModule : ISharedRegionModule
{
const byte MATURITY_GENERAL = 0; // G
const byte MATURITY_MATURE = 1; // M
const byte MATURITY_ADULT = 2; // A
private readonly List _scenes = new List();
// Config
private bool _enabled = true;
private string _welcomeRegionName = "G-Welcome";
private Vector3 _welcomeLanding = new Vector3(128f, 128f, 25f);
private bool _blockLocalChatForUnverified = true; // affects HG only (locals are "verified")
private bool _redirectOnFail = true; // always true per rule (10)
private string _allowedGGroupName = ""; // optional group name allowed to build on G
private UUID _allowedGGroupID = UUID.Zero; // optional explicit group UUID
public string Name => "AgeGateModule";
public Type ReplaceableInterface => null;
public void Initialise(IConfigSource source)
{
IConfig cfg = source.Configs["AgeGate"];
if (cfg == null)
{
// Create a default section so admins see the knobs
cfg = source.AddConfig("AgeGate");
cfg.Set("Enabled", _enabled);
cfg.Set("WelcomeRegionName", _welcomeRegionName);
cfg.Set("WelcomeLanding", "128,128,25");
cfg.Set("BlockLocalChatForUnverified", _blockLocalChatForUnverified);
cfg.Set("RedirectOnFail", _redirectOnFail);
cfg.Set("AllowedGBuildersGroup", _allowedGGroupName);
cfg.Set("AllowedGBuildersGroupID", _allowedGGroupID.ToString());
}
_enabled = cfg.GetBoolean("Enabled", true);
_welcomeRegionName = cfg.GetString("WelcomeRegionName", _welcomeRegionName);
_blockLocalChatForUnverified = cfg.GetBoolean("BlockLocalChatForUnverified", true);
_redirectOnFail = cfg.GetBoolean("RedirectOnFail", true);
_allowedGGroupName = cfg.GetString("AllowedGBuildersGroup", _allowedGGroupName);
UUID parsed;
if (UUID.TryParse(cfg.GetString("AllowedGBuildersGroupID", UUID.Zero.ToString()), out parsed))
_allowedGGroupID = parsed;
// landing vector parsing "x,y,z"
var v = cfg.GetString("WelcomeLanding", "128,128,25").Split(',');
if (v.Length == 3)
{
float x, y, z;
if (float.TryParse(v[0], out x) && float.TryParse(v[1], out y) && float.TryParse(v[2], out z))
_welcomeLanding = new Vector3(x, y, z);
}
}
public void PostInitialise() { }
public void Close() { _scenes.Clear(); }
public void AddRegion(Scene scene)
{
if (!_enabled) return;
_scenes.Add(scene);
scene.EventManager.OnNewClient += OnNewClient;
scene.EventManager.OnClientClosed += OnClientClosed;
scene.EventManager.OnSignificantClientMovement += OnMove;
scene.EventManager.OnObjectBeingAddedToScene += OnObjectBeingAddedToScene;
scene.EventManager.OnLandObjectUpdated += OnLandObjectUpdated;
}
public void RemoveRegion(Scene scene)
{
if (!_enabled) return;
scene.EventManager.OnNewClient -= OnNewClient;
scene.EventManager.OnClientClosed -= OnClientClosed;
scene.EventManager.OnSignificantClientMovement -= OnMove;
scene.EventManager.OnObjectBeingAddedToScene -= OnObjectBeingAddedToScene;
scene.EventManager.OnLandObjectUpdated -= OnLandObjectUpdated;
_scenes.Remove(scene);
}
public void RegionLoaded(Scene scene) { }
// -------------------- Client lifecycle --------------------
private void OnNewClient(IClientAPI client)
{
// Cache "verified" flag per session:
bool verified = IsVerifiedLocal(client);
SetVerifiedFlag(client, verified);
// Gate teleports up-front as well:
client.OnTeleportLocationRequest += OnTeleportLocationRequest;
// Block local chat for unverified (HG) if configured
if (_blockLocalChatForUnverified)
client.OnChatFromClient += OnChatFromClient;
}
private void OnClientClosed(UUID agentID, Scene scene) { /* nothing */ }
// -------------------- Core policy helpers --------------------
private static void SetVerifiedFlag(IClientAPI client, bool isVerified)
{
// stash in the SceneAgent service URLs map as a simple cache
try
{
if (client?.SceneAgent != null)
{
client.SceneAgent.ServiceURLs["AgeVerified"] = isVerified ? "1" : "0";
}
}
catch { /* ignore */ }
}
private static bool GetVerifiedFlag(IClientAPI client)
{
try
{
string v;
if (client?.SceneAgent != null &&
client.SceneAgent.ServiceURLs.TryGetValue("AgeVerified", out v))
return v == "1";
}
catch { }
return false;
}
private bool IsVerifiedLocal(IClientAPI client)
{
// Rule (9): all locals are considered verified; HG visitors are not.
bool isForeign = IsForeignUser(client);
return !isForeign;
}
private bool IsForeignUser(IClientAPI client)
{
try
{
var um = (client.Scene as Scene)?.RequestModuleInterface();
if (um != null)
return !um.IsLocalGridUser(client.AgentId);
}
catch { }
// Be conservative: if we can't tell, treat as foreign
return true;
}
// -------------------- Movement / TP gating --------------------
private void OnMove(ScenePresence sp)
{
if (sp == null || sp.IsChildAgent || sp.Scene == null) return;
ILandObject land;
if (!TryGetParcel(sp.Scene, sp.AbsolutePosition, out land)) return;
GateAccess(sp.Scene, sp.ControllingClient, land);
}
private void OnTeleportLocationRequest(IClientAPI client, ulong regionHandle, Vector3 position, Vector3 lookAt, uint flags)
{
// Try to pre-gate based on destination maturity if we can resolve the parcel/region.
var scene = client.Scene as Scene;
if (scene == null) return;
ILandObject destLand;
if (TryResolveDestinationParcel(scene, regionHandle, position, out destLand))
{
if (GateAccess(scene, client, destLand))
return; // we redirected; stop normal TP
}
// else: allow TP and we'll catch it on arrival via OnMove
}
// Returns true if it handled (redirected / blocked)
private bool GateAccess(Scene scene, IClientAPI client, ILandObject land)
{
if (land == null) return false;
byte maturity = land.LandData.Maturity;
bool isForeign = IsForeignUser(client); // HG => unverified
bool verified = GetVerifiedFlag(client); // locals => true
if (maturity > MATURITY_GENERAL && (!verified || isForeign))
{
// Rule (10): redirect to G Welcome
client.SendAgentAlertMessage("This area requires local (age-verified) access. Redirecting to the Welcome region.", false);
if (_redirectOnFail)
{
TeleportToWelcome(scene, client);
return true;
}
client.SendTeleportFailed("Destination requires local (age-verified) access.");
return true;
}
return false;
}
// -------------------- G-land UGC lock --------------------
private void OnObjectBeingAddedToScene(SceneObjectGroup sog)
{
try
{
if (sog == null) return;
var scene = sog.Scene;
var owner = sog.OwnerID;
ILandObject land;
if (!TryGetParcel(scene, sog.AbsolutePosition, out land)) return;
if (land.LandData.Maturity == MATURITY_GENERAL)
{
// Allow only staff / allowed group to build in G parcels
var sp = scene.GetScenePresence(owner);
var client = sp?.ControllingClient;
if (client == null) { sog.DeleteGroup(false); return; }
if (!IsAllowedGBuild(client, land))
{
client.SendAgentAlertMessage("Building is disabled in G areas (staff/allowed group only).", false);
sog.DeleteGroup(false);
}
}
}
catch { }
}
private void OnLandObjectUpdated(uint localID, ILandObject land, IClientAPI who, LandUpdateArgs args)
{
// Prevent users from setting parcels to G unless staff
try
{
if (args == null || land == null || who == null) return;
if (args.Maturity == MATURITY_GENERAL && !IsStaff(who))
{
who.SendAlertMessage("Only staff may set parcels to General (G) on this grid.");
// Best-effort revert to previous maturity if we can
if (land.LandData.Maturity != MATURITY_GENERAL)
args.Maturity = land.LandData.Maturity;
else
args.Maturity = MATURITY_MATURE;
}
}
catch { }
}
private bool IsAllowedGBuild(IClientAPI client, ILandObject land)
{
if (IsStaff(client)) return true;
// Optional group check
if (_allowedGGroupID != UUID.Zero)
{
var groups = (client.Scene as Scene)?.RequestModuleInterface();
if (groups != null && groups.IsGroupMember(client.AgentId, _allowedGGroupID))
return true;
}
else if (!string.IsNullOrEmpty(_allowedGGroupName))
{
var groups = (client.Scene as Scene)?.RequestModuleInterface();
if (groups != null)
{
GroupRecord rec;
if (groups.TryGetGroupRecord(_allowedGGroupName, out rec) && rec != null && rec.GroupID != UUID.Zero)
{
if (groups.IsGroupMember(client.AgentId, rec.GroupID))
return true;
}
}
}
// Encourage also setting parcel flags to disable build; we enforce here as well.
return false;
}
private bool IsStaff(IClientAPI client)
{
try
{
var scene = client.Scene as Scene;
if (scene == null) return false;
var est = scene.RegionInfo.EstateSettings;
if (client.AgentId == est.EstateOwner) return true;
foreach (UUID m in est.EstateManagers)
if (m == client.AgentId) return true;
// God mode check
return scene.Permissions.IsGod(client.AgentId);
}
catch { }
return false;
}
// -------------------- Utilities --------------------
private static bool TryGetParcel(Scene scene, Vector3 pos, out ILandObject land)
{
land = null;
try
{
if (scene == null || scene.LandChannel == null) return false;
land = scene.LandChannel.GetLandObject(pos.X, pos.Y);
return land != null;
}
catch { return false; }
}
// Try to resolve destination parcel from region handle + position (pre-TP gate)
private bool TryResolveDestinationParcel(Scene currentScene, ulong regionHandle, Vector3 pos, out ILandObject land)
{
land = null;
try
{
// If the TP is within the same region, just read local land
if (regionHandle == currentScene.RegionInfo.RegionHandle)
return TryGetParcel(currentScene, pos, out land);
// If cross-region: ask GridService, then defer to OnMove after arrival
return false;
}
catch { return false; }
}
private void TeleportToWelcome(Scene scene, IClientAPI client)
{
var grid = scene.GridService;
if (grid == null)
{
client.SendAgentAlertMessage("Welcome region unavailable (no GridService).", false);
return;
}
var regs = grid.GetRegionsByName(scene.RegionInfo.ScopeID, _welcomeRegionName, 1);
if (regs == null || regs.Count == 0)
{
client.SendAgentAlertMessage($"Welcome region '{_welcomeRegionName}' not found.", false);
return;
}
var wel = regs[0];
try
{
scene.RequestTeleportLocation(client, wel.RegionHandle, _welcomeLanding, Vector3.Zero, (uint)TeleportFlags.ViaRegionID);
}
catch
{
client.SendAgentAlertMessage("Teleport to Welcome region failed.", false);
}
}
private void OnChatFromClient(IClientAPI client, OSChatMessage msg)
{
try
{
if (msg.Channel == 0)
{
bool verified = GetVerifiedFlag(client); // locals => true
if (!verified)
{
msg.Handled = true;
client.SendAgentAlertMessage("Local chat is available to local (age-verified) users only.", false);
}
}
}
catch { }
}
}
// Minimal IGrou​psModule helpers to avoid compile surprises across minor versions
public static class GroupsModuleExtensions
{
public static bool IsGroupMember(this IGroupsModule gm, UUID agent, UUID group)
{
try { return gm?.IsAgentMemberOfGroup(agent, group) ?? false; }
catch { return false; }
}
public static bool TryGetGroupRecord(this IGroupsModule gm, string name, out GroupRecord rec)
{
rec = null;
try { rec = gm.GetGroupRecord(name); return rec != null; }
catch { return false; }
}
}
}